diff --git a/languages/python/README.md b/languages/python/README.md new file mode 100644 index 00000000..cda80b25 --- /dev/null +++ b/languages/python/README.md @@ -0,0 +1,88 @@ +# STACKIT Python SDK Generator + +## Quick Update Guide + +| File | Action | +|------------------------------------|-----------------------------------------| +| `configuration.mustache` | do nothing | +| `readme.mustache` | do nothing | +| `pyproject.mustache` | check dependencies, adjust if necessary | +| `rest.mustache` | port changes | +| `api.mustache` | port changes | +| `api_client.mustache` | port changes | +| `model_generic.mustache` | port changes | +| `model_oneof.mustache` | port changes | +| `exceptions.mustache` | port changes | +| `__init__package.mustache` | port changes | +| `README_onlypackage.mustache` | do nothing, unchanged | +| `api_doc.mustache` | do nothing, unchanged | +| `api_doc_example.mustache` | do nothing, unchanged | +| `api_response.mustache` | do nothing, unchanged | +| `api_test.mustache` | do nothing, unchanged | +| `asyncio` | do nothing, unchanged | +| `common_README.mustache` | do nothing, unchanged | +| `__init__.mustache` | do nothing, unchanged | +| `__init__api.mustache` | do nothing, unchanged | +| `__init__model.mustache` | do nothing, unchanged | +| `git_push.sh.mustache` | do nothing, unchanged | +| `github-workflow.mustache` | do nothing, unchanged | +| `gitignore.mustache` | do nothing, unchanged | +| `gitlab-ci.mustache` | do nothing, unchanged | +| `model.mustache` | do nothing, unchanged | +| `model_anyof.mustache` | do nothing, unchanged | +| `model_doc.mustache` | do nothing, unchanged | +| `model_enum.mustache` | do nothing, unchanged | +| `model_test.mustache` | do nothing, unchanged | +| `partial_api.mustache` | do nothing, unchanged | +| `partial_api_args.mustache` | do nothing, unchanged | +| `partial_header.mustache` | do nothing, unchanged | +| `py.typed.mustache` | do nothing, unchanged | +| `python_doc_auth_partial.mustache` | do nothing, unchanged | +| `requirements.mustache` | do nothing, unchanged | +| `rest.mustache` | do nothing, unchanged | +| `setup.mustache` | do nothing, unchanged | +| `setup_cfg.mustache` | do nothing, unchanged | +| `signing.mustache` | do nothing, unchanged | +| `test-requirements.mustache` | do nothing, unchanged | +| `tornado` | do nothing, unchanged | +| `tox.mustache` | do nothing, unchanged | +| `travis.mustache` | do nothing, unchanged | +| `exports_api.mustache` | do nothing, unchanged | +| `exports_model.mustache` | do nothing, unchanged | +| `exports_package.mustache` | do nothing, unchanged | + + +If upstream contains a template, not listed here, adjust the table accordingly. + +## Template adjustments + +The following templates were customized but don't need to be adjusted when updating the Python SDK generator to a newer +upstream version: + +- `configuration.mustache`: This template was entirely overwritten to provide a custom `HostConfiguration`. The + `Configuration` from `core` is used instead of generating a new one for each service. +- `readme.mustache`: it's the readme +- `pyproject.mustache`: heavily customized to use `uv`, just check the dependencies + +The following templates were customized and need to be checked for adjustments when updating the Python SDK generator to +a newer upstream version: + +- `rest.mustache`: + - uses `requests` instead of `urllib3` for making HTTP requests. This is done to use the same library as `core` does. + `core` uses `requests` because of an ADR: "HTTP Client for Python Core Implementation" (internal link). + - upstream also configures a `urllib3.PoolManager` for each API client. In `requests` this is done by using + `requests.Session()` for doing requests. A global pool can be used by setting `custom_http_session` on `Config`. +- `api.mustache`: + - customized to use `core`'s `Configuration` +- `api_client.mustache`: + - customized to use `core`'s `Configuration` + - `update_params_for_auth` and `_apply_auth_params` removed, authentication is done in `core` +- `model_generic.mustache`: + - contains a temporary workaround for the year 0 issue +- `model_oneof.mustache`: + - workaround for pattern containing leading and trailing `/` + - removal of `ValueError` if multiple matches are found +- `exceptions.mustache`: + - use `requests` instead of `urllib3` +- `__init__package.mustache`: + - customized imports diff --git a/languages/python/templates/__init__package.mustache b/languages/python/templates/__init__package.mustache index cbe100cc..6f1bdfd0 100644 --- a/languages/python/templates/__init__package.mustache +++ b/languages/python/templates/__init__package.mustache @@ -1,9 +1,11 @@ +{{! This template was customized. See https://github.com/OpenAPITools/openapi-generator/blob/v7.20.0/modules/openapi-generator/src/main/resources/python/__init__package.mustache for the original template }} # coding: utf-8 # flake8: noqa {{>partial_header}} + __version__ = "{{packageVersion}}" # Define package exports @@ -11,7 +13,9 @@ __all__ = [ {{#apiInfo}}{{#apis}}"{{classname}}", {{/apis}}{{/apiInfo}}"ApiResponse", "ApiClient", +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config }} "HostConfiguration", +{{! TEMPLATE CUSTOMIZATION - END - custom config }} "OpenApiException", "ApiTypeError", "ApiValueError", @@ -23,6 +27,7 @@ __all__ = [ {{/-last}}{{#-last}},{{/-last}}{{/model}}{{/models}} ] +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom imports }} # import apis into sdk package {{#apiInfo}}{{#apis}}from {{apiPackage}}.{{classFilename}} import {{classname}} as {{classname}} {{/apis}}{{/apiInfo}} @@ -46,6 +51,7 @@ from {{packageName}}.signing import HttpSigningConfiguration as HttpSigningConfi from {{modelPackage}}.{{classFilename}} import {{classname}} as {{classname}} {{/model}} {{/models}} +{{! TEMPLATE CUSTOMIZATION - END - custom imports }} {{#recursionLimit}} __import__('sys').setrecursionlimit({{{.}}}) diff --git a/languages/python/templates/api.mustache b/languages/python/templates/api.mustache index e8c70976..939a8a6d 100644 --- a/languages/python/templates/api.mustache +++ b/languages/python/templates/api.mustache @@ -1,3 +1,4 @@ +{{! This template was customized. See https://github.com/OpenAPITools/openapi-generator/blob/v7.20.0/modules/openapi-generator/src/main/resources/python/api.mustache for the original template }} # coding: utf-8 {{>partial_header}} @@ -10,7 +11,9 @@ from typing_extensions import Annotated {{import}} {{/imports}} +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config }} from stackit.core.configuration import Configuration +{{! TEMPLATE CUSTOMIZATION - END - custom config }} from {{packageName}}.api_client import ApiClient, RequestSerialized from {{packageName}}.api_response import ApiResponse from {{packageName}}.rest import RESTResponseType @@ -24,22 +27,25 @@ class {{classname}}: Do not edit the class manually. """ +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config }} def __init__(self, configuration: Configuration = None) -> None: if configuration is None: configuration = Configuration() self.configuration = configuration self.api_client = ApiClient(self.configuration) +{{! TEMPLATE CUSTOMIZATION - END - custom config }} {{#operation}} @validate_call - {{#asyncio}}async {{/asyncio}}def {{operationId}}{{>partial_api_args}} -> {{{returnType}}}{{^returnType}}None{{/returnType}}: + {{#async}}async {{/async}}def {{operationId}}{{>partial_api_args}} -> {{{returnType}}}{{^returnType}}None{{/returnType}}: {{>partial_api}} - response_data = {{#asyncio}}await {{/asyncio}}self.api_client.call_api( + + response_data = {{#async}}await {{/async}}self.api_client.call_api( *_param, _request_timeout=_request_timeout ) - {{#asyncio}}await {{/asyncio}}response_data.read() + {{#async}}await {{/async}}response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, @@ -47,13 +53,14 @@ class {{classname}}: @validate_call - {{#asyncio}}async {{/asyncio}}def {{operationId}}_with_http_info{{>partial_api_args}} -> ApiResponse[{{{returnType}}}{{^returnType}}None{{/returnType}}]: + {{#async}}async {{/async}}def {{operationId}}_with_http_info{{>partial_api_args}} -> ApiResponse[{{{returnType}}}{{^returnType}}None{{/returnType}}]: {{>partial_api}} - response_data = {{#asyncio}}await {{/asyncio}}self.api_client.call_api( + + response_data = {{#async}}await {{/async}}self.api_client.call_api( *_param, _request_timeout=_request_timeout ) - {{#asyncio}}await {{/asyncio}}response_data.read() + {{#async}}await {{/async}}response_data.read() return self.api_client.response_deserialize( response_data=response_data, response_types_map=_response_types_map, @@ -61,9 +68,10 @@ class {{classname}}: @validate_call - {{#asyncio}}async {{/asyncio}}def {{operationId}}_without_preload_content{{>partial_api_args}} -> RESTResponseType: + {{#async}}async {{/async}}def {{operationId}}_without_preload_content{{>partial_api_args}} -> RESTResponseType: {{>partial_api}} - response_data = {{#asyncio}}await {{/asyncio}}self.api_client.call_api( + + response_data = {{#async}}await {{/async}}self.api_client.call_api( *_param, _request_timeout=_request_timeout ) @@ -183,7 +191,7 @@ class {{classname}}: {{#constantParams}} {{#isQueryParam}} # Set client side default value of Query Param "{{baseName}}". - _query_params['{{baseName}}'] = {{#_enum}}'{{{.}}}'{{/_enum}} + _query_params.append(('{{baseName}}', {{#_enum}}'{{{.}}}'{{/_enum}})) {{/isQueryParam}} {{#isHeaderParam}} # Set client side default value of Header Param "{{baseName}}". diff --git a/languages/python/templates/api_client.mustache b/languages/python/templates/api_client.mustache index 410eae0a..64ac142f 100644 --- a/languages/python/templates/api_client.mustache +++ b/languages/python/templates/api_client.mustache @@ -1,3 +1,4 @@ +{{! This template was customized. See https://github.com/OpenAPITools/openapi-generator/blob/v7.20.0/modules/openapi-generator/src/main/resources/python/api_client.mustache for the original template }} # coding: utf-8 {{>partial_header}} @@ -5,17 +6,24 @@ import datetime from dateutil.parser import parse from enum import Enum +import decimal import json import mimetypes import os import re import tempfile +import uuid from urllib.parse import quote from typing import Tuple, Optional, List, Dict, Union from pydantic import SecretStr +{{#tornado}} +import tornado.gen +{{/tornado}} +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config }} from stackit.core.configuration import Configuration +{{! TEMPLATE CUSTOMIZATION - END - custom config }} from {{packageName}}.configuration import HostConfiguration from {{packageName}}.api_response import ApiResponse, T as ApiResponseT import {{modelPackage}} @@ -57,16 +65,21 @@ class ApiClient: 'bool': bool, 'date': datetime.date, 'datetime': datetime.datetime, + 'decimal': decimal.Decimal, 'object': object, } + _pool = None def __init__( self, +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config }} configuration, +{{! TEMPLATE CUSTOMIZATION - END - custom config }} header_name=None, header_value=None, cookie=None ) -> None: +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config }} self.config: Configuration = configuration if self.config.custom_endpoint is None: @@ -77,6 +90,7 @@ class ApiClient: self.host = host_config.host else: self.host = self.config.custom_endpoint +{{! TEMPLATE CUSTOMIZATION - END - custom config }} self.rest_client = rest.RESTClientObject(self.config) self.default_headers = {} @@ -85,6 +99,7 @@ class ApiClient: self.cookie = cookie # Set default User-Agent. self.user_agent = '{{{httpUserAgent}}}{{^httpUserAgent}}OpenAPI-Generator/{{{packageVersion}}}/python{{/httpUserAgent}}' +{{! TEMPLATE CUSTOMIZATION - DELETE - set client_side_validation from config }} {{#asyncio}} async def __aenter__(self): @@ -180,6 +195,7 @@ class ApiClient: body, post_params, files) """ +{{! TEMPLATE CUSTOMIZATION - DELETE - set config }} # header parameters header_params = header_params or {} header_params.update(self.default_headers) @@ -202,7 +218,9 @@ class ApiClient: # specified safe chars, encode everything resource_path = resource_path.replace( '{%s}' % k, +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config }} quote(str(v)) +{{! TEMPLATE CUSTOMIZATION - END - custom config }} ) # post parameters @@ -216,13 +234,16 @@ class ApiClient: if files: post_params.extend(self.files_parameters(files)) +{{! TEMPLATE CUSTOMIZATION - DELETE - call to update_params_for_auth }} # body if body: body = self.sanitize_for_serialization(body) # request url +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config }} if _host is None: url = self.host + resource_path +{{! TEMPLATE CUSTOMIZATION - END - custom config }} else: # use server/host defined in path or operation instead url = _host + resource_path @@ -285,8 +306,10 @@ class ApiClient: """ msg = "RESTResponse.read() must be called before passing it to response_deserialize()" +{{! TEMPLATE CUSTOMIZATION - BEGIN - ValueError instead of assert }} if response_data.data is None: raise ValueError(msg) +{{! TEMPLATE CUSTOMIZATION - END - ValueError instead of assert }} response_type = response_types_map.get(str(response_data.status), None) if not response_type and isinstance(response_data.status, int) and 100 <= response_data.status <= 599: @@ -303,7 +326,7 @@ class ApiClient: return_data = self.__deserialize_file(response_data) elif response_type is not None: match = None - content_type = response_data.getheader('content-type') + content_type = response_data.headers.get('content-type') if content_type is not None: match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) encoding = match.group(1) if match else "utf-8" @@ -320,7 +343,7 @@ class ApiClient: return ApiResponse( status_code = response_data.status, data = return_data, - headers = response_data.getheaders(), + headers = response_data.headers, raw_data = response_data.data ) @@ -332,6 +355,7 @@ class ApiClient: 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 decimal.Decimal return string representation. 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. @@ -347,6 +371,8 @@ class ApiClient: return obj.get_secret_value() elif isinstance(obj, self.PRIMITIVE_TYPES): return obj + elif isinstance(obj, uuid.UUID): + return str(obj) elif isinstance(obj, list): return [ self.sanitize_for_serialization(sub_obj) for sub_obj in obj @@ -357,6 +383,8 @@ class ApiClient: ) elif isinstance(obj, (datetime.datetime, datetime.date)): return obj.isoformat() + elif isinstance(obj, decimal.Decimal): + return str(obj) elif isinstance(obj, dict): obj_dict = obj @@ -366,7 +394,7 @@ class ApiClient: # and attributes which value is not None. # Convert attribute name to json key in # model definition for request. - if hasattr(obj, 'to_dict') and callable(obj.to_dict): + if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')): obj_dict = obj.to_dict() else: obj_dict = obj.__dict__ @@ -397,7 +425,7 @@ class ApiClient: data = json.loads(response_text) except ValueError: data = response_text - elif re.match(r'^application/(json|[\w!#$&.+-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE): + elif re.match(r'^application/(json|[\w!#$&.+\-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE): if response_text == "": data = "" else: @@ -426,16 +454,20 @@ class ApiClient: if isinstance(klass, str): if klass.startswith('List['): m = re.match(r'List\[(.*)]', klass) +{{! TEMPLATE CUSTOMIZATION - BEGIN - ValueError instead of assert }} if m is None: raise ValueError("Malformed List type definition") +{{! TEMPLATE CUSTOMIZATION - END - ValueError instead of assert }} sub_kls = m.group(1) return [self.__deserialize(sub_data, sub_kls) for sub_data in data] if klass.startswith('Dict['): m = re.match(r'Dict\[([^,]*), (.*)]', klass) - if m is None: +{{! TEMPLATE CUSTOMIZATION - BEGIN - ValueError instead of assert }} + if m is None: raise ValueError("Malformed Dict type definition") +{{! TEMPLATE CUSTOMIZATION - END - ValueError instead of assert }} sub_kls = m.group(2) return {k: self.__deserialize(v, sub_kls) for k, v in data.items()} @@ -448,12 +480,14 @@ class ApiClient: if klass in self.PRIMITIVE_TYPES: return self.__deserialize_primitive(data, klass) - elif klass == object: + elif klass is object: return self.__deserialize_object(data) - elif klass == datetime.date: + elif klass is datetime.date: return self.__deserialize_date(data) - elif klass == datetime.datetime: + elif klass is datetime.datetime: return self.__deserialize_datetime(data) + elif klass is decimal.Decimal: + return decimal.Decimal(data) elif issubclass(klass, Enum): return self.__deserialize_enum(data, klass) else: @@ -592,6 +626,7 @@ class ApiClient: return content_type return content_types[0] +{{! TEMPLATE CUSTOMIZATION - DELETE - update_params_for_auth and _apply_auth_params }} def __deserialize_file(self, response): """Deserializes body to file @@ -609,15 +644,19 @@ class ApiClient: os.close(fd) os.remove(path) - content_disposition = response.getheader("Content-Disposition") + content_disposition = response.headers.get("Content-Disposition") if content_disposition: m = re.search( r'filename=[\'"]?([^\'"\s]+)[\'"]?', content_disposition ) +{{! TEMPLATE CUSTOMIZATION - BEGIN - ValueError instead of assert }} if m is None: raise ValueError("Unexpected 'content-disposition' header value") - filename = m.group(1) +{{! TEMPLATE CUSTOMIZATION - END - ValueError instead of assert }} + filename = os.path.basename(m.group(1)) # Strip any directory traversal + if filename in ("", ".", ".."): # fall back to tmp filename + filename = os.path.basename(path) path = os.path.join(os.path.dirname(path), filename) with open(path, "wb") as f: diff --git a/languages/python/templates/exceptions.mustache b/languages/python/templates/exceptions.mustache index e53ba94c..4ef22932 100644 --- a/languages/python/templates/exceptions.mustache +++ b/languages/python/templates/exceptions.mustache @@ -1,6 +1,8 @@ +{{! This template was customized. See https://github.com/OpenAPITools/openapi-generator/blob/v7.20.0/modules/openapi-generator/src/main/resources/python/exceptions.mustache for the original template }} # coding: utf-8 {{>partial_header}} + from typing import Any, Optional from typing_extensions import Self @@ -94,9 +96,9 @@ class ApiKeyError(OpenApiException, KeyError): class ApiException(OpenApiException): def __init__( - self, - status=None, - reason=None, + self, + status=None, + reason=None, http_resp=None, *, body: Optional[str] = None, @@ -118,14 +120,14 @@ class ApiException(OpenApiException): self.body = http_resp.data.decode('utf-8') except Exception: # noqa: S110 pass - self.headers = http_resp.getheaders() + self.headers = http_resp.headers @classmethod def from_response( - cls, - *, - http_resp, - body: Optional[str], + cls, + *, + http_resp, + body: Optional[str], data: Optional[Any], ) -> Self: if http_resp.status == 400: @@ -159,8 +161,10 @@ class ApiException(OpenApiException): error_message += "HTTP response headers: {0}\n".format( self.headers) +{{! TEMPLATE CUSTOMIZATION - BEGIN - requests instead of urllib3 }} if self.data or self.body: error_message += "HTTP response body: {0}\n".format(self.data or self.body) +{{! TEMPLATE CUSTOMIZATION - END - requests instead of urllib3 }} return error_message diff --git a/languages/python/templates/model_generic.mustache b/languages/python/templates/model_generic.mustache index 028d086c..2508acbf 100644 --- a/languages/python/templates/model_generic.mustache +++ b/languages/python/templates/model_generic.mustache @@ -1,3 +1,4 @@ +{{! This template was customized. See https://github.com/OpenAPITools/openapi-generator/blob/v7.20.0/modules/openapi-generator/src/main/resources/python/model_generic.mustache for the original template }} from __future__ import annotations import pprint import re # noqa: F401 @@ -9,7 +10,9 @@ import json {{#vendorExtensions.x-py-model-imports}} {{{.}}} {{/vendorExtensions.x-py-model-imports}} +{{! TEMPLATE CUSTOMIZATION - BEGIN - import for workaround below }} from pydantic import field_validator +{{! TEMPLATE CUSTOMIZATION - END - import for workaround below }} from typing import Optional, Set from typing_extensions import Self @@ -190,10 +193,10 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} # override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list of list) _items = [] if self.{{{name}}}: - for _item in self.{{{name}}}: - if _item: + for _item_{{{name}}} in self.{{{name}}}: + if _item_{{{name}}}: _items.append( - [_inner_item.to_dict() for _inner_item in _item if _inner_item is not None] + [_inner_item.to_dict() for _inner_item in _item_{{{name}}} if _inner_item is not None] ) _dict['{{{baseName}}}'] = _items {{/items.items.isPrimitiveType}} @@ -204,9 +207,9 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} # override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list) _items = [] if self.{{{name}}}: - for _item in self.{{{name}}}: - if _item: - _items.append(_item.to_dict()) + for _item_{{{name}}} in self.{{{name}}}: + if _item_{{{name}}}: + _items.append(_item_{{{name}}}.to_dict()) _dict['{{{baseName}}}'] = _items {{/items.isEnumOrRef}} {{/items.isPrimitiveType}} @@ -218,10 +221,10 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} # override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict of array) _field_dict_of_array = {} if self.{{{name}}}: - for _key in self.{{{name}}}: - if self.{{{name}}}[_key] is not None: - _field_dict_of_array[_key] = [ - _item.to_dict() for _item in self.{{{name}}}[_key] + for _key_{{{name}}} in self.{{{name}}}: + if self.{{{name}}}[_key_{{{name}}}] is not None: + _field_dict_of_array[_key_{{{name}}}] = [ + _item.to_dict() for _item in self.{{{name}}}[_key_{{{name}}}] ] _dict['{{{baseName}}}'] = _field_dict_of_array {{/items.items.isPrimitiveType}} @@ -232,9 +235,9 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} # override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict) _field_dict = {} if self.{{{name}}}: - for _key in self.{{{name}}}: - if self.{{{name}}}[_key]: - _field_dict[_key] = self.{{{name}}}[_key].to_dict() + for _key_{{{name}}} in self.{{{name}}}: + if self.{{{name}}}[_key_{{{name}}}]: + _field_dict[_key_{{{name}}}] = self.{{{name}}}[_key_{{{name}}}].to_dict() _dict['{{{baseName}}}'] = _field_dict {{/items.isEnumOrRef}} {{/items.isPrimitiveType}} @@ -387,7 +390,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} "{{{baseName}}}": {{{dataType}}}.from_dict(obj["{{{baseName}}}"]) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} {{/isEnumOrRef}} {{#isEnumOrRef}} - "{{{baseName}}}": obj.get("{{{baseName}}}"){{#defaultValue}} if obj.get("{{baseName}}") is not None else {{defaultValue}}{{/defaultValue}}{{^-last}},{{/-last}} + "{{{baseName}}}": obj.get("{{{baseName}}}"){{#defaultValue}} if obj.get("{{baseName}}") is not None else {{{defaultValue}}}{{/defaultValue}}{{^-last}},{{/-last}} {{/isEnumOrRef}} {{/isPrimitiveType}} {{#isPrimitiveType}} diff --git a/languages/python/templates/model_oneof.mustache b/languages/python/templates/model_oneof.mustache index 699bf022..d71027b2 100644 --- a/languages/python/templates/model_oneof.mustache +++ b/languages/python/templates/model_oneof.mustache @@ -1,7 +1,10 @@ +{{! This template was customized. See https://github.com/OpenAPITools/openapi-generator/blob/v7.20.0/modules/openapi-generator/src/main/resources/python/model_oneof.mustache for the original template }} from __future__ import annotations import json import pprint +{{! TEMPLATE CUSTOMIZATION - BEGIN - workaround }} import re +{{! TEMPLATE CUSTOMIZATION - END - workaround }} {{#vendorExtensions.x-py-other-imports}} {{{.}}} {{/vendorExtensions.x-py-other-imports}} @@ -21,12 +24,14 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{#composedSchemas.oneOf}} # data type: {{{dataType}}} {{#isString}} +{{! TEMPLATE CUSTOMIZATION - BEGIN - workaround }} # BEGIN of the workaround until upstream issues are fixed: # https://github.com/OpenAPITools/openapi-generator/issues/19034 from Jun 28, 2024 # and https://github.com/OpenAPITools/openapi-generator/issues/19842 from Oct 11, 2024 # Tracking issue on our side: https://jira.schwarz/browse/STACKITSDK-227 {{vendorExtensions.x-py-name}}: Optional[Annotated[{{{dataType}}}, Field(strict=True)]] = Field(default=None, description="{{{description}}}"{{#pattern}}, pattern=re.sub(r'^\/|\/$', '',r"{{.}}"){{/pattern}}) # END of the workaround +{{! TEMPLATE CUSTOMIZATION - END - workaround }} {{/isString}} {{^isString}} {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} @@ -94,9 +99,11 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{/isPrimitiveType}} {{/isContainer}} {{/composedSchemas.oneOf}} +{{! TEMPLATE CUSTOMIZATION - BEGIN - accept multiple matches }} if match == 0: # no match raise ValueError("No match found when setting `actual_instance` in {{{classname}}} with oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}. Details: " + ", ".join(error_messages)) +{{! TEMPLATE CUSTOMIZATION - END - accept multiple matches }} else: return v diff --git a/languages/python/templates/rest.mustache b/languages/python/templates/rest.mustache index 4b2157c5..86375547 100644 --- a/languages/python/templates/rest.mustache +++ b/languages/python/templates/rest.mustache @@ -1,3 +1,4 @@ +{{! This template was customized. See https://github.com/OpenAPITools/openapi-generator/blob/v7.20.0/modules/openapi-generator/src/main/resources/python/rest.mustache for the original template }} # coding: utf-8 {{>partial_header}} @@ -6,42 +7,61 @@ import io import json import re +{{! TEMPLATE CUSTOMIZATION - BEGIN - requests instead of urllib3 }} import requests +{{! TEMPLATE CUSTOMIZATION - END - requests instead of urllib3 }} +{{! TEMPLATE CUSTOMIZATION - BEGIN - custom config, auth, errors }} from stackit.core.configuration import Configuration from stackit.core.authorization import Authorization from {{packageName}}.exceptions import ApiException, ApiValueError +{{! TEMPLATE CUSTOMIZATION - END - custom config, auth, errors }} +{{! TEMPLATE CUSTOMIZATION - DELETE - SUPPORTED_SOCKS_PROXIES and is_socks_proxy_url function }} + +{{! TEMPLATE CUSTOMIZATION - BEGIN - requests instead of urllib3 }} RESTResponseType = requests.Response +{{! TEMPLATE CUSTOMIZATION - END - requests instead of urllib3 }} class RESTResponse(io.IOBase): def __init__(self, resp) -> None: self.response = resp +{{! TEMPLATE CUSTOMIZATION - BEGIN - requests instead of urllib3 }} self.status = resp.status_code +{{! TEMPLATE CUSTOMIZATION - END - requests instead of urllib3 }} self.reason = resp.reason self.data = None def read(self): if self.data is None: +{{! TEMPLATE CUSTOMIZATION - BEGIN - requests instead of urllib3 }} self.data = self.response.content +{{! TEMPLATE CUSTOMIZATION - END - requests instead of urllib3 }} return self.data + @property + def headers(self): + """Returns a dictionary of response headers.""" + return self.response.headers + def getheaders(self): - """Returns a dictionary of the response headers.""" + """Returns a dictionary of the response headers; use ``headers`` instead.""" return self.response.headers def getheader(self, name, default=None): - """Returns a given response header.""" + """Returns a given response header; use ``headers.get()`` instead.""" return self.response.headers.get(name, default) class RESTClientObject: def __init__(self, config: Configuration) -> None: +{{! TEMPLATE CUSTOMIZATION - BEGIN - requests instead of urllib3 }} self.session = config.custom_http_session if config.custom_http_session else requests.Session() authorization = Authorization(config) self.session.auth = authorization.auth_method +{{! TEMPLATE CUSTOMIZATION - END - requests instead of urllib3 }} def request( self, @@ -67,6 +87,7 @@ class RESTClientObject: (connection, read) timeouts. """ method = method.upper() +{{! TEMPLATE CUSTOMIZATION - BEGIN - ValueError instead of assert }} if method not in [ 'GET', 'HEAD', @@ -77,6 +98,7 @@ class RESTClientObject: 'OPTIONS' ]: raise ValueError("Method %s not allowed", method) +{{! TEMPLATE CUSTOMIZATION - END - ValueError instead of assert }} if post_params and body: raise ApiValueError( @@ -86,6 +108,7 @@ class RESTClientObject: post_params = post_params or {} headers = headers or {} +{{! TEMPLATE CUSTOMIZATION - DELETE - urllib3 timeout handling }} try: # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: @@ -99,6 +122,7 @@ class RESTClientObject: request_body = None if body is not None: request_body = json.dumps(body{{#setEnsureAsciiToFalse}}, ensure_ascii=False{{/setEnsureAsciiToFalse}}) +{{! TEMPLATE CUSTOMIZATION - BEGIN - requests instead of urllib3 }} r = self.session.request( method, url, @@ -166,5 +190,6 @@ class RESTClientObject: except requests.exceptions.SSLError as e: msg = "\n".join([type(e).__name__, str(e)]) raise ApiException(status=0, reason=msg) +{{! TEMPLATE CUSTOMIZATION - END - requests instead of urllib3 }} return RESTResponse(r) diff --git a/scripts/generate-sdk/generate-sdk.sh b/scripts/generate-sdk/generate-sdk.sh index 8020bf8e..03b2689c 100755 --- a/scripts/generate-sdk/generate-sdk.sh +++ b/scripts/generate-sdk/generate-sdk.sh @@ -60,7 +60,7 @@ go) python) # When the GENERATOR_VERSION changes, migrate also the templates in templates/python # Renovate: datasource=github-tags depName=OpenAPITools/openapi-generator versioning=semver - GENERATOR_VERSION="v7.14.0" + GENERATOR_VERSION="v7.20.0" ;; java) # When the GENERATOR_VERSION changes, migrate also the templates in templates/java