diff --git a/openapi-generator-cli.jar b/openapi-generator-cli.jar index 58d9645fa..e4b45257f 100644 Binary files a/openapi-generator-cli.jar and b/openapi-generator-cli.jar differ diff --git a/requirements.txt b/requirements.txt index 258c179c1..c86a1f3eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ python_dateutil >= 2.5.3 setuptools >= 21.0.0 urllib3 >= 1.25.3, < 2.1.0 -pydantic >= 1.10.5, < 2 +pydantic >= 2 aenum >= 3.1.11 +mypy>=1.4.1 +types-python-dateutil>=2.8.19 diff --git a/sdk-resources/resources/README.mustache b/sdk-resources/resources/README.mustache index 201df14f1..bceb88f3b 100644 --- a/sdk-resources/resources/README.mustache +++ b/sdk-resources/resources/README.mustache @@ -10,6 +10,7 @@ This Python package is automatically generated by the [OpenAPI Generator](https: {{^hideGenerationTimestamp}} - Build date: {{generatedDate}} {{/hideGenerationTimestamp}} +- Generator version: {{generatorVersion}} - Build package: {{generatorClass}} {{#infoUrl}} For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) diff --git a/sdk-resources/resources/README_onlypackage.mustache b/sdk-resources/resources/README_onlypackage.mustache index 7195605a0..ae547b1e6 100644 --- a/sdk-resources/resources/README_onlypackage.mustache +++ b/sdk-resources/resources/README_onlypackage.mustache @@ -10,6 +10,7 @@ The `{{packageName}}` package is automatically generated by the [OpenAPI Generat {{^hideGenerationTimestamp}} - Build date: {{generatedDate}} {{/hideGenerationTimestamp}} +- Generator version: {{generatorVersion}} - Build package: {{generatorClass}} {{#infoUrl}} For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) diff --git a/sdk-resources/resources/api.mustache b/sdk-resources/resources/api.mustache index fdfbfbb87..8baf78f7a 100644 --- a/sdk-resources/resources/api.mustache +++ b/sdk-resources/resources/api.mustache @@ -1,23 +1,16 @@ # coding: utf-8 {{>partial_header}} - -import io import warnings - from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt -from typing import Dict, List, Optional, Tuple, Union, Any - -try: - from typing import Annotated -except ImportError: - from typing_extensions import Annotated +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated {{#imports}} {{import}} {{/imports}} -from {{packageName}}.api_client import ApiClient +from {{packageName}}.api_client import ApiClient, RequestSerialized from {{packageName}}.api_response import ApiResponse from {{packageName}}.rest import RESTResponseType @@ -84,7 +77,7 @@ class {{classname}}: _content_type, _headers, _host_index, - ) -> Tuple: + ) -> RequestSerialized: {{#servers.0}} _hosts = [{{#servers}} @@ -108,7 +101,7 @@ 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, str] = {} + _files: Dict[str, Union[str, bytes]] = {} _body_params: Optional[bytes] = None # process the path parameters @@ -170,7 +163,7 @@ class {{classname}}: {{#isBinary}} # convert to byte array if the input is a file name (str) if isinstance({{paramName}}, str): - with io.open({{paramName}}, "rb") as _fp: + with open({{paramName}}, "rb") as _fp: _body_params = _fp.read() else: _body_params = {{paramName}} @@ -183,7 +176,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}}". @@ -193,11 +186,12 @@ class {{classname}}: {{#hasProduces}} # set the HTTP header `Accept` - _header_params['Accept'] = self.api_client.select_header_accept( - [{{#produces}} - '{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}} - ] - ) + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [{{#produces}} + '{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}} + ] + ) {{/hasProduces}} {{#hasConsumes}} diff --git a/sdk-resources/resources/api_client.mustache b/sdk-resources/resources/api_client.mustache index b8514e376..e8745a3eb 100644 --- a/sdk-resources/resources/api_client.mustache +++ b/sdk-resources/resources/api_client.mustache @@ -2,9 +2,10 @@ {{>partial_header}} -import atexit import datetime from dateutil.parser import parse +from enum import Enum +import decimal import json import mimetypes import os @@ -12,13 +13,14 @@ import re import tempfile from urllib.parse import quote -from typing import Tuple, Optional, List +from typing import Tuple, Optional, List, Dict, Union +from pydantic import SecretStr {{#tornado}} import tornado.gen {{/tornado}} from sailpoint.configuration import Configuration -from {{packageName}}.api_response import ApiResponse +from {{packageName}}.api_response import ApiResponse, T as ApiResponseT import {{modelPackage}} from {{packageName}} import rest from {{packageName}}.exceptions import ( @@ -42,6 +44,7 @@ class bcolors: BOLD = '\033[1m' UNDERLINE = '\033[4m' +RequestSerialized = Tuple[str, str, Dict[str, str], Optional[str], List[str]] class ApiClient: """Generic API client for OpenAPI client library builds. @@ -68,6 +71,7 @@ class ApiClient: 'bool': bool, 'date': datetime.date, 'datetime': datetime.datetime, + 'decimal': decimal.Decimal, 'object': object, } _pool = None @@ -163,7 +167,7 @@ class ApiClient: collection_formats=None, _host=None, _request_auth=None - ) -> Tuple: + ) -> RequestSerialized: """Builds the HTTP request params needed by the request. :param method: Method to call. @@ -229,7 +233,8 @@ class ApiClient: post_params, collection_formats ) - post_params.extend(self.files_parameters(files)) + if files: + post_params.extend(self.files_parameters(files)) # auth setting self.update_params_for_auth( @@ -299,23 +304,23 @@ class ApiClient: ) except ApiException as e: - if e.body: - e.body = e.body.decode('utf-8') raise e return response_data def response_deserialize( self, - response_data: rest.RESTResponse = None, - response_types_map=None - ) -> ApiResponse: + response_data: rest.RESTResponse, + response_types_map: Optional[Dict[str, ApiResponseT]]=None + ) -> ApiResponse[ApiResponseT]: """Deserializes response into an object. :param response_data: RESTResponse object to be deserialized. :param response_types_map: dict of response types. :return: ApiResponse """ + msg = "RESTResponse.read() must be called before passing it to response_deserialize()" + assert response_data.data is not None, msg 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: @@ -337,7 +342,7 @@ class ApiClient: match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) encoding = match.group(1) if match else "utf-8" response_text = response_data.data.decode(encoding) - return_data = self.deserialize(response_text, response_type) + return_data = self.deserialize(response_text, response_type, content_type) finally: if not 200 <= response_data.status <= 299: raise ApiException.from_response( @@ -357,9 +362,11 @@ class ApiClient: """Builds a JSON POST object. If obj is None, return None. + If obj is SecretStr, return obj.get_secret_value() 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. @@ -369,6 +376,10 @@ class ApiClient: """ if obj is None: return None + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, SecretStr): + return obj.get_secret_value() elif isinstance(obj, self.PRIMITIVE_TYPES): return obj elif isinstance(obj, list): @@ -381,6 +392,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 @@ -390,28 +403,45 @@ class ApiClient: # and attributes which value is not None. # Convert attribute name to json key in # model definition for request. - obj_dict = obj.to_dict() + if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')): + obj_dict = obj.to_dict() + else: + obj_dict = obj.__dict__ return { key: self.sanitize_for_serialization(val) for key, val in obj_dict.items() } - def deserialize(self, response_text, response_type): + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): """Deserializes response into an object. :param response: RESTResponse object to be deserialized. :param response_type: class literal for deserialized object, or string of class name. + :param content_type: content type of response. :return: deserialized object. """ # fetch data from response object - try: - data = json.loads(response_text) - except ValueError: + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif content_type.startswith("application/json"): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif content_type.startswith("text/plain"): data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) return self.__deserialize(data, response_type) @@ -428,12 +458,16 @@ class ApiClient: if isinstance(klass, str): if klass.startswith('List['): - sub_kls = re.match(r'List\[(.*)]', klass).group(1) + m = re.match(r'List\[(.*)]', klass) + assert m is not None, "Malformed List type definition" + sub_kls = m.group(1) return [self.__deserialize(sub_data, sub_kls) for sub_data in data] if klass.startswith('Dict['): - sub_kls = re.match(r'Dict\[([^,]*), (.*)]', klass).group(2) + m = re.match(r'Dict\[([^,]*), (.*)]', klass) + assert m is not None, "Malformed Dict type definition" + sub_kls = m.group(2) return {k: self.__deserialize(v, sub_kls) for k, v in data.items()} @@ -451,6 +485,10 @@ class ApiClient: return self.__deserialize_date(data) elif klass == datetime.datetime: return self.__deserialize_datetime(data) + elif klass == decimal.Decimal: + return decimal.Decimal(data) + elif issubclass(klass, Enum): + return self.__deserialize_enum(data, klass) else: return self.__deserialize_model(data, klass) @@ -461,7 +499,7 @@ class ApiClient: :param dict collection_formats: Parameter collection formats :return: Parameters as list of tuples, collections formatted """ - new_params = [] + new_params: List[Tuple[str, str]] = [] if collection_formats is None: collection_formats = {} for k, v in params.items() if isinstance(params, dict) else params: @@ -491,7 +529,7 @@ class ApiClient: :param dict collection_formats: Parameter collection formats :return: URL query string (e.g. a=Hello%20World&b=123) """ - new_params = [] + new_params: List[Tuple[str, str]] = [] if collection_formats is None: collection_formats = {} for k, v in params.items() if isinstance(params, dict) else params: @@ -505,7 +543,7 @@ class ApiClient: if k in collection_formats: collection_format = collection_formats[k] if collection_format == 'multi': - new_params.extend((k, value) for value in v) + new_params.extend((k, str(value)) for value in v) else: if collection_format == 'ssv': delimiter = ' ' @@ -521,33 +559,32 @@ class ApiClient: else: new_params.append((k, quote(str(v)))) - return "&".join(["=".join(item) for item in new_params]) + return "&".join(["=".join(map(str, item)) for item in new_params]) - def files_parameters(self, files=None): + def files_parameters(self, files: Dict[str, Union[str, bytes]]): """Builds form parameters. :param files: File parameters. :return: Form parameters with files. """ params = [] - - if files: - for k, v in files.items(): - if not v: - continue - file_names = v if type(v) is list else [v] - for n in file_names: - with open(n, 'rb') as f: - filename = os.path.basename(f.name) - filedata = f.read() - mimetype = ( - mimetypes.guess_type(filename)[0] - or 'application/octet-stream' - ) - params.append( - tuple([k, tuple([filename, filedata, mimetype])]) - ) - + for k, v in files.items(): + if isinstance(v, str): + with open(v, 'rb') as f: + filename = os.path.basename(f.name) + filedata = f.read() + elif isinstance(v, bytes): + filename = k + filedata = v + else: + raise ValueError("Unsupported file value") + mimetype = ( + mimetypes.guess_type(filename)[0] + or 'application/octet-stream' + ) + params.append( + tuple([k, tuple([filename, filedata, mimetype])]) + ) return params def select_header_accept(self, accepts: List[str]) -> Optional[str]: @@ -685,10 +722,12 @@ class ApiClient: content_disposition = response.getheader("Content-Disposition") if content_disposition: - filename = re.search( + m = re.search( r'filename=[\'"]?([^\'"\s]+)[\'"]?', content_disposition - ).group(1) + ) + assert m is not None, "Unexpected 'content-disposition' header value" + filename = m.group(1) path = os.path.join(os.path.dirname(path), filename) with open(path, "wb") as f: @@ -755,6 +794,24 @@ class ApiClient: ) ) + def __deserialize_enum(self, data, klass): + """Deserializes primitive type to enum. + + :param data: primitive type. + :param klass: class literal. + :return: enum value. + """ + try: + return klass(data) + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as `{1}`" + .format(data, klass) + ) + ) + def __deserialize_model(self, data, klass): """Deserializes list or dict to model. diff --git a/sdk-resources/resources/api_doc_example.mustache b/sdk-resources/resources/api_doc_example.mustache index ad276db28..f5d7eef12 100644 --- a/sdk-resources/resources/api_doc_example.mustache +++ b/sdk-resources/resources/api_doc_example.mustache @@ -1,7 +1,5 @@ ```python -import time -import os import {{{packageName}}} {{#vendorExtensions.x-py-example-import}} {{{.}}} diff --git a/sdk-resources/resources/api_response.mustache b/sdk-resources/resources/api_response.mustache index 2ac1ada6e..9bc7c11f6 100644 --- a/sdk-resources/resources/api_response.mustache +++ b/sdk-resources/resources/api_response.mustache @@ -1,8 +1,8 @@ """API response object.""" from __future__ import annotations -from typing import Any, Dict, Optional, Generic, TypeVar -from pydantic import Field, StrictInt, StrictStr, StrictBytes, BaseModel +from typing import Optional, Generic, Mapping, TypeVar +from pydantic import Field, StrictInt, StrictBytes, BaseModel T = TypeVar("T") @@ -12,7 +12,7 @@ class ApiResponse(BaseModel, Generic[T]): """ status_code: StrictInt = Field(description="HTTP status code") - headers: Optional[Dict[StrictStr, StrictStr]] = Field(None, description="HTTP headers") + 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)") diff --git a/sdk-resources/resources/asyncio/rest.mustache b/sdk-resources/resources/asyncio/rest.mustache index bc1e8138c..c3f864368 100644 --- a/sdk-resources/resources/asyncio/rest.mustache +++ b/sdk-resources/resources/asyncio/rest.mustache @@ -6,6 +6,7 @@ import io import json import re import ssl +from typing import Optional, Union import aiohttp import aiohttp_retry @@ -72,13 +73,14 @@ class RESTClientObject: ) 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, + factor=2.0, + start_timeout=0.1, max_timeout=120.0 ) ) @@ -175,10 +177,10 @@ class RESTClientObject: data.add_field(k, v) args["data"] = data - # Pass a `bytes` parameter directly in the body to support + # 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, bytes): + elif isinstance(body, str) or isinstance(body, bytes): args["data"] = body else: # Cannot generate the request from given parameters @@ -187,6 +189,7 @@ class RESTClientObject: 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: diff --git a/sdk-resources/resources/common_README.mustache b/sdk-resources/resources/common_README.mustache index 706d9b344..b7ce4615d 100644 --- a/sdk-resources/resources/common_README.mustache +++ b/sdk-resources/resources/common_README.mustache @@ -1,6 +1,5 @@ ```python {{#apiInfo}}{{#apis}}{{#-last}}{{#hasHttpSignatureMethods}}import datetime{{/hasHttpSignatureMethods}}{{/-last}}{{/apis}}{{/apiInfo}} -import time import {{{packageName}}} from {{{packageName}}}.rest import ApiException from pprint import pprint diff --git a/sdk-resources/resources/configuration.mustache b/sdk-resources/resources/configuration.mustache index c3799c0c5..8b9a6f485 100644 --- a/sdk-resources/resources/configuration.mustache +++ b/sdk-resources/resources/configuration.mustache @@ -4,10 +4,12 @@ import copy import logging +from logging import FileHandler {{^asyncio}} import multiprocessing {{/asyncio}} import sys +from typing import Optional import urllib3 import http.client as httplib @@ -22,6 +24,9 @@ class Configuration: """This class contains various settings of the API client. :param host: Base url. + :param ignore_operation_servers + Boolean to ignore operation servers for the API client. + Config will use `host` as the base url regardless of the operation servers. :param api_key: Dict to store API key(s). Each entry in the dict specifies an API key. The dict key is the name of the security scheme in the OAS specification. @@ -48,6 +53,7 @@ class Configuration: values before. :param ssl_ca_cert: str - the path to a file of concatenated CA certificates in PEM format. + :param retries: Number of retries for API requests. {{#hasAuthMethods}} :Example: @@ -145,7 +151,11 @@ conf = {{{packageName}}}.Configuration( {{/hasHttpSignatureMethods}} server_index=None, server_variables=None, server_operation_index=None, server_operation_variables=None, + ignore_operation_servers=False, ssl_ca_cert=None, + retries=None, + *, + debug: Optional[bool] = None ) -> None: """Constructor """ @@ -160,6 +170,9 @@ conf = {{{packageName}}}.Configuration( self.server_operation_variables = server_operation_variables or {} """Default server variables """ + self.ignore_operation_servers = ignore_operation_servers + """Ignore operation servers + """ self.temp_folder_path = None """Temp file folder for downloading files """ @@ -204,13 +217,16 @@ conf = {{{packageName}}}.Configuration( self.logger_stream_handler = None """Log stream handler """ - self.logger_file_handler = None + self.logger_file_handler: Optional[FileHandler] = None """Log file handler """ self.logger_file = None """Debug file location """ - self.debug = False + if debug is not None: + self.debug = debug + else: + self.__debug = False """Debug switch """ @@ -252,7 +268,7 @@ conf = {{{packageName}}}.Configuration( """ {{/asyncio}} - self.proxy = None + self.proxy: Optional[str] = None """Proxy URL """ self.proxy_headers = None @@ -261,7 +277,7 @@ conf = {{{packageName}}}.Configuration( self.safe_chars_for_path_param = '' """Safe chars for path_param """ - self.retries = None + self.retries = retries """Adding retries to override urllib3 default value 3 """ # Enable client side validation diff --git a/sdk-resources/resources/exceptions.mustache b/sdk-resources/resources/exceptions.mustache index 7f1f6c2ca..cb1993236 100644 --- a/sdk-resources/resources/exceptions.mustache +++ b/sdk-resources/resources/exceptions.mustache @@ -2,7 +2,6 @@ {{>partial_header}} from typing import Any, Optional - from typing_extensions import Self class OpenApiException(Exception): diff --git a/sdk-resources/resources/model_anyof.mustache b/sdk-resources/resources/model_anyof.mustache index 0d575011b..e035e4829 100644 --- a/sdk-resources/resources/model_anyof.mustache +++ b/sdk-resources/resources/model_anyof.mustache @@ -3,22 +3,15 @@ from inspect import getfullargspec import json import pprint import re # noqa: F401 -{{#vendorExtensions.x-py-datetime-imports}}{{#-first}}from datetime import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-datetime-imports}} -{{#vendorExtensions.x-py-typing-imports}}{{#-first}}from typing import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-typing-imports}} -{{#vendorExtensions.x-py-pydantic-imports}}{{#-first}}from pydantic import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-pydantic-imports}} {{#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, TYPE_CHECKING, Optional, Dict -from typing_extensions import Literal -from pydantic import StrictStr, Field -try: - from typing import Self -except ImportError: - from typing_extensions import Self +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}}] @@ -35,7 +28,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} actual_instance: Optional[Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}]] = None else: actual_instance: Any = None - any_of_schemas: List[str] = Literal[{{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ANY_OF_SCHEMAS] + any_of_schemas: Set[str] = { {{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}} } model_config = { "validate_assignment": True, @@ -102,7 +95,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} return v @classmethod - def from_dict(cls, obj: dict) -> Self: + def from_dict(cls, obj: Dict[str, Any]) -> Self: return cls.from_json(json.dumps(obj)) @classmethod @@ -161,22 +154,20 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} if self.actual_instance is None: return "null" - to_json = getattr(self.actual_instance, "to_json", None) - if callable(to_json): + 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) -> Dict: + 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 "null" + return None - to_json = getattr(self.actual_instance, "to_json", None) - if callable(to_json): + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): return self.actual_instance.to_dict() else: - return json.dumps(self.actual_instance) + return self.actual_instance def to_str(self) -> str: """Returns the string representation of the actual instance""" diff --git a/sdk-resources/resources/model_doc.mustache b/sdk-resources/resources/model_doc.mustache index deb49f248..98d50cf8e 100644 --- a/sdk-resources/resources/model_doc.mustache +++ b/sdk-resources/resources/model_doc.mustache @@ -3,6 +3,7 @@ {{#description}}{{&description}} {{/description}} +{{^isEnum}} ## Properties Name | Type | Description | Notes @@ -10,7 +11,6 @@ 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}} -{{^isEnum}} ## Example ```python @@ -21,14 +21,20 @@ 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() +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}}_form_dict = {{#lambda.snakecase}}{{classname}}{{/lambda.snakecase}}.from_dict({{#lambda.snakecase}}{{classname}}{{/lambda.snakecase}}_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/sdk-resources/resources/model_enum.mustache b/sdk-resources/resources/model_enum.mustache index 0bd7dafbe..3f449b121 100644 --- a/sdk-resources/resources/model_enum.mustache +++ b/sdk-resources/resources/model_enum.mustache @@ -1,15 +1,10 @@ from __future__ import annotations import json -import pprint -import re # noqa: F401 from enum import Enum -{{#vendorExtensions.x-py-datetime-imports}}{{#-first}}from datetime import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-datetime-imports}} -{{#vendorExtensions.x-py-typing-imports}}{{#-first}}from typing import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-typing-imports}} -{{#vendorExtensions.x-py-pydantic-imports}}{{#-first}}from pydantic import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-pydantic-imports}} -try: - from typing import Self -except ImportError: - from typing_extensions import Self +{{#vendorExtensions.x-py-other-imports}} +{{{.}}} +{{/vendorExtensions.x-py-other-imports}} +from typing_extensions import Self class {{classname}}({{vendorExtensions.x-py-enum-type}}, Enum): diff --git a/sdk-resources/resources/model_generic.mustache b/sdk-resources/resources/model_generic.mustache index 1b676e469..768f4f750 100644 --- a/sdk-resources/resources/model_generic.mustache +++ b/sdk-resources/resources/model_generic.mustache @@ -3,20 +3,26 @@ import pprint import re # noqa: F401 import json -{{#vendorExtensions.x-py-datetime-imports}}{{#-first}}from datetime import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-datetime-imports}} -{{#vendorExtensions.x-py-typing-imports}}{{#-first}}from typing import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-typing-imports}} -{{#vendorExtensions.x-py-pydantic-imports}}{{#-first}}from pydantic import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-pydantic-imports}} {{#vendorExtensions.x-py-other-imports}} {{{.}}} {{/vendorExtensions.x-py-other-imports}} {{#vendorExtensions.x-py-model-imports}} {{{.}}} {{/vendorExtensions.x-py-model-imports}} -try: - from typing import Self -except ImportError: - from typing_extensions import Self +from typing import Optional, Set +from typing_extensions import Self +{{#hasChildren}} +{{#discriminator}} +{{! If this model is a super class, importlib is used. So import the necessary modules for the type here. }} +from typing import TYPE_CHECKING +if TYPE_CHECKING: +{{#mappedModels}} + from {{packageName}}.models.{{model.classVarName}} import {{modelName}} +{{/mappedModels}} + +{{/discriminator}} +{{/hasChildren}} class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}): """ {{#description}}{{{description}}}{{/description}}{{^description}}{{{classname}}}{{/description}} @@ -69,28 +75,28 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{/required}} {{#isArray}} for i in value: - if i not in ({{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}): + 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}} - if value not in ({{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}): + 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}} return value {{/isEnum}} {{/vars}} - model_config = { - "populate_by_name": True, - "validate_assignment": True, - "protected_namespaces": (), - } + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) {{#hasChildren}} {{#discriminator}} # JSON field name that stores the object type - __discriminator_property_name: ClassVar[List[str]] = '{{discriminator.propertyBaseName}}' + __discriminator_property_name: ClassVar[str] = '{{discriminator.propertyBaseName}}' # discriminator mappings __discriminator_value_class_map: ClassVar[Dict[str, str]] = { @@ -98,7 +104,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} } @classmethod - def get_discriminator_value(cls, obj: Dict) -> str: + def get_discriminator_value(cls, obj: Dict[str, Any]) -> Optional[str]: """Returns the discriminator value (object type) of the data""" discriminator_value = obj[cls.__discriminator_property_name] if discriminator_value: @@ -118,7 +124,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} return json.dumps(self.to_dict()) @classmethod - def from_json(cls, json_str: str) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}: + def from_json(cls, json_str: str) -> Optional[{{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#mappedModels}}{{{modelName}}}{{^-last}}, {{/-last}}{{/mappedModels}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}]: """Create an instance of {{{classname}}} from a JSON string""" return cls.from_dict(json.loads(json_str)) @@ -138,16 +144,18 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} * Fields in `self.additional_properties` are added to the output dict. {{/isAdditionalPropertiesTrue}} """ + excluded_fields: Set[str] = set([ + {{#vendorExtensions.x-py-readonly}} + "{{{.}}}", + {{/vendorExtensions.x-py-readonly}} + {{#isAdditionalPropertiesTrue}} + "additional_properties", + {{/isAdditionalPropertiesTrue}} + ]) + _dict = self.model_dump( by_alias=True, - exclude={ - {{#vendorExtensions.x-py-readonly}} - "{{{.}}}", - {{/vendorExtensions.x-py-readonly}} - {{#isAdditionalPropertiesTrue}} - "additional_properties", - {{/isAdditionalPropertiesTrue}} - }, + exclude=excluded_fields, exclude_none=True, ) {{#allVars}} @@ -158,10 +166,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}} @@ -172,9 +180,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}} @@ -186,10 +194,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}} @@ -200,9 +208,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}} @@ -237,23 +245,27 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{/allVars}} return _dict + {{#hasChildren}} @classmethod - def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}: + def from_dict(cls, obj: Dict[str, Any]) -> Optional[{{#discriminator}}Union[{{#mappedModels}}{{{modelName}}}{{^-last}}, {{/-last}}{{/mappedModels}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}]: """Create an instance of {{{classname}}} from a dict""" - {{#hasChildren}} {{#discriminator}} # look up the object type based on discriminator mapping object_type = cls.get_discriminator_value(obj) - if object_type: - klass = globals()[object_type] - return klass.from_dict(obj) - else: - raise ValueError("{{{classname}}} failed to lookup discriminator value from " + - json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + - ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) + {{#mappedModels}} + if object_type == '{{{modelName}}}': + return import_module("{{packageName}}.models.{{model.classVarName}}").{{modelName}}.from_dict(obj) + {{/mappedModels}} + + raise ValueError("{{{classname}}} failed to lookup discriminator value from " + + json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + + ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) {{/discriminator}} - {{/hasChildren}} - {{^hasChildren}} + {{/hasChildren}} + {{^hasChildren}} + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of {{{classname}}} from a dict""" if obj is None: return None @@ -280,7 +292,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{^items.items.isPrimitiveType}} "{{{baseName}}}": [ [{{{items.items.dataType}}}.from_dict(_inner_item) for _inner_item in _item] - for _item in obj.get("{{{baseName}}}") + for _item in obj["{{{baseName}}}"] ] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} {{/items.items.isPrimitiveType}} {{/items.isArray}} @@ -290,7 +302,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} {{/items.isEnumOrRef}} {{^items.isEnumOrRef}} - "{{{baseName}}}": [{{{items.dataType}}}.from_dict(_item) for _item in obj.get("{{{baseName}}}")] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} + "{{{baseName}}}": [{{{items.dataType}}}.from_dict(_item) for _item in obj["{{{baseName}}}"]] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} {{/items.isEnumOrRef}} {{/items.isPrimitiveType}} {{#items.isPrimitiveType}} @@ -323,14 +335,14 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} if _v is not None else None ) - for _k, _v in obj.get("{{{baseName}}}").items() + for _k, _v in obj.get("{{{baseName}}}", {}).items() ){{^-last}},{{/-last}} {{/items.isArray}} {{/items.isContainer}} {{^items.isContainer}} "{{{baseName}}}": dict( (_k, {{{items.dataType}}}.from_dict(_v)) - for _k, _v in obj.get("{{{baseName}}}").items() + for _k, _v in obj["{{{baseName}}}"].items() ) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} @@ -348,10 +360,10 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{^isContainer}} {{^isPrimitiveType}} {{^isEnumOrRef}} - "{{{baseName}}}": {{{dataType}}}.from_dict(obj.get("{{{baseName}}}")) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} + "{{{baseName}}}": {{{dataType}}}.from_dict(obj["{{{baseName}}}"]) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} {{/isEnumOrRef}} {{#isEnumOrRef}} - "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} + "{{{baseName}}}": obj.get("{{{baseName}}}"){{#defaultValue}} if obj.get("{{baseName}}") is not None else {{defaultValue}}{{/defaultValue}}{{^-last}},{{/-last}} {{/isEnumOrRef}} {{/isPrimitiveType}} {{#isPrimitiveType}} @@ -373,7 +385,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{/isAdditionalPropertiesTrue}} return _obj - {{/hasChildren}} + {{/hasChildren}} {{#vendorExtensions.x-py-postponed-model-imports.size}} {{#vendorExtensions.x-py-postponed-model-imports}} diff --git a/sdk-resources/resources/model_oneof.mustache b/sdk-resources/resources/model_oneof.mustache index b87c42cf2..07a4d93f9 100644 --- a/sdk-resources/resources/model_oneof.mustache +++ b/sdk-resources/resources/model_oneof.mustache @@ -1,24 +1,15 @@ from __future__ import annotations -from inspect import getfullargspec import json import pprint -import re # noqa: F401 -{{#vendorExtensions.x-py-datetime-imports}}{{#-first}}from datetime import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-datetime-imports}} -{{#vendorExtensions.x-py-typing-imports}}{{#-first}}from typing import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-typing-imports}} -{{#vendorExtensions.x-py-pydantic-imports}}{{#-first}}from pydantic import{{/-first}} {{{.}}}{{^-last}},{{/-last}}{{/vendorExtensions.x-py-pydantic-imports}} {{#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, TYPE_CHECKING, Optional, Dict -from typing_extensions import Literal from pydantic import StrictStr, Field -try: - from typing import Self -except ImportError: - from typing_extensions import Self +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self {{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ONE_OF_SCHEMAS = [{{#oneOf}}"{{.}}"{{^-last}}, {{/-last}}{{/oneOf}}] @@ -31,12 +22,12 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} {{/composedSchemas.oneOf}} actual_instance: Optional[Union[{{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}]] = None - one_of_schemas: List[str] = Literal[{{#oneOf}}"{{.}}"{{^-last}}, {{/-last}}{{/oneOf}}] + one_of_schemas: Set[str] = { {{#oneOf}}"{{.}}"{{^-last}}, {{/-last}}{{/oneOf}} } - model_config = { - "validate_assignment": True, - "protected_namespaces": (), - } + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) {{#discriminator}} @@ -102,11 +93,16 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} return v @classmethod - def from_dict(cls, obj: dict) -> Self: + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: return cls.from_json(json.dumps(obj)) @classmethod + {{#isNullable}} + def from_json(cls, json_str: Optional[str]) -> Self: + {{/isNullable}} + {{^isNullable}} def from_json(cls, json_str: str) -> Self: + {{/isNullable}} """Returns the object represented by the json string""" instance = cls.model_construct() {{#isNullable}} @@ -184,19 +180,17 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} if self.actual_instance is None: return "null" - to_json = getattr(self.actual_instance, "to_json", None) - if callable(to_json): + 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) -> Dict: + def to_dict(self) -> Optional[Union[Dict[str, Any], {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}]]: """Returns the dict representation of the actual instance""" if self.actual_instance is None: return None - to_dict = getattr(self.actual_instance, "to_dict", None) - if callable(to_dict): + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): return self.actual_instance.to_dict() else: # primitive type diff --git a/sdk-resources/resources/model_test.mustache b/sdk-resources/resources/model_test.mustache index 9919e3d7a..08088557c 100644 --- a/sdk-resources/resources/model_test.mustache +++ b/sdk-resources/resources/model_test.mustache @@ -3,7 +3,6 @@ {{>partial_header}} import unittest -import datetime {{#models}} {{#model}} @@ -21,7 +20,7 @@ class Test{{classname}}(unittest.TestCase): def make_instance(self, include_optional) -> {{classname}}: """Test {{classname}} - include_option is a boolean, when False only required + 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}}}` diff --git a/sdk-resources/resources/partial_api_args.mustache b/sdk-resources/resources/partial_api_args.mustache index 3244bdcbf..55241e316 100644 --- a/sdk-resources/resources/partial_api_args.mustache +++ b/sdk-resources/resources/partial_api_args.mustache @@ -8,7 +8,7 @@ {{#allParams}} {{#isHeaderParam}} {{paramName}}: {{{vendorExtensions.x-py-typing}}}{{#required}}{{#isHeaderParam}} = {{{defaultValue}}}{{/isHeaderParam}}{{/required}}{{^required}} = None{{/required}}, - {{/isHeaderParam}} + {{/isHeaderParam}} {{/allParams}} _request_timeout: Union[ None, diff --git a/sdk-resources/resources/pyproject.mustache b/sdk-resources/resources/pyproject.mustache index f4e117c27..24030f9e9 100644 --- a/sdk-resources/resources/pyproject.mustache +++ b/sdk-resources/resources/pyproject.mustache @@ -32,6 +32,9 @@ typing-extensions = ">=4.7.1" pytest = ">=7.2.1" tox = ">=3.9.0" flake8 = ">=4.0.0" +types-python-dateutil = ">=2.8.19.14" +mypy = "1.4.1" + [build-system] requires = ["setuptools"] @@ -39,3 +42,41 @@ build-backend = "setuptools.build_meta" [tool.pylint.'MESSAGES CONTROL'] extension-pkg-whitelist = "pydantic" + +[tool.mypy] +files = [ + "{{{packageName}}}", + #"test", # auto-generated tests + "tests", # hand-written tests +] +# TODO: enable "strict" once all these individual checks are passing +# strict = true + +# List from: https://mypy.readthedocs.io/en/stable/existing_code.html#introduce-stricter-options +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true + +## Getting these passing should be easy +strict_equality = true +strict_concatenate = true + +## Strongly recommend enabling this one as soon as you can +check_untyped_defs = true + +## These shouldn't be too much additional work, but may be tricky to +## get passing if you use a lot of untyped libraries +disallow_subclassing_any = true +disallow_untyped_decorators = true +disallow_any_generics = true + +### These next few are various gradations of forcing use of type annotations +#disallow_untyped_calls = true +#disallow_incomplete_defs = true +#disallow_untyped_defs = true +# +### This one isn't too hard to get passing, but return on investment is lower +#no_implicit_reexport = true +# +### This one can be tricky to get passing if you use a lot of untyped libraries +#warn_return_any = true diff --git a/sdk-resources/resources/rest.mustache b/sdk-resources/resources/rest.mustache index f09988458..07aa7ee3f 100644 --- a/sdk-resources/resources/rest.mustache +++ b/sdk-resources/resources/rest.mustache @@ -61,56 +61,45 @@ class RESTClientObject: else: cert_reqs = ssl.CERT_NONE - addition_pool_args = {} + pool_args = { + "cert_reqs": cert_reqs, + "ca_certs": configuration.ssl_ca_cert, + "cert_file": configuration.cert_file, + "key_file": configuration.key_file, + } if configuration.assert_hostname is not None: - addition_pool_args['assert_hostname'] = ( + pool_args['assert_hostname'] = ( configuration.assert_hostname ) if configuration.retries is not None: - addition_pool_args['retries'] = configuration.retries + pool_args['retries'] = configuration.retries if configuration.tls_server_name: - addition_pool_args['server_hostname'] = configuration.tls_server_name + pool_args['server_hostname'] = configuration.tls_server_name if configuration.socket_options is not None: - addition_pool_args['socket_options'] = configuration.socket_options + pool_args['socket_options'] = configuration.socket_options if configuration.connection_pool_maxsize is not None: - addition_pool_args['maxsize'] = configuration.connection_pool_maxsize + pool_args['maxsize'] = configuration.connection_pool_maxsize # https pool manager + self.pool_manager: urllib3.PoolManager + if configuration.proxy: if is_socks_proxy_url(configuration.proxy): from urllib3.contrib.socks import SOCKSProxyManager - self.pool_manager = SOCKSProxyManager( - cert_reqs=cert_reqs, - ca_certs=configuration.ssl_ca_cert, - cert_file=configuration.cert_file, - key_file=configuration.key_file, - proxy_url=configuration.proxy, - headers=configuration.proxy_headers, - **addition_pool_args - ) + pool_args["proxy_url"] = configuration.proxy + pool_args["headers"] = configuration.proxy_headers + self.pool_manager = SOCKSProxyManager(**pool_args) else: - self.pool_manager = urllib3.ProxyManager( - cert_reqs=cert_reqs, - ca_certs=configuration.ssl_ca_cert, - cert_file=configuration.cert_file, - key_file=configuration.key_file, - proxy_url=configuration.proxy, - proxy_headers=configuration.proxy_headers, - **addition_pool_args - ) + pool_args["proxy_url"] = configuration.proxy + pool_args["proxy_headers"] = configuration.proxy_headers + self.pool_manager = urllib3.ProxyManager(**pool_args) else: - self.pool_manager = urllib3.PoolManager( - cert_reqs=cert_reqs, - ca_certs=configuration.ssl_ca_cert, - cert_file=configuration.cert_file, - key_file=configuration.key_file, - **addition_pool_args - ) + self.pool_manager = urllib3.PoolManager(**pool_args) def request( self, @@ -179,7 +168,7 @@ class RESTClientObject: ): request_body = None if body is not None: - request_body = json.dumps(body) + request_body = json.dumps(body{{#setEnsureAsciiToFalse}}, ensure_ascii=False{{/setEnsureAsciiToFalse}}) r = self.pool_manager.request( method, url, @@ -203,6 +192,8 @@ class RESTClientObject: # Content-Type which generated by urllib3 will be # overwritten. del headers['Content-Type'] + # Ensures that dict objects are serialized + post_params = [(a, json.dumps(b)) if isinstance(b, dict) else (a,b) for a, b in post_params] r = self.pool_manager.request( method, url, @@ -213,14 +204,13 @@ class RESTClientObject: preload_content=False ) # Pass a `string` parameter directly in the body to support - # other content types than Json when `body` argument is - # provided in serialized form + # other content types than JSON when `body` argument is + # provided in serialized form. elif isinstance(body, str) or isinstance(body, bytes): - request_body = body r = self.pool_manager.request( method, url, - body=request_body, + body=body, timeout=timeout, headers=headers, preload_content=False diff --git a/sdk-resources/resources/signing.mustache b/sdk-resources/resources/signing.mustache index bb2850fdc..4d00424ea 100644 --- a/sdk-resources/resources/signing.mustache +++ b/sdk-resources/resources/signing.mustache @@ -3,13 +3,16 @@ 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 json 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 @@ -58,18 +61,20 @@ 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, + + 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. + 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 + 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 @@ -78,18 +83,19 @@ class HttpSigningConfiguration: 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 + 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 + 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. + ``(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. + 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: @@ -107,12 +113,16 @@ class HttpSigningConfiguration: :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, signing_scheme, private_key_path, - private_key_passphrase=None, - signed_headers=None, - signing_algorithm=None, - hash_algorithm=None, - signature_max_validity=None) -> None: + 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)) @@ -156,11 +166,11 @@ class HttpSigningConfiguration: if HEADER_AUTHORIZATION in signed_headers: raise Exception("'Authorization' header cannot be included in signed headers") self.signed_headers = signed_headers - self.private_key = None + 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 = None + self.host: Optional[str] = None """The host name, optionally followed by a colon and TCP port number. """ self._load_private_key() @@ -198,7 +208,7 @@ class HttpSigningConfiguration: def get_public_key(self): """Returns the public key object associated with the private key. """ - pubkey = None + 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): @@ -227,8 +237,11 @@ class HttpSigningConfiguration: 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. - (key_binary, pem_header, is_encrypted) = \ - PEM.decode(pem_data, self.private_key_passphrase) + 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': @@ -309,8 +322,11 @@ class HttpSigningConfiguration: request_headers_dict[HEADER_DIGEST] = '{0}{1}'.format( digest_prefix, b64_body_digest.decode('ascii')) elif hdr_key == HEADER_HOST.lower(): - value = target_host - request_headers_dict[HEADER_HOST] = '{0}'.format(target_host) + 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: @@ -331,6 +347,9 @@ class HttpSigningConfiguration: 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=' diff --git a/sdk-resources/resources/test-requirements.mustache b/sdk-resources/resources/test-requirements.mustache index 3a0d0b939..8e6d8cb13 100644 --- a/sdk-resources/resources/test-requirements.mustache +++ b/sdk-resources/resources/test-requirements.mustache @@ -1,3 +1,5 @@ pytest~=7.1.3 pytest-cov>=2.8.1 pytest-randomly>=3.12.0 +mypy>=1.4.1 +types-python-dateutil>=2.8.19