-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: support $ref from endpoint response to components/responses BNCH-109662 #213
Changes from all commits
41e954d
fb0182e
fe6b7dd
8b0aca1
2edc6fe
9df3bb4
78bd860
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
from http import HTTPStatus | ||
from typing import Any, Dict, Optional, Union | ||
|
||
import httpx | ||
|
||
from ... import errors | ||
from ...client import AuthenticatedClient, Client | ||
from ...models.a_model import AModel | ||
from ...types import Response | ||
|
||
|
||
def _get_kwargs() -> Dict[str, Any]: | ||
_kwargs: Dict[str, Any] = { | ||
"method": "get", | ||
"url": "/responses/reference", | ||
} | ||
|
||
return _kwargs | ||
|
||
|
||
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[AModel]: | ||
if response.status_code == 200: | ||
response_200 = AModel.from_dict(response.json()) | ||
|
||
return response_200 | ||
if client.raise_on_unexpected_status: | ||
raise errors.UnexpectedStatus(response.status_code, response.content) | ||
else: | ||
return None | ||
|
||
|
||
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[AModel]: | ||
return Response( | ||
status_code=HTTPStatus(response.status_code), | ||
content=response.content, | ||
headers=response.headers, | ||
parsed=_parse_response(client=client, response=response), | ||
) | ||
|
||
|
||
def sync_detailed( | ||
*, | ||
client: Union[AuthenticatedClient, Client], | ||
) -> Response[AModel]: | ||
"""Endpoint using predefined response | ||
|
||
Raises: | ||
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. | ||
httpx.TimeoutException: If the request takes longer than Client.timeout. | ||
|
||
Returns: | ||
Response[AModel] | ||
""" | ||
|
||
kwargs = _get_kwargs() | ||
|
||
response = client.get_httpx_client().request( | ||
**kwargs, | ||
) | ||
|
||
return _build_response(client=client, response=response) | ||
|
||
|
||
def sync( | ||
*, | ||
client: Union[AuthenticatedClient, Client], | ||
) -> Optional[AModel]: | ||
"""Endpoint using predefined response | ||
|
||
Raises: | ||
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. | ||
httpx.TimeoutException: If the request takes longer than Client.timeout. | ||
|
||
Returns: | ||
AModel | ||
""" | ||
|
||
return sync_detailed( | ||
client=client, | ||
).parsed | ||
|
||
|
||
async def asyncio_detailed( | ||
*, | ||
client: Union[AuthenticatedClient, Client], | ||
) -> Response[AModel]: | ||
"""Endpoint using predefined response | ||
|
||
Raises: | ||
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. | ||
httpx.TimeoutException: If the request takes longer than Client.timeout. | ||
|
||
Returns: | ||
Response[AModel] | ||
""" | ||
|
||
kwargs = _get_kwargs() | ||
|
||
response = await client.get_async_httpx_client().request(**kwargs) | ||
|
||
return _build_response(client=client, response=response) | ||
|
||
|
||
async def asyncio( | ||
*, | ||
client: Union[AuthenticatedClient, Client], | ||
) -> Optional[AModel]: | ||
"""Endpoint using predefined response | ||
|
||
Raises: | ||
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. | ||
httpx.TimeoutException: If the request takes longer than Client.timeout. | ||
|
||
Returns: | ||
AModel | ||
""" | ||
|
||
return ( | ||
await asyncio_detailed( | ||
client=client, | ||
) | ||
).parsed |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,13 @@ def parse_reference_path(ref_path_raw: str) -> Union[ReferencePath, ParseError]: | |
return cast(ReferencePath, parsed.fragment) | ||
|
||
|
||
def get_reference_simple_name(ref_path: str) -> str: | ||
""" | ||
Takes a path like `/components/schemas/NameOfThing` and returns a string like `NameOfThing`. | ||
""" | ||
return ref_path.split("/", 3)[-1] | ||
|
||
|
||
Comment on lines
+49
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice 👍 |
||
@define | ||
class Class: | ||
"""Represents Python class which will be generated from an OpenAPI schema""" | ||
|
@@ -56,7 +63,7 @@ class Class: | |
@staticmethod | ||
def from_string(*, string: str, config: Config) -> "Class": | ||
"""Get a Class from an arbitrary string""" | ||
class_name = string.split("/")[-1] # Get rid of ref path stuff | ||
class_name = get_reference_simple_name(string) # Get rid of ref path stuff | ||
class_name = ClassName(class_name, config.field_prefix) | ||
override = config.class_overrides.get(class_name) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,12 @@ | ||
__all__ = ["Response", "response_from_data"] | ||
|
||
from http import HTTPStatus | ||
from typing import Optional, Tuple, TypedDict, Union | ||
from typing import Dict, Optional, Tuple, TypedDict, Union | ||
|
||
from attrs import define | ||
|
||
from openapi_python_client import utils | ||
from openapi_python_client.parser.properties.schemas import get_reference_simple_name, parse_reference_path | ||
|
||
from .. import Config | ||
from .. import schema as oai | ||
|
@@ -79,27 +80,30 @@ def empty_response( | |
) | ||
|
||
|
||
def response_from_data( | ||
def response_from_data( # noqa: PLR0911 | ||
*, | ||
status_code: HTTPStatus, | ||
data: Union[oai.Response, oai.Reference], | ||
schemas: Schemas, | ||
responses: Dict[str, Union[oai.Response, oai.Reference]], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a breaking change to this function's signature....What's the general methodology to validate that this is safe (possibly other places that might call this function?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's validated by the existing unit tests for the repo, which include 1. a bunch of tests of building an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
oh okayy cool... this is what I wanted to confirm! |
||
parent_name: str, | ||
config: Config, | ||
) -> Tuple[Union[Response, ParseError], Schemas]: | ||
"""Generate a Response from the OpenAPI dictionary representation of it""" | ||
|
||
response_name = f"response_{status_code}" | ||
if isinstance(data, oai.Reference): | ||
return ( | ||
empty_response( | ||
status_code=status_code, | ||
response_name=response_name, | ||
config=config, | ||
data=data, | ||
), | ||
schemas, | ||
) | ||
ref_path = parse_reference_path(data.ref) | ||
if isinstance(ref_path, ParseError): | ||
return ref_path, schemas | ||
if not ref_path.startswith("/components/responses/"): | ||
return ParseError(data=data, detail=f"$ref to {data.ref} not allowed in responses"), schemas | ||
resp_data = responses.get(get_reference_simple_name(ref_path), None) | ||
if not resp_data: | ||
return ParseError(data=data, detail=f"Could not find reference: {data.ref}"), schemas | ||
if not isinstance(resp_data, oai.Response): | ||
return ParseError(data=data, detail="Top-level $ref inside components/responses is not supported"), schemas | ||
data = resp_data | ||
|
||
content = data.content | ||
if not content: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of the files under
end_to_end_tests
can be ignored - they're just example data that's used by a test that runs the generator against a big spec file and checks for the expected output files.