diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java index 760033ec35de..f9d25b47d251 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenModel.java @@ -595,4 +595,8 @@ public void removeSelfReferenceImport() { } } } + + public boolean isComposed() { + return !allOf.isEmpty() || !oneOf.isEmpty() || !anyOf.isEmpty(); + } } diff --git a/modules/openapi-generator/src/main/resources/python/api_client.mustache b/modules/openapi-generator/src/main/resources/python/api_client.mustache index 768b8d973f8f..7af5bc0ce6f3 100644 --- a/modules/openapi-generator/src/main/resources/python/api_client.mustache +++ b/modules/openapi-generator/src/main/resources/python/api_client.mustache @@ -72,6 +72,7 @@ class ApiClient(object): self.cookie = cookie # Set default User-Agent. self.user_agent = '{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{{packageVersion}}}/python{{/httpUserAgent}}' + self.__deserializer_track = [] def __del__(self): if self._pool: @@ -268,6 +269,7 @@ class ApiClient(object): """ if data is None: return None + self.__deserializer_track.append(klass) if type(klass) == str: if klass.startswith('list['): @@ -640,6 +642,60 @@ class ApiClient(object): instance = klass(**kwargs) + if hasattr(instance, 'composed_hierarchy'): + hierarchy = instance.composed_hierarchy + if sum([1 if v else 0 for v in hierarchy.values()]) > 1: + # ignore implicit AllOf + return instance + + def error_msg(composed): + return "Failed to parse `{0}` as {1} ({2} {3})" \ + .format(data, klass, + composed, + ' '.join(hierarchy[composed])) + + if hierarchy['allOf']: + matches = [] + for sub_klass in hierarchy['allOf']: + try: + if sub_klass in self.__deserializer_track: + # seen it before, break the recursion + matches.append(None) + else: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == len(hierarchy['allOf']): + self.__deserializer_track = [] + return instance + + # not all matched -> error + raise rest.ApiException(status=0, reason=error_msg('allOf')) + + if hierarchy['anyOf']: + for sub_klass in hierarchy['anyOf']: + try: + # if at least one of the sub-class matches + # terminate with an instance of this class + return self.__deserialize(data, sub_klass) + except: # noqa: E722 + pass + # none matched -> error + raise rest.ApiException(status=0, reason=error_msg('anyOf')) + + if hierarchy['oneOf']: + matches = [] + for sub_klass in hierarchy['oneOf']: + try: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == 1: + return matches[0] + + # none matched, or more than one matched -> error + raise rest.ApiException(status=0, reason=error_msg('oneOf')) + if hasattr(instance, 'get_real_child_model'): klass_name = instance.get_real_child_model(data) if klass_name: diff --git a/modules/openapi-generator/src/main/resources/python/model.mustache b/modules/openapi-generator/src/main/resources/python/model.mustache index 16c8dadbf620..f7d9cda91e5c 100644 --- a/modules/openapi-generator/src/main/resources/python/model.mustache +++ b/modules/openapi-generator/src/main/resources/python/model.mustache @@ -49,6 +49,16 @@ class {{classname}}(object): {{#children}}'{{^vendorExtensions.x-discriminator-value}}{{name}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}': '{{{classname}}}'{{^-last}}, {{/-last}}{{/children}} } +{{/discriminator}} +{{^discriminator}} +{{#isComposed}} + + composed_hierarchy = { + 'anyOf': [{{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}}], + 'allOf': [{{#allOf}}"{{.}}"{{^-last}}, {{/-last}}{{/allOf}}], + 'oneOf': [{{#oneOf}}"{{.}}"{{^-last}}, {{/-last}}{{/oneOf}}], + } +{{/isComposed}} {{/discriminator}} def __init__(self{{#vars}}, {{name}}={{#defaultValue}}{{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}None{{/defaultValue}}{{/vars}}): # noqa: E501 diff --git a/samples/client/petstore-security-test/python/petstore_api/api_client.py b/samples/client/petstore-security-test/python/petstore_api/api_client.py new file mode 100644 index 000000000000..3a356c64a950 --- /dev/null +++ b/samples/client/petstore-security-test/python/petstore_api/api_client.py @@ -0,0 +1,697 @@ +# coding: utf-8 +""" + OpenAPI Petstore */ ' \" =end -- \\r\\n \\n \\r + + This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\ */ ' \" =end -- # noqa: E501 + + OpenAPI spec version: 1.0.0 */ ' \" =end -- \\r\\n \\n \\r + Contact: something@something.abc */ ' \" =end -- \\r\\n \\n \\r + Generated by: https://openapi-generator.tech +""" + +from __future__ import absolute_import + +import datetime +import json +import mimetypes +from multiprocessing.pool import ThreadPool +import os +import re +import tempfile + +# python 2 and python 3 compatibility library +import six +from six.moves.urllib.parse import quote + +from petstore_api.configuration import Configuration +import petstore_api.models +from petstore_api import rest + + +class ApiClient(object): + """Generic API client for OpenAPI client library builds. + + OpenAPI generic API client. This client handles the client- + server communication, and is invariant across implementations. Specifics of + the methods and models for each application are generated from the OpenAPI + templates. + + NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + Do not edit the class manually. + + :param configuration: .Configuration object for this client + :param header_name: a header to pass when making calls to the API. + :param header_value: a header value to pass when making calls to + the API. + :param cookie: a cookie to include in the header when making calls + to the API + :param pool_threads: The number of threads to use for async requests + to the API. More threads means more concurrent API requests. + """ + + PRIMITIVE_TYPES = (float, bool, bytes, six.text_type) + six.integer_types + NATIVE_TYPES_MAPPING = { + 'int': int, + 'long': int if six.PY3 else long, # noqa: F821 + 'float': float, + 'str': str, + 'bool': bool, + 'date': datetime.date, + 'datetime': datetime.datetime, + 'object': object, + } + _pool = None + + def __init__(self, configuration=None, header_name=None, header_value=None, + cookie=None, pool_threads=1): + if configuration is None: + configuration = Configuration() + self.configuration = configuration + self.pool_threads = pool_threads + + self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} + if header_name is not None: + self.default_headers[header_name] = header_value + self.cookie = cookie + # Set default User-Agent. + self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.__deserializer_track = [] + + def __del__(self): + if self._pool: + self._pool.close() + self._pool.join() + self._pool = None + + @property + def pool(self): + """Create thread pool on first request + avoids instantiating unused threadpool for blocking clients. + """ + if self._pool is None: + self._pool = ThreadPool(self.pool_threads) + return self._pool + + @property + def user_agent(self): + """User agent for this API client""" + return self.default_headers['User-Agent'] + + @user_agent.setter + def user_agent(self, value): + self.default_headers['User-Agent'] = value + + def set_default_header(self, header_name, header_value): + self.default_headers[header_name] = header_value + + def __call_api( + self, resource_path, method, path_params=None, + query_params=None, header_params=None, body=None, post_params=None, + files=None, response_type=None, auth_settings=None, + _return_http_data_only=None, collection_formats=None, + _preload_content=True, _request_timeout=None, _host=None): + + config = self.configuration + + # header parameters + header_params = header_params or {} + header_params.update(self.default_headers) + if self.cookie: + header_params['Cookie'] = self.cookie + if header_params: + header_params = self.sanitize_for_serialization(header_params) + header_params = dict(self.parameters_to_tuples(header_params, + collection_formats)) + + # path parameters + if path_params: + path_params = self.sanitize_for_serialization(path_params) + path_params = self.parameters_to_tuples(path_params, + collection_formats) + for k, v in path_params: + # specified safe chars, encode everything + resource_path = resource_path.replace( + '{%s}' % k, + quote(str(v), safe=config.safe_chars_for_path_param) + ) + + # query parameters + if query_params: + query_params = self.sanitize_for_serialization(query_params) + query_params = self.parameters_to_tuples(query_params, + collection_formats) + + # post parameters + if post_params or files: + post_params = self.prepare_post_parameters(post_params, files) + post_params = self.sanitize_for_serialization(post_params) + post_params = self.parameters_to_tuples(post_params, + collection_formats) + + # auth setting + self.update_params_for_auth(header_params, query_params, auth_settings) + + # body + if body: + body = self.sanitize_for_serialization(body) + + # request url + if _host is None: + url = self.configuration.host + resource_path + else: + # use server/host defined in path or operation instead + url = _host + resource_path + + # perform request and return response + response_data = self.request( + method, url, query_params=query_params, headers=header_params, + post_params=post_params, body=body, + _preload_content=_preload_content, + _request_timeout=_request_timeout) + + self.last_response = response_data + + return_data = response_data + if _preload_content: + # deserialize response data + if response_type: + return_data = self.deserialize(response_data, response_type) + else: + return_data = None + + if _return_http_data_only: + return (return_data) + else: + return (return_data, response_data.status, + response_data.getheaders()) + + def sanitize_for_serialization(self, obj): + """Builds a JSON POST object. + + If obj is None, return None. + If obj is str, int, long, float, bool, return directly. + If obj is datetime.datetime, datetime.date + convert to string in iso8601 format. + If obj is list, sanitize each element in the list. + If obj is dict, return the dict. + If obj is OpenAPI model, return the properties dict. + + :param obj: The data to serialize. + :return: The serialized form of data. + """ + if obj is None: + return None + elif isinstance(obj, self.PRIMITIVE_TYPES): + return obj + elif isinstance(obj, list): + return [self.sanitize_for_serialization(sub_obj) + for sub_obj in obj] + elif isinstance(obj, tuple): + return tuple(self.sanitize_for_serialization(sub_obj) + for sub_obj in obj) + elif isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + + if isinstance(obj, dict): + obj_dict = obj + else: + # Convert model obj to dict except + # attributes `openapi_types`, `attribute_map` + # and attributes which value is not None. + # Convert attribute name to json key in + # model definition for request. + obj_dict = {obj.attribute_map[attr]: getattr(obj, attr) + for attr, _ in six.iteritems(obj.openapi_types) + if getattr(obj, attr) is not None} + + return {key: self.sanitize_for_serialization(val) + for key, val in six.iteritems(obj_dict)} + + def deserialize(self, response, response_type): + """Deserializes response into an object. + + :param response: RESTResponse object to be deserialized. + :param response_type: class literal for + deserialized object, or string of class name. + + :return: deserialized object. + """ + # handle file downloading + # save response body into a tmp file and return the instance + if response_type == "file": + return self.__deserialize_file(response) + + # fetch data from response object + try: + data = json.loads(response.data) + except ValueError: + data = response.data + + return self.__deserialize(data, response_type) + + def __deserialize(self, data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + self.__deserializer_track.append(klass) + + if type(klass) == str: + if klass.startswith('list['): + sub_kls = re.match(r'list\[(.*)\]', klass).group(1) + return [self.__deserialize(sub_data, sub_kls) + for sub_data in data] + + if klass.startswith('dict('): + sub_kls = re.match(r'dict\(([^,]*), (.*)\)', klass).group(2) + return {k: self.__deserialize(v, sub_kls) + for k, v in six.iteritems(data)} + + # convert str to class + if klass in self.NATIVE_TYPES_MAPPING: + klass = self.NATIVE_TYPES_MAPPING[klass] + else: + klass = getattr(petstore_api.models, klass) + + if klass in self.PRIMITIVE_TYPES: + return self.__deserialize_primitive(data, klass) + elif klass == object: + return self.__deserialize_object(data) + elif klass == datetime.date: + return self.__deserialize_date(data) + elif klass == datetime.datetime: + return self.__deserialize_datatime(data) + else: + return self.__deserialize_model(data, klass) + + def call_api(self, resource_path, method, + path_params=None, query_params=None, header_params=None, + body=None, post_params=None, files=None, + response_type=None, auth_settings=None, async_req=None, + _return_http_data_only=None, collection_formats=None, + _preload_content=True, _request_timeout=None, _host=None): + """Makes the HTTP request (synchronous) and returns deserialized data. + + To make an async_req request, set the async_req parameter. + + :param resource_path: Path to method endpoint. + :param method: Method to call. + :param path_params: Path parameters in the url. + :param query_params: Query parameters in the url. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param auth_settings list: Auth Settings names for the request. + :param response: Response data type. + :param files dict: key -> filename, value -> filepath, + for `multipart/form-data`. + :param async_req bool: execute request asynchronously + :param _return_http_data_only: response data without head status code + and headers + :param collection_formats: dict of collection formats for path, query, + header, and post parameters. + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :return: + If async_req parameter is True, + the request will be called asynchronously. + The method will return the request thread. + If parameter async_req is False or missing, + then the method will return the response directly. + """ + if not async_req: + return self.__call_api(resource_path, method, + path_params, query_params, header_params, + body, post_params, files, + response_type, auth_settings, + _return_http_data_only, collection_formats, + _preload_content, _request_timeout, _host) + else: + thread = self.pool.apply_async(self.__call_api, (resource_path, + method, path_params, query_params, + header_params, body, + post_params, files, + response_type, auth_settings, + _return_http_data_only, + collection_formats, + _preload_content, + _request_timeout, + _host)) + return thread + + def request(self, method, url, query_params=None, headers=None, + post_params=None, body=None, _preload_content=True, + _request_timeout=None): + """Makes the HTTP request using RESTClient.""" + if method == "GET": + return self.rest_client.GET(url, + query_params=query_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + headers=headers) + elif method == "HEAD": + return self.rest_client.HEAD(url, + query_params=query_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + headers=headers) + elif method == "OPTIONS": + return self.rest_client.OPTIONS(url, + query_params=query_params, + headers=headers, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + elif method == "POST": + return self.rest_client.POST(url, + query_params=query_params, + headers=headers, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + elif method == "PUT": + return self.rest_client.PUT(url, + query_params=query_params, + headers=headers, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + elif method == "PATCH": + return self.rest_client.PATCH(url, + query_params=query_params, + headers=headers, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + elif method == "DELETE": + return self.rest_client.DELETE(url, + query_params=query_params, + headers=headers, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + else: + raise ValueError( + "http method must be `GET`, `HEAD`, `OPTIONS`," + " `POST`, `PATCH`, `PUT` or `DELETE`." + ) + + def parameters_to_tuples(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: Parameters as list of tuples, collections formatted + """ + new_params = [] + if collection_formats is None: + collection_formats = {} + for k, v in six.iteritems(params) if isinstance(params, dict) else params: # noqa: E501 + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, value) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(str(value) for value in v))) + else: + new_params.append((k, v)) + return new_params + + def prepare_post_parameters(self, post_params=None, files=None): + """Builds form parameters. + + :param post_params: Normal form parameters. + :param files: File parameters. + :return: Form parameters with files. + """ + params = [] + + if post_params: + params = post_params + + if files: + for k, v in six.iteritems(files): + if not v: + continue + file_names = v if type(v) is list else [v] + for n in file_names: + with open(n, 'rb') as f: + filename = os.path.basename(f.name) + filedata = f.read() + mimetype = (mimetypes.guess_type(filename)[0] or + 'application/octet-stream') + params.append( + tuple([k, tuple([filename, filedata, mimetype])])) + + return params + + def select_header_accept(self, accepts): + """Returns `Accept` based on an array of accepts provided. + + :param accepts: List of headers. + :return: Accept (e.g. application/json). + """ + if not accepts: + return + + accepts = [x.lower() for x in accepts] + + if 'application/json' in accepts: + return 'application/json' + else: + return ', '.join(accepts) + + def select_header_content_type(self, content_types): + """Returns `Content-Type` based on an array of content_types provided. + + :param content_types: List of content-types. + :return: Content-Type (e.g. application/json). + """ + if not content_types: + return 'application/json' + + content_types = [x.lower() for x in content_types] + + if 'application/json' in content_types or '*/*' in content_types: + return 'application/json' + else: + return content_types[0] + + def update_params_for_auth(self, headers, querys, auth_settings): + """Updates header and query params based on authentication setting. + + :param headers: Header parameters dict to be updated. + :param querys: Query parameters tuple list to be updated. + :param auth_settings: Authentication setting identifiers list. + """ + if not auth_settings: + return + + for auth in auth_settings: + auth_setting = self.configuration.auth_settings().get(auth) + if auth_setting: + if not auth_setting['value']: + continue + elif auth_setting['in'] == 'header': + headers[auth_setting['key']] = auth_setting['value'] + elif auth_setting['in'] == 'query': + querys.append((auth_setting['key'], auth_setting['value'])) + else: + raise ValueError( + 'Authentication token must be in `query` or `header`' + ) + + def __deserialize_file(self, response): + """Deserializes body to file + + Saves response body into a file in a temporary folder, + using the filename from the `Content-Disposition` header if provided. + + :param response: RESTResponse. + :return: file path. + """ + fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path) + os.close(fd) + os.remove(path) + + content_disposition = response.getheader("Content-Disposition") + if content_disposition: + filename = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?', + content_disposition).group(1) + path = os.path.join(os.path.dirname(path), filename) + + with open(path, "wb") as f: + f.write(response.data) + + return path + + def __deserialize_primitive(self, data, klass): + """Deserializes string to primitive type. + + :param data: str. + :param klass: class literal. + + :return: int, long, float, str, bool. + """ + try: + return klass(data) + except UnicodeEncodeError: + return six.text_type(data) + except TypeError: + return data + + def __deserialize_object(self, value): + """Return an original value. + + :return: object. + """ + return value + + def __deserialize_date(self, string): + """Deserializes string to date. + + :param string: str. + :return: date. + """ + try: + from dateutil.parser import parse + return parse(string).date() + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason="Failed to parse `{0}` as date object".format(string) + ) + + def __deserialize_datatime(self, string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :return: datetime. + """ + try: + from dateutil.parser import parse + return parse(string) + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as datetime object" + .format(string) + ) + ) + + def __deserialize_model(self, data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :param klass: class literal. + :return: model object. + """ + + if not klass.openapi_types and not hasattr(klass, + 'get_real_child_model'): + return data + + kwargs = {} + if klass.openapi_types is not None: + for attr, attr_type in six.iteritems(klass.openapi_types): + if (data is not None and + klass.attribute_map[attr] in data and + isinstance(data, (list, dict))): + value = data[klass.attribute_map[attr]] + kwargs[attr] = self.__deserialize(value, attr_type) + + instance = klass(**kwargs) + + if hasattr(instance, 'composed_hierarchy'): + hierarchy = instance.composed_hierarchy + if sum([1 if v else 0 for v in hierarchy.values()]) > 1: + # ignore implicit AllOf + return instance + + def error_msg(composed): + return "Failed to parse `{0}` as {1} ({2} {3})" \ + .format(data, klass, + composed, + ' '.join(hierarchy[composed])) + + if hierarchy['allOf']: + matches = [] + for sub_klass in hierarchy['allOf']: + try: + if sub_klass in self.__deserializer_track: + # seen it before, break the recursion + matches.append(None) + else: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == len(hierarchy['allOf']): + self.__deserializer_track = [] + return instance + + # not all matched -> error + raise rest.ApiException(status=0, reason=error_msg('allOf')) + + if hierarchy['anyOf']: + for sub_klass in hierarchy['anyOf']: + try: + # if at least one of the sub-class matches + # terminate with an instance of this class + return self.__deserialize(data, sub_klass) + except: # noqa: E722 + pass + # none matched -> error + raise rest.ApiException(status=0, reason=error_msg('anyOf')) + + if hierarchy['oneOf']: + matches = [] + for sub_klass in hierarchy['oneOf']: + try: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == 1: + return matches[0] + + # none matched, or more than one matched -> error + raise rest.ApiException(status=0, reason=error_msg('oneOf')) + + if hasattr(instance, 'get_real_child_model'): + klass_name = instance.get_real_child_model(data) + if klass_name: + instance = self.__deserialize(data, klass_name) + return instance diff --git a/samples/client/petstore/python-asyncio/petstore_api/api_client.py b/samples/client/petstore/python-asyncio/petstore_api/api_client.py index c6fc3a0609cd..515bed38fdae 100644 --- a/samples/client/petstore/python-asyncio/petstore_api/api_client.py +++ b/samples/client/petstore/python-asyncio/petstore_api/api_client.py @@ -77,6 +77,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None, self.cookie = cookie # Set default User-Agent. self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.__deserializer_track = [] def __del__(self): if self._pool: @@ -261,6 +262,7 @@ def __deserialize(self, data, klass): """ if data is None: return None + self.__deserializer_track.append(klass) if type(klass) == str: if klass.startswith('list['): @@ -633,6 +635,60 @@ def __deserialize_model(self, data, klass): instance = klass(**kwargs) + if hasattr(instance, 'composed_hierarchy'): + hierarchy = instance.composed_hierarchy + if sum([1 if v else 0 for v in hierarchy.values()]) > 1: + # ignore implicit AllOf + return instance + + def error_msg(composed): + return "Failed to parse `{0}` as {1} ({2} {3})" \ + .format(data, klass, + composed, + ' '.join(hierarchy[composed])) + + if hierarchy['allOf']: + matches = [] + for sub_klass in hierarchy['allOf']: + try: + if sub_klass in self.__deserializer_track: + # seen it before, break the recursion + matches.append(None) + else: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == len(hierarchy['allOf']): + self.__deserializer_track = [] + return instance + + # not all matched -> error + raise rest.ApiException(status=0, reason=error_msg('allOf')) + + if hierarchy['anyOf']: + for sub_klass in hierarchy['anyOf']: + try: + # if at least one of the sub-class matches + # terminate with an instance of this class + return self.__deserialize(data, sub_klass) + except: # noqa: E722 + pass + # none matched -> error + raise rest.ApiException(status=0, reason=error_msg('anyOf')) + + if hierarchy['oneOf']: + matches = [] + for sub_klass in hierarchy['oneOf']: + try: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == 1: + return matches[0] + + # none matched, or more than one matched -> error + raise rest.ApiException(status=0, reason=error_msg('oneOf')) + if hasattr(instance, 'get_real_child_model'): klass_name = instance.get_real_child_model(data) if klass_name: diff --git a/samples/client/petstore/python-asyncio/petstore_api/models/cat.py b/samples/client/petstore/python-asyncio/petstore_api/models/cat.py index 216e5123538c..009c310a694a 100644 --- a/samples/client/petstore/python-asyncio/petstore_api/models/cat.py +++ b/samples/client/petstore/python-asyncio/petstore_api/models/cat.py @@ -38,6 +38,12 @@ class Cat(object): 'declawed': 'declawed' } + composed_hierarchy = { + 'anyOf': [], + 'allOf': ["Animal", "CatAllOf"], + 'oneOf': [], + } + def __init__(self, declawed=None): # noqa: E501 """Cat - a model defined in OpenAPI""" # noqa: E501 diff --git a/samples/client/petstore/python-asyncio/petstore_api/models/dog.py b/samples/client/petstore/python-asyncio/petstore_api/models/dog.py index c325cb252c32..fb3dc6740796 100644 --- a/samples/client/petstore/python-asyncio/petstore_api/models/dog.py +++ b/samples/client/petstore/python-asyncio/petstore_api/models/dog.py @@ -38,6 +38,12 @@ class Dog(object): 'breed': 'breed' } + composed_hierarchy = { + 'anyOf': [], + 'allOf': ["Animal", "DogAllOf"], + 'oneOf': [], + } + def __init__(self, breed=None): # noqa: E501 """Dog - a model defined in OpenAPI""" # noqa: E501 diff --git a/samples/client/petstore/python-tornado/petstore_api/api_client.py b/samples/client/petstore/python-tornado/petstore_api/api_client.py index 59c17a32f28b..8944af1960a4 100644 --- a/samples/client/petstore/python-tornado/petstore_api/api_client.py +++ b/samples/client/petstore/python-tornado/petstore_api/api_client.py @@ -78,6 +78,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None, self.cookie = cookie # Set default User-Agent. self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.__deserializer_track = [] def __del__(self): if self._pool: @@ -263,6 +264,7 @@ def __deserialize(self, data, klass): """ if data is None: return None + self.__deserializer_track.append(klass) if type(klass) == str: if klass.startswith('list['): @@ -635,6 +637,60 @@ def __deserialize_model(self, data, klass): instance = klass(**kwargs) + if hasattr(instance, 'composed_hierarchy'): + hierarchy = instance.composed_hierarchy + if sum([1 if v else 0 for v in hierarchy.values()]) > 1: + # ignore implicit AllOf + return instance + + def error_msg(composed): + return "Failed to parse `{0}` as {1} ({2} {3})" \ + .format(data, klass, + composed, + ' '.join(hierarchy[composed])) + + if hierarchy['allOf']: + matches = [] + for sub_klass in hierarchy['allOf']: + try: + if sub_klass in self.__deserializer_track: + # seen it before, break the recursion + matches.append(None) + else: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == len(hierarchy['allOf']): + self.__deserializer_track = [] + return instance + + # not all matched -> error + raise rest.ApiException(status=0, reason=error_msg('allOf')) + + if hierarchy['anyOf']: + for sub_klass in hierarchy['anyOf']: + try: + # if at least one of the sub-class matches + # terminate with an instance of this class + return self.__deserialize(data, sub_klass) + except: # noqa: E722 + pass + # none matched -> error + raise rest.ApiException(status=0, reason=error_msg('anyOf')) + + if hierarchy['oneOf']: + matches = [] + for sub_klass in hierarchy['oneOf']: + try: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == 1: + return matches[0] + + # none matched, or more than one matched -> error + raise rest.ApiException(status=0, reason=error_msg('oneOf')) + if hasattr(instance, 'get_real_child_model'): klass_name = instance.get_real_child_model(data) if klass_name: diff --git a/samples/client/petstore/python-tornado/petstore_api/models/cat.py b/samples/client/petstore/python-tornado/petstore_api/models/cat.py index 216e5123538c..009c310a694a 100644 --- a/samples/client/petstore/python-tornado/petstore_api/models/cat.py +++ b/samples/client/petstore/python-tornado/petstore_api/models/cat.py @@ -38,6 +38,12 @@ class Cat(object): 'declawed': 'declawed' } + composed_hierarchy = { + 'anyOf': [], + 'allOf': ["Animal", "CatAllOf"], + 'oneOf': [], + } + def __init__(self, declawed=None): # noqa: E501 """Cat - a model defined in OpenAPI""" # noqa: E501 diff --git a/samples/client/petstore/python-tornado/petstore_api/models/dog.py b/samples/client/petstore/python-tornado/petstore_api/models/dog.py index c325cb252c32..fb3dc6740796 100644 --- a/samples/client/petstore/python-tornado/petstore_api/models/dog.py +++ b/samples/client/petstore/python-tornado/petstore_api/models/dog.py @@ -38,6 +38,12 @@ class Dog(object): 'breed': 'breed' } + composed_hierarchy = { + 'anyOf': [], + 'allOf': ["Animal", "DogAllOf"], + 'oneOf': [], + } + def __init__(self, breed=None): # noqa: E501 """Dog - a model defined in OpenAPI""" # noqa: E501 diff --git a/samples/client/petstore/python/petstore_api/api_client.py b/samples/client/petstore/python/petstore_api/api_client.py index df3a9815aa04..683dcd2041d1 100644 --- a/samples/client/petstore/python/petstore_api/api_client.py +++ b/samples/client/petstore/python/petstore_api/api_client.py @@ -77,6 +77,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None, self.cookie = cookie # Set default User-Agent. self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.__deserializer_track = [] def __del__(self): if self._pool: @@ -261,6 +262,7 @@ def __deserialize(self, data, klass): """ if data is None: return None + self.__deserializer_track.append(klass) if type(klass) == str: if klass.startswith('list['): @@ -633,6 +635,60 @@ def __deserialize_model(self, data, klass): instance = klass(**kwargs) + if hasattr(instance, 'composed_hierarchy'): + hierarchy = instance.composed_hierarchy + if sum([1 if v else 0 for v in hierarchy.values()]) > 1: + # ignore implicit AllOf + return instance + + def error_msg(composed): + return "Failed to parse `{0}` as {1} ({2} {3})" \ + .format(data, klass, + composed, + ' '.join(hierarchy[composed])) + + if hierarchy['allOf']: + matches = [] + for sub_klass in hierarchy['allOf']: + try: + if sub_klass in self.__deserializer_track: + # seen it before, break the recursion + matches.append(None) + else: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == len(hierarchy['allOf']): + self.__deserializer_track = [] + return instance + + # not all matched -> error + raise rest.ApiException(status=0, reason=error_msg('allOf')) + + if hierarchy['anyOf']: + for sub_klass in hierarchy['anyOf']: + try: + # if at least one of the sub-class matches + # terminate with an instance of this class + return self.__deserialize(data, sub_klass) + except: # noqa: E722 + pass + # none matched -> error + raise rest.ApiException(status=0, reason=error_msg('anyOf')) + + if hierarchy['oneOf']: + matches = [] + for sub_klass in hierarchy['oneOf']: + try: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == 1: + return matches[0] + + # none matched, or more than one matched -> error + raise rest.ApiException(status=0, reason=error_msg('oneOf')) + if hasattr(instance, 'get_real_child_model'): klass_name = instance.get_real_child_model(data) if klass_name: diff --git a/samples/client/petstore/python/petstore_api/models/cat.py b/samples/client/petstore/python/petstore_api/models/cat.py index 216e5123538c..009c310a694a 100644 --- a/samples/client/petstore/python/petstore_api/models/cat.py +++ b/samples/client/petstore/python/petstore_api/models/cat.py @@ -38,6 +38,12 @@ class Cat(object): 'declawed': 'declawed' } + composed_hierarchy = { + 'anyOf': [], + 'allOf': ["Animal", "CatAllOf"], + 'oneOf': [], + } + def __init__(self, declawed=None): # noqa: E501 """Cat - a model defined in OpenAPI""" # noqa: E501 diff --git a/samples/client/petstore/python/petstore_api/models/dog.py b/samples/client/petstore/python/petstore_api/models/dog.py index c325cb252c32..fb3dc6740796 100644 --- a/samples/client/petstore/python/petstore_api/models/dog.py +++ b/samples/client/petstore/python/petstore_api/models/dog.py @@ -38,6 +38,12 @@ class Dog(object): 'breed': 'breed' } + composed_hierarchy = { + 'anyOf': [], + 'allOf': ["Animal", "DogAllOf"], + 'oneOf': [], + } + def __init__(self, breed=None): # noqa: E501 """Dog - a model defined in OpenAPI""" # noqa: E501 diff --git a/samples/client/petstore/python/tests/test_deserialization.py b/samples/client/petstore/python/tests/test_deserialization.py index 6c4e083d1cd7..a1d9c018068c 100644 --- a/samples/client/petstore/python/tests/test_deserialization.py +++ b/samples/client/petstore/python/tests/test_deserialization.py @@ -85,7 +85,7 @@ def test_deserialize_dict_str_dog(self): "id": 0, "className": "Dog", "color": "white", - "bread": "Jack Russel Terrier" + "breed": "Jack Russel Terrier" } } response = MockResponse(data=json.dumps(data)) diff --git a/samples/openapi3/client/petstore/python/petstore_api/api_client.py b/samples/openapi3/client/petstore/python/petstore_api/api_client.py index df3a9815aa04..683dcd2041d1 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/api_client.py +++ b/samples/openapi3/client/petstore/python/petstore_api/api_client.py @@ -77,6 +77,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None, self.cookie = cookie # Set default User-Agent. self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.__deserializer_track = [] def __del__(self): if self._pool: @@ -261,6 +262,7 @@ def __deserialize(self, data, klass): """ if data is None: return None + self.__deserializer_track.append(klass) if type(klass) == str: if klass.startswith('list['): @@ -633,6 +635,60 @@ def __deserialize_model(self, data, klass): instance = klass(**kwargs) + if hasattr(instance, 'composed_hierarchy'): + hierarchy = instance.composed_hierarchy + if sum([1 if v else 0 for v in hierarchy.values()]) > 1: + # ignore implicit AllOf + return instance + + def error_msg(composed): + return "Failed to parse `{0}` as {1} ({2} {3})" \ + .format(data, klass, + composed, + ' '.join(hierarchy[composed])) + + if hierarchy['allOf']: + matches = [] + for sub_klass in hierarchy['allOf']: + try: + if sub_klass in self.__deserializer_track: + # seen it before, break the recursion + matches.append(None) + else: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == len(hierarchy['allOf']): + self.__deserializer_track = [] + return instance + + # not all matched -> error + raise rest.ApiException(status=0, reason=error_msg('allOf')) + + if hierarchy['anyOf']: + for sub_klass in hierarchy['anyOf']: + try: + # if at least one of the sub-class matches + # terminate with an instance of this class + return self.__deserialize(data, sub_klass) + except: # noqa: E722 + pass + # none matched -> error + raise rest.ApiException(status=0, reason=error_msg('anyOf')) + + if hierarchy['oneOf']: + matches = [] + for sub_klass in hierarchy['oneOf']: + try: + matches.append(self.__deserialize(data, sub_klass)) + except: # noqa: E722 + pass + if len(matches) == 1: + return matches[0] + + # none matched, or more than one matched -> error + raise rest.ApiException(status=0, reason=error_msg('oneOf')) + if hasattr(instance, 'get_real_child_model'): klass_name = instance.get_real_child_model(data) if klass_name: diff --git a/samples/openapi3/client/petstore/python/petstore_api/models/cat.py b/samples/openapi3/client/petstore/python/petstore_api/models/cat.py index 216e5123538c..009c310a694a 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/models/cat.py +++ b/samples/openapi3/client/petstore/python/petstore_api/models/cat.py @@ -38,6 +38,12 @@ class Cat(object): 'declawed': 'declawed' } + composed_hierarchy = { + 'anyOf': [], + 'allOf': ["Animal", "CatAllOf"], + 'oneOf': [], + } + def __init__(self, declawed=None): # noqa: E501 """Cat - a model defined in OpenAPI""" # noqa: E501 diff --git a/samples/openapi3/client/petstore/python/petstore_api/models/dog.py b/samples/openapi3/client/petstore/python/petstore_api/models/dog.py index c325cb252c32..fb3dc6740796 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/models/dog.py +++ b/samples/openapi3/client/petstore/python/petstore_api/models/dog.py @@ -38,6 +38,12 @@ class Dog(object): 'breed': 'breed' } + composed_hierarchy = { + 'anyOf': [], + 'allOf': ["Animal", "DogAllOf"], + 'oneOf': [], + } + def __init__(self, breed=None): # noqa: E501 """Dog - a model defined in OpenAPI""" # noqa: E501