[python-experimental] fixes json + charset use case (#12114)

* Adds code to detect json content type when charset is also set

* Updates template to properly render content type, regenerates samples

* Adds test_json_with_charset

* Reverts version file

* Fixes typo
This commit is contained in:
Justin Black 2022-04-11 20:05:26 -07:00 committed by GitHub
parent d17316e8d9
commit b29b5e1045
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 327 additions and 16 deletions

View File

@ -719,7 +719,20 @@ class ApiResponseWithoutDeserialization(ApiResponse):
headers: typing.Union[Unset, typing.List[HeaderParameter]] = unset headers: typing.Union[Unset, typing.List[HeaderParameter]] = unset
class OpenApiResponse: class JSONDetector:
@staticmethod
def content_type_is_json(content_type: str) -> bool:
"""
for when content_type strings also include charset info like:
application/json; charset=UTF-8
"""
content_type_piece = content_type.split(';')[0]
if content_type_piece == 'application/json':
return True
return False
class OpenApiResponse(JSONDetector):
def __init__( def __init__(
self, self,
response_cls: typing.Type[ApiResponse] = ApiResponse, response_cls: typing.Type[ApiResponse] = ApiResponse,
@ -734,8 +747,8 @@ class OpenApiResponse:
@staticmethod @staticmethod
def __deserialize_json(response: urllib3.HTTPResponse) -> typing.Any: def __deserialize_json(response: urllib3.HTTPResponse) -> typing.Any:
decoded_data = response.data.decode("utf-8") # python must be >= 3.9 so we can pass in bytes into json.loads
return json.loads(decoded_data) return json.loads(response.data)
@staticmethod @staticmethod
def __file_name_from_content_disposition(content_disposition: typing.Optional[str]) -> typing.Optional[str]: def __file_name_from_content_disposition(content_disposition: typing.Optional[str]) -> typing.Optional[str]:
@ -796,7 +809,7 @@ class OpenApiResponse:
deserialized_body = unset deserialized_body = unset
streamed = response.supports_chunked_reads() streamed = response.supports_chunked_reads()
if self.content is not None: if self.content is not None:
if content_type == 'application/json': if self.content_type_is_json(content_type):
body_data = self.__deserialize_json(response) body_data = self.__deserialize_json(response)
elif content_type == 'application/octet-stream': elif content_type == 'application/octet-stream':
body_data = self.__deserialize_application_octet_stream(response) body_data = self.__deserialize_application_octet_stream(response)
@ -1245,7 +1258,7 @@ class SerializedRequestBody(typing.TypedDict, total=False):
fields: typing.Tuple[typing.Union[RequestField, tuple[str, str]], ...] fields: typing.Tuple[typing.Union[RequestField, tuple[str, str]], ...]
class RequestBody(StyleFormSerializer): class RequestBody(StyleFormSerializer, JSONDetector):
""" """
A request body parameter A request body parameter
content: content_type to MediaType Schema info content: content_type to MediaType Schema info
@ -1382,7 +1395,7 @@ class RequestBody(StyleFormSerializer):
cast_in_data = media_type.schema(in_data) cast_in_data = media_type.schema(in_data)
# TODO check for and use encoding if it exists # TODO check for and use encoding if it exists
# and content_type is multipart or application/x-www-form-urlencoded # and content_type is multipart or application/x-www-form-urlencoded
if content_type == 'application/json': if self.content_type_is_json(content_type):
return self.__serialize_json(cast_in_data) return self.__serialize_json(cast_in_data)
elif content_type == 'text/plain': elif content_type == 'text/plain':
return self.__serialize_text_plain(cast_in_data) return self.__serialize_text_plain(cast_in_data)

View File

@ -182,7 +182,7 @@ class RequestCookieParams(RequestRequiredCookieParams, RequestOptionalCookiePara
request_body_{{paramName}} = api_client.RequestBody( request_body_{{paramName}} = api_client.RequestBody(
content={ content={
{{#each content}} {{#each content}}
'{{@key}}': api_client.MediaType( '{{{@key}}}': api_client.MediaType(
schema={{this.schema.baseName}}), schema={{this.schema.baseName}}),
{{/each}} {{/each}}
}, },
@ -323,7 +323,7 @@ _response_for_{{code}} = api_client.OpenApiResponse(
{{#if @first}} {{#if @first}}
content={ content={
{{/if}} {{/if}}
'{{@key}}': api_client.MediaType( '{{{@key}}}': api_client.MediaType(
schema={{this.schema.baseName}}), schema={{this.schema.baseName}}),
{{#if @last}} {{#if @last}}
}, },
@ -351,7 +351,7 @@ _status_code_to_response = {
{{#if @first}} {{#if @first}}
_all_accept_content_types = ( _all_accept_content_types = (
{{/if}} {{/if}}
'{{this.mediaType}}', '{{{this.mediaType}}}',
{{#if @last}} {{#if @last}}
) )
{{/if}} {{/if}}
@ -382,7 +382,7 @@ class {{operationIdCamelCase}}(api_client.Api):
{{#with bodyParam}} {{#with bodyParam}}
{{#each content}} {{#each content}}
{{#if @first}} {{#if @first}}
content_type: str = '{{@key}}', content_type: str = '{{{@key}}}',
{{/if}} {{/if}}
{{/each}} {{/each}}
{{/with}} {{/with}}

View File

@ -1530,6 +1530,22 @@ paths:
responses: responses:
'200': '200':
description: ok description: ok
'/fake/jsonWithCharset':
post:
tags:
- fake
summary: json with charset tx and rx
operationId: jsonWithCharset
requestBody:
content:
application/json; charset=utf-8:
schema: {}
responses:
200:
description: success
content:
application/json; charset=utf-8:
schema: {}
servers: servers:
- url: 'http://{server}.swagger.io:{port}/v2' - url: 'http://{server}.swagger.io:{port}/v2'
description: petstore server description: petstore server

View File

@ -100,6 +100,7 @@ Class | Method | HTTP request | Description
*FakeApi* | [**inline_additional_properties**](docs/FakeApi.md#inline_additional_properties) | **POST** /fake/inline-additionalProperties | test inline additionalProperties *FakeApi* | [**inline_additional_properties**](docs/FakeApi.md#inline_additional_properties) | **POST** /fake/inline-additionalProperties | test inline additionalProperties
*FakeApi* | [**inline_composition**](docs/FakeApi.md#inline_composition) | **POST** /fake/inlineComposition/ | testing composed schemas at inline locations *FakeApi* | [**inline_composition**](docs/FakeApi.md#inline_composition) | **POST** /fake/inlineComposition/ | testing composed schemas at inline locations
*FakeApi* | [**json_form_data**](docs/FakeApi.md#json_form_data) | **GET** /fake/jsonFormData | test json serialization of form data *FakeApi* | [**json_form_data**](docs/FakeApi.md#json_form_data) | **GET** /fake/jsonFormData | test json serialization of form data
*FakeApi* | [**json_with_charset**](docs/FakeApi.md#json_with_charset) | **POST** /fake/jsonWithCharset | json with charset tx and rx
*FakeApi* | [**mammal**](docs/FakeApi.md#mammal) | **POST** /fake/refs/mammal | *FakeApi* | [**mammal**](docs/FakeApi.md#mammal) | **POST** /fake/refs/mammal |
*FakeApi* | [**number_with_validations**](docs/FakeApi.md#number_with_validations) | **POST** /fake/refs/number | *FakeApi* | [**number_with_validations**](docs/FakeApi.md#number_with_validations) | **POST** /fake/refs/number |
*FakeApi* | [**object_in_query**](docs/FakeApi.md#object_in_query) | **GET** /fake/objInQuery | user list *FakeApi* | [**object_in_query**](docs/FakeApi.md#object_in_query) | **GET** /fake/objInQuery | user list

View File

@ -20,6 +20,7 @@ Method | HTTP request | Description
[**inline_additional_properties**](FakeApi.md#inline_additional_properties) | **POST** /fake/inline-additionalProperties | test inline additionalProperties [**inline_additional_properties**](FakeApi.md#inline_additional_properties) | **POST** /fake/inline-additionalProperties | test inline additionalProperties
[**inline_composition**](FakeApi.md#inline_composition) | **POST** /fake/inlineComposition/ | testing composed schemas at inline locations [**inline_composition**](FakeApi.md#inline_composition) | **POST** /fake/inlineComposition/ | testing composed schemas at inline locations
[**json_form_data**](FakeApi.md#json_form_data) | **GET** /fake/jsonFormData | test json serialization of form data [**json_form_data**](FakeApi.md#json_form_data) | **GET** /fake/jsonFormData | test json serialization of form data
[**json_with_charset**](FakeApi.md#json_with_charset) | **POST** /fake/jsonWithCharset | json with charset tx and rx
[**mammal**](FakeApi.md#mammal) | **POST** /fake/refs/mammal | [**mammal**](FakeApi.md#mammal) | **POST** /fake/refs/mammal |
[**number_with_validations**](FakeApi.md#number_with_validations) | **POST** /fake/refs/number | [**number_with_validations**](FakeApi.md#number_with_validations) | **POST** /fake/refs/number |
[**object_in_query**](FakeApi.md#object_in_query) | **GET** /fake/objInQuery | user list [**object_in_query**](FakeApi.md#object_in_query) | **GET** /fake/objInQuery | user list
@ -1618,6 +1619,87 @@ No authorization required
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **json_with_charset**
> bool, date, datetime, dict, float, int, list, str, none_type json_with_charset()
json with charset tx and rx
### Example
```python
import petstore_api
from petstore_api.api import fake_api
from pprint import pprint
# Defining the host is optional and defaults to http://petstore.swagger.io:80/v2
# See configuration.py for a list of all supported configuration parameters.
configuration = petstore_api.Configuration(
host = "http://petstore.swagger.io:80/v2"
)
# Enter a context with an instance of the API client
with petstore_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = fake_api.FakeApi(api_client)
# example passing only optional values
body = None
try:
# json with charset tx and rx
api_response = api_instance.json_with_charset(
body=body,
)
pprint(api_response)
except petstore_api.ApiException as e:
print("Exception when calling FakeApi->json_with_charset: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
body | typing.Union[SchemaForRequestBodyApplicationJsonCharsetutf8, Unset] | optional, default is unset |
content_type | str | optional, default is 'application/json; charset=utf-8' | Selects the schema and serialization of the request body
accept_content_types | typing.Tuple[str] | default is ('application/json; charset=utf-8', ) | Tells the server the content type(s) that are accepted by the client
stream | bool | default is False | if True then the response.content will be streamed and loaded from a file like object. When downloading a file, set this to True to force the code to deserialize the content to a FileSchema file
timeout | typing.Optional[typing.Union[int, typing.Tuple]] | default is None | the timeout used by the rest client
skip_deserialization | bool | default is False | when True, headers and body will be unset and an instance of api_client.ApiResponseWithoutDeserialization will be returned
### body
#### SchemaForRequestBodyApplicationJsonCharsetutf8
Type | Description | Notes
------------- | ------------- | -------------
typing.Union[dict, frozendict, str, date, datetime, int, float, bool, Decimal, None, list, tuple, bytes] | |
### Return Types, Responses
Code | Class | Description
------------- | ------------- | -------------
n/a | api_client.ApiResponseWithoutDeserialization | When skip_deserialization is True this response is returned
200 | ApiResponseFor200 | success
#### ApiResponseFor200
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
response | urllib3.HTTPResponse | Raw response |
body | typing.Union[SchemaFor200ResponseBodyApplicationJsonCharsetutf8, ] | |
headers | Unset | headers were not defined |
#### SchemaFor200ResponseBodyApplicationJsonCharsetutf8
Type | Description | Notes
------------- | ------------- | -------------
typing.Union[dict, frozendict, str, date, datetime, int, float, bool, Decimal, None, list, tuple, bytes] | |
**bool, date, datetime, dict, float, int, list, str, none_type**
### Authorization
No authorization required
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **mammal** # **mammal**
> Mammal mammal(mammal) > Mammal mammal(mammal)

View File

@ -26,6 +26,7 @@ from petstore_api.api.fake_api_endpoints.group_parameters import GroupParameters
from petstore_api.api.fake_api_endpoints.inline_additional_properties import InlineAdditionalProperties from petstore_api.api.fake_api_endpoints.inline_additional_properties import InlineAdditionalProperties
from petstore_api.api.fake_api_endpoints.inline_composition import InlineComposition from petstore_api.api.fake_api_endpoints.inline_composition import InlineComposition
from petstore_api.api.fake_api_endpoints.json_form_data import JsonFormData from petstore_api.api.fake_api_endpoints.json_form_data import JsonFormData
from petstore_api.api.fake_api_endpoints.json_with_charset import JsonWithCharset
from petstore_api.api.fake_api_endpoints.mammal import Mammal from petstore_api.api.fake_api_endpoints.mammal import Mammal
from petstore_api.api.fake_api_endpoints.number_with_validations import NumberWithValidations from petstore_api.api.fake_api_endpoints.number_with_validations import NumberWithValidations
from petstore_api.api.fake_api_endpoints.object_in_query import ObjectInQuery from petstore_api.api.fake_api_endpoints.object_in_query import ObjectInQuery
@ -57,6 +58,7 @@ class FakeApi(
InlineAdditionalProperties, InlineAdditionalProperties,
InlineComposition, InlineComposition,
JsonFormData, JsonFormData,
JsonWithCharset,
Mammal, Mammal,
NumberWithValidations, NumberWithValidations,
ObjectInQuery, ObjectInQuery,

View File

@ -0,0 +1,161 @@
# coding: utf-8
"""
Generated by: https://openapi-generator.tech
"""
from dataclasses import dataclass
import re # noqa: F401
import sys # noqa: F401
import typing
import urllib3
from urllib3._collections import HTTPHeaderDict
from petstore_api import api_client, exceptions
import decimal # noqa: F401
from datetime import date, datetime # noqa: F401
from frozendict import frozendict # noqa: F401
from petstore_api.schemas import ( # noqa: F401
AnyTypeSchema,
ComposedSchema,
DictSchema,
ListSchema,
StrSchema,
IntSchema,
Int32Schema,
Int64Schema,
Float32Schema,
Float64Schema,
NumberSchema,
DateSchema,
DateTimeSchema,
DecimalSchema,
BoolSchema,
BinarySchema,
NoneSchema,
none_type,
Configuration,
Unset,
unset,
ComposedBase,
ListBase,
DictBase,
NoneBase,
StrBase,
IntBase,
Int32Base,
Int64Base,
Float32Base,
Float64Base,
NumberBase,
DateBase,
DateTimeBase,
BoolBase,
BinaryBase,
Schema,
_SchemaValidator,
_SchemaTypeChecker,
_SchemaEnumMaker
)
# body param
SchemaForRequestBodyApplicationJsonCharsetutf8 = AnyTypeSchema
request_body_body = api_client.RequestBody(
content={
'application/json; charset=utf-8': api_client.MediaType(
schema=SchemaForRequestBodyApplicationJsonCharsetutf8),
},
)
_path = '/fake/jsonWithCharset'
_method = 'POST'
SchemaFor200ResponseBodyApplicationJsonCharsetutf8 = AnyTypeSchema
@dataclass
class ApiResponseFor200(api_client.ApiResponse):
response: urllib3.HTTPResponse
body: typing.Union[
SchemaFor200ResponseBodyApplicationJsonCharsetutf8,
]
headers: Unset = unset
_response_for_200 = api_client.OpenApiResponse(
response_cls=ApiResponseFor200,
content={
'application/json; charset=utf-8': api_client.MediaType(
schema=SchemaFor200ResponseBodyApplicationJsonCharsetutf8),
},
)
_status_code_to_response = {
'200': _response_for_200,
}
_all_accept_content_types = (
'application/json; charset=utf-8',
)
class JsonWithCharset(api_client.Api):
def json_with_charset(
self: api_client.Api,
body: typing.Union[SchemaForRequestBodyApplicationJsonCharsetutf8, Unset] = unset,
content_type: str = 'application/json; charset=utf-8',
accept_content_types: typing.Tuple[str] = _all_accept_content_types,
stream: bool = False,
timeout: typing.Optional[typing.Union[int, typing.Tuple]] = None,
skip_deserialization: bool = False,
) -> typing.Union[
ApiResponseFor200,
api_client.ApiResponseWithoutDeserialization
]:
"""
json with charset tx and rx
:param skip_deserialization: If true then api_response.response will be set but
api_response.body and api_response.headers will not be deserialized into schema
class instances
"""
_headers = HTTPHeaderDict()
# TODO add cookie handling
if accept_content_types:
for accept_content_type in accept_content_types:
_headers.add('Accept', accept_content_type)
_fields = None
_body = None
if body is not unset:
serialized_data = request_body_body.serialize(body, content_type)
_headers.add('Content-Type', content_type)
if 'fields' in serialized_data:
_fields = serialized_data['fields']
elif 'body' in serialized_data:
_body = serialized_data['body']
response = self.api_client.call_api(
resource_path=_path,
method=_method,
headers=_headers,
fields=_fields,
body=_body,
stream=stream,
timeout=timeout,
)
if skip_deserialization:
api_response = api_client.ApiResponseWithoutDeserialization(response=response)
else:
response_for_status = _status_code_to_response.get(str(response.status))
if response_for_status:
api_response = response_for_status.deserialize(response, self.api_client.configuration)
else:
api_response = api_client.ApiResponseWithoutDeserialization(response=response)
if not 200 <= response.status <= 299:
raise exceptions.ApiException(api_response=api_response)
return api_response

View File

@ -723,7 +723,20 @@ class ApiResponseWithoutDeserialization(ApiResponse):
headers: typing.Union[Unset, typing.List[HeaderParameter]] = unset headers: typing.Union[Unset, typing.List[HeaderParameter]] = unset
class OpenApiResponse: class JSONDetector:
@staticmethod
def content_type_is_json(content_type: str) -> bool:
"""
for when content_type strings also include charset info like:
application/json; charset=UTF-8
"""
content_type_piece = content_type.split(';')[0]
if content_type_piece == 'application/json':
return True
return False
class OpenApiResponse(JSONDetector):
def __init__( def __init__(
self, self,
response_cls: typing.Type[ApiResponse] = ApiResponse, response_cls: typing.Type[ApiResponse] = ApiResponse,
@ -738,8 +751,8 @@ class OpenApiResponse:
@staticmethod @staticmethod
def __deserialize_json(response: urllib3.HTTPResponse) -> typing.Any: def __deserialize_json(response: urllib3.HTTPResponse) -> typing.Any:
decoded_data = response.data.decode("utf-8") # python must be >= 3.9 so we can pass in bytes into json.loads
return json.loads(decoded_data) return json.loads(response.data)
@staticmethod @staticmethod
def __file_name_from_content_disposition(content_disposition: typing.Optional[str]) -> typing.Optional[str]: def __file_name_from_content_disposition(content_disposition: typing.Optional[str]) -> typing.Optional[str]:
@ -800,7 +813,7 @@ class OpenApiResponse:
deserialized_body = unset deserialized_body = unset
streamed = response.supports_chunked_reads() streamed = response.supports_chunked_reads()
if self.content is not None: if self.content is not None:
if content_type == 'application/json': if self.content_type_is_json(content_type):
body_data = self.__deserialize_json(response) body_data = self.__deserialize_json(response)
elif content_type == 'application/octet-stream': elif content_type == 'application/octet-stream':
body_data = self.__deserialize_application_octet_stream(response) body_data = self.__deserialize_application_octet_stream(response)
@ -1244,7 +1257,7 @@ class SerializedRequestBody(typing.TypedDict, total=False):
fields: typing.Tuple[typing.Union[RequestField, tuple[str, str]], ...] fields: typing.Tuple[typing.Union[RequestField, tuple[str, str]], ...]
class RequestBody(StyleFormSerializer): class RequestBody(StyleFormSerializer, JSONDetector):
""" """
A request body parameter A request body parameter
content: content_type to MediaType Schema info content: content_type to MediaType Schema info
@ -1381,7 +1394,7 @@ class RequestBody(StyleFormSerializer):
cast_in_data = media_type.schema(in_data) cast_in_data = media_type.schema(in_data)
# TODO check for and use encoding if it exists # TODO check for and use encoding if it exists
# and content_type is multipart or application/x-www-form-urlencoded # and content_type is multipart or application/x-www-form-urlencoded
if content_type == 'application/json': if self.content_type_is_json(content_type):
return self.__serialize_json(cast_in_data) return self.__serialize_json(cast_in_data)
elif content_type == 'text/plain': elif content_type == 'text/plain':
return self.__serialize_text_plain(cast_in_data) return self.__serialize_text_plain(cast_in_data)

View File

@ -679,6 +679,29 @@ class TestFakeApi(unittest.TestCase):
accept_content_types=(content_type,) accept_content_types=(content_type,)
) )
def test_json_with_charset(self):
# serialization + deserialization of json with charset works
with patch.object(RESTClientObject, 'request') as mock_request:
body = None
content_type_with_charset = 'application/json; charset=utf-8'
mock_request.return_value = self.__response(
self.__json_bytes(body),
content_type=content_type_with_charset
)
api_response = self.api.json_with_charset(body=body)
self.__assert_request_called_with(
mock_request,
'http://petstore.swagger.io:80/v2/fake/jsonWithCharset',
body=self.__json_bytes(body),
content_type=content_type_with_charset,
accept_content_type=content_type_with_charset
)
assert isinstance(api_response.body, schemas.AnyTypeSchema)
assert isinstance(api_response.body, schemas.NoneClass)
assert api_response.body.is_none()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()