From 729c354d4edaef84687bbce207c51d1b4b475629 Mon Sep 17 00:00:00 2001 From: Ben Gruber Date: Fri, 26 Aug 2022 11:57:00 +0000 Subject: [PATCH 1/2] Resolve circular import for Operation and PathItem --- .../openapi_schema_pydantic/operation.py | 7 ++++++ .../openapi_schema_pydantic/path_item.py | 23 +++++++++++-------- tests/test_schema/test_open_api.py | 20 ++++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/openapi_python_client/schema/openapi_schema_pydantic/operation.py b/openapi_python_client/schema/openapi_schema_pydantic/operation.py index 19c78c6d5..51d84e734 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/operation.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/operation.py @@ -5,6 +5,9 @@ from .callback import Callback from .external_documentation import ExternalDocumentation from .parameter import Parameter + +# Required to update forward ref after object creation, as this is not imported yet +from .path_item import PathItem # pylint: disable=unused-import from .reference import Reference from .request_body import RequestBody from .responses import Responses @@ -79,3 +82,7 @@ class Config: # pylint: disable=missing-class-docstring } ] } + + +# PathItem in Callback uses Operation, so we need to update forward refs due to circular dependency +Operation.update_forward_refs() diff --git a/openapi_python_client/schema/openapi_schema_pydantic/path_item.py b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py index af1a1a6a3..9fc51eb85 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/path_item.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py @@ -2,7 +2,6 @@ from pydantic import BaseModel, Extra, Field -from .operation import Operation from .parameter import Parameter from .reference import Reference from .server import Server @@ -23,14 +22,14 @@ class PathItem(BaseModel): ref: Optional[str] = Field(default=None, alias="$ref") summary: Optional[str] = None description: Optional[str] = None - get: Optional[Operation] = None - put: Optional[Operation] = None - post: Optional[Operation] = None - delete: Optional[Operation] = None - options: Optional[Operation] = None - head: Optional[Operation] = None - patch: Optional[Operation] = None - trace: Optional[Operation] = None + get: Optional["Operation"] = None + put: Optional["Operation"] = None + post: Optional["Operation"] = None + delete: Optional["Operation"] = None + options: Optional["Operation"] = None + head: Optional["Operation"] = None + patch: Optional["Operation"] = None + trace: Optional["Operation"] = None servers: Optional[List[Server]] = None parameters: Optional[List[Union[Parameter, Reference]]] = None @@ -70,3 +69,9 @@ class Config: # pylint: disable=missing-class-docstring } ] } + + +# Operation uses PathItem via Callback, so we need late import and to update forward refs due to circular dependency +from .operation import Operation # pylint: disable=wrong-import-position unused-import + +PathItem.update_forward_refs() diff --git a/tests/test_schema/test_open_api.py b/tests/test_schema/test_open_api.py index e332e4fca..7e9b8be9d 100644 --- a/tests/test_schema/test_open_api.py +++ b/tests/test_schema/test_open_api.py @@ -14,3 +14,23 @@ def test_validate_version(version, valid): else: with pytest.raises(ValidationError): OpenAPI.parse_obj(data) + + +def test_parse_with_callback(): + data = { + "openapi": "3.0.1", + "info": {"title": "API with Callback", "version": ""}, + "paths": { + "/create": { + "post": { + "responses": {"200": {"description": "Success"}}, + "callbacks": {"event": {"callback": {"post": {"responses": {"200": {"description": "Success"}}}}}}, + } + } + }, + } + + open_api = OpenAPI.parse_obj(data) + create_endpoint = open_api.paths["/create"] + assert "200" in create_endpoint.post.responses + assert "200" in create_endpoint.post.callbacks["event"]["callback"].post.responses From b27aa1f23316f609e2d310c34ffda6c60509c16d Mon Sep 17 00:00:00 2001 From: Ben Gruber Date: Sat, 27 Aug 2022 21:09:34 +0000 Subject: [PATCH 2/2] Add e2e test for callback parsing --- .../my_test_api_client/api/tests/__init__.py | 8 + .../api/tests/callback_test.py | 152 ++++++++++++++++++ end_to_end_tests/openapi.json | 56 +++++++ 3 files changed, 216 insertions(+) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py index 8924dfa7a..13120943a 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py @@ -3,6 +3,7 @@ import types from . import ( + callback_test, defaults_tests_defaults_post, get_basic_list_of_booleans, get_basic_list_of_floats, @@ -150,3 +151,10 @@ def token_with_cookie_auth_token_with_cookie_get(cls) -> types.ModuleType: Test optional cookie parameters """ return token_with_cookie_auth_token_with_cookie_get + + @classmethod + def callback_test(cls) -> types.ModuleType: + """ + Try sending a request related to a callback + """ + return callback_test diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py new file mode 100644 index 000000000..e76778af3 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py @@ -0,0 +1,152 @@ +from typing import Any, Dict, Optional, Union, cast + +import httpx + +from ...client import Client +from ...models.a_model import AModel +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + *, + client: Client, + json_body: AModel, +) -> Dict[str, Any]: + url = "{}/tests/callback".format(client.base_url) + + headers: Dict[str, str] = client.get_headers() + cookies: Dict[str, Any] = client.get_cookies() + + json_json_body = json_body.to_dict() + + return { + "method": "post", + "url": url, + "headers": headers, + "cookies": cookies, + "timeout": client.get_timeout(), + "json": json_json_body, + } + + +def _parse_response(*, response: httpx.Response) -> Optional[Union[Any, HTTPValidationError]]: + if response.status_code == 200: + response_200 = cast(Any, response.json()) + return response_200 + if response.status_code == 422: + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 + return None + + +def _build_response(*, response: httpx.Response) -> Response[Union[Any, HTTPValidationError]]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def sync_detailed( + *, + client: Client, + json_body: AModel, +) -> Response[Union[Any, HTTPValidationError]]: + """Path with callback + + Try sending a request related to a callback + + Args: + json_body (AModel): A Model for testing all the ways custom objects can be used + + Returns: + Response[Union[Any, HTTPValidationError]] + """ + + kwargs = _get_kwargs( + client=client, + json_body=json_body, + ) + + response = httpx.request( + verify=client.verify_ssl, + **kwargs, + ) + + return _build_response(response=response) + + +def sync( + *, + client: Client, + json_body: AModel, +) -> Optional[Union[Any, HTTPValidationError]]: + """Path with callback + + Try sending a request related to a callback + + Args: + json_body (AModel): A Model for testing all the ways custom objects can be used + + Returns: + Response[Union[Any, HTTPValidationError]] + """ + + return sync_detailed( + client=client, + json_body=json_body, + ).parsed + + +async def asyncio_detailed( + *, + client: Client, + json_body: AModel, +) -> Response[Union[Any, HTTPValidationError]]: + """Path with callback + + Try sending a request related to a callback + + Args: + json_body (AModel): A Model for testing all the ways custom objects can be used + + Returns: + Response[Union[Any, HTTPValidationError]] + """ + + kwargs = _get_kwargs( + client=client, + json_body=json_body, + ) + + async with httpx.AsyncClient(verify=client.verify_ssl) as _client: + response = await _client.request(**kwargs) + + return _build_response(response=response) + + +async def asyncio( + *, + client: Client, + json_body: AModel, +) -> Optional[Union[Any, HTTPValidationError]]: + """Path with callback + + Try sending a request related to a callback + + Args: + json_body (AModel): A Model for testing all the ways custom objects can be used + + Returns: + Response[Union[Any, HTTPValidationError]] + """ + + return ( + await asyncio_detailed( + client=client, + json_body=json_body, + ) + ).parsed diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 746fb90e4..8298670fd 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1173,6 +1173,62 @@ } } } + }, + "/tests/callback": { + "post": { + "tags": [ + "tests" + ], + "summary": "Path with callback", + "description": "Try sending a request related to a callback", + "operationId": "callback_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "callbacks": { + "event": { + "callback": { + "post": { + "responses": { + "200": { + "description": "Success" + }, + "503": { + "description": "Unavailable" + } + } + } + } + } + } + } } }, "components": {