-
Notifications
You must be signed in to change notification settings - Fork 1
fix: support $ref from endpoint response to components/responses BNCH-109662 #213
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
Changes from 6 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 |
---|---|---|
@@ -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 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(ref_path.split("/")[-1], None) | ||
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. I'm a little surprised that getting reference data is not a little more standardized or at least with a helper.... is doing a 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. There's no use case in the rest of the repo for getting just the type name from a 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. Note that this isn't really "getting reference data". The helper is just converting the string 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. Whoops, I was wrong, there are a couple other usages like this. So I've added a helper. |
||
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 | ||
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. instead of individual return statements like these,... what if we used a 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. I honestly find it easier to follow the logic when there's explicit short-circuiting like this. Maybe it's because I did a lot of Go and that's the preferred pattern in Go. But if I see a bunch of this kind of thing—
—it bothers me a little because now the code paths for the error conditions are not really independent of each other; if for instance I messed up and put "if" instead of "elif" for the second one, now the bad_condition_1 test would be effectively a no-op. It also means that I have to be constructing the same kind of error, ParseError, each time (and the same way each time: PLR0911 is only a helpful rule when it's helpful, and this codebase disables it in quite a few parsing/validation functions like this. 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.
okay this sounds good to me |
||
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.