Skip to content

Commit

Permalink
Fix click typing and added test for bad responses
Browse files Browse the repository at this point in the history
  • Loading branch information
tarsil committed Jul 12, 2023
1 parent f9b2168 commit 5a4ebac
Show file tree
Hide file tree
Showing 11 changed files with 79 additions and 13 deletions.
2 changes: 1 addition & 1 deletion esmerald/core/directives/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def invoke(self, ctx: click.Context) -> typing.Any:
return super().invoke(ctx)


@click.group(
@click.group( # type: ignore
cls=DirectiveGroup,
)
@click.option(
Expand Down
2 changes: 1 addition & 1 deletion esmerald/core/directives/operations/createapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
printer = Print()


@click.option("-v", "--verbosity", default=1, type=int, help="Displays the files generated")
@click.option("-v", "--verbosity", default=1, type=int, help="Displays the files generated") # type: ignore
@click.argument("name", type=str)
@click.command(name="createapp")
def create_app(name: str, verbosity: int) -> None:
Expand Down
2 changes: 1 addition & 1 deletion esmerald/core/directives/operations/createproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
printer = Print()


@click.option("-v", "--verbosity", default=1, type=int, help="Displays the files generated")
@click.option("-v", "--verbosity", default=1, type=int, help="Displays the files generated") # type: ignore
@click.argument("name", type=str)
@click.command(name="createproject")
def create_project(name: str, verbosity: int) -> None:
Expand Down
2 changes: 1 addition & 1 deletion esmerald/core/directives/operations/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Position(int, Enum):
BACK = 3


@click.option(
@click.option( # type: ignore
"--directive",
"directive",
required=True,
Expand Down
2 changes: 1 addition & 1 deletion esmerald/core/directives/operations/runserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
terminal = Terminal()


@click.option(
@click.option( # type: ignore
"-p",
"--port",
type=int,
Expand Down
2 changes: 1 addition & 1 deletion esmerald/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class MissingDependency(EsmeraldAPIException, ImportError):
...


class OpenAPIError(ValueError):
class OpenAPIException(ImproperlyConfigured):
...


Expand Down
7 changes: 1 addition & 6 deletions esmerald/openapi/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from typing_extensions import Literal

from esmerald.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.models import Contact, Info, License, OpenAPI, Operation, Parameter, Tag
from esmerald.openapi.responses import create_internal_response
from esmerald.openapi.utils import (
Expand Down Expand Up @@ -264,10 +263,6 @@ def get_openapi_path(

openapi_response = operation_responses.setdefault(status_code_key, {})

assert isinstance(
process_response, OpenAPIResponse
), "An additional response must be an instance of OpenAPIResponse"

field = handler.responses.get(additional_status_code)
additional_field_schema: Optional[Dict[str, Any]] = None
model_schema = process_response.model.model_json_schema()
Expand Down Expand Up @@ -424,7 +419,7 @@ def iterate_routes(
definitions, components = iterate_routes(
route.routes, definitions, components, prefix=route_path
)
continue
continue

if isinstance(route, gateways.Gateway):
result = get_openapi_path(
Expand Down
7 changes: 6 additions & 1 deletion esmerald/openapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ def is_status_code_allowed(status_code: Union[int, str, None]) -> bool:
return True
if status_code in ALLOWED_STATUS_CODE:
return True
current_status_code = int(status_code)

try:
current_status_code = int(status_code)
except ValueError:
return False

return not (current_status_code < 200 or current_status_code in {204, 304})


Expand Down
18 changes: 18 additions & 0 deletions esmerald/routing/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@
ImproperlyConfigured,
MethodNotAllowed,
NotFound,
OpenAPIException,
ValidationErrorException,
)
from esmerald.interceptors.types import Interceptor
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.utils import is_status_code_allowed
from esmerald.params import Body
from esmerald.requests import Request
from esmerald.responses import Response
Expand Down Expand Up @@ -557,6 +559,22 @@ def __init__(
self.route_map: Dict[str, Tuple["HTTPHandler", "TransformerModel"]] = {}
self.path_regex, self.path_format, self.param_convertors = compile_path(path)

if self.responses:
self.validate_responses(responses=self.responses)

def validate_responses(self, responses: Dict[int, OpenAPIResponse]) -> None:
"""
Checks if the responses are valid or raises an exception otherwise.
"""
for status_code, response in responses.items():
if not isinstance(response, OpenAPIResponse):
raise OpenAPIException(
detail="An additional response must be an instance of OpenAPIResponse."
)

if not is_status_code_allowed(status_code):
raise OpenAPIException(detail="The status is not a valid OpenAPI status response.")

@property
def http_methods(self) -> List[str]:
"""
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies = [
"aiofiles>=0.8.0,<24",
"anyio>=3.6.2,<4.0.0",
"awesome-slugify>=1.6.5,<2",
"click>=8.1.4,<9.0.0",
"httpx>=0.24.0,<0.30.0",
"itsdangerous>=2.1.2,<3.0.0",
"jinja2>=3.1.2,<4.0.0",
Expand Down
47 changes: 47 additions & 0 deletions tests/openapi/test_bad_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Dict, Union

import pytest
from pydantic import BaseModel

from esmerald import Gateway, get
from esmerald.exceptions import OpenAPIException
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.testclient import create_client


class Item(BaseModel):
sku: Union[int, str]


async def test_invalid_response(test_client_factory):
with pytest.raises(OpenAPIException) as raised:

@get("/test", responses={"hello": {"description": "Not a valid response"}})
def read_people() -> Dict[str, str]:
return {"id": "foo"}

with create_client(
routes=[
Gateway(handler=read_people),
]
) as client:
client.get("/openapi.json")

assert raised.value.detail == "An additional response must be an instance of OpenAPIResponse."


async def test_invalid_response_status(test_client_factory):
with pytest.raises(OpenAPIException) as raised:

@get("/test", responses={"hello": OpenAPIResponse(model=Item)})
def read_people() -> Dict[str, str]:
return {"id": "foo"}

with create_client(
routes=[
Gateway(handler=read_people),
]
) as client:
client.get("/openapi.json")

assert raised.value.detail == "The status is not a valid OpenAPI status response."

0 comments on commit 5a4ebac

Please sign in to comment.