diff --git a/.github/renovate.json b/.github/renovate.json index 11ad200..73446b4 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -14,20 +14,20 @@ "customManagers": [ { "customType": "regex", - "fileMatch": [ - "(^|\\/)generate-sdk\\.sh$" + "managerFilePatterns": [ + "/(^|\\/)generate-sdk\\.sh$/" ], "matchStrings": [ - "# Renovate: datasource=(?.*?) depName=(?.*?) versioning=(?.*?)?\\sGENERATOR_VERSION=\\\"?(?.*?)\\\"\\s" + "# Renovate: datasource=(?.*?) depName=(?.*?) versioning=(?.*?)?\\s+GENERATOR_VERSION=\"?(?.*?)\"" ] }, { "customType": "regex", - "fileMatch": [ - "(^|\\/)go\\.mod\\.mustache$" + "managerFilePatterns": [ + "/(^|\\/)go\\.mod\\.mustache$/" ], "matchStrings": [ - "{{gitHost}}\\/{{gitUserId}}\\/{{gitRepoId}}\\/core ?(?.*?)\\s" + "github.com\/stackitcloud\/stackit-sdk-go\/core (?.*?)\\s" ], "datasourceTemplate": "go", "depNameTemplate": "github.com/stackitcloud/stackit-sdk-go", diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 50a7257..fed36f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -79,7 +79,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} steps: - name: Install SSH Key diff --git a/scripts/generate-sdk/generate-sdk.sh b/scripts/generate-sdk/generate-sdk.sh index 5fac558..aec217b 100755 --- a/scripts/generate-sdk/generate-sdk.sh +++ b/scripts/generate-sdk/generate-sdk.sh @@ -57,7 +57,8 @@ go) GENERATOR_VERSION="v6.6.0" # There are issues with GO SDK generation in version v7 ;; python) - GENERATOR_VERSION="v7.9.0" +# Renovate: datasource=github-tags depName=OpenAPITools/openapi-generator versioning=semver + GENERATOR_VERSION="v7.14.0" ;; *) echo "SDK language not supported." @@ -90,8 +91,8 @@ python) echo -e "\n>> Generating the Python SDK..." source ${LANGUAGE_GENERATORS_FOLDER_PATH}/${LANGUAGE}.sh - # Usage: generate_python_sdk GENERATOR_PATH GIT_HOST GIT_USER_ID [GIT_REPO_ID] [SDK_REPO_URL] - generate_python_sdk ${jar_path} ${GIT_HOST} ${GIT_USER_ID} ${GIT_REPO_ID} ${SDK_REPO_URL} + # Usage: generate_python_sdk GENERATOR_PATH GIT_HOST GIT_USER_ID [GIT_REPO_ID] [SDK_REPO_URL] [SDK_BRANCH] + generate_python_sdk "${jar_path}" "${GIT_HOST}" "${GIT_USER_ID}" "${GIT_REPO_ID}" "${SDK_REPO_URL}" "${SDK_BRANCH}" ;; *) echo "! SDK language not supported." diff --git a/scripts/generate-sdk/languages/python.sh b/scripts/generate-sdk/languages/python.sh index cf749dc..4e3ddac 100644 --- a/scripts/generate-sdk/languages/python.sh +++ b/scripts/generate-sdk/languages/python.sh @@ -21,6 +21,7 @@ generate_python_sdk() { # Optional parameters local GIT_REPO_ID=$4 local SDK_REPO_URL=$5 + local SDK_BRANCH=$6 # Check required parameters if [[ -z ${GIT_HOST} ]]; then @@ -54,7 +55,7 @@ generate_python_sdk() { echo "Old SDK repo clone was found, it will be removed." rm -rf ${SDK_REPO_LOCAL_PATH} fi - git clone --quiet ${SDK_REPO_URL} ${SDK_REPO_LOCAL_PATH} + git clone --quiet -b ${SDK_BRANCH} ${SDK_REPO_URL} ${SDK_REPO_LOCAL_PATH} # Install SDK project tools cd ${ROOT_DIR} diff --git a/templates/python/README.md b/templates/python/README.md new file mode 100644 index 0000000..9c23c0d --- /dev/null +++ b/templates/python/README.md @@ -0,0 +1,14 @@ +# Python templates + +This folder contains only our customized python templates. Beside these customized templates, +the original templates of openapi-generator for python are used. These can be found in the +official GitHub repo of the [openapi-generator](https://github.com/OpenAPITools/openapi-generator/tree/v7.14.0/modules/openapi-generator/src/main/resources/python). + +If you need to change something in the Python Generator, try always first to add +[user-defined templates](https://openapi-generator.tech/docs/customization#user-defined-templates), +instead of overwriting existing templates. These ensure an easier upgrade process, to newer +versions of the openapi-generator. + +If it's required to customize the original templates, you can copy them into this directory. +Try to minimize the customization as much as possible, to ensure, that we can easily upgrade +to newer versions in the future. diff --git a/templates/python/README_onlypackage.mustache b/templates/python/README_onlypackage.mustache deleted file mode 100644 index ae547b1..0000000 --- a/templates/python/README_onlypackage.mustache +++ /dev/null @@ -1,44 +0,0 @@ -# {{{projectName}}} -{{#appDescription}} -{{{.}}} -{{/appDescription}} - -The `{{packageName}}` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: - -- API version: {{appVersion}} -- Package version: {{packageVersion}} -{{^hideGenerationTimestamp}} -- Build date: {{generatedDate}} -{{/hideGenerationTimestamp}} -- Generator version: {{generatorVersion}} -- Build package: {{generatorClass}} -{{#infoUrl}} -For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) -{{/infoUrl}} - -## Requirements. - -Python {{{generatorLanguageVersion}}} - -## Installation & Usage - -This python library package is generated without supporting files like setup.py or requirements files - -To be able to use it, you will need these dependencies in your own package that uses this library: - -* urllib3 >= 1.25.3 -* python-dateutil -{{#asyncio}} -* aiohttp -{{/asyncio}} -{{#tornado}} -* tornado>=4.2,<5 -{{/tornado}} -* pydantic - -## Getting Started - -In your own code, to use this library to connect and interact with {{{projectName}}}, -you can run the following: - -{{> common_README }} diff --git a/templates/python/__init__.mustache b/templates/python/__init__.mustache deleted file mode 100644 index e69de29..0000000 diff --git a/templates/python/__init__api.mustache b/templates/python/__init__api.mustache deleted file mode 100644 index 8870835..0000000 --- a/templates/python/__init__api.mustache +++ /dev/null @@ -1,5 +0,0 @@ -# flake8: noqa - -# import apis into api package -{{#apiInfo}}{{#apis}}from {{apiPackage}}.{{classFilename}} import {{classname}} -{{/apis}}{{/apiInfo}} diff --git a/templates/python/__init__model.mustache b/templates/python/__init__model.mustache deleted file mode 100644 index 0e1b55e..0000000 --- a/templates/python/__init__model.mustache +++ /dev/null @@ -1,11 +0,0 @@ -# coding: utf-8 - -# flake8: noqa -{{>partial_header}} - -# import models into model package -{{#models}} -{{#model}} -from {{modelPackage}}.{{classFilename}} import {{classname}} -{{/model}} -{{/models}} diff --git a/templates/python/__init__package.mustache b/templates/python/__init__package.mustache index df912a3..cbe100c 100644 --- a/templates/python/__init__package.mustache +++ b/templates/python/__init__package.mustache @@ -6,27 +6,44 @@ __version__ = "{{packageVersion}}" +# Define package exports +__all__ = [ + {{#apiInfo}}{{#apis}}"{{classname}}", + {{/apis}}{{/apiInfo}}"ApiResponse", + "ApiClient", + "HostConfiguration", + "OpenApiException", + "ApiTypeError", + "ApiValueError", + "ApiKeyError", + "ApiAttributeError", + "ApiException", + {{#hasHttpSignatureMethods}}"HttpSigningConfiguration", + {{/hasHttpSignatureMethods}}{{#models}}{{#model}}"{{classname}}"{{^-last}}, + {{/-last}}{{#-last}},{{/-last}}{{/model}}{{/models}} +] + # import apis into sdk package -{{#apiInfo}}{{#apis}}from {{apiPackage}}.{{classFilename}} import {{classname}} +{{#apiInfo}}{{#apis}}from {{apiPackage}}.{{classFilename}} import {{classname}} as {{classname}} {{/apis}}{{/apiInfo}} # import ApiClient -from {{packageName}}.api_response import ApiResponse -from {{packageName}}.api_client import ApiClient -from {{packageName}}.configuration import HostConfiguration -from {{packageName}}.exceptions import OpenApiException -from {{packageName}}.exceptions import ApiTypeError -from {{packageName}}.exceptions import ApiValueError -from {{packageName}}.exceptions import ApiKeyError -from {{packageName}}.exceptions import ApiAttributeError -from {{packageName}}.exceptions import ApiException +from {{packageName}}.api_response import ApiResponse as ApiResponse +from {{packageName}}.api_client import ApiClient as ApiClient +from {{packageName}}.configuration import HostConfiguration as HostConfiguration +from {{packageName}}.exceptions import OpenApiException as OpenApiException +from {{packageName}}.exceptions import ApiTypeError as ApiTypeError +from {{packageName}}.exceptions import ApiValueError as ApiValueError +from {{packageName}}.exceptions import ApiKeyError as ApiKeyError +from {{packageName}}.exceptions import ApiAttributeError as ApiAttributeError +from {{packageName}}.exceptions import ApiException as ApiException {{#hasHttpSignatureMethods}} -from {{packageName}}.signing import HttpSigningConfiguration +from {{packageName}}.signing import HttpSigningConfiguration as HttpSigningConfiguration {{/hasHttpSignatureMethods}} # import models into sdk package {{#models}} {{#model}} -from {{modelPackage}}.{{classFilename}} import {{classname}} +from {{modelPackage}}.{{classFilename}} import {{classname}} as {{classname}} {{/model}} {{/models}} {{#recursionLimit}} diff --git a/templates/python/api.mustache b/templates/python/api.mustache index d607df3..e8c7097 100644 --- a/templates/python/api.mustache +++ b/templates/python/api.mustache @@ -103,7 +103,9 @@ class {{classname}}: _query_params: List[Tuple[str, str]] = [] _header_params: Dict[str, Optional[str]] = _headers or {} _form_params: List[Tuple[str, str]] = [] - _files: Dict[str, Union[str, bytes]] = {} + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} _body_params: Optional[bytes] = None # process the path parameters @@ -167,6 +169,9 @@ class {{classname}}: if isinstance({{paramName}}, str): with open({{paramName}}, "rb") as _fp: _body_params = _fp.read() + elif isinstance({{paramName}}, tuple): + # drop the filename from the tuple + _body_params = {{paramName}}[1] else: _body_params = {{paramName}} {{/isBinary}} diff --git a/templates/python/api_client.mustache b/templates/python/api_client.mustache index 8dd1f4b..f4651d7 100644 --- a/templates/python/api_client.mustache +++ b/templates/python/api_client.mustache @@ -32,7 +32,6 @@ from {{packageName}}.exceptions import ( RequestSerialized = Tuple[str, str, Dict[str, str], Optional[str], List[str]] - class ApiClient: """Generic API client for OpenAPI client library builds. @@ -372,6 +371,10 @@ class ApiClient: else: obj_dict = obj.__dict__ + if isinstance(obj_dict, list): + # here we handle instances that can either be a list or something else, and only became a real list by calling to_dict() + return self.sanitize_for_serialization(obj_dict) + return { key: self.sanitize_for_serialization(val) for key, val in obj_dict.items() @@ -394,12 +397,12 @@ class ApiClient: data = json.loads(response_text) except ValueError: data = response_text - elif content_type.startswith("application/json"): + elif re.match(r'^application/(json|[\w!#$&.+-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE): if response_text == "": data = "" else: data = json.loads(response_text) - elif content_type.startswith("text/plain"): + elif re.match(r'^text\/[a-z.+-]+\s*(;|$)', content_type, re.IGNORECASE): data = response_text else: raise ApiException( @@ -507,7 +510,7 @@ class ApiClient: if k in collection_formats: collection_format = collection_formats[k] if collection_format == 'multi': - new_params.extend((k, str(value)) for value in v) + new_params.extend((k, quote(str(value))) for value in v) else: if collection_format == 'ssv': delimiter = ' ' @@ -525,7 +528,10 @@ class ApiClient: return "&".join(["=".join(map(str, item)) for item in new_params]) - def files_parameters(self, files: Dict[str, Union[str, bytes]]): + def files_parameters( + self, + files: Dict[str, Union[str, bytes, List[str], List[bytes], Tuple[str, bytes]]], + ): """Builds form parameters. :param files: File parameters. @@ -540,6 +546,12 @@ class ApiClient: elif isinstance(v, bytes): filename = k filedata = v + elif isinstance(v, tuple): + filename, filedata = v + elif isinstance(v, list): + for file_param in v: + params.extend(self.files_parameters({k: file_param})) + continue else: raise ValueError("Unsupported file value") mimetype = ( diff --git a/templates/python/api_doc.mustache b/templates/python/api_doc.mustache deleted file mode 100644 index 6a58fc9..0000000 --- a/templates/python/api_doc.mustache +++ /dev/null @@ -1,76 +0,0 @@ -# {{packageName}}.{{classname}}{{#description}} -{{.}}{{/description}} - -All URIs are relative to *{{basePath}}* - -Method | HTTP request | Description -------------- | ------------- | ------------- -{{#operations}}{{#operation}}[**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} -{{/operation}}{{/operations}} - -{{#operations}} -{{#operation}} -# **{{{operationId}}}** -> {{#returnType}}{{{.}}} {{/returnType}}{{{operationId}}}({{#allParams}}{{#required}}{{{paramName}}}{{/required}}{{^required}}{{{paramName}}}={{{paramName}}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) - -{{{summary}}}{{#notes}} - -{{{.}}}{{/notes}} - -### Example - -{{#hasAuthMethods}} -{{#authMethods}} -{{#isBasic}} -{{#isBasicBasic}} -* Basic Authentication ({{name}}): -{{/isBasicBasic}} -{{#isBasicBearer}} -* Bearer{{#bearerFormat}} ({{{.}}}){{/bearerFormat}} Authentication ({{name}}): -{{/isBasicBearer}} -{{/isBasic}} -{{#isApiKey}} -* Api Key Authentication ({{name}}): -{{/isApiKey }} -{{#isOAuth}} -* OAuth Authentication ({{name}}): -{{/isOAuth }} -{{/authMethods}} -{{/hasAuthMethods}} -{{> api_doc_example }} - -### Parameters - -{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#allParams}}{{#-last}} -Name | Type | Description | Notes -------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}} -{{#allParams}} **{{paramName}}** | {{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}{{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{dataType}}**]({{baseType}}.md){{/isPrimitiveType}}{{/isFile}}| {{description}} | {{^required}}[optional] {{/required}}{{#defaultValue}}[default to {{.}}]{{/defaultValue}} -{{/allParams}} - -### Return type - -{{#returnType}}{{#returnTypeIsPrimitive}}**{{{returnType}}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{{returnType}}}**]({{returnBaseType}}.md){{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}void (empty response body){{/returnType}} - -### Authorization - -{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{{name}}}](../README.md#{{{name}}}){{^-last}}, {{/-last}}{{/authMethods}} - -### HTTP request headers - - - **Content-Type**: {{#consumes}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} - - **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}} - -{{#responses.0}} -### HTTP response details - -| Status code | Description | Response headers | -|-------------|-------------|------------------| -{{#responses}} -**{{code}}** | {{message}} | {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}} | -{{/responses}} -{{/responses.0}} - -[[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) - -{{/operation}} -{{/operations}} diff --git a/templates/python/api_doc_example.mustache b/templates/python/api_doc_example.mustache deleted file mode 100644 index f5d7eef..0000000 --- a/templates/python/api_doc_example.mustache +++ /dev/null @@ -1,37 +0,0 @@ - -```python -import {{{packageName}}} -{{#vendorExtensions.x-py-example-import}} -{{{.}}} -{{/vendorExtensions.x-py-example-import}} -from {{{packageName}}}.rest import ApiException -from pprint import pprint - -{{> python_doc_auth_partial}} -# Enter a context with an instance of the API client -{{#asyncio}}async {{/asyncio}}with {{{packageName}}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = {{{packageName}}}.{{{classname}}}(api_client) - {{#allParams}} - {{paramName}} = {{{example}}} # {{{dataType}}} | {{{description}}}{{^required}} (optional){{/required}}{{#defaultValue}} (default to {{{.}}}){{/defaultValue}} - {{/allParams}} - - try: - {{#summary}} - # {{{.}}} - {{/summary}} - {{#returnType}}api_response = {{/returnType}}{{#asyncio}}await {{/asyncio}}api_instance.{{{operationId}}}({{#allParams}}{{#required}}{{paramName}}{{/required}}{{^required}}{{paramName}}={{paramName}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) - {{#returnType}} - print("The response of {{classname}}->{{operationId}}:\n") - pprint(api_response) - {{/returnType}} - except Exception as e: - print("Exception when calling {{classname}}->{{operationId}}: %s\n" % e) -``` - -{{#vendorExtensions.x-py-postponed-example-imports.size}} -{{#vendorExtensions.x-py-postponed-example-imports}} -{{{.}}} -{{/vendorExtensions.x-py-postponed-example-imports}} -{{classname}}.update_forward_refs() -{{/vendorExtensions.x-py-postponed-example-imports.size}} diff --git a/templates/python/api_response.mustache b/templates/python/api_response.mustache deleted file mode 100644 index 9bc7c11..0000000 --- a/templates/python/api_response.mustache +++ /dev/null @@ -1,21 +0,0 @@ -"""API response object.""" - -from __future__ import annotations -from typing import Optional, Generic, Mapping, TypeVar -from pydantic import Field, StrictInt, StrictBytes, BaseModel - -T = TypeVar("T") - -class ApiResponse(BaseModel, Generic[T]): - """ - API response object - """ - - status_code: StrictInt = Field(description="HTTP status code") - headers: Optional[Mapping[str, str]] = Field(None, description="HTTP headers") - data: T = Field(description="Deserialized data given the data type") - raw_data: StrictBytes = Field(description="Raw data (HTTP response body)") - - model_config = { - "arbitrary_types_allowed": True - } diff --git a/templates/python/api_test.mustache b/templates/python/api_test.mustache deleted file mode 100644 index 5354d3c..0000000 --- a/templates/python/api_test.mustache +++ /dev/null @@ -1,33 +0,0 @@ -# coding: utf-8 - -{{>partial_header}} - -import unittest - -from {{apiPackage}}.{{classFilename}} import {{classname}} - - -class {{#operations}}Test{{classname}}(unittest.TestCase): - """{{classname}} unit test stubs""" - - def setUp(self) -> None: - self.api = {{classname}}() - - def tearDown(self) -> None: - pass - - {{#operation}} - def test_{{operationId}}(self) -> None: - """Test case for {{{operationId}}} - -{{#summary}} - {{{.}}} -{{/summary}} - """ - pass - - {{/operation}} -{{/operations}} - -if __name__ == '__main__': - unittest.main() diff --git a/templates/python/asyncio/rest.mustache b/templates/python/asyncio/rest.mustache deleted file mode 100644 index edfcc66..0000000 --- a/templates/python/asyncio/rest.mustache +++ /dev/null @@ -1,205 +0,0 @@ -# coding: utf-8 - -{{>partial_header}} - -import io -import json -import re -import ssl -from typing import Optional, Union - -import aiohttp -import aiohttp_retry - -from {{packageName}}.exceptions import ApiException, ApiValueError - -RESTResponseType = aiohttp.ClientResponse - -ALLOW_RETRY_METHODS = frozenset({'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT', 'TRACE'}) - -class RESTResponse(io.IOBase): - - def __init__(self, resp) -> None: - self.response = resp - self.status = resp.status - self.reason = resp.reason - self.data = None - - async def read(self): - if self.data is None: - self.data = await self.response.read() - return self.data - - def getheaders(self): - """Returns a CIMultiDictProxy of the response headers.""" - return self.response.headers - - def getheader(self, name, default=None): - """Returns a given response header.""" - return self.response.headers.get(name, default) - - -class RESTClientObject: - - def __init__(self, configuration) -> None: - - # maxsize is number of requests to host that are allowed in parallel - maxsize = configuration.connection_pool_maxsize - - ssl_context = ssl.create_default_context( - cafile=configuration.ssl_ca_cert - ) - if configuration.cert_file: - ssl_context.load_cert_chain( - configuration.cert_file, keyfile=configuration.key_file - ) - - if not configuration.verify_ssl: - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - - connector = aiohttp.TCPConnector( - limit=maxsize, - ssl=ssl_context - ) - - self.proxy = configuration.proxy - self.proxy_headers = configuration.proxy_headers - - # https pool manager - self.pool_manager = aiohttp.ClientSession( - connector=connector, - trust_env=True - ) - - retries = configuration.retries - self.retry_client: Optional[aiohttp_retry.RetryClient] - if retries is not None: - self.retry_client = aiohttp_retry.RetryClient( - client_session=self.pool_manager, - retry_options=aiohttp_retry.ExponentialRetry( - attempts=retries, - factor=0.0, - start_timeout=0.0, - max_timeout=120.0 - ) - ) - else: - self.retry_client = None - - async def close(self): - await self.pool_manager.close() - if self.retry_client is not None: - await self.retry_client.close() - - async def request( - self, - method, - url, - headers=None, - body=None, - post_params=None, - _request_timeout=None - ): - """Execute request - - :param method: http request method - :param url: http request url - :param headers: http request headers - :param body: request json body, for `application/json` - :param post_params: request post parameters, - `application/x-www-form-urlencoded` - and `multipart/form-data` - :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. - """ - method = method.upper() - assert method in [ - 'GET', - 'HEAD', - 'DELETE', - 'POST', - 'PUT', - 'PATCH', - 'OPTIONS' - ] - - if post_params and body: - raise ApiValueError( - "body parameter cannot be used with post_params parameter." - ) - - post_params = post_params or {} - headers = headers or {} - # url already contains the URL query string - timeout = _request_timeout or 5 * 60 - - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' - - args = { - "method": method, - "url": url, - "timeout": timeout, - "headers": headers - } - - if self.proxy: - args["proxy"] = self.proxy - if self.proxy_headers: - args["proxy_headers"] = self.proxy_headers - - # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` - if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: - if re.search('json', headers['Content-Type'], re.IGNORECASE): - if body is not None: - body = json.dumps(body) - args["data"] = body - elif headers['Content-Type'] == 'application/x-www-form-urlencoded': - args["data"] = aiohttp.FormData(post_params) - elif headers['Content-Type'] == 'multipart/form-data': - # must del headers['Content-Type'], or the correct - # Content-Type which generated by aiohttp - del headers['Content-Type'] - data = aiohttp.FormData() - for param in post_params: - k, v = param - if isinstance(v, tuple) and len(v) == 3: - data.add_field( - k, - value=v[1], - filename=v[0], - content_type=v[2] - ) - else: - data.add_field(k, v) - args["data"] = data - - # Pass a `bytes` or `str` parameter directly in the body to support - # other content types than Json when `body` argument is provided - # in serialized form - elif isinstance(body, str) or isinstance(body, bytes): - args["data"] = body - else: - # Cannot generate the request from given parameters - msg = """Cannot prepare a request message for provided - arguments. Please check that your arguments match - declared content type.""" - raise ApiException(status=0, reason=msg) - - pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient] - if self.retry_client is not None and method in ALLOW_RETRY_METHODS: - pool_manager = self.retry_client - else: - pool_manager = self.pool_manager - - r = await pool_manager.request(**args) - - return RESTResponse(r) - - - - - diff --git a/templates/python/common_README.mustache b/templates/python/common_README.mustache deleted file mode 100644 index b7ce461..0000000 --- a/templates/python/common_README.mustache +++ /dev/null @@ -1,84 +0,0 @@ -```python -{{#apiInfo}}{{#apis}}{{#-last}}{{#hasHttpSignatureMethods}}import datetime{{/hasHttpSignatureMethods}}{{/-last}}{{/apis}}{{/apiInfo}} -import {{{packageName}}} -from {{{packageName}}}.rest import ApiException -from pprint import pprint -{{#apiInfo}}{{#apis}}{{#-first}}{{#operations}}{{#operation}}{{#-first}} -{{> python_doc_auth_partial}} - -# Enter a context with an instance of the API client -{{#asyncio}}async {{/asyncio}}with {{{packageName}}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = {{{packageName}}}.{{{classname}}}(api_client) - {{#allParams}} - {{paramName}} = {{{example}}} # {{{dataType}}} | {{{description}}}{{^required}} (optional){{/required}}{{#defaultValue}} (default to {{{.}}}){{/defaultValue}} - {{/allParams}} - - try: - {{#summary}} - # {{{.}}} - {{/summary}} - {{#returnType}}api_response = {{/returnType}}{{#asyncio}}await {{/asyncio}}api_instance.{{{operationId}}}({{#allParams}}{{#required}}{{paramName}}{{/required}}{{^required}}{{paramName}}={{paramName}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) - {{#returnType}} - print("The response of {{classname}}->{{operationId}}:\n") - pprint(api_response) - {{/returnType}} - except ApiException as e: - print("Exception when calling {{classname}}->{{operationId}}: %s\n" % e) -{{/-first}}{{/operation}}{{/operations}}{{/-first}}{{/apis}}{{/apiInfo}} -``` - -## Documentation for API Endpoints - -All URIs are relative to *{{{basePath}}}* - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{summary}} -{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} - -## Documentation For Models - -{{#models}}{{#model}} - [{{{classname}}}]({{modelDocPath}}{{{classname}}}.md) -{{/model}}{{/models}} - - -## Documentation For Authorization - -{{^authMethods}}Endpoints do not require authorization.{{/authMethods}} -{{#hasAuthMethods}}Authentication schemes defined for the API:{{/hasAuthMethods}} -{{#authMethods}} - -### {{{name}}} - -{{#isApiKey}} -- **Type**: API key -- **API key parameter name**: {{{keyParamName}}} -- **Location**: {{#isKeyInQuery}}URL query string{{/isKeyInQuery}}{{#isKeyInHeader}}HTTP header{{/isKeyInHeader}} -{{/isApiKey}} -{{#isBasic}} -{{#isBasicBasic}} -- **Type**: HTTP basic authentication -{{/isBasicBasic}} -{{#isBasicBearer}} -- **Type**: Bearer authentication{{#bearerFormat}} ({{{.}}}){{/bearerFormat}} -{{/isBasicBearer}} -{{#isHttpSignature}} -- **Type**: HTTP signature authentication -{{/isHttpSignature}} -{{/isBasic}} -{{#isOAuth}} -- **Type**: OAuth -- **Flow**: {{{flow}}} -- **Authorization URL**: {{{authorizationUrl}}} -- **Scopes**: {{^scopes}}N/A{{/scopes}} -{{#scopes}} - **{{{scope}}}**: {{{description}}} -{{/scopes}} -{{/isOAuth}} - -{{/authMethods}} - -## Author - -{{#apiInfo}}{{#apis}}{{#-last}}{{infoEmail}} -{{/-last}}{{/apis}}{{/apiInfo}} diff --git a/templates/python/configuration.mustache b/templates/python/configuration.mustache index 658c805..d3afc3a 100644 --- a/templates/python/configuration.mustache +++ b/templates/python/configuration.mustache @@ -1,8 +1,23 @@ # coding: utf-8 +{{>partial_header}} import sys +from typing import Dict, List, Optional, TypedDict +from typing_extensions import NotRequired + +ServerVariablesT = Dict[str, str] + +class HostSettingVariable(TypedDict): + description: str + default_value: str + enum_values: List[str] + + +class HostSetting(TypedDict): + url: str + description: str + variables: NotRequired[Dict[str, HostSettingVariable]] -{{>partial_header}} class HostConfiguration: def __init__(self, region=None, @@ -32,8 +47,7 @@ class HostConfiguration: self.ignore_operation_servers = ignore_operation_servers """Ignore operation servers """ - - def get_host_settings(self): + def get_host_settings(self) -> List[HostSetting]: """Gets an array of host settings :return: An array of host settings @@ -68,7 +82,12 @@ class HostConfiguration: {{/servers}} ] - def get_host_from_settings(self, index, variables=None, servers=None): + def get_host_from_settings( + self, + index: Optional[int], + variables: Optional[ServerVariablesT]=None, + servers: Optional[List[HostSetting]]=None, + ) -> str: """Gets host URL based on the index and variables :param index: array index of the host settings :param variables: hash of variable and the corresponding value @@ -105,7 +124,7 @@ class HostConfiguration: region_env is None and \ variables.get(variable_name) is not None: raise ValueError( - "this API does not support setting a region in the the client configuration, " + "this API does not support setting a region in the client configuration, " "please check if the region can be specified as a function parameter" ) used_value = variables.get( @@ -127,12 +146,12 @@ class HostConfiguration: return url @property - def host(self): + def host(self) -> str: """Return generated host.""" return self.get_host_from_settings(self.server_index, variables=self.server_variables) @host.setter - def host(self, value): + def host(self, value: str) -> None: """Fix base path.""" self._base_path = value self.server_index = None diff --git a/templates/python/exceptions.mustache b/templates/python/exceptions.mustache deleted file mode 100644 index 1c9fee3..0000000 --- a/templates/python/exceptions.mustache +++ /dev/null @@ -1,189 +0,0 @@ -# coding: utf-8 - -{{>partial_header}} -from typing import Any, Optional -from typing_extensions import Self - -class OpenApiException(Exception): - """The base exception class for all OpenAPIExceptions""" - - -class ApiTypeError(OpenApiException, TypeError): - def __init__(self, msg, path_to_item=None, valid_classes=None, - key_type=None) -> None: - """ Raises an exception for TypeErrors - - Args: - msg (str): the exception message - - Keyword Args: - path_to_item (list): a list of keys an indices to get to the - current_item - None if unset - valid_classes (tuple): the primitive classes that current item - should be an instance of - None if unset - key_type (bool): False if our value is a value in a dict - True if it is a key in a dict - False if our item is an item in a list - None if unset - """ - self.path_to_item = path_to_item - self.valid_classes = valid_classes - self.key_type = key_type - full_msg = msg - if path_to_item: - full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) - super(ApiTypeError, self).__init__(full_msg) - - -class ApiValueError(OpenApiException, ValueError): - def __init__(self, msg, path_to_item=None) -> None: - """ - Args: - msg (str): the exception message - - Keyword Args: - path_to_item (list) the path to the exception in the - received_data dict. None if unset - """ - - self.path_to_item = path_to_item - full_msg = msg - if path_to_item: - full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) - super(ApiValueError, self).__init__(full_msg) - - -class ApiAttributeError(OpenApiException, AttributeError): - def __init__(self, msg, path_to_item=None) -> None: - """ - Raised when an attribute reference or assignment fails. - - Args: - msg (str): the exception message - - Keyword Args: - path_to_item (None/list) the path to the exception in the - received_data dict - """ - self.path_to_item = path_to_item - full_msg = msg - if path_to_item: - full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) - super(ApiAttributeError, self).__init__(full_msg) - - -class ApiKeyError(OpenApiException, KeyError): - def __init__(self, msg, path_to_item=None) -> None: - """ - Args: - msg (str): the exception message - - Keyword Args: - path_to_item (None/list) the path to the exception in the - received_data dict - """ - self.path_to_item = path_to_item - full_msg = msg - if path_to_item: - full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) - super(ApiKeyError, self).__init__(full_msg) - - -class ApiException(OpenApiException): - - def __init__( - self, - status=None, - reason=None, - http_resp=None, - *, - body: Optional[str] = None, - data: Optional[Any] = None, - ) -> None: - self.status = status - self.reason = reason - self.body = body - self.data = data - self.headers = None - - if http_resp: - if self.status is None: - self.status = http_resp.status - if self.reason is None: - self.reason = http_resp.reason - if self.body is None: - try: - self.body = http_resp.data.decode('utf-8') - except Exception: # noqa: S110 - pass - self.headers = http_resp.getheaders() - - @classmethod - def from_response( - cls, - *, - http_resp, - body: Optional[str], - data: Optional[Any], - ) -> Self: - if http_resp.status == 400: - raise BadRequestException(http_resp=http_resp, body=body, data=data) - - if http_resp.status == 401: - raise UnauthorizedException(http_resp=http_resp, body=body, data=data) - - if http_resp.status == 403: - raise ForbiddenException(http_resp=http_resp, body=body, data=data) - - if http_resp.status == 404: - raise NotFoundException(http_resp=http_resp, body=body, data=data) - - if 500 <= http_resp.status <= 599: - raise ServiceException(http_resp=http_resp, body=body, data=data) - raise ApiException(http_resp=http_resp, body=body, data=data) - - def __str__(self): - """Custom error messages for exception""" - error_message = "({0})\n"\ - "Reason: {1}\n".format(self.status, self.reason) - if self.headers: - error_message += "HTTP response headers: {0}\n".format( - self.headers) - - if self.data or self.body: - error_message += "HTTP response body: {0}\n".format(self.data or self.body) - - return error_message - - -class BadRequestException(ApiException): - pass - - -class NotFoundException(ApiException): - pass - - -class UnauthorizedException(ApiException): - pass - - -class ForbiddenException(ApiException): - pass - - -class ServiceException(ApiException): - pass - - -def render_path(path_to_item): - """Returns a string representation of a path""" - result = "" - for pth in path_to_item: - if isinstance(pth, int): - result += "[{0}]".format(pth) - else: - result += "['{0}']".format(pth) - return result diff --git a/templates/python/git_push.sh.mustache b/templates/python/git_push.sh.mustache deleted file mode 100755 index 0e3776a..0000000 --- a/templates/python/git_push.sh.mustache +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh -# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ -# -# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" - -git_user_id=$1 -git_repo_id=$2 -release_note=$3 -git_host=$4 - -if [ "$git_host" = "" ]; then - git_host="{{{gitHost}}}" - echo "[INFO] No command line input provided. Set \$git_host to $git_host" -fi - -if [ "$git_user_id" = "" ]; then - git_user_id="{{{gitUserId}}}" - echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" -fi - -if [ "$git_repo_id" = "" ]; then - git_repo_id="{{{gitRepoId}}}" - echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" -fi - -if [ "$release_note" = "" ]; then - release_note="{{{releaseNote}}}" - echo "[INFO] No command line input provided. Set \$release_note to $release_note" -fi - -# Initialize the local directory as a Git repository -git init - -# Adds the files in the local repository and stages them for commit. -git add . - -# Commits the tracked changes and prepares them to be pushed to a remote repository. -git commit -m "$release_note" - -# Sets the new remote -git_remote=$(git remote) -if [ "$git_remote" = "" ]; then # git remote not defined - - if [ "$GIT_TOKEN" = "" ]; then - echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." - git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git - else - git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git - fi - -fi - -git pull origin master - -# Pushes (Forces) the changes in the local repository up to the remote repository -echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" -git push origin master 2>&1 | grep -v 'To https' diff --git a/templates/python/github-workflow.mustache b/templates/python/github-workflow.mustache deleted file mode 100644 index 868124c..0000000 --- a/templates/python/github-workflow.mustache +++ /dev/null @@ -1,39 +0,0 @@ -# NOTE: This file is auto generated by OpenAPI Generator. -# URL: https://openapi-generator.tech -# -# ref: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: {{packageName}} Python package -{{=<% %>=}} - -on: [push, pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - pytest diff --git a/templates/python/gitignore.mustache b/templates/python/gitignore.mustache deleted file mode 100644 index 43995bd..0000000 --- a/templates/python/gitignore.mustache +++ /dev/null @@ -1,66 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ -venv/ -.venv/ -.python-version -.pytest_cache - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -#Ipython Notebook -.ipynb_checkpoints diff --git a/templates/python/gitlab-ci.mustache b/templates/python/gitlab-ci.mustache deleted file mode 100644 index 8a6130a..0000000 --- a/templates/python/gitlab-ci.mustache +++ /dev/null @@ -1,31 +0,0 @@ -# NOTE: This file is auto generated by OpenAPI Generator. -# URL: https://openapi-generator.tech -# -# ref: https://docs.gitlab.com/ee/ci/README.html -# ref: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml - -stages: - - test - -.pytest: - stage: test - script: - - pip install -r requirements.txt - - pip install -r test-requirements.txt - - pytest --cov={{{packageName}}} - -pytest-3.7: - extends: .pytest - image: python:3.7-alpine -pytest-3.8: - extends: .pytest - image: python:3.8-alpine -pytest-3.9: - extends: .pytest - image: python:3.9-alpine -pytest-3.10: - extends: .pytest - image: python:3.10-alpine -pytest-3.11: - extends: .pytest - image: python:3.11-alpine diff --git a/templates/python/model.mustache b/templates/python/model.mustache deleted file mode 100644 index 84792dd..0000000 --- a/templates/python/model.mustache +++ /dev/null @@ -1,14 +0,0 @@ -# coding: utf-8 - -{{>partial_header}} - -{{#models}} -{{#model}} -{{#isEnum}} -{{>model_enum}} -{{/isEnum}} -{{^isEnum}} -{{#oneOf}}{{#-first}}{{>model_oneof}}{{/-first}}{{/oneOf}}{{^oneOf}}{{#anyOf}}{{#-first}}{{>model_anyof}}{{/-first}}{{/anyOf}}{{^anyOf}}{{>model_generic}}{{/anyOf}}{{/oneOf}} -{{/isEnum}} -{{/model}} -{{/models}} \ No newline at end of file diff --git a/templates/python/model_anyof.mustache b/templates/python/model_anyof.mustache deleted file mode 100644 index ce454c4..0000000 --- a/templates/python/model_anyof.mustache +++ /dev/null @@ -1,181 +0,0 @@ -from __future__ import annotations -from inspect import getfullargspec -import json -import pprint -import re -{{#vendorExtensions.x-py-other-imports}} -{{{.}}} -{{/vendorExtensions.x-py-other-imports}} -{{#vendorExtensions.x-py-model-imports}} -{{{.}}} -{{/vendorExtensions.x-py-model-imports}} -from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict -from typing_extensions import Literal, Self -from pydantic import Field - -{{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ANY_OF_SCHEMAS = [{{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}}] - -class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}): - """ - {{{description}}}{{^description}}{{{classname}}}{{/description}} - """ - -{{#composedSchemas.anyOf}} - # data type: {{{dataType}}} - {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} -{{/composedSchemas.anyOf}} - if TYPE_CHECKING: - actual_instance: Optional[Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}]] = None - else: - actual_instance: Any = None - any_of_schemas: Set[str] = { {{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}} } - - model_config = { - "validate_assignment": True, - "protected_namespaces": (), - } -{{#discriminator}} - - discriminator_value_class_map: Dict[str, str] = { -{{#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}} - - def __init__(self, *args, **kwargs) -> None: - if args: - if len(args) > 1: - raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") - if kwargs: - raise ValueError("If a position argument is used, keyword arguments cannot be used.") - super().__init__(actual_instance=args[0]) - else: - super().__init__(**kwargs) - - @field_validator('actual_instance') - def actual_instance_must_validate_anyof(cls, v): - {{#isNullable}} - if v is None: - return v - - {{/isNullable}} - instance = {{{classname}}}.model_construct() - error_messages = [] - {{#composedSchemas.anyOf}} - # validate data type: {{{dataType}}} - {{#isContainer}} - try: - instance.{{vendorExtensions.x-py-name}} = v - return v - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isContainer}} - {{^isContainer}} - {{#isPrimitiveType}} - try: - instance.{{vendorExtensions.x-py-name}} = v - return v - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{^isPrimitiveType}} - if not isinstance(v, {{{dataType}}}): - error_messages.append(f"Error! Input type `{type(v)}` is not `{{{dataType}}}`") - else: - return v - - {{/isPrimitiveType}} - {{/isContainer}} - {{/composedSchemas.anyOf}} - if error_messages: - # no match - raise ValueError("No match found when setting the actual_instance in {{{classname}}} with anyOf schemas: {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}. Details: " + ", ".join(error_messages)) - else: - return v - - @classmethod - def from_dict(cls, obj: Dict[str, Any]) -> Self: - return cls.from_json(json.dumps(obj)) - - @classmethod - def from_json(cls, json_str: str) -> Self: - """Returns the object represented by the json string""" - instance = cls.model_construct() - {{#isNullable}} - if json_str is None: - return instance - - {{/isNullable}} - error_messages = [] - {{#composedSchemas.anyOf}} - {{#isContainer}} - # deserialize data into {{{dataType}}} - try: - # validation - instance.{{vendorExtensions.x-py-name}} = json.loads(json_str) - # assign value to actual_instance - instance.actual_instance = instance.{{vendorExtensions.x-py-name}} - return instance - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isContainer}} - {{^isContainer}} - {{#isPrimitiveType}} - # deserialize data into {{{dataType}}} - try: - # validation - instance.{{vendorExtensions.x-py-name}} = json.loads(json_str) - # assign value to actual_instance - instance.actual_instance = instance.{{vendorExtensions.x-py-name}} - return instance - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{^isPrimitiveType}} - try: - instance.actual_instance = {{{dataType}}}.from_json(json_str) - return instance - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{/isContainer}} - {{/composedSchemas.anyOf}} - - if error_messages: - # no match - raise ValueError("No match found when deserializing the JSON string into {{{classname}}} with anyOf schemas: {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}. Details: " + ", ".join(error_messages)) - else: - return instance - - def to_json(self) -> str: - """Returns the JSON representation of the actual instance""" - if self.actual_instance is None: - return "null" - - if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): - return self.actual_instance.to_json() - else: - return json.dumps(self.actual_instance) - - def to_dict(self) -> Optional[Union[Dict[str, Any], {{#anyOf}}{{.}}{{^-last}}, {{/-last}}{{/anyOf}}]]: - """Returns the dict representation of the actual instance""" - if self.actual_instance is None: - return None - - if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): - return self.actual_instance.to_dict() - else: - return self.actual_instance - - def to_str(self) -> str: - """Returns the string representation of the actual instance""" - return pprint.pformat(self.model_dump()) - -{{#vendorExtensions.x-py-postponed-model-imports.size}} -{{#vendorExtensions.x-py-postponed-model-imports}} -{{{.}}} -{{/vendorExtensions.x-py-postponed-model-imports}} -# TODO: Rewrite to not use raise_errors -{{classname}}.model_rebuild(raise_errors=False) -{{/vendorExtensions.x-py-postponed-model-imports.size}} diff --git a/templates/python/model_doc.mustache b/templates/python/model_doc.mustache deleted file mode 100644 index 98d50cf..0000000 --- a/templates/python/model_doc.mustache +++ /dev/null @@ -1,40 +0,0 @@ -{{#models}}{{#model}}# {{classname}} - -{{#description}}{{&description}} -{{/description}} - -{{^isEnum}} -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -{{#vars}}**{{name}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{dataType}}**]({{complexType}}.md){{/isPrimitiveType}} | {{description}} | {{^required}}[optional] {{/required}}{{#isReadOnly}}[readonly] {{/isReadOnly}}{{#defaultValue}}[default to {{{.}}}]{{/defaultValue}} -{{/vars}} - -## Example - -```python -from {{modelPackage}}.{{#lambda.snakecase}}{{classname}}{{/lambda.snakecase}} import {{classname}} - -# TODO update the JSON string below -json = "{}" -# create an instance of {{classname}} from a JSON string -{{#lambda.snakecase}}{{classname}}{{/lambda.snakecase}}_instance = {{classname}}.from_json(json) -# print the JSON string representation of the object -print({{classname}}.to_json()) - -# convert the object into a dict -{{#lambda.snakecase}}{{classname}}{{/lambda.snakecase}}_dict = {{#lambda.snakecase}}{{classname}}{{/lambda.snakecase}}_instance.to_dict() -# create an instance of {{classname}} from a dict -{{#lambda.snakecase}}{{classname}}{{/lambda.snakecase}}_from_dict = {{classname}}.from_dict({{#lambda.snakecase}}{{classname}}{{/lambda.snakecase}}_dict) -``` -{{/isEnum}} -{{#isEnum}} -## Enum -{{#allowableValues}}{{#enumVars}} -* `{{name}}` (value: `{{{value}}}`) -{{/enumVars}}{{/allowableValues}} -{{/isEnum}} -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - -{{/model}}{{/models}} diff --git a/templates/python/model_enum.mustache b/templates/python/model_enum.mustache deleted file mode 100644 index 3f449b1..0000000 --- a/templates/python/model_enum.mustache +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations -import json -from enum import Enum -{{#vendorExtensions.x-py-other-imports}} -{{{.}}} -{{/vendorExtensions.x-py-other-imports}} -from typing_extensions import Self - - -class {{classname}}({{vendorExtensions.x-py-enum-type}}, Enum): - """ - {{{description}}}{{^description}}{{{classname}}}{{/description}} - """ - - """ - allowed enum values - """ -{{#allowableValues}} - {{#enumVars}} - {{{name}}} = {{{value}}} - {{/enumVars}} - - @classmethod - def from_json(cls, json_str: str) -> Self: - """Create an instance of {{classname}} from a JSON string""" - return cls(json.loads(json_str)) - - {{#defaultValue}} - - # - @classmethod - def _missing_value_(cls, value): - if value is no_arg: - return cls.{{{.}}} - {{/defaultValue}} -{{/allowableValues}} diff --git a/templates/python/model_generic.mustache b/templates/python/model_generic.mustache index e6624b8..c8ee8b5 100644 --- a/templates/python/model_generic.mustache +++ b/templates/python/model_generic.mustache @@ -1,15 +1,15 @@ from __future__ import annotations import pprint -import re +import re # noqa: F401 import json {{#vendorExtensions.x-py-other-imports}} -{{{.}}} +{{{.}}} {{/vendorExtensions.x-py-other-imports}} {{#vendorExtensions.x-py-model-imports}} -{{{.}}} +{{{.}}} {{/vendorExtensions.x-py-model-imports}} -from typing import Set, Optional +from typing import Optional, Set from typing_extensions import Self {{#hasChildren}} @@ -18,7 +18,7 @@ from typing_extensions import Self from typing import TYPE_CHECKING if TYPE_CHECKING: {{#mappedModels}} - from {{packageName}}.models.{{model.classVarName}} import {{modelName}} + from {{packageName}}.models.{{model.classFilename}} import {{modelName}} {{/mappedModels}} {{/discriminator}} @@ -26,7 +26,7 @@ if TYPE_CHECKING: class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}): """ {{#description}}{{{description}}}{{/description}}{{^description}}{{{classname}}}{{/description}} - """ + """ # noqa: E501 {{#vars}} {{name}}: {{{vendorExtensions.x-py-typing}}} {{/vars}} @@ -73,15 +73,22 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{/isNullable}} {{/required}} + {{#isContainer}} {{#isArray}} for i in value: if i not in set([{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]): raise ValueError("each list item must be one of ({{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}})") {{/isArray}} - {{^isArray}} + {{#isMap}} + for i in value.values(): + if i not in set([{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]): + raise ValueError("dict values must be one of enum values ({{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}})") + {{/isMap}} + {{/isContainer}} + {{^isContainer}} if value not in set([{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]): raise ValueError("must be one of enum values ({{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}})") - {{/isArray}} + {{/isContainer}} return value {{/isEnum}} {{/vars}} @@ -254,7 +261,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} object_type = cls.get_discriminator_value(obj) {{#mappedModels}} if object_type == '{{{modelName}}}': - return import_module("{{packageName}}.models.{{model.classVarName}}").{{modelName}}.from_dict(obj) + return import_module("{{packageName}}.models.{{model.classFilename}}").{{modelName}}.from_dict(obj) {{/mappedModels}} raise ValueError("{{{classname}}} failed to lookup discriminator value from " + @@ -349,7 +356,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{/items.isContainer}} {{/items.isEnumOrRef}} {{#items.isEnumOrRef}} - "{{{baseName}}}": dict((_k, _v) for _k, _v in obj.get("{{{baseName}}}").items()){{^-last}},{{/-last}} + "{{{baseName}}}": dict((_k, _v) for _k, _v in obj.get("{{{baseName}}}").items()) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} {{/items.isEnumOrRef}} {{/items.isPrimitiveType}} {{#items.isPrimitiveType}} diff --git a/templates/python/model_test.mustache b/templates/python/model_test.mustache deleted file mode 100644 index 0808855..0000000 --- a/templates/python/model_test.mustache +++ /dev/null @@ -1,59 +0,0 @@ -# coding: utf-8 - -{{>partial_header}} - -import unittest - -{{#models}} -{{#model}} -from {{modelPackage}}.{{classFilename}} import {{classname}} - -class Test{{classname}}(unittest.TestCase): - """{{classname}} unit test stubs""" - - def setUp(self): - pass - - def tearDown(self): - pass -{{^isEnum}} - - def make_instance(self, include_optional) -> {{classname}}: - """Test {{classname}} - include_optional is a boolean, when False only required - params are included, when True both required and - optional params are included """ - # uncomment below to create an instance of `{{{classname}}}` - """ - model = {{classname}}() - if include_optional: - return {{classname}}( - {{#vars}} - {{name}} = {{{example}}}{{^example}}None{{/example}}{{^-last}},{{/-last}} - {{/vars}} - ) - else: - return {{classname}}( - {{#vars}} - {{#required}} - {{name}} = {{{example}}}{{^example}}None{{/example}}, - {{/required}} - {{/vars}} - ) - """ -{{/isEnum}} - - def test{{classname}}(self): - """Test {{classname}}""" -{{^isEnum}} - # inst_req_only = self.make_instance(include_optional=False) - # inst_req_and_optional = self.make_instance(include_optional=True) -{{/isEnum}} -{{#isEnum}} - # inst = {{{classname}}}() -{{/isEnum}} -{{/model}} -{{/models}} - -if __name__ == '__main__': - unittest.main() diff --git a/templates/python/partial_api.mustache b/templates/python/partial_api.mustache deleted file mode 100644 index cf2fd0a..0000000 --- a/templates/python/partial_api.mustache +++ /dev/null @@ -1,52 +0,0 @@ - """{{#isDeprecated}}(Deprecated) {{/isDeprecated}}{{{summary}}}{{^summary}}{{operationId}}{{/summary}} - - {{#notes}} - {{{.}}} - {{/notes}} - - {{#allParams}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}}{{#required}} (required){{/required}}{{#optional}}(optional){{/optional}} - :type {{paramName}}: {{dataType}}{{#optional}}, optional{{/optional}} - {{/allParams}} - :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. - :type _request_timeout: int, tuple(int, int), optional - :param _request_auth: set to override the auth_settings for an a single - request; this effectively ignores the - authentication in the spec for a single request. - :type _request_auth: dict, optional - :param _content_type: force content-type for the request. - :type _content_type: str, Optional - :param _headers: set to override the headers for a single - request; this effectively ignores the headers - in the spec for a single request. - :type _headers: dict, optional - :param _host_index: set to override the host_index for a single - request; this effectively ignores the host_index - in the spec for a single request. - :type _host_index: int, optional - :return: Returns the result object. - """ # noqa: E501 docstring might be too long - {{#isDeprecated}} - warnings.warn("{{{httpMethod}}} {{{path}}} is deprecated.", DeprecationWarning) - {{/isDeprecated}} - - _param = self._{{operationId}}_serialize( - {{#allParams}} - {{paramName}}={{paramName}}, - {{/allParams}} - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - {{#responses}} - {{^isWildcard}} - '{{code}}': {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}}, - {{/isWildcard}} - {{/responses}} - } \ No newline at end of file diff --git a/templates/python/partial_api_args.mustache b/templates/python/partial_api_args.mustache deleted file mode 100644 index 379b67d..0000000 --- a/templates/python/partial_api_args.mustache +++ /dev/null @@ -1,18 +0,0 @@ -( - self, - {{#allParams}} - {{paramName}}: {{{vendorExtensions.x-py-typing}}}{{^required}} = None{{/required}}, - {{/allParams}} - _request_timeout: Union[ - None, - Annotated[StrictFloat, Field(gt=0)], - Tuple[ - Annotated[StrictFloat, Field(gt=0)], - Annotated[StrictFloat, Field(gt=0)] - ] - ] = None, - _request_auth: Optional[Dict[StrictStr, Any]] = None, - _content_type: Optional[StrictStr] = None, - _headers: Optional[Dict[StrictStr, Any]] = None, - _host_index: Annotated[StrictInt, Field(ge=0, le={{#servers.size}}{{servers.size}}{{/servers.size}}{{^servers.size}}1{{/servers.size}})] = 0, - ) \ No newline at end of file diff --git a/templates/python/partial_header.mustache b/templates/python/partial_header.mustache deleted file mode 100644 index 092aad7..0000000 --- a/templates/python/partial_header.mustache +++ /dev/null @@ -1,19 +0,0 @@ -""" -{{#appName}} - {{{.}}} - -{{/appName}} -{{#appDescription}} - {{{.}}} - -{{/appDescription}} - {{#version}} - The version of the OpenAPI document: {{{.}}} - {{/version}} - {{#infoEmail}} - Contact: {{{.}}} - {{/infoEmail}} - Generated by OpenAPI Generator (https://openapi-generator.tech) - - Do not edit the class manually. -""" # noqa: E501 docstring might be too long diff --git a/templates/python/py.typed.mustache b/templates/python/py.typed.mustache deleted file mode 100644 index e69de29..0000000 diff --git a/templates/python/pyproject.mustache b/templates/python/pyproject.mustache index 80c6621..5c1ae96 100644 --- a/templates/python/pyproject.mustache +++ b/templates/python/pyproject.mustache @@ -20,7 +20,7 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.8,<4.0" +python = "^3.9" stackit-core = ">=0.0.1a" requests = ">=2.32.3" pydantic = ">=2.9.2" diff --git a/templates/python/python_doc_auth_partial.mustache b/templates/python/python_doc_auth_partial.mustache deleted file mode 100644 index f478fe0..0000000 --- a/templates/python/python_doc_auth_partial.mustache +++ /dev/null @@ -1,108 +0,0 @@ -# Defining the host is optional and defaults to {{{basePath}}} -# See configuration.py for a list of all supported configuration parameters. -configuration = {{{packageName}}}.Configuration( - host = "{{{basePath}}}" -) - -{{#hasAuthMethods}} -# The client must configure the authentication and authorization parameters -# in accordance with the API server security policy. -# Examples for each auth method are provided below, use the example that -# satisfies your auth use case. -{{#authMethods}} -{{#isBasic}} -{{#isBasicBasic}} - -# Configure HTTP basic authorization: {{{name}}} -configuration = {{{packageName}}}.Configuration( - username = os.environ["USERNAME"], - password = os.environ["PASSWORD"] -) -{{/isBasicBasic}} -{{#isBasicBearer}} - -# Configure Bearer authorization{{#bearerFormat}} ({{{.}}}){{/bearerFormat}}: {{{name}}} -configuration = {{{packageName}}}.Configuration( - access_token = os.environ["BEARER_TOKEN"] -) -{{/isBasicBearer}} -{{#isHttpSignature}} - -# Configure HTTP message signature: {{{name}}} -# The HTTP Signature Header mechanism that can be used by a client to -# authenticate the sender of a message and ensure that particular headers -# have not been modified in transit. -# -# You can specify the signing key-id, private key path, signing scheme, -# signing algorithm, list of signed headers and signature max validity. -# The 'key_id' parameter is an opaque string that the API server can use -# to lookup the client and validate the signature. -# The 'private_key_path' parameter should be the path to a file that -# contains a DER or base-64 encoded private key. -# The 'private_key_passphrase' parameter is optional. Set the passphrase -# if the private key is encrypted. -# The 'signed_headers' parameter is used to specify the list of -# HTTP headers included when generating the signature for the message. -# You can specify HTTP headers that you want to protect with a cryptographic -# signature. Note that proxies may add, modify or remove HTTP headers -# for legitimate reasons, so you should only add headers that you know -# will not be modified. For example, if you want to protect the HTTP request -# body, you can specify the Digest header. In that case, the client calculates -# the digest of the HTTP request body and includes the digest in the message -# signature. -# The 'signature_max_validity' parameter is optional. It is configured as a -# duration to express when the signature ceases to be valid. The client calculates -# the expiration date every time it generates the cryptographic signature -# of an HTTP request. The API server may have its own security policy -# that controls the maximum validity of the signature. The client max validity -# must be lower than the server max validity. -# The time on the client and server must be synchronized, otherwise the -# server may reject the client signature. -# -# The client must use a combination of private key, signing scheme, -# signing algorithm and hash algorithm that matches the security policy of -# the API server. -# -# See {{{packageName}}}.signing for a list of all supported parameters. -from {{{packageName}}} import signing -import datetime - -configuration = {{{packageName}}}.Configuration( - host = "{{{basePath}}}", - signing_info = {{{packageName}}}.HttpSigningConfiguration( - key_id = 'my-key-id', - private_key_path = 'private_key.pem', - private_key_passphrase = 'YOUR_PASSPHRASE', - signing_scheme = {{{packageName}}}.signing.SCHEME_HS2019, - signing_algorithm = {{{packageName}}}.signing.ALGORITHM_ECDSA_MODE_FIPS_186_3, - hash_algorithm = {{{packageName}}}.signing.SCHEME_RSA_SHA256, - signed_headers = [ - {{{packageName}}}.signing.HEADER_REQUEST_TARGET, - {{{packageName}}}.signing.HEADER_CREATED, - {{{packageName}}}.signing.HEADER_EXPIRES, - {{{packageName}}}.signing.HEADER_HOST, - {{{packageName}}}.signing.HEADER_DATE, - {{{packageName}}}.signing.HEADER_DIGEST, - 'Content-Type', - 'Content-Length', - 'User-Agent' - ], - signature_max_validity = datetime.timedelta(minutes=5) - ) -) -{{/isHttpSignature}} -{{/isBasic}} -{{#isApiKey}} - -# Configure API key authorization: {{{name}}} -configuration.api_key['{{{name}}}'] = os.environ["API_KEY"] - -# Uncomment below to setup prefix (e.g. Bearer) for API key, if needed -# configuration.api_key_prefix['{{name}}'] = 'Bearer' -{{/isApiKey}} -{{#isOAuth}} - -configuration.access_token = os.environ["ACCESS_TOKEN"] -{{/isOAuth}} -{{/authMethods}} -{{/hasAuthMethods}} diff --git a/templates/python/requirements.mustache b/templates/python/requirements.mustache deleted file mode 100644 index 5412515..0000000 --- a/templates/python/requirements.mustache +++ /dev/null @@ -1,12 +0,0 @@ -python_dateutil >= 2.5.3 -setuptools >= 21.0.0 -urllib3 >= 1.25.3, < 2.1.0 -pydantic >= 2 -typing-extensions >= 4.7.1 -{{#asyncio}} -aiohttp >= 3.0.0 -aiohttp-retry >= 2.8.3 -{{/asyncio}} -{{#hasHttpSignatureMethods}} -pycryptodome >= 3.9.0 -{{/hasHttpSignatureMethods}} diff --git a/templates/python/rest.mustache b/templates/python/rest.mustache index 5d6305f..f0e0fec 100644 --- a/templates/python/rest.mustache +++ b/templates/python/rest.mustache @@ -135,7 +135,7 @@ class RESTClientObject: data=body, headers=headers, ) - elif headers['Content-Type'] == 'text/plain' and isinstance(body, bool): + elif headers['Content-Type'].startswith('text/') and isinstance(body, bool): request_body = "true" if body else "false" r = self.session.request( method, diff --git a/templates/python/setup.mustache b/templates/python/setup.mustache deleted file mode 100644 index 9c68cf3..0000000 --- a/templates/python/setup.mustache +++ /dev/null @@ -1,57 +0,0 @@ -# coding: utf-8 - -{{>partial_header}} - -from setuptools import setup, find_packages - -# To install the library, run the following -# -# python setup.py install -# -# prerequisite: setuptools -# http://pypi.python.org/pypi/setuptools -NAME = "{{{projectName}}}" -VERSION = "{{packageVersion}}" -PYTHON_REQUIRES = ">=3.7" -{{#apiInfo}} -{{#apis}} -{{#-last}} -REQUIRES = [ - "urllib3 >= 1.25.3, < 2.1.0", - "python-dateutil", -{{#asyncio}} - "aiohttp >= 3.0.0", - "aiohttp-retry >= 2.8.3", -{{/asyncio}} -{{#tornado}} - "tornado>=4.2,<5", -{{/tornado}} -{{#hasHttpSignatureMethods}} - "pem>=19.3.0", - "pycryptodome>=3.9.0", -{{/hasHttpSignatureMethods}} - "pydantic >= 2", - "typing-extensions >= 4.7.1", -] - -setup( - name=NAME, - version=VERSION, - description="{{appName}}", - author="{{infoName}}{{^infoName}}OpenAPI Generator community{{/infoName}}", - author_email="{{infoEmail}}{{^infoEmail}}team@openapitools.org{{/infoEmail}}", - url="{{packageUrl}}", - keywords=["OpenAPI", "OpenAPI-Generator", "{{{appName}}}"], - install_requires=REQUIRES, - packages=find_packages(exclude=["test", "tests"]), - include_package_data=True, - {{#licenseInfo}}license="{{.}}", - {{/licenseInfo}}long_description_content_type='text/markdown', - long_description="""\ - {{appDescription}} - """, # noqa: E501 docstring might be too long - package_data={"{{{packageName}}}": ["py.typed"]}, -) -{{/-last}} -{{/apis}} -{{/apiInfo}} diff --git a/templates/python/setup_cfg.mustache b/templates/python/setup_cfg.mustache deleted file mode 100644 index 11433ee..0000000 --- a/templates/python/setup_cfg.mustache +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length=99 diff --git a/templates/python/signing.mustache b/templates/python/signing.mustache deleted file mode 100644 index 4d00424..0000000 --- a/templates/python/signing.mustache +++ /dev/null @@ -1,422 +0,0 @@ -{{>partial_header}} - -from base64 import b64encode -from Crypto.IO import PEM, PKCS8 -from Crypto.Hash import SHA256, SHA512 -from Crypto.Hash.SHA512 import SHA512Hash -from Crypto.Hash.SHA256 import SHA256Hash -from Crypto.PublicKey import RSA, ECC -from Crypto.Signature import PKCS1_v1_5, pss, DSS -from datetime import timedelta -from email.utils import formatdate -import os -import re -from time import time -from typing import List, Optional, Union -from urllib.parse import urlencode, urlparse - -# The constants below define a subset of HTTP headers that can be included in the -# HTTP signature scheme. Additional headers may be included in the signature. - -# The '(request-target)' header is a calculated field that includes the HTTP verb, -# the URL path and the URL query. -HEADER_REQUEST_TARGET = '(request-target)' -# The time when the HTTP signature was generated. -HEADER_CREATED = '(created)' -# The time when the HTTP signature expires. The API server should reject HTTP requests -# that have expired. -HEADER_EXPIRES = '(expires)' -# The 'Host' header. -HEADER_HOST = 'Host' -# The 'Date' header. -HEADER_DATE = 'Date' -# When the 'Digest' header is included in the HTTP signature, the client automatically -# computes the digest of the HTTP request body, per RFC 3230. -HEADER_DIGEST = 'Digest' -# The 'Authorization' header is automatically generated by the client. It includes -# the list of signed headers and a base64-encoded signature. -HEADER_AUTHORIZATION = 'Authorization' - -# The constants below define the cryptographic schemes for the HTTP signature scheme. -SCHEME_HS2019 = 'hs2019' -SCHEME_RSA_SHA256 = 'rsa-sha256' -SCHEME_RSA_SHA512 = 'rsa-sha512' - -# The constants below define the signature algorithms that can be used for the HTTP -# signature scheme. -ALGORITHM_RSASSA_PSS = 'RSASSA-PSS' -ALGORITHM_RSASSA_PKCS1v15 = 'RSASSA-PKCS1-v1_5' - -ALGORITHM_ECDSA_MODE_FIPS_186_3 = 'fips-186-3' -ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979 = 'deterministic-rfc6979' -ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS = { - ALGORITHM_ECDSA_MODE_FIPS_186_3, - ALGORITHM_ECDSA_MODE_DETERMINISTIC_RFC6979 -} - -# The cryptographic hash algorithm for the message signature. -HASH_SHA256 = 'sha256' -HASH_SHA512 = 'sha512' - - -class HttpSigningConfiguration: - """The configuration parameters for the HTTP signature security scheme. - - The HTTP signature security scheme is used to sign HTTP requests with a private key - which is in possession of the API client. - - An ``Authorization`` header is calculated by creating a hash of select headers, - and optionally the body of the HTTP request, then signing the hash value using - a private key. The ``Authorization`` header is added to outbound HTTP requests. - - :param key_id: A string value specifying the identifier of the cryptographic key, - when signing HTTP requests. - :param signing_scheme: A string value specifying the signature scheme, when - signing HTTP requests. - Supported value are: ``hs2019``, ``rsa-sha256``, ``rsa-sha512``. - Avoid using ``rsa-sha256``, ``rsa-sha512`` as they are deprecated. These values are - available for server-side applications that only support the older - HTTP signature algorithms. - :param private_key_path: A string value specifying the path of the file containing - a private key. The private key is used to sign HTTP requests. - :param private_key_passphrase: A string value specifying the passphrase to decrypt - the private key. - :param signed_headers: A list of strings. Each value is the name of a HTTP header - that must be included in the HTTP signature calculation. - The two special signature headers ``(request-target)`` and ``(created)`` SHOULD be - included in SignedHeaders. - The ``(created)`` header expresses when the signature was created. - The ``(request-target)`` header is a concatenation of the lowercased :method, an - ASCII space, and the :path pseudo-headers. - When signed_headers is not specified, the client defaults to a single value, - ``(created)``, in the list of HTTP headers. - When SignedHeaders contains the 'Digest' value, the client performs the - following operations: - 1. Calculate a digest of request body, as specified in `RFC3230, - section 4.3.2`_. - 2. Set the ``Digest`` header in the request body. - 3. Include the ``Digest`` header and value in the HTTP signature. - :param signing_algorithm: A string value specifying the signature algorithm, when - signing HTTP requests. - Supported values are: - 1. For RSA keys: RSASSA-PSS, RSASSA-PKCS1-v1_5. - 2. For ECDSA keys: fips-186-3, deterministic-rfc6979. - If None, the signing algorithm is inferred from the private key. - The default signing algorithm for RSA keys is RSASSA-PSS. - The default signing algorithm for ECDSA keys is fips-186-3. - :param hash_algorithm: The hash algorithm for the signature. Supported values are - sha256 and sha512. - If the signing_scheme is rsa-sha256, the hash algorithm must be set - to None or sha256. - If the signing_scheme is rsa-sha512, the hash algorithm must be set - to None or sha512. - :param signature_max_validity: The signature max validity, expressed as - a datetime.timedelta value. It must be a positive value. - """ - def __init__(self, - key_id: str, - signing_scheme: str, - private_key_path: str, - private_key_passphrase: Union[None, str]=None, - signed_headers: Optional[List[str]]=None, - signing_algorithm: Optional[str]=None, - hash_algorithm: Optional[str]=None, - signature_max_validity: Optional[timedelta]=None, - ) -> None: - self.key_id = key_id - if signing_scheme not in {SCHEME_HS2019, SCHEME_RSA_SHA256, SCHEME_RSA_SHA512}: - raise Exception("Unsupported security scheme: {0}".format(signing_scheme)) - self.signing_scheme = signing_scheme - if not os.path.exists(private_key_path): - raise Exception("Private key file does not exist") - self.private_key_path = private_key_path - self.private_key_passphrase = private_key_passphrase - self.signing_algorithm = signing_algorithm - self.hash_algorithm = hash_algorithm - if signing_scheme == SCHEME_RSA_SHA256: - if self.hash_algorithm is None: - self.hash_algorithm = HASH_SHA256 - elif self.hash_algorithm != HASH_SHA256: - raise Exception("Hash algorithm must be sha256 when security scheme is %s" % - SCHEME_RSA_SHA256) - elif signing_scheme == SCHEME_RSA_SHA512: - if self.hash_algorithm is None: - self.hash_algorithm = HASH_SHA512 - elif self.hash_algorithm != HASH_SHA512: - raise Exception("Hash algorithm must be sha512 when security scheme is %s" % - SCHEME_RSA_SHA512) - elif signing_scheme == SCHEME_HS2019: - if self.hash_algorithm is None: - self.hash_algorithm = HASH_SHA256 - elif self.hash_algorithm not in {HASH_SHA256, HASH_SHA512}: - raise Exception("Invalid hash algorithm") - if signature_max_validity is not None and signature_max_validity.total_seconds() < 0: - raise Exception("The signature max validity must be a positive value") - self.signature_max_validity = signature_max_validity - # If the user has not provided any signed_headers, the default must be set to '(created)', - # as specified in the 'HTTP signature' standard. - if signed_headers is None or len(signed_headers) == 0: - signed_headers = [HEADER_CREATED] - if self.signature_max_validity is None and HEADER_EXPIRES in signed_headers: - raise Exception( - "Signature max validity must be set when " - "'(expires)' signature parameter is specified") - if len(signed_headers) != len(set(signed_headers)): - raise Exception("Cannot have duplicates in the signed_headers parameter") - if HEADER_AUTHORIZATION in signed_headers: - raise Exception("'Authorization' header cannot be included in signed headers") - self.signed_headers = signed_headers - self.private_key: Optional[Union[ECC.EccKey, RSA.RsaKey]] = None - """The private key used to sign HTTP requests. - Initialized when the PEM-encoded private key is loaded from a file. - """ - self.host: Optional[str] = None - """The host name, optionally followed by a colon and TCP port number. - """ - self._load_private_key() - - def get_http_signature_headers(self, resource_path, method, headers, body, query_params): - """Create a cryptographic message signature for the HTTP request and add the signed headers. - - :param resource_path : A string representation of the HTTP request resource path. - :param method: A string representation of the HTTP request method, e.g. GET, POST. - :param headers: A dict containing the HTTP request headers. - :param body: The object representing the HTTP request body. - :param query_params: A string representing the HTTP request query parameters. - :return: A dict of HTTP headers that must be added to the outbound HTTP request. - """ - if method is None: - raise Exception("HTTP method must be set") - if resource_path is None: - raise Exception("Resource path must be set") - - signed_headers_list, request_headers_dict = self._get_signed_header_info( - resource_path, method, headers, body, query_params) - - header_items = [ - "{0}: {1}".format(key.lower(), value) for key, value in signed_headers_list] - string_to_sign = "\n".join(header_items) - - digest, digest_prefix = self._get_message_digest(string_to_sign.encode()) - b64_signed_msg = self._sign_digest(digest) - - request_headers_dict[HEADER_AUTHORIZATION] = self._get_authorization_header( - signed_headers_list, b64_signed_msg) - - return request_headers_dict - - def get_public_key(self): - """Returns the public key object associated with the private key. - """ - pubkey: Optional[Union[ECC.EccKey, RSA.RsaKey]] = None - if isinstance(self.private_key, RSA.RsaKey): - pubkey = self.private_key.publickey() - elif isinstance(self.private_key, ECC.EccKey): - pubkey = self.private_key.public_key() - return pubkey - - def _load_private_key(self): - """Load the private key used to sign HTTP requests. - The private key is used to sign HTTP requests as defined in - https://datatracker.ietf.org/doc/draft-cavage-http-signatures/. - """ - if self.private_key is not None: - return - with open(self.private_key_path, 'r') as f: - pem_data = f.read() - # Verify PEM Pre-Encapsulation Boundary - r = re.compile(r"\s*-----BEGIN (.*)-----\s+") - m = r.match(pem_data) - if not m: - raise ValueError("Not a valid PEM pre boundary") - pem_header = m.group(1) - if pem_header == 'RSA PRIVATE KEY': - self.private_key = RSA.importKey(pem_data, self.private_key_passphrase) - elif pem_header == 'EC PRIVATE KEY': - self.private_key = ECC.import_key(pem_data, self.private_key_passphrase) - elif pem_header in {'PRIVATE KEY', 'ENCRYPTED PRIVATE KEY'}: - # Key is in PKCS8 format, which is capable of holding many different - # types of private keys, not just EC keys. - if self.private_key_passphrase is not None: - passphrase = self.private_key_passphrase.encode("utf-8") - else: - passphrase = None - (key_binary, pem_header, is_encrypted) = PEM.decode(pem_data, passphrase) - (oid, privkey, params) = \ - PKCS8.unwrap(key_binary, passphrase=self.private_key_passphrase) - if oid == '1.2.840.10045.2.1': - self.private_key = ECC.import_key(pem_data, self.private_key_passphrase) - else: - raise Exception("Unsupported key: {0}. OID: {1}".format(pem_header, oid)) - else: - raise Exception("Unsupported key: {0}".format(pem_header)) - # Validate the specified signature algorithm is compatible with the private key. - if self.signing_algorithm is not None: - supported_algs = None - if isinstance(self.private_key, RSA.RsaKey): - supported_algs = {ALGORITHM_RSASSA_PSS, ALGORITHM_RSASSA_PKCS1v15} - elif isinstance(self.private_key, ECC.EccKey): - supported_algs = ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS - if supported_algs is not None and self.signing_algorithm not in supported_algs: - raise Exception( - "Signing algorithm {0} is not compatible with private key".format( - self.signing_algorithm)) - - def _get_signed_header_info(self, resource_path, method, headers, body, query_params): - """Build the HTTP headers (name, value) that need to be included in - the HTTP signature scheme. - - :param resource_path : A string representation of the HTTP request resource path. - :param method: A string representation of the HTTP request method, e.g. GET, POST. - :param headers: A dict containing the HTTP request headers. - :param body: The object (e.g. a dict) representing the HTTP request body. - :param query_params: A string representing the HTTP request query parameters. - :return: A tuple containing two dict objects: - The first dict contains the HTTP headers that are used to calculate - the HTTP signature. - The second dict contains the HTTP headers that must be added to - the outbound HTTP request. - """ - - if body is None: - body = '' - else: - body = body.to_json() - - # Build the '(request-target)' HTTP signature parameter. - target_host = urlparse(self.host).netloc - target_path = urlparse(self.host).path - request_target = method.lower() + " " + target_path + resource_path - if query_params: - request_target += "?" + urlencode(query_params) - - # Get UNIX time, e.g. seconds since epoch, not including leap seconds. - now = time() - # Format date per RFC 7231 section-7.1.1.2. An example is: - # Date: Wed, 21 Oct 2015 07:28:00 GMT - cdate = formatdate(timeval=now, localtime=False, usegmt=True) - # The '(created)' value MUST be a Unix timestamp integer value. - # Subsecond precision is not supported. - created = int(now) - if self.signature_max_validity is not None: - expires = now + self.signature_max_validity.total_seconds() - - signed_headers_list = [] - request_headers_dict = {} - for hdr_key in self.signed_headers: - hdr_key = hdr_key.lower() - if hdr_key == HEADER_REQUEST_TARGET: - value = request_target - elif hdr_key == HEADER_CREATED: - value = '{0}'.format(created) - elif hdr_key == HEADER_EXPIRES: - value = '{0}'.format(expires) - elif hdr_key == HEADER_DATE.lower(): - value = cdate - request_headers_dict[HEADER_DATE] = '{0}'.format(cdate) - elif hdr_key == HEADER_DIGEST.lower(): - request_body = body.encode() - body_digest, digest_prefix = self._get_message_digest(request_body) - b64_body_digest = b64encode(body_digest.digest()) - value = digest_prefix + b64_body_digest.decode('ascii') - request_headers_dict[HEADER_DIGEST] = '{0}{1}'.format( - digest_prefix, b64_body_digest.decode('ascii')) - elif hdr_key == HEADER_HOST.lower(): - if isinstance(target_host, bytes): - value = target_host.decode('ascii') - else: - value = target_host - request_headers_dict[HEADER_HOST] = value - else: - value = next((v for k, v in headers.items() if k.lower() == hdr_key), None) - if value is None: - raise Exception( - "Cannot sign HTTP request. " - "Request does not contain the '{0}' header".format(hdr_key)) - signed_headers_list.append((hdr_key, value)) - - return signed_headers_list, request_headers_dict - - def _get_message_digest(self, data): - """Calculates and returns a cryptographic digest of a specified HTTP request. - - :param data: The string representation of the date to be hashed with a cryptographic hash. - :return: A tuple of (digest, prefix). - The digest is a hashing object that contains the cryptographic digest of - the HTTP request. - The prefix is a string that identifies the cryptographic hash. It is used - to generate the 'Digest' header as specified in RFC 3230. - """ - - digest: Union[SHA256Hash, SHA512Hash] - - if self.hash_algorithm == HASH_SHA512: - digest = SHA512.new() - prefix = 'SHA-512=' - elif self.hash_algorithm == HASH_SHA256: - digest = SHA256.new() - prefix = 'SHA-256=' - else: - raise Exception("Unsupported hash algorithm: {0}".format(self.hash_algorithm)) - digest.update(data) - return digest, prefix - - def _sign_digest(self, digest): - """Signs a message digest with a private key specified in the signing_info. - - :param digest: A hashing object that contains the cryptographic digest of the HTTP request. - :return: A base-64 string representing the cryptographic signature of the input digest. - """ - sig_alg = self.signing_algorithm - if isinstance(self.private_key, RSA.RsaKey): - if sig_alg is None or sig_alg == ALGORITHM_RSASSA_PSS: - # RSASSA-PSS in Section 8.1 of RFC8017. - signature = pss.new(self.private_key).sign(digest) - elif sig_alg == ALGORITHM_RSASSA_PKCS1v15: - # RSASSA-PKCS1-v1_5 in Section 8.2 of RFC8017. - signature = PKCS1_v1_5.new(self.private_key).sign(digest) - else: - raise Exception("Unsupported signature algorithm: {0}".format(sig_alg)) - elif isinstance(self.private_key, ECC.EccKey): - if sig_alg is None: - sig_alg = ALGORITHM_ECDSA_MODE_FIPS_186_3 - if sig_alg in ALGORITHM_ECDSA_KEY_SIGNING_ALGORITHMS: - # draft-ietf-httpbis-message-signatures-00 does not specify the ECDSA encoding. - # Issue: https://github.com/w3c-ccg/http-signatures/issues/107 - signature = DSS.new(key=self.private_key, mode=sig_alg, - encoding='der').sign(digest) - else: - raise Exception("Unsupported signature algorithm: {0}".format(sig_alg)) - else: - raise Exception("Unsupported private key: {0}".format(type(self.private_key))) - return b64encode(signature) - - def _get_authorization_header(self, signed_headers, signed_msg): - """Calculates and returns the value of the 'Authorization' header when signing HTTP requests. - - :param signed_headers : A list of tuples. Each value is the name of a HTTP header that - must be included in the HTTP signature calculation. - :param signed_msg: A base-64 encoded string representation of the signature. - :return: The string value of the 'Authorization' header, representing the signature - of the HTTP request. - """ - created_ts = None - expires_ts = None - for k, v in signed_headers: - if k == HEADER_CREATED: - created_ts = v - elif k == HEADER_EXPIRES: - expires_ts = v - lower_keys = [k.lower() for k, v in signed_headers] - headers_value = " ".join(lower_keys) - - auth_str = "Signature keyId=\"{0}\",algorithm=\"{1}\",".format( - self.key_id, self.signing_scheme) - if created_ts is not None: - auth_str = auth_str + "created={0},".format(created_ts) - if expires_ts is not None: - auth_str = auth_str + "expires={0},".format(expires_ts) - auth_str = auth_str + "headers=\"{0}\",signature=\"{1}\"".format( - headers_value, signed_msg.decode('ascii')) - - return auth_str diff --git a/templates/python/test-requirements.mustache b/templates/python/test-requirements.mustache deleted file mode 100644 index 8e6d8cb..0000000 --- a/templates/python/test-requirements.mustache +++ /dev/null @@ -1,5 +0,0 @@ -pytest~=7.1.3 -pytest-cov>=2.8.1 -pytest-randomly>=3.12.0 -mypy>=1.4.1 -types-python-dateutil>=2.8.19 diff --git a/templates/python/tornado/rest.mustache b/templates/python/tornado/rest.mustache deleted file mode 100644 index f4bfbfb..0000000 --- a/templates/python/tornado/rest.mustache +++ /dev/null @@ -1,142 +0,0 @@ -# coding: utf-8 - -{{>partial_header}} - -import io -import json -import re - -from urllib.parse import urlencode -import tornado -import tornado.gen -from tornado import httpclient -from urllib3.filepost import encode_multipart_formdata - -from {{packageName}}.exceptions import ApiException, ApiValueError - -RESTResponseType = httpclient.HTTPResponse - -class RESTResponse(io.IOBase): - - def __init__(self, resp) -> None: - self.response = resp - self.status = resp.code - self.reason = resp.reason - self.data = None - - def read(self): - if self.data is None: - self.data = self.response.body - return self.data - - def getheaders(self): - """Returns a CIMultiDictProxy of the response headers.""" - return self.response.headers - - def getheader(self, name, default=None): - """Returns a given response header.""" - return self.response.headers.get(name, default) - - -class RESTClientObject: - - def __init__(self, configuration) -> None: - - self.ca_certs = configuration.ssl_ca_cert - self.client_key = configuration.key_file - self.client_cert = configuration.cert_file - - self.proxy_port = self.proxy_host = None - - # https pool manager - if configuration.proxy: - self.proxy_port = 80 - self.proxy_host = configuration.proxy - - self.pool_manager = httpclient.AsyncHTTPClient() - - @tornado.gen.coroutine - def request( - self, - method, - url, - headers=None, - body=None, - post_params=None, - _request_timeout=None - ): - """Execute Request - - :param method: http request method - :param url: http request url - :param headers: http request headers - :param body: request json body, for `application/json` - :param post_params: request post parameters, - `application/x-www-form-urlencoded` - and `multipart/form-data` - :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. - """ - method = method.upper() - assert method in [ - 'GET', - 'HEAD', - 'DELETE', - 'POST', - 'PUT', - 'PATCH', - 'OPTIONS' - ] - - if post_params and body: - raise ApiValueError( - "body parameter cannot be used with post_params parameter." - ) - - request = httpclient.HTTPRequest(url) - request.allow_nonstandard_methods = True - request.ca_certs = self.ca_certs - request.client_key = self.client_key - request.client_cert = self.client_cert - request.proxy_host = self.proxy_host - request.proxy_port = self.proxy_port - request.method = method - if headers: - request.headers = headers - if 'Content-Type' not in headers: - request.headers['Content-Type'] = 'application/json' - request.request_timeout = _request_timeout or 5 * 60 - - post_params = post_params or {} - - # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` - if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: - if re.search('json', headers['Content-Type'], re.IGNORECASE): - if body: - body = json.dumps(body) - request.body = body - elif headers['Content-Type'] == 'application/x-www-form-urlencoded': - request.body = urlencode(post_params) - elif headers['Content-Type'] == 'multipart/form-data': - multipart = encode_multipart_formdata(post_params) - request.body, headers['Content-Type'] = multipart - # Pass a `bytes` parameter directly in the body to support - # other content types than Json when `body` argument is provided - # in serialized form - elif isinstance(body, bytes): - request.body = body - else: - # Cannot generate the request from given parameters - msg = """Cannot prepare a request message for provided - arguments. Please check that your arguments match - declared content type.""" - raise ApiException(status=0, reason=msg) - - r = yield self.pool_manager.fetch(request, raise_error=False) - - - r = RESTResponse(r) - - raise tornado.gen.Return(r) diff --git a/templates/python/tox.mustache b/templates/python/tox.mustache deleted file mode 100644 index 9d717c3..0000000 --- a/templates/python/tox.mustache +++ /dev/null @@ -1,9 +0,0 @@ -[tox] -envlist = py3 - -[testenv] -deps=-r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt - -commands= - pytest --cov={{{packageName}}} diff --git a/templates/python/travis.mustache b/templates/python/travis.mustache deleted file mode 100644 index 53cb57e..0000000 --- a/templates/python/travis.mustache +++ /dev/null @@ -1,17 +0,0 @@ -# ref: https://docs.travis-ci.com/user/languages/python -language: python -python: - - "3.7" - - "3.8" - - "3.9" - - "3.10" - - "3.11" - # uncomment the following if needed - #- "3.11-dev" # 3.11 development branch - #- "nightly" # nightly build -# command to install dependencies -install: - - "pip install -r requirements.txt" - - "pip install -r test-requirements.txt" -# command to run tests -script: pytest --cov={{{packageName}}}