Skip to content

Commit

Permalink
Merge branch 'main' into fix/clone
Browse files Browse the repository at this point in the history
  • Loading branch information
jdkent authored Jun 13, 2024
2 parents 10b8fd9 + a930303 commit 185c88c
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 51 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ You can install them as follows:

```shell
$ pip install connexion[swagger-ui]
$ pip install connexion[swagger-ui,uvicorn].
$ pip install connexion[swagger-ui,uvicorn]
```

<p align="right">(<a href="#top">back to top</a>)</p>
Expand Down Expand Up @@ -280,4 +280,4 @@ Tools to help you work spec-first:
[Pycharm plugin]: https://plugins.jetbrains.com/plugin/14837-openapi-swagger-editor
[examples]: https://github.com/spec-first/connexion/blob/main/examples
[Releases]: https://github.com/spec-first/connexion/releases
[Architecture]: https://github.com/spec-first/connexion/blob/main/docs/images/architecture.png
[Architecture]: https://github.com/spec-first/connexion/blob/main/docs/images/architecture.png
3 changes: 2 additions & 1 deletion connexion/decorators/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ def _unpack_handler_response(
elif len(handler_response) == 2:
data, status_code_or_headers = handler_response
if isinstance(status_code_or_headers, int):
status_code = status_code_or_headers
# Extra int call because of int subclasses such as http.HTTPStatus (IntEnum)
status_code = int(status_code_or_headers)
elif isinstance(status_code_or_headers, Enum) and isinstance(
status_code_or_headers.value, int
):
Expand Down
14 changes: 12 additions & 2 deletions connexion/middleware/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ def add_exception_handler(
@staticmethod
def problem_handler(_request: ConnexionRequest, exc: ProblemException):
"""Default handler for Connexion ProblemExceptions"""
logger.error("%r", exc)

if 400 <= exc.status <= 499:
logger.warning("%r", exc)
else:
logger.error("%r", exc)

return exc.to_problem()

@staticmethod
Expand All @@ -81,7 +86,12 @@ def http_exception(
_request: StarletteRequest, exc: HTTPException, **kwargs
) -> StarletteResponse:
"""Default handler for Starlette HTTPException"""
logger.error("%r", exc)

if 400 <= exc.status_code <= 499:
logger.warning("%r", exc)
else:
logger.error("%r", exc)

return problem(
title=http_facts.HTTP_STATUS_CODES.get(exc.status_code),
detail=exc.detail,
Expand Down
7 changes: 4 additions & 3 deletions connexion/validators/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def _validate(self, body: t.Any) -> t.Optional[dict]:
return self._validator.validate(body)
except ValidationError as exception:
error_path_msg = format_error_with_path(exception=exception)
logger.error(
logger.info(
f"Validation error: {exception.message}{error_path_msg}",
extra={"validator": "body"},
)
Expand All @@ -77,7 +77,8 @@ def _validate(self, body: t.Any) -> t.Optional[dict]:

class DefaultsJSONRequestBodyValidator(JSONRequestBodyValidator):
"""Request body validator for json content types which fills in default values. This Validator
intercepts the body, makes changes to it, and replays it for the next ASGI application."""
intercepts the body, makes changes to it, and replays it for the next ASGI application.
"""

MUTABLE_VALIDATION = True
"""This validator might mutate to the body."""
Expand Down Expand Up @@ -129,7 +130,7 @@ def _validate(self, body: dict):
self.validator.validate(body)
except ValidationError as exception:
error_path_msg = format_error_with_path(exception=exception)
logger.error(
logger.warning(
f"Validation error: {exception.message}{error_path_msg}",
extra={"validator": "body"},
)
Expand Down
14 changes: 4 additions & 10 deletions connexion/validators/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import logging

from jsonschema import Draft4Validator, ValidationError
from starlette.requests import Request

from connexion.exceptions import BadRequestProblem, ExtraParameterProblem
from connexion.lifecycle import ConnexionRequest
from connexion.utils import boolean, is_null, is_nullable

logger = logging.getLogger("connexion.validators.parameter")
Expand Down Expand Up @@ -82,17 +82,11 @@ def validate_query_parameter(self, param, request):
:type param: dict
:rtype: str
"""
# Convert to dict of lists
query_params = {
k: request.query_params.getlist(k) for k in request.query_params
}
query_params = self.uri_parser.resolve_query(query_params)
val = query_params.get(param["name"])
val = request.query_params.get(param["name"])
return self.validate_parameter("query", val, param)

def validate_path_parameter(self, param, request):
path_params = self.uri_parser.resolve_path(request.path_params)
val = path_params.get(param["name"].replace("-", "_"))
val = request.path_params.get(param["name"].replace("-", "_"))
return self.validate_parameter("path", val, param)

def validate_header_parameter(self, param, request):
Expand All @@ -106,7 +100,7 @@ def validate_cookie_parameter(self, param, request):
def validate(self, scope):
logger.debug("%s validating parameters...", scope.get("path"))

request = Request(scope)
request = ConnexionRequest(scope, uri_parser=self.uri_parser)
self.validate_request(request)

def validate_request(self, request):
Expand Down
1 change: 1 addition & 0 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ register an API defined by an OpenAPI (or Swagger) specification.
operationId: run.post_greeting
responses:
200:
description: "Greeting response"
content:
text/plain:
schema:
Expand Down
2 changes: 1 addition & 1 deletion docs/response.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Response Serialization
def endpoint():
data = "success"
status_code = 200
headers = {"Content-Type": "text/plain}
headers = {"Content-Type": "text/plain"}
return data, status_code, headers
Data
Expand Down
7 changes: 7 additions & 0 deletions tests/api/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ def test_pass_through(simple_app):
)


def test_can_use_httpstatus_enum(simple_openapi_app):
app_client = simple_openapi_app.test_client()

response = app_client.get("/v1.0/httpstatus")
assert response.status_code == 201


def test_empty(simple_app):
app_client = simple_app.test_client()

Expand Down
57 changes: 57 additions & 0 deletions tests/decorators/test_uri_parsing.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from urllib.parse import quote_plus

import pytest
from connexion.uri_parsing import (
AlwaysMultiURIParser,
FirstValueURIParser,
OpenAPIURIParser,
Swagger2URIParser,
)
from starlette.datastructures import QueryParams
from werkzeug.datastructures import MultiDict

QUERY1 = MultiDict([("letters", "a"), ("letters", "b,c"), ("letters", "d,e,f")])
Expand Down Expand Up @@ -262,3 +265,57 @@ class Request:
parser = parser_class(parameters, body_defn)
res = parser.resolve_query(request.query.to_dict(flat=False))
assert res == expected


def test_parameter_coercion():
params = [
{"name": "p1", "in": "path", "type": "integer", "required": True},
{"name": "h1", "in": "header", "type": "string", "enum": ["a", "b"]},
{"name": "q1", "in": "query", "type": "integer", "maximum": 3},
{
"name": "a1",
"in": "query",
"type": "array",
"minItems": 2,
"maxItems": 3,
"items": {"type": "integer", "minimum": 0},
},
]

uri_parser = Swagger2URIParser(params, {})

parsed_param = uri_parser.resolve_path({"p1": "123"})
assert parsed_param == {"p1": 123}

parsed_param = uri_parser.resolve_path({"p1": ""})
assert parsed_param == {"p1": ""}

parsed_param = uri_parser.resolve_path({"p1": "foo"})
assert parsed_param == {"p1": "foo"}

parsed_param = uri_parser.resolve_path({"p1": "1.2"})
assert parsed_param == {"p1": "1.2"}

parsed_param = uri_parser.resolve_path({"p1": 1})
assert parsed_param == {"p1": 1}

parsed_param = uri_parser.resolve_query(QueryParams("q1=4"))
assert parsed_param == {"q1": 4}

parsed_param = uri_parser.resolve_query(QueryParams("q1=3"))
assert parsed_param == {"q1": 3}

parsed_param = uri_parser.resolve_query(QueryParams(f"a1={quote_plus('1,2')}"))
assert parsed_param == {"a1": [2]} # Swagger2URIParser

parsed_param = uri_parser.resolve_query(QueryParams(f"a1={quote_plus('1,a')}"))
assert parsed_param == {"a1": ["a"]} # Swagger2URIParser

parsed_param = uri_parser.resolve_query(QueryParams(f"a1={quote_plus('1,-1')}"))
assert parsed_param == {"a1": [1]} # Swagger2URIParser

parsed_param = uri_parser.resolve_query(QueryParams(f"a1=1"))
assert parsed_param == {"a1": [1]} # Swagger2URIParser

parsed_param = uri_parser.resolve_query(QueryParams(f"a1={quote_plus('1,2,3,4')}"))
assert parsed_param == {"a1": [4]} # Swagger2URIParser
5 changes: 5 additions & 0 deletions tests/fakeapi/hello/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import datetime
import uuid
from http import HTTPStatus

import flask
from connexion import NoContent, ProblemException, context, request
Expand Down Expand Up @@ -737,3 +738,7 @@ def get_streaming_response():

async def async_route():
return {}, 200


def httpstatus():
return {}, HTTPStatus.CREATED
18 changes: 17 additions & 1 deletion tests/fixtures/simple/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1316,7 +1316,23 @@ paths:
responses:
200:
description: 'OK'

/httpstatus:
get:
operationId: fakeapi.hello.httpstatus
responses:
201:
description: "happy path"
default:
description: "default"
content:
application/json:
schema:
type: object
properties:
error_code:
type: integer
required:
- error_code

servers:
- url: http://localhost:{port}/{basePath}
Expand Down
Loading

0 comments on commit 185c88c

Please sign in to comment.