Skip to content

Commit 6245b18

Browse files
dachuckydbanty
andauthored
fix: Exception when parsing documents which contain callbacks [#661]. Thanks @dachucky!
Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>
1 parent 227bc5e commit 6245b18

File tree

6 files changed

+257
-9
lines changed

6 files changed

+257
-9
lines changed

end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import types
44

55
from . import (
6+
callback_test,
67
defaults_tests_defaults_post,
78
get_basic_list_of_booleans,
89
get_basic_list_of_floats,
@@ -150,3 +151,10 @@ def token_with_cookie_auth_token_with_cookie_get(cls) -> types.ModuleType:
150151
Test optional cookie parameters
151152
"""
152153
return token_with_cookie_auth_token_with_cookie_get
154+
155+
@classmethod
156+
def callback_test(cls) -> types.ModuleType:
157+
"""
158+
Try sending a request related to a callback
159+
"""
160+
return callback_test
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from typing import Any, Dict, Optional, Union, cast
2+
3+
import httpx
4+
5+
from ...client import Client
6+
from ...models.a_model import AModel
7+
from ...models.http_validation_error import HTTPValidationError
8+
from ...types import Response
9+
10+
11+
def _get_kwargs(
12+
*,
13+
client: Client,
14+
json_body: AModel,
15+
) -> Dict[str, Any]:
16+
url = "{}/tests/callback".format(client.base_url)
17+
18+
headers: Dict[str, str] = client.get_headers()
19+
cookies: Dict[str, Any] = client.get_cookies()
20+
21+
json_json_body = json_body.to_dict()
22+
23+
return {
24+
"method": "post",
25+
"url": url,
26+
"headers": headers,
27+
"cookies": cookies,
28+
"timeout": client.get_timeout(),
29+
"json": json_json_body,
30+
}
31+
32+
33+
def _parse_response(*, response: httpx.Response) -> Optional[Union[Any, HTTPValidationError]]:
34+
if response.status_code == 200:
35+
response_200 = cast(Any, response.json())
36+
return response_200
37+
if response.status_code == 422:
38+
response_422 = HTTPValidationError.from_dict(response.json())
39+
40+
return response_422
41+
return None
42+
43+
44+
def _build_response(*, response: httpx.Response) -> Response[Union[Any, HTTPValidationError]]:
45+
return Response(
46+
status_code=response.status_code,
47+
content=response.content,
48+
headers=response.headers,
49+
parsed=_parse_response(response=response),
50+
)
51+
52+
53+
def sync_detailed(
54+
*,
55+
client: Client,
56+
json_body: AModel,
57+
) -> Response[Union[Any, HTTPValidationError]]:
58+
"""Path with callback
59+
60+
Try sending a request related to a callback
61+
62+
Args:
63+
json_body (AModel): A Model for testing all the ways custom objects can be used
64+
65+
Returns:
66+
Response[Union[Any, HTTPValidationError]]
67+
"""
68+
69+
kwargs = _get_kwargs(
70+
client=client,
71+
json_body=json_body,
72+
)
73+
74+
response = httpx.request(
75+
verify=client.verify_ssl,
76+
**kwargs,
77+
)
78+
79+
return _build_response(response=response)
80+
81+
82+
def sync(
83+
*,
84+
client: Client,
85+
json_body: AModel,
86+
) -> Optional[Union[Any, HTTPValidationError]]:
87+
"""Path with callback
88+
89+
Try sending a request related to a callback
90+
91+
Args:
92+
json_body (AModel): A Model for testing all the ways custom objects can be used
93+
94+
Returns:
95+
Response[Union[Any, HTTPValidationError]]
96+
"""
97+
98+
return sync_detailed(
99+
client=client,
100+
json_body=json_body,
101+
).parsed
102+
103+
104+
async def asyncio_detailed(
105+
*,
106+
client: Client,
107+
json_body: AModel,
108+
) -> Response[Union[Any, HTTPValidationError]]:
109+
"""Path with callback
110+
111+
Try sending a request related to a callback
112+
113+
Args:
114+
json_body (AModel): A Model for testing all the ways custom objects can be used
115+
116+
Returns:
117+
Response[Union[Any, HTTPValidationError]]
118+
"""
119+
120+
kwargs = _get_kwargs(
121+
client=client,
122+
json_body=json_body,
123+
)
124+
125+
async with httpx.AsyncClient(verify=client.verify_ssl) as _client:
126+
response = await _client.request(**kwargs)
127+
128+
return _build_response(response=response)
129+
130+
131+
async def asyncio(
132+
*,
133+
client: Client,
134+
json_body: AModel,
135+
) -> Optional[Union[Any, HTTPValidationError]]:
136+
"""Path with callback
137+
138+
Try sending a request related to a callback
139+
140+
Args:
141+
json_body (AModel): A Model for testing all the ways custom objects can be used
142+
143+
Returns:
144+
Response[Union[Any, HTTPValidationError]]
145+
"""
146+
147+
return (
148+
await asyncio_detailed(
149+
client=client,
150+
json_body=json_body,
151+
)
152+
).parsed

end_to_end_tests/openapi.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,62 @@
11731173
}
11741174
}
11751175
}
1176+
},
1177+
"/tests/callback": {
1178+
"post": {
1179+
"tags": [
1180+
"tests"
1181+
],
1182+
"summary": "Path with callback",
1183+
"description": "Try sending a request related to a callback",
1184+
"operationId": "callback_test",
1185+
"requestBody": {
1186+
"content": {
1187+
"application/json": {
1188+
"schema": {
1189+
"$ref": "#/components/schemas/AModel"
1190+
}
1191+
}
1192+
},
1193+
"required": true
1194+
},
1195+
"responses": {
1196+
"200": {
1197+
"description": "Successful Response",
1198+
"content": {
1199+
"application/json": {
1200+
"schema": {}
1201+
}
1202+
}
1203+
},
1204+
"422": {
1205+
"description": "Validation Error",
1206+
"content": {
1207+
"application/json": {
1208+
"schema": {
1209+
"$ref": "#/components/schemas/HTTPValidationError"
1210+
}
1211+
}
1212+
}
1213+
}
1214+
},
1215+
"callbacks": {
1216+
"event": {
1217+
"callback": {
1218+
"post": {
1219+
"responses": {
1220+
"200": {
1221+
"description": "Success"
1222+
},
1223+
"503": {
1224+
"description": "Unavailable"
1225+
}
1226+
}
1227+
}
1228+
}
1229+
}
1230+
}
1231+
}
11761232
}
11771233
},
11781234
"components": {

openapi_python_client/schema/openapi_schema_pydantic/operation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from .callback import Callback
66
from .external_documentation import ExternalDocumentation
77
from .parameter import Parameter
8+
9+
# Required to update forward ref after object creation, as this is not imported yet
10+
from .path_item import PathItem # pylint: disable=unused-import
811
from .reference import Reference
912
from .request_body import RequestBody
1013
from .responses import Responses
@@ -79,3 +82,7 @@ class Config: # pylint: disable=missing-class-docstring
7982
}
8083
]
8184
}
85+
86+
87+
# PathItem in Callback uses Operation, so we need to update forward refs due to circular dependency
88+
Operation.update_forward_refs()

openapi_python_client/schema/openapi_schema_pydantic/path_item.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from pydantic import BaseModel, Extra, Field
44

5-
from .operation import Operation
65
from .parameter import Parameter
76
from .reference import Reference
87
from .server import Server
@@ -23,14 +22,14 @@ class PathItem(BaseModel):
2322
ref: Optional[str] = Field(default=None, alias="$ref")
2423
summary: Optional[str] = None
2524
description: Optional[str] = None
26-
get: Optional[Operation] = None
27-
put: Optional[Operation] = None
28-
post: Optional[Operation] = None
29-
delete: Optional[Operation] = None
30-
options: Optional[Operation] = None
31-
head: Optional[Operation] = None
32-
patch: Optional[Operation] = None
33-
trace: Optional[Operation] = None
25+
get: Optional["Operation"] = None
26+
put: Optional["Operation"] = None
27+
post: Optional["Operation"] = None
28+
delete: Optional["Operation"] = None
29+
options: Optional["Operation"] = None
30+
head: Optional["Operation"] = None
31+
patch: Optional["Operation"] = None
32+
trace: Optional["Operation"] = None
3433
servers: Optional[List[Server]] = None
3534
parameters: Optional[List[Union[Parameter, Reference]]] = None
3635

@@ -70,3 +69,9 @@ class Config: # pylint: disable=missing-class-docstring
7069
}
7170
]
7271
}
72+
73+
74+
# Operation uses PathItem via Callback, so we need late import and to update forward refs due to circular dependency
75+
from .operation import Operation # pylint: disable=wrong-import-position unused-import
76+
77+
PathItem.update_forward_refs()

tests/test_schema/test_open_api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,23 @@ def test_validate_version(version, valid):
1414
else:
1515
with pytest.raises(ValidationError):
1616
OpenAPI.parse_obj(data)
17+
18+
19+
def test_parse_with_callback():
20+
data = {
21+
"openapi": "3.0.1",
22+
"info": {"title": "API with Callback", "version": ""},
23+
"paths": {
24+
"/create": {
25+
"post": {
26+
"responses": {"200": {"description": "Success"}},
27+
"callbacks": {"event": {"callback": {"post": {"responses": {"200": {"description": "Success"}}}}}},
28+
}
29+
}
30+
},
31+
}
32+
33+
open_api = OpenAPI.parse_obj(data)
34+
create_endpoint = open_api.paths["/create"]
35+
assert "200" in create_endpoint.post.responses
36+
assert "200" in create_endpoint.post.callbacks["event"]["callback"].post.responses

0 commit comments

Comments
 (0)