Skip to content

Commit

Permalink
improvement: respect returning nested properties in python (#4236)
Browse files Browse the repository at this point in the history
  • Loading branch information
armandobelardo authored Aug 8, 2024
1 parent 565cf8d commit 862e05f
Show file tree
Hide file tree
Showing 219 changed files with 5,302 additions and 660 deletions.
3 changes: 2 additions & 1 deletion generators/python/core_utilities/sdk/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ def get_request_body(
# If both data and json are None, we send json data in the event extra properties are specified
json_body = maybe_filter_request_body(json, request_options, omit)

return json_body, data_body
# If you have an empty JSON body, you should just send None
return (json_body if json_body != {} else None), data_body if data_body != {} else None


class HttpClient:
Expand Down
24 changes: 24 additions & 0 deletions generators/python/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.6.0] - 2024-08-08
- Feat: The generator now respects returning nested properties, these can be specified via:

In OpenAPI below, we'd like to only return the property `jobId` from the `Job` object we get back from our server to our SDK users:
```yaml
my/endpoint:
get:
x-fern-sdk-return-value: jobId
response: Job
```
For a similar situation using the Fern definition:
```yaml
endpoints:
getJob:
method: GET
path: my/endpoint
response:
type: Job
property: jobId
```
- Fix: The underlying content no longer sends empty JSON bodies, instead it'll pass a `None` value to httpx

## [3.5.1] - 2024-08-05

- Fix: The root type for unions with visitors now has it's parent typed correctly. This allows auto-complete to work once again on the union when it's nested within other pydantic models.
Expand Down
2 changes: 1 addition & 1 deletion generators/python/sdk/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.5.1
3.6.0
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from typing import Dict, List, Optional, Set, Tuple, Union

import fern.ir.resources as ir_types
from typing_extensions import Never

from fern_python.codegen import AST
from fern_python.codegen.ast.ast_node.node_writer import NodeWriter
Expand Down Expand Up @@ -1020,9 +1019,25 @@ def _get_json_response_body_type(
response=lambda response: self._context.pydantic_generator_context.get_type_hint_for_type_reference(
response.response_body_type,
),
nested_property_as_response=lambda _: raise_json_nested_property_as_response_unsupported(),
# TODO: What is the case where you have a nested property as response, but no response property configured?
nested_property_as_response=lambda response: self._get_nested_json_response_type(response),
)

def _get_nested_json_response_type(self, response: ir_types.JsonResponseBodyWithProperty) -> AST.TypeHint:
response_type = self._context.pydantic_generator_context.get_type_hint_for_type_reference(
response.response_body_type
)
property_type = self._context.pydantic_generator_context.get_type_hint_for_type_reference(
response.response_property.value_type
if response.response_property is not None
else response.response_body_type
)

if response_type.is_optional and not property_type.is_optional:
return AST.TypeHint.optional(property_type)

return property_type

def _get_streaming_response_body_type(
self, *, stream_response: ir_types.StreamingResponse, is_async: bool
) -> AST.TypeHint:
Expand Down Expand Up @@ -1661,7 +1676,3 @@ def unwrap_optional_type(type_reference: ir_types.TypeReference) -> ir_types.Typ
if container_as_union.type == "optional":
return unwrap_optional_type(container_as_union.optional)
return type_reference


def raise_json_nested_property_as_response_unsupported() -> Never:
raise RuntimeError("nested property json response is unsupported")
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Callable, Optional
from urllib import response

import fern.ir.resources as ir_types
from typing_extensions import Never

from fern_python.codegen import AST
from fern_python.external_dependencies.httpx_sse import HttpxSSE
Expand Down Expand Up @@ -158,6 +158,50 @@ def _handle_success_json(
else f"{EndpointResponseCodeWriter.RESPONSE_VARIABLE}.json()"
),
)

# Validation rules limit the type of the response object to be either
# an object or optional object
property_access_expression: Optional[AST.Expression] = None
response_union = json_response.get_as_union()
if response_union.type == "nestedPropertyAsResponse":
response_body: ir_types.TypeReference = response_union.response_body_type
response_body_union = response_body.get_as_union()
response_property = (
response_union.response_property.name.name.snake_case.safe_name
if response_union.response_property is not None
else None
)
if response_body_union.type == "container":
response_container = response_body_union.container.get_as_union()
if response_container.type == "optional" and response_property is not None:
property_access_expression = AST.Expression(
f"{EndpointResponseCodeWriter.PARSED_RESPONSE_VARIABLE}.{response_property} if {EndpointResponseCodeWriter.PARSED_RESPONSE_VARIABLE} is not None else {EndpointResponseCodeWriter.PARSED_RESPONSE_VARIABLE}"
)
elif response_body_union.type == "named":
response_named = self._context.pydantic_generator_context.get_declaration_for_type_id(
response_body_union.type_id
)
property_access_expression = response_named.shape.visit(
object=lambda _: AST.Expression(
f"{EndpointResponseCodeWriter.PARSED_RESPONSE_VARIABLE}.{response_property}"
),
alias=lambda _: None,
enum=lambda _: None,
union=lambda _: None,
undiscriminated_union=lambda _: None,
)

if property_access_expression is not None:
# If you are indeed accessing a property, set the parsed response to an intermediate variable
writer.write_node(
AST.VariableDeclaration(
name=EndpointResponseCodeWriter.PARSED_RESPONSE_VARIABLE, initializer=pydantic_parse_expression
)
)

# Then use the property accessed expression moving forward
pydantic_parse_expression = property_access_expression

if self._pagination is not None:
paginator = self._pagination.visit(
cursor=lambda cursor: CursorPagination(
Expand Down Expand Up @@ -404,7 +448,10 @@ def _get_json_response_body_type(
response=lambda response: self._context.pydantic_generator_context.get_type_hint_for_type_reference(
response.response_body_type
),
nested_property_as_response=lambda _: raise_json_nested_property_as_response_unsupported(),
# TODO: What is the case where you have a nested property as response, but no response property configured?
nested_property_as_response=lambda response: self._context.pydantic_generator_context.get_type_hint_for_type_reference(
response.response_body_type
),
)

def _get_streaming_response_data_type(self, streaming_response: ir_types.StreamingResponse) -> AST.TypeHint:
Expand All @@ -416,7 +463,3 @@ def _get_streaming_response_data_type(self, streaming_response: ir_types.Streami
if union.type == "text":
return AST.TypeHint.str_()
raise RuntimeError(f"{union.type} streaming response is unsupported")


def raise_json_nested_property_as_response_unsupported() -> Never:
raise RuntimeError("nested property json response is unsupported")
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,15 @@ def _generate_service_test(self, service: ir_types.HttpService, snippet_writer:
or endpoint.request_body.get_as_union().type == "bytes"
)
)
# TODO(FER-2852): support test generation for nested property responses
or (
endpoint.response is not None
and endpoint.response.body
and (
endpoint.response.body.get_as_union().type == "json"
and endpoint.response.body.get_as_union().value.get_as_union().type == "nestedPropertyAsResponse" # type: ignore
)
)
):
continue
endpoint_name = endpoint.name.snake_case.safe_name
Expand Down
21 changes: 21 additions & 0 deletions generators/python/tests/utils/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,24 @@ def test_get_none_request_body() -> None:

assert json_body_extras == {"see you": "later"}
assert data_body_extras is None

def test_get_empty_json_request_body() -> None:
unrelated_request_options: RequestOptions = {"max_retries": 3}
json_body, data_body = get_request_body(
json=None,
data=None,
request_options=unrelated_request_options,
omit=None
)
assert json_body is None
assert data_body is None

json_body_extras, data_body_extras = get_request_body(
json={},
data=None,
request_options=unrelated_request_options,
omit=None
)

assert json_body_extras is None
assert data_body_extras is None
3 changes: 2 additions & 1 deletion seed/python-sdk/alias-extends/src/seed/core/http_client.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions seed/python-sdk/alias-extends/tests/utils/test_http_client.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion seed/python-sdk/alias/src/seed/core/http_client.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions seed/python-sdk/alias/tests/utils/test_http_client.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions seed/python-sdk/api-wide-base-path/tests/utils/test_http_client.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion seed/python-sdk/audiences/src/seed/core/http_client.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions seed/python-sdk/audiences/tests/utils/test_http_client.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion seed/python-sdk/basic-auth/src/seed/core/http_client.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 862e05f

Please sign in to comment.