forked from loafle/openapi-generator-original
[python-experimental] Allow response media types to omit schema (#12135)
* Adds issue spec file and attemts to generate code from it * Adds missing schema definitions * Skips fromProperty invocation if the passed in schema is none in getContent * Makes MediaType.schema optional * Adds checking that the content type is in self.content * Sets ApiResponse body type as Unset if there is no schema for it * Handles schema = None case * Adds endpoint without response schema * Reverts version files * Adds test_response_without_schema
This commit is contained in:
parent
7851dfe148
commit
1deaaa88fe
@ -6753,7 +6753,10 @@ public class DefaultCodegen implements CodegenConfig {
|
||||
}
|
||||
}
|
||||
String contentType = contentEntry.getKey();
|
||||
CodegenProperty schemaProp = fromProperty(toMediaTypeSchemaName(contentType, mediaTypeSchemaSuffix), mt.getSchema());
|
||||
CodegenProperty schemaProp = null;
|
||||
if (mt.getSchema() != null) {
|
||||
schemaProp = fromProperty(toMediaTypeSchemaName(contentType, mediaTypeSchemaSuffix), mt.getSchema());
|
||||
}
|
||||
CodegenMediaType codegenMt = new CodegenMediaType(schemaProp, ceMap);
|
||||
cmtContent.put(contentType, codegenMt);
|
||||
if (schemaProp != null) {
|
||||
|
@ -673,6 +673,7 @@ class Encoding:
|
||||
self.allow_reserved = allow_reserved
|
||||
|
||||
|
||||
@dataclass
|
||||
class MediaType:
|
||||
"""
|
||||
Used to store request and response body schema information
|
||||
@ -682,14 +683,8 @@ class MediaType:
|
||||
The encoding object SHALL only apply to requestBody objects when the media type is
|
||||
multipart or application/x-www-form-urlencoded.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schema: typing.Type[Schema],
|
||||
encoding: typing.Optional[typing.Dict[str, Encoding]] = None,
|
||||
):
|
||||
self.schema = schema
|
||||
self.encoding = encoding
|
||||
schema: typing.Optional[typing.Type[Schema]] = None
|
||||
encoding: typing.Optional[typing.Dict[str, Encoding]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -808,7 +803,27 @@ class OpenApiResponse(JSONDetector):
|
||||
content_type = response.getheader('content-type')
|
||||
deserialized_body = unset
|
||||
streamed = response.supports_chunked_reads()
|
||||
|
||||
deserialized_headers = unset
|
||||
if self.headers is not None:
|
||||
# TODO add header deserialiation here
|
||||
pass
|
||||
|
||||
if self.content is not None:
|
||||
if content_type not in self.content:
|
||||
raise ApiValueError(
|
||||
f'Invalid content_type={content_type} returned for response with '
|
||||
'status_code={str(response.status)}'
|
||||
)
|
||||
body_schema = self.content[content_type].schema
|
||||
if body_schema is None:
|
||||
# some specs do not define response content media type schemas
|
||||
return self.response_cls(
|
||||
response=response,
|
||||
headers=deserialized_headers,
|
||||
body=unset
|
||||
)
|
||||
|
||||
if self.content_type_is_json(content_type):
|
||||
body_data = self.__deserialize_json(response)
|
||||
elif content_type == 'application/octet-stream':
|
||||
@ -818,16 +833,11 @@ class OpenApiResponse(JSONDetector):
|
||||
content_type = 'multipart/form-data'
|
||||
else:
|
||||
raise NotImplementedError('Deserialization of {} has not yet been implemented'.format(content_type))
|
||||
body_schema = self.content[content_type].schema
|
||||
deserialized_body = body_schema._from_openapi_data(
|
||||
body_data, _configuration=configuration)
|
||||
elif streamed:
|
||||
response.release_conn()
|
||||
|
||||
deserialized_headers = unset
|
||||
if self.headers is not None:
|
||||
deserialized_headers = unset
|
||||
|
||||
return self.response_cls(
|
||||
response=response,
|
||||
headers=deserialized_headers,
|
||||
|
@ -175,7 +175,7 @@ default | ApiResponseForDefault | {{message}}
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
response | urllib3.HTTPResponse | Raw response |
|
||||
body | {{#unless content}}Unset{{else}}typing.Union[{{#each content}}{{this.schema.baseName}}, {{/each}}]{{/unless}} | {{#unless content}}body was not defined{{/unless}} |
|
||||
body | {{#unless content}}Unset{{else}}typing.Union[{{#each content}}{{#if this.schema}}{{this.schema.baseName}}{{else}}Unset{{/if}}, {{/each}}]{{/unless}} | {{#unless content}}body was not defined{{/unless}} |
|
||||
headers | {{#unless responseHeaders}}Unset{{else}}ResponseHeadersFor{{code}}{{/unless}} | {{#unless responseHeaders}}headers were not defined{{/unless}} |
|
||||
{{#each content}}
|
||||
{{#with this.schema}}
|
||||
|
@ -182,8 +182,8 @@ class RequestCookieParams(RequestRequiredCookieParams, RequestOptionalCookiePara
|
||||
request_body_{{paramName}} = api_client.RequestBody(
|
||||
content={
|
||||
{{#each content}}
|
||||
'{{{@key}}}': api_client.MediaType(
|
||||
schema={{this.schema.baseName}}),
|
||||
'{{{@key}}}': api_client.MediaType({{#if this.schema}}
|
||||
schema={{this.schema.baseName}}{{/if}}),
|
||||
{{/each}}
|
||||
},
|
||||
{{#if required}}
|
||||
@ -285,7 +285,11 @@ class ApiResponseFor{{code}}(api_client.ApiResponse):
|
||||
{{#and responseHeaders content}}
|
||||
body: typing.Union[
|
||||
{{#each content}}
|
||||
{{#if this.schema}}
|
||||
{{this.schema.baseName}},
|
||||
{{else}}
|
||||
Unset,
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
]
|
||||
headers: ResponseHeadersFor{{code}}
|
||||
@ -297,7 +301,11 @@ class ApiResponseFor{{code}}(api_client.ApiResponse):
|
||||
{{else}}
|
||||
body: typing.Union[
|
||||
{{#each content}}
|
||||
{{#if this.schema}}
|
||||
{{this.schema.baseName}},
|
||||
{{else}}
|
||||
Unset,
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
]
|
||||
headers: Unset = unset
|
||||
@ -323,8 +331,8 @@ _response_for_{{code}} = api_client.OpenApiResponse(
|
||||
{{#if @first}}
|
||||
content={
|
||||
{{/if}}
|
||||
'{{{@key}}}': api_client.MediaType(
|
||||
schema={{this.schema.baseName}}),
|
||||
'{{{@key}}}': api_client.MediaType({{#if this.schema}}
|
||||
schema={{this.schema.baseName}}{{/if}}),
|
||||
{{#if @last}}
|
||||
},
|
||||
{{/if}}
|
||||
|
@ -1546,6 +1546,18 @@ paths:
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema: {}
|
||||
"/fake/responseWithoutSchema":
|
||||
get:
|
||||
tags:
|
||||
- fake
|
||||
summary: receives a response without schema
|
||||
operationId: responseWithoutSchema
|
||||
responses:
|
||||
'200':
|
||||
description: contents without schema definition
|
||||
content:
|
||||
application/json: {}
|
||||
application/xml: {}
|
||||
servers:
|
||||
- url: 'http://{server}.swagger.io:{port}/v2'
|
||||
description: petstore server
|
||||
|
@ -108,6 +108,7 @@ Class | Method | HTTP request | Description
|
||||
*FakeApi* | [**parameter_collisions**](docs/FakeApi.md#parameter_collisions) | **POST** /fake/parameterCollisions/{1}/{aB}/{Ab}/{self}/{A-B}/ | parameter collision case
|
||||
*FakeApi* | [**query_parameter_collection_format**](docs/FakeApi.md#query_parameter_collection_format) | **PUT** /fake/test-query-paramters |
|
||||
*FakeApi* | [**ref_object_in_query**](docs/FakeApi.md#ref_object_in_query) | **GET** /fake/refObjInQuery | user list
|
||||
*FakeApi* | [**response_without_schema**](docs/FakeApi.md#response_without_schema) | **GET** /fake/responseWithoutSchema | receives a response without schema
|
||||
*FakeApi* | [**string**](docs/FakeApi.md#string) | **POST** /fake/refs/string |
|
||||
*FakeApi* | [**string_enum**](docs/FakeApi.md#string_enum) | **POST** /fake/refs/enum |
|
||||
*FakeApi* | [**upload_download_file**](docs/FakeApi.md#upload_download_file) | **POST** /fake/uploadDownloadFile | uploads a file and downloads a file using application/octet-stream
|
||||
|
@ -28,6 +28,7 @@ Method | HTTP request | Description
|
||||
[**parameter_collisions**](FakeApi.md#parameter_collisions) | **POST** /fake/parameterCollisions/{1}/{aB}/{Ab}/{self}/{A-B}/ | parameter collision case
|
||||
[**query_parameter_collection_format**](FakeApi.md#query_parameter_collection_format) | **PUT** /fake/test-query-paramters |
|
||||
[**ref_object_in_query**](FakeApi.md#ref_object_in_query) | **GET** /fake/refObjInQuery | user list
|
||||
[**response_without_schema**](FakeApi.md#response_without_schema) | **GET** /fake/responseWithoutSchema | receives a response without schema
|
||||
[**string**](FakeApi.md#string) | **POST** /fake/refs/string |
|
||||
[**string_enum**](FakeApi.md#string_enum) | **POST** /fake/refs/enum |
|
||||
[**upload_download_file**](FakeApi.md#upload_download_file) | **POST** /fake/uploadDownloadFile | uploads a file and downloads a file using application/octet-stream
|
||||
@ -2547,6 +2548,61 @@ body | Unset | body was not defined |
|
||||
headers | Unset | headers were not defined |
|
||||
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### 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)
|
||||
|
||||
# **response_without_schema**
|
||||
> response_without_schema()
|
||||
|
||||
receives a response without schema
|
||||
|
||||
### 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, this endpoint has no required or optional parameters
|
||||
try:
|
||||
# receives a response without schema
|
||||
api_response = api_instance.response_without_schema()
|
||||
except petstore_api.ApiException as e:
|
||||
print("Exception when calling FakeApi->response_without_schema: %s\n" % e)
|
||||
```
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return Types, Responses
|
||||
|
||||
Code | Class | Description
|
||||
------------- | ------------- | -------------
|
||||
n/a | api_client.ApiResponseWithoutDeserialization | When skip_deserialization is True this response is returned
|
||||
200 | ApiResponseFor200 | contents without schema definition
|
||||
|
||||
#### ApiResponseFor200
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
response | urllib3.HTTPResponse | Raw response |
|
||||
body | typing.Union[Unset, Unset, ] | |
|
||||
headers | Unset | headers were not defined |
|
||||
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
@ -34,6 +34,7 @@ from petstore_api.api.fake_api_endpoints.object_model_with_ref_props import Obje
|
||||
from petstore_api.api.fake_api_endpoints.parameter_collisions import ParameterCollisions
|
||||
from petstore_api.api.fake_api_endpoints.query_parameter_collection_format import QueryParameterCollectionFormat
|
||||
from petstore_api.api.fake_api_endpoints.ref_object_in_query import RefObjectInQuery
|
||||
from petstore_api.api.fake_api_endpoints.response_without_schema import ResponseWithoutSchema
|
||||
from petstore_api.api.fake_api_endpoints.string import String
|
||||
from petstore_api.api.fake_api_endpoints.string_enum import StringEnum
|
||||
from petstore_api.api.fake_api_endpoints.upload_download_file import UploadDownloadFile
|
||||
@ -66,6 +67,7 @@ class FakeApi(
|
||||
ParameterCollisions,
|
||||
QueryParameterCollectionFormat,
|
||||
RefObjectInQuery,
|
||||
ResponseWithoutSchema,
|
||||
String,
|
||||
StringEnum,
|
||||
UploadDownloadFile,
|
||||
|
@ -0,0 +1,139 @@
|
||||
# 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
|
||||
)
|
||||
|
||||
_path = '/fake/responseWithoutSchema'
|
||||
_method = 'GET'
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApiResponseFor200(api_client.ApiResponse):
|
||||
response: urllib3.HTTPResponse
|
||||
body: typing.Union[
|
||||
Unset,
|
||||
Unset,
|
||||
]
|
||||
headers: Unset = unset
|
||||
|
||||
|
||||
_response_for_200 = api_client.OpenApiResponse(
|
||||
response_cls=ApiResponseFor200,
|
||||
content={
|
||||
'application/json': api_client.MediaType(),
|
||||
'application/xml': api_client.MediaType(),
|
||||
},
|
||||
)
|
||||
_status_code_to_response = {
|
||||
'200': _response_for_200,
|
||||
}
|
||||
_all_accept_content_types = (
|
||||
'application/json',
|
||||
'application/xml',
|
||||
)
|
||||
|
||||
|
||||
class ResponseWithoutSchema(api_client.Api):
|
||||
|
||||
def response_without_schema(
|
||||
self: api_client.Api,
|
||||
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
|
||||
]:
|
||||
"""
|
||||
receives a response without schema
|
||||
: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)
|
||||
|
||||
response = self.api_client.call_api(
|
||||
resource_path=_path,
|
||||
method=_method,
|
||||
headers=_headers,
|
||||
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
|
@ -677,6 +677,7 @@ class Encoding:
|
||||
self.allow_reserved = allow_reserved
|
||||
|
||||
|
||||
@dataclass
|
||||
class MediaType:
|
||||
"""
|
||||
Used to store request and response body schema information
|
||||
@ -686,14 +687,8 @@ class MediaType:
|
||||
The encoding object SHALL only apply to requestBody objects when the media type is
|
||||
multipart or application/x-www-form-urlencoded.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schema: typing.Type[Schema],
|
||||
encoding: typing.Optional[typing.Dict[str, Encoding]] = None,
|
||||
):
|
||||
self.schema = schema
|
||||
self.encoding = encoding
|
||||
schema: typing.Optional[typing.Type[Schema]] = None
|
||||
encoding: typing.Optional[typing.Dict[str, Encoding]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -812,7 +807,27 @@ class OpenApiResponse(JSONDetector):
|
||||
content_type = response.getheader('content-type')
|
||||
deserialized_body = unset
|
||||
streamed = response.supports_chunked_reads()
|
||||
|
||||
deserialized_headers = unset
|
||||
if self.headers is not None:
|
||||
# TODO add header deserialiation here
|
||||
pass
|
||||
|
||||
if self.content is not None:
|
||||
if content_type not in self.content:
|
||||
raise ApiValueError(
|
||||
f'Invalid content_type={content_type} returned for response with '
|
||||
'status_code={str(response.status)}'
|
||||
)
|
||||
body_schema = self.content[content_type].schema
|
||||
if body_schema is None:
|
||||
# some specs do not define response content media type schemas
|
||||
return self.response_cls(
|
||||
response=response,
|
||||
headers=deserialized_headers,
|
||||
body=unset
|
||||
)
|
||||
|
||||
if self.content_type_is_json(content_type):
|
||||
body_data = self.__deserialize_json(response)
|
||||
elif content_type == 'application/octet-stream':
|
||||
@ -822,16 +837,11 @@ class OpenApiResponse(JSONDetector):
|
||||
content_type = 'multipart/form-data'
|
||||
else:
|
||||
raise NotImplementedError('Deserialization of {} has not yet been implemented'.format(content_type))
|
||||
body_schema = self.content[content_type].schema
|
||||
deserialized_body = body_schema._from_openapi_data(
|
||||
body_data, _configuration=configuration)
|
||||
elif streamed:
|
||||
response.release_conn()
|
||||
|
||||
deserialized_headers = unset
|
||||
if self.headers is not None:
|
||||
deserialized_headers = unset
|
||||
|
||||
return self.response_cls(
|
||||
response=response,
|
||||
headers=deserialized_headers,
|
||||
|
@ -70,29 +70,34 @@ class TestFakeApi(unittest.TestCase):
|
||||
def __assert_request_called_with(
|
||||
mock_request,
|
||||
url: str,
|
||||
method: str = 'POST',
|
||||
body: typing.Optional[bytes] = None,
|
||||
content_type: str = 'application/json',
|
||||
content_type: typing.Optional[str] = 'application/json',
|
||||
fields: typing.Optional[tuple[api_client.RequestField, ...]] = None,
|
||||
accept_content_type: str = 'application/json',
|
||||
stream: bool = False,
|
||||
query_params: typing.Optional[typing.Tuple[typing.Tuple[str, str], ...]] = None
|
||||
):
|
||||
mock_request.assert_called_with(
|
||||
'POST',
|
||||
url,
|
||||
headers=HTTPHeaderDict(
|
||||
{
|
||||
headers = {
|
||||
'Accept': accept_content_type,
|
||||
'Content-Type': content_type,
|
||||
'User-Agent': 'OpenAPI-Generator/1.0.0/python'
|
||||
}
|
||||
),
|
||||
body=body,
|
||||
if content_type:
|
||||
headers['Content-Type'] = content_type
|
||||
kwargs = dict(
|
||||
headers=HTTPHeaderDict(headers),
|
||||
query_params=query_params,
|
||||
fields=fields,
|
||||
stream=stream,
|
||||
timeout=None,
|
||||
)
|
||||
if method != 'GET':
|
||||
kwargs['body'] = body
|
||||
mock_request.assert_called_with(
|
||||
method,
|
||||
url,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def test_array_model(self):
|
||||
from petstore_api.model import animal_farm, animal
|
||||
@ -702,6 +707,37 @@ class TestFakeApi(unittest.TestCase):
|
||||
assert isinstance(api_response.body, schemas.NoneClass)
|
||||
assert api_response.body.is_none()
|
||||
|
||||
def test_response_without_schema(self):
|
||||
# received response is not loaded into body because there is no deserialization schema defined
|
||||
with patch.object(RESTClientObject, 'request') as mock_request:
|
||||
body = None
|
||||
content_type = 'application/json'
|
||||
mock_request.return_value = self.__response(
|
||||
self.__json_bytes(body),
|
||||
)
|
||||
|
||||
api_response = self.api.response_without_schema()
|
||||
self.__assert_request_called_with(
|
||||
mock_request,
|
||||
'http://petstore.swagger.io:80/v2/fake/responseWithoutSchema',
|
||||
method='GET',
|
||||
accept_content_type='application/json, application/xml',
|
||||
content_type=None
|
||||
)
|
||||
|
||||
assert isinstance(api_response.body, schemas.Unset)
|
||||
|
||||
|
||||
with patch.object(RESTClientObject, 'request') as mock_request:
|
||||
mock_request.return_value = self.__response(
|
||||
'blah',
|
||||
content_type='text/plain'
|
||||
)
|
||||
|
||||
# when an incorrect content-type is sent back, and exception is raised
|
||||
with self.assertRaises(exceptions.ApiValueError):
|
||||
self.api.response_without_schema()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
Loading…
x
Reference in New Issue
Block a user