diff --git a/generators/python/core_utilities/sdk/http_client.py b/generators/python/core_utilities/sdk/http_client.py index 0d4f2d5566a..e8e0f28fae9 100644 --- a/generators/python/core_utilities/sdk/http_client.py +++ b/generators/python/core_utilities/sdk/http_client.py @@ -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: diff --git a/generators/python/sdk/CHANGELOG.md b/generators/python/sdk/CHANGELOG.md index bdcae8448e8..9386e7dec29 100644 --- a/generators/python/sdk/CHANGELOG.md +++ b/generators/python/sdk/CHANGELOG.md @@ -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. diff --git a/generators/python/sdk/VERSION b/generators/python/sdk/VERSION index d5c0c991428..40c341bdcdb 100644 --- a/generators/python/sdk/VERSION +++ b/generators/python/sdk/VERSION @@ -1 +1 @@ -3.5.1 +3.6.0 diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_function_generator.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_function_generator.py index 4571c83b352..d05c225b6b0 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_function_generator.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_function_generator.py @@ -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 @@ -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: @@ -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") diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py index 9bf3750465c..3f03fc10aee 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py @@ -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 @@ -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( @@ -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: @@ -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") diff --git a/generators/python/src/fern_python/snippet/snippet_test_factory.py b/generators/python/src/fern_python/snippet/snippet_test_factory.py index 23f0554cf22..2d0dac9616a 100644 --- a/generators/python/src/fern_python/snippet/snippet_test_factory.py +++ b/generators/python/src/fern_python/snippet/snippet_test_factory.py @@ -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 diff --git a/generators/python/tests/utils/test_http_client.py b/generators/python/tests/utils/test_http_client.py index 58feee97c16..670e0f2ad1b 100644 --- a/generators/python/tests/utils/test_http_client.py +++ b/generators/python/tests/utils/test_http_client.py @@ -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 \ No newline at end of file diff --git a/seed/python-sdk/alias-extends/src/seed/core/http_client.py b/seed/python-sdk/alias-extends/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/alias-extends/src/seed/core/http_client.py +++ b/seed/python-sdk/alias-extends/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/alias-extends/tests/utils/test_http_client.py b/seed/python-sdk/alias-extends/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/alias-extends/tests/utils/test_http_client.py +++ b/seed/python-sdk/alias-extends/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/alias/src/seed/core/http_client.py b/seed/python-sdk/alias/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/alias/src/seed/core/http_client.py +++ b/seed/python-sdk/alias/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/alias/tests/utils/test_http_client.py b/seed/python-sdk/alias/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/alias/tests/utils/test_http_client.py +++ b/seed/python-sdk/alias/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/api-wide-base-path/src/seed/core/http_client.py b/seed/python-sdk/api-wide-base-path/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/api-wide-base-path/src/seed/core/http_client.py +++ b/seed/python-sdk/api-wide-base-path/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/api-wide-base-path/tests/utils/test_http_client.py b/seed/python-sdk/api-wide-base-path/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/api-wide-base-path/tests/utils/test_http_client.py +++ b/seed/python-sdk/api-wide-base-path/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/audiences/src/seed/core/http_client.py b/seed/python-sdk/audiences/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/audiences/src/seed/core/http_client.py +++ b/seed/python-sdk/audiences/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/audiences/tests/utils/test_http_client.py b/seed/python-sdk/audiences/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/audiences/tests/utils/test_http_client.py +++ b/seed/python-sdk/audiences/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/auth-environment-variables/src/seed/core/http_client.py b/seed/python-sdk/auth-environment-variables/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/auth-environment-variables/src/seed/core/http_client.py +++ b/seed/python-sdk/auth-environment-variables/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/auth-environment-variables/tests/utils/test_http_client.py b/seed/python-sdk/auth-environment-variables/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/auth-environment-variables/tests/utils/test_http_client.py +++ b/seed/python-sdk/auth-environment-variables/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_client.py b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_client.py +++ b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/basic-auth-environment-variables/tests/utils/test_http_client.py b/seed/python-sdk/basic-auth-environment-variables/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/basic-auth-environment-variables/tests/utils/test_http_client.py +++ b/seed/python-sdk/basic-auth-environment-variables/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/basic-auth/src/seed/core/http_client.py b/seed/python-sdk/basic-auth/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/basic-auth/src/seed/core/http_client.py +++ b/seed/python-sdk/basic-auth/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/basic-auth/tests/utils/test_http_client.py b/seed/python-sdk/basic-auth/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/basic-auth/tests/utils/test_http_client.py +++ b/seed/python-sdk/basic-auth/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_client.py b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_client.py +++ b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/bearer-token-environment-variable/tests/utils/test_http_client.py b/seed/python-sdk/bearer-token-environment-variable/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/bearer-token-environment-variable/tests/utils/test_http_client.py +++ b/seed/python-sdk/bearer-token-environment-variable/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/bytes/src/seed/core/http_client.py b/seed/python-sdk/bytes/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/bytes/src/seed/core/http_client.py +++ b/seed/python-sdk/bytes/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/bytes/tests/utils/test_http_client.py b/seed/python-sdk/bytes/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/bytes/tests/utils/test_http_client.py +++ b/seed/python-sdk/bytes/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/circular-references-advanced/src/seed/core/http_client.py b/seed/python-sdk/circular-references-advanced/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/circular-references-advanced/src/seed/core/http_client.py +++ b/seed/python-sdk/circular-references-advanced/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/circular-references-advanced/tests/utils/test_http_client.py b/seed/python-sdk/circular-references-advanced/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/circular-references-advanced/tests/utils/test_http_client.py +++ b/seed/python-sdk/circular-references-advanced/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/circular-references/src/seed/core/http_client.py b/seed/python-sdk/circular-references/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/circular-references/src/seed/core/http_client.py +++ b/seed/python-sdk/circular-references/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/circular-references/tests/utils/test_http_client.py b/seed/python-sdk/circular-references/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/circular-references/tests/utils/test_http_client.py +++ b/seed/python-sdk/circular-references/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/code-samples/src/seed/core/http_client.py b/seed/python-sdk/code-samples/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/code-samples/src/seed/core/http_client.py +++ b/seed/python-sdk/code-samples/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/code-samples/tests/utils/test_http_client.py b/seed/python-sdk/code-samples/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/code-samples/tests/utils/test_http_client.py +++ b/seed/python-sdk/code-samples/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/custom-auth/src/seed/core/http_client.py b/seed/python-sdk/custom-auth/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/custom-auth/src/seed/core/http_client.py +++ b/seed/python-sdk/custom-auth/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/custom-auth/tests/utils/test_http_client.py b/seed/python-sdk/custom-auth/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/custom-auth/tests/utils/test_http_client.py +++ b/seed/python-sdk/custom-auth/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/enum/no-custom-config/src/seed/core/http_client.py b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/enum/no-custom-config/src/seed/core/http_client.py +++ b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/enum/no-custom-config/tests/utils/test_http_client.py b/seed/python-sdk/enum/no-custom-config/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/enum/no-custom-config/tests/utils/test_http_client.py +++ b/seed/python-sdk/enum/no-custom-config/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/enum/strenum/src/seed/core/http_client.py b/seed/python-sdk/enum/strenum/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/enum/strenum/src/seed/core/http_client.py +++ b/seed/python-sdk/enum/strenum/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/enum/strenum/tests/utils/test_http_client.py b/seed/python-sdk/enum/strenum/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/enum/strenum/tests/utils/test_http_client.py +++ b/seed/python-sdk/enum/strenum/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/error-property/src/seed/core/http_client.py b/seed/python-sdk/error-property/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/error-property/src/seed/core/http_client.py +++ b/seed/python-sdk/error-property/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/error-property/tests/utils/test_http_client.py b/seed/python-sdk/error-property/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/error-property/tests/utils/test_http_client.py +++ b/seed/python-sdk/error-property/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/examples/client-filename/src/seed/core/http_client.py b/seed/python-sdk/examples/client-filename/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/examples/client-filename/src/seed/core/http_client.py +++ b/seed/python-sdk/examples/client-filename/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/examples/client-filename/tests/utils/test_http_client.py b/seed/python-sdk/examples/client-filename/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/examples/client-filename/tests/utils/test_http_client.py +++ b/seed/python-sdk/examples/client-filename/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/examples/no-custom-config/src/seed/core/http_client.py b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/examples/no-custom-config/src/seed/core/http_client.py +++ b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/examples/no-custom-config/tests/utils/test_http_client.py b/seed/python-sdk/examples/no-custom-config/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/examples/no-custom-config/tests/utils/test_http_client.py +++ b/seed/python-sdk/examples/no-custom-config/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/examples/readme/src/seed/core/http_client.py b/seed/python-sdk/examples/readme/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/examples/readme/src/seed/core/http_client.py +++ b/seed/python-sdk/examples/readme/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/examples/readme/tests/utils/test_http_client.py b/seed/python-sdk/examples/readme/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/examples/readme/tests/utils/test_http_client.py +++ b/seed/python-sdk/examples/readme/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/deps_with_min_python_version/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/deps_with_min_python_version/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/deps_with_min_python_version/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/deps_with_min_python_version/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/deps_with_min_python_version/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/deps_with_min_python_version/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/deps_with_min_python_version/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/deps_with_min_python_version/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/extra_dependencies/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/extra_dependencies/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/extra_dependencies/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/extra_dependencies/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/extra_dev_dependencies/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/five-second-timeout/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/five-second-timeout/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/five-second-timeout/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/five-second-timeout/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/follow_redirects_by_default/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/improved_imports/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/improved_imports/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/improved_imports/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/improved_imports/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/infinite-timeout/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/infinite-timeout/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/infinite-timeout/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/infinite-timeout/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/inline_request_params/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/inline_request_params/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/inline_request_params/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/inline_request_params/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/no-custom-config/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/no-custom-config/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/no-custom-config/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/no-custom-config/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/pydantic-extra-fields/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/pydantic-v1/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/pydantic-v1/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/pydantic-v1/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/pydantic-v1/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/skip-pydantic-validation/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_client.py b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_client.py +++ b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/exhaustive/union-utils/tests/utils/test_http_client.py b/seed/python-sdk/exhaustive/union-utils/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/exhaustive/union-utils/tests/utils/test_http_client.py +++ b/seed/python-sdk/exhaustive/union-utils/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/extends/src/seed/core/http_client.py b/seed/python-sdk/extends/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/extends/src/seed/core/http_client.py +++ b/seed/python-sdk/extends/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/extends/tests/utils/test_http_client.py b/seed/python-sdk/extends/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/extends/tests/utils/test_http_client.py +++ b/seed/python-sdk/extends/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/extra-properties/src/seed/core/http_client.py b/seed/python-sdk/extra-properties/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/extra-properties/src/seed/core/http_client.py +++ b/seed/python-sdk/extra-properties/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/extra-properties/tests/utils/test_http_client.py b/seed/python-sdk/extra-properties/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/extra-properties/tests/utils/test_http_client.py +++ b/seed/python-sdk/extra-properties/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/file-download/src/seed/core/http_client.py b/seed/python-sdk/file-download/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/file-download/src/seed/core/http_client.py +++ b/seed/python-sdk/file-download/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/file-download/tests/utils/test_http_client.py b/seed/python-sdk/file-download/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/file-download/tests/utils/test_http_client.py +++ b/seed/python-sdk/file-download/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/file-upload/src/seed/core/http_client.py b/seed/python-sdk/file-upload/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/file-upload/src/seed/core/http_client.py +++ b/seed/python-sdk/file-upload/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/file-upload/tests/utils/test_http_client.py b/seed/python-sdk/file-upload/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/file-upload/tests/utils/test_http_client.py +++ b/seed/python-sdk/file-upload/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/folders/src/seed/core/http_client.py b/seed/python-sdk/folders/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/folders/src/seed/core/http_client.py +++ b/seed/python-sdk/folders/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/folders/tests/utils/test_http_client.py b/seed/python-sdk/folders/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/folders/tests/utils/test_http_client.py +++ b/seed/python-sdk/folders/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/grpc/.mock/proto/google/api/annotations.proto b/seed/python-sdk/grpc/.mock/proto/google/api/annotations.proto deleted file mode 100644 index efdab3db6ca..00000000000 --- a/seed/python-sdk/grpc/.mock/proto/google/api/annotations.proto +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2015 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -import "google/api/http.proto"; -import "google/protobuf/descriptor.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "AnnotationsProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.MethodOptions { - // See `HttpRule`. - HttpRule http = 72295728; -} diff --git a/seed/python-sdk/grpc/.mock/proto/google/api/field_behavior.proto b/seed/python-sdk/grpc/.mock/proto/google/api/field_behavior.proto deleted file mode 100644 index 344cb0b1fc2..00000000000 --- a/seed/python-sdk/grpc/.mock/proto/google/api/field_behavior.proto +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -import "google/protobuf/descriptor.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "FieldBehaviorProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.FieldOptions { - // A designation of a specific field behavior (required, output only, etc.) - // in protobuf messages. - // - // Examples: - // - // string name = 1 [(google.api.field_behavior) = REQUIRED]; - // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; - // google.protobuf.Duration ttl = 1 - // [(google.api.field_behavior) = INPUT_ONLY]; - // google.protobuf.Timestamp expire_time = 1 - // [(google.api.field_behavior) = OUTPUT_ONLY, - // (google.api.field_behavior) = IMMUTABLE]; - repeated google.api.FieldBehavior field_behavior = 1052; -} - -// An indicator of the behavior of a given field (for example, that a field -// is required in requests, or given as output but ignored as input). -// This **does not** change the behavior in protocol buffers itself; it only -// denotes the behavior and may affect how API tooling handles the field. -// -// Note: This enum **may** receive new values in the future. -enum FieldBehavior { - // Conventional default for enums. Do not use this. - FIELD_BEHAVIOR_UNSPECIFIED = 0; - - // Specifically denotes a field as optional. - // While all fields in protocol buffers are optional, this may be specified - // for emphasis if appropriate. - OPTIONAL = 1; - - // Denotes a field as required. - // This indicates that the field **must** be provided as part of the request, - // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). - REQUIRED = 2; - - // Denotes a field as output only. - // This indicates that the field is provided in responses, but including the - // field in a request does nothing (the server *must* ignore it and - // *must not* throw an error as a result of the field's presence). - OUTPUT_ONLY = 3; - - // Denotes a field as input only. - // This indicates that the field is provided in requests, and the - // corresponding field is not included in output. - INPUT_ONLY = 4; - - // Denotes a field as immutable. - // This indicates that the field may be set once in a request to create a - // resource, but may not be changed thereafter. - IMMUTABLE = 5; - - // Denotes that a (repeated) field is an unordered list. - // This indicates that the service may provide the elements of the list - // in any arbitrary order, rather than the order the user originally - // provided. Additionally, the list's order may or may not be stable. - UNORDERED_LIST = 6; - - // Denotes that this field returns a non-empty default value if not set. - // This indicates that if the user provides the empty value in a request, - // a non-empty value will be returned. The user will not be aware of what - // non-empty value to expect. - NON_EMPTY_DEFAULT = 7; - - // Denotes that the field in a resource (a message annotated with - // google.api.resource) is used in the resource name to uniquely identify the - // resource. For AIP-compliant APIs, this should only be applied to the - // `name` field on the resource. - // - // This behavior should not be applied to references to other resources within - // the message. - // - // The identifier field of resources often have different field behavior - // depending on the request it is embedded in (e.g. for Create methods name - // is optional and unused, while for Update methods it is required). Instead - // of method-specific annotations, only `IDENTIFIER` is required. - IDENTIFIER = 8; -} diff --git a/seed/python-sdk/grpc/.mock/proto/google/api/http.proto b/seed/python-sdk/grpc/.mock/proto/google/api/http.proto deleted file mode 100644 index 31d867a27d5..00000000000 --- a/seed/python-sdk/grpc/.mock/proto/google/api/http.proto +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "HttpProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Defines the HTTP configuration for an API service. It contains a list of -// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method -// to one or more HTTP REST API methods. -message Http { - // A list of HTTP configuration rules that apply to individual API methods. - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated HttpRule rules = 1; - - // When set to true, URL path parameters will be fully URI-decoded except in - // cases of single segment matches in reserved expansion, where "%2F" will be - // left encoded. - // - // The default behavior is to not decode RFC 6570 reserved characters in multi - // segment matches. - bool fully_decode_reserved_expansion = 2; -} - -// # gRPC Transcoding -// -// gRPC Transcoding is a feature for mapping between a gRPC method and one or -// more HTTP REST endpoints. It allows developers to build a single API service -// that supports both gRPC APIs and REST APIs. Many systems, including [Google -// APIs](https://github.com/googleapis/googleapis), -// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC -// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), -// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature -// and use it for large scale production services. -// -// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies -// how different portions of the gRPC request message are mapped to the URL -// path, URL query parameters, and HTTP request body. It also controls how the -// gRPC response message is mapped to the HTTP response body. `HttpRule` is -// typically specified as an `google.api.http` annotation on the gRPC method. -// -// Each mapping specifies a URL path template and an HTTP method. The path -// template may refer to one or more fields in the gRPC request message, as long -// as each field is a non-repeated field with a primitive (non-message) type. -// The path template controls how fields of the request message are mapped to -// the URL path. -// -// Example: -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http) = { -// get: "/v1/{name=messages/*}" -// }; -// } -// } -// message GetMessageRequest { -// string name = 1; // Mapped to URL path. -// } -// message Message { -// string text = 1; // The resource content. -// } -// -// This enables an HTTP REST to gRPC mapping as below: -// -// HTTP | gRPC -// -----|----- -// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` -// -// Any fields in the request message which are not bound by the path template -// automatically become HTTP query parameters if there is no HTTP request body. -// For example: -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http) = { -// get:"/v1/messages/{message_id}" -// }; -// } -// } -// message GetMessageRequest { -// message SubMessage { -// string subfield = 1; -// } -// string message_id = 1; // Mapped to URL path. -// int64 revision = 2; // Mapped to URL query parameter `revision`. -// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. -// } -// -// This enables a HTTP JSON to RPC mapping as below: -// -// HTTP | gRPC -// -----|----- -// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | -// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: -// "foo"))` -// -// Note that fields which are mapped to URL query parameters must have a -// primitive type or a repeated primitive type or a non-repeated message type. -// In the case of a repeated type, the parameter can be repeated in the URL -// as `...?param=A¶m=B`. In the case of a message type, each field of the -// message is mapped to a separate parameter, such as -// `...?foo.a=A&foo.b=B&foo.c=C`. -// -// For HTTP methods that allow a request body, the `body` field -// specifies the mapping. Consider a REST update method on the -// message resource collection: -// -// service Messaging { -// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { -// option (google.api.http) = { -// patch: "/v1/messages/{message_id}" -// body: "message" -// }; -// } -// } -// message UpdateMessageRequest { -// string message_id = 1; // mapped to the URL -// Message message = 2; // mapped to the body -// } -// -// The following HTTP JSON to RPC mapping is enabled, where the -// representation of the JSON in the request body is determined by -// protos JSON encoding: -// -// HTTP | gRPC -// -----|----- -// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: -// "123456" message { text: "Hi!" })` -// -// The special name `*` can be used in the body mapping to define that -// every field not bound by the path template should be mapped to the -// request body. This enables the following alternative definition of -// the update method: -// -// service Messaging { -// rpc UpdateMessage(Message) returns (Message) { -// option (google.api.http) = { -// patch: "/v1/messages/{message_id}" -// body: "*" -// }; -// } -// } -// message Message { -// string message_id = 1; -// string text = 2; -// } -// -// -// The following HTTP JSON to RPC mapping is enabled: -// -// HTTP | gRPC -// -----|----- -// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: -// "123456" text: "Hi!")` -// -// Note that when using `*` in the body mapping, it is not possible to -// have HTTP parameters, as all fields not bound by the path end in -// the body. This makes this option more rarely used in practice when -// defining REST APIs. The common usage of `*` is in custom methods -// which don't use the URL at all for transferring data. -// -// It is possible to define multiple HTTP methods for one RPC by using -// the `additional_bindings` option. Example: -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http) = { -// get: "/v1/messages/{message_id}" -// additional_bindings { -// get: "/v1/users/{user_id}/messages/{message_id}" -// } -// }; -// } -// } -// message GetMessageRequest { -// string message_id = 1; -// string user_id = 2; -// } -// -// This enables the following two alternative HTTP JSON to RPC mappings: -// -// HTTP | gRPC -// -----|----- -// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` -// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: -// "123456")` -// -// ## Rules for HTTP mapping -// -// 1. Leaf request fields (recursive expansion nested messages in the request -// message) are classified into three categories: -// - Fields referred by the path template. They are passed via the URL path. -// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They -// are passed via the HTTP -// request body. -// - All other fields are passed via the URL query parameters, and the -// parameter name is the field path in the request message. A repeated -// field can be represented as multiple query parameters under the same -// name. -// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL -// query parameter, all fields -// are passed via URL path and HTTP request body. -// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP -// request body, all -// fields are passed via URL path and URL query parameters. -// -// ### Path template syntax -// -// Template = "/" Segments [ Verb ] ; -// Segments = Segment { "/" Segment } ; -// Segment = "*" | "**" | LITERAL | Variable ; -// Variable = "{" FieldPath [ "=" Segments ] "}" ; -// FieldPath = IDENT { "." IDENT } ; -// Verb = ":" LITERAL ; -// -// The syntax `*` matches a single URL path segment. The syntax `**` matches -// zero or more URL path segments, which must be the last part of the URL path -// except the `Verb`. -// -// The syntax `Variable` matches part of the URL path as specified by its -// template. A variable template must not contain other variables. If a variable -// matches a single path segment, its template may be omitted, e.g. `{var}` -// is equivalent to `{var=*}`. -// -// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` -// contains any reserved character, such characters should be percent-encoded -// before the matching. -// -// If a variable contains exactly one path segment, such as `"{var}"` or -// `"{var=*}"`, when such a variable is expanded into a URL path on the client -// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The -// server side does the reverse decoding. Such variables show up in the -// [Discovery -// Document](https://developers.google.com/discovery/v1/reference/apis) as -// `{var}`. -// -// If a variable contains multiple path segments, such as `"{var=foo/*}"` -// or `"{var=**}"`, when such a variable is expanded into a URL path on the -// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. -// The server side does the reverse decoding, except "%2F" and "%2f" are left -// unchanged. Such variables show up in the -// [Discovery -// Document](https://developers.google.com/discovery/v1/reference/apis) as -// `{+var}`. -// -// ## Using gRPC API Service Configuration -// -// gRPC API Service Configuration (service config) is a configuration language -// for configuring a gRPC service to become a user-facing product. The -// service config is simply the YAML representation of the `google.api.Service` -// proto message. -// -// As an alternative to annotating your proto file, you can configure gRPC -// transcoding in your service config YAML files. You do this by specifying a -// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same -// effect as the proto annotation. This can be particularly useful if you -// have a proto that is reused in multiple services. Note that any transcoding -// specified in the service config will override any matching transcoding -// configuration in the proto. -// -// Example: -// -// http: -// rules: -// # Selects a gRPC method and applies HttpRule to it. -// - selector: example.v1.Messaging.GetMessage -// get: /v1/messages/{message_id}/{sub.subfield} -// -// ## Special notes -// -// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the -// proto to JSON conversion must follow the [proto3 -// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). -// -// While the single segment variable follows the semantics of -// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String -// Expansion, the multi segment variable **does not** follow RFC 6570 Section -// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion -// does not expand special characters like `?` and `#`, which would lead -// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding -// for multi segment variables. -// -// The path variables **must not** refer to any repeated or mapped field, -// because client libraries are not capable of handling such variable expansion. -// -// The path variables **must not** capture the leading "/" character. The reason -// is that the most common use case "{var}" does not capture the leading "/" -// character. For consistency, all path variables must share the same behavior. -// -// Repeated message fields must not be mapped to URL query parameters, because -// no client library can support such complicated mapping. -// -// If an API needs to use a JSON array for request or response body, it can map -// the request or response body to a repeated field. However, some gRPC -// Transcoding implementations may not support this feature. -message HttpRule { - // Selects a method to which this rule applies. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax - // details. - string selector = 1; - - // Determines the URL pattern is matched by this rules. This pattern can be - // used with any of the {get|put|post|delete|patch} methods. A custom method - // can be defined using the 'custom' field. - oneof pattern { - // Maps to HTTP GET. Used for listing and getting information about - // resources. - string get = 2; - - // Maps to HTTP PUT. Used for replacing a resource. - string put = 3; - - // Maps to HTTP POST. Used for creating a resource or performing an action. - string post = 4; - - // Maps to HTTP DELETE. Used for deleting a resource. - string delete = 5; - - // Maps to HTTP PATCH. Used for updating a resource. - string patch = 6; - - // The custom pattern is used for specifying an HTTP method that is not - // included in the `pattern` field, such as HEAD, or "*" to leave the - // HTTP method unspecified for this rule. The wild-card rule is useful - // for services that provide content to Web (HTML) clients. - CustomHttpPattern custom = 8; - } - - // The name of the request field whose value is mapped to the HTTP request - // body, or `*` for mapping all request fields not captured by the path - // pattern to the HTTP body, or omitted for not having any HTTP request body. - // - // NOTE: the referred field must be present at the top-level of the request - // message type. - string body = 7; - - // Optional. The name of the response field whose value is mapped to the HTTP - // response body. When omitted, the entire response message will be used - // as the HTTP response body. - // - // NOTE: The referred field must be present at the top-level of the response - // message type. - string response_body = 12; - - // Additional HTTP bindings for the selector. Nested bindings must - // not contain an `additional_bindings` field themselves (that is, - // the nesting may only be one level deep). - repeated HttpRule additional_bindings = 11; -} - -// A custom pattern is used for defining custom HTTP verb. -message CustomHttpPattern { - // The name of this custom HTTP verb. - string kind = 1; - - // The path matched by this custom verb. - string path = 2; -} diff --git a/seed/python-sdk/grpc/.mock/proto/user/v1/user.proto b/seed/python-sdk/grpc/.mock/proto/user/v1/user.proto deleted file mode 100644 index adab9ceefb9..00000000000 --- a/seed/python-sdk/grpc/.mock/proto/user/v1/user.proto +++ /dev/null @@ -1,55 +0,0 @@ -syntax = "proto3"; - -package user.v1; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/struct.proto"; - -option csharp_namespace = "User.V1"; - -message User { - string username = 1 [ - (google.api.field_behavior) = REQUIRED - ]; - string email = 2; - uint32 age = 3; - float weight = 4; - google.protobuf.Struct metadata = 5; -} - -message CreateUserRequest { - string username = 1 [ - (google.api.field_behavior) = REQUIRED - ]; - string email = 2; - uint32 age = 3; - float weight = 4; - google.protobuf.Struct metadata = 5; -} - -message CreateUserResponse { - User user = 1; -} - -message GetUserRequest { - string username = 1; - string email = 2; - uint32 age = 3; - float weight = 4; -} - -service UserService { - rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) { - option (google.api.http) = { - post: "/users" - body: "*" - }; - } - - rpc GetUser(GetUserRequest) returns (User) { - option (google.api.http) = { - get: "/users" - }; - } -} diff --git a/seed/python-sdk/grpc/src/seed/core/http_client.py b/seed/python-sdk/grpc/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/grpc/src/seed/core/http_client.py +++ b/seed/python-sdk/grpc/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/grpc/tests/utils/test_http_client.py b/seed/python-sdk/grpc/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/grpc/tests/utils/test_http_client.py +++ b/seed/python-sdk/grpc/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/idempotency-headers/src/seed/core/http_client.py b/seed/python-sdk/idempotency-headers/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/idempotency-headers/src/seed/core/http_client.py +++ b/seed/python-sdk/idempotency-headers/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/idempotency-headers/tests/utils/test_http_client.py b/seed/python-sdk/idempotency-headers/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/idempotency-headers/tests/utils/test_http_client.py +++ b/seed/python-sdk/idempotency-headers/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/imdb/src/seed/core/http_client.py b/seed/python-sdk/imdb/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/imdb/src/seed/core/http_client.py +++ b/seed/python-sdk/imdb/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/imdb/tests/utils/test_http_client.py b/seed/python-sdk/imdb/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/imdb/tests/utils/test_http_client.py +++ b/seed/python-sdk/imdb/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/literal/no-custom-config/src/seed/core/http_client.py b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/literal/no-custom-config/src/seed/core/http_client.py +++ b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/literal/no-custom-config/tests/utils/test_http_client.py b/seed/python-sdk/literal/no-custom-config/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/literal/no-custom-config/tests/utils/test_http_client.py +++ b/seed/python-sdk/literal/no-custom-config/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_client.py b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_client.py +++ b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/literal/use_typeddict_requests/tests/utils/test_http_client.py b/seed/python-sdk/literal/use_typeddict_requests/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/literal/use_typeddict_requests/tests/utils/test_http_client.py +++ b/seed/python-sdk/literal/use_typeddict_requests/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/mixed-case/src/seed/core/http_client.py b/seed/python-sdk/mixed-case/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/mixed-case/src/seed/core/http_client.py +++ b/seed/python-sdk/mixed-case/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/mixed-case/tests/utils/test_http_client.py b/seed/python-sdk/mixed-case/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/mixed-case/tests/utils/test_http_client.py +++ b/seed/python-sdk/mixed-case/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/multi-line-docs/src/seed/core/http_client.py b/seed/python-sdk/multi-line-docs/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/multi-line-docs/src/seed/core/http_client.py +++ b/seed/python-sdk/multi-line-docs/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/multi-line-docs/tests/utils/test_http_client.py b/seed/python-sdk/multi-line-docs/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/multi-line-docs/tests/utils/test_http_client.py +++ b/seed/python-sdk/multi-line-docs/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_client.py b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_client.py +++ b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/multi-url-environment-no-default/tests/utils/test_http_client.py b/seed/python-sdk/multi-url-environment-no-default/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/multi-url-environment-no-default/tests/utils/test_http_client.py +++ b/seed/python-sdk/multi-url-environment-no-default/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/multi-url-environment/src/seed/core/http_client.py b/seed/python-sdk/multi-url-environment/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/multi-url-environment/src/seed/core/http_client.py +++ b/seed/python-sdk/multi-url-environment/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/multi-url-environment/tests/utils/test_http_client.py b/seed/python-sdk/multi-url-environment/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/multi-url-environment/tests/utils/test_http_client.py +++ b/seed/python-sdk/multi-url-environment/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/no-environment/src/seed/core/http_client.py b/seed/python-sdk/no-environment/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/no-environment/src/seed/core/http_client.py +++ b/seed/python-sdk/no-environment/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/no-environment/tests/utils/test_http_client.py b/seed/python-sdk/no-environment/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/no-environment/tests/utils/test_http_client.py +++ b/seed/python-sdk/no-environment/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_client.py b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_client.py +++ b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/oauth-client-credentials-default/tests/utils/test_http_client.py b/seed/python-sdk/oauth-client-credentials-default/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/oauth-client-credentials-default/tests/utils/test_http_client.py +++ b/seed/python-sdk/oauth-client-credentials-default/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_client.py b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_client.py +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/tests/utils/test_http_client.py b/seed/python-sdk/oauth-client-credentials-environment-variables/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/oauth-client-credentials-environment-variables/tests/utils/test_http_client.py +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_client.py b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_client.py +++ b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/tests/utils/test_http_client.py b/seed/python-sdk/oauth-client-credentials-nested-root/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/oauth-client-credentials-nested-root/tests/utils/test_http_client.py +++ b/seed/python-sdk/oauth-client-credentials-nested-root/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/oauth-client-credentials/src/seed/core/http_client.py b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/oauth-client-credentials/src/seed/core/http_client.py +++ b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/oauth-client-credentials/tests/utils/test_http_client.py b/seed/python-sdk/oauth-client-credentials/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/oauth-client-credentials/tests/utils/test_http_client.py +++ b/seed/python-sdk/oauth-client-credentials/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/object/src/seed/core/http_client.py b/seed/python-sdk/object/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/object/src/seed/core/http_client.py +++ b/seed/python-sdk/object/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/object/tests/utils/test_http_client.py b/seed/python-sdk/object/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/object/tests/utils/test_http_client.py +++ b/seed/python-sdk/object/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/objects-with-imports/src/seed/core/http_client.py b/seed/python-sdk/objects-with-imports/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/objects-with-imports/src/seed/core/http_client.py +++ b/seed/python-sdk/objects-with-imports/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/objects-with-imports/tests/utils/test_http_client.py b/seed/python-sdk/objects-with-imports/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/objects-with-imports/tests/utils/test_http_client.py +++ b/seed/python-sdk/objects-with-imports/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/optional/src/seed/core/http_client.py b/seed/python-sdk/optional/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/optional/src/seed/core/http_client.py +++ b/seed/python-sdk/optional/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/optional/tests/utils/test_http_client.py b/seed/python-sdk/optional/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/optional/tests/utils/test_http_client.py +++ b/seed/python-sdk/optional/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/package-yml/src/seed/core/http_client.py b/seed/python-sdk/package-yml/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/package-yml/src/seed/core/http_client.py +++ b/seed/python-sdk/package-yml/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/package-yml/tests/utils/test_http_client.py b/seed/python-sdk/package-yml/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/package-yml/tests/utils/test_http_client.py +++ b/seed/python-sdk/package-yml/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/pagination/src/seed/core/http_client.py b/seed/python-sdk/pagination/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/pagination/src/seed/core/http_client.py +++ b/seed/python-sdk/pagination/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/pagination/tests/utils/test_http_client.py b/seed/python-sdk/pagination/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/pagination/tests/utils/test_http_client.py +++ b/seed/python-sdk/pagination/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/plain-text/src/seed/core/http_client.py b/seed/python-sdk/plain-text/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/plain-text/src/seed/core/http_client.py +++ b/seed/python-sdk/plain-text/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/plain-text/tests/utils/test_http_client.py b/seed/python-sdk/plain-text/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/plain-text/tests/utils/test_http_client.py +++ b/seed/python-sdk/plain-text/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/query-parameters/src/seed/core/http_client.py b/seed/python-sdk/query-parameters/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/query-parameters/src/seed/core/http_client.py +++ b/seed/python-sdk/query-parameters/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/query-parameters/tests/utils/test_http_client.py b/seed/python-sdk/query-parameters/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/query-parameters/tests/utils/test_http_client.py +++ b/seed/python-sdk/query-parameters/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/reserved-keywords/src/seed/core/http_client.py b/seed/python-sdk/reserved-keywords/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/reserved-keywords/src/seed/core/http_client.py +++ b/seed/python-sdk/reserved-keywords/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/reserved-keywords/tests/utils/test_http_client.py b/seed/python-sdk/reserved-keywords/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/reserved-keywords/tests/utils/test_http_client.py +++ b/seed/python-sdk/reserved-keywords/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/response-property/.github/workflows/ci.yml b/seed/python-sdk/response-property/.github/workflows/ci.yml new file mode 100644 index 00000000000..b7316b8cab7 --- /dev/null +++ b/seed/python-sdk/response-property/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: ci + +on: [push] +jobs: + compile: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Compile + run: poetry run mypy . + test: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + + - name: Install Fern + run: npm install -g fern-api + - name: Test + run: fern test --command "poetry run pytest -rP ." + + publish: + needs: [compile, test] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Publish to pypi + run: | + poetry config repositories.remote + poetry --no-interaction -v publish --build --repository remote --username "$" --password "$" + env: + : ${{ secrets. }} + : ${{ secrets. }} diff --git a/seed/python-sdk/response-property/.gitignore b/seed/python-sdk/response-property/.gitignore new file mode 100644 index 00000000000..42cb863501e --- /dev/null +++ b/seed/python-sdk/response-property/.gitignore @@ -0,0 +1,4 @@ +dist/ +.mypy_cache/ +__pycache__/ +poetry.toml diff --git a/seed/python-sdk/response-property/.mock/definition/__package__.yml b/seed/python-sdk/response-property/.mock/definition/__package__.yml new file mode 100644 index 00000000000..708a5a1139d --- /dev/null +++ b/seed/python-sdk/response-property/.mock/definition/__package__.yml @@ -0,0 +1,10 @@ +types: + StringResponse: + properties: + data: string + + OptionalStringResponse: optional + + WithMetadata: + properties: + metadata: map diff --git a/seed/python-sdk/response-property/.mock/definition/api.yml b/seed/python-sdk/response-property/.mock/definition/api.yml new file mode 100644 index 00000000000..4a1bb4a3045 --- /dev/null +++ b/seed/python-sdk/response-property/.mock/definition/api.yml @@ -0,0 +1 @@ +name: response-property diff --git a/seed/python-sdk/response-property/.mock/definition/service.yml b/seed/python-sdk/response-property/.mock/definition/service.yml new file mode 100644 index 00000000000..15de6ab767f --- /dev/null +++ b/seed/python-sdk/response-property/.mock/definition/service.yml @@ -0,0 +1,81 @@ +imports: + root: __package__.yml + +types: + WithDocs: + properties: + docs: string + + OptionalWithDocs: optional + + Movie: + properties: + id: string + name: string + + Response: + extends: + - root.WithMetadata + - WithDocs + properties: + data: Movie + +service: + auth: false + base-path: "" + endpoints: + getMovie: + method: POST + path: /movie + request: string + response: + type: Response + property: data + + getMovieDocs: + method: POST + path: /movie + request: string + response: + type: Response + property: docs + + getMovieName: + method: POST + path: /movie + request: string + response: + type: root.StringResponse + property: data + + getMovieMetadata: + method: POST + path: /movie + request: string + response: + type: Response + property: metadata + + getOptionalMovie: + method: POST + path: /movie + request: string + response: + type: optional + property: data + + getOptionalMovieDocs: + method: POST + path: /movie + request: string + response: + type: OptionalWithDocs + property: docs + + getOptionalMovieName: + method: POST + path: /movie + request: string + response: + type: root.OptionalStringResponse + property: data diff --git a/seed/python-sdk/response-property/.mock/fern.config.json b/seed/python-sdk/response-property/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/python-sdk/response-property/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/python-sdk/response-property/.mock/generators.yml b/seed/python-sdk/response-property/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/python-sdk/response-property/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/python-sdk/response-property/README.md b/seed/python-sdk/response-property/README.md new file mode 100644 index 00000000000..7b0623c3ad8 --- /dev/null +++ b/seed/python-sdk/response-property/README.md @@ -0,0 +1,134 @@ +# Seed Python Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-SDK%20generated%20by%20Fern-brightgreen)](https://github.com/fern-api/fern) +[![pypi](https://img.shields.io/pypi/v/fern_response-property)](https://pypi.python.org/pypi/fern_response-property) + +The Seed Python library provides convenient access to the Seed API from Python. + +## Installation + +```sh +pip install fern_response-property +``` + +## Usage + +Instantiate and use the client with the following: + +```python +from seed import SeedResponseProperty + +client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) +client.service.get_movie( + request="string", +) +``` + +## Async Client + +The SDK also exports an `async` client so that you can make non-blocking calls to our API. + +```python +import asyncio + +from seed import AsyncSeedResponseProperty + +client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) + + +async def main() -> None: + await client.service.get_movie( + request="string", + ) + + +asyncio.run(main()) +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```python +from seed.core.api_error import ApiError + +try: + client.service.get_movie(...) +except ApiError as e: + print(e.status_code) + print(e.body) +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retriable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retriable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `max_retries` request option to configure this behavior. + +```python +client.service.get_movie(..., { + "max_retries": 1 +}) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. + +```python + +from seed import SeedResponseProperty + +client = SeedResponseProperty( + ..., + timeout=20.0, +) + + +# Override timeout for a specific method +client.service.get_movie(..., { + "timeout_in_seconds": 1 +}) +``` + +### Custom Client + +You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies +and transports. +```python +import httpx +from seed import SeedResponseProperty + +client = SeedResponseProperty( + ..., + httpx_client=httpx.Client( + proxies="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/python-sdk/response-property/pyproject.toml b/seed/python-sdk/response-property/pyproject.toml new file mode 100644 index 00000000000..135de21d070 --- /dev/null +++ b/seed/python-sdk/response-property/pyproject.toml @@ -0,0 +1,57 @@ +[tool.poetry] +name = "fern_response-property" +version = "0.0.1" +description = "" +readme = "README.md" +authors = [] +keywords = [] + +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed" +] +packages = [ + { include = "seed", from = "src"} +] + +[project.urls] +Repository = 'https://github.com/response-property/fern' + +[tool.poetry.dependencies] +python = "^3.8" +httpx = ">=0.21.2" +pydantic = ">= 1.9.2" +pydantic-core = "^2.18.2" +typing_extensions = ">= 4.0.0" + +[tool.poetry.dev-dependencies] +mypy = "1.0.1" +pytest = "^7.4.0" +pytest-asyncio = "^0.23.5" +python-dateutil = "^2.9.0" +types-python-dateutil = "^2.9.0.20240316" + +[tool.pytest.ini_options] +testpaths = [ "tests" ] +asyncio_mode = "auto" + +[tool.mypy] +plugins = ["pydantic.mypy"] + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/seed/python-sdk/response-property/reference.md b/seed/python-sdk/response-property/reference.md new file mode 100644 index 00000000000..04a2a3d063e --- /dev/null +++ b/seed/python-sdk/response-property/reference.md @@ -0,0 +1,394 @@ +# Reference +## Service +
client.service.get_movie(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedResponseProperty + +client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) +client.service.get_movie( + request="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.service.get_movie_docs(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedResponseProperty + +client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) +client.service.get_movie_docs( + request="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.service.get_movie_name(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedResponseProperty + +client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) +client.service.get_movie_name( + request="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.service.get_movie_metadata(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedResponseProperty + +client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) +client.service.get_movie_metadata( + request="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.service.get_optional_movie(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedResponseProperty + +client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) +client.service.get_optional_movie( + request="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.service.get_optional_movie_docs(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedResponseProperty + +client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) +client.service.get_optional_movie_docs( + request="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.service.get_optional_movie_name(...) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedResponseProperty + +client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", +) +client.service.get_optional_movie_name( + request="string", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/seed/python-sdk/response-property/snippet-templates.json b/seed/python-sdk/response-property/snippet-templates.json new file mode 100644 index 00000000000..0076286c97f --- /dev/null +++ b/seed/python-sdk/response-property/snippet-templates.json @@ -0,0 +1,632 @@ +[ + { + "sdk": { + "package": "fern_response-property", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/movie", + "method": "POST", + "identifierOverride": "endpoint_service.getMovie" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.service.get_movie(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.service.get_movie(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_response-property", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/movie", + "method": "POST", + "identifierOverride": "endpoint_service.getMovieDocs" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.service.get_movie_docs(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.service.get_movie_docs(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_response-property", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/movie", + "method": "POST", + "identifierOverride": "endpoint_service.getMovieName" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.service.get_movie_name(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.service.get_movie_name(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_response-property", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/movie", + "method": "POST", + "identifierOverride": "endpoint_service.getMovieMetadata" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.service.get_movie_metadata(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.service.get_movie_metadata(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_response-property", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/movie", + "method": "POST", + "identifierOverride": "endpoint_service.getOptionalMovie" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.service.get_optional_movie(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.service.get_optional_movie(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_response-property", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/movie", + "method": "POST", + "identifierOverride": "endpoint_service.getOptionalMovieDocs" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.service.get_optional_movie_docs(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.service.get_optional_movie_docs(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + }, + { + "sdk": { + "package": "fern_response-property", + "version": "0.0.1", + "type": "python" + }, + "endpointId": { + "path": "/movie", + "method": "POST", + "identifierOverride": "endpoint_service.getOptionalMovieName" + }, + "snippetTemplate": { + "clientInstantiation": { + "imports": [ + "from seed import SeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "client.service.get_optional_movie_name(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + }, + "additionalTemplates": { + "async": { + "clientInstantiation": { + "imports": [ + "from seed import AsyncSeedResponseProperty" + ], + "isOptional": true, + "templateString": "client = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)", + "templateInputs": [], + "inputDelimiter": ",", + "type": "generic" + }, + "functionInvocation": { + "imports": [], + "isOptional": true, + "templateString": "await client.service.get_optional_movie_name(\n\t$FERN_INPUT\n)", + "templateInputs": [ + { + "type": "template", + "value": { + "imports": [], + "isOptional": true, + "templateString": "request=$FERN_INPUT", + "templateInputs": [ + { + "location": "BODY", + "path": null, + "type": "payload" + } + ], + "type": "generic" + } + } + ], + "inputDelimiter": ",\n\t", + "type": "generic" + }, + "type": "v1" + } + } + } +] \ No newline at end of file diff --git a/seed/python-sdk/response-property/snippet.json b/seed/python-sdk/response-property/snippet.json new file mode 100644 index 00000000000..0a309770792 --- /dev/null +++ b/seed/python-sdk/response-property/snippet.json @@ -0,0 +1,96 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": "default", + "id": { + "path": "/movie", + "method": "POST", + "identifier_override": "endpoint_service.getMovie" + }, + "snippet": { + "sync_client": "from seed import SeedResponseProperty\n\nclient = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.service.get_movie(\n request=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedResponseProperty\n\nclient = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.service.get_movie(\n request=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/movie", + "method": "POST", + "identifier_override": "endpoint_service.getMovieDocs" + }, + "snippet": { + "sync_client": "from seed import SeedResponseProperty\n\nclient = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.service.get_movie_docs(\n request=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedResponseProperty\n\nclient = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.service.get_movie_docs(\n request=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/movie", + "method": "POST", + "identifier_override": "endpoint_service.getMovieName" + }, + "snippet": { + "sync_client": "from seed import SeedResponseProperty\n\nclient = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.service.get_movie_name(\n request=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedResponseProperty\n\nclient = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.service.get_movie_name(\n request=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/movie", + "method": "POST", + "identifier_override": "endpoint_service.getMovieMetadata" + }, + "snippet": { + "sync_client": "from seed import SeedResponseProperty\n\nclient = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.service.get_movie_metadata(\n request=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedResponseProperty\n\nclient = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.service.get_movie_metadata(\n request=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/movie", + "method": "POST", + "identifier_override": "endpoint_service.getOptionalMovie" + }, + "snippet": { + "sync_client": "from seed import SeedResponseProperty\n\nclient = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.service.get_optional_movie(\n request=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedResponseProperty\n\nclient = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.service.get_optional_movie(\n request=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/movie", + "method": "POST", + "identifier_override": "endpoint_service.getOptionalMovieDocs" + }, + "snippet": { + "sync_client": "from seed import SeedResponseProperty\n\nclient = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.service.get_optional_movie_docs(\n request=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedResponseProperty\n\nclient = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.service.get_optional_movie_docs(\n request=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/movie", + "method": "POST", + "identifier_override": "endpoint_service.getOptionalMovieName" + }, + "snippet": { + "sync_client": "from seed import SeedResponseProperty\n\nclient = SeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.service.get_optional_movie_name(\n request=\"string\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedResponseProperty\n\nclient = AsyncSeedResponseProperty(\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.service.get_optional_movie_name(\n request=\"string\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + } + ] +} \ No newline at end of file diff --git a/seed/python-sdk/response-property/src/seed/__init__.py b/seed/python-sdk/response-property/src/seed/__init__.py new file mode 100644 index 00000000000..67d1a775d1f --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/__init__.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import OptionalStringResponse, StringResponse, WithMetadata +from . import service +from .client import AsyncSeedResponseProperty, SeedResponseProperty +from .service import Movie, OptionalWithDocs, Response, WithDocs +from .version import __version__ + +__all__ = [ + "AsyncSeedResponseProperty", + "Movie", + "OptionalStringResponse", + "OptionalWithDocs", + "Response", + "SeedResponseProperty", + "StringResponse", + "WithDocs", + "WithMetadata", + "__version__", + "service", +] diff --git a/seed/python-sdk/response-property/src/seed/client.py b/seed/python-sdk/response-property/src/seed/client.py new file mode 100644 index 00000000000..ee82d98022c --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/client.py @@ -0,0 +1,104 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .service.client import AsyncServiceClient, ServiceClient + + +class SeedResponseProperty: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from seed import SeedResponseProperty + + client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + self._client_wrapper = SyncClientWrapper( + base_url=base_url, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.service = ServiceClient(client_wrapper=self._client_wrapper) + + +class AsyncSeedResponseProperty: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from seed import AsyncSeedResponseProperty + + client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + self._client_wrapper = AsyncClientWrapper( + base_url=base_url, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.service = AsyncServiceClient(client_wrapper=self._client_wrapper) diff --git a/seed/python-sdk/response-property/src/seed/core/__init__.py b/seed/python-sdk/response-property/src/seed/core/__init__.py new file mode 100644 index 00000000000..5a0bee343d0 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/__init__.py @@ -0,0 +1,48 @@ +# This file was auto-generated by Fern from our API Definition. + +from .api_error import ApiError +from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper +from .datetime_utils import serialize_datetime +from .file import File, convert_file_dict_to_httpx_tuples +from .http_client import AsyncHttpClient, HttpClient +from .jsonable_encoder import jsonable_encoder +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + deep_union_pydantic_dicts, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, +) +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions +from .serialization import FieldMetadata, convert_and_respect_annotation_metadata + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "BaseClientWrapper", + "FieldMetadata", + "File", + "HttpClient", + "IS_PYDANTIC_V2", + "RequestOptions", + "SyncClientWrapper", + "UniversalBaseModel", + "UniversalRootModel", + "convert_and_respect_annotation_metadata", + "convert_file_dict_to_httpx_tuples", + "deep_union_pydantic_dicts", + "encode_query", + "jsonable_encoder", + "parse_obj_as", + "remove_none_from_dict", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", +] diff --git a/seed/python-sdk/response-property/src/seed/core/api_error.py b/seed/python-sdk/response-property/src/seed/core/api_error.py new file mode 100644 index 00000000000..2e9fc5431cd --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/api_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + + +class ApiError(Exception): + status_code: typing.Optional[int] + body: typing.Any + + def __init__(self, *, status_code: typing.Optional[int] = None, body: typing.Any = None): + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"status_code: {self.status_code}, body: {self.body}" diff --git a/seed/python-sdk/response-property/src/seed/core/client_wrapper.py b/seed/python-sdk/response-property/src/seed/core/client_wrapper.py new file mode 100644 index 00000000000..35303ce1eb4 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/client_wrapper.py @@ -0,0 +1,49 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from .http_client import AsyncHttpClient, HttpClient + + +class BaseClientWrapper: + def __init__(self, *, base_url: str, timeout: typing.Optional[float] = None): + self._base_url = base_url + self._timeout = timeout + + def get_headers(self) -> typing.Dict[str, str]: + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + "X-Fern-SDK-Name": "fern_response-property", + "X-Fern-SDK-Version": "0.0.1", + } + return headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + +class SyncClientWrapper(BaseClientWrapper): + def __init__(self, *, base_url: str, timeout: typing.Optional[float] = None, httpx_client: httpx.Client): + super().__init__(base_url=base_url, timeout=timeout) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers(), + base_timeout=self.get_timeout(), + base_url=self.get_base_url(), + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__(self, *, base_url: str, timeout: typing.Optional[float] = None, httpx_client: httpx.AsyncClient): + super().__init__(base_url=base_url, timeout=timeout) + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers(), + base_timeout=self.get_timeout(), + base_url=self.get_base_url(), + ) diff --git a/seed/python-sdk/response-property/src/seed/core/datetime_utils.py b/seed/python-sdk/response-property/src/seed/core/datetime_utils.py new file mode 100644 index 00000000000..7c9864a944c --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/datetime_utils.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/seed/python-sdk/response-property/src/seed/core/file.py b/seed/python-sdk/response-property/src/seed/core/file.py new file mode 100644 index 00000000000..cb0d40bbbf3 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/file.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = typing.Union[typing.IO[bytes], bytes, str] +File = typing.Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + typing.Tuple[typing.Optional[str], FileContent], + # (filename, file (or bytes), content_type) + typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str]], + # (filename, file (or bytes), content_type, headers) + typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str], typing.Mapping[str, str]], +] + + +def convert_file_dict_to_httpx_tuples( + d: typing.Dict[str, typing.Union[File, typing.List[File]]] +) -> typing.List[typing.Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples diff --git a/seed/python-sdk/response-property/src/seed/core/http_client.py b/seed/python-sdk/response-property/src/seed/core/http_client.py new file mode 100644 index 00000000000..356880bbc3e --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/http_client.py @@ -0,0 +1,476 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import json +import re +import time +import typing +import urllib.parse +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx + +from .file import File, convert_file_dict_to_httpx_tuples +from .jsonable_encoder import jsonable_encoder +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions + +INITIAL_RETRY_DELAY_SECONDS = 0.5 +MAX_RETRY_DELAY_SECONDS = 10 +MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: + return retry_after + + # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. + retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + + # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. + timeout = retry_delay * (1 - 0.25 * random()) + return timeout if timeout >= 0 else 0 + + +def _should_retry(response: httpx.Response) -> bool: + retriable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retriable_400s + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], omit: typing.Optional[typing.Any] +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # 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) + + # 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: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Optional[float], + base_headers: typing.Dict[str, str], + base_url: typing.Optional[str] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = self.base_url if maybe_base_url is None else maybe_base_url + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + response = self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Optional[float], + base_headers: typing.Dict[str, str], + base_url: typing.Optional[str] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = self.base_url if maybe_base_url is None else maybe_base_url + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + # Add the input to each of these and do None-safety checks + response = await self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]]] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + async with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + timeout=timeout, + ) as stream: + yield stream diff --git a/seed/python-sdk/response-property/src/seed/core/jsonable_encoder.py b/seed/python-sdk/response-property/src/seed/core/jsonable_encoder.py new file mode 100644 index 00000000000..d3fd328fd41 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/jsonable_encoder.py @@ -0,0 +1,97 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union + +import pydantic + +from .datetime_utils import serialize_datetime +from .pydantic_utilities import IS_PYDANTIC_V2, encode_by_type, to_jsonable_with_fallback + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) diff --git a/seed/python-sdk/response-property/src/seed/core/pydantic_utilities.py b/seed/python-sdk/response-property/src/seed/core/pydantic_utilities.py new file mode 100644 index 00000000000..d7fb87bf581 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/pydantic_utilities.py @@ -0,0 +1,179 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +import typing +from collections import defaultdict +from functools import wraps + +import pydantic + +from .datetime_utils import serialize_datetime + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + # isort will try to reformat the comments on these imports, which breaks mypy + # isort: off + from pydantic.v1.datetime_parse import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + parse_date as parse_date, + ) + from pydantic.v1.datetime_parse import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + parse_datetime as parse_datetime, + ) + from pydantic.v1.json import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + ENCODERS_BY_TYPE as encoders_by_type, + ) + from pydantic.v1.typing import ( # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 + get_args as get_args, + ) + from pydantic.v1.typing import get_origin as get_origin # pyright: ignore[reportMissingImports] # Pydantic v2 + from pydantic.v1.typing import ( # pyright: ignore[reportMissingImports] # Pydantic v2 + is_literal_type as is_literal_type, + ) + from pydantic.v1.typing import is_union as is_union # pyright: ignore[reportMissingImports] # Pydantic v2 + from pydantic.v1.fields import ModelField as ModelField # type: ignore # pyright: ignore[reportMissingImports] # Pydantic v2 +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore # Pydantic v1 + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore # Pydantic v1 + from pydantic.fields import ModelField as ModelField # type: ignore # Pydantic v1 + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore # Pydantic v1 + from pydantic.typing import get_args as get_args # type: ignore # Pydantic v1 + from pydantic.typing import get_origin as get_origin # type: ignore # Pydantic v1 + from pydantic.typing import is_literal_type as is_literal_type # type: ignore # Pydantic v1 + from pydantic.typing import is_union as is_union # type: ignore # Pydantic v1 + + # isort: on + + +T = typing.TypeVar("T") +Model = typing.TypeVar("Model", bound=pydantic.BaseModel) + + +def deep_union_pydantic_dicts( + source: typing.Dict[str, typing.Any], destination: typing.Dict[str, typing.Any] +) -> typing.Dict[str, typing.Any]: + for key, value in source.items(): + if isinstance(value, dict): + node = destination.setdefault(key, {}) + deep_union_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +def parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T: + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore # Pydantic v2 + return adapter.validate_python(object_) + else: + return pydantic.parse_obj_as(type_, object_) + + +def to_jsonable_with_fallback( + obj: typing.Any, fallback_serializer: typing.Callable[[typing.Any], typing.Any] +) -> typing.Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + else: + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + class Config: + populate_by_name = True + smart_union = True + allow_population_by_field_name = True + json_encoders = {dt.datetime: serialize_datetime} + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore # Pydantic v2 + else: + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + kwargs_with_defaults_exclude_none: typing.Any = {"by_alias": True, "exclude_none": True, **kwargs} + + if IS_PYDANTIC_V2: + return deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore # Pydantic v2 + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore # Pydantic v2 + ) + else: + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), super().dict(**kwargs_with_defaults_exclude_none) + ) + + +UniversalRootModel: typing.Type[pydantic.BaseModel] +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore # Pydantic v2 + pass + + UniversalRootModel = V2RootModel +else: + UniversalRootModel = UniversalBaseModel + + +def encode_by_type(o: typing.Any) -> typing.Any: + encoders_by_class_tuples: typing.Dict[ + typing.Callable[[typing.Any], typing.Any], typing.Tuple[typing.Any, ...] + ] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: typing.Type["Model"], **localns: typing.Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(force=True, raise_errors=False) # type: ignore # Pydantic v2 + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = typing.Callable[..., typing.Any] + + +def universal_root_validator(pre: bool = False) -> typing.Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + @wraps(func) + def validate(*args: typing.Any, **kwargs: typing.Any) -> AnyCallable: + if IS_PYDANTIC_V2: + wrapped_func = pydantic.model_validator("before" if pre else "after")(func) # type: ignore # Pydantic v2 + else: + wrapped_func = pydantic.root_validator(pre=pre)(func) # type: ignore # Pydantic v1 + + return wrapped_func(*args, **kwargs) # type: ignore # Pydantic v2 + + return validate + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> typing.Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + @wraps(func) + def validate(*args: typing.Any, **kwargs: typing.Any) -> AnyCallable: + if IS_PYDANTIC_V2: + wrapped_func = pydantic.field_validator(field_name, mode="before" if pre else "after")(func) # type: ignore # Pydantic v2 + else: + wrapped_func = pydantic.validator(field_name, pre=pre)(func) # type: ignore # Pydantic v1 + + return wrapped_func(*args, **kwargs) + + return validate + + return decorator diff --git a/seed/python-sdk/response-property/src/seed/core/query_encoder.py b/seed/python-sdk/response-property/src/seed/core/query_encoder.py new file mode 100644 index 00000000000..24076d72ee9 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/query_encoder.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections import ChainMap +from typing import Any, Dict, Optional + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> Dict[str, Any]: + result = {} + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.update(traverse_query_dict(v, key)) + else: + result[key] = v + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> Dict[str, Any]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + + return {query_key: query_value} + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + return dict(ChainMap(*[single_query_encoder(k, v) for k, v in query.items()])) if query is not None else None diff --git a/seed/python-sdk/response-property/src/seed/core/remove_none_from_dict.py b/seed/python-sdk/response-property/src/seed/core/remove_none_from_dict.py new file mode 100644 index 00000000000..c2298143f14 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/seed/python-sdk/response-property/src/seed/core/request_options.py b/seed/python-sdk/response-property/src/seed/core/request_options.py new file mode 100644 index 00000000000..d0bf0dbcecd --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/request_options.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + """ + + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] diff --git a/seed/python-sdk/response-property/src/seed/core/serialization.py b/seed/python-sdk/response-property/src/seed/core/serialization.py new file mode 100644 index 00000000000..8ad5cf8125f --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/serialization.py @@ -0,0 +1,167 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import typing + +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, object_: typing.Any, annotation: typing.Any, inner_type: typing.Optional[typing.Any] = None +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_typeddict(object_, clean_type) + + if ( + # If you're iterating on a string, do not bother to coerce it to a sequence. + (not isinstance(object_, str)) + and ( + ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) + or ( + ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) + and isinstance(object_, typing.Set) + ) + or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ) + ) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata(object_=item, annotation=annotation, inner_type=inner_type) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata(object_=object_, annotation=annotation, inner_type=member) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_typeddict(object_: typing.Mapping[str, object], expected_type: typing.Any) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + for key, value in object_.items(): + type_ = annotations.get(key) + if type_ is None: + converted_object[key] = value + else: + converted_object[_alias_key(key, type_)] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_ + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def _alias_key(key: str, type_: typing.Any) -> str: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + + return key diff --git a/seed/python-sdk/response-property/src/seed/py.typed b/seed/python-sdk/response-property/src/seed/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/python-sdk/response-property/src/seed/service/__init__.py b/seed/python-sdk/response-property/src/seed/service/__init__.py new file mode 100644 index 00000000000..dd885d02ab5 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/service/__init__.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import Movie, OptionalWithDocs, Response, WithDocs + +__all__ = ["Movie", "OptionalWithDocs", "Response", "WithDocs"] diff --git a/seed/python-sdk/response-property/src/seed/service/client.py b/seed/python-sdk/response-property/src/seed/service/client.py new file mode 100644 index 00000000000..645bf18eb5e --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/service/client.py @@ -0,0 +1,605 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..types.optional_string_response import OptionalStringResponse +from ..types.string_response import StringResponse +from .types.movie import Movie +from .types.optional_with_docs import OptionalWithDocs +from .types.response import Response + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ServiceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_movie(self, *, request: str, request_options: typing.Optional[RequestOptions] = None) -> Movie: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Movie + + Examples + -------- + from seed import SeedResponseProperty + + client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + client.service.get_movie( + request="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(Response, parse_obj_as(type_=Response, object_=_response.json())) # type: ignore + + return _parsed_response.data + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_movie_docs(self, *, request: str, request_options: typing.Optional[RequestOptions] = None) -> str: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + str + + Examples + -------- + from seed import SeedResponseProperty + + client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + client.service.get_movie_docs( + request="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(Response, parse_obj_as(type_=Response, object_=_response.json())) # type: ignore + + return _parsed_response.docs + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_movie_name(self, *, request: str, request_options: typing.Optional[RequestOptions] = None) -> str: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + str + + Examples + -------- + from seed import SeedResponseProperty + + client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + client.service.get_movie_name( + request="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(StringResponse, parse_obj_as(type_=StringResponse, object_=_response.json())) # type: ignore + + return _parsed_response.data + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_movie_metadata( + self, *, request: str, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Dict[str, str]: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.Dict[str, str] + + Examples + -------- + from seed import SeedResponseProperty + + client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + client.service.get_movie_metadata( + request="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(Response, parse_obj_as(type_=Response, object_=_response.json())) # type: ignore + + return _parsed_response.metadata + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_optional_movie( + self, *, request: str, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Optional[Movie]: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.Optional[Movie] + + Examples + -------- + from seed import SeedResponseProperty + + client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + client.service.get_optional_movie( + request="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(typing.Optional[Response], parse_obj_as(type_=typing.Optional[Response], object_=_response.json())) # type: ignore + + return _parsed_response.data if _parsed_response is not None else _parsed_response + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_optional_movie_docs(self, *, request: str, request_options: typing.Optional[RequestOptions] = None) -> str: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + str + + Examples + -------- + from seed import SeedResponseProperty + + client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + client.service.get_optional_movie_docs( + request="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast(OptionalWithDocs, parse_obj_as(type_=OptionalWithDocs, object_=_response.json())) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def get_optional_movie_name(self, *, request: str, request_options: typing.Optional[RequestOptions] = None) -> str: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + str + + Examples + -------- + from seed import SeedResponseProperty + + client = SeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + client.service.get_optional_movie_name( + request="string", + ) + """ + _response = self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast(OptionalStringResponse, parse_obj_as(type_=OptionalStringResponse, object_=_response.json())) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +class AsyncServiceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_movie(self, *, request: str, request_options: typing.Optional[RequestOptions] = None) -> Movie: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Movie + + Examples + -------- + import asyncio + + from seed import AsyncSeedResponseProperty + + client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.service.get_movie( + request="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(Response, parse_obj_as(type_=Response, object_=_response.json())) # type: ignore + + return _parsed_response.data + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_movie_docs(self, *, request: str, request_options: typing.Optional[RequestOptions] = None) -> str: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + str + + Examples + -------- + import asyncio + + from seed import AsyncSeedResponseProperty + + client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.service.get_movie_docs( + request="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(Response, parse_obj_as(type_=Response, object_=_response.json())) # type: ignore + + return _parsed_response.docs + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_movie_name(self, *, request: str, request_options: typing.Optional[RequestOptions] = None) -> str: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + str + + Examples + -------- + import asyncio + + from seed import AsyncSeedResponseProperty + + client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.service.get_movie_name( + request="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(StringResponse, parse_obj_as(type_=StringResponse, object_=_response.json())) # type: ignore + + return _parsed_response.data + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_movie_metadata( + self, *, request: str, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Dict[str, str]: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.Dict[str, str] + + Examples + -------- + import asyncio + + from seed import AsyncSeedResponseProperty + + client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.service.get_movie_metadata( + request="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(Response, parse_obj_as(type_=Response, object_=_response.json())) # type: ignore + + return _parsed_response.metadata + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_optional_movie( + self, *, request: str, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Optional[Movie]: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.Optional[Movie] + + Examples + -------- + import asyncio + + from seed import AsyncSeedResponseProperty + + client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.service.get_optional_movie( + request="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + _parsed_response = typing.cast(typing.Optional[Response], parse_obj_as(type_=typing.Optional[Response], object_=_response.json())) # type: ignore + + return _parsed_response.data if _parsed_response is not None else _parsed_response + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_optional_movie_docs( + self, *, request: str, request_options: typing.Optional[RequestOptions] = None + ) -> str: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + str + + Examples + -------- + import asyncio + + from seed import AsyncSeedResponseProperty + + client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.service.get_optional_movie_docs( + request="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast(OptionalWithDocs, parse_obj_as(type_=OptionalWithDocs, object_=_response.json())) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def get_optional_movie_name( + self, *, request: str, request_options: typing.Optional[RequestOptions] = None + ) -> str: + """ + Parameters + ---------- + request : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + str + + Examples + -------- + import asyncio + + from seed import AsyncSeedResponseProperty + + client = AsyncSeedResponseProperty( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.service.get_optional_movie_name( + request="string", + ) + + + asyncio.run(main()) + """ + _response = await self._client_wrapper.httpx_client.request( + "movie", method="POST", json=request, request_options=request_options, omit=OMIT + ) + try: + if 200 <= _response.status_code < 300: + return typing.cast(OptionalStringResponse, parse_obj_as(type_=OptionalStringResponse, object_=_response.json())) # type: ignore + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) diff --git a/seed/python-sdk/response-property/src/seed/service/types/__init__.py b/seed/python-sdk/response-property/src/seed/service/types/__init__.py new file mode 100644 index 00000000000..c856a2a230c --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/service/types/__init__.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +from .movie import Movie +from .optional_with_docs import OptionalWithDocs +from .response import Response +from .with_docs import WithDocs + +__all__ = ["Movie", "OptionalWithDocs", "Response", "WithDocs"] diff --git a/seed/python-sdk/response-property/src/seed/service/types/movie.py b/seed/python-sdk/response-property/src/seed/service/types/movie.py new file mode 100644 index 00000000000..c6fc813b7d5 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/service/types/movie.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic + +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class Movie(UniversalBaseModel): + id: str + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/response-property/src/seed/service/types/optional_with_docs.py b/seed/python-sdk/response-property/src/seed/service/types/optional_with_docs.py new file mode 100644 index 00000000000..66a04dd87ae --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/service/types/optional_with_docs.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .with_docs import WithDocs + +OptionalWithDocs = typing.Optional[WithDocs] diff --git a/seed/python-sdk/response-property/src/seed/service/types/response.py b/seed/python-sdk/response-property/src/seed/service/types/response.py new file mode 100644 index 00000000000..4a2c3672cee --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/service/types/response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic + +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...types.with_metadata import WithMetadata +from .movie import Movie +from .with_docs import WithDocs + + +class Response(WithMetadata, WithDocs): + data: Movie + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/response-property/src/seed/service/types/with_docs.py b/seed/python-sdk/response-property/src/seed/service/types/with_docs.py new file mode 100644 index 00000000000..07945cf00c3 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/service/types/with_docs.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic + +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class WithDocs(UniversalBaseModel): + docs: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/response-property/src/seed/types/__init__.py b/seed/python-sdk/response-property/src/seed/types/__init__.py new file mode 100644 index 00000000000..218937165a0 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/types/__init__.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +from .optional_string_response import OptionalStringResponse +from .string_response import StringResponse +from .with_metadata import WithMetadata + +__all__ = ["OptionalStringResponse", "StringResponse", "WithMetadata"] diff --git a/seed/python-sdk/response-property/src/seed/types/optional_string_response.py b/seed/python-sdk/response-property/src/seed/types/optional_string_response.py new file mode 100644 index 00000000000..587d8cb4304 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/types/optional_string_response.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .string_response import StringResponse + +OptionalStringResponse = typing.Optional[StringResponse] diff --git a/seed/python-sdk/response-property/src/seed/types/string_response.py b/seed/python-sdk/response-property/src/seed/types/string_response.py new file mode 100644 index 00000000000..e7a1a52ab60 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/types/string_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic + +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class StringResponse(UniversalBaseModel): + data: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/response-property/src/seed/types/with_metadata.py b/seed/python-sdk/response-property/src/seed/types/with_metadata.py new file mode 100644 index 00000000000..8e35717387b --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/types/with_metadata.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic + +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class WithMetadata(UniversalBaseModel): + metadata: typing.Dict[str, str] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/response-property/src/seed/version.py b/seed/python-sdk/response-property/src/seed/version.py new file mode 100644 index 00000000000..f3da3e93291 --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/version.py @@ -0,0 +1,4 @@ + +from importlib import metadata + +__version__ = metadata.version("fern_response-property") diff --git a/seed/python-sdk/response-property/tests/__init__.py b/seed/python-sdk/response-property/tests/__init__.py new file mode 100644 index 00000000000..f3ea2659bb1 --- /dev/null +++ b/seed/python-sdk/response-property/tests/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/seed/python-sdk/response-property/tests/conftest.py b/seed/python-sdk/response-property/tests/conftest.py new file mode 100644 index 00000000000..86df378ef4f --- /dev/null +++ b/seed/python-sdk/response-property/tests/conftest.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import os + +import pytest +from seed import AsyncSeedResponseProperty, SeedResponseProperty + + +@pytest.fixture +def client() -> SeedResponseProperty: + return SeedResponseProperty(base_url=os.getenv("TESTS_BASE_URL", "base_url")) + + +@pytest.fixture +def async_client() -> AsyncSeedResponseProperty: + return AsyncSeedResponseProperty(base_url=os.getenv("TESTS_BASE_URL", "base_url")) diff --git a/seed/python-sdk/response-property/tests/custom/test_client.py b/seed/python-sdk/response-property/tests/custom/test_client.py new file mode 100644 index 00000000000..60a58e64c27 --- /dev/null +++ b/seed/python-sdk/response-property/tests/custom/test_client.py @@ -0,0 +1,6 @@ +import pytest + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True == True diff --git a/seed/python-sdk/response-property/tests/utilities.py b/seed/python-sdk/response-property/tests/utilities.py new file mode 100644 index 00000000000..13da208eb3a --- /dev/null +++ b/seed/python-sdk/response-property/tests/utilities.py @@ -0,0 +1,138 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +import uuid + +import pydantic +from dateutil import parser + + +def cast_field(json_expectation: typing.Any, type_expectation: typing.Any) -> typing.Any: + # Cast these specific types which come through as string and expect our + # models to cast to the correct type. + if type_expectation == "uuid": + return uuid.UUID(json_expectation) + elif type_expectation == "date": + return parser.parse(json_expectation).date() + elif type_expectation == "datetime": + return parser.parse(json_expectation) + elif type_expectation == "set": + return set(json_expectation) + elif type_expectation == "integer": + # Necessary as we allow numeric keys, but JSON makes them strings + return int(json_expectation) + + return json_expectation + + +def validate_field(response: typing.Any, json_expectation: typing.Any, type_expectation: typing.Any) -> None: + # Allow for an escape hatch if the object cannot be validated + if type_expectation == "no_validate": + return + + is_container_of_complex_type = False + # Parse types in containers, note that dicts are handled within `validate_response` + if isinstance(json_expectation, list): + if isinstance(type_expectation, tuple): + container_expectation = type_expectation[0] + contents_expectation = type_expectation[1] + + cast_json_expectation = [] + for idx, ex in enumerate(json_expectation): + if isinstance(contents_expectation, dict): + entry_expectation = contents_expectation.get(idx) + if isinstance(entry_expectation, dict): + is_container_of_complex_type = True + validate_response( + response=response[idx], json_expectation=ex, type_expectations=entry_expectation + ) + else: + cast_json_expectation.append(cast_field(ex, entry_expectation)) + else: + cast_json_expectation.append(ex) + json_expectation = cast_json_expectation + + # Note that we explicitly do not allow for sets of pydantic models as they are not hashable, so + # if any of the values of the set have a type_expectation of a dict, we're assuming it's a pydantic + # model and keeping it a list. + if container_expectation != "set" or not any( + map(lambda value: isinstance(value, dict), list(contents_expectation.values())) + ): + json_expectation = cast_field(json_expectation, container_expectation) + elif isinstance(type_expectation, tuple): + container_expectation = type_expectation[0] + contents_expectation = type_expectation[1] + if isinstance(contents_expectation, dict): + json_expectation = { + cast_field( + key, contents_expectation.get(idx)[0] if contents_expectation.get(idx) is not None else None # type: ignore + ): cast_field( + value, contents_expectation.get(idx)[1] if contents_expectation.get(idx) is not None else None # type: ignore + ) + for idx, (key, value) in enumerate(json_expectation.items()) + } + else: + json_expectation = cast_field(json_expectation, container_expectation) + elif type_expectation is not None: + json_expectation = cast_field(json_expectation, type_expectation) + + # When dealing with containers of models, etc. we're validating them implicitly, so no need to check the resultant list + if not is_container_of_complex_type: + assert ( + json_expectation == response + ), "Primitives found, expected: {0} (type: {1}), Actual: {2} (type: {3})".format( + json_expectation, type(json_expectation), response, type(response) + ) + + +# Arg type_expectations is a deeply nested structure that matches the response, but with the values replaced with the expected types +def validate_response(response: typing.Any, json_expectation: typing.Any, type_expectations: typing.Any) -> None: + # Allow for an escape hatch if the object cannot be validated + if type_expectations == "no_validate": + return + + if ( + not isinstance(response, list) + and not isinstance(response, dict) + and not issubclass(type(response), pydantic.BaseModel) + ): + validate_field(response=response, json_expectation=json_expectation, type_expectation=type_expectations) + return + + if isinstance(response, list): + assert len(response) == len(json_expectation), "Length mismatch, expected: {0}, Actual: {1}".format( + len(response), len(json_expectation) + ) + content_expectation = type_expectations + if isinstance(type_expectations, tuple): + content_expectation = type_expectations[1] + for idx, item in enumerate(response): + validate_response( + response=item, json_expectation=json_expectation[idx], type_expectations=content_expectation[idx] + ) + else: + response_json = response + if issubclass(type(response), pydantic.BaseModel): + response_json = response.dict(by_alias=True) + + for key, value in json_expectation.items(): + assert key in response_json, "Field {0} not found within the response object: {1}".format( + key, response_json + ) + + type_expectation = None + if type_expectations is not None and isinstance(type_expectations, dict): + type_expectation = type_expectations.get(key) + + # If your type_expectation is a tuple then you have a container field, process it as such + # Otherwise, we're just validating a single field that's a pydantic model. + if isinstance(value, dict) and not isinstance(type_expectation, tuple): + validate_response( + response=response_json[key], json_expectation=value, type_expectations=type_expectation + ) + else: + validate_field(response=response_json[key], json_expectation=value, type_expectation=type_expectation) + + # Ensure there are no additional fields here either + del response_json[key] + assert len(response_json) == 0, "Additional fields found, expected None: {0}".format(response_json) diff --git a/seed/python-sdk/response-property/tests/utils/__init__.py b/seed/python-sdk/response-property/tests/utils/__init__.py new file mode 100644 index 00000000000..f3ea2659bb1 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/seed/python-sdk/response-property/tests/utils/assets/models/__init__.py b/seed/python-sdk/response-property/tests/utils/assets/models/__init__.py new file mode 100644 index 00000000000..2cf01263529 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/assets/models/__init__.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from .circle import CircleParams +from .object_with_defaults import ObjectWithDefaultsParams +from .object_with_optional_field import ObjectWithOptionalFieldParams +from .shape import Shape_CircleParams, Shape_SquareParams, ShapeParams +from .square import SquareParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +__all__ = [ + "CircleParams", + "ObjectWithDefaultsParams", + "ObjectWithOptionalFieldParams", + "ShapeParams", + "Shape_CircleParams", + "Shape_SquareParams", + "SquareParams", + "UndiscriminatedShapeParams", +] diff --git a/seed/python-sdk/response-property/tests/utils/assets/models/circle.py b/seed/python-sdk/response-property/tests/utils/assets/models/circle.py new file mode 100644 index 00000000000..af7a1bf8a8e --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/assets/models/circle.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from seed.core.serialization import FieldMetadata + + +class CircleParams(typing_extensions.TypedDict): + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] diff --git a/seed/python-sdk/response-property/tests/utils/assets/models/color.py b/seed/python-sdk/response-property/tests/utils/assets/models/color.py new file mode 100644 index 00000000000..2aa2c4c52f0 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/assets/models/color.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +Color = typing.Union[typing.Literal["red", "blue"], typing.Any] diff --git a/seed/python-sdk/response-property/tests/utils/assets/models/object_with_defaults.py b/seed/python-sdk/response-property/tests/utils/assets/models/object_with_defaults.py new file mode 100644 index 00000000000..a977b1d2aa1 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/assets/models/object_with_defaults.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ObjectWithDefaultsParams(typing_extensions.TypedDict): + """ + Defines properties with default values and validation rules. + """ + + decimal: typing_extensions.NotRequired[float] + string: typing_extensions.NotRequired[str] + required_string: str diff --git a/seed/python-sdk/response-property/tests/utils/assets/models/object_with_optional_field.py b/seed/python-sdk/response-property/tests/utils/assets/models/object_with_optional_field.py new file mode 100644 index 00000000000..3ad93d5f305 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/assets/models/object_with_optional_field.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +import uuid + +import typing_extensions +from seed.core.serialization import FieldMetadata + +from .color import Color +from .shape import ShapeParams +from .undiscriminated_shape import UndiscriminatedShapeParams + + +class ObjectWithOptionalFieldParams(typing_extensions.TypedDict): + literal: typing.Literal["lit_one"] + string: typing_extensions.NotRequired[str] + integer: typing_extensions.NotRequired[int] + long_: typing_extensions.NotRequired[typing_extensions.Annotated[int, FieldMetadata(alias="long")]] + double: typing_extensions.NotRequired[float] + bool_: typing_extensions.NotRequired[typing_extensions.Annotated[bool, FieldMetadata(alias="bool")]] + datetime: typing_extensions.NotRequired[dt.datetime] + date: typing_extensions.NotRequired[dt.date] + uuid_: typing_extensions.NotRequired[typing_extensions.Annotated[uuid.UUID, FieldMetadata(alias="uuid")]] + base_64: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="base64")]] + list_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Sequence[str], FieldMetadata(alias="list")]] + set_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Set[str], FieldMetadata(alias="set")]] + map_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Dict[int, str], FieldMetadata(alias="map")]] + enum: typing_extensions.NotRequired[Color] + union: typing_extensions.NotRequired[ShapeParams] + second_union: typing_extensions.NotRequired[ShapeParams] + undiscriminated_union: typing_extensions.NotRequired[UndiscriminatedShapeParams] + any: typing.Any diff --git a/seed/python-sdk/response-property/tests/utils/assets/models/shape.py b/seed/python-sdk/response-property/tests/utils/assets/models/shape.py new file mode 100644 index 00000000000..2c33c877951 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/assets/models/shape.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions +from seed.core.serialization import FieldMetadata + + +class Base(typing_extensions.TypedDict): + id: str + + +class Shape_CircleParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["circle"], FieldMetadata(alias="shapeType")] + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] + + +class Shape_SquareParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["square"], FieldMetadata(alias="shapeType")] + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] + + +ShapeParams = typing.Union[Shape_CircleParams, Shape_SquareParams] diff --git a/seed/python-sdk/response-property/tests/utils/assets/models/square.py b/seed/python-sdk/response-property/tests/utils/assets/models/square.py new file mode 100644 index 00000000000..b9b7dd319bc --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/assets/models/square.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from seed.core.serialization import FieldMetadata + + +class SquareParams(typing_extensions.TypedDict): + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] diff --git a/seed/python-sdk/response-property/tests/utils/assets/models/undiscriminated_shape.py b/seed/python-sdk/response-property/tests/utils/assets/models/undiscriminated_shape.py new file mode 100644 index 00000000000..99f12b300d1 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/assets/models/undiscriminated_shape.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .circle import CircleParams +from .square import SquareParams + +UndiscriminatedShapeParams = typing.Union[CircleParams, SquareParams] diff --git a/seed/python-sdk/response-property/tests/utils/test_http_client.py b/seed/python-sdk/response-property/tests/utils/test_http_client.py new file mode 100644 index 00000000000..a541bae6531 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/test_http_client.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed.core.http_client import get_request_body +from seed.core.request_options import RequestOptions + + +def get_request_options() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later"}} + + +def test_get_json_request_body() -> None: + json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) + assert json_body == {"hello": "world"} + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={"goodbye": "world"}, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"goodbye": "world", "see you": "later"} + assert data_body_extras is None + + +def test_get_files_request_body() -> None: + json_body, data_body = get_request_body(json=None, data={"hello": "world"}, request_options=None, omit=None) + assert data_body == {"hello": "world"} + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data={"goodbye": "world"}, request_options=get_request_options(), omit=None + ) + + assert data_body_extras == {"goodbye": "world", "see you": "later"} + assert json_body_extras is None + + +def test_get_none_request_body() -> None: + json_body, data_body = get_request_body(json=None, data=None, request_options=None, omit=None) + assert data_body is None + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data=None, request_options=get_request_options(), omit=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 diff --git a/seed/python-sdk/response-property/tests/utils/test_query_encoding.py b/seed/python-sdk/response-property/tests/utils/test_query_encoding.py new file mode 100644 index 00000000000..247e1551b46 --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/test_query_encoding.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed.core.query_encoder import encode_query + + +def test_query_encoding() -> None: + assert encode_query({"hello world": "hello world"}) == {"hello world": "hello world"} + assert encode_query({"hello_world": {"hello": "world"}}) == {"hello_world[hello]": "world"} + assert encode_query({"hello_world": {"hello": {"world": "today"}, "test": "this"}, "hi": "there"}) == { + "hello_world[hello][world]": "today", + "hello_world[test]": "this", + "hi": "there", + } + + +def test_encode_query_with_none() -> None: + encoded = encode_query(None) + assert encoded == None diff --git a/seed/python-sdk/response-property/tests/utils/test_serialization.py b/seed/python-sdk/response-property/tests/utils/test_serialization.py new file mode 100644 index 00000000000..58b1ed66e6d --- /dev/null +++ b/seed/python-sdk/response-property/tests/utils/test_serialization.py @@ -0,0 +1,66 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, List + +from seed.core.serialization import convert_and_respect_annotation_metadata + +from .assets.models import ObjectWithOptionalFieldParams, ShapeParams + +UNION_TEST: ShapeParams = {"radius_measurement": 1.0, "shape_type": "circle", "id": "1"} +UNION_TEST_CONVERTED = {"shapeType": "circle", "radiusMeasurement": 1.0, "id": "1"} + + +def test_convert_and_respect_annotation_metadata() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "bool_": True, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata(object_=data, annotation=ObjectWithOptionalFieldParams) + assert converted == {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"} + + +def test_convert_and_respect_annotation_metadata_in_list() -> None: + data: List[ObjectWithOptionalFieldParams] = [ + {"string": "string", "long_": 12345, "bool_": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long_": 67890, "list_": [], "literal": "lit_one", "any": "any"}, + ] + converted = convert_and_respect_annotation_metadata(object_=data, annotation=List[ObjectWithOptionalFieldParams]) + + assert converted == [ + {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long": 67890, "list": [], "literal": "lit_one", "any": "any"}, + ] + + +def test_convert_and_respect_annotation_metadata_in_nested_object() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "union": UNION_TEST, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata(object_=data, annotation=ObjectWithOptionalFieldParams) + + assert converted == { + "string": "string", + "long": 12345, + "union": UNION_TEST_CONVERTED, + "literal": "lit_one", + "any": "any", + } + + +def test_convert_and_respect_annotation_metadata_in_union() -> None: + converted = convert_and_respect_annotation_metadata(object_=UNION_TEST, annotation=ShapeParams) + + assert converted == UNION_TEST_CONVERTED + + +def test_convert_and_respect_annotation_metadata_with_empty_object() -> None: + data: Any = {} + converted = convert_and_respect_annotation_metadata(object_=data, annotation=ShapeParams) + assert converted == data diff --git a/seed/python-sdk/seed.yml b/seed/python-sdk/seed.yml index 77f972aa466..3520e8dcf20 100644 --- a/seed/python-sdk/seed.yml +++ b/seed/python-sdk/seed.yml @@ -177,7 +177,6 @@ allowedFailures: - exhaustive:pydantic-v1-wrapped # TODO(FER-1837): Address this list - - response-property - websocket - exhaustive:union-utils - extra-properties diff --git a/seed/python-sdk/server-sent-events/src/seed/core/http_client.py b/seed/python-sdk/server-sent-events/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/server-sent-events/src/seed/core/http_client.py +++ b/seed/python-sdk/server-sent-events/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/server-sent-events/tests/utils/test_http_client.py b/seed/python-sdk/server-sent-events/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/server-sent-events/tests/utils/test_http_client.py +++ b/seed/python-sdk/server-sent-events/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/single-url-environment-default/src/seed/core/http_client.py b/seed/python-sdk/single-url-environment-default/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/single-url-environment-default/src/seed/core/http_client.py +++ b/seed/python-sdk/single-url-environment-default/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/single-url-environment-default/tests/utils/test_http_client.py b/seed/python-sdk/single-url-environment-default/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/single-url-environment-default/tests/utils/test_http_client.py +++ b/seed/python-sdk/single-url-environment-default/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_client.py b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_client.py +++ b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/single-url-environment-no-default/tests/utils/test_http_client.py b/seed/python-sdk/single-url-environment-no-default/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/single-url-environment-no-default/tests/utils/test_http_client.py +++ b/seed/python-sdk/single-url-environment-no-default/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/streaming-parameter/src/seed/core/http_client.py b/seed/python-sdk/streaming-parameter/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/streaming-parameter/src/seed/core/http_client.py +++ b/seed/python-sdk/streaming-parameter/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/streaming-parameter/tests/utils/test_http_client.py b/seed/python-sdk/streaming-parameter/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/streaming-parameter/tests/utils/test_http_client.py +++ b/seed/python-sdk/streaming-parameter/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_client.py b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_client.py +++ b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/streaming/no-custom-config/tests/utils/test_http_client.py b/seed/python-sdk/streaming/no-custom-config/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/streaming/no-custom-config/tests/utils/test_http_client.py +++ b/seed/python-sdk/streaming/no-custom-config/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_client.py b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_client.py +++ b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/tests/utils/test_http_client.py b/seed/python-sdk/streaming/skip-pydantic-validation/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/streaming/skip-pydantic-validation/tests/utils/test_http_client.py +++ b/seed/python-sdk/streaming/skip-pydantic-validation/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/undiscriminated-unions/src/seed/core/http_client.py b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/undiscriminated-unions/src/seed/core/http_client.py +++ b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/undiscriminated-unions/tests/utils/test_http_client.py b/seed/python-sdk/undiscriminated-unions/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/undiscriminated-unions/tests/utils/test_http_client.py +++ b/seed/python-sdk/undiscriminated-unions/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/unions/no-custom-config/src/seed/core/http_client.py b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/unions/no-custom-config/src/seed/core/http_client.py +++ b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/unions/no-custom-config/tests/utils/test_http_client.py b/seed/python-sdk/unions/no-custom-config/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/unions/no-custom-config/tests/utils/test_http_client.py +++ b/seed/python-sdk/unions/no-custom-config/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/unions/union-utils/src/seed/core/http_client.py b/seed/python-sdk/unions/union-utils/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/unions/union-utils/src/seed/core/http_client.py +++ b/seed/python-sdk/unions/union-utils/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/unions/union-utils/tests/utils/test_http_client.py b/seed/python-sdk/unions/union-utils/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/unions/union-utils/tests/utils/test_http_client.py +++ b/seed/python-sdk/unions/union-utils/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/unknown/src/seed/core/http_client.py b/seed/python-sdk/unknown/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/unknown/src/seed/core/http_client.py +++ b/seed/python-sdk/unknown/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/unknown/tests/utils/test_http_client.py b/seed/python-sdk/unknown/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/unknown/tests/utils/test_http_client.py +++ b/seed/python-sdk/unknown/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/validation/no-custom-config/src/seed/core/http_client.py b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/validation/no-custom-config/src/seed/core/http_client.py +++ b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/validation/no-custom-config/tests/utils/test_http_client.py b/seed/python-sdk/validation/no-custom-config/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/validation/no-custom-config/tests/utils/test_http_client.py +++ b/seed/python-sdk/validation/no-custom-config/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/validation/with-defaults/src/seed/core/http_client.py b/seed/python-sdk/validation/with-defaults/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/validation/with-defaults/src/seed/core/http_client.py +++ b/seed/python-sdk/validation/with-defaults/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/validation/with-defaults/tests/utils/test_http_client.py b/seed/python-sdk/validation/with-defaults/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/validation/with-defaults/tests/utils/test_http_client.py +++ b/seed/python-sdk/validation/with-defaults/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/variables/src/seed/core/http_client.py b/seed/python-sdk/variables/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/variables/src/seed/core/http_client.py +++ b/seed/python-sdk/variables/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/variables/tests/utils/test_http_client.py b/seed/python-sdk/variables/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/variables/tests/utils/test_http_client.py +++ b/seed/python-sdk/variables/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/version-no-default/src/seed/core/http_client.py b/seed/python-sdk/version-no-default/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/version-no-default/src/seed/core/http_client.py +++ b/seed/python-sdk/version-no-default/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/version-no-default/tests/utils/test_http_client.py b/seed/python-sdk/version-no-default/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/version-no-default/tests/utils/test_http_client.py +++ b/seed/python-sdk/version-no-default/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/version/src/seed/core/http_client.py b/seed/python-sdk/version/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/version/src/seed/core/http_client.py +++ b/seed/python-sdk/version/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/version/tests/utils/test_http_client.py b/seed/python-sdk/version/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/version/tests/utils/test_http_client.py +++ b/seed/python-sdk/version/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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 diff --git a/seed/python-sdk/websocket/src/seed/core/http_client.py b/seed/python-sdk/websocket/src/seed/core/http_client.py index 9333d8a7f15..356880bbc3e 100644 --- a/seed/python-sdk/websocket/src/seed/core/http_client.py +++ b/seed/python-sdk/websocket/src/seed/core/http_client.py @@ -142,7 +142,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: diff --git a/seed/python-sdk/websocket/tests/utils/test_http_client.py b/seed/python-sdk/websocket/tests/utils/test_http_client.py index edd11ca7afb..a541bae6531 100644 --- a/seed/python-sdk/websocket/tests/utils/test_http_client.py +++ b/seed/python-sdk/websocket/tests/utils/test_http_client.py @@ -45,3 +45,17 @@ 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