From d0149a1bdc64419ae75376ea51ee1150ba54aa52 Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Sat, 4 Jan 2025 22:37:35 +0500 Subject: [PATCH 1/6] adds initial typing support --- apimatic_core/http/request/http_request.py | 44 +++++++++++--------- apimatic_core/http/response/http_response.py | 18 +++++--- apimatic_core/types/file_wrapper.py | 26 +++++++----- apimatic_core/utilities/api_helper.py | 6 ++- apimatic_core/utilities/datetime_helper.py | 13 ++++++ 5 files changed, 70 insertions(+), 37 deletions(-) diff --git a/apimatic_core/http/request/http_request.py b/apimatic_core/http/request/http_request.py index d22a3f1..bcc4f31 100644 --- a/apimatic_core/http/request/http_request.py +++ b/apimatic_core/http/request/http_request.py @@ -2,9 +2,12 @@ from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core_interfaces.types.http_method_enum import HttpMethodEnum +from pydantic import BaseModel +from typing import Optional, Dict, List, Any, Tuple -class HttpRequest(object): +class HttpRequest(BaseModel): """Information about an HTTP Request including its method, headers, parameters, URL, and Basic Auth details @@ -19,13 +22,20 @@ class HttpRequest(object): """ + http_method: str + query_url: str + headers: Optional[Dict[str, str]] = None + query_parameters: Optional[Dict[str, Any]] = None + parameters: Optional[List[Tuple[str, Any]]] = None + files: Optional[Dict[str, Any]] = None + def __init__(self, - http_method, - query_url, - headers=None, - query_parameters=None, - parameters=None, - files=None): + http_method: str, + query_url: str, + headers: Optional[Dict[str, str]]=None, + query_parameters: Optional[Dict[str, Any]]=None, + parameters: Optional[List[Tuple[str, Any]]]=None, + files: Optional[Dict[str, Any]]=None) -> None: """Constructor for the HttpRequest class Args: @@ -39,14 +49,10 @@ def __init__(self, files (dict, optional): Files to be sent with the request. """ - self.http_method = http_method - self.query_url = query_url - self.headers = headers - self.query_parameters = query_parameters - self.parameters = parameters - self.files = files - - def add_header(self, name, value): + super().__init__(http_method=http_method, query_url=query_url, headers=headers, + query_parameters=query_parameters, parameters=parameters, files=files) + + def add_header(self, name: str, value: str) -> None: """ Add a header to the HttpRequest. Args: @@ -56,7 +62,7 @@ def add_header(self, name, value): """ self.headers[name] = value - def add_parameter(self, name, value): # pragma: no cover + def add_parameter(self, name: str, value: str) -> None: # pragma: no cover """ Add a parameter to the HttpRequest. Args: @@ -66,7 +72,7 @@ def add_parameter(self, name, value): # pragma: no cover """ self.parameters[name] = value - def add_query_parameter(self, name, value): + def add_query_parameter(self, name: str, value: str) -> None: """ Add a query parameter to the HttpRequest. Args: @@ -80,5 +86,5 @@ def add_query_parameter(self, name, value): ) self.query_url = ApiHelper.clean_url(self.query_url) - def __repr__(self): - return str(self.__dict__) + def __repr__(self) -> str: + return str(self.__dict__) \ No newline at end of file diff --git a/apimatic_core/http/response/http_response.py b/apimatic_core/http/response/http_response.py index 9b0aa38..4a20842 100644 --- a/apimatic_core/http/response/http_response.py +++ b/apimatic_core/http/response/http_response.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +from apimatic_core.http.request.http_request import HttpRequest +from pydantic import BaseModel +from typing import Dict -class HttpResponse(object): +class HttpResponse(BaseModel): """Information about an HTTP Response including its status code, returned headers, and raw body @@ -16,6 +19,12 @@ class HttpResponse(object): """ + status_code: int + reason_phrase: str + headers: Dict[str, str] + text: str + request: HttpRequest + def __init__(self, status_code, reason_phrase, @@ -32,8 +41,5 @@ def __init__(self, request (HttpRequest): The request that resulted in this response. """ - self.status_code = status_code - self.reason_phrase = reason_phrase - self.headers = headers - self.text = text - self.request = request + super().__init__(status_code=status_code, reason_phrase=reason_phrase, + headers=headers, text=text, request=request) diff --git a/apimatic_core/types/file_wrapper.py b/apimatic_core/types/file_wrapper.py index dedd151..9ba8fd3 100644 --- a/apimatic_core/types/file_wrapper.py +++ b/apimatic_core/types/file_wrapper.py @@ -1,14 +1,20 @@ -class FileWrapper: +from io import BufferedIOBase, TextIOWrapper + +from pydantic import BaseModel +from typing import Optional, Union, TextIO, BinaryIO + +FileType = Union[TextIO, BinaryIO, BufferedIOBase, TextIOWrapper] + +class FileWrapper(BaseModel): """A wrapper to allow passing in content type for file uploads.""" - def __init__(self, file, content_type): - self._file_stream = file - self._content_type = content_type + file_stream: FileType + content_type: Optional[str] = None - @property - def file_stream(self): - return self._file_stream + model_config = { + "arbitrary_types_allowed": True + } - @property - def content_type(self): - return self._content_type + def __init__(self, file_stream: FileType, + content_type: Optional[str]=None) -> None: + super().__init__(file_stream=file_stream, content_type=content_type) diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index d74747a..1e47c1f 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -10,6 +10,8 @@ import jsonpickle import dateutil.parser from jsonpointer import JsonPointerException, resolve_pointer +from pydantic import BaseModel + from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.types.file_wrapper import FileWrapper from apimatic_core.types.array_serialization_format import SerializationFormats @@ -48,7 +50,7 @@ def json_serialize_wrapped_params(obj): return jsonpickle.encode(val, False) @staticmethod - def json_serialize(obj, should_encode=True): + def json_serialize(obj: BaseModel, should_encode=True): """JSON Serialization of a given object. Args: @@ -89,7 +91,7 @@ def json_serialize(obj, should_encode=True): obj = value else: if hasattr(obj, "_names"): - obj = ApiHelper.to_dictionary(obj) + return obj.model_dump_json() if not should_encode: return obj return jsonpickle.encode(obj, False) diff --git a/apimatic_core/utilities/datetime_helper.py b/apimatic_core/utilities/datetime_helper.py index 662409e..b9fe5ba 100644 --- a/apimatic_core/utilities/datetime_helper.py +++ b/apimatic_core/utilities/datetime_helper.py @@ -1,5 +1,6 @@ from datetime import datetime, date from apimatic_core.types.datetime_format import DateTimeFormat +from apimatic_core.utilities.api_helper import ApiHelper class DateTimeHelper: @@ -54,3 +55,15 @@ def is_unix_timestamp(timestamp): return True except (ValueError, AttributeError, TypeError): return False + + @staticmethod + def to_rfc3339_date_time(value): + return ApiHelper.apply_datetime_converter(value, ApiHelper.RFC3339DateTime) + + @staticmethod + def to_rfc1123_date_time(value): + return ApiHelper.apply_datetime_converter(value, ApiHelper.HttpDateTime) + + @staticmethod + def to_unix_timestamp(value): + return ApiHelper.apply_datetime_converter(value, ApiHelper.UnixDateTime) From 4ca19dec8c22dd7dcc22e744e7af0030b8e2554e Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Wed, 8 Jan 2025 17:16:21 +0500 Subject: [PATCH 2/6] adds support for python --- apimatic_core/types/union_types/leaf_type.py | 18 +++-- apimatic_core/utilities/api_helper.py | 27 ++++--- apimatic_core/utilities/datetime_helper.py | 79 +++++++++++++++++++- 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/apimatic_core/types/union_types/leaf_type.py b/apimatic_core/types/union_types/leaf_type.py index fa98916..d09579e 100644 --- a/apimatic_core/types/union_types/leaf_type.py +++ b/apimatic_core/types/union_types/leaf_type.py @@ -4,7 +4,7 @@ from apimatic_core.utilities.api_helper import ApiHelper from apimatic_core.utilities.datetime_helper import DateTimeHelper from apimatic_core.utilities.union_type_helper import UnionTypeHelper - +from pydantic import BaseModel class LeafType(UnionType): @@ -116,8 +116,12 @@ def _validate_value_with_discriminator(self, value, context): if discriminator and discriminator_value: return self._validate_with_discriminator(discriminator, discriminator_value, value) - if hasattr(self.type_to_match, 'validate'): - return self.type_to_match.validate(value) + if issubclass(self.type_to_match, BaseModel): + try: + self.type_to_match.model_validate(value) + return True + except: + return False return type(value) is self.type_to_match @@ -125,8 +129,12 @@ def _validate_with_discriminator(self, discriminator, discriminator_value, value if not isinstance(value, dict) or value.get(discriminator) != discriminator_value: return False - if hasattr(self.type_to_match, 'validate'): - return self.type_to_match.validate(value) + if issubclass(self.type_to_match, BaseModel): + try: + self.type_to_match.model_validate(value) + return True + except: + return False return type(value) is self.type_to_match diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index 1e47c1f..b588e3d 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -10,12 +10,11 @@ import jsonpickle import dateutil.parser from jsonpointer import JsonPointerException, resolve_pointer -from pydantic import BaseModel - from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.types.file_wrapper import FileWrapper from apimatic_core.types.array_serialization_format import SerializationFormats from requests.utils import quote +from pydantic import BaseModel class ApiHelper(object): @@ -50,7 +49,7 @@ def json_serialize_wrapped_params(obj): return jsonpickle.encode(val, False) @staticmethod - def json_serialize(obj: BaseModel, should_encode=True): + def json_serialize(obj, should_encode=True): """JSON Serialization of a given object. Args: @@ -74,8 +73,8 @@ def json_serialize(obj: BaseModel, should_encode=True): for item in obj: if isinstance(item, dict) or isinstance(item, list): value.append(ApiHelper.json_serialize(item, False)) - elif hasattr(item, "_names"): - value.append(ApiHelper.to_dictionary(item)) + elif ApiHelper.is_custom_type(item): + value.append(item.model_dump_json()) if should_encode else obj.model_dump() else: value.append(item) obj = value @@ -84,18 +83,24 @@ def json_serialize(obj: BaseModel, should_encode=True): for key, item in obj.items(): if isinstance(item, list) or isinstance(item, dict): value[key] = ApiHelper.json_serialize(item, False) - elif hasattr(item, "_names"): - value[key] = ApiHelper.to_dictionary(item) + elif ApiHelper.is_custom_type(item): + value[key] = item.model_dump_json() if should_encode else obj.model_dump() else: value[key] = item obj = value else: - if hasattr(obj, "_names"): - return obj.model_dump_json() + if ApiHelper.is_custom_type(obj): + return obj.model_dump_json() if should_encode else obj.model_dump() if not should_encode: return obj return jsonpickle.encode(obj, False) + @staticmethod + def is_custom_type(obj_or_class): + return (isinstance(obj_or_class, type) and + issubclass(obj_or_class, BaseModel) or + isinstance(obj_or_class, BaseModel)) + @staticmethod def json_deserialize(json, unboxing_function=None, as_dict=False): """JSON Deserialization of a given string. @@ -477,8 +482,8 @@ def form_encode(obj, """ retval = [] # If we received an object, resolve it's field names. - if hasattr(obj, "_names"): - obj = ApiHelper.to_dictionary(obj) + if ApiHelper.is_custom_type(obj): + obj = ApiHelper.json_serialize(obj, should_encode=False) if obj is None: return [] diff --git a/apimatic_core/utilities/datetime_helper.py b/apimatic_core/utilities/datetime_helper.py index b9fe5ba..0a9fc03 100644 --- a/apimatic_core/utilities/datetime_helper.py +++ b/apimatic_core/utilities/datetime_helper.py @@ -58,12 +58,85 @@ def is_unix_timestamp(timestamp): @staticmethod def to_rfc3339_date_time(value): - return ApiHelper.apply_datetime_converter(value, ApiHelper.RFC3339DateTime) + if value is None: + return None + + date_time: ApiHelper.CustomDate = ApiHelper.apply_datetime_converter(value, ApiHelper.RFC3339DateTime) + return DateTimeHelper._convert(date_time) @staticmethod def to_rfc1123_date_time(value): - return ApiHelper.apply_datetime_converter(value, ApiHelper.HttpDateTime) + if value is None: + return None + + date_time: ApiHelper.CustomDate = ApiHelper.apply_datetime_converter(value, ApiHelper.HttpDateTime) + return DateTimeHelper._convert(date_time) @staticmethod def to_unix_timestamp(value): - return ApiHelper.apply_datetime_converter(value, ApiHelper.UnixDateTime) + if value is None: + return None + + date_time: ApiHelper.CustomDate = ApiHelper.apply_datetime_converter(value, ApiHelper.UnixDateTime) + return DateTimeHelper._convert(date_time) + + @staticmethod + def _convert(date_time): + if date_time is None: + return None + + if isinstance(date_time, list): + return [DateTimeHelper._convert(element) for element in date_time] + + if isinstance(date_time, dict): + return {key: DateTimeHelper._convert(element) for key, element in date_time.items()} + + return date_time.value + + @staticmethod + def try_parse_from_rfc3339_date_time(value): + if value is None: + return None + + if isinstance(value, list): + return [DateTimeHelper.try_parse_from_rfc3339_date_time(element) for element in value] + + if isinstance(value, dict): + return {key: DateTimeHelper.try_parse_from_rfc3339_date_time(element) for key, element in value.items()} + + if isinstance(value, datetime): + return value + + return ApiHelper.RFC3339DateTime.from_value(value).datetime + + @staticmethod + def try_parse_from_rfc1123_date_time(value): + if value is None: + return None + + if isinstance(value, list): + return [DateTimeHelper.try_parse_from_rfc1123_date_time(element) for element in value] + + if isinstance(value, dict): + return {key: DateTimeHelper.try_parse_from_rfc1123_date_time(element) for key, element in value.items()} + + if isinstance(value, datetime): + return value + + return ApiHelper.HttpDateTime.from_value(value).datetime + + @staticmethod + def try_parse_from_unix_timestamp(value): + if value is None: + return None + + if isinstance(value, list): + return [DateTimeHelper.try_parse_from_unix_timestamp(element) for element in value] + + if isinstance(value, dict): + return {key: DateTimeHelper.try_parse_from_unix_timestamp(element) for key, element in value.items()} + + if isinstance(value, datetime): + return value + + return ApiHelper.UnixDateTime.from_value(value).datetime From 417e3f2fc2ccda24b376a25ef944547835f36afd Mon Sep 17 00:00:00 2001 From: "DESKTOP-10GD4VQ\\Sufyan" Date: Wed, 19 Feb 2025 20:28:51 +0500 Subject: [PATCH 3/6] adds typing support in the core library --- apimatic_core/__init__.py | 1 - apimatic_core/api_call.py | 44 +- apimatic_core/authentication/header_auth.py | 26 +- .../authentication/multiple/and_auth_group.py | 13 +- .../authentication/multiple/auth_group.py | 42 +- .../authentication/multiple/or_auth_group.py | 13 +- .../authentication/multiple/single_auth.py | 31 +- apimatic_core/authentication/query_auth.py | 35 +- .../configurations/global_configuration.py | 89 +-- apimatic_core/constants/logger_constants.py | 42 +- apimatic_core/decorators/lazy_property.py | 14 +- .../exceptions/anyof_validation_exception.py | 4 +- .../exceptions/auth_validation_exception.py | 4 +- .../exceptions/oneof_validation_exception.py | 4 +- apimatic_core/factories/__init__.py | 3 - .../factories/http_response_factory.py | 11 - .../http_client_configuration.py | 114 +-- apimatic_core/http/http_callback.py | 10 +- apimatic_core/http/request/http_request.py | 73 +- apimatic_core/http/response/api_response.py | 44 +- apimatic_core/http/response/http_response.py | 19 - .../api_logging_configuration.py | 156 +--- apimatic_core/logger/default_logger.py | 13 +- apimatic_core/logger/sdk_logger.py | 56 +- apimatic_core/py.typed | 0 apimatic_core/request_builder.py | 247 ++++--- apimatic_core/response_handler.py | 114 ++- .../types/array_serialization_format.py | 2 +- apimatic_core/types/datetime_format.py | 2 +- apimatic_core/types/error_case.py | 94 +-- apimatic_core/types/file_wrapper.py | 5 +- apimatic_core/types/parameter.py | 63 +- apimatic_core/types/union_types/any_of.py | 61 +- apimatic_core/types/union_types/leaf_type.py | 108 ++- apimatic_core/types/union_types/one_of.py | 63 +- .../types/union_types/union_type_context.py | 12 +- apimatic_core/types/xml_attributes.py | 33 +- apimatic_core/utilities/api_helper.py | 487 ++++++------ apimatic_core/utilities/auth_helper.py | 38 +- apimatic_core/utilities/comparison_helper.py | 93 +-- apimatic_core/utilities/datetime_helper.py | 177 +++-- apimatic_core/utilities/file_helper.py | 40 +- apimatic_core/utilities/union_type_helper.py | 170 +++-- apimatic_core/utilities/xml_helper.py | 310 ++++---- mypy.ini | 8 + requirements.txt | 7 +- .../api_call_tests/test_api_call.py | 111 ++- .../api_logger_tests/test_api_logger.py | 103 +-- .../test_logging_configuration.py | 129 ++-- tests/apimatic_core/base.py | 472 +++++++----- .../mocks/authentications/basic_auth.py | 41 +- .../mocks/authentications/bearer_auth.py | 34 +- .../custom_header_authentication.py | 30 +- .../custom_query_authentication.py | 43 +- .../mocks/callables/base_uri_callable.py | 11 +- .../mocks/exceptions/api_exception.py | 17 +- .../mocks/exceptions/global_test_exception.py | 42 +- .../mocks/exceptions/local_test_exception.py | 36 +- .../exceptions/nested_model_exception.py | 50 +- tests/apimatic_core/mocks/http/http_client.py | 36 +- .../mocks/http/http_response_catcher.py | 12 +- .../apimatic_core/mocks/logger/api_logger.py | 12 +- .../mocks/models/api_response.py | 25 +- tests/apimatic_core/mocks/models/atom.py | 93 +-- tests/apimatic_core/mocks/models/cat_model.py | 28 +- .../mocks/models/complex_type.py | 138 ++-- tests/apimatic_core/mocks/models/days.py | 24 +- tests/apimatic_core/mocks/models/deer.py | 88 +-- tests/apimatic_core/mocks/models/dog_model.py | 28 +- .../mocks/models/grand_parent_class_model.py | 481 +++++------- .../mocks/models/inner_complex_type.py | 100 +-- tests/apimatic_core/mocks/models/lion.py | 114 +-- .../model_with_additional_properties.py | 537 +++++++------- tests/apimatic_core/mocks/models/months.py | 22 +- .../apimatic_core/mocks/models/one_of_xml.py | 34 +- tests/apimatic_core/mocks/models/orbit.py | 72 +- tests/apimatic_core/mocks/models/person.py | 360 ++++----- tests/apimatic_core/mocks/models/rabbit.py | 105 +-- .../mocks/models/union_type_scalar_model.py | 146 +--- tests/apimatic_core/mocks/models/validate.py | 83 +-- .../apimatic_core/mocks/models/wolf_model.py | 23 +- tests/apimatic_core/mocks/models/xml_model.py | 93 +-- .../apimatic_core/mocks/union_type_lookup.py | 39 +- .../test_request_builder.py | 366 +++++---- .../test_response_handler.py | 87 ++- .../union_type_tests/test_any_of.py | 674 +++++++++-------- .../union_type_tests/test_one_of.py | 697 +++++++++--------- .../utility_tests/test_api_helper.py | 499 +++---------- .../utility_tests/test_auth_helper.py | 9 - .../utility_tests/test_datetime_helper.py | 5 +- .../utility_tests/test_file_helper.py | 3 +- .../utility_tests/test_xml_helper.py | 25 +- 92 files changed, 4316 insertions(+), 4856 deletions(-) delete mode 100644 apimatic_core/factories/__init__.py delete mode 100644 apimatic_core/factories/http_response_factory.py create mode 100644 apimatic_core/py.typed create mode 100644 mypy.ini diff --git a/apimatic_core/__init__.py b/apimatic_core/__init__.py index d233d02..be8ef50 100644 --- a/apimatic_core/__init__.py +++ b/apimatic_core/__init__.py @@ -7,7 +7,6 @@ 'decorators', 'http', 'utilities', - 'factories', 'types', 'logger', 'exceptions', diff --git a/apimatic_core/api_call.py b/apimatic_core/api_call.py index d3b5b6e..99a8faa 100644 --- a/apimatic_core/api_call.py +++ b/apimatic_core/api_call.py @@ -1,40 +1,52 @@ -from apimatic_core.configurations.endpoint_configuration import EndpointConfiguration +from __future__ import annotations + +from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration +from apimatic_core_interfaces.logger.api_logger import ApiLogger +from pydantic import validate_call +from typing import Any + from apimatic_core.configurations.global_configuration import GlobalConfiguration from apimatic_core.logger.sdk_logger import LoggerFactory +from apimatic_core.request_builder import RequestBuilder from apimatic_core.response_handler import ResponseHandler class ApiCall: @property - def new_builder(self): + def new_builder(self) -> 'ApiCall': return ApiCall(self._global_configuration) def __init__( self, - global_configuration=GlobalConfiguration() + global_configuration: GlobalConfiguration=GlobalConfiguration() ): - self._global_configuration = global_configuration - self._request_builder = None - self._response_handler = ResponseHandler() - self._endpoint_configuration = EndpointConfiguration() - self._api_logger = LoggerFactory.get_api_logger(self._global_configuration.get_http_client_configuration() - .logging_configuration) - - def request(self, request_builder): + self._global_configuration: GlobalConfiguration = global_configuration + self._request_builder: RequestBuilder = RequestBuilder() + self._response_handler: ResponseHandler = ResponseHandler() + self._endpoint_configuration: EndpointConfiguration = EndpointConfiguration() + self._api_logger: ApiLogger = LoggerFactory.get_api_logger( + self._global_configuration.http_client_configuration.logging_configuration + ) + + @validate_call(config=dict(arbitrary_types_allowed=True)) + def request(self, request_builder: RequestBuilder) -> 'ApiCall': self._request_builder = request_builder return self - def response(self, response_handler): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def response(self, response_handler: ResponseHandler) -> 'ApiCall': self._response_handler = response_handler return self - def endpoint_configuration(self, endpoint_configuration): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def endpoint_configuration(self, endpoint_configuration: EndpointConfiguration) -> 'ApiCall': self._endpoint_configuration = endpoint_configuration return self - def execute(self): - _http_client_configuration = self._global_configuration.get_http_client_configuration() + @validate_call + def execute(self) -> Any: + _http_client_configuration = self._global_configuration.http_client_configuration if _http_client_configuration.http_client is None: raise ValueError("An HTTP client instance is required to execute an Api call.") @@ -61,4 +73,4 @@ def execute(self): if _http_callback is not None: _http_callback.on_after_response(_http_response) - return self._response_handler.handle(_http_response, self._global_configuration.get_global_errors()) + return self._response_handler.handle(_http_response, self._global_configuration.global_errors) diff --git a/apimatic_core/authentication/header_auth.py b/apimatic_core/authentication/header_auth.py index a9bcd3f..074dd37 100644 --- a/apimatic_core/authentication/header_auth.py +++ b/apimatic_core/authentication/header_auth.py @@ -1,17 +1,33 @@ -from apimatic_core_interfaces.types.authentication import Authentication +from abc import abstractmethod + +from apimatic_core_interfaces.authentication.authentication import Authentication +from typing import Dict, Optional + +from apimatic_core_interfaces.http.http_request import HttpRequest +from pydantic import validate_call from apimatic_core.utilities.auth_helper import AuthHelper class HeaderAuth(Authentication): - def __init__(self, auth_params): + @property + @abstractmethod + def error_message(self) -> str: + ... + + def with_auth_managers(self, auth_managers: Dict[str, 'Authentication']): + pass + + @validate_call + def __init__(self, auth_params: Dict[str, str]): self._auth_params = auth_params - self._error_message = None + self._error_message: Optional[str] = None - def is_valid(self): + def is_valid(self) -> bool: return AuthHelper.is_valid_auth(self._auth_params) - def apply(self, http_request): + @validate_call + def apply(self, http_request: HttpRequest) -> None: AuthHelper.apply(self._auth_params, http_request.add_header) diff --git a/apimatic_core/authentication/multiple/and_auth_group.py b/apimatic_core/authentication/multiple/and_auth_group.py index 5a5a3d2..efb86fd 100644 --- a/apimatic_core/authentication/multiple/and_auth_group.py +++ b/apimatic_core/authentication/multiple/and_auth_group.py @@ -1,17 +1,24 @@ +from apimatic_core_interfaces.authentication.authentication import Authentication +from pydantic import validate_call +from typing import Union + from apimatic_core.authentication.multiple.auth_group import AuthGroup class And(AuthGroup): @property - def error_message(self): + @validate_call + def error_message(self) -> str: return " and ".join(self._error_messages) - def __init__(self, *auth_group): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def __init__(self, *auth_group: Union[str, Authentication]): super(And, self).__init__(auth_group) self._is_valid_group = True - def is_valid(self): + @validate_call + def is_valid(self) -> bool: if not self.mapped_group: return False diff --git a/apimatic_core/authentication/multiple/auth_group.py b/apimatic_core/authentication/multiple/auth_group.py index 315151c..951eb97 100644 --- a/apimatic_core/authentication/multiple/auth_group.py +++ b/apimatic_core/authentication/multiple/auth_group.py @@ -1,47 +1,63 @@ -from apimatic_core_interfaces.types.authentication import Authentication +from abc import abstractmethod + +from apimatic_core_interfaces.authentication.authentication import Authentication +from apimatic_core_interfaces.http.http_request import HttpRequest +from pydantic import validate_call +from typing import Union, List, Dict, Tuple + from apimatic_core.authentication.multiple.single_auth import Single class AuthGroup(Authentication): @property - def auth_participants(self): + @abstractmethod + def error_message(self) -> str: + ... + + @property + def auth_participants(self) -> List[Authentication]: return self._auth_participants @property - def mapped_group(self): + def mapped_group(self) -> List[Authentication]: return self._mapped_group @property - def error_messages(self): + def error_messages(self) -> List[str]: return self._error_messages @property - def is_valid_group(self): + def is_valid_group(self) -> bool: return self._is_valid_group - def __init__(self, auth_group): - self._auth_participants = [] + @validate_call(config=dict(arbitrary_types_allowed=True)) + def __init__(self, auth_group: Tuple[Union[str, Authentication], ...]): + self._auth_participants: List[Authentication] = [] for auth_participant in auth_group: if auth_participant is not None and isinstance(auth_participant, str): self._auth_participants.append(Single(auth_participant)) elif auth_participant is not None: self._auth_participants.append(auth_participant) - self._mapped_group = [] - self._error_messages = [] - self._is_valid_group = None + self._mapped_group: List[Authentication] = [] + self._error_messages: List[str] = [] + self._is_valid_group: bool = False - def with_auth_managers(self, auth_managers): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def with_auth_managers(self, auth_managers: Dict[str, Authentication]) -> 'Authentication': for participant in self.auth_participants: self.mapped_group.append(participant.with_auth_managers(auth_managers)) return self - def is_valid(self): # pragma: no cover + @validate_call + @abstractmethod + def is_valid(self) -> bool: # pragma: no cover ... - def apply(self, http_request): + @validate_call + def apply(self, http_request: HttpRequest): if not self.is_valid_group: return diff --git a/apimatic_core/authentication/multiple/or_auth_group.py b/apimatic_core/authentication/multiple/or_auth_group.py index 2074d0a..73e75df 100644 --- a/apimatic_core/authentication/multiple/or_auth_group.py +++ b/apimatic_core/authentication/multiple/or_auth_group.py @@ -1,17 +1,24 @@ +from apimatic_core_interfaces.authentication.authentication import Authentication +from typing import Union + +from pydantic import validate_call + from apimatic_core.authentication.multiple.auth_group import AuthGroup class Or(AuthGroup): @property - def error_message(self): + def error_message(self) -> str: return " or ".join(self._error_messages) - def __init__(self, *auth_group): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def __init__(self, *auth_group: Union[str, Authentication]): super(Or, self).__init__(auth_group) self._is_valid_group = False - def is_valid(self): + @validate_call + def is_valid(self) -> bool: if not self.mapped_group: return False diff --git a/apimatic_core/authentication/multiple/single_auth.py b/apimatic_core/authentication/multiple/single_auth.py index 877a21f..8b42360 100644 --- a/apimatic_core/authentication/multiple/single_auth.py +++ b/apimatic_core/authentication/multiple/single_auth.py @@ -1,20 +1,25 @@ -from apimatic_core_interfaces.types.authentication import Authentication +from apimatic_core_interfaces.authentication.authentication import Authentication +from apimatic_core_interfaces.http.http_request import HttpRequest +from pydantic import validate_call +from typing import Dict, Optional class Single(Authentication): @property - def error_message(self): + def error_message(self) -> str: return "[{}]".format(self._error_message) - def __init__(self, auth_participant): + @validate_call + def __init__(self, auth_participant: str): super(Single, self).__init__() self._auth_participant = auth_participant - self._mapped_auth = None - self._error_message = None - self._is_valid = False + self._mapped_auth: Optional[Authentication] = None + self._error_message: Optional[str] = None + self._is_valid: bool = False - def with_auth_managers(self, auth_managers): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def with_auth_managers(self, auth_managers: Dict[str, Authentication]) -> Authentication: if not auth_managers.get(self._auth_participant): raise ValueError("Auth key is invalid.") @@ -22,16 +27,18 @@ def with_auth_managers(self, auth_managers): return self - def is_valid(self): - self._is_valid = self._mapped_auth.is_valid() + @validate_call + def is_valid(self) -> bool: + self._is_valid = self._mapped_auth.is_valid() if self._mapped_auth else False if not self._is_valid: - self._error_message = self._mapped_auth.error_message + self._error_message = self._mapped_auth.error_message if self._mapped_auth else "Invalid auth key." return self._is_valid - def apply(self, http_request): + @validate_call + def apply(self, http_request: HttpRequest): - if not self._is_valid: + if self._mapped_auth is None or not self._is_valid: return self._mapped_auth.apply(http_request) diff --git a/apimatic_core/authentication/query_auth.py b/apimatic_core/authentication/query_auth.py index 5503276..7b500ae 100644 --- a/apimatic_core/authentication/query_auth.py +++ b/apimatic_core/authentication/query_auth.py @@ -1,15 +1,40 @@ -from apimatic_core_interfaces.types.authentication import Authentication +from abc import abstractmethod +from apimatic_core_interfaces.authentication.authentication import Authentication +from apimatic_core_interfaces.http.http_request import HttpRequest +from pydantic import validate_call +from typing import Dict + +from apimatic_core.utilities.api_helper import ApiHelper from apimatic_core.utilities.auth_helper import AuthHelper class QueryAuth(Authentication): - def __init__(self, auth_params): + @property + @abstractmethod + def error_message(self) -> str: + ... + + def with_auth_managers(self, auth_managers: Dict[str, 'Authentication']): + pass + + @validate_call + def __init__(self, auth_params: Dict[str, str]): self._auth_params = auth_params - def is_valid(self): + def is_valid(self) -> bool: return AuthHelper.is_valid_auth(self._auth_params) - def apply(self, http_request): - return AuthHelper.apply(self._auth_params, http_request.add_query_parameter) + @validate_call + def apply(self, http_request: HttpRequest): + for key, value in self._auth_params.items(): + self._add_query_parameter(key, value, http_request) + + @validate_call + def _add_query_parameter(self, key: str, value: str, http_request: HttpRequest): + query_url = ApiHelper.append_url_with_query_parameters( + http_request.query_url, + {key: value} + ) + http_request.query_url = ApiHelper.clean_url(query_url) diff --git a/apimatic_core/configurations/global_configuration.py b/apimatic_core/configurations/global_configuration.py index 2befe69..5deb90a 100644 --- a/apimatic_core/configurations/global_configuration.py +++ b/apimatic_core/configurations/global_configuration.py @@ -1,72 +1,43 @@ -from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration -from apimatic_core.utilities.api_helper import ApiHelper - - -class GlobalConfiguration: - - def get_http_client_configuration(self): - return self._http_client_configuration - - def get_global_errors(self): - return self._global_errors +from typing import Dict, Optional, Callable, Any - def get_global_headers(self): - return self._global_headers +from apimatic_core_interfaces.authentication.authentication import Authentication +from pydantic import validate_call, BaseModel, Field - def get_additional_headers(self): - return self._additional_headers - - def get_auth_managers(self): - return self._auth_managers - - def get_base_uri(self, server): - return self._base_uri_executor(server) - - def __init__( - self, http_client_configuration=HttpClientConfiguration() - ): - self._http_client_configuration = http_client_configuration - self._global_errors = {} - self._global_headers = {} - self._additional_headers = {} - self._auth_managers = {} - self._base_uri_executor = None - - def global_errors(self, global_errors): - self._global_errors = global_errors - return self +from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration +from apimatic_core.types.error_case import ErrorCase +from apimatic_core.utilities.api_helper import ApiHelper - def global_headers(self, global_headers): - self._global_headers = global_headers - return self - def global_header(self, key, value): - self._global_headers[key] = value - return self +class GlobalConfiguration(BaseModel): + class Config: + arbitrary_types_allowed = True + http_client_configuration: HttpClientConfiguration = Field(default_factory=HttpClientConfiguration) + global_errors: Dict[str, ErrorCase] = Field(default_factory=dict) + global_headers: Dict[str, Any] = Field(default_factory=dict) + additional_headers: Dict[str, Any] = Field(default_factory=dict) + auth_managers: Dict[str, Authentication] = Field(default_factory=dict) + base_uri_executor: Optional[Callable[[Any], Optional[str]]] = None - def additional_headers(self, additional_headers): - self._additional_headers = additional_headers - return self + @validate_call + def get_base_uri(self, server: Any = None) -> Optional[str]: + if self.base_uri_executor is None: + return None - def additional_header(self, key, value): - self._additional_headers[key] = value - return self + return self.base_uri_executor(server) - def auth_managers(self, auth_managers): - self._auth_managers = auth_managers - return self + def set_global_error(self, key: str, error_case: ErrorCase): + self.global_errors[key] = error_case - def user_agent(self, user_agent, user_agent_parameters={}): - self.add_useragent_in_global_headers(user_agent, user_agent_parameters) - return self + def set_global_header(self, key: str, value: str): + self.global_headers[key] = value - def base_uri_executor(self, base_uri_executor): - self._base_uri_executor = base_uri_executor - return self + def set_additional_header(self, key: str, value: str): + self.additional_headers[key] = value - def add_useragent_in_global_headers(self, user_agent, user_agent_parameters): + def add_user_agent(self, user_agent: str, user_agent_parameters: Optional[Dict[str, Dict[str, Any]]] = None): if user_agent_parameters: user_agent = ApiHelper.append_url_with_template_parameters( - user_agent, user_agent_parameters).replace(' ', ' ') + user_agent, user_agent_parameters + ).replace(' ', ' ') if user_agent: - self._global_headers['user-agent'] = user_agent + self.global_headers['user-agent'] = user_agent diff --git a/apimatic_core/constants/logger_constants.py b/apimatic_core/constants/logger_constants.py index c5c788f..59e6ec9 100644 --- a/apimatic_core/constants/logger_constants.py +++ b/apimatic_core/constants/logger_constants.py @@ -1,44 +1,46 @@ +from typing import Tuple + class LoggerConstants: - METHOD = "method" + METHOD: str = 'method' """Key representing the method of the HTTP request.""" - URL = "url" + URL: str = 'url' """Key representing the URL of the HTTP request.""" - QUERY_PARAMETER = "query_parameter" + QUERY_PARAMETER: str = 'query_parameter' """Key representing the query parameters of the HTTP request.""" - HEADERS = "headers" + HEADERS: str = 'headers' """Key representing the headers of the HTTP request or response.""" - BODY = "body" + BODY: str = 'body' """Key representing the body of the HTTP request or response.""" - STATUS_CODE = "status_code" + STATUS_CODE: str = 'status_code' """Key representing the status code of the HTTP response.""" - CONTENT_LENGTH = "content_length" + CONTENT_LENGTH: str = 'content_length' """Key representing the content length of the HTTP response.""" - CONTENT_TYPE = "content_type" + CONTENT_TYPE: str = 'content_type' """Key representing the content type of the HTTP response.""" - CONTENT_LENGTH_HEADER = "content-length" + CONTENT_LENGTH_HEADER: str = 'content-length' """Key representing the content length header.""" - CONTENT_TYPE_HEADER = "content-type" + CONTENT_TYPE_HEADER: str = 'content-type' """Key representing the content type header.""" - NON_SENSITIVE_HEADERS = tuple(map(lambda x: x.lower(), [ - "Accept", "Accept-Charset", "Accept-Encoding", "Accept-Language", - "Access-Control-Allow-Origin", "Cache-Control", "Connection", - "Content-Encoding", "Content-Language", "Content-Length", "Content-Location", - "Content-MD5", "Content-Range", "Content-Type", "Date", "ETag", "Expect", - "Expires", "From", "Host", "If-Match", "If-Modified-Since", "If-None-Match", - "If-Range", "If-Unmodified-Since", "Keep-Alive", "Last-Modified", "Location", - "Max-Forwards", "Pragma", "Range", "Referer", "Retry-After", "Server", - "Trailer", "Transfer-Encoding", "Upgrade", "User-Agent", "Vary", "Via", - "Warning", "X-Forwarded-For", "X-Requested-With", "X-Powered-By" + NON_SENSITIVE_HEADERS: Tuple[str, ...] = tuple(map(lambda x: x.lower(), [ + 'Accept', 'Accept-Charset', 'Accept-Encoding', 'Accept-Language', + 'Access-Control-Allow-Origin', 'Cache-Control', 'Connection', + 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-Location', + 'Content-MD5', 'Content-Range', 'Content-Type', 'Date', 'ETag', 'Expect', + 'Expires', 'From', 'Host', 'If-Match', 'If-Modified-Since', 'If-None-Match', + 'If-Range', 'If-Unmodified-Since', 'Keep-Alive', 'Last-Modified', 'Location', + 'Max-Forwards', 'Pragma', 'Range', 'Referer', 'Retry-After', 'Server', + 'Trailer', 'Transfer-Encoding', 'Upgrade', 'User-Agent', 'Vary', 'Via', + 'Warning', 'X-Forwarded-For', 'X-Requested-With', 'X-Powered-By' ])) """List of sensitive headers that need to be filtered.""" diff --git a/apimatic_core/decorators/lazy_property.py b/apimatic_core/decorators/lazy_property.py index c0dd08a..23a2a69 100644 --- a/apimatic_core/decorators/lazy_property.py +++ b/apimatic_core/decorators/lazy_property.py @@ -1,14 +1,20 @@ +from typing import Callable, Optional, Type +from pydantic import Field, validate_call -class LazyProperty(object): +class LazyProperty: """A decorator class for lazy instantiation.""" - def __init__(self, f_get): + f_get: Callable[[object], object] = Field(...) + func_name: str = Field(...) + + @validate_call + def __init__(self, f_get: Callable[[object], object]): self.f_get = f_get self.func_name = f_get.__name__ - def __get__(self, obj, cls): + def __get__(self, obj: Optional[object], cls: Optional[Type[object]]) -> Optional[object]: if obj is None: return None value = self.f_get(obj) setattr(obj, self.func_name, value) - return value + return value \ No newline at end of file diff --git a/apimatic_core/exceptions/anyof_validation_exception.py b/apimatic_core/exceptions/anyof_validation_exception.py index ff37b3b..db7256f 100644 --- a/apimatic_core/exceptions/anyof_validation_exception.py +++ b/apimatic_core/exceptions/anyof_validation_exception.py @@ -1,10 +1,12 @@ """ This is an exception class which will be raised when union type validation fails. """ +from pydantic import validate_call class AnyOfValidationException(Exception): - def __init__(self, message): + @validate_call + def __init__(self, message: str): self.message = message super().__init__(self.message) diff --git a/apimatic_core/exceptions/auth_validation_exception.py b/apimatic_core/exceptions/auth_validation_exception.py index eb74a9c..42efbd4 100644 --- a/apimatic_core/exceptions/auth_validation_exception.py +++ b/apimatic_core/exceptions/auth_validation_exception.py @@ -1,10 +1,12 @@ """ This is an exception class which will be raised when auth validation fails. """ +from pydantic import validate_call class AuthValidationException(Exception): - def __init__(self, message): + @validate_call + def __init__(self, message: str): self.message = message super().__init__(self.message) diff --git a/apimatic_core/exceptions/oneof_validation_exception.py b/apimatic_core/exceptions/oneof_validation_exception.py index 9d6bb3a..f8aeb27 100644 --- a/apimatic_core/exceptions/oneof_validation_exception.py +++ b/apimatic_core/exceptions/oneof_validation_exception.py @@ -1,10 +1,12 @@ """ This is an exception class which will be raised when union type validation fails. """ +from pydantic import validate_call class OneOfValidationException(Exception): - def __init__(self, message): + @validate_call + def __init__(self, message: str): self.message = message super().__init__(self.message) diff --git a/apimatic_core/factories/__init__.py b/apimatic_core/factories/__init__.py deleted file mode 100644 index d40e52b..0000000 --- a/apimatic_core/factories/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = [ - 'http_response_factory' -] \ No newline at end of file diff --git a/apimatic_core/factories/http_response_factory.py b/apimatic_core/factories/http_response_factory.py deleted file mode 100644 index aadbcef..0000000 --- a/apimatic_core/factories/http_response_factory.py +++ /dev/null @@ -1,11 +0,0 @@ -from apimatic_core_interfaces.factories.response_factory import ResponseFactory -from apimatic_core.http.response.http_response import HttpResponse - - -class HttpResponseFactory(ResponseFactory): - - def __init__(self): - pass - - def create(self, status_code, reason, headers, body, request): - return HttpResponse(status_code, reason, headers, body, request) diff --git a/apimatic_core/http/configurations/http_client_configuration.py b/apimatic_core/http/configurations/http_client_configuration.py index 41b7f35..1fb5bc0 100644 --- a/apimatic_core/http/configurations/http_client_configuration.py +++ b/apimatic_core/http/configurations/http_client_configuration.py @@ -1,97 +1,27 @@ -# -*- coding: utf-8 -*- -from apimatic_core.factories.http_response_factory import HttpResponseFactory +from apimatic_core_interfaces.http.http_client import HttpClient +from pydantic import BaseModel, Field +from requests import Session +from typing import List, Optional +from apimatic_core.http.http_callback import HttpCallBack +from apimatic_core.logger.configuration.api_logging_configuration import ApiLoggingConfiguration -class HttpClientConfiguration(object): # pragma: no cover + +class HttpClientConfiguration(BaseModel): # pragma: no cover """A class used for configuring the SDK by a user. """ - @property - def http_response_factory(self): - return self._http_response_factory - - @property - def http_client(self): - return self._http_client - - @property - def http_callback(self): - return self._http_call_back - - @property - def http_client_instance(self): - return self._http_client_instance - - @property - def override_http_client_configuration(self): - return self._override_http_client_configuration - - @property - def timeout(self): - return self._timeout - - @property - def max_retries(self): - return self._max_retries - - @property - def backoff_factor(self): - return self._backoff_factor - - @property - def retry_statuses(self): - return self._retry_statuses - - @property - def retry_methods(self): - return self._retry_methods - - @property - def logging_configuration(self): - return self._logging_configuration - - def __init__(self, http_client_instance=None, - override_http_client_configuration=False, http_call_back=None, - timeout=60, max_retries=0, backoff_factor=2, - retry_statuses=None, retry_methods=None, logging_configuration=None): - if retry_statuses is None: - retry_statuses = [408, 413, 429, 500, 502, 503, 504, 521, 522, 524] - - if retry_methods is None: - retry_methods = ['GET', 'PUT'] - - self._http_response_factory = HttpResponseFactory() - - # The Http Client passed from the sdk user for making requests - self._http_client_instance = http_client_instance - - # The value which determines to override properties of the passed Http Client from the sdk user - self._override_http_client_configuration = override_http_client_configuration - - # The callback value that is invoked before and after an HTTP call is made to an endpoint - self._http_call_back = http_call_back - - # The value to use for connection timeout - self._timeout = timeout - - # The number of times to retry an endpoint call if it fails - self._max_retries = max_retries - - # A backoff factor to apply between attempts after the second try. - # urllib3 will sleep for: - # `{backoff factor} * (2 ** ({number of total retries} - 1))` - self._backoff_factor = backoff_factor - - # The http statuses on which retry is to be done - self._retry_statuses = retry_statuses - - # The http methods on which retry is to be done - self._retry_methods = retry_methods - - # The Http Client to use for making requests. - self._http_client = None - - self._logging_configuration = logging_configuration - - def set_http_client(self, http_client): - self._http_client = http_client + class Config: + arbitrary_types_allowed = True + + http_client: Optional[HttpClient] = None + http_client_instance: Optional[Session] = None + override_http_client_configuration: bool = False + http_callback: Optional[HttpCallBack] = None + timeout: int = 60 + max_retries: int = 0 + backoff_factor: float = 2 + retry_statuses: Optional[List[int]] = Field(default_factory=lambda: [ + 408, 413, 429, 500, 502, 503, 504, 521, 522, 524]) + retry_methods: Optional[List[str]] = Field(default_factory=lambda: ['GET', 'PUT']) + logging_configuration: Optional[ApiLoggingConfiguration] = None \ No newline at end of file diff --git a/apimatic_core/http/http_callback.py b/apimatic_core/http/http_callback.py index 368e599..33f943d 100644 --- a/apimatic_core/http/http_callback.py +++ b/apimatic_core/http/http_callback.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call + class HttpCallBack(object): """An interface for the callback to be called before and after the @@ -9,7 +13,8 @@ class HttpCallBack(object): """ - def on_before_request(self, request): # pragma: no cover + @validate_call + def on_before_request(self, request: HttpRequest): # pragma: no cover """The controller will call this method before making the HttpRequest. Args: @@ -18,7 +23,8 @@ def on_before_request(self, request): # pragma: no cover """ raise NotImplementedError("This method has not been implemented.") - def on_after_response(self, http_response): # pragma: no cover + @validate_call + def on_after_response(self, http_response: HttpResponse): # pragma: no cover """The controller will call this method after making the HttpRequest. Args: diff --git a/apimatic_core/http/request/http_request.py b/apimatic_core/http/request/http_request.py index bcc4f31..c758a15 100644 --- a/apimatic_core/http/request/http_request.py +++ b/apimatic_core/http/request/http_request.py @@ -1,10 +1,6 @@ -# -*- coding: utf-8 -*- - - -from apimatic_core.utilities.api_helper import ApiHelper -from apimatic_core_interfaces.types.http_method_enum import HttpMethodEnum -from pydantic import BaseModel -from typing import Optional, Dict, List, Any, Tuple +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from pydantic import BaseModel, validate_call +from typing import Optional, Dict, List, Any, Tuple, Union class HttpRequest(BaseModel): @@ -14,45 +10,25 @@ class HttpRequest(BaseModel): Attributes: http_method (HttpMethodEnum): The HTTP Method that this request should perform when called. + query_url (string): The URL that the request should be sent to. headers (dict): A dictionary of headers (key : value) that should be sent along with the request. - query_url (string): The URL that the request should be sent to. + query_parameters (dict): A dictionary of request query parameters. parameters (dict): A dictionary of parameters that are to be sent along - with the request in the form body of the request + with the request in the body of the request + files (dict): A dictionary of files that are to be sent in the multipart requests. """ - http_method: str + http_method: HttpMethodEnum query_url: str headers: Optional[Dict[str, str]] = None query_parameters: Optional[Dict[str, Any]] = None - parameters: Optional[List[Tuple[str, Any]]] = None + parameters: Any = None files: Optional[Dict[str, Any]] = None - def __init__(self, - http_method: str, - query_url: str, - headers: Optional[Dict[str, str]]=None, - query_parameters: Optional[Dict[str, Any]]=None, - parameters: Optional[List[Tuple[str, Any]]]=None, - files: Optional[Dict[str, Any]]=None) -> None: - """Constructor for the HttpRequest class - - Args: - http_method (HttpMethodEnum): The HTTP Method. - query_url (string): The URL to send the request to. - headers (dict, optional): The headers for the HTTP Request. - query_parameters (dict, optional): Query parameters to add in the - URL. - parameters (dict, optional): Form or body parameters to be included - in the body. - files (dict, optional): Files to be sent with the request. - - """ - super().__init__(http_method=http_method, query_url=query_url, headers=headers, - query_parameters=query_parameters, parameters=parameters, files=files) - - def add_header(self, name: str, value: str) -> None: + @validate_call + def add_header(self, name: str, value: str): """ Add a header to the HttpRequest. Args: @@ -60,31 +36,10 @@ def add_header(self, name: str, value: str) -> None: value (string): The value of the header. """ - self.headers[name] = value - - def add_parameter(self, name: str, value: str) -> None: # pragma: no cover - """ Add a parameter to the HttpRequest. - - Args: - name (string): The name of the parameter. - value (string): The value of the parameter. - - """ - self.parameters[name] = value + if self.headers is None: + self.headers = {} - def add_query_parameter(self, name: str, value: str) -> None: - """ Add a query parameter to the HttpRequest. - - Args: - name (string): The name of the query parameter. - value (string): The value of the query parameter. - - """ - self.query_url = ApiHelper.append_url_with_query_parameters( - self.query_url, - {name: value} - ) - self.query_url = ApiHelper.clean_url(self.query_url) + self.headers[name] = value def __repr__(self) -> str: return str(self.__dict__) \ No newline at end of file diff --git a/apimatic_core/http/response/api_response.py b/apimatic_core/http/response/api_response.py index bef86b3..19ce3d7 100644 --- a/apimatic_core/http/response/api_response.py +++ b/apimatic_core/http/response/api_response.py @@ -1,5 +1,11 @@ +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from typing import Optional, List, Any, Dict -class ApiResponse: +from pydantic import BaseModel + + +class ApiResponse(BaseModel): """Http response received. @@ -15,39 +21,45 @@ class ApiResponse: errors (Array): Any errors returned by the server """ + status_code: int + reason_phrase: Optional[str] + headers: Dict[str, str] + text: Any + request: HttpRequest + body: Any + errors: Optional[List[str]] - def __init__(self, http_response, - body=None, - errors=None): + def __init__(self, http_response: HttpResponse, + body: Any=None, + errors: Optional[List[str]]=None): """The Constructor Args: http_response (HttpResponse): The original, raw response from the api - data (Object): The data field specified for the response + body (Any): The response body errors (Array): Any errors returned by the server """ + super().__init__(status_code=http_response.status_code, + reason_phrase=http_response.reason_phrase, + headers=http_response.headers, + text=http_response.text, + request=http_response.request, + body=body, + errors=errors) - self.status_code = http_response.status_code - self.reason_phrase = http_response.reason_phrase - self.headers = http_response.headers - self.text = http_response.text - self.request = http_response.request - self.body = body - self.errors = errors - - def is_success(self): + def is_success(self) -> bool: """ Returns true if status code is between 200-300 """ return 200 <= self.status_code < 300 - def is_error(self): + def is_error(self) -> bool: """ Returns true if status code is between 400-600 """ return 400 <= self.status_code < 600 - def __repr__(self): + def __repr__(self) -> str: return ''.format(self.text) diff --git a/apimatic_core/http/response/http_response.py b/apimatic_core/http/response/http_response.py index 4a20842..504da45 100644 --- a/apimatic_core/http/response/http_response.py +++ b/apimatic_core/http/response/http_response.py @@ -24,22 +24,3 @@ class HttpResponse(BaseModel): headers: Dict[str, str] text: str request: HttpRequest - - def __init__(self, - status_code, - reason_phrase, - headers, - text, - request): - """Constructor for the HttpResponse class - - Args: - status_code (int): The response status code. - reason_phrase (string): The response reason phrase. - headers (dict): The response headers. - text (string): The raw body from the server. - request (HttpRequest): The request that resulted in this response. - - """ - super().__init__(status_code=status_code, reason_phrase=reason_phrase, - headers=headers, text=text, request=request) diff --git a/apimatic_core/logger/configuration/api_logging_configuration.py b/apimatic_core/logger/configuration/api_logging_configuration.py index af6d4ae..c193ac6 100644 --- a/apimatic_core/logger/configuration/api_logging_configuration.py +++ b/apimatic_core/logger/configuration/api_logging_configuration.py @@ -1,94 +1,26 @@ import logging from apimatic_core_interfaces.logger.logger import Logger +from pydantic import validate_call, BaseModel, field_validator, Field +from typing import List, Dict, Any from apimatic_core.constants.logger_constants import LoggerConstants from apimatic_core.logger.default_logger import ConsoleLogger from apimatic_core.utilities.api_helper import ApiHelper +class BaseHttpLoggingConfiguration(BaseModel): + log_body: bool + log_headers: bool + headers_to_include: List[str] = Field(default_factory=list) + headers_to_exclude: List[str] = Field(default_factory=list) + headers_to_unmask: List[str] = Field(default_factory=list) -class ApiLoggingConfiguration: + @field_validator("headers_to_include", "headers_to_exclude", "headers_to_unmask", mode="before") + def to_lower_case_list(cls, value): + return ApiHelper.to_lower_case(value) if value else [] - @property - def logger(self): - return self._logger - - @property - def log_level(self): - return self._log_level - - @property - def mask_sensitive_headers(self): - return self._mask_sensitive_headers - - @property - def request_logging_config(self): - return self._request_logging_config - - @property - def response_logging_config(self): - return self._response_logging_config - - def __init__(self, logger, log_level, mask_sensitive_headers, - request_logging_config, response_logging_config): - """Default constructor. - - Args: - logger (Logger): The logger implementation to log with. - log_level (LogLevel): The log level to apply to the log message. - mask_sensitive_headers (bool): Flag to control masking of sensitive headers. - request_logging_config (ApiRequestLoggingConfiguration): The API request logging configuration. - response_logging_config (ApiResponseLoggingConfiguration): The API response logging configuration. - """ - - self._logger = ConsoleLogger() if logger is None else logger - self._log_level = logging.INFO if log_level is None else log_level - self._mask_sensitive_headers = mask_sensitive_headers - self._request_logging_config = request_logging_config - self._response_logging_config = response_logging_config - - -class BaseHttpLoggingConfiguration: - - @property - def log_body(self): - return self._log_body - - @property - def log_headers(self): - return self._log_headers - - @property - def headers_to_include(self): - return self._headers_to_include - - @property - def headers_to_exclude(self): - return self._headers_to_exclude - - @property - def headers_to_unmask(self): - return self._headers_to_unmask - - def __init__(self, log_body, log_headers, headers_to_include, - headers_to_exclude, headers_to_unmask): - """Default constructor. - - Args: - log_body (bool): Controls the logging of the request body. - log_headers (bool): Controls the logging of request headers. - headers_to_include (List[str]): Includes only specified headers in the log output. - headers_to_exclude (List[str]): Excludes specified headers from the log output. - headers_to_unmask (List[str]): Logs specified headers without masking, revealing their actual values. - """ - - self._log_body = log_body - self._log_headers = log_headers - self._headers_to_include = [] if headers_to_include is None else ApiHelper.to_lower_case(headers_to_include) - self._headers_to_exclude = [] if headers_to_exclude is None else ApiHelper.to_lower_case(headers_to_exclude) - self._headers_to_unmask = [] if headers_to_unmask is None else ApiHelper.to_lower_case(headers_to_unmask) - - def get_loggable_headers(self, headers, mask_sensitive_headers): + @validate_call + def get_loggable_headers(self, headers: Dict[str, Any], mask_sensitive_headers: bool) -> Dict[str, Any]: """ Retrieves the headers to be logged based on the provided logging configuration, headers, and sensitivity masking configuration. @@ -106,7 +38,8 @@ def get_loggable_headers(self, headers, mask_sensitive_headers): return self._mask_sensitive_headers(extracted_headers, mask_sensitive_headers) - def _extract_headers_to_log(self, headers): + @validate_call + def _extract_headers_to_log(self, headers: Dict[str, Any]) -> Dict[str, Any]: """ Extracts headers to log based on inclusion and exclusion criteria. @@ -123,7 +56,8 @@ def _extract_headers_to_log(self, headers): return headers - def _mask_sensitive_headers(self, headers, mask_sensitive_headers): + @validate_call + def _mask_sensitive_headers(self, headers: Dict[str, Any], mask_sensitive_headers: bool) -> Dict[str, Any]: """ Masks sensitive headers from the given list of request headers. @@ -146,7 +80,8 @@ def _mask_sensitive_headers(self, headers, mask_sensitive_headers): return filtered_headers - def _filter_included_headers(self, headers): + @validate_call + def _filter_included_headers(self, headers: Dict[str, Any]) -> Dict[str, Any]: """ Filters headers to log based on inclusion criteria. @@ -162,7 +97,7 @@ def _filter_included_headers(self, headers): extracted_headers[key] = value return extracted_headers - def _filter_excluded_headers(self, headers): + def _filter_excluded_headers(self, headers: Dict[str, Any]) -> Dict[str, Any]: """ Filters headers to log based on exclusion criteria. @@ -178,31 +113,11 @@ def _filter_excluded_headers(self, headers): extracted_headers[key] = value return extracted_headers - class ApiRequestLoggingConfiguration(BaseHttpLoggingConfiguration): + include_query_in_path: bool - @property - def include_query_in_path(self): - return self._include_query_in_path - - def __init__(self, log_body, log_headers, headers_to_include, - headers_to_exclude, headers_to_unmask, - include_query_in_path): - """Default constructor. - - Args: - log_body (bool): Controls the logging of the request body. - log_headers (bool): Controls the logging of request headers. - headers_to_include (List[str]): Includes only specified headers in the log output. - headers_to_exclude (List[str]): Excludes specified headers from the log output. - headers_to_unmask (List[str]): Logs specified headers without masking, revealing their actual values. - include_query_in_path (bool): Determines whether to include query parameters in the logged request path. - """ - super(ApiRequestLoggingConfiguration, self).__init__(log_body, log_headers, headers_to_include, - headers_to_exclude, headers_to_unmask) - self._include_query_in_path = include_query_in_path - - def get_loggable_url(self, query_url): + @validate_call + def get_loggable_url(self, query_url: str) -> str: """ Retrieves a URL suitable for logging purposes. @@ -223,18 +138,13 @@ def get_loggable_url(self, query_url): class ApiResponseLoggingConfiguration(BaseHttpLoggingConfiguration): - - def __init__(self, log_body, log_headers, headers_to_include, - headers_to_exclude, headers_to_unmask): - """Default constructor. - - Args: - log_body (bool): Controls the logging of the request body. - log_headers (bool): Controls the logging of request headers. - headers_to_include (List[str]): Includes only specified headers in the log output. - headers_to_exclude (List[str]): Excludes specified headers from the log output. - headers_to_unmask (List[str]): Logs specified headers without masking, revealing their actual values. - """ - - super(ApiResponseLoggingConfiguration, self).__init__(log_body, log_headers, headers_to_include, - headers_to_exclude, headers_to_unmask) + pass + +class ApiLoggingConfiguration(BaseModel): + class Config: + arbitrary_types_allowed = True + logger: Logger = ConsoleLogger() + log_level: int = logging.INFO + mask_sensitive_headers: bool + request_logging_config: ApiRequestLoggingConfiguration + response_logging_config: ApiResponseLoggingConfiguration diff --git a/apimatic_core/logger/default_logger.py b/apimatic_core/logger/default_logger.py index c6f55af..c9c44fa 100644 --- a/apimatic_core/logger/default_logger.py +++ b/apimatic_core/logger/default_logger.py @@ -1,19 +1,24 @@ import sys +from logging import StreamHandler, Formatter from apimatic_core_interfaces.logger.logger import Logger import logging +from pydantic import validate_call +from typing import Dict, Any + class ConsoleLogger(Logger): def __init__(self): - self._logger = logging.Logger(name='') - stdout = logging.StreamHandler(stream=sys.stdout) - fmt = logging.Formatter("%(levelname)s %(message)s") + self._logger: logging.Logger = logging.Logger(name='') + stdout: StreamHandler = logging.StreamHandler(stream=sys.stdout) + fmt: Formatter = logging.Formatter("%(levelname)s %(message)s") stdout.setFormatter(fmt) self._logger.addHandler(stdout) - def log(self, level, message, params): + @validate_call + def log(self, level: int, message: str, params: Dict[str, Any]): """Logs a message with a specified log level and additional parameters. Args: diff --git a/apimatic_core/logger/sdk_logger.py b/apimatic_core/logger/sdk_logger.py index cea0bd4..630175c 100644 --- a/apimatic_core/logger/sdk_logger.py +++ b/apimatic_core/logger/sdk_logger.py @@ -1,22 +1,32 @@ +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse from apimatic_core_interfaces.logger.api_logger import ApiLogger +from apimatic_core_interfaces.logger.logger import Logger +from pydantic import validate_call +from typing import Dict, Optional + from apimatic_core.constants.logger_constants import LoggerConstants +from apimatic_core.logger.configuration.api_logging_configuration import ApiLoggingConfiguration, \ + ApiResponseLoggingConfiguration, ApiRequestLoggingConfiguration, BaseHttpLoggingConfiguration + class SdkLogger(ApiLogger): - def __init__(self, api_logging_configuration): + @validate_call + def __init__(self, api_logging_configuration: ApiLoggingConfiguration): """Default Constructor. Args: api_logging_configuration (ApiLoggingConfiguration): The Api logging configuration. """ - self._api_logging_config = api_logging_configuration - self._logger = self._api_logging_config.logger - self._level = self._api_logging_config.log_level - self._mask_sensitive_headers = self._api_logging_config.mask_sensitive_headers - self._request_logging_config = self._api_logging_config.request_logging_config - self._response_logging_config = self._api_logging_config.response_logging_config - - def log_request(self, http_request): + self._api_logging_config: ApiLoggingConfiguration = api_logging_configuration + self._logger: Logger = self._api_logging_config.logger + self._level: int = self._api_logging_config.log_level + self._mask_sensitive_headers: bool = self._api_logging_config.mask_sensitive_headers + self._request_logging_config: ApiRequestLoggingConfiguration = self._api_logging_config.request_logging_config + self._response_logging_config: ApiResponseLoggingConfiguration = self._api_logging_config.response_logging_config + + def log_request(self, http_request: HttpRequest): """Logs the given HTTP request. Args: @@ -25,13 +35,13 @@ def log_request(self, http_request): _lowered_case_headers = {key.lower(): value for key, value in http_request.headers.items()} _content_type = _lowered_case_headers.get(LoggerConstants.CONTENT_TYPE_HEADER) _url = self._request_logging_config.get_loggable_url(http_request.query_url) - params = { - LoggerConstants.METHOD: http_request.http_method, + request_metadata = { + LoggerConstants.METHOD: http_request.http_method.value, LoggerConstants.URL: _url, LoggerConstants.CONTENT_TYPE: _content_type, } - self._logger.log(self._level, "Request %s %s %s", params) + self._logger.log(self._level, "Request %s %s %s", request_metadata) if self._request_logging_config.log_headers: self._logger.log(self._level, "Request Headers %s", @@ -39,12 +49,12 @@ def log_request(self, http_request): if self._request_logging_config.log_body: body = http_request.parameters if http_request.parameters is not None else http_request.files - params = { + request_payload = { LoggerConstants.BODY: body } - self._logger.log(self._level, "Request Body %s", params) + self._logger.log(self._level, "Request Body %s", request_payload) - def log_response(self, http_response): + def log_response(self, http_response: HttpResponse): """Logs the given HTTP response. Args: @@ -53,24 +63,24 @@ def log_response(self, http_response): _lowered_case_headers = {key.lower(): value for key, value in http_response.headers.items()} _content_type = _lowered_case_headers.get(LoggerConstants.CONTENT_TYPE_HEADER) _content_length = _lowered_case_headers.get(LoggerConstants.CONTENT_LENGTH_HEADER) - params = { + response_metadata = { LoggerConstants.STATUS_CODE: http_response.status_code, LoggerConstants.CONTENT_TYPE: _content_type, LoggerConstants.CONTENT_LENGTH: _content_length, } - self._logger.log(self._level, "Response %s %s %s", params) + self._logger.log(self._level, "Response %s %s %s", response_metadata) if self._response_logging_config.log_headers: self._logger.log(self._level, "Response Headers %s", self._get_loggable_headers(self._response_logging_config, http_response.headers)) if self._response_logging_config.log_body: - params = { + response_payload = { LoggerConstants.BODY: http_response.text } - self._logger.log(self._level, "Response Body %s", params) + self._logger.log(self._level, "Response Body %s", response_payload) - def _get_loggable_headers(self, logging_config, headers): + def _get_loggable_headers(self, logging_config: BaseHttpLoggingConfiguration, headers: Dict[str, str]): return { LoggerConstants.HEADERS: logging_config.get_loggable_headers(headers, self._mask_sensitive_headers) } @@ -78,7 +88,7 @@ def _get_loggable_headers(self, logging_config, headers): class NoneSdkLogger(ApiLogger): - def log_request(self, http_request): + def log_request(self, http_request: HttpRequest): """Logs the given HTTP request. Args: @@ -86,7 +96,7 @@ def log_request(self, http_request): """ pass - def log_response(self, http_response): + def log_response(self, http_response: HttpResponse): """Logs the given HTTP response. Args: @@ -98,7 +108,7 @@ def log_response(self, http_response): class LoggerFactory: @classmethod - def get_api_logger(cls, api_logging_configuration): + def get_api_logger(cls, api_logging_configuration: Optional[ApiLoggingConfiguration]) -> ApiLogger: """Default Constructor. Args: diff --git a/apimatic_core/py.typed b/apimatic_core/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/apimatic_core/request_builder.py b/apimatic_core/request_builder.py index 64b6687..5eec817 100644 --- a/apimatic_core/request_builder.py +++ b/apimatic_core/request_builder.py @@ -1,153 +1,173 @@ +from __future__ import annotations +from typing import Optional, Dict, Any, List, Union, Tuple + +from apimatic_core_interfaces.authentication.authentication import Authentication +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from apimatic_core_interfaces.http.http_request import HttpRequest +from pydantic import validate_call + +from apimatic_core.configurations.global_configuration import GlobalConfiguration from apimatic_core.exceptions.auth_validation_exception import AuthValidationException -from apimatic_core.http.request.http_request import HttpRequest from apimatic_core.types.array_serialization_format import SerializationFormats +from apimatic_core.types.file_wrapper import FileType +from apimatic_core.types.parameter import Parameter +from apimatic_core.types.xml_attributes import XmlAttributes from apimatic_core.utilities.api_helper import ApiHelper class RequestBuilder: - @staticmethod - def get_param_name(param_value): - if isinstance(param_value, str): - return None - return param_value.name - - def __init__( - self - ): - - self._server = None - self._path = None - self._http_method = None - self._template_params = {} - self._header_params = {} - self._query_params = {} - self._form_params = {} - self._additional_form_params = {} - self._additional_query_params = {} - self._multipart_params = [] - self._body_param = None - self._body_serializer = None - self._auth = None - self._array_serialization_format = SerializationFormats.INDEXED - self._xml_attributes = None - - def server(self, server): + def __init__(self): + + self._server: Any = None + self._path: Optional[str] = None + self._http_method: Optional[HttpMethodEnum] = None + self._template_params: Dict[str, Dict[str, Any]] = {} + self._header_params: Dict[str, Any] = {} + self._query_params: Dict[str, Any] = {} + self._form_params: Dict[str, Any] = {} + self._additional_form_params: Dict[str, Any] = {} + self._additional_query_params: Dict[str, Any] = {} + self._multipart_params: List[Parameter] = [] + self._body_param: Any = None + self._body_serializer: Any = None + self._auth: Optional[Authentication] = None + self._array_serialization_format: SerializationFormats = SerializationFormats.INDEXED + self._xml_attributes: Optional[XmlAttributes] = None + + @validate_call + def server(self, server: Any) -> 'RequestBuilder': self._server = server return self - def path(self, path): + @validate_call + def path(self, path: str) -> 'RequestBuilder': self._path = path return self - def http_method(self, http_method): + @validate_call + def http_method(self, http_method: HttpMethodEnum) -> 'RequestBuilder': self._http_method = http_method return self - def template_param(self, template_param): - template_param.validate() - self._template_params[template_param.get_key()] = {'value': template_param.get_value(), - 'encode': template_param.need_to_encode()} + @validate_call + def template_param(self, template_param: Parameter) -> 'RequestBuilder': + if template_param.is_valid_parameter() and template_param.key is not None: + self._template_params[template_param.key] = { + 'value': template_param.value, 'encode': template_param.should_encode + } return self - def header_param(self, header_param): - header_param.validate() - self._header_params[header_param.get_key()] = header_param.get_value() + @validate_call + def header_param(self, header_param: Parameter) -> 'RequestBuilder': + if header_param.is_valid_parameter() and header_param.key is not None: + self._header_params[header_param.key] = header_param.value return self - def query_param(self, query_param): - query_param.validate() - self._query_params[query_param.get_key()] = query_param.get_value() + @validate_call + def query_param(self, query_param: Parameter) -> 'RequestBuilder': + if query_param.is_valid_parameter() and query_param.key is not None: + self._query_params[query_param.key] = query_param.value return self - def form_param(self, form_param): - form_param.validate() - self._form_params[form_param.get_key()] = form_param.get_value() + @validate_call + def form_param(self, form_param: Parameter) -> 'RequestBuilder': + if form_param.is_valid_parameter() and form_param.key is not None: + self._form_params[form_param.key] = form_param.value return self - def additional_form_params(self, additional_form_params): + @validate_call + def additional_form_params(self, additional_form_params: Dict[str, Any]) -> 'RequestBuilder': self._additional_form_params = additional_form_params return self - def additional_query_params(self, additional_query_params): + @validate_call + def additional_query_params(self, additional_query_params: Dict[str, Any]) -> 'RequestBuilder': self._additional_query_params = additional_query_params return self - def multipart_param(self, multipart_param): - multipart_param.validate() - self._multipart_params.append(multipart_param) + @validate_call + def multipart_param(self, multipart_param: Parameter) -> 'RequestBuilder': + if multipart_param.is_valid_parameter(): + self._multipart_params.append(multipart_param) return self - def body_param(self, body_param): - body_param.validate() - if body_param.get_key(): - if not self._body_param: - self._body_param = dict() - self._body_param[body_param.get_key()] = body_param.get_value() - else: - self._body_param = body_param.get_value() + @validate_call + def body_param(self, body_param: Parameter): + if body_param.is_valid_parameter(): + if body_param.key is not None: + if not self._body_param: + self._body_param = dict() + self._body_param[body_param.key] = body_param.value + else: + self._body_param = body_param.value return self - def body_serializer(self, body_serializer): + @validate_call + def body_serializer(self, body_serializer: Any) -> 'RequestBuilder': self._body_serializer = body_serializer return self - def auth(self, auth): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def auth(self, auth: Authentication) -> 'RequestBuilder': self._auth = auth return self - def array_serialization_format(self, array_serialization_format): + @validate_call + def array_serialization_format(self, array_serialization_format: SerializationFormats) -> 'RequestBuilder': self._array_serialization_format = array_serialization_format return self - def xml_attributes(self, xml_attributes): + @validate_call + def xml_attributes(self, xml_attributes: XmlAttributes) -> 'RequestBuilder': self._xml_attributes = xml_attributes return self - def build(self, global_configuration): - _url = self.process_url(global_configuration) + @validate_call + def build(self, global_configuration: GlobalConfiguration) -> HttpRequest: + _url: str = self.process_url(global_configuration) - _request_headers = self.process_request_headers(global_configuration) + _request_headers: Dict[str, Any] = self.process_request_headers(global_configuration) - _request_body = self.process_body_params() + _request_body: Any = self.process_body_params() - _multipart_params = self.process_multipart_params() + _multipart_params: Dict[str, Any] = self.process_multipart_params() - http_request = HttpRequest(http_method=self._http_method, - query_url=_url, - headers=_request_headers, - parameters=_request_body, - files=_multipart_params) + http_request: HttpRequest = HttpRequest( + http_method=self._http_method, query_url=_url, headers=_request_headers, + parameters=_request_body, files=_multipart_params) - self.apply_auth(global_configuration.get_auth_managers(), http_request) + self.apply_auth(global_configuration.auth_managers, http_request) return http_request - def process_url(self, global_configuration): - _url = global_configuration.get_base_uri(self._server) - _updated_url_with_template_params = self.get_updated_url_with_template_params() - _url += _updated_url_with_template_params if _updated_url_with_template_params else self._path + @validate_call + def process_url(self, global_configuration: GlobalConfiguration) -> str: + _url: str = global_configuration.get_base_uri(self._server) or '' + if self._template_params: + _url += ApiHelper.append_url_with_template_parameters( + self._path, self._template_params) + elif self._path is not None: + _url += self._path _url = self.get_updated_url_with_query_params(_url) return ApiHelper.clean_url(_url) - def get_updated_url_with_template_params(self): - return ApiHelper.append_url_with_template_parameters(self._path, - self._template_params) if self._template_params else None - - def get_updated_url_with_query_params(self, _query_builder): + @validate_call + def get_updated_url_with_query_params(self, _query_builder: str) -> str: if self._additional_query_params: self.add_additional_query_params() - return ApiHelper.append_url_with_query_parameters(_query_builder, self._query_params, - self._array_serialization_format)\ + return ApiHelper.append_url_with_query_parameters( + _query_builder, self._query_params, self._array_serialization_format)\ if self._query_params else _query_builder - def process_request_headers(self, global_configuration): - request_headers = self._header_params - global_headers = global_configuration.get_global_headers() - additional_headers = global_configuration.get_additional_headers() + @validate_call + def process_request_headers(self, global_configuration: GlobalConfiguration) -> Dict[str, Any]: + request_headers: Dict[str, Any] = self._header_params + global_headers: Dict[str, Any] = global_configuration.global_headers + additional_headers: Dict[str, Any] = global_configuration.additional_headers if global_headers: - prepared_headers = {key: str(value) if value is not None else value + prepared_headers: Dict[str, Any] = {key: str(value) if value is not None else value for key, value in self._header_params.items()} request_headers = {**global_headers, **prepared_headers} @@ -156,7 +176,10 @@ def process_request_headers(self, global_configuration): return request_headers - def process_body_params(self): + @validate_call + def process_body_params( + self + ) -> Union[None, List[Tuple[str, Any]], Dict[str, Any], Union[FileType, Parameter], str]: if self._xml_attributes: return self.process_xml_parameters(self._body_serializer) elif self._form_params or self._additional_form_params: @@ -167,24 +190,33 @@ def process_body_params(self): elif self._body_param is not None and not self._body_serializer: return self.resolve_body_param() - def process_xml_parameters(self, body_serializer): - if self._xml_attributes.get_array_item_name(): - return body_serializer(self._xml_attributes.get_value(), - self._xml_attributes.get_root_element_name(), - self._xml_attributes.get_array_item_name()) + return None + + @validate_call + def process_xml_parameters(self, body_serializer: Any) -> str: + if self._xml_attributes is None: + raise ValueError('XML attributes are not set') + + if self._xml_attributes.array_item_name: + return body_serializer(self._xml_attributes.value, + self._xml_attributes.root_element_name, + self._xml_attributes.array_item_name) - return body_serializer(self._xml_attributes.get_value(), self._xml_attributes.get_root_element_name()) + return body_serializer(self._xml_attributes.value, self._xml_attributes.root_element_name) + @validate_call def add_additional_form_params(self): if self._additional_form_params: for form_param in self._additional_form_params: self._form_params[form_param] = self._additional_form_params[form_param] + @validate_call def add_additional_query_params(self): for query_param in self._additional_query_params: self._query_params[query_param] = self._additional_query_params[query_param] - def resolve_body_param(self): + @validate_call + def resolve_body_param(self) -> Union[FileType, Parameter]: if ApiHelper.is_file_wrapper_instance(self._body_param): if self._body_param.content_type: self._header_params['content-type'] = self._body_param.content_type @@ -192,19 +224,28 @@ def resolve_body_param(self): return self._body_param - def process_multipart_params(self): - multipart_params = {} + @validate_call + def process_multipart_params(self) -> Dict[str, Union[Tuple[Optional[str], FileType, Optional[str]]]]: + multipart_params: Dict[str, Union[Tuple[Optional[str], FileType, Optional[str]]]] = {} for multipart_param in self._multipart_params: - param_value = multipart_param.get_value() + if multipart_param.key is None: + continue + + param_value = multipart_param.value if ApiHelper.is_file_wrapper_instance(param_value): - file = param_value.file_stream - multipart_params[multipart_param.get_key()] = (file.name, file, param_value.content_type) + file: FileType = param_value.file_stream # TextIO, BinaryIO, BufferedIOBase, TextIOWrapper + multipart_params[multipart_param.key] = (file.name if hasattr(file, "name") else None, + file, param_value.content_type) else: - multipart_params[multipart_param.get_key()] = (self.get_param_name(param_value), param_value, - multipart_param.get_default_content_type()) + param_name = param_value.name if not isinstance(param_value, str) else None + multipart_params[multipart_param.key] = ( + param_name, param_value, multipart_param.default_content_type + ) + return multipart_params - def apply_auth(self, auth_managers, http_request): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def apply_auth(self, auth_managers: Dict[str, Authentication], http_request: HttpRequest): if self._auth: if self._auth.with_auth_managers(auth_managers).is_valid(): self._auth.apply(http_request) diff --git a/apimatic_core/response_handler.py b/apimatic_core/response_handler.py index d696e42..1511de8 100644 --- a/apimatic_core/response_handler.py +++ b/apimatic_core/response_handler.py @@ -1,66 +1,89 @@ +from __future__ import annotations import re + +from typing import Callable, Union, Any, Optional, Dict, List, Type, Literal + +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call + from apimatic_core.http.response.api_response import ApiResponse -from apimatic_core.types.error_case import ErrorCase +from apimatic_core.types.error_case import ErrorCase, MessageType class ResponseHandler: def __init__(self): - self._deserializer = None - self._convertor = None - self._deserialize_into = None - self._is_api_response = False - self._is_nullify404 = False - self._local_errors = {} - self._datetime_format = None - self._is_xml_response = False - self._xml_item_name = None - - def deserializer(self, deserializer): + self._deserializer: Any = None + self._convertor: Optional[Callable[[Any], Any]] = None + self._deserialize_into: Any = None + self._is_api_response: bool = False + self._is_nullify404: bool = False + self._local_errors: Dict[str, ErrorCase] = {} + self._datetime_format: Optional[DateTimeFormat] = None + self._is_xml_response: bool = False + self._xml_item_name: Optional[str] = None + + @validate_call + def deserializer(self, deserializer: Optional[Callable[[Union[str, bytes]], Any]]) -> 'ResponseHandler': self._deserializer = deserializer return self - def convertor(self, convertor): + @validate_call + def convertor(self, convertor: Optional[Callable[[Any], Any]]) -> 'ResponseHandler': self._convertor = convertor return self - def deserialize_into(self, deserialize_into): + @validate_call + def deserialize_into(self, deserialize_into: Any) -> 'ResponseHandler': self._deserialize_into = deserialize_into return self - def is_api_response(self, is_api_response): + @validate_call + def is_api_response(self, is_api_response: bool) -> 'ResponseHandler': self._is_api_response = is_api_response return self - def is_nullify404(self, is_nullify404): + @validate_call + def is_nullify404(self, is_nullify404: bool) -> 'ResponseHandler': self._is_nullify404 = is_nullify404 return self - def local_error(self, error_code, error_message, exception_type): - self._local_errors[str(error_code)] = ErrorCase()\ - .error_message(error_message)\ - .exception_type(exception_type) + @validate_call + def local_error( + self, error_code: Union[int, str], error_message: str, exception_type: Type[Any] + ) -> 'ResponseHandler': + self._local_errors[str(error_code)] = ErrorCase(message=error_message, + message_type=MessageType.SIMPLE, + exception_type=exception_type) return self - def local_error_template(self, error_code, error_message_template, exception_type): - self._local_errors[str(error_code)] = ErrorCase()\ - .error_message_template(error_message_template)\ - .exception_type(exception_type) + @validate_call + def local_error_template( + self, error_code: Union[int, str], error_message: str, exception_type: Type[Any] + ) -> 'ResponseHandler': + self._local_errors[str(error_code)] = ErrorCase(message=error_message, + message_type=MessageType.TEMPLATE, + exception_type=exception_type) return self - def datetime_format(self, datetime_format): + @validate_call + def datetime_format(self, datetime_format: DateTimeFormat) -> 'ResponseHandler': self._datetime_format = datetime_format return self - def is_xml_response(self, is_xml_response): + @validate_call + def is_xml_response(self, is_xml_response: bool) -> 'ResponseHandler': self._is_xml_response = is_xml_response return self - def xml_item_name(self, xml_item_name): + @validate_call + def xml_item_name(self, xml_item_name: Optional[str]) -> 'ResponseHandler': self._xml_item_name = xml_item_name return self - def handle(self, response, global_errors): + @validate_call + def handle(self, response: HttpResponse, global_errors: Dict[str, ErrorCase]) -> Any: # checking Nullify 404 if response.status_code == 404 and self._is_nullify404: @@ -80,7 +103,8 @@ def handle(self, response, global_errors): return deserialized_value - def validate(self, response, global_errors): + @validate_call + def validate(self, response: HttpResponse, global_errors: Dict[str, ErrorCase]): if response.status_code in range(200, 300): return @@ -88,12 +112,17 @@ def validate(self, response, global_errors): self.validate_against_error_cases(response, global_errors) - def apply_xml_deserializer(self, response): + @validate_call + def apply_xml_deserializer(self, response: HttpResponse) -> Any: + if self._deserializer is None: + return + if self._xml_item_name: return self._deserializer(response.text, self._xml_item_name, self._deserialize_into) return self._deserializer(response.text, self._deserialize_into) - def apply_deserializer(self, response): + @validate_call + def apply_deserializer(self, response: HttpResponse) -> Any: if self._is_xml_response: return self.apply_xml_deserializer(response) elif self._deserialize_into: @@ -105,35 +134,40 @@ def apply_deserializer(self, response): else: return response.text - def apply_api_response(self, response, deserialized_value): + @validate_call + def apply_api_response(self, response: HttpResponse, deserialized_value: Any) -> Any: if self._is_api_response: return ApiResponse(response, body=deserialized_value, errors=deserialized_value.get('errors') if type(deserialized_value) is dict else None) return deserialized_value - def apply_convertor(self, deserialized_value): + @validate_call + def apply_convertor(self, deserialized_value: Any) -> Any: if self._convertor: return self._convertor(deserialized_value) return deserialized_value @staticmethod - def validate_against_error_cases(response, error_cases): - actual_status_code = str(response.status_code) + @validate_call + def validate_against_error_cases(response: HttpResponse, error_cases: Dict[str, ErrorCase]): + actual_status_code: str = str(response.status_code) # Handling error case when configured as explicit error code - error_case = error_cases.get(actual_status_code) if error_cases else None + error_case: Optional[ErrorCase] = error_cases.get(actual_status_code) if error_cases else None if error_case: error_case.raise_exception(response) # Handling error case when configured as explicit error codes range - default_range_error_case = [error_cases[status_code] for status_code, error_case in error_cases.items() - if re.match(r'^[{}]XX$'.format(actual_status_code[0]), - status_code)] if error_cases else None + default_range_error_case: Optional[List[ErrorCase]] = [ + error_cases[status_code] + for status_code, error_case in error_cases.items() + if re.match(r'^[{}]XX$'.format(actual_status_code[0]), status_code) + ] if error_cases else None if default_range_error_case: default_range_error_case[0].raise_exception(response) # Handling default error case if configured - default_error_case = error_cases.get('default') if error_cases else None + default_error_case: Optional[ErrorCase] = error_cases.get('default') if error_cases else None if default_error_case: default_error_case.raise_exception(response) diff --git a/apimatic_core/types/array_serialization_format.py b/apimatic_core/types/array_serialization_format.py index e89a606..2b1b507 100644 --- a/apimatic_core/types/array_serialization_format.py +++ b/apimatic_core/types/array_serialization_format.py @@ -1,7 +1,7 @@ from enum import Enum -class SerializationFormats(Enum): +class SerializationFormats(str, Enum): """Enumeration of Array serialization formats Attributes: diff --git a/apimatic_core/types/datetime_format.py b/apimatic_core/types/datetime_format.py index d598a11..5592f23 100644 --- a/apimatic_core/types/datetime_format.py +++ b/apimatic_core/types/datetime_format.py @@ -1,7 +1,7 @@ from enum import Enum -class DateTimeFormat(Enum): +class DateTimeFormat(str, Enum): """Enumeration of Date time formats Attributes: diff --git a/apimatic_core/types/error_case.py b/apimatic_core/types/error_case.py index 18e4f47..994b735 100644 --- a/apimatic_core/types/error_case.py +++ b/apimatic_core/types/error_case.py @@ -1,94 +1,60 @@ import re -from apimatic_core.utilities.api_helper import ApiHelper - - -class ErrorCase: +from enum import Enum - def is_error_message_template(self): - """Checks if the set exception message is a template or not. - - Returns: - string: True if the exception message is a template. - """ - return True if self._error_message_template else False - - def __init__(self): - self._error_message = None - self._error_message_template = None - self._exception_type = None - - def error_message(self, error_message): - """Setter for the error message. - Args: - error_message: The simple exception message. - """ - self._error_message = error_message - return self +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call, BaseModel +from typing import Optional, Literal - def error_message_template(self, error_message_template): - """Setter for the error message template. - Args: - error_message_template: The exception message template containing placeholders. - """ - self._error_message_template = error_message_template - return self +from apimatic_core.utilities.api_helper import ApiHelper - def exception_type(self, exception_type): - """Setter for the exception type. - Args: - exception_type: The exception type to raise. - """ - self._exception_type = exception_type - return self - def get_error_message(self, response): - """Getter for the error message for the exception case. This considers both error message - and error template message. Error message template has the higher precedence over an error message. - Args: - response: The received http response. +class MessageType(str, Enum): + SIMPLE = 'simple' + TEMPLATE = 'template' - Returns: - string: The resolved exception message. - """ - if self.is_error_message_template(): - return self._get_resolved_error_message_template(response) - return self._error_message +class ErrorCase(BaseModel): + message: str + message_type: MessageType = MessageType.SIMPLE + exception_type: type - def raise_exception(self, response): + @validate_call + def raise_exception(self, response: HttpResponse): """Raises the exception for the current error case type. Args: - response: The received http response. + response (HttpResponse): The received http response. """ - raise self._exception_type(self.get_error_message(response), response) + if self.message_type == MessageType.TEMPLATE: + raise self.exception_type(self._get_resolved_error_message_template(response), response) + + raise self.exception_type(self.message, response) - def _get_resolved_error_message_template(self, response): + @validate_call + def _get_resolved_error_message_template(self, response: HttpResponse): """Updates all placeholders in the given message template with provided value. Args: - response: The received http response. + response (HttpResponse): The received http response. Returns: string: The resolved template value. """ - placeholders = re.findall(r'\{\$.*?\}', self._error_message_template) + placeholders = re.findall(r'\{\$.*?\}', self.message) status_code_placeholder = set(filter(lambda element: element == '{$statusCode}', placeholders)) header_placeholders = set(filter(lambda element: element.startswith('{$response.header'), placeholders)) body_placeholders = set(filter(lambda element: element.startswith('{$response.body'), placeholders)) # Handling response code placeholder - error_message_template = ApiHelper.resolve_template_placeholders(status_code_placeholder, - str(response.status_code), - self._error_message_template) + error_message_template = ApiHelper.resolve_template_placeholders( + status_code_placeholder, str(response.status_code), self.message) # Handling response header placeholder - error_message_template = ApiHelper.resolve_template_placeholders(header_placeholders, response.headers, - error_message_template) + error_message_template = ApiHelper.resolve_template_placeholders( + header_placeholders, response.headers, error_message_template) # Handling response body placeholder - response_payload = ApiHelper.json_deserialize(response.text, as_dict=True) - error_message_template = ApiHelper.resolve_template_placeholders_using_json_pointer(body_placeholders, - response_payload, - error_message_template) + response_payload = ApiHelper.json_deserialize(str(response.text), as_dict=True) + error_message_template = ApiHelper.resolve_template_placeholders_using_json_pointer( + body_placeholders, response_payload, error_message_template) return error_message_template diff --git a/apimatic_core/types/file_wrapper.py b/apimatic_core/types/file_wrapper.py index 9ba8fd3..db1946c 100644 --- a/apimatic_core/types/file_wrapper.py +++ b/apimatic_core/types/file_wrapper.py @@ -1,9 +1,9 @@ from io import BufferedIOBase, TextIOWrapper -from pydantic import BaseModel +from pydantic import BaseModel, validate_call from typing import Optional, Union, TextIO, BinaryIO -FileType = Union[TextIO, BinaryIO, BufferedIOBase, TextIOWrapper] +FileType = Union[TextIO, BinaryIO, TextIOWrapper, BufferedIOBase] class FileWrapper(BaseModel): """A wrapper to allow passing in content type for file uploads.""" @@ -15,6 +15,7 @@ class FileWrapper(BaseModel): "arbitrary_types_allowed": True } + @validate_call(config=dict(arbitrary_types_allowed=True)) def __init__(self, file_stream: FileType, content_type: Optional[str]=None) -> None: super().__init__(file_stream=file_stream, content_type=content_type) diff --git a/apimatic_core/types/parameter.py b/apimatic_core/types/parameter.py index 6f2110f..3170d91 100644 --- a/apimatic_core/types/parameter.py +++ b/apimatic_core/types/parameter.py @@ -1,56 +1,23 @@ +from typing import Any, Callable, Optional -class Parameter: +from pydantic import BaseModel - def get_key(self): - return self._key - def get_value(self): - return self._value +class Parameter(BaseModel): - def need_to_encode(self): - return self._should_encode + key: Optional[str] = None + value: Any = None + is_required: bool = False + should_encode: bool = False + default_content_type: Optional[str] = None + validator: Optional[Callable[[Any], bool]] = None - def get_default_content_type(self): - return self._default_content_type + def is_valid_parameter(self): + if self.is_required and self.value is None: + raise ValueError("Required parameter {} cannot be None.".format(self.key)) - def __init__( - self - ): - self._key = None - self._value = None - self._is_required = False - self._should_encode = False - self._default_content_type = None - self._validator = None + if self.validator is None: + return True - def key(self, key): - self._key = key - return self - - def value(self, value): - self._value = value - return self - - def is_required(self, is_required): - self._is_required = is_required - return self - - def should_encode(self, should_encode): - self._should_encode = should_encode - return self - - def default_content_type(self, default_content_type): - self._default_content_type = default_content_type - return self - - def validator(self, validator): - self._validator = validator - return self - - def validate(self): - if self._is_required and self._value is None: - raise ValueError("Required parameter {} cannot be None.".format(self._key)) - - if self._validator is not None and self._validator(self._value): - return + return self.validator(self.value) diff --git a/apimatic_core/types/union_types/any_of.py b/apimatic_core/types/union_types/any_of.py index 667f9b6..aaa38a0 100644 --- a/apimatic_core/types/union_types/any_of.py +++ b/apimatic_core/types/union_types/any_of.py @@ -1,20 +1,25 @@ from apimatic_core_interfaces.types.union_type import UnionType -from apimatic_core.types.union_types.union_type_context import UnionTypeContext +from typing import List, Any + +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import validate_call + from apimatic_core.utilities.union_type_helper import UnionTypeHelper class AnyOf(UnionType): - def __init__(self, union_types, union_type_context: UnionTypeContext = UnionTypeContext()): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def __init__(self, union_types: List[UnionType], union_type_context: UnionTypeContext = UnionTypeContext()): super(AnyOf, self).__init__(union_types, union_type_context) - self.collection_cases = None + self.collection_cases: Any = None - def validate(self, value): + @validate_call + def validate(self, value: Any) -> 'UnionType': context = self._union_type_context UnionTypeHelper.update_nested_flag_for_union_types(self._union_types) - is_optional_or_nullable = UnionTypeHelper.is_optional_or_nullable_case(context, - [nested_type.get_context() - for nested_type in self._union_types]) + is_optional_or_nullable = UnionTypeHelper.is_optional_or_nullable_case( + context, [nested_type.get_context() for nested_type in self._union_types]) if value is None and is_optional_or_nullable: self.is_valid = True @@ -22,36 +27,40 @@ def validate(self, value): if value is None: self.is_valid = False - self.error_messages = UnionTypeHelper.process_errors(value, self._union_types, self.error_messages, - self.get_context().is_nested, False) + self.error_messages = UnionTypeHelper.process_errors( + value, self._union_types, self.error_messages, self.get_context().is_nested, False) return self self._validate_value_against_case(value, context) if not self.is_valid: - self.error_messages = UnionTypeHelper.process_errors(value, self._union_types, self.error_messages, - self.get_context().is_nested, False) + self.error_messages = UnionTypeHelper.process_errors( + value, self._union_types, self.error_messages, self.get_context().is_nested, False) return self - def deserialize(self, value): + @validate_call + def deserialize(self, value: Any) -> Any: if value is None: return None - return UnionTypeHelper.deserialize_value(value, self._union_type_context, self.collection_cases, - self._union_types) - - def _validate_value_against_case(self, value, context): - if context.is_array() and context.is_dict() and context.is_array_of_dict(): - self.is_valid, self.collection_cases = UnionTypeHelper.validate_array_of_dict_case(self._union_types, value, - False) - elif context.is_array() and context.is_dict(): - self.is_valid, self.collection_cases = UnionTypeHelper.validate_dict_of_array_case(self._union_types, value, - False) - elif context.is_array(): - self.is_valid, self.collection_cases = UnionTypeHelper.validate_array_case(self._union_types, value, False) - elif context.is_dict(): - self.is_valid, self.collection_cases = UnionTypeHelper.validate_dict_case(self._union_types, value, False) + return UnionTypeHelper.deserialize_value( + value, self._union_type_context, self.collection_cases, self._union_types) + + @validate_call + def _validate_value_against_case(self, value: Any, context: UnionTypeContext): + if context.is_array and context.is_dict and context.is_array_of_dict: + self.is_valid, self.collection_cases = UnionTypeHelper.validate_array_of_dict_case( + self._union_types, value, False) + elif context.is_array and context.is_dict: + self.is_valid, self.collection_cases = UnionTypeHelper.validate_dict_of_array_case( + self._union_types, value,False) + elif context.is_array: + self.is_valid, self.collection_cases = UnionTypeHelper.validate_array_case( + self._union_types, value, False) + elif context.is_dict: + self.is_valid, self.collection_cases = UnionTypeHelper.validate_dict_case( + self._union_types, value, False) else: self.is_valid = UnionTypeHelper.get_matched_count(value, self._union_types, False) >= 1 diff --git a/apimatic_core/types/union_types/leaf_type.py b/apimatic_core/types/union_types/leaf_type.py index d09579e..0e5cd6b 100644 --- a/apimatic_core/types/union_types/leaf_type.py +++ b/apimatic_core/types/union_types/leaf_type.py @@ -1,28 +1,39 @@ from datetime import date, datetime +from enum import Enum + from apimatic_core_interfaces.types.union_type import UnionType -from apimatic_core.types.union_types.union_type_context import UnionTypeContext +from typing import Type, Any, Dict, List + +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext + from apimatic_core.utilities.api_helper import ApiHelper from apimatic_core.utilities.datetime_helper import DateTimeHelper from apimatic_core.utilities.union_type_helper import UnionTypeHelper -from pydantic import BaseModel +from pydantic import BaseModel, validate_call + class LeafType(UnionType): - def __init__(self, type_to_match: type, union_type_context: UnionTypeContext = UnionTypeContext()): - super(LeafType, self).__init__(None, union_type_context) + type_to_match: Type[Any] + + @validate_call + def __init__(self, type_to_match: Type[Any], union_type_context: UnionTypeContext = UnionTypeContext()): + super(LeafType, self).__init__([], union_type_context) self.type_to_match = type_to_match - def validate(self, value): + @validate_call + def validate(self, value: Any) -> 'UnionType': context = self._union_type_context if value is None: - self.is_valid = context.is_nullable_or_optional() + self.is_valid = context.is_nullable_or_optional else: self.is_valid = self._validate_value_against_case(value, context) return self - def deserialize(self, value): + @validate_call + def deserialize(self, value: Any) -> Any: if value is None: return None @@ -31,22 +42,24 @@ def deserialize(self, value): return deserialized_value - def _validate_value_against_case(self, value, context): - if context.is_array() and context.is_dict() and context.is_array_of_dict(): + @validate_call + def _validate_value_against_case(self, value: Any, context: UnionTypeContext): + if context.is_array and context.is_dict and context.is_array_of_dict: return self._validate_array_of_dict_case(value) - if context.is_array() and context.is_dict(): + if context.is_array and context.is_dict: return self._validate_dict_of_array_case(value) - if context.is_array(): + if context.is_array: return self._validate_array_case(value) - if context.is_dict(): + if context.is_dict: return self._validate_dict_case(value) return self._validate_simple_case(value) - def _validate_dict_case(self, dict_value): + @validate_call + def _validate_dict_case(self, dict_value: Any) -> bool: if not isinstance(dict_value, dict): return False @@ -57,7 +70,8 @@ def _validate_dict_case(self, dict_value): return True - def _validate_dict_of_array_case(self, dict_value): + @validate_call + def _validate_dict_of_array_case(self, dict_value: Any) -> bool: if not isinstance(dict_value, dict): return False @@ -68,7 +82,8 @@ def _validate_dict_of_array_case(self, dict_value): return True - def _validate_array_case(self, array_value): + @validate_call + def _validate_array_case(self, array_value: Any) -> bool: if not isinstance(array_value, list): return False @@ -79,7 +94,8 @@ def _validate_array_case(self, array_value): return True - def _validate_array_of_dict_case(self, array_value): + @validate_call + def _validate_array_of_dict_case(self, array_value: Any) -> bool: if not isinstance(array_value, list): return False @@ -90,10 +106,11 @@ def _validate_array_of_dict_case(self, array_value): return True - def _validate_simple_case(self, value): + @validate_call + def _validate_simple_case(self, value: Any) -> bool: context = self._union_type_context - if value is None or context.is_nullable_or_optional(): + if value is None or context.is_nullable_or_optional: return True if value is None or isinstance(value, list): @@ -101,18 +118,23 @@ def _validate_simple_case(self, value): return self._validate_value(value, context) - def _validate_value(self, value, context): + @validate_call + def _validate_value(self, value: Any, context: UnionTypeContext) -> bool: if self.type_to_match is datetime: return UnionTypeHelper.validate_date_time(value, context) - if self.type_to_match is date: + if self.type_to_match is date and type(value) in [str, date]: return DateTimeHelper.validate_date(value) + if issubclass(self.type_to_match, Enum) and hasattr(self.type_to_match, 'is_valid'): + return self.type_to_match.is_valid(value) + return self._validate_value_with_discriminator(value, context) - def _validate_value_with_discriminator(self, value, context): - discriminator = context.get_discriminator() - discriminator_value = context.get_discriminator_value() + @validate_call + def _validate_value_with_discriminator(self, value: Any, context: UnionTypeContext) -> bool: + discriminator = context.discriminator + discriminator_value = context.discriminator_value if discriminator and discriminator_value: return self._validate_with_discriminator(discriminator, discriminator_value, value) @@ -125,7 +147,8 @@ def _validate_value_with_discriminator(self, value, context): return type(value) is self.type_to_match - def _validate_with_discriminator(self, discriminator, discriminator_value, value): + @validate_call + def _validate_with_discriminator(self, discriminator: str, discriminator_value: str, value: Any) -> bool: if not isinstance(value, dict) or value.get(discriminator) != discriminator_value: return False @@ -138,22 +161,24 @@ def _validate_with_discriminator(self, discriminator, discriminator_value, value return type(value) is self.type_to_match - def _deserialize_value_against_case(self, value, context): - if context.is_array() and context.is_dict() and context.is_array_of_dict(): + @validate_call + def _deserialize_value_against_case(self, value: Any, context: UnionTypeContext) -> Any: + if context.is_array and context.is_dict and context.is_array_of_dict: return self._deserialize_array_of_dict_case(value) - if context.is_array() and context.is_dict(): + if context.is_array and context.is_dict: return self._deserialize_dict_of_array_case(value) - if context.is_array(): + if context.is_array: return self._deserialize_array_case(value) - if context.is_dict(): + if context.is_dict: return self._deserialize_dict_case(value) return self._deserialize_simple_case(value) - def _deserialize_dict_case(self, dict_value): + @validate_call + def _deserialize_dict_case(self, dict_value: Any) -> Dict[str, Any]: deserialized_value = {} for key, value in dict_value.items(): result_value = self._deserialize_simple_case(value) @@ -161,7 +186,8 @@ def _deserialize_dict_case(self, dict_value): return deserialized_value - def _deserialize_dict_of_array_case(self, dict_value): + @validate_call + def _deserialize_dict_of_array_case(self, dict_value: Any) -> Dict[str, List[Any]]: deserialized_value = {} for key, value in dict_value.items(): result_value = self._deserialize_array_case(value) @@ -169,7 +195,8 @@ def _deserialize_dict_of_array_case(self, dict_value): return deserialized_value - def _deserialize_array_case(self, array_value): + @validate_call + def _deserialize_array_case(self, array_value: Any) -> List[Any]: deserialized_value = [] for item in array_value: result_value = self._deserialize_simple_case(item) @@ -177,7 +204,8 @@ def _deserialize_array_case(self, array_value): return deserialized_value - def _deserialize_array_of_dict_case(self, array_value): + @validate_call + def _deserialize_array_of_dict_case(self, array_value: Any) -> List[Dict[str, Any]]: deserialized_value = [] for item in array_value: result_value = self._deserialize_dict_case(item) @@ -185,19 +213,25 @@ def _deserialize_array_of_dict_case(self, array_value): return deserialized_value - def _deserialize_simple_case(self, value): - if hasattr(self.type_to_match, 'from_dictionary'): - return self.type_to_match.from_dictionary(value) + @validate_call + def _deserialize_simple_case(self, value: Any) -> Any: + + if issubclass(self.type_to_match, Enum): + return self.type_to_match(value) + + if issubclass(self.type_to_match, BaseModel): + return self.type_to_match.model_validate(value) if self.type_to_match is date: return ApiHelper.date_deserialize(value) if self.type_to_match is datetime: return ApiHelper.datetime_deserialize( - value, self._union_type_context.get_date_time_format()) + value, self._union_type_context.date_time_format) return value + @validate_call def __deepcopy__(self, memo={}): copy_object = LeafType(self.type_to_match, self._union_type_context) copy_object._union_types = self._union_types diff --git a/apimatic_core/types/union_types/one_of.py b/apimatic_core/types/union_types/one_of.py index e1212b5..a3711d8 100644 --- a/apimatic_core/types/union_types/one_of.py +++ b/apimatic_core/types/union_types/one_of.py @@ -1,20 +1,25 @@ from apimatic_core_interfaces.types.union_type import UnionType -from apimatic_core.types.union_types.union_type_context import UnionTypeContext +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from typing import List, Any + +from pydantic import validate_call + from apimatic_core.utilities.union_type_helper import UnionTypeHelper class OneOf(UnionType): - def __init__(self, union_types, union_type_context: UnionTypeContext = UnionTypeContext()): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def __init__(self, union_types: List[UnionType], union_type_context: UnionTypeContext = UnionTypeContext()): super(OneOf, self).__init__(union_types, union_type_context) - self.collection_cases = None + self.collection_cases: Any = None - def validate(self, value): + @validate_call + def validate(self, value: Any) -> 'UnionType': context = self._union_type_context UnionTypeHelper.update_nested_flag_for_union_types(self._union_types) - is_optional_or_nullable = UnionTypeHelper.is_optional_or_nullable_case(context, - [nested_type.get_context() - for nested_type in self._union_types]) + is_optional_or_nullable = UnionTypeHelper.is_optional_or_nullable_case( + context, [nested_type.get_context() for nested_type in self._union_types]) if value is None and is_optional_or_nullable: self.is_valid = True @@ -22,39 +27,45 @@ def validate(self, value): if value is None: self.is_valid = False - self.error_messages = UnionTypeHelper.process_errors(value, self._union_types, self.error_messages, - self.get_context().is_nested, True) + self.error_messages = UnionTypeHelper.process_errors( + value, self._union_types, self.error_messages, self.get_context().is_nested, True) return self self._validate_value_against_case(value, context) if not self.is_valid: - self.error_messages = UnionTypeHelper.process_errors(value, self._union_types, self.error_messages, - self.get_context().is_nested, True) + self.error_messages = UnionTypeHelper.process_errors( + value, self._union_types, self.error_messages, self.get_context().is_nested, True) return self - def deserialize(self, value): + @validate_call + def deserialize(self, value: Any) -> Any: if value is None: return None - return UnionTypeHelper.deserialize_value(value, self._union_type_context, - self.collection_cases, self._union_types) - - def _validate_value_against_case(self, value, context): - if context.is_array() and context.is_dict() and context.is_array_of_dict(): - self.is_valid, self.collection_cases = UnionTypeHelper.validate_array_of_dict_case(self._union_types, value, - True) - elif context.is_array() and context.is_dict(): - self.is_valid, self.collection_cases = UnionTypeHelper.validate_dict_of_array_case(self._union_types, value, - True) - elif context.is_array(): - self.is_valid, self.collection_cases = UnionTypeHelper.validate_array_case(self._union_types, value, True) - elif context.is_dict(): - self.is_valid, self.collection_cases = UnionTypeHelper.validate_dict_case(self._union_types, value, True) + return UnionTypeHelper.deserialize_value( + value, self._union_type_context, self.collection_cases, self._union_types + ) + + @validate_call + def _validate_value_against_case(self, value: Any, context: UnionTypeContext): + if context.is_array and context.is_dict and context.is_array_of_dict: + self.is_valid, self.collection_cases = UnionTypeHelper.validate_array_of_dict_case( + self._union_types, value, True) + elif context.is_array and context.is_dict: + self.is_valid, self.collection_cases = UnionTypeHelper.validate_dict_of_array_case( + self._union_types, value, True) + elif context.is_array: + self.is_valid, self.collection_cases = UnionTypeHelper.validate_array_case( + self._union_types, value, True) + elif context.is_dict: + self.is_valid, self.collection_cases = UnionTypeHelper.validate_dict_case( + self._union_types, value, True) else: self.is_valid = UnionTypeHelper.get_matched_count(value, self._union_types, True) == 1 + @validate_call def __deepcopy__(self, memo={}): copy_object = OneOf(self._union_types, self._union_type_context) copy_object.is_valid = self.is_valid diff --git a/apimatic_core/types/union_types/union_type_context.py b/apimatic_core/types/union_types/union_type_context.py index 4933f65..54bfde3 100644 --- a/apimatic_core/types/union_types/union_type_context.py +++ b/apimatic_core/types/union_types/union_type_context.py @@ -1,17 +1,7 @@ class UnionTypeContext: - @classmethod - def create(cls, is_array=False, is_dict=False, is_array_of_dict=False, is_optional=False, is_nullable=False, - discriminator=None, discriminator_value=None, date_time_format=None, date_time_converter=None): - return cls().array(is_array).dict(is_dict)\ - .array_of_dict(is_array_of_dict)\ - .optional(is_optional)\ - .nullable(is_nullable)\ - .discriminator(discriminator)\ - .discriminator_value(discriminator_value)\ - .date_time_format(date_time_format)\ - .date_time_converter(date_time_converter) + def __init__(self): self._is_array = False diff --git a/apimatic_core/types/xml_attributes.py b/apimatic_core/types/xml_attributes.py index a884f5d..14677f5 100644 --- a/apimatic_core/types/xml_attributes.py +++ b/apimatic_core/types/xml_attributes.py @@ -1,30 +1,9 @@ -class XmlAttributes: +from typing import Any, Optional - def get_value(self): - return self._value +from pydantic import BaseModel - def get_root_element_name(self): - return self._root_element_name - - def get_array_item_name(self): - return self._array_item_name - - def __init__( - self - ): - self._value = None - self._root_element_name = None - self._array_item_name = None - - def value(self, value): - self._value = value - return self - - def root_element_name(self, root_element_name): - self._root_element_name = root_element_name - return self - - def array_item_name(self, array_item_name): - self._array_item_name = array_item_name - return self +class XmlAttributes(BaseModel): + value: Any = None + root_element_name: Optional[str] = None + array_item_name: Optional[str] = None diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index b588e3d..ad6c2f4 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations +from abc import abstractmethod, ABC from collections import abc import re import datetime @@ -9,12 +10,16 @@ import jsonpickle import dateutil.parser +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.types.union_type import UnionType from jsonpointer import JsonPointerException, resolve_pointer -from apimatic_core.types.datetime_format import DateTimeFormat +from typing import Optional, Any, Dict, Type, Callable, List, Union, Tuple, Set + from apimatic_core.types.file_wrapper import FileWrapper from apimatic_core.types.array_serialization_format import SerializationFormats -from requests.utils import quote -from pydantic import BaseModel +from urllib.parse import quote +from pydantic import BaseModel, validate_call +from pydantic.fields import FieldInfo class ApiHelper(object): @@ -27,10 +32,85 @@ class ApiHelper(object): """ + class CustomDate(ABC): + + """ A base class for wrapper classes of datetime. + + This class contains methods which help in + appropriate serialization of datetime objects. + + """ + + def __init__(self, dtime, value=None): + self.datetime = dtime + if not value: + self.value = self.from_datetime(dtime) + else: + self.value = value + + def __repr__(self): + return str(self.value) + + def __getstate__(self): + return self.value + + def __setstate__(self, state): # pragma: no cover + pass + + @classmethod + @abstractmethod + def from_datetime(cls, date_time: datetime.datetime): + pass + + @classmethod + @abstractmethod + def from_value(cls, value: str): + pass + + class HttpDateTime(CustomDate): + + """ A wrapper class for datetime to support HTTP date format.""" + + @classmethod + def from_datetime(cls, date_time): + return eut.formatdate(timeval=mktime(date_time.timetuple()), localtime=False, usegmt=True) + + @classmethod + def from_value(cls, value): + dtime = datetime.datetime.fromtimestamp(eut.mktime_tz(eut.parsedate_tz(value))) + return cls(dtime, value) + + class UnixDateTime(CustomDate): + + """ A wrapper class for datetime to support Unix date format.""" + + @classmethod + def from_datetime(cls, date_time): + return calendar.timegm(date_time.utctimetuple()) + + @classmethod + def from_value(cls, value): + dtime = datetime.datetime.utcfromtimestamp(float(value)) + return cls(dtime, int(value)) + + class RFC3339DateTime(CustomDate): + + """ A wrapper class for datetime to support Rfc 3339 format.""" + + @classmethod + def from_datetime(cls, date_time): + return date_time.isoformat() + + @classmethod + def from_value(cls, value): + dtime = dateutil.parser.parse(value) + return cls(dtime, value) + SKIP = '#$%^S0K1I2P3))*' @staticmethod - def json_serialize_wrapped_params(obj): + @validate_call + def json_serialize_wrapped_params(obj: Optional[Dict[str, Any]]) -> Optional[str]: """JSON Serialization of a given wrapped object. Args: @@ -49,7 +129,8 @@ def json_serialize_wrapped_params(obj): return jsonpickle.encode(val, False) @staticmethod - def json_serialize(obj, should_encode=True): + @validate_call + def json_serialize(obj: Any, should_encode: bool=True) -> Optional[str]: """JSON Serialization of a given object. Args: @@ -69,40 +150,43 @@ def json_serialize(obj, should_encode=True): # Resolve any Names if it's one of our objects that needs to have this called on if isinstance(obj, list): - value = list() + value_list: List[Optional[str]] = list() for item in obj: if isinstance(item, dict) or isinstance(item, list): - value.append(ApiHelper.json_serialize(item, False)) + value_list.append(ApiHelper.json_serialize(item, should_encode=False)) elif ApiHelper.is_custom_type(item): - value.append(item.model_dump_json()) if should_encode else obj.model_dump() + value_list.append(ApiHelper.json_serialize(item, should_encode=False)) else: - value.append(item) - obj = value + value_list.append(item) + obj = value_list elif isinstance(obj, dict): - value = dict() + value_dict: Dict[str, Optional[str]] = dict() for key, item in obj.items(): if isinstance(item, list) or isinstance(item, dict): - value[key] = ApiHelper.json_serialize(item, False) + value_dict[key] = ApiHelper.json_serialize(item, should_encode=False) elif ApiHelper.is_custom_type(item): - value[key] = item.model_dump_json() if should_encode else obj.model_dump() + value_dict[key] = ApiHelper.json_serialize(item, should_encode=False) else: - value[key] = item - obj = value - else: - if ApiHelper.is_custom_type(obj): - return obj.model_dump_json() if should_encode else obj.model_dump() + value_dict[key] = item + obj = value_dict + elif ApiHelper.is_custom_type(obj): + return obj.model_dump_json() if should_encode else obj.model_dump() if not should_encode: return obj return jsonpickle.encode(obj, False) @staticmethod - def is_custom_type(obj_or_class): + @validate_call + def is_custom_type(obj_or_class: Any) -> bool: return (isinstance(obj_or_class, type) and issubclass(obj_or_class, BaseModel) or isinstance(obj_or_class, BaseModel)) @staticmethod - def json_deserialize(json, unboxing_function=None, as_dict=False): + @validate_call + def json_deserialize( + json: Optional[str], unboxing_function: Optional[Callable[[Any], Any]]=None, as_dict: bool=False + ) -> Any: """JSON Deserialization of a given string. Args: @@ -134,8 +218,11 @@ def json_deserialize(json, unboxing_function=None, as_dict=False): return unboxing_function(decoded) @staticmethod - def apply_unboxing_function(value, unboxing_function, is_array=False, is_dict=False, is_array_of_map=False, - is_map_of_array=False, dimension_count=1): + @validate_call + def apply_unboxing_function( + value: Any, unboxing_function: Callable[[Any], Any], is_array: bool=False, is_dict: bool=False, + is_array_of_map: bool=False, is_map_of_array: bool=False, dimension_count: int=1 + ) -> Any: if is_dict: if is_map_of_array: return {k: ApiHelper.apply_unboxing_function(v, @@ -164,7 +251,8 @@ def apply_unboxing_function(value, unboxing_function, is_array=False, is_dict=Fa return unboxing_function(value) @staticmethod - def dynamic_deserialize(dynamic_response): + @validate_call + def dynamic_deserialize(dynamic_response: Optional[str]) -> Any: """JSON Deserialization of a given string. Args: @@ -180,7 +268,8 @@ def dynamic_deserialize(dynamic_response): return ApiHelper.json_deserialize(dynamic_response) @staticmethod - def date_deserialize(response): + @validate_call + def date_deserialize(json: Optional[str]) -> Union[datetime.date, List[datetime.date]]: """JSON Deserialization of a given string. Args: @@ -191,18 +280,21 @@ def date_deserialize(response): JSON serialized string. """ - deserialized_response = ApiHelper.json_deserialize(response) + deserialized_response = ApiHelper.json_deserialize(json) if isinstance(deserialized_response, list): return [dateutil.parser.parse(element).date() for element in deserialized_response] return dateutil.parser.parse(deserialized_response).date() @staticmethod - def datetime_deserialize(response, datetime_format): + @validate_call + def datetime_deserialize( + value: Optional[Union[str, float]], datetime_format: Optional[DateTimeFormat] + ) -> Union[None, CustomDate, Dict[str, CustomDate], List[CustomDate]]: """JSON Deserialization of a given string. Args: - response: the response to deserialize + value: the response to deserialize datetime_format: The date time format to deserialize into Returns: @@ -210,44 +302,50 @@ def datetime_deserialize(response, datetime_format): JSON serialized string. """ - if response is None: + if value is None: return None - if isinstance(response, str): - deserialized_response = ApiHelper.json_deserialize(response) + if isinstance(value, str): + deserialized_response = ApiHelper.json_deserialize(value) else: - deserialized_response = response + deserialized_response = value if DateTimeFormat.HTTP_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(response, ApiHelper.HttpDateTime.from_value)] + ApiHelper.json_deserialize(value, ApiHelper.HttpDateTime.from_value)] else: - return ApiHelper.HttpDateTime.from_value(response).datetime + return ApiHelper.HttpDateTime.from_value(value).datetime elif DateTimeFormat.UNIX_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(response, ApiHelper.UnixDateTime.from_value)] + ApiHelper.json_deserialize(value, ApiHelper.UnixDateTime.from_value)] else: - return ApiHelper.UnixDateTime.from_value(response).datetime + return ApiHelper.UnixDateTime.from_value(value).datetime elif DateTimeFormat.RFC3339_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(response, ApiHelper.RFC3339DateTime.from_value)] + ApiHelper.json_deserialize(value, ApiHelper.RFC3339DateTime.from_value)] else: - return ApiHelper.RFC3339DateTime.from_value(response).datetime + return ApiHelper.RFC3339DateTime.from_value(value).datetime + + return deserialized_response @staticmethod - def deserialize_union_type(union_type, response, should_deserialize=True): - if should_deserialize: - response = ApiHelper.json_deserialize(response, as_dict=True) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def deserialize_union_type( + union_type: UnionType, value: Any, should_deserialize: bool=True + ) -> Any: + if should_deserialize and isinstance(value, str): + value = ApiHelper.json_deserialize(value, as_dict=True) - union_type_result = union_type.validate(response) + union_type_result = union_type.validate(value) - return union_type_result.deserialize(response) + return union_type_result.deserialize(value) @staticmethod - def get_content_type(value): + @validate_call + def get_content_type(value: Any) -> Optional[str]: """Get content type header for oneof. Args: @@ -269,7 +367,8 @@ def get_content_type(value): return 'application/json; charset=utf-8' @staticmethod - def get_schema_path(path, file_name): + @validate_call + def get_schema_path(path: str, file_name: str) -> str: """Return the Schema's path Returns: @@ -282,7 +381,11 @@ def get_schema_path(path, file_name): return path @staticmethod - def serialize_array(key, array, formatting="indexed", is_query=False): + @validate_call + def serialize_array( + key: str, array: List[Any], formatting: SerializationFormats=SerializationFormats.INDEXED, + is_query: bool=False + ) -> List[Tuple[str, Any]]: """Converts an array parameter to a list of key value tuples. Args: @@ -295,7 +398,7 @@ def serialize_array(key, array, formatting="indexed", is_query=False): list: A list with key value tuples for the array elements. """ - tuples = [] + tuples: List[Tuple[str, Any]] = [] serializable_types = (str, int, float, bool, datetime.date, ApiHelper.CustomDate) @@ -325,7 +428,8 @@ def serialize_array(key, array, formatting="indexed", is_query=False): return tuples @staticmethod - def append_url_with_template_parameters(url, parameters): + @validate_call + def append_url_with_template_parameters(url: Optional[str], parameters: Optional[Dict[str, Dict[str, Any]]]) -> str: """Replaces template parameters in the given url. Args: @@ -339,6 +443,7 @@ def append_url_with_template_parameters(url, parameters): # Parameter validation if url is None: raise ValueError("URL is None.") + if parameters is None: return url @@ -361,7 +466,11 @@ def append_url_with_template_parameters(url, parameters): return url @staticmethod - def append_url_with_query_parameters(url, parameters, array_serialization="indexed"): + @validate_call + def append_url_with_query_parameters( + url: Optional[str], parameters: Optional[Dict[str, Any]], + array_serialization: SerializationFormats=SerializationFormats.INDEXED + ) -> str: """Adds query parameters to a URL. Args: @@ -376,10 +485,12 @@ def append_url_with_query_parameters(url, parameters, array_serialization="index # Parameter validation if url is None: raise ValueError("URL is None.") + if parameters is None: return url - parameters = ApiHelper.process_complex_types_parameters(parameters, array_serialization) - for value in parameters: + + query_parameters = ApiHelper.process_complex_types_parameters(parameters, array_serialization) + for value in query_parameters: key = value[0] val = value[1] seperator = '&' if '?' in url else '?' @@ -389,7 +500,8 @@ def append_url_with_query_parameters(url, parameters, array_serialization="index return url @staticmethod - def get_url_without_query(url): + @validate_call + def get_url_without_query(url: str) -> str: """ Extracts the protocol, domain, and path from a URL excluding the query parameters. @@ -411,7 +523,10 @@ def get_url_without_query(url): raise ValueError(f"Error parsing URL: {e}") from e @staticmethod - def process_complex_types_parameters(query_parameters, array_serialization): + @validate_call + def process_complex_types_parameters( + query_parameters: Dict[str, Any], array_serialization: SerializationFormats + ) -> List[Tuple[str, Any]]: processed_params = [] for key, value in query_parameters.items(): processed_params.extend( @@ -419,7 +534,8 @@ def process_complex_types_parameters(query_parameters, array_serialization): return processed_params @staticmethod - def clean_url(url): + @validate_call + def clean_url(url: str) -> str: """Validates and processes the given query Url to clean empty slashes. Args: @@ -444,7 +560,10 @@ def clean_url(url): return protocol + query_url + parameters @staticmethod - def form_encode_parameters(form_parameters, array_serialization="indexed"): + @validate_call + def form_encode_parameters( + form_parameters: Dict[str, Any], array_serialization: SerializationFormats=SerializationFormats.INDEXED + ) -> List[Tuple[str, Any]]: """Form encodes a dictionary of form parameters Args: @@ -464,9 +583,11 @@ def form_encode_parameters(form_parameters, array_serialization="indexed"): return encoded @staticmethod - def form_encode(obj, - instance_name, - array_serialization="indexed", is_query=False): + @validate_call + def form_encode( + obj: Any, instance_name: str, + array_serialization: SerializationFormats=SerializationFormats.INDEXED, is_query: bool=False + ) -> List[Tuple[str, Any]]: """Encodes a model in a form-encoded manner such as person[Name] Args: @@ -481,7 +602,7 @@ def form_encode(obj, """ retval = [] - # If we received an object, resolve it's field names. + # If we received an object, resolve its field names. if ApiHelper.is_custom_type(obj): obj = ApiHelper.json_serialize(obj, should_encode=False) @@ -502,105 +623,10 @@ def form_encode(obj, return retval @staticmethod - def to_dictionary(obj, should_ignore_null_values=False): - """Creates a dictionary representation of a class instance. The - keys are taken from the API description and may differ from language - specific variable names of properties. - - Args: - obj: The object to be converted into a dictionary. - should_ignore_null_values: Flag to ignore null values from the object dictionary - - Returns: - dictionary: A dictionary form of the model with properties in - their API formats. - - """ - dictionary = dict() - - optional_fields = obj._optionals if hasattr(obj, "_optionals") else [] - nullable_fields = obj._nullables if hasattr(obj, "_nullables") else [] - - if hasattr(obj, 'validate'): - obj.validate(obj) - - # Loop through all properties in this model - names = {k: v for k, v in obj.__dict__.items() if v is not None} if should_ignore_null_values else obj._names - for name in names: - value = getattr(obj, name, ApiHelper.SKIP) - - if value is ApiHelper.SKIP: - continue - - if value is None: - if name not in optional_fields and not name in nullable_fields: - raise ValueError(f"The value for {name} can not be None for {obj}") - else: - dictionary[obj._names[name]] = None - elif isinstance(value, list): - # Loop through each item - dictionary[obj._names[name]] = list() - for item in value: - if isinstance(item, list) or isinstance(item, dict): - dictionary[obj._names[name]].append(ApiHelper.process_nested_collection( - item, should_ignore_null_values)) - else: - dictionary[obj._names[name]].append(ApiHelper.to_dictionary(item, should_ignore_null_values) - if hasattr(item, "_names") else item) - elif isinstance(value, dict): - # Loop through each item - dictionary[obj._names[name]] = dict() - for k, v in value.items(): - if isinstance(v, list) or isinstance(v, dict): - dictionary[obj._names[name]][k] = ApiHelper.process_nested_collection( - v, should_ignore_null_values) - else: - dictionary[obj._names[name]][k] = ApiHelper.to_dictionary(value[k], should_ignore_null_values) \ - if hasattr(value[k], "_names") else value[k] - else: - dictionary[obj._names[name]] = ApiHelper.to_dictionary(value, should_ignore_null_values) if \ - hasattr(value, "_names") else value - - # Loop through all additional properties in this model - if hasattr(obj, "additional_properties"): - for name in obj.additional_properties: - - if name in dictionary.keys(): - raise ValueError(f'An additional property key, \'{name}\' conflicts with one of the model\'s properties') - - value = obj.additional_properties.get(name) - if isinstance(value, list): - # Loop through each item - dictionary[name] = list() - for item in value: - dictionary[name].append( - ApiHelper.to_dictionary(item, should_ignore_null_values) if hasattr(item, "_names") else item) - elif isinstance(value, dict): - # Loop through each item - dictionary[name] = dict() - for key in value: - dictionary[name][key] = ApiHelper.to_dictionary(value[key], should_ignore_null_values) if hasattr(value[key], - "_names") else \ - value[key] - else: - dictionary[name] = ApiHelper.to_dictionary(value, should_ignore_null_values) if hasattr(value, - "_names") else value - - # Return the result - return dictionary - - @staticmethod - def process_nested_collection(value, should_ignore_null_values): - if isinstance(value, list): - return [ApiHelper.process_nested_collection(item, should_ignore_null_values) for item in value] - - if isinstance(value, dict): - return {k: ApiHelper.process_nested_collection(v, should_ignore_null_values) for k, v in value.items()} - - return ApiHelper.to_dictionary(value, should_ignore_null_values) if hasattr(value, "_names") else value - - @staticmethod - def apply_datetime_converter(value, datetime_converter_obj): + @validate_call + def apply_datetime_converter( + value: Any, + datetime_converter_obj: Callable[[Any], Any]) -> Any: if isinstance(value, list): return [ApiHelper.apply_datetime_converter(item, datetime_converter_obj) for item in value] @@ -613,16 +639,20 @@ def apply_datetime_converter(value, datetime_converter_obj): return value @staticmethod - def when_defined(func, value): + @validate_call + def when_defined(func: Callable[[datetime.datetime], Any], value: datetime.datetime) -> Any: return func(value) if value else None @staticmethod - def is_file_wrapper_instance(param): + @validate_call + def is_file_wrapper_instance(param: Any) -> bool: return isinstance(param, FileWrapper) @staticmethod - def is_valid_type(value, type_callable, is_value_nullable=False, is_model_dict=False, - is_inner_model_dict=False): + def is_valid_type( + value: Any, type_callable: Callable[[Any], bool], is_value_nullable: bool=False, is_model_dict: bool=False, + is_inner_model_dict: bool=False + ) -> bool: if value is None and is_value_nullable: return True @@ -636,7 +666,10 @@ def is_valid_type(value, type_callable, is_value_nullable=False, is_model_dict=F return value is not None and type_callable(value) @staticmethod - def resolve_template_placeholders_using_json_pointer(placeholders, value, template): + @validate_call + def resolve_template_placeholders_using_json_pointer( + placeholders: Set[str], value: Optional[Dict[str, Any]], template: str + ) -> str: """Updates all placeholders in the given message template with provided value. Args: @@ -648,7 +681,7 @@ def resolve_template_placeholders_using_json_pointer(placeholders, value, templa string: The resolved template value. """ for placeholder in placeholders: - extracted_value = '' + extracted_value: Optional[str] = '' if '#' in placeholder: # pick the 2nd chunk then remove the last character (i.e. `}`) of the string value @@ -661,12 +694,13 @@ def resolve_template_placeholders_using_json_pointer(placeholders, value, templa pass elif value is not None: extracted_value = ApiHelper.json_serialize(value) - template = template.replace(placeholder, extracted_value) + template = template.replace(placeholder, extracted_value or '') return template @staticmethod - def resolve_template_placeholders(placeholders, values, template): + @validate_call + def resolve_template_placeholders(placeholders: Set[str], values: Union[str, Dict[str, Any]], template: str) -> str: """Updates all placeholders in the given message template with provided value. Args: @@ -691,7 +725,8 @@ def resolve_template_placeholders(placeholders, values, template): return template @staticmethod - def to_lower_case(list_of_string): + @validate_call + def to_lower_case(list_of_string: Optional[List[str]]) -> Optional[List[str]]: """Converts all strings in a list to lowercase. Args: @@ -710,7 +745,10 @@ def to_lower_case(list_of_string): return list(map(lambda x: x.lower(), list_of_string)) @staticmethod - def get_additional_properties(dictionary, unboxing_function): + @validate_call + def get_additional_properties( + dictionary: Dict[str, Any], unboxing_function: Callable[[Any], Any] + ) -> Dict[str, Any]: """Extracts additional properties from the dictionary. Args: @@ -729,67 +767,54 @@ def get_additional_properties(dictionary, unboxing_function): return additional_properties + @staticmethod + def sanitize_model(**kwargs: Any) -> Dict[str, Any]: + _sanitized_dump: Dict[str, Any] = {} + _nullable_fields: Set[str] = kwargs.get('nullable_fields', set()) + _optional_fields: Set[str] = kwargs.get('optional_fields', set()) + _model_dump: Dict[str, Any] = kwargs.get('model_dump', {}) + _model_fields: Dict[str, FieldInfo] = kwargs.get('model_fields', {}) + _model_fields_set: Set[str] = kwargs.get('model_fields_set', set()) + + for _name, _field_info in _model_fields.items(): + _value = _model_dump.pop(_name, None) + _alias = _field_info.serialization_alias or _name + + if _name not in _nullable_fields and _name not in _optional_fields: + # Always include required properties + _sanitized_dump[_alias] = _value + + elif _name in _nullable_fields and _name in _optional_fields: + # Include optional_nullable properties if explicitly set or not None + if _value is not None or _name in _model_fields_set: + _sanitized_dump[_alias] = _value + + elif _name in _optional_fields: + # Exclude optional properties if None + if _value is not None: + _sanitized_dump[_alias] = _value + + elif _name in _nullable_fields: + # Always include nullable properties, even if None + _sanitized_dump[_alias] = _value + + return _sanitized_dump - class CustomDate(object): - - """ A base class for wrapper classes of datetime. - - This class contains methods which help in - appropriate serialization of datetime objects. - - """ - - def __init__(self, dtime, value=None): - self.datetime = dtime - if not value: - self.value = self.from_datetime(dtime) - else: - self.value = value - - def __repr__(self): - return str(self.value) - - def __getstate__(self): - return self.value - - def __setstate__(self, state): # pragma: no cover - pass - - class HttpDateTime(CustomDate): - - """ A wrapper class for datetime to support HTTP date format.""" - - @classmethod - def from_datetime(cls, date_time): - return eut.formatdate(timeval=mktime(date_time.timetuple()), localtime=False, usegmt=True) - - @classmethod - def from_value(cls, value): - dtime = datetime.datetime.fromtimestamp(eut.mktime_tz(eut.parsedate_tz(value))) - return cls(dtime, value) - - class UnixDateTime(CustomDate): - - """ A wrapper class for datetime to support Unix date format.""" - - @classmethod - def from_datetime(cls, date_time): - return calendar.timegm(date_time.utctimetuple()) - - @classmethod - def from_value(cls, value): - dtime = datetime.datetime.utcfromtimestamp(float(value)) - return cls(dtime, int(value)) - - class RFC3339DateTime(CustomDate): - - """ A wrapper class for datetime to support Rfc 3339 format.""" - - @classmethod - def from_datetime(cls, date_time): - return date_time.isoformat() - - @classmethod - def from_value(cls, value): - dtime = dateutil.parser.parse(value) - return cls(dtime, value) + @staticmethod + def check_conflicts_with_additional_properties( + model_cls: BaseModel, properties: Dict[str, Any], additional_props_field: str + ) -> None: + """Raise ValueError if properties contain names conflicting with model fields.""" + defined_fields = { + alias or name + for name, field_info in model_cls.model_fields.items() + if name != additional_props_field # Dynamically exclude additional properties field + for alias in (field_info.serialization_alias, name) + } + + conflicting_keys = defined_fields.intersection(properties) + if conflicting_keys: + raise ValueError( + f"Invalid additional properties: {conflicting_keys}. " + "These names conflict with existing model properties." + ) diff --git a/apimatic_core/utilities/auth_helper.py b/apimatic_core/utilities/auth_helper.py index 26bc073..556e971 100644 --- a/apimatic_core/utilities/auth_helper.py +++ b/apimatic_core/utilities/auth_helper.py @@ -1,43 +1,51 @@ import base64 import calendar from datetime import datetime +from typing import Callable, Dict, Optional, Union, Any +from pydantic import validate_call class AuthHelper: - @staticmethod - def get_base64_encoded_value(*props, delimiter=':'): - if props.__len__() > 0 and not any(prop is None for prop in props): + @validate_call + def get_base64_encoded_value(*props: str, delimiter: str = ':') -> Optional[str]: + if props and not any(prop is None for prop in props): joined = delimiter.join(props) encoded = base64.b64encode(str.encode(joined)).decode('iso-8859-1') return encoded + return None @staticmethod - def get_token_expiry(current_timestamp, expires_in): + @validate_call + def get_token_expiry(current_timestamp: int, expires_in: Union[int, str]) -> int: return current_timestamp + int(expires_in) @staticmethod - def get_current_utc_timestamp(): + @validate_call + def get_current_utc_timestamp() -> int: return calendar.timegm(datetime.now().utctimetuple()) @staticmethod - def is_token_expired(token_expiry, clock_skew_time=None): - """ Checks if OAuth token has expired. + @validate_call + def is_token_expired(token_expiry: int, clock_skew_time: Optional[int] = None) -> bool: + """ + Checks if OAuth token has expired. Returns: bool: True if token has expired, False otherwise. - """ - if clock_skew_time is not None and token_expiry is not None: + if clock_skew_time is not None: token_expiry -= clock_skew_time utc_now = AuthHelper.get_current_utc_timestamp() - return token_expiry is not None and token_expiry < utc_now + return token_expiry < utc_now @staticmethod - def is_valid_auth(auth_params): - return auth_params and all(param and auth_params[param] for param in auth_params) + @validate_call + def is_valid_auth(auth_params: Dict[str, Any]) -> bool: + return bool(auth_params and all(param and auth_params[param] for param in auth_params)) @staticmethod - def apply(auth_params, func): - for param in auth_params: - func(param, auth_params[param]) + @validate_call + def apply(auth_params: Dict[str, Any], func: Callable[[str, Any], None]) -> None: + for param, value in auth_params.items(): + func(param, value) \ No newline at end of file diff --git a/apimatic_core/utilities/comparison_helper.py b/apimatic_core/utilities/comparison_helper.py index a1be75e..5c13822 100644 --- a/apimatic_core/utilities/comparison_helper.py +++ b/apimatic_core/utilities/comparison_helper.py @@ -1,26 +1,31 @@ +from typing import Any, Dict, List, Union, Optional, TypeVar +from pydantic import validate_call + +T = TypeVar("T") class ComparisonHelper: - """A Helper Class used for the comparison of expected and actual API response. - """ + """A Helper Class used for the comparison of expected and actual API response.""" @staticmethod - def match_headers(expected_headers, - received_headers, - allow_extra=True): + @validate_call + def match_headers( + expected_headers: Dict[str, Optional[str]], + received_headers: Dict[str, str], + allow_extra: bool = True + ) -> bool: """Static method to compare the received headers with the expected headers. - + Args: - expected_headers (dict): A dictionary of expected headers (keys in lower case). - received_headers (dict): A dictionary of headers received. - allow_extra (Boolean, optional): A flag which determines if we - allow extra headers. + expected_headers (Dict[str, Optional[str]]): A dictionary of expected headers (keys in lower case). + received_headers (Dict[str, str]): A dictionary of headers received. + allow_extra (bool, optional): A flag which determines if we allow extra headers. + Returns: - Boolean: True if headers match, False otherwise. - + bool: True if headers match, False otherwise. """ if ((len(received_headers) < len(expected_headers)) or - ((allow_extra is False) and (len(expected_headers) != len(received_headers)))): + ((not allow_extra) and (len(expected_headers) != len(received_headers)))): return False received_headers = {k.lower(): v for k, v in received_headers.items()} @@ -34,55 +39,53 @@ def match_headers(expected_headers, return True @staticmethod - def match_body(expected_body, - received_body, - check_values=False, - check_order=False, - check_count=False): + @validate_call + def match_body( + expected_body: Union[Dict[str, Any], List[Any], T], + received_body: Union[Dict[str, Any], List[Any], T], + check_values: bool = False, + check_order: bool = False, + check_count: bool = False + ) -> bool: """Static method to compare the received body with the expected body. - + Args: - expected_body (dynamic): The expected body. - received_body (dynamic): The received body. - check_values (Boolean, optional): A flag which determines if we - check values in dictionaries. - check_order (Boolean, optional): A flag which determines if we - check the order of array elements. - check_count (Boolean, optional): A flag which determines if we - check the count of array elements. + expected_body (Union[Dict[str, Any], List[Any], T]): The expected body. + received_body (Union[Dict[str, Any], List[Any], T]): The received body. + check_values (bool, optional): A flag which determines if we check values in dictionaries. + check_order (bool, optional): A flag which determines if we check the order of array elements. + check_count (bool, optional): A flag which determines if we check the count of array elements. + Returns: - Boolean: True if bodies match, False otherwise. - + bool: True if bodies match, False otherwise. """ - if type(expected_body) == dict: - if type(received_body) != dict: + if isinstance(expected_body, dict): + if not isinstance(received_body, dict): return False for key in expected_body: if key not in received_body: return False - if check_values or type(expected_body[key]) == dict: - if ComparisonHelper.match_body(expected_body[key], received_body[key], - check_values, check_order, check_count) is False: + if check_values or isinstance(expected_body[key], dict): + if not ComparisonHelper.match_body(expected_body[key], received_body[key], + check_values, check_order, check_count): return False - elif type(expected_body) == list: - if type(received_body) != list: + elif isinstance(expected_body, list): + if not isinstance(received_body, list): return False - if check_count is True and (len(expected_body) != len(received_body)): + if check_count and len(expected_body) != len(received_body): return False else: - previous_matches = [] + previous_matches: List[int] = [] for i, expected_element in enumerate(expected_body): - matches = [j for j, received_element - in enumerate(received_body) + matches = [j for j, received_element in enumerate(received_body) if ComparisonHelper.match_body(expected_element, received_element, - check_values, check_order, check_count)] - if len(matches) == 0: + check_values, check_order, check_count)] + if not matches: return False - if check_order is True: - if i != 0 and all([all(y > x for y in previous_matches) for x in matches]): + if check_order: + if i != 0 and all(all(y > x for y in previous_matches) for x in matches): return False previous_matches = matches elif expected_body != received_body: return False return True - diff --git a/apimatic_core/utilities/datetime_helper.py b/apimatic_core/utilities/datetime_helper.py index 0a9fc03..00a4185 100644 --- a/apimatic_core/utilities/datetime_helper.py +++ b/apimatic_core/utilities/datetime_helper.py @@ -1,37 +1,46 @@ from datetime import datetime, date -from apimatic_core.types.datetime_format import DateTimeFormat +from typing import Optional, Union, List, Dict + +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from pydantic import validate_call from apimatic_core.utilities.api_helper import ApiHelper class DateTimeHelper: - @staticmethod - def validate_datetime(datetime_value, datetime_format): - if DateTimeFormat.RFC3339_DATE_TIME == datetime_format: - return DateTimeHelper.is_rfc_3339(datetime_value) - elif DateTimeFormat.UNIX_DATE_TIME == datetime_format: - return DateTimeHelper.is_unix_timestamp(datetime_value) - elif DateTimeFormat.HTTP_DATE_TIME == datetime_format: - return DateTimeHelper.is_rfc_1123(datetime_value) + @validate_call + def validate_datetime(datetime_value: Union[str, float, int], datetime_format: Optional[DateTimeFormat]) -> bool: + if datetime_format is None: + return False + """Validate datetime string against a given format.""" + if datetime_format == DateTimeFormat.RFC3339_DATE_TIME: + return DateTimeHelper.is_rfc_3339(str(datetime_value)) + elif datetime_format == DateTimeFormat.UNIX_DATE_TIME: + return DateTimeHelper.is_unix_timestamp(datetime_value) + elif datetime_format == DateTimeFormat.HTTP_DATE_TIME: + return DateTimeHelper.is_rfc_1123(str(datetime_value)) return False @staticmethod - def validate_date(date_value): + @validate_call + def validate_date(date_value: Union[str, date]) -> bool: + """Validate a date string or `date` object.""" try: if isinstance(date_value, date): datetime.strptime(date_value.isoformat(), "%Y-%m-%d") - return True elif isinstance(date_value, str): datetime.strptime(date_value, "%Y-%m-%d") - return True else: return False + return True except ValueError: return False @staticmethod - def is_rfc_1123(datetime_value): + @validate_call + def is_rfc_1123(datetime_value: str) -> bool: + """Check if the string is in RFC 1123 format.""" try: datetime.strptime(datetime_value, "%a, %d %b %Y %H:%M:%S %Z") return True @@ -39,17 +48,21 @@ def is_rfc_1123(datetime_value): return False @staticmethod - def is_rfc_3339(datetime_value): + @validate_call + def is_rfc_3339(datetime_value: str) -> bool: + """Check if the string is in RFC 3339 format.""" try: - if '.' in datetime_value: - datetime_value = datetime_value[:datetime_value.rindex('.')] + if "." in datetime_value: + datetime_value = datetime_value[:datetime_value.rindex(".")] datetime.strptime(datetime_value, "%Y-%m-%dT%H:%M:%S") return True except (ValueError, AttributeError, TypeError): return False @staticmethod - def is_unix_timestamp(timestamp): + @validate_call + def is_unix_timestamp(timestamp: Union[str, int, float]) -> bool: + """Check if the value is a valid Unix timestamp.""" try: datetime.fromtimestamp(float(timestamp)) return True @@ -57,86 +70,132 @@ def is_unix_timestamp(timestamp): return False @staticmethod - def to_rfc3339_date_time(value): + @validate_call(config={"arbitrary_types_allowed": True}) + def to_rfc3339_date_time( + value: Union[None, List[datetime], Dict[str, datetime], datetime] + ) -> Union[None, str, List[str], Dict[str, str]]: + """Convert datetime values to RFC 3339 format.""" if value is None: return None + date_time = ApiHelper.apply_datetime_converter(value, ApiHelper.RFC3339DateTime) + return DateTimeHelper._convert_to_rfc3339(date_time) - date_time: ApiHelper.CustomDate = ApiHelper.apply_datetime_converter(value, ApiHelper.RFC3339DateTime) - return DateTimeHelper._convert(date_time) + @staticmethod + def _convert_to_rfc3339( + date_time: Union[None, ApiHelper.CustomDate, List[ApiHelper.CustomDate], Dict[str, ApiHelper.CustomDate]] + ) -> Union[None, str, List[str], Dict[str, str]]: + """Convert CustomDate objects to RFC 3339 strings.""" + if date_time is None: + return None + if isinstance(date_time, ApiHelper.CustomDate): + return str(date_time.value) + if isinstance(date_time, list): + return [DateTimeHelper._convert_to_rfc3339(element) for element in date_time] # type: ignore[misc] + if isinstance(date_time, dict): + return {key: DateTimeHelper._convert_to_rfc3339(value) for key, value in date_time.items()} # type: ignore[misc] + raise TypeError("Unsupported type for RFC 3339 conversion") @staticmethod - def to_rfc1123_date_time(value): + @validate_call(config={"arbitrary_types_allowed": True}) + def to_rfc1123_date_time( + value: Union[None, List[datetime], Dict[str, datetime], datetime] + ) -> Union[None, str, List[str], Dict[str, str]]: + """Convert datetime values to RFC 1123 format.""" if value is None: return None + date_time = ApiHelper.apply_datetime_converter(value, ApiHelper.HttpDateTime) + return DateTimeHelper._convert_to_rfc1123(date_time) - date_time: ApiHelper.CustomDate = ApiHelper.apply_datetime_converter(value, ApiHelper.HttpDateTime) - return DateTimeHelper._convert(date_time) + @staticmethod + def _convert_to_rfc1123( + date_time: Union[None, ApiHelper.CustomDate, List[ApiHelper.CustomDate], Dict[str, ApiHelper.CustomDate]] + ) -> Union[None, str, List[str], Dict[str, str]]: + """Convert CustomDate objects to RFC 1123 strings.""" + if date_time is None: + return None + if isinstance(date_time, ApiHelper.CustomDate): + return str(date_time.value) + if isinstance(date_time, list): + return [DateTimeHelper._convert_to_rfc1123(element) for element in date_time] # type: ignore[misc] + if isinstance(date_time, dict): + return {key: DateTimeHelper._convert_to_rfc1123(value) for key, value in date_time.items()} # type: ignore[misc] + raise TypeError("Unsupported type for RFC 3339 conversion") @staticmethod - def to_unix_timestamp(value): + @validate_call(config={"arbitrary_types_allowed": True}) + def to_unix_timestamp( + value: Union[None, List[datetime], Dict[str, datetime], datetime] + ) -> Union[None, float, List[float], Dict[str, float]]: + """Convert datetime values to Unix timestamps.""" if value is None: return None - - date_time: ApiHelper.CustomDate = ApiHelper.apply_datetime_converter(value, ApiHelper.UnixDateTime) - return DateTimeHelper._convert(date_time) + date_time = ApiHelper.apply_datetime_converter(value, ApiHelper.UnixDateTime) + return DateTimeHelper._convert_to_unix(date_time) @staticmethod - def _convert(date_time): + def _convert_to_unix( + date_time: Union[None, ApiHelper.CustomDate, List[ApiHelper.CustomDate], Dict[str, ApiHelper.CustomDate]] + ) -> Union[None, float, List[float], Dict[str, float]]: + """Convert CustomDate objects to Unix timestamps.""" if date_time is None: return None - + if isinstance(date_time, ApiHelper.CustomDate): + return float(date_time.value) if isinstance(date_time, list): - return [DateTimeHelper._convert(element) for element in date_time] - + return [DateTimeHelper._convert_to_unix(element) for element in date_time] # type: ignore[misc] if isinstance(date_time, dict): - return {key: DateTimeHelper._convert(element) for key, element in date_time.items()} - - return date_time.value + return {key: DateTimeHelper._convert_to_unix(value) for key, value in date_time.items()} # type: ignore[misc] + raise TypeError("Unsupported type for Unix timestamp conversion") @staticmethod - def try_parse_from_rfc3339_date_time(value): + @validate_call + def try_parse_from_rfc3339_date_time( + value: Optional[Union[str, datetime, List[Union[str, datetime]], Dict[str, Union[str, datetime]]]] + ) -> Optional[Union[datetime, List[datetime], Dict[str, datetime]]]: + """Parse RFC 3339 strings into datetime objects.""" if value is None: return None - - if isinstance(value, list): - return [DateTimeHelper.try_parse_from_rfc3339_date_time(element) for element in value] - - if isinstance(value, dict): - return {key: DateTimeHelper.try_parse_from_rfc3339_date_time(element) for key, element in value.items()} - if isinstance(value, datetime): return value + if isinstance(value, list): + return [DateTimeHelper.try_parse_from_rfc3339_date_time(element) for element in value] # type: ignore[misc] + if isinstance(value, dict): + return {key: DateTimeHelper.try_parse_from_rfc3339_date_time(element) for key, element in value.items()} # type: ignore[misc] return ApiHelper.RFC3339DateTime.from_value(value).datetime @staticmethod - def try_parse_from_rfc1123_date_time(value): + @validate_call + def try_parse_from_rfc1123_date_time( + value: Optional[Union[str, datetime, List[Union[str, datetime]], Dict[str, Union[str, datetime]]]] + ) -> Optional[Union[datetime, List[datetime], Dict[str, datetime]]]: + """Parse RFC 1123 strings into datetime objects.""" if value is None: return None - - if isinstance(value, list): - return [DateTimeHelper.try_parse_from_rfc1123_date_time(element) for element in value] - - if isinstance(value, dict): - return {key: DateTimeHelper.try_parse_from_rfc1123_date_time(element) for key, element in value.items()} - if isinstance(value, datetime): return value + if isinstance(value, list): + return [DateTimeHelper.try_parse_from_rfc1123_date_time(element) for element in value] # type: ignore[misc] + if isinstance(value, dict): + return {key: DateTimeHelper.try_parse_from_rfc1123_date_time(element) for key, element in value.items()} # type: ignore[misc] return ApiHelper.HttpDateTime.from_value(value).datetime @staticmethod - def try_parse_from_unix_timestamp(value): + @validate_call + def try_parse_from_unix_timestamp( + value: Optional[ + Union[int, float, datetime, List[Union[int, float, datetime]], Dict[str, Union[int, float, datetime]]] + ] + ) -> Optional[Union[datetime, List[datetime], Dict[str, datetime]]]: + """Parse Unix timestamps into datetime objects.""" if value is None: return None - - if isinstance(value, list): - return [DateTimeHelper.try_parse_from_unix_timestamp(element) for element in value] - - if isinstance(value, dict): - return {key: DateTimeHelper.try_parse_from_unix_timestamp(element) for key, element in value.items()} - if isinstance(value, datetime): return value - return ApiHelper.UnixDateTime.from_value(value).datetime + if isinstance(value, list): + return [DateTimeHelper.try_parse_from_unix_timestamp(element) for element in value] # type: ignore[misc] + if isinstance(value, dict): + return {key: DateTimeHelper.try_parse_from_unix_timestamp(element) for key, element in value.items()} # type: ignore[misc] + return datetime.fromtimestamp(float(value)) diff --git a/apimatic_core/utilities/file_helper.py b/apimatic_core/utilities/file_helper.py index b4cb5f4..452e702 100644 --- a/apimatic_core/utilities/file_helper.py +++ b/apimatic_core/utilities/file_helper.py @@ -1,33 +1,39 @@ -import tempfile - +from typing import Dict, BinaryIO, IO +from tempfile import _TemporaryFileWrapper, NamedTemporaryFile import requests +from pydantic import validate_call class FileHelper: """A Helper Class for files. - Attributes: - cache (Set): Class variable which stores hashes of file URLs so we don't - download the same file twice in a test session. - - """ + Attributes: + cache (Dict[str, BinaryIO]): Class variable which stores references to temporary files + for file URLs so the same file isn't downloaded multiple times in a session. + """ - cache = {} + cache: Dict[str, IO[bytes]] = {} @classmethod - def get_file(cls, url): - """Class method which takes a URL, downloads the file (if not - already downloaded for this test session) and returns a file object for - the file in read-binary mode. + @validate_call + def get_file(cls, url: str) -> IO[bytes]: + """Class method to download a file from a URL (if not already downloaded) and return its file object. Args: - url (string): The URL of the required file. - Returns: - FileObject: The file object of the required file (opened with "rb"). + url (str): The URL of the required file. + Returns: + BinaryIO: A temporary file object opened in read-binary mode. """ if url not in cls.cache: - cls.cache[url] = tempfile.NamedTemporaryFile() - cls.cache[url].write(requests.get(url).content) + # Download the file and store it in a named temporary file + response = requests.get(url) + response.raise_for_status() # Raise an exception for HTTP errors + temp_file = NamedTemporaryFile(delete=False, mode='w+b') # Explicitly open in write-binary mode + temp_file.write(response.content) + temp_file.flush() # Ensure all content is written to disk + cls.cache[url] = temp_file + + # Reset the file pointer before returning the file cls.cache[url].seek(0) return cls.cache[url] diff --git a/apimatic_core/utilities/union_type_helper.py b/apimatic_core/utilities/union_type_helper.py index 61fcddf..d144a48 100644 --- a/apimatic_core/utilities/union_type_helper.py +++ b/apimatic_core/utilities/union_type_helper.py @@ -1,5 +1,12 @@ import copy from datetime import datetime + +from apimatic_core_interfaces.types.union_type import UnionType +from typing import List, Any, Tuple, Dict, Set + +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import validate_call + from apimatic_core.exceptions.anyof_validation_exception import AnyOfValidationException from apimatic_core.exceptions.oneof_validation_exception import OneOfValidationException from apimatic_core.types.datetime_format import DateTimeFormat @@ -9,17 +16,21 @@ class UnionTypeHelper: - NONE_MATCHED_ERROR_MESSAGE = 'We could not match any acceptable types against the given JSON.' - MORE_THAN_1_MATCHED_ERROR_MESSAGE = 'There are more than one acceptable type matched against the given JSON.' + NONE_MATCHED_ERROR_MESSAGE: str = 'We could not match any acceptable types against the given JSON.' + MORE_THAN_1_MATCHED_ERROR_MESSAGE: str = 'There are more than one acceptable type matched against the given JSON.' @staticmethod - def get_deserialized_value(union_types, value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def get_deserialized_value(union_types: List[UnionType], value: Any) -> Any: return [union_type for union_type in union_types if union_type.is_valid][0].deserialize(value) @staticmethod - def validate_array_of_dict_case(union_types, array_value, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def validate_array_of_dict_case( + union_types: List[UnionType], array_value: Any, is_for_one_of: bool + ) -> Tuple[bool, List[Dict[str, List[UnionType]]]]: if UnionTypeHelper.is_invalid_array_value(array_value): - return tuple((False, [])) + return False, [] collection_cases = [] valid_cases = [] @@ -28,12 +39,15 @@ def validate_array_of_dict_case(union_types, array_value, is_for_one_of): collection_cases.append(inner_dictionary) valid_cases.append(case_validity) is_valid = sum(valid_cases) == array_value.__len__() - return tuple((is_valid, collection_cases)) + return is_valid, collection_cases @staticmethod - def validate_dict_of_array_case(union_types, dict_value, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def validate_dict_of_array_case( + union_types: List[UnionType], dict_value: Any, is_for_one_of: bool + ) -> Tuple[bool, Dict[str, List[List[UnionType]]]]: if UnionTypeHelper.is_invalid_dict_value(dict_value): - return tuple((False, [])) + return False, {} collection_cases = {} valid_cases = [] @@ -42,19 +56,25 @@ def validate_dict_of_array_case(union_types, dict_value, is_for_one_of): collection_cases[key] = inner_array valid_cases.append(case_validity) is_valid = sum(valid_cases) == dict_value.__len__() - return tuple((is_valid, collection_cases)) + return is_valid, collection_cases @staticmethod - def validate_dict_case(union_types, dict_value, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def validate_dict_case( + union_types: List[UnionType], dict_value: Any, is_for_one_of: bool + ) -> Tuple[bool, Dict[str, List[UnionType]]]: if UnionTypeHelper.is_invalid_dict_value(dict_value): - return tuple((False, [])) + return False, {} is_valid, collection_cases = UnionTypeHelper.process_dict_items(union_types, dict_value, is_for_one_of) - return tuple((is_valid, collection_cases)) + return is_valid, collection_cases @staticmethod - def process_dict_items(union_types, dict_value, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def process_dict_items( + union_types: List[UnionType], dict_value: Any, is_for_one_of: bool + ) -> Tuple[bool, Dict[str, List[UnionType]]]: is_valid = True collection_cases = {} @@ -67,16 +87,22 @@ def process_dict_items(union_types, dict_value, is_for_one_of): return is_valid, collection_cases @staticmethod - def validate_array_case(union_types, array_value, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def validate_array_case( + union_types: List[UnionType], array_value: Any, is_for_one_of: bool + ) -> Tuple[bool, List[List[UnionType]]]: if UnionTypeHelper.is_invalid_array_value(array_value): - return tuple((False, [])) + return False, [] is_valid, collection_cases = UnionTypeHelper.process_array_items(union_types, array_value, is_for_one_of) - return tuple((is_valid, collection_cases)) + return is_valid, collection_cases @staticmethod - def process_array_items(union_types, array_value, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def process_array_items( + union_types: List[UnionType], array_value: Any, is_for_one_of: bool + ) -> Tuple[bool, List[List[UnionType]]]: is_valid = True collection_cases = [] @@ -89,7 +115,20 @@ def process_array_items(union_types, array_value, is_for_one_of): return is_valid, collection_cases @staticmethod - def check_item_validity(is_for_one_of, is_valid, matched_count): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def process_item( + value: Any, union_types: List[UnionType], is_for_one_of: bool + ) -> Tuple[bool, List[UnionType]]: + is_valid = True + union_type_cases = UnionTypeHelper.make_deep_copies(union_types) + matched_count = UnionTypeHelper.get_matched_count(value, union_type_cases, is_for_one_of) + is_valid = UnionTypeHelper.check_item_validity(is_for_one_of, is_valid, matched_count) + + return is_valid, union_type_cases + + @staticmethod + @validate_call + def check_item_validity(is_for_one_of: bool, is_valid: bool, matched_count: int) -> bool: if is_valid and is_for_one_of: is_valid = matched_count == 1 elif is_valid: @@ -97,7 +136,8 @@ def check_item_validity(is_for_one_of, is_valid, matched_count): return is_valid @staticmethod - def make_deep_copies(union_types): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def make_deep_copies(union_types: List[UnionType]) -> List[UnionType]: nested_cases = [] for union_type in union_types: nested_cases.append(copy.deepcopy(union_type)) @@ -105,7 +145,8 @@ def make_deep_copies(union_types): return nested_cases @staticmethod - def get_matched_count(value, union_types, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def get_matched_count(value: Any, union_types: List[UnionType], is_for_one_of: bool) -> int: matched_count = UnionTypeHelper.get_valid_cases_count(value, union_types) if is_for_one_of and matched_count == 1: @@ -117,77 +158,90 @@ def get_matched_count(value, union_types, is_for_one_of): return matched_count @staticmethod - def get_valid_cases_count(value, union_types): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def get_valid_cases_count(value: Any, union_types: List[UnionType]) -> int: return sum(union_type.validate(value).is_valid for union_type in union_types) @staticmethod - def handle_discriminator_cases(value, union_types): - has_discriminator_cases = all(union_type.get_context().get_discriminator() is not None and - union_type.get_context().get_discriminator_value() is not None + @validate_call(config=dict(arbitrary_types_allowed=True)) + def handle_discriminator_cases(value: Any, union_types: List[UnionType]) -> int: + has_discriminator_cases = all(union_type.get_context().discriminator is not None and + union_type.get_context().discriminator_value is not None for union_type in union_types) if has_discriminator_cases: for union_type in union_types: - union_type.get_context().discriminator(None) - union_type.get_context().discriminator_value(None) + union_type.get_context().discriminator = None + union_type.get_context().discriminator_value = None return UnionTypeHelper.get_valid_cases_count(value, union_types) return 0 @staticmethod - def validate_date_time(value, context): + @validate_call + def validate_date_time(value: Any, context: UnionTypeContext) -> bool: if isinstance(value, ApiHelper.RFC3339DateTime): - return context.get_date_time_format() == DateTimeFormat.RFC3339_DATE_TIME + return context.date_time_format == DateTimeFormat.RFC3339_DATE_TIME if isinstance(value, ApiHelper.HttpDateTime): - return context.get_date_time_format() == DateTimeFormat.HTTP_DATE_TIME + return context.date_time_format == DateTimeFormat.HTTP_DATE_TIME if isinstance(value, ApiHelper.UnixDateTime): - return context.get_date_time_format() == DateTimeFormat.UNIX_DATE_TIME + return context.date_time_format == DateTimeFormat.UNIX_DATE_TIME - if isinstance(value, datetime) and context.get_date_time_converter() is not None: - serialized_dt = str(ApiHelper.when_defined(context.get_date_time_converter(), value)) - return DateTimeHelper.validate_datetime(serialized_dt, context.get_date_time_format()) + if isinstance(value, datetime) and context.date_time_converter is not None: + serialized_dt = str(ApiHelper.when_defined(context.date_time_converter, value)) + return DateTimeHelper.validate_datetime(serialized_dt, context.date_time_format) - return DateTimeHelper.validate_datetime(value, context.get_date_time_format()) + return DateTimeHelper.validate_datetime(value, context.date_time_format) @staticmethod - def is_optional_or_nullable_case(current_context, inner_contexts): - return current_context.is_nullable_or_optional() or \ - any(context.is_nullable_or_optional() for context in inner_contexts) + @validate_call + def is_optional_or_nullable_case(current_context: UnionTypeContext, inner_contexts: List[UnionTypeContext]) -> bool: + return current_context.is_nullable_or_optional or \ + any(context.is_nullable_or_optional for context in inner_contexts) @staticmethod - def update_nested_flag_for_union_types(nested_union_types): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def update_nested_flag_for_union_types(nested_union_types: List[UnionType]) -> None: for union_type in nested_union_types: union_type.get_context().is_nested = True @staticmethod - def is_invalid_array_value(value): + @validate_call + def is_invalid_array_value(value: Any) -> bool: return value is None or not isinstance(value, list) @staticmethod - def is_invalid_dict_value(value): + @validate_call + def is_invalid_dict_value(value: Any) -> bool: return value is None or not isinstance(value, dict) @staticmethod - def deserialize_value(value, context, collection_cases, union_types): - if context.is_array() and context.is_dict() and context.is_array_of_dict(): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def deserialize_value( + value: Any, context: UnionTypeContext, collection_cases: Any, union_types: List[UnionType] + ) -> Any: + if context.is_array and context.is_dict and context.is_array_of_dict: return UnionTypeHelper.deserialize_array_of_dict_case(value, collection_cases) - if context.is_array() and context.is_dict(): + if context.is_array and context.is_dict: return UnionTypeHelper.deserialize_dict_of_array_case(value, collection_cases) - if context.is_array(): + if context.is_array: return UnionTypeHelper.deserialize_array_case(value, collection_cases) - if context.is_dict(): + if context.is_dict: return UnionTypeHelper.deserialize_dict_case(value, collection_cases) return UnionTypeHelper.get_deserialized_value(union_types, value) @staticmethod - def deserialize_array_of_dict_case(array_value, collection_cases): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def deserialize_array_of_dict_case( + array_value: List[Dict[str, Any]], collection_cases: List[Dict[str, List[UnionType]]] + ) -> List[Dict[str, Any]]: deserialized_value = [] for index, item in enumerate(array_value): deserialized_value.append(UnionTypeHelper.deserialize_dict_case(item, collection_cases[index])) @@ -195,7 +249,10 @@ def deserialize_array_of_dict_case(array_value, collection_cases): return deserialized_value @staticmethod - def deserialize_dict_of_array_case(dict_value, collection_cases): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def deserialize_dict_of_array_case( + dict_value: Dict[str, List[Any]], collection_cases: Dict[str, List[List[UnionType]]] + ) -> Dict[str, List[Any]]: deserialized_value = {} for key, value in dict_value.items(): deserialized_value[key] = UnionTypeHelper.deserialize_array_case(value, collection_cases[key]) @@ -203,7 +260,10 @@ def deserialize_dict_of_array_case(dict_value, collection_cases): return deserialized_value @staticmethod - def deserialize_dict_case(dict_value, collection_cases): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def deserialize_dict_case( + dict_value: Dict[str, Any], collection_cases: Dict[str, List[UnionType]] + ) -> Dict[str, Any]: deserialized_value = {} for key, value in dict_value.items(): valid_case = [case for case in collection_cases[key] if case.is_valid][0] @@ -212,7 +272,8 @@ def deserialize_dict_case(dict_value, collection_cases): return deserialized_value @staticmethod - def deserialize_array_case(array_value, collection_cases): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def deserialize_array_case(array_value: List[Any], collection_cases: List[List[UnionType]]) -> List[Any]: deserialized_value = [] for index, item in enumerate(array_value): valid_case = [case for case in collection_cases[index] if case.is_valid][0] @@ -221,7 +282,10 @@ def deserialize_array_case(array_value, collection_cases): return deserialized_value @staticmethod - def process_errors(value, union_types, error_messages, is_nested, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def process_errors( + value: Any, union_types: List[UnionType], error_messages: Set[str], is_nested: bool, is_for_one_of: bool + ) -> Set[str]: error_messages.add(', '.join(UnionTypeHelper.get_combined_error_messages(union_types))) if not is_nested: @@ -230,7 +294,8 @@ def process_errors(value, union_types, error_messages, is_nested, is_for_one_of) return error_messages @staticmethod - def get_combined_error_messages(union_types): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def get_combined_error_messages(union_types: List[UnionType]) -> List[str]: combined_error_messages = [] from apimatic_core.types.union_types.leaf_type import LeafType for union_type in union_types: @@ -241,7 +306,8 @@ def get_combined_error_messages(union_types): return combined_error_messages @staticmethod - def raise_validation_exception(value, union_types, error_message, is_for_one_of): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def raise_validation_exception(value: Any, union_types: List[UnionType], error_message: str, is_for_one_of: bool): if is_for_one_of: matched_count = sum(union_type.is_valid for union_type in union_types) message = UnionTypeHelper.MORE_THAN_1_MATCHED_ERROR_MESSAGE if matched_count > 0 \ diff --git a/apimatic_core/utilities/xml_helper.py b/apimatic_core/utilities/xml_helper.py index ccf0c20..700dc20 100644 --- a/apimatic_core/utilities/xml_helper.py +++ b/apimatic_core/utilities/xml_helper.py @@ -10,6 +10,9 @@ import xml.etree.ElementTree as ET import datetime import dateutil.parser +from typing import Any, List, Dict, Optional, Type, Callable, Union + +from pydantic import validate_call from apimatic_core.utilities.api_helper import ApiHelper @@ -20,12 +23,16 @@ class XmlHelper: """ @staticmethod - def serialize_to_xml(value, root_element_name): - """Serializes a given value to an xml document. + @validate_call + def serialize_to_xml(value: Any, root_element_name: str) -> str: + """Serializes a given value to an XML document. Args: - value (mixed): The value to serialize. + value (Any): The value to serialize. root_element_name (str): The name of the document's root element. + + Returns: + str: Serialized XML string. """ root = ET.Element(root_element_name) @@ -35,14 +42,17 @@ def serialize_to_xml(value, root_element_name): return ET.tostring(root).decode() @staticmethod - def serialize_list_to_xml(value, root_element_name, array_item_name): - """Serializes a given list of values to an xml document. + @validate_call + def serialize_list_to_xml(value: List[Any], root_element_name: str, array_item_name: str) -> str: + """Serializes a given list of values to an XML document. Args: - value (mixed): The value to serialize. + value (List[Any]): The list of values to serialize. root_element_name (str): The name of the document's root element. - array_item_name (str): The element name to use for each item in - 'values'. + array_item_name (str): The element name to use for each item in 'values'. + + Returns: + str: Serialized XML string. """ root = ET.Element(root_element_name) @@ -51,12 +61,16 @@ def serialize_list_to_xml(value, root_element_name, array_item_name): return ET.tostring(root).decode() @staticmethod - def serialize_dict_to_xml(value, root_element_name): - """Serializes a given dict of values to an xml document. + @validate_call + def serialize_dict_to_xml(value: Dict[str, Any], root_element_name: str) -> str: + """Serializes a given dictionary of values to an XML document. Args: - value (mixed): The dict to serialize. + value (Dict[str, Any]): The dictionary to serialize. root_element_name (str): The name of the document's root element. + + Returns: + str: Serialized XML string. """ root = ET.Element(root_element_name) @@ -65,47 +79,45 @@ def serialize_dict_to_xml(value, root_element_name): return ET.tostring(root).decode() @staticmethod - def add_to_element(element, value): - """Converts the given value to xml and adds it to an - existing xml.etree.Element. + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_to_element(element: ET.Element, value: Any) -> None: + """Converts the given value to XML and adds it to an existing ET.Element. Args: - element (xml.etree.Element): The xml tag to add the 'value' to. - value (mixed): The value to add to the element. + element (ET.Element): The XML tag to add the 'value' to. + value (Any): The value to add to the element. """ - # These classes can be cast directly. + if value is None: + return + if isinstance(value, bool): element.text = str(value).lower() - elif isinstance(value, (int, float, str, datetime.date, - ApiHelper.CustomDate)): + elif isinstance(value, (int, float, str, datetime.date, ApiHelper.CustomDate)): element.text = str(value) else: value.to_xml_sub_element(element) @staticmethod - def add_as_attribute(root, value, name): - """Sets an attribute on an xml.etree.Element instance if the value - isn't None. + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_as_attribute(root: ET.Element, value: Any, name: str) -> None: + """Sets an attribute on an ET.Element instance if the value isn't None. Args: - root (xml.etree.Element): The parent of this xml attribute. - value (mixed): The value to set to the attribute. - name (str): The name of attribute being set. + root (ET.Element): The parent of this XML attribute. + value (Any): The value to set to the attribute. + name (str): The name of the attribute being set. """ if value is not None: - if isinstance(value, bool): - root.set(name, str(value).lower()) - else: - root.set(name, str(value)) + root.set(name, str(value).lower() if isinstance(value, bool) else str(value)) @staticmethod - def add_as_subelement(root, value, name): - """Converts the given value to an xml.etree.Element if it is not None - and adds it to an existing xml.etree.Element. + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_as_subelement(root: ET.Element, value: Any, name: str) -> None: + """Converts the given value to an ET.Element if it is not None and adds it to an existing ET.Element. Args: - root (xml.etree.Element): The parent of this xml element. - value (mixed): The value to add to the element. + root (ET.Element): The parent of this XML element. + value (Any): The value to add to the element. name (str): The name of the element being added. """ if value is not None: @@ -113,45 +125,35 @@ def add_as_subelement(root, value, name): XmlHelper.add_to_element(tag, value) @staticmethod - def add_list_as_subelement(root, items, item_name, - wrapping_element_name=None): - """Converts the given list to an xml.etree.Element if it is not None - and adds it to an existing xml.etree.Element. + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_list_as_subelement(root: ET.Element, items: List[Any], item_name: str, + wrapping_element_name: Optional[str] = None) -> None: + """Converts the given list to an ET.Element if it is not None and adds it to an existing ET.Element. Args: - root (xml.etree.Element): The parent of this xml element. - items (list): The list of values to add to the element. + root (ET.Element): The parent of this XML element. + items (List[Any]): The list of values to add to the element. item_name (str): The element name to use for each item in 'items'. - wrapping_element_name (str): The element name to use for the - wrapping element, if needed. + wrapping_element_name (Optional[str]): The element name to use for the wrapping element, if needed. """ if items is not None: - if wrapping_element_name is not None: - parent = ET.SubElement(root, wrapping_element_name) - else: - parent = root - + parent = ET.SubElement(root, wrapping_element_name) if wrapping_element_name else root for item in items: sub_elem = ET.SubElement(parent, item_name) XmlHelper.add_to_element(sub_elem, item) @staticmethod - def add_dict_as_subelement(root, items, dictionary_name=None): - """Converts the given dict to an xml.etree.Element if it is not None - and adds it to an existing xml.etree.Element. + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_dict_as_subelement(root: ET.Element, items: Dict[str, Any], dictionary_name: Optional[str] = None) -> None: + """Converts the given dictionary to an ET.Element if it is not None and adds it to an existing ET.Element. Args: - root (xml.etree.Element): The parent of this xml element. - items (dict): The dict of values to add to the element. - dictionary_name (str): The element name to use for the - encapsulating element. + root (ET.Element): The parent of this XML element. + items (Dict[str, Any]): The dictionary of values to add to the element. + dictionary_name (Optional[str]): The element name to use for the encapsulating element. """ if items is not None: - if dictionary_name is not None: - parent = ET.SubElement(root, dictionary_name) - else: - parent = root - + parent = ET.SubElement(root, dictionary_name) if dictionary_name else root for key, value in items.items(): if isinstance(value, list): XmlHelper.add_list_as_subelement(parent, value, key) @@ -159,50 +161,53 @@ def add_dict_as_subelement(root, items, dictionary_name=None): XmlHelper.add_as_subelement(parent, value, key) @staticmethod - def deserialize_xml(xml, clazz): - """Deserializes an xml document to a python object of the type given - by 'clazz'. + @validate_call + def deserialize_xml(xml: Optional[str], clazz: Type[Any]) -> Optional[Any]: + """Deserializes an XML document to a Python object of the type given by 'clazz'. Args: - xml (str): An xml document to deserialize. - clazz (class): The class that the deserialized object should - belong to. + xml (Optional[str]): An XML document to deserialize. + clazz (Type[Any]): The class that the deserialized object should belong to. + + Returns: + Optional[Any]: An instance of the specified class, or None if input is empty. """ if not xml: return None root = ET.fromstring(xml) - return XmlHelper.value_from_xml_element(root, clazz) @staticmethod - def deserialize_xml_to_list(xml, item_name, clazz): - """Deserializes an xml document to a list of python objects, each of - the type given by 'clazz'. + @validate_call + def deserialize_xml_to_list(xml: Optional[str], item_name: str, clazz: Type[Any]) -> Optional[List[Any]]: + """Deserializes an XML document to a list of Python objects, each of the type given by 'clazz'. Args: - xml (str): An xml document to deserialize. - item_name (str): The name of the elements that need to be extracted - into a list. - clazz (class): The class that the deserialized object should - belong to. + xml (Optional[str]): An XML document to deserialize. + item_name (str): The name of the elements that need to be extracted into a list. + clazz (Type[Any]): The class that the deserialized objects should belong to. + + Returns: + Optional[List[Any]]: A list of instances of the specified class, or None if input is empty. """ if not xml: return None root = ET.fromstring(xml) - return XmlHelper.list_from_xml_element(root, item_name, clazz) @staticmethod - def deserialize_xml_to_dict(xml, clazz): - """Deserializes an xml document to a dictionary of python objects, each of - the type given by 'clazz'. + @validate_call + def deserialize_xml_to_dict(xml: Optional[str], clazz: Type[Any]) -> Optional[Dict[str, Any]]: + """Deserializes an XML document to a dictionary of Python objects, each of the type given by 'clazz'. Args: - xml (str): An xml document to deserialize. - clazz (class): The class that the values of the dictionary should - belong to. + xml (Optional[str]): An XML document to deserialize. + clazz (Type[Any]): The class that the dictionary values should belong to. + + Returns: + Optional[Dict[str, Any]]: A dictionary with deserialized objects, or None if input is empty. """ if not xml: return None @@ -211,12 +216,14 @@ def deserialize_xml_to_dict(xml, clazz): return XmlHelper.dict_from_xml_element(root, clazz) @staticmethod - def value_from_xml_attribute(attribute, clazz): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_xml_attribute(attribute: Optional[Union[ET.Element, str]], clazz: Type[Any]) -> Optional[Any]: """Extracts the value from an attribute and converts it to the type given by 'clazz'. Args: - clazz (class): The class that the deserialized object should + attribute (str): The XML attribute to extract the value from. + clazz (Type[Any]): The class that the deserialized object should belong to. """ if attribute is None: @@ -224,44 +231,51 @@ def value_from_xml_attribute(attribute, clazz): conversion_function = XmlHelper.converter(clazz) - return conversion_function(attribute) if conversion_function is not None else None + return conversion_function(attribute)\ + if conversion_function is not None and isinstance(attribute, str) else None @staticmethod - def value_from_xml_element(element, clazz): - """Extracts the value from an element and converts it to the type given - by 'clazz'. + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_xml_element(element: Optional[ET.Element], clazz: Type[Any]) -> Optional[Any]: + """Extracts the value from an element and converts it to the type given by 'clazz'. Args: - clazz (class): The class that the deserialized object should - belong to. + element (ET.Element): The XML element to extract the value from. + clazz (Type[Any]): The class that the deserialized object should belong to. + + Returns: + Optional[Any]: An instance of the specified class, or None if the element is None. """ if element is None: return None # These classes can be cast directly. - if clazz in [int, float, str, bool, datetime.date] or\ + if clazz in [int, float, str, bool, datetime.date] or \ issubclass(clazz, ApiHelper.CustomDate): conversion_function = XmlHelper.converter(clazz) - value = element.text - else: - conversion_function = clazz.from_element - value = element + return conversion_function(element.text)\ + if conversion_function is not None and element.text is not None else None + + conversion_function = clazz.from_element if hasattr(clazz, 'from_element') else None + + return conversion_function(element) if conversion_function is not None and element is not None else None + - return conversion_function(value) @staticmethod - def list_from_xml_element(root, item_name, clazz, wrapping_element_name=None): - """Deserializes an xml document to a list of python objects, each of - the type given by 'clazz'. + @validate_call(config=dict(arbitrary_types_allowed=True)) + def list_from_xml_element(root: Optional[ET.Element], item_name: str, clazz: Type[Any], + wrapping_element_name: Optional[str] = None) -> Optional[List[Any]]: + """Deserializes an XML document to a list of Python objects, each of the type given by 'clazz'. Args: - root (str): An xml document to deserialize. - item_name (str): The name of the elements that need to be extracted - into a list. - clazz (class): The class that the deserialized object should - belong to. - wrapping_element_name (str): The name of the wrapping element for - the xml element array. + root (ET.Element): The root XML element. + item_name (str): The name of the elements that need to be extracted into a list. + clazz (Type[Any]): The class that the deserialized objects should belong to. + wrapping_element_name (Optional[str]): The name of the wrapping element for the XML element array. + + Returns: + Optional[List[Any]]: A list of deserialized objects, or None if no matching elements are found. """ if root is None: return None @@ -271,62 +285,67 @@ def list_from_xml_element(root, item_name, clazz, wrapping_element_name=None): if elements is None: return None - return [XmlHelper.value_from_xml_element(element, clazz) for - element in elements] + return [XmlHelper.value_from_xml_element(element, clazz) for element in elements] @staticmethod - def dict_from_xml_element(element, clazz): - """Extracts the values from an element and converts them to a - dictionary with values of type 'clazz'. + @validate_call(config=dict(arbitrary_types_allowed=True)) + def dict_from_xml_element(element: Optional[ET.Element], clazz: Type[Any]) -> Optional[Dict[str, Any]]: + """Extracts the values from an element and converts them to a dictionary with values of type 'clazz'. Args: - clazz (class): The class that the entries of the dictionary should - belong to. + element (ET.Element): The XML element to convert. + clazz (Type[Any]): The class that the dictionary values should belong to. + + Returns: + Optional[Dict[str, Any]]: A dictionary of deserialized values, or None if the element is None. """ if element is None: return None entries = list(element) - conversion_function = XmlHelper.converter(clazz) - return {entry.tag: conversion_function(entry.text) for - entry in entries} + return { + entry.tag: conversion_function(entry.text) if entry.text is not None else None + for entry in entries if conversion_function + } @staticmethod - def converter(clazz): - """Provides the function to use for converting a string to the type - given by 'clazz'. + @validate_call + def converter(clazz: Type[Any]) -> Optional[Callable[[str], Any]]: + """Provides the function to use for converting a string to the type given by 'clazz'. Args: - clazz (class): The class to find the conversion function for. + clazz (Type[Any]): The class to find the conversion function for. + + Returns: + Callable[[str], Any]: A conversion function, or None if not applicable. """ - # These classes can be cast directly. if clazz in [int, float, str]: - def conversion_function(value): - return clazz(value) + return lambda value: clazz(value) elif clazz is bool: - def conversion_function(value): - return value.lower() == 'true' + return lambda value: value.lower() == 'true' elif clazz is datetime.date: - def conversion_function(value): - return dateutil.parser.parse(value).date() - # DateTime classes have their own method to convert from string. + return lambda value: dateutil.parser.parse(value).date() + # DateTime classes have their own method to convert from string. elif issubclass(clazz, ApiHelper.CustomDate): - def conversion_function(value): - return clazz.from_value(value) + return clazz.from_value - return conversion_function + return None @staticmethod - def value_from_one_of_xml_elements(root, mapping_data): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_one_of_xml_elements( + root: Optional[ET.Element], mapping_data: Optional[Dict[str, Any]] + ) -> Optional[Any]: """Extracts the value from an element and converts it to the type given by 'clazz'. Args: + root (ET.Element): The root XML element. mapping_data (dict): A dictionary mapping possible element names for a given field to corresponding types. """ - if not mapping_data: + if not mapping_data or root is None: return None for element_name, tup in mapping_data.items(): @@ -342,15 +361,17 @@ def value_from_one_of_xml_elements(root, mapping_data): element = root.find(element_name) if element is not None: return XmlHelper.value_from_xml_element(element, clazz) + return None @staticmethod - def list_from_multiple_one_of_xml_element(root, mapping_data): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def list_from_multiple_one_of_xml_element(root: ET.Element, mapping_data: Dict[str, Any]) -> Optional[List[Any]]: """Deserializes an xml document to a list of python objects where all types of oneof schemas are allowed (when the outer model is an array) Args: - root (str): An xml document to deserialize. + root (ET.Element): An xml document to deserialize. mapping_data (dict): A dictionary mapping possible element names for a given field to corresponding types. @@ -366,11 +387,24 @@ def list_from_multiple_one_of_xml_element(root, mapping_data): return None @staticmethod - def get_elements(root, wrapping_element_name, item_name): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def get_elements(root: Optional[ET.Element], wrapping_element_name: Optional[str], item_name: str) -> Optional[List[ET.Element]]: + """Retrieves a list of XML elements based on the specified wrapping element and item name. + + Args: + root (ET.Element): The root XML element. + wrapping_element_name (Optional[str]): The name of the wrapping element. + item_name (str): The name of the desired elements. + + Returns: + Optional[List[ET.Element]]: A list of matching elements, or None if not found. + """ + if root is None: + return None if wrapping_element_name is None: return root.findall(item_name) - elif root.find(wrapping_element_name) is None: + wrapping_element = root.find(wrapping_element_name) + if wrapping_element is None: return None - else: - return root.find(wrapping_element_name).findall(item_name) + return wrapping_element.findall(item_name) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..68d6768 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,8 @@ +[mypy] +check_untyped_defs = True + +[mypy-jsonpickle.*] +ignore_missing_imports = True + +[mypy-jsonpointer.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a4f7487..0730f7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ jsonpickle~=3.3.0 python-dateutil~=2.8 -apimatic-core-interfaces~=0.1.0 +git+https://github.com/apimatic/core-interfaces-python@typing-support requests~=2.31 -setuptools>=68.0.0 jsonpointer~=2.3 +typing~=3.7.4.3 +pydantic~=2.10.4 +setuptools~=70.0.0 +mypy~=1.0.1 diff --git a/tests/apimatic_core/api_call_tests/test_api_call.py b/tests/apimatic_core/api_call_tests/test_api_call.py index ee1abca..95a3e4d 100644 --- a/tests/apimatic_core/api_call_tests/test_api_call.py +++ b/tests/apimatic_core/api_call_tests/test_api_call.py @@ -1,10 +1,15 @@ import pytest -from apimatic_core.configurations.endpoint_configuration import EndpointConfiguration +from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration +from apimatic_core_interfaces.http.http_client import HttpClient + +from apimatic_core.api_call import ApiCall +from apimatic_core.configurations.global_configuration import GlobalConfiguration +from apimatic_core.http.http_callback import HttpCallBack from apimatic_core.request_builder import RequestBuilder from apimatic_core.response_handler import ResponseHandler from apimatic_core.types.parameter import Parameter from apimatic_core.utilities.api_helper import ApiHelper -from apimatic_core_interfaces.types.http_method_enum import HttpMethodEnum +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum from tests.apimatic_core.base import Base from tests.apimatic_core.mocks.callables.base_uri_callable import Server from tests.apimatic_core.mocks.models.person import Employee @@ -12,11 +17,11 @@ class TestApiCall(Base): - def setup_test(self, global_config): - self.global_config = global_config - self.http_response_catcher = self.global_config.get_http_client_configuration().http_callback - self.http_client = self.global_config.get_http_client_configuration().http_client - self.api_call_builder = self.new_api_call_builder(self.global_config) + def setup_test(self, global_config: GlobalConfiguration): + self.global_config: GlobalConfiguration = global_config + self.http_response_catcher: HttpCallBack = self.global_config.http_client_configuration.http_callback + self.http_client: HttpClient = self.global_config.http_client_configuration.http_client + self.api_call_builder: ApiCall = self.new_api_call_builder(self.global_config) def test_end_to_end_with_uninitialized_http_client(self): self.setup_test(self.default_global_configuration) @@ -25,49 +30,37 @@ def test_end_to_end_with_uninitialized_http_client(self): RequestBuilder().server(Server.DEFAULT) .path('/body/model') .http_method(HttpMethodEnum.POST) - .header_param(Parameter() - .key('Content-Type') - .value('application/json')) - .body_param(Parameter() - .value(Base.employee_model()) - .is_required(True)) - .header_param(Parameter() - .key('accept') - .value('application/json')) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) .body_serializer(ApiHelper.json_serialize) ).response( ResponseHandler() .is_nullify404(True) .deserializer(ApiHelper.json_deserialize) - .deserialize_into(Employee.from_dictionary) + .deserialize_into(Employee.model_validate) ).execute() assert exception.value.args[0] == 'An HTTP client instance is required to execute an Api call.' def test_end_to_end_with_uninitialized_http_callback(self): self.setup_test(self.global_configuration_without_http_callback) - actual_employee_model = self.api_call_builder.new_builder.request( + actual_employee_model: Employee = self.api_call_builder.new_builder.request( RequestBuilder().server(Server.DEFAULT) .path('/body/model') .http_method(HttpMethodEnum.POST) - .header_param(Parameter() - .key('Content-Type') - .value('application/json')) - .body_param(Parameter() - .value(Base.employee_model()) - .is_required(True)) - .header_param(Parameter() - .key('accept') - .value('application/json')) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) .body_serializer(ApiHelper.json_serialize) ).response( ResponseHandler() .is_nullify404(True) .deserializer(ApiHelper.json_deserialize) - .deserialize_into(Employee.from_dictionary) + .deserialize_into(Employee.model_validate) ).execute() - assert self.http_client._should_retry is None - assert self.http_client._contains_binary_response is None + assert self.http_client.should_retry is False + assert self.http_client.contains_binary_response is False assert self.http_response_catcher is None assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) @@ -78,50 +71,38 @@ def test_end_to_end_with_not_implemented_http_callback(self): RequestBuilder().server(Server.DEFAULT) .path('/body/model') .http_method(HttpMethodEnum.POST) - .header_param(Parameter() - .key('Content-Type') - .value('application/json')) - .body_param(Parameter() - .value(Base.employee_model()) - .is_required(True)) - .header_param(Parameter() - .key('accept') - .value('application/json')) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) .body_serializer(ApiHelper.json_serialize) ).response( ResponseHandler() .is_nullify404(True) .deserializer(ApiHelper.json_deserialize) - .deserialize_into(Employee.from_dictionary) + .deserialize_into(Employee.model_validate) ).execute() assert not_implemented_exception.value.args[0] == 'This method has not been implemented.' def test_end_to_end_without_endpoint_configurations(self): self.setup_test(self.global_configuration) - actual_employee_model = self.api_call_builder.new_builder.request( + actual_employee_model: Employee = self.api_call_builder.new_builder.request( RequestBuilder().server(Server.DEFAULT) .path('/body/model') .http_method(HttpMethodEnum.POST) - .header_param(Parameter() - .key('Content-Type') - .value('application/json')) - .body_param(Parameter() - .value(Base.employee_model()) - .is_required(True)) - .header_param(Parameter() - .key('accept') - .value('application/json')) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) .body_serializer(ApiHelper.json_serialize) ).response( ResponseHandler() .is_nullify404(True) .deserializer(ApiHelper.json_deserialize) - .deserialize_into(Employee.from_dictionary) + .deserialize_into(Employee.model_validate) ).execute() - assert self.http_client._should_retry is None - assert self.http_client._contains_binary_response is None + assert self.http_client.should_retry is False + assert self.http_client.contains_binary_response is False assert self.http_response_catcher.response.status_code == 200 assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) @@ -136,32 +117,24 @@ def test_end_to_end_without_endpoint_configurations(self): def test_end_to_end_with_endpoint_configurations(self, input_to_retry, input_contains_binary_response, expected_to_retry, expected_contains_binary_response): self.setup_test(self.global_configuration) - actual_employee_model = self.api_call_builder.new_builder.request( + actual_employee_model: Employee = self.api_call_builder.new_builder.request( RequestBuilder().server(Server.DEFAULT) .path('/body/model') .http_method(HttpMethodEnum.POST) - .header_param(Parameter() - .key('Content-Type') - .value('application/json')) - .body_param(Parameter() - .value(Base.employee_model()) - .is_required(True)) - .header_param(Parameter() - .key('accept') - .value('application/json')) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) .body_serializer(ApiHelper.json_serialize) ).response( ResponseHandler() .is_nullify404(True) .deserializer(ApiHelper.json_deserialize) - .deserialize_into(Employee.from_dictionary) + .deserialize_into(Employee.model_validate) ).endpoint_configuration( - EndpointConfiguration() - .to_retry(input_to_retry) - .has_binary_response(input_contains_binary_response) + EndpointConfiguration(should_retry=input_to_retry, has_binary_response=input_contains_binary_response) ).execute() - assert self.http_client._should_retry == expected_to_retry - assert self.http_client._contains_binary_response == expected_contains_binary_response + assert self.http_client.should_retry == expected_to_retry + assert self.http_client.contains_binary_response == expected_contains_binary_response assert self.http_response_catcher.response.status_code == 200 assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) diff --git a/tests/apimatic_core/api_logger_tests/test_api_logger.py b/tests/apimatic_core/api_logger_tests/test_api_logger.py index fad8df3..0f1257a 100644 --- a/tests/apimatic_core/api_logger_tests/test_api_logger.py +++ b/tests/apimatic_core/api_logger_tests/test_api_logger.py @@ -1,7 +1,17 @@ import logging +from logging import LogRecord +from typing import Any, Iterable, Callable, List + import pytest -from apimatic_core_interfaces.types.http_method_enum import HttpMethodEnum +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from apimatic_core_interfaces.logger.api_logger import ApiLogger +from apimatic_core_interfaces.logger.logger import Logger + +from apimatic_core.logger.configuration.api_logging_configuration import ApiRequestLoggingConfiguration, \ + ApiResponseLoggingConfiguration from apimatic_core.logger.sdk_logger import SdkLogger from apimatic_core.request_builder import RequestBuilder from apimatic_core.response_handler import ResponseHandler @@ -9,7 +19,7 @@ from apimatic_core.utilities.api_helper import ApiHelper from tests.apimatic_core.base import Base from tests.apimatic_core.mocks.callables.base_uri_callable import Server -from tests.apimatic_core.mocks.logger.api_logger import ApiLogger +from tests.apimatic_core.mocks.logger.api_logger import ApiLogger as MockedApiLogger from testfixtures import LogCapture from tests.apimatic_core.mocks.models.person import Employee @@ -18,15 +28,16 @@ class TestApiLogger(Base): @pytest.fixture - def log_capture(self): + def log_capture(self) -> LogCapture: """Fixture to capture logs during the test.""" with LogCapture() as capture: yield capture - def init_sdk_logger(self, logger, log_level=logging.INFO, mask_sensitive_headers=True, - request_logging_configuration=None, - response_logging_configuration=None): - self._api_request = self.request( + def init_sdk_logger( + self, logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, + request_logging_configuration: ApiRequestLoggingConfiguration=None, + response_logging_configuration: ApiResponseLoggingConfiguration=None): + self._api_request: HttpRequest = self.request( http_method=HttpMethodEnum.POST, query_url='http://localhost:3000/body/model?key=value', headers={ @@ -34,26 +45,28 @@ def init_sdk_logger(self, logger, log_level=logging.INFO, mask_sensitive_headers 'Accept': 'application/json'}, parameters=ApiHelper.json_serialize({"Key": "Value"}) ) - self._api_response = self.response( + self._api_response: HttpResponse = self.response( text=ApiHelper.json_serialize({"Key": "Value"}), headers={ 'Content-Type': 'application/json', 'Content-Length': 50 } ) - self._api_logger = self.get_api_logger(logger, log_level, mask_sensitive_headers, request_logging_configuration, - response_logging_configuration) + self._api_logger: ApiLogger = self.get_api_logger( + logger, log_level, mask_sensitive_headers, request_logging_configuration, response_logging_configuration) - def get_api_logger(self, logger, log_level=logging.INFO, mask_sensitive_headers=True, - request_logging_configuration=None, - response_logging_configuration=None): + def get_api_logger( + self, logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, + request_logging_configuration: ApiRequestLoggingConfiguration=None, + response_logging_configuration: ApiResponseLoggingConfiguration=None + ) -> ApiLogger: return SdkLogger(self.api_logging_configuration( logger, log_level=log_level, mask_sensitive_headers=mask_sensitive_headers, request_logging_configuration=request_logging_configuration, response_logging_configuration=response_logging_configuration)) - def test_custom_logger_for_request(self, log_capture): - self.init_sdk_logger(logger=ApiLogger()) + def test_custom_logger_for_request(self, log_capture: LogCapture): + self.init_sdk_logger(logger=MockedApiLogger()) self._api_logger.log_request(self._api_request) # Assert the captured logs records = log_capture.records @@ -65,8 +78,8 @@ def test_custom_logger_for_request(self, log_capture): max_checks=1) assert all(record.levelname == "INFO" for record in records) - def test_custom_logger_for_request_with_log_level(self, log_capture): - self.init_sdk_logger(logger=ApiLogger(), log_level=logging.WARN) + def test_custom_logger_for_request_with_log_level(self, log_capture: LogCapture): + self.init_sdk_logger(logger=MockedApiLogger(), log_level=logging.WARN) self._api_logger.log_request(self._api_request) # Assert the captured logs records = log_capture.records @@ -78,8 +91,8 @@ def test_custom_logger_for_request_with_log_level(self, log_capture): max_checks=1) assert all(record.levelname == "WARNING" for record in records) - def test_custom_logger_for_request_with_include_query_in_path(self, log_capture): - self.init_sdk_logger(logger=ApiLogger(), + def test_custom_logger_for_request_with_include_query_in_path(self, log_capture: LogCapture): + self.init_sdk_logger(logger=MockedApiLogger(), request_logging_configuration=self.api_request_logging_configuration( include_query_in_path=True)) self._api_logger.log_request(self._api_request) @@ -93,9 +106,10 @@ def test_custom_logger_for_request_with_include_query_in_path(self, log_capture) max_checks=1) assert all(record.levelname == "INFO" for record in records) - def test_custom_logger_for_request_with_log_request_header(self, log_capture): - self.init_sdk_logger(logger=ApiLogger(), - request_logging_configuration=self.api_request_logging_configuration(log_headers=True)) + def test_custom_logger_for_request_with_log_request_header(self, log_capture: LogCapture): + self.init_sdk_logger( + logger=MockedApiLogger(), + request_logging_configuration=self.api_request_logging_configuration(log_headers=True)) self._api_logger.log_request(self._api_request) # Assert the captured logs @@ -114,8 +128,8 @@ def test_custom_logger_for_request_with_log_request_header(self, log_capture): max_checks=1) assert all(record.levelname == "INFO" for record in records) - def test_custom_logger_for_request_with_log_request_body(self, log_capture): - self.init_sdk_logger(logger=ApiLogger(), + def test_custom_logger_for_request_with_log_request_body(self, log_capture: LogCapture): + self.init_sdk_logger(logger=MockedApiLogger(), request_logging_configuration=self.api_request_logging_configuration(log_body=True)) self._api_logger.log_request(self._api_request) @@ -135,8 +149,8 @@ def test_custom_logger_for_request_with_log_request_body(self, log_capture): max_checks=1) assert all(record.levelname == "INFO" for record in records) - def test_custom_logger_for_response(self, log_capture): - self.init_sdk_logger(logger=ApiLogger()) + def test_custom_logger_for_response(self, log_capture: LogCapture): + self.init_sdk_logger(logger=MockedApiLogger()) self._api_logger.log_response(self._api_response) # Assert the captured logs records = log_capture.records @@ -148,8 +162,8 @@ def test_custom_logger_for_response(self, log_capture): max_checks=1) assert all(record.levelname == "INFO" for record in records) - def test_custom_logger_for_response_with_log_level(self, log_capture): - self.init_sdk_logger(logger=ApiLogger(), log_level=logging.WARN) + def test_custom_logger_for_response_with_log_level(self, log_capture: LogCapture): + self.init_sdk_logger(logger=MockedApiLogger(), log_level=logging.WARN) self._api_logger.log_response(self._api_response) # Assert the captured logs records = log_capture.records @@ -161,9 +175,10 @@ def test_custom_logger_for_response_with_log_level(self, log_capture): max_checks=1) assert all(record.levelname == "WARNING" for record in records) - def test_custom_logger_for_response_with_log_response_header(self, log_capture): - self.init_sdk_logger(logger=ApiLogger(), - response_logging_configuration=self.api_response_logging_configuration(log_headers=True)) + def test_custom_logger_for_response_with_log_response_header(self, log_capture: LogCapture): + self.init_sdk_logger( + logger=MockedApiLogger(), + response_logging_configuration=self.api_response_logging_configuration(log_headers=True)) self._api_logger.log_response(self._api_response) # Assert the captured logs @@ -182,8 +197,8 @@ def test_custom_logger_for_response_with_log_response_header(self, log_capture): max_checks=1) assert all(record.levelname == "INFO" for record in records) - def test_custom_logger_for_response_with_log_response_body(self, log_capture): - self.init_sdk_logger(logger=ApiLogger(), + def test_custom_logger_for_response_with_log_response_body(self, log_capture: LogCapture): + self.init_sdk_logger(logger=MockedApiLogger(), response_logging_configuration=self.api_response_logging_configuration(log_body=True)) self._api_logger.log_response(self._api_response) @@ -203,8 +218,8 @@ def test_custom_logger_for_response_with_log_response_body(self, log_capture): max_checks=1) assert all(record.levelname == "INFO" for record in records) - def test_custom_logger_for_end_to_end_api_call_test(self, log_capture): - self.execute_api_call_with_logging(logger=ApiLogger()) + def test_custom_logger_for_end_to_end_api_call_test(self, log_capture: LogCapture): + self.execute_api_call_with_logging(logger=MockedApiLogger()) # Assert the captured logs records = log_capture.records @@ -222,31 +237,25 @@ def test_custom_logger_for_end_to_end_api_call_test(self, log_capture): max_checks=1) assert all(record.levelname == "INFO" for record in records) - def execute_api_call_with_logging(self, logger): + def execute_api_call_with_logging(self, logger: Logger): _api_call_builder = self.new_api_call_builder(self.global_configuration_with_logging(logger)) _api_call_builder.new_builder \ .request(RequestBuilder().server(Server.DEFAULT) .path('/body/model') .http_method(HttpMethodEnum.POST) - .header_param(Parameter() - .key('Content-Type') - .value('application/json')) - .body_param(Parameter() - .value({"Key": "Value"}) - .is_required(True)) - .query_param(Parameter() - .key('accept') - .value('application/json')) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .query_param(Parameter(key='accept', value='application/json')) .body_serializer(ApiHelper.json_serialize) ) \ .response(ResponseHandler() .is_nullify404(True) .deserializer(ApiHelper.json_deserialize) - .deserialize_into(Employee.from_dictionary)) \ + .deserialize_into(Employee.model_validate)) \ .execute() @staticmethod - def any_with_limit(iterable, condition, max_checks): + def any_with_limit(iterable: Iterable, condition: Callable[[List[LogRecord]], bool], max_checks: int): """Checks if any element in iterable meets the condition, with a limit on checks. Args: diff --git a/tests/apimatic_core/api_logger_tests/test_logging_configuration.py b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py index 090c4df..3e5eac8 100644 --- a/tests/apimatic_core/api_logger_tests/test_logging_configuration.py +++ b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py @@ -1,10 +1,12 @@ import pytest +from typing import Dict, List, Any + from apimatic_core.logger.configuration.api_logging_configuration import ApiRequestLoggingConfiguration class TestLogHelper: @staticmethod - def create_sample_headers(): + def create_sample_headers() -> Dict[str, str]: return { "Accept": "application/json", "Authorization": "Bearer token", @@ -13,155 +15,166 @@ def create_sample_headers(): } @staticmethod - def create_sample_request_logging_configuration(headers_to_include=None, headers_to_exclude=None, - headers_to_unmask=None): + def create_sample_request_logging_configuration( + headers_to_include: List[str]=None, headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None + ) -> ApiRequestLoggingConfiguration: return ApiRequestLoggingConfiguration(log_body=False, log_headers=False, include_query_in_path=False, headers_to_include=headers_to_include or [], headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or []) def test_get_headers_to_log_include(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_include=["Authorization"]) - result = logging_config.get_loggable_headers(headers, False) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() def test_get_headers_to_log_exclude(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_exclude=["Authorization"]) - result = logging_config.get_loggable_headers(headers, False) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() def test_get_headers_to_log_include_and_exclude(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_include=["Authorization"], - headers_to_exclude=["Accept"]) - result = logging_config.get_loggable_headers(headers, False) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"], headers_to_exclude=["Accept"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() def test_get_headers_to_log_sensitive_masking_enabled(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration() - result = logging_config.get_loggable_headers(headers, True) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) assert "Authorization" in result.keys() assert "**Redacted**" == result.get("Authorization") def test_get_headers_to_log_sensitive_masking_disabled(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration() - result = logging_config.get_loggable_headers(headers, False) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") def test_get_headers_to_log_sensitive_masking_disabled_with_headers_to_unmask(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_unmask=["Authorization"]) - result = logging_config.get_loggable_headers(headers, False) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") def test_get_headers_to_log_sensitive_masking_enabled_with_headers_to_unmask(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_unmask=["Authorization"]) - result = logging_config.get_loggable_headers(headers, True) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") def test_extract_headers_to_log_include(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_include=["Authorization"]) - result = logging_config._extract_headers_to_log(headers) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() def test_extract_headers_to_log_exclude(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_exclude=["Authorization"]) - result = logging_config._extract_headers_to_log(headers) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() def test_extract_headers_to_log_no_criteria(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration() - result = logging_config._extract_headers_to_log(headers) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) assert "Authorization" in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() def test_mask_sensitive_headers_masking_enabled(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration() - result = logging_config._mask_sensitive_headers(headers, True) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) assert "Authorization" in result.keys() assert "**Redacted**" == result.get("Authorization") def test_mask_sensitive_headers_masking_enabled_with_headers_to_unmask(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_unmask=["Authorization"]) - result = logging_config._mask_sensitive_headers(headers, True) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") def test_mask_sensitive_headers_masking_disable(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration() - result = logging_config._mask_sensitive_headers(headers, False) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") def test_mask_sensitive_headers_masking_disabled_with_headers_to_unmask(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_unmask=["Authorization"]) - result = logging_config._mask_sensitive_headers(headers, False) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") def test_filter_included_headers(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_include=["Authorization"]) - result = logging_config._filter_included_headers(headers) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config._filter_included_headers(headers) assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() def test_filter_included_headers_no_inclusion(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration() - result = logging_config._filter_included_headers(headers) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._filter_included_headers(headers) assert "Authorization" not in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() def test_filter_excluded_headers(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration(headers_to_exclude=["Authorization"]) - result = logging_config._filter_excluded_headers(headers) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() def test_filter_excluded_headers_no_exclusion(self): - headers = self.create_sample_headers() - logging_config = self.create_sample_request_logging_configuration() - result = logging_config._filter_excluded_headers(headers) + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) assert "Authorization" in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() diff --git a/tests/apimatic_core/base.py b/tests/apimatic_core/base.py index 425eb85..dae7bb7 100644 --- a/tests/apimatic_core/base.py +++ b/tests/apimatic_core/base.py @@ -1,25 +1,33 @@ +import json import logging import os import platform from datetime import datetime, date +from apimatic_core_interfaces.http.http_client import HttpClient +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from typing import Optional, Union, Dict, IO, Any, List + +from apimatic_core_interfaces.logger.logger import Logger + from apimatic_core.api_call import ApiCall from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration from apimatic_core.http.http_callback import HttpCallBack from apimatic_core.logger.configuration.api_logging_configuration import ApiLoggingConfiguration, \ ApiRequestLoggingConfiguration, ApiResponseLoggingConfiguration from apimatic_core.utilities.api_helper import ApiHelper -from apimatic_core_interfaces.types.http_method_enum import HttpMethodEnum +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum from apimatic_core.configurations.global_configuration import GlobalConfiguration -from apimatic_core.http.request.http_request import HttpRequest -from apimatic_core.http.response.http_response import HttpResponse from apimatic_core.request_builder import RequestBuilder from apimatic_core.response_handler import ResponseHandler -from apimatic_core.types.error_case import ErrorCase -from tests.apimatic_core.mocks.authentications.basic_auth import BasicAuth -from tests.apimatic_core.mocks.authentications.bearer_auth import BearerAuth -from tests.apimatic_core.mocks.authentications.custom_header_authentication import CustomHeaderAuthentication -from tests.apimatic_core.mocks.authentications.custom_query_authentication import CustomQueryAuthentication +from apimatic_core.types.error_case import ErrorCase, MessageType +from tests.apimatic_core.mocks.authentications.basic_auth import BasicAuth, BasicAuthCredentials +from tests.apimatic_core.mocks.authentications.bearer_auth import BearerAuth, BearerAuthCredentials +from tests.apimatic_core.mocks.authentications.custom_header_authentication import CustomHeaderAuthentication, \ + CustomHeaderAuthenticationCredentials +from tests.apimatic_core.mocks.authentications.custom_query_authentication import CustomQueryAuthentication, \ + CustomQueryAuthenticationCredentials from tests.apimatic_core.mocks.exceptions.global_test_exception import GlobalTestException from tests.apimatic_core.mocks.exceptions.nested_model_exception import NestedModelException from tests.apimatic_core.mocks.http.http_response_catcher import HttpResponseCatcher @@ -40,120 +48,227 @@ class Base: @staticmethod - def employee_model(): - return Employee(name='Bob', uid=1234567, address='street abc', department='IT', birthday=str(date(1994, 2, 13)), - birthtime=datetime(1994, 2, 13, 5, 30, 15), age=27, - additional_properties={'key1': 'value1', 'key2': 'value2'}, - hired_at=datetime(1994, 2, 13, 5, 30, 15), joining_day=Days.MONDAY, - working_days=[Days.MONDAY, Days.TUESDAY], salary=30000, - dependents=[Person(name='John', - uid=7654321, - address='street abc', - birthday=str(date(1994, 2, 13)), - birthtime=datetime(1994, 2, 13, 5, 30, 15), - age=12, - additional_properties={'key1': 'value1', 'key2': 'value2'})]) + def employee_model() -> Employee: + return Employee( + name='Bob', uid='1234567', address='street abc', department='IT', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=27, + additional_properties={'key1': 'value1', 'key2': 'value2'}, + hired_at=datetime(1994, 2, 13, 5, 30, 15), joining_day=Days.MONDAY, + working_days=[Days.MONDAY, Days.TUESDAY], salary=30000, + dependents=[ + Person(name='John', uid='7654321', address='street abc', birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=12, + additional_properties={ + 'key1': 'value1', + 'key2': 'value2' + }, person_type="Per")], + person_type="Empl") + + @staticmethod + def employee_model_str(beautify_with_spaces: bool=True) -> Union[Employee, str]: + if beautify_with_spaces: + return ('{{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": "{1}", "name": "Bob",' + ' "uid": "1234567", "personType": "Empl", "department": "IT", "dependents": [{{"address": "street abc",' + ' "age": 12, "birthday": "1994-02-13", "birthtime": "{1}", "name": "John", "uid": "7654321",' + ' "personType": "Per", "key1": "value1", "key2": "value2"}}], "hiredAt": "{0}", "joiningDay": "Monday",' + ' "salary": 30000, "workingDays": ["Monday", "Tuesday"], "key1": "value1", "key2": "value2"}}').format( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) + + return ('{{"address":"street abc","age":27,"birthday":"1994-02-13","birthtime":"{1}","name":"Bob",' + '"uid":"1234567","personType":"Empl","department":"IT","dependents":[{{"address":"street abc",' + '"age":12,"birthday":"1994-02-13","birthtime":"{1}","name":"John","uid":"7654321",' + '"personType":"Per","key1":"value1","key2":"value2"}}],"hiredAt":"{0}","joiningDay":"Monday",' + '"salary":30000,"workingDays":["Monday","Tuesday"],"key1":"value1","key2":"value2"}}').format( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) + + @staticmethod + def employee_model_additional_dictionary() -> Employee: + return Employee( + name='Bob', + uid='1234567', + address='street abc', + department='IT', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), + age=27, + additional_properties={ + 'key1': 'value1', + 'key2': 'value2' + }, + hired_at=datetime(1994, 2, 13, 5, 30, 15), + joining_day=Days.MONDAY, + working_days=[ + Days.MONDAY, + Days.TUESDAY + ], + salary=30000, + dependents=[ + Person( + name='John', + uid='7654321', + address='street abc', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=12, + additional_properties={ + 'key1': { + 'inner_key1': 'inner_val1', + 'inner_key2': 'inner_val2' + }, + 'key2': ['value2', 'value3'] + }, + person_type="Per") + ], + person_type="Empl") + + @staticmethod + def get_employee_dictionary() -> Dict[str, Employee]: + return { + "address": "street abc", + "age": 27, + "birthday": "1994-02-13", + "birthtime": Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + "name": "Bob", + "uid": '1234567', + "personType": "Empl", + "department": "IT", + "dependents": [ + { + "address": "street abc", + "age": 12, + "birthday": "1994-02-13", + "birthtime": Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + "name": "John", + "uid": '7654321', + "personType": "Per", + "key1": "value1", + "key2": "value2" + } + ], + "hiredAt": Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + "joiningDay": "Monday", + "salary": 30000, + "workingDays": ["Monday", "Tuesday"], + "key1": "value1", + "key2": "value2" + } + + @staticmethod + def get_complex_type() -> ComplexType: + inner_complex_type = InnerComplexType( + boolean_type=True, long_type=100003, string_type='abc', precision_type=55.44, + string_list_type=['item1', 'item2'], additional_properties={'key0': 'abc', 'key1': 400}) + + return ComplexType( + inner_complex_type=inner_complex_type, inner_complex_list_type=[inner_complex_type, inner_complex_type], + inner_complex_list_of_map_type=[{'key0': inner_complex_type, 'key1': inner_complex_type}], + inner_complex_map_type={'key0': inner_complex_type, 'key1': inner_complex_type}, + inner_complex_map_of_list_type={'key0': [inner_complex_type, inner_complex_type], + 'key2': [inner_complex_type, inner_complex_type]}, + additional_properties={'prop1': [1, 2, 3], 'prop2': {'key0': 'abc', 'key1': 'def'}}) @staticmethod - def employee_model_additional_dictionary(): - return Employee(name='Bob', uid=1234567, address='street abc', department='IT', birthday=str(date(1994, 2, 13)), - birthtime=datetime(1994, 2, 13, 5, 30, 15), age=27, - additional_properties={'key1': 'value1', 'key2': 'value2'}, - hired_at=datetime(1994, 2, 13, 5, 30, 15), joining_day=Days.MONDAY, - working_days=[Days.MONDAY, Days.TUESDAY], salary=30000, - dependents=[Person(name='John', - uid=7654321, - address='street abc', - birthday=str(date(1994, 2, 13)), - birthtime=datetime(1994, 2, 13, 5, 30, 15), - age=12, - additional_properties={ - 'key1': {'inner_key1': 'inner_val1', 'inner_key2': 'inner_val2'}, - 'key2': ['value2', 'value3']})]) + def get_union_type_scalar_model() -> UnionTypeScalarModel: + return UnionTypeScalarModel( + any_of_required=1.5, + one_of_req_nullable='abc', + one_of_optional=200, + any_of_opt_nullable=True + ) @staticmethod - def get_employee_dictionary(): - return {"address": "street abc", "age": 27, "birthday": "1994-02-13", - "birthtime": Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - "department": "IT", "dependents": [{"address": "street abc", "age": 12, "birthday": "1994-02-13", - "birthtime": Base.get_rfc3339_datetime( - datetime(1994, 2, 13, 5, 30, 15)), - "name": "John", "uid": 7654321, "personType": "Per", - "key1": "value1", "key2": "value2"}], - "hiredAt": Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), - "joiningDay": "Monday", "name": "Bob", "salary": 30000, "uid": 1234567, - "workingDays": ["Monday", "Tuesday"], "personType": "Empl"} + def get_serialized_employee() -> str: + employee_dictionary = Base.get_employee_dictionary() + return json.dumps(employee_dictionary, separators=(',', ':')) @staticmethod - def basic_auth(): - return BasicAuth(basic_auth_user_name='test_username', basic_auth_password='test_password') + def basic_auth() -> BasicAuth: + return BasicAuth(BasicAuthCredentials(username='test_username', password='test_password')) @staticmethod - def bearer_auth(): - return BearerAuth(access_token='0b79bab50daca910b000d4f1a2b675d604257e42') + def bearer_auth() -> BearerAuth: + return BearerAuth(BearerAuthCredentials(access_token='0b79bab50daca910b000d4f1a2b675d604257e42')) @staticmethod - def custom_header_auth(): - return CustomHeaderAuthentication(token='Qaws2W233WedeRe4T56G6Vref2') + def custom_header_auth() -> CustomHeaderAuthentication: + return CustomHeaderAuthentication(CustomHeaderAuthenticationCredentials(token='Qaws2W233WedeRe4T56G6Vref2')) @staticmethod - def custom_query_auth(): - return CustomQueryAuthentication(api_key='W233WedeRe4T56G6Vref2', token='Qaws2W233WedeRe4T56G6Vref2') + def custom_query_auth() -> CustomQueryAuthentication: + return CustomQueryAuthentication( + CustomQueryAuthenticationCredentials(api_key='W233WedeRe4T56G6Vref2', token='Qaws2W233WedeRe4T56G6Vref2')) @staticmethod - def xml_model(): + def xml_model() -> XMLModel: + model = XMLModel(string_attr='String', number_attr=10000, boolean_attr=False, + string_element='Hey! I am being tested.', number_element=5000, + boolean_element=False, elements=['a', 'b', 'c']) return XMLModel(string_attr='String', number_attr=10000, boolean_attr=False, string_element='Hey! I am being tested.', number_element=5000, boolean_element=False, elements=['a', 'b', 'c']) @staticmethod - def one_of_xml_dog_model(): + def one_of_xml_dog_model() -> OneOfXML: return OneOfXML(value=DogModel(barks=True)) @staticmethod - def one_of_xml_cat_model(): - return OneOfXML(value=[CatModel(meows=True), CatModel(meows=False)]) + def one_of_xml_cat_model() -> OneOfXML: + return OneOfXML(value=[ + CatModel(meows=True), + CatModel(meows=False) + ]) @staticmethod - def one_of_xml_wolf_model(): - return OneOfXML(value=[WolfModel(howls=True), WolfModel(howls=False)]) + def one_of_xml_wolf_model() -> OneOfXML: + return OneOfXML(value=[ + WolfModel(howls=True), + WolfModel(howls=False) + ]) @staticmethod - def read_file(file_name): + def read_file(file_name) -> IO: real_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) file_path = os.path.join(real_path, 'apimatic_core', 'mocks/files', file_name) return open(file_path, "rb") @staticmethod - def global_errors(): + def global_errors() -> Dict[str, ErrorCase]: return { - '400': ErrorCase().error_message('400 Global').exception_type(GlobalTestException), - '412': ErrorCase().error_message('Precondition Failed').exception_type(NestedModelException), - '3XX': ErrorCase().error_message('3XX Global').exception_type(GlobalTestException), - 'default': ErrorCase().error_message('Invalid response').exception_type(GlobalTestException), + '400': ErrorCase(message='400 Global', exception_type=GlobalTestException), + '412': ErrorCase(message='Precondition Failed', exception_type=NestedModelException), + '3XX': ErrorCase(message='3XX Global', exception_type=GlobalTestException), + 'default': ErrorCase(message='Invalid response', exception_type=GlobalTestException), } @staticmethod - def global_errors_with_template_message(): + def global_errors_with_template_message() -> Dict[str, ErrorCase]: return { - '400': ErrorCase() - .error_message_template('error_code => {$statusCode}, header => {$response.header.accept}, ' - 'body => {$response.body#/ServerCode} - {$response.body#/ServerMessage}') - .exception_type(GlobalTestException), - '412': ErrorCase() - .error_message_template('global error message -> error_code => {$statusCode}, header => ' - '{$response.header.accept}, body => {$response.body#/ServerCode} - ' - '{$response.body#/ServerMessage} - {$response.body#/model/name}') - .exception_type(NestedModelException) + '400': ErrorCase( + message='error_code => {$statusCode}, header => {$response.header.accept}, ' + 'body => {$response.body#/ServerCode} - {$response.body#/ServerMessage}', + message_type=MessageType.TEMPLATE, exception_type=GlobalTestException), + '412': ErrorCase( + message='global error message -> error_code => {$statusCode}, header => ' + '{$response.header.accept}, body => {$response.body#/ServerCode} - ' + '{$response.body#/ServerMessage} - {$response.body#/model/name}', + message_type=MessageType.TEMPLATE, exception_type=NestedModelException) } @staticmethod - def request(http_method=HttpMethodEnum.GET, - query_url='http://localhost:3000/test', - headers=None, - query_parameters=None, - parameters=None, - files=None): + def request(http_method: HttpMethodEnum=HttpMethodEnum.GET, + query_url: str='http://localhost:3000/test', + headers: Dict[str, Any]=None, + query_parameters: Dict[str, Any]=None, + parameters: Any=None, + files: Any=None) -> HttpRequest: + if headers is None: + headers = {} + if query_parameters is None: + query_parameters = {} + return HttpRequest(http_method=http_method, query_url=query_url, headers=headers, @@ -162,32 +277,42 @@ def request(http_method=HttpMethodEnum.GET, files=files) @staticmethod - def response(status_code=200, reason_phrase=None, headers=None, text=None): - return HttpResponse(status_code=status_code, reason_phrase=reason_phrase, - headers=headers, text=text, request=Base.request()) + def response( + status_code: int=200, reason_phrase: Optional[str]=None, headers: Dict[str, Any]=None, text: Any=None + ) -> HttpResponse: + if headers is None: + headers = {} + return HttpResponse( + status_code=status_code, reason_phrase=reason_phrase, + headers=headers, text=text, request=Base.request() + ) @staticmethod - def get_http_datetime(datetime_value, should_return_string=True): + def get_http_datetime( + datetime_value: datetime, should_return_string: bool=True + ) -> Union[ApiHelper.CustomDate, str]: if should_return_string is True: return ApiHelper.HttpDateTime.from_datetime(datetime_value) return ApiHelper.HttpDateTime(datetime_value) @staticmethod - def get_rfc3339_datetime(datetime_value, should_return_string=True): + def get_rfc3339_datetime( + datetime_value: datetime, should_return_string: bool=True + ) -> Union[ApiHelper.CustomDate, str]: if should_return_string is True: return ApiHelper.RFC3339DateTime.from_datetime(datetime_value) return ApiHelper.RFC3339DateTime(datetime_value) @staticmethod - def new_api_call_builder(global_configuration): + def new_api_call_builder(global_configuration: GlobalConfiguration) -> ApiCall: return ApiCall(global_configuration) @staticmethod - def user_agent(): + def user_agent() -> str: return 'Python|31.8.0|{engine}|{engine-version}|{os-info}' @staticmethod - def user_agent_parameters(): + def user_agent_parameters() -> Dict[str, Dict[str, Any]]: return { 'engine': {'value': platform.python_implementation(), 'encode': False}, 'engine-version': {'value': "", 'encode': False}, @@ -195,132 +320,133 @@ def user_agent_parameters(): } @staticmethod - def wrapped_parameters(): + def wrapped_parameters() -> Dict[str, Any]: return { 'bodyScalar': True, 'bodyNonScalar': Base.employee_model(), } @staticmethod - def mocked_http_client(): + def mocked_http_client() -> HttpClient: return MockHttpClient() @staticmethod - def http_client_configuration(http_callback=HttpResponseCatcher(), logging_configuration=None): - http_client_configurations = HttpClientConfiguration(http_call_back=http_callback, - logging_configuration=logging_configuration) - http_client_configurations.set_http_client(Base.mocked_http_client()) - return http_client_configurations + def http_client_configuration( + http_callback: Optional[HttpCallBack]=HttpResponseCatcher(), + logging_configuration: Optional[ApiLoggingConfiguration]=None + ) -> HttpClientConfiguration: + return HttpClientConfiguration( + http_callback=http_callback, logging_configuration=logging_configuration, + http_client=Base.mocked_http_client() + ) @property - def new_request_builder(self): + def new_request_builder(self) -> RequestBuilder: return RequestBuilder().path('/test') \ .server(Server.DEFAULT) @property - def new_response_handler(self): + def new_response_handler(self) -> ResponseHandler: return ResponseHandler() @property - def global_configuration(self): - return GlobalConfiguration(self.http_client_configuration()) \ - .base_uri_executor(BaseUriCallable().get_base_uri) \ - .global_errors(self.global_errors()) + def global_configuration(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(), + base_uri_executor=BaseUriCallable().get_base_uri, + global_errors=self.global_errors()) @property - def global_configuration_without_http_callback(self): - return GlobalConfiguration(self.http_client_configuration(None)) \ - .base_uri_executor(BaseUriCallable().get_base_uri) + def global_configuration_without_http_callback(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(None), + base_uri_executor=BaseUriCallable().get_base_uri) @property - def global_configuration_unimplemented_http_callback(self): - return GlobalConfiguration(self.http_client_configuration(HttpCallBack())) \ - .base_uri_executor(BaseUriCallable().get_base_uri) + def global_configuration_unimplemented_http_callback(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(HttpCallBack()), + base_uri_executor=BaseUriCallable().get_base_uri) @property - def default_global_configuration(self): + def default_global_configuration(self) -> GlobalConfiguration: return GlobalConfiguration() @property - def global_configuration_with_useragent(self): - return self.global_configuration \ - .user_agent(self.user_agent(), self.user_agent_parameters()) + def global_configuration_with_useragent(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.add_user_agent(self.user_agent(), self.user_agent_parameters()) + return global_configuration @property - def global_configuration_with_auth(self): - return self.global_configuration.auth_managers( - {'basic_auth': self.basic_auth(), 'bearer_auth': self.bearer_auth(), - 'custom_header_auth': self.custom_header_auth(), 'custom_query_auth': self.custom_query_auth()}) + def global_configuration_with_auth(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': self.basic_auth(), + 'bearer_auth': self.bearer_auth(), + 'custom_header_auth': self.custom_header_auth(), + 'custom_query_auth': self.custom_query_auth() + } + return global_configuration @staticmethod - def api_request_logging_configuration(log_body=False, log_headers=False, headers_to_include=None, - headers_to_exclude=None, headers_to_unmask=None, - include_query_in_path=False): - return ApiRequestLoggingConfiguration(log_body=log_body, log_headers=log_headers, - headers_to_include=headers_to_include, - headers_to_exclude=headers_to_exclude, - headers_to_unmask=headers_to_unmask, - include_query_in_path=include_query_in_path) + def api_request_logging_configuration( + log_body: bool=False, log_headers: bool=False, headers_to_include: List[str]=None, + headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None, + include_query_in_path: bool=False + ) -> ApiRequestLoggingConfiguration: + return ApiRequestLoggingConfiguration( + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include, + headers_to_exclude=headers_to_exclude, headers_to_unmask=headers_to_unmask, + include_query_in_path=include_query_in_path) @staticmethod - def api_response_logging_configuration(log_body=False, log_headers=False, headers_to_include=None, - headers_to_exclude=None, headers_to_unmask=None): - return ApiResponseLoggingConfiguration(log_body=log_body, log_headers=log_headers, - headers_to_include=headers_to_include, - headers_to_exclude=headers_to_exclude, - headers_to_unmask=headers_to_unmask) + def api_response_logging_configuration( + log_body: bool=False, log_headers: bool=False, headers_to_include: List[str]=None, + headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None + ) -> ApiResponseLoggingConfiguration: + return ApiResponseLoggingConfiguration( + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include, + headers_to_exclude=headers_to_exclude, headers_to_unmask=headers_to_unmask) @staticmethod - def api_logging_configuration(logger, log_level=logging.INFO, mask_sensitive_headers=True, - request_logging_configuration=None, - response_logging_configuration=None): + def api_logging_configuration( + logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, + request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, + response_logging_configuration: Optional[ApiResponseLoggingConfiguration]=None + ) -> ApiLoggingConfiguration: if request_logging_configuration is None: request_logging_configuration = Base.api_request_logging_configuration() if response_logging_configuration is None: response_logging_configuration = Base.api_response_logging_configuration() - return ApiLoggingConfiguration(logger, log_level, mask_sensitive_headers, - request_logging_configuration, - response_logging_configuration) + return ApiLoggingConfiguration( + logger=logger, log_level=log_level, mask_sensitive_headers=mask_sensitive_headers, + request_logging_config=request_logging_configuration, + response_logging_config=response_logging_configuration) @staticmethod - def global_configuration_with_logging(logger): - return GlobalConfiguration(Base.http_client_configuration( - logging_configuration=Base.api_logging_configuration(logger))) \ - .base_uri_executor(BaseUriCallable().get_base_uri) + def global_configuration_with_logging(logger: Logger) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=Base.http_client_configuration( + logging_configuration=Base.api_logging_configuration(logger)), + base_uri_executor=BaseUriCallable().get_base_uri) @property - def global_configuration_with_uninitialized_auth_params(self): - return self.global_configuration.auth_managers( - {'basic_auth': BasicAuth(None, None), 'bearer_auth': BearerAuth(None), - 'custom_header_auth': CustomHeaderAuthentication(None)}) + def global_configuration_with_uninitialized_auth_params(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': BasicAuth(None), 'bearer_auth': BearerAuth(None), + 'custom_header_auth': CustomHeaderAuthentication(None) + } + return global_configuration @property - def global_configuration_with_partially_initialized_auth_params(self): - return self.global_configuration.auth_managers( - {'basic_auth': BasicAuth(None, None), 'custom_header_auth': self.custom_header_auth()}) - - @staticmethod - def get_complex_type(): - inner_complex_type = InnerComplexType(boolean_type=True, - long_type=100003, - string_type='abc', - precision_type=55.44, - string_list_type=['item1', 'item2'], - additional_properties={'key0': 'abc', 'key1': 400}) - - return ComplexType(inner_complex_type=inner_complex_type, - inner_complex_list_type=[inner_complex_type, inner_complex_type], - inner_complex_list_of_map_type=[{'key0': inner_complex_type, 'key1': inner_complex_type}], - inner_complex_map_type={'key0': inner_complex_type, 'key1': inner_complex_type}, - inner_complex_map_of_list_type={'key0': [inner_complex_type, inner_complex_type], - 'key2': [inner_complex_type, inner_complex_type]}, - additional_properties={'prop1': [1, 2, 3], 'prop2': {'key0': 'abc', 'key1': 'def'}}) - - @staticmethod - def get_union_type_scalar_model(): - return UnionTypeScalarModel(any_of_required=1.5, - one_of_req_nullable='abc', - one_of_optional=200, - any_of_opt_nullable=True) + def global_configuration_with_partially_initialized_auth_params(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': BasicAuth(None), + 'custom_header_auth': self.custom_header_auth() + } + return global_configuration diff --git a/tests/apimatic_core/mocks/authentications/basic_auth.py b/tests/apimatic_core/mocks/authentications/basic_auth.py index 764c3ab..09a8d3b 100644 --- a/tests/apimatic_core/mocks/authentications/basic_auth.py +++ b/tests/apimatic_core/mocks/authentications/basic_auth.py @@ -1,19 +1,38 @@ + +from __future__ import annotations +from typing import Dict, Optional + from apimatic_core.authentication.header_auth import HeaderAuth from apimatic_core.utilities.auth_helper import AuthHelper +from pydantic import BaseModel, validate_call + +class BasicAuthCredentials(BaseModel): + username: str + password: str + + def clone_with(self, + username: Optional[str]=None, + password: Optional[str]=None) -> 'BasicAuthCredentials': + return BasicAuthCredentials( + username=username or self.username, + password=password or self.password) class BasicAuth(HeaderAuth): @property - def error_message(self): - """Returns the auth specific error message""" - return "BasicAuth: _basic_auth_user_name or _basic_auth_password is undefined." - - def __init__(self, basic_auth_user_name, basic_auth_password): - auth_params = {} - if basic_auth_user_name and basic_auth_password: - auth_params = {'Basic-Authorization': "Basic {}".format( - AuthHelper.get_base64_encoded_value(basic_auth_user_name, basic_auth_password))} + def error_message(self) -> str: + """Display error message on occurrence of authentication failure + in BasicAuth + + """ + return "BasicAuth: username or password is undefined." + + @validate_call + def __init__(self, basic_auth_credentials: Optional['BasicAuthCredentials']) -> None: + auth_params: Dict[str, str] = {} + if basic_auth_credentials is not None \ + and basic_auth_credentials.username and basic_auth_credentials.password: + auth_params = {"Basic-Authorization": "Basic {}".format( + AuthHelper.get_base64_encoded_value(basic_auth_credentials.username, basic_auth_credentials.password))} super().__init__(auth_params=auth_params) - self._basic_auth_user_name = basic_auth_user_name - self._basic_auth_password = basic_auth_password diff --git a/tests/apimatic_core/mocks/authentications/bearer_auth.py b/tests/apimatic_core/mocks/authentications/bearer_auth.py index 6a37b29..33ac191 100644 --- a/tests/apimatic_core/mocks/authentications/bearer_auth.py +++ b/tests/apimatic_core/mocks/authentications/bearer_auth.py @@ -1,16 +1,32 @@ + +from __future__ import annotations +from typing import Dict, Optional + from apimatic_core.authentication.header_auth import HeaderAuth +from pydantic import BaseModel, validate_call + +class BearerAuthCredentials(BaseModel): + access_token: str + + def clone_with(self, access_token: Optional[str]=None) -> 'BearerAuthCredentials': + return BearerAuthCredentials(access_token=access_token or self.access_token) class BearerAuth(HeaderAuth): @property - def error_message(self): - """Returns the auth specific error message""" - return "BearerAuth: _access_token is undefined." - - def __init__(self, access_token): - auth_params = {} - if access_token: - auth_params = {'Bearer-Authorization': "Bearer {}".format(access_token)} + def error_message(self) -> str: + """Display error message on occurrence of authentication failure + in OAuthBearerToken + + """ + return "BearerAuth: access_token is undefined." + + @validate_call + def __init__(self, o_auth_bearer_token_credentials: Optional['BearerAuthCredentials']) -> None: + self._access_token: Optional[str] = o_auth_bearer_token_credentials.access_token \ + if o_auth_bearer_token_credentials is not None else None + auth_params: Dict[str, str] = {} + if self._access_token: + auth_params = {"Bearer-Authorization": "Bearer {}".format(self._access_token)} super().__init__(auth_params=auth_params) - self._access_token = access_token diff --git a/tests/apimatic_core/mocks/authentications/custom_header_authentication.py b/tests/apimatic_core/mocks/authentications/custom_header_authentication.py index 69a7ff9..8f93abe 100644 --- a/tests/apimatic_core/mocks/authentications/custom_header_authentication.py +++ b/tests/apimatic_core/mocks/authentications/custom_header_authentication.py @@ -1,16 +1,30 @@ + +from __future__ import annotations +from typing import Dict, Optional from apimatic_core.authentication.header_auth import HeaderAuth +from pydantic import BaseModel, validate_call +class CustomHeaderAuthenticationCredentials(BaseModel): -class CustomHeaderAuthentication(HeaderAuth): + token: str + + def clone_with(self, token: Optional[str]=None) -> 'CustomHeaderAuthenticationCredentials': + return CustomHeaderAuthenticationCredentials(token=token or self.token) +class CustomHeaderAuthentication(HeaderAuth): @property - def error_message(self): - """Returns the auth specific error message""" + def error_message(self) -> str: + """Display error message on occurrence of authentication failure + in OAuthBearerToken + + """ return "CustomHeaderAuthentication: token is undefined." - def __init__(self, token): - auth_params = {} - if token: - auth_params = {'token': token} + @validate_call + def __init__( + self, custom_header_authentication_credentials: Optional['CustomHeaderAuthenticationCredentials']): + auth_params: Dict[str, str] = {} + if custom_header_authentication_credentials is not None \ + and custom_header_authentication_credentials.token is not None: + auth_params = {"token": custom_header_authentication_credentials.token} super().__init__(auth_params=auth_params) - self._token = token diff --git a/tests/apimatic_core/mocks/authentications/custom_query_authentication.py b/tests/apimatic_core/mocks/authentications/custom_query_authentication.py index 5ebc3b0..2fcd1e2 100644 --- a/tests/apimatic_core/mocks/authentications/custom_query_authentication.py +++ b/tests/apimatic_core/mocks/authentications/custom_query_authentication.py @@ -1,18 +1,37 @@ + +from __future__ import annotations +from typing import Dict, Optional +from pydantic import BaseModel, validate_call + from apimatic_core.authentication.query_auth import QueryAuth -class CustomQueryAuthentication(QueryAuth): +class CustomQueryAuthenticationCredentials(BaseModel): + + token: str + api_key: str + + def clone_with( + self, token: Optional[str]=None, api_key: Optional[str]=None) -> 'CustomQueryAuthenticationCredentials': + return CustomQueryAuthenticationCredentials(token=token or self.token, api_key=api_key or self.api_key) +class CustomQueryAuthentication(QueryAuth): @property - def error_message(self): - """Returns the auth specific error message""" - return "CustomQueryAuthentication: _token or _api_key is undefined." - - def __init__(self, token, api_key): - auth_params = {} - if token and api_key: - auth_params = {'token': token, 'api-key': api_key} - super().__init__(auth_params=auth_params) - self._token = token - self._api_key = api_key + def error_message(self) -> str: + """Display error message on occurrence of authentication failure + in OAuthBearerToken + """ + return "CustomQueryAuthentication: token or api_key is undefined." + + @validate_call + def __init__( + self, custom_query_authentication_credentials: Optional['CustomQueryAuthenticationCredentials']): + auth_params: Dict[str, str] = {} + if custom_query_authentication_credentials is not None \ + and custom_query_authentication_credentials.token is not None: + auth_params["token"] = custom_query_authentication_credentials.token + if custom_query_authentication_credentials is not None \ + and custom_query_authentication_credentials.api_key is not None: + auth_params["api-key"] = custom_query_authentication_credentials.api_key + super().__init__(auth_params=auth_params) diff --git a/tests/apimatic_core/mocks/callables/base_uri_callable.py b/tests/apimatic_core/mocks/callables/base_uri_callable.py index 6ccc752..f46b61d 100644 --- a/tests/apimatic_core/mocks/callables/base_uri_callable.py +++ b/tests/apimatic_core/mocks/callables/base_uri_callable.py @@ -1,26 +1,29 @@ from enum import Enum + +from typing import Dict + from apimatic_core.utilities.api_helper import ApiHelper -class Environment(Enum): +class Environment(int, Enum): TESTING = 1 -class Server(Enum): +class Server(int, Enum): """An enum for API servers""" DEFAULT = 0 AUTH_SERVER = 1 class BaseUriCallable: - environments = { + environments: Dict[Environment, Dict[Server, str]] = { Environment.TESTING: { Server.DEFAULT: 'http://localhost:3000', Server.AUTH_SERVER: 'http://authserver:5000' } } - def get_base_uri(self, server=Server.DEFAULT): + def get_base_uri(self, server: Server=Server.DEFAULT): """Generates the appropriate base URI for the environment and the server. diff --git a/tests/apimatic_core/mocks/exceptions/api_exception.py b/tests/apimatic_core/mocks/exceptions/api_exception.py index d482a61..70e7aaa 100644 --- a/tests/apimatic_core/mocks/exceptions/api_exception.py +++ b/tests/apimatic_core/mocks/exceptions/api_exception.py @@ -1,3 +1,10 @@ +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import BaseModel + + +class APIExceptionValidator(BaseModel): + reason: str + response: HttpResponse class APIException(Exception): @@ -9,9 +16,13 @@ class APIException(Exception): """ + reason: str + response: HttpResponse + response_code: int + def __init__(self, - reason, - response): + reason: str, + response: HttpResponse): """Constructor for the APIException class Args: @@ -20,6 +31,8 @@ def __init__(self, response (HttpResponse): The HttpResponse of the API call. """ + + APIExceptionValidator(reason=reason, response=response) super(APIException, self).__init__(reason) self.reason = reason self.response = response diff --git a/tests/apimatic_core/mocks/exceptions/global_test_exception.py b/tests/apimatic_core/mocks/exceptions/global_test_exception.py index 1a721c7..8c1ffdc 100644 --- a/tests/apimatic_core/mocks/exceptions/global_test_exception.py +++ b/tests/apimatic_core/mocks/exceptions/global_test_exception.py @@ -1,9 +1,30 @@ +from typing import Optional + +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import BaseModel, Field, AliasChoices +from typing_extensions import Annotated + from apimatic_core.utilities.api_helper import ApiHelper from tests.apimatic_core.mocks.exceptions.api_exception import APIException +class GlobalTestExceptionValidator(BaseModel): + server_message: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("server_message", "ServerMessage"), + serialization_alias="ServerMessage") + ] = None + server_code: Annotated[ + Optional[int], + Field(validation_alias=AliasChoices("server_code", "ServerCode"), + serialization_alias="ServerCode") + ] = None + class GlobalTestException(APIException): - def __init__(self, reason, response): + server_message: Optional[str] = None + server_code: Optional[int] = None + + def __init__(self, reason: str, response: HttpResponse): """Constructor for the GlobalTestException class Args: @@ -13,18 +34,7 @@ def __init__(self, reason, response): """ super(GlobalTestException, self).__init__(reason, response) - dictionary = ApiHelper.json_deserialize(self.response.text) - if isinstance(dictionary, dict): - self.unbox(dictionary) - - def unbox(self, dictionary): - """Populates the properties of this object by extracting them from a dictionary. - - Args: - dictionary (dictionary): A dictionary representation of the object as - obtained from the deserialization of the server's response. The keys - MUST match property names in the API description. - - """ - self.server_message = dictionary.get("ServerMessage") if dictionary.get("ServerMessage") else None - self.server_code = dictionary.get("ServerCode") if dictionary.get("ServerCode") else None + dictionary = ApiHelper.json_deserialize(self.response.text) or {} + validated_data = GlobalTestExceptionValidator(**dictionary) + self.server_message = validated_data.server_message + self.server_code = validated_data.server_code diff --git a/tests/apimatic_core/mocks/exceptions/local_test_exception.py b/tests/apimatic_core/mocks/exceptions/local_test_exception.py index d16abe2..3c6596e 100644 --- a/tests/apimatic_core/mocks/exceptions/local_test_exception.py +++ b/tests/apimatic_core/mocks/exceptions/local_test_exception.py @@ -1,9 +1,25 @@ +from typing import Optional + +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import BaseModel, Field, AliasChoices +from typing_extensions import Annotated + from apimatic_core.utilities.api_helper import ApiHelper +from tests.apimatic_core.mocks.exceptions.api_exception import APIException from tests.apimatic_core.mocks.exceptions.global_test_exception import GlobalTestException +class LocalTestExceptionValidator(BaseModel): + secret_message_for_endpoint: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("secret_message_for_endpoint", "SecretMessageForEndpoint"), + serialization_alias="SecretMessageForEndpoint") + ] = None + class LocalTestException(GlobalTestException): - def __init__(self, reason, response): + secret_message_for_endpoint: Optional[str] = None + + def __init__(self, reason: str, response: HttpResponse): """Constructor for the LocalTestException class Args: @@ -13,18 +29,6 @@ def __init__(self, reason, response): """ super(LocalTestException, self).__init__(reason, response) - dictionary = ApiHelper.json_deserialize(self.response.text) - if isinstance(dictionary, dict): - self.unbox(dictionary) - - def unbox(self, dictionary): - """Populates the properties of this object by extracting them from a dictionary. - - Args: - dictionary (dictionary): A dictionary representation of the object as - obtained from the deserialization of the server's response. The keys - MUST match property names in the API description. - - """ - super(LocalTestException, self).unbox(dictionary) - self.secret_message_for_endpoint = dictionary.get("SecretMessageForEndpoint") if dictionary.get("SecretMessageForEndpoint") else None + dictionary = ApiHelper.json_deserialize(self.response.text) or {} + validated_data = LocalTestExceptionValidator(**dictionary) + self.secret_message_for_endpoint = validated_data.secret_message_for_endpoint diff --git a/tests/apimatic_core/mocks/exceptions/nested_model_exception.py b/tests/apimatic_core/mocks/exceptions/nested_model_exception.py index c894e21..c1a05d9 100644 --- a/tests/apimatic_core/mocks/exceptions/nested_model_exception.py +++ b/tests/apimatic_core/mocks/exceptions/nested_model_exception.py @@ -1,10 +1,37 @@ +from typing import Optional + +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import BaseModel, AliasChoices, Field +from typing_extensions import Annotated + from apimatic_core.utilities.api_helper import ApiHelper from tests.apimatic_core.mocks.exceptions.api_exception import APIException from tests.apimatic_core.mocks.models.validate import Validate +class NestedModelExceptionValidator(BaseModel): + server_message: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("server_message", "ServerMessage"), + serialization_alias="ServerMessage") + ] = None + server_code: Annotated[ + Optional[int], + Field(validation_alias=AliasChoices("server_code", "ServerCode"), + serialization_alias="ServerCode") + ] = None + model: Annotated[ + Optional[Validate], + Field(validation_alias=AliasChoices("model", "model"), + serialization_alias="model") + ] = None + class NestedModelException(APIException): - def __init__(self, reason, response): + server_message: Optional[str] = None + server_code: Optional[int] = None + model: Optional[str] = None + + def __init__(self, reason: str, response: HttpResponse): """Constructor for the NestedModelException class Args: @@ -14,19 +41,8 @@ def __init__(self, reason, response): """ super(NestedModelException, self).__init__(reason, response) - dictionary = ApiHelper.json_deserialize(self.response.text) - if isinstance(dictionary, dict): - self.unbox(dictionary) - - def unbox(self, dictionary): - """Populates the properties of this object by extracting them from a dictionary. - - Args: - dictionary (dictionary): A dictionary representation of the object as - obtained from the deserialization of the server's response. The keys - MUST match property names in the API description. - - """ - self.server_message = dictionary.get("ServerMessage") if dictionary.get("ServerMessage") else None - self.server_code = dictionary.get("ServerCode") if dictionary.get("ServerCode") else None - self.model = Validate.from_dictionary(dictionary.get('model')) if dictionary.get('model') else None + dictionary = ApiHelper.json_deserialize(self.response.text) or {} + validated_data = NestedModelExceptionValidator(**dictionary) + self.server_message = validated_data.server_message + self.server_code = validated_data.server_code + self.model = validated_data.model diff --git a/tests/apimatic_core/mocks/http/http_client.py b/tests/apimatic_core/mocks/http/http_client.py index a76da68..0280780 100644 --- a/tests/apimatic_core/mocks/http/http_client.py +++ b/tests/apimatic_core/mocks/http/http_client.py @@ -1,16 +1,17 @@ -from apimatic_core.factories.http_response_factory import HttpResponseFactory -from apimatic_core.http.response.http_response import HttpResponse -from apimatic_core_interfaces.client.http_client import HttpClient +from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration +from apimatic_core_interfaces.http.http_client import HttpClient +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call class MockHttpClient(HttpClient): - def __init__(self): - self._should_retry = None - self._contains_binary_response = None - self.response_factory = HttpResponseFactory() + should_retry: bool = False + contains_binary_response: bool = False - def execute(self, request, endpoint_configuration): + @validate_call + def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfiguration = None) -> HttpResponse: """Execute a given CoreHttpRequest to get a string response back Args: @@ -21,12 +22,19 @@ def execute(self, request, endpoint_configuration): HttpResponse: The response of the CoreHttpRequest. """ - self._should_retry = endpoint_configuration.should_retry - self._contains_binary_response = endpoint_configuration.contains_binary_response - return self.response_factory.create(status_code=200, reason=None, - headers=request.headers, body=str(request.parameters), request=request) - - def convert_response(self, response, contains_binary_response, http_request): + self.should_retry = endpoint_configuration.should_retry + self.contains_binary_response = endpoint_configuration.has_binary_response + return HttpResponse( + status_code=200, + reason_phrase=None, + headers=request.headers, + text=str(request.parameters), + request=request) + + @validate_call + def convert_response( + self, response: HttpResponse, contains_binary_response: bool, http_request: HttpRequest + ) -> HttpResponse: """Converts the Response object of the CoreHttpClient into an CoreHttpResponse object. diff --git a/tests/apimatic_core/mocks/http/http_response_catcher.py b/tests/apimatic_core/mocks/http/http_response_catcher.py index adbd867..6d94b6a 100644 --- a/tests/apimatic_core/mocks/http/http_response_catcher.py +++ b/tests/apimatic_core/mocks/http/http_response_catcher.py @@ -1,3 +1,7 @@ +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call + from apimatic_core.http.http_callback import HttpCallBack @@ -10,10 +14,14 @@ class HttpResponseCatcher(HttpCallBack): after a request is executed. """ - def on_before_request(self, request): + response: HttpResponse = None + + @validate_call + def on_before_request(self, request: HttpRequest): pass - def on_after_response(self, response): + @validate_call + def on_after_response(self, response: HttpResponse): self.response = response diff --git a/tests/apimatic_core/mocks/logger/api_logger.py b/tests/apimatic_core/mocks/logger/api_logger.py index 46944bf..615a013 100644 --- a/tests/apimatic_core/mocks/logger/api_logger.py +++ b/tests/apimatic_core/mocks/logger/api_logger.py @@ -1,14 +1,16 @@ import logging from apimatic_core_interfaces.logger.logger import Logger +from typing import Dict, Any +from pydantic import validate_call -class ApiLogger(Logger): - def __init__(self): - self._logger = logging.getLogger('mocked_logger') +class ApiLogger(Logger): + logger: logging.Logger = logging.getLogger('mocked_logger') - def log(self, level, message, params): - self._logger.log(level, message, *params.values()) + @validate_call + def log(self, level: int, message: str, params: Dict[str, Any]): + self.logger.log(level, message, *params.values()) diff --git a/tests/apimatic_core/mocks/models/api_response.py b/tests/apimatic_core/mocks/models/api_response.py index 9a4af81..62eb17b 100644 --- a/tests/apimatic_core/mocks/models/api_response.py +++ b/tests/apimatic_core/mocks/models/api_response.py @@ -1,8 +1,10 @@ -from apimatic_core.http.response.api_response import ApiResponse -from apimatic_core.http.response.http_response import HttpResponse +from apimatic_core_interfaces.http.http_response import HttpResponse +from typing import Any, Optional, List +from apimatic_core.http.response.api_response import ApiResponse as CoreApiResponse -class ApiResponse(ApiResponse): + +class ApiResponse(CoreApiResponse): """Http response received. Attributes: @@ -18,9 +20,9 @@ class ApiResponse(ApiResponse): """ - def __init__(self, http_response, - body=None, - errors=None): + def __init__(self, http_response: HttpResponse, + body: Any = None, + errors: Optional[List[str]] = None): """The Constructor Args: @@ -31,15 +33,14 @@ def __init__(self, http_response, """ super().__init__(http_response, body, errors) - if type(self.body) is dict: - self.cursor = self.body.get('cursor') def __repr__(self): return '' % self.text @staticmethod - def create(_parent_instance): + def create(_parent_instance: CoreApiResponse): return ApiResponse( - HttpResponse(_parent_instance.status_code, _parent_instance.reason_phrase, _parent_instance.headers, - _parent_instance.text, _parent_instance.request), _parent_instance.body, - _parent_instance.errors) + HttpResponse( + status_code=_parent_instance.status_code, reason_phrase=_parent_instance.reason_phrase, + headers=_parent_instance.headers, text=_parent_instance.text, request=_parent_instance.request), + body=_parent_instance.body, errors=_parent_instance.errors) diff --git a/tests/apimatic_core/mocks/models/atom.py b/tests/apimatic_core/mocks/models/atom.py index 254643a..0ed4b95 100644 --- a/tests/apimatic_core/mocks/models/atom.py +++ b/tests/apimatic_core/mocks/models/atom.py @@ -1,7 +1,13 @@ +from typing import Optional + +from pydantic import BaseModel, model_serializer, Field, AliasChoices +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from typing_extensions import Annotated + from apimatic_core.utilities.api_helper import ApiHelper -class Atom(object): +class Atom(BaseModel): """Implementation of the 'Atom' model. @@ -13,78 +19,21 @@ class Atom(object): """ - # Create a mapping from Model property names to API property names - _names = { - "atom_number_of_electrons": 'AtomNumberOfElectrons', - "atom_number_of_protons": 'AtomNumberOfProtons' - } - - _optionals = [ - 'atom_number_of_protons', + atom_number_of_electrons: Annotated[ + int, + Field(validation_alias=AliasChoices("atom_number_of_electrons", "AtomNumberOfElectrons"), + serialization_alias="AtomNumberOfElectrons") ] - def __init__(self, - atom_number_of_electrons=None, - atom_number_of_protons=ApiHelper.SKIP): - """Constructor for the Atom class""" - - # Initialize members of the class - self.atom_number_of_electrons = atom_number_of_electrons - if atom_number_of_protons is not ApiHelper.SKIP: - self.atom_number_of_protons = atom_number_of_protons - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - atom_number_of_electrons = dictionary.get("AtomNumberOfElectrons") if \ - dictionary.get("AtomNumberOfElectrons") else None - atom_number_of_protons = dictionary.get("AtomNumberOfProtons") if \ - dictionary.get("AtomNumberOfProtons") else ApiHelper.SKIP - # Return an object of this model - return cls(atom_number_of_electrons, - atom_number_of_protons) - - @classmethod - def validate(cls, dictionary): - """Validates dictionary against class required properties - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - boolean : if dictionary is valid contains required properties. - - """ - - if isinstance(dictionary, cls): - return ApiHelper.is_valid_type(value=dictionary.atom_number_of_electrons, - type_callable=lambda value: isinstance(value, int)) - - if not isinstance(dictionary, dict): - return False + atom_number_of_protons: Annotated[ + Optional[int], + Field(validation_alias=AliasChoices("atom_number_of_protons", "AtomNumberOfProtons"), + serialization_alias="AtomNumberOfProtons") + ] = None - return ApiHelper.is_valid_type(value=dictionary.get('AtomNumberOfElectrons'), - type_callable=lambda value: isinstance(value, int)) + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"atom_number_of_protons"} - def __eq__(self, other): - if isinstance(self, other.__class__): - return self.atom_number_of_electrons == other.atom_number_of_electrons and \ - self.atom_number_of_protons == other.atom_number_of_protons - return False + return ApiHelper.sanitize_model(optional_fields=_optional_fields, model_dump=nxt(self), + model_fields=self.model_fields, model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/mocks/models/cat_model.py b/tests/apimatic_core/mocks/models/cat_model.py index 41980b1..4ed6aaa 100644 --- a/tests/apimatic_core/mocks/models/cat_model.py +++ b/tests/apimatic_core/mocks/models/cat_model.py @@ -1,7 +1,12 @@ +from typing import Optional + +from pydantic import AliasChoices, Field, BaseModel +from typing_extensions import Annotated +import xml.etree.ElementTree as ET from apimatic_core.utilities.xml_helper import XmlHelper -class CatModel(object): +class CatModel(BaseModel): """Implementation of the 'Cat' model. @@ -12,20 +17,13 @@ class CatModel(object): """ - # Create a mapping from Model property names to API property names - _names = { - "meows": 'Meows' - } - - def __init__(self, - meows=None): - """Constructor for the CatModel class""" - - # Initialize members of the class - self.meows = meows + meows: Annotated[ + Optional[bool], + Field(AliasChoices("meows", "Meows"), serialization_alias="Meows") + ] = None @classmethod - def from_element(cls, root): + def from_element(cls, root: ET.Element) -> "CatModel": """Initialize an instance of this class using an xml.etree.Element. Args: @@ -37,9 +35,9 @@ def from_element(cls, root): """ meows = XmlHelper.value_from_xml_element(root.find('Meows'), bool) - return cls(meows) + return cls(meows=meows) - def to_xml_sub_element(self, root): + def to_xml_sub_element(self, root: ET.Element): """Convert this object to an instance of xml.etree.Element. Args: diff --git a/tests/apimatic_core/mocks/models/complex_type.py b/tests/apimatic_core/mocks/models/complex_type.py index aaf19c4..a459083 100644 --- a/tests/apimatic_core/mocks/models/complex_type.py +++ b/tests/apimatic_core/mocks/models/complex_type.py @@ -1,103 +1,55 @@ +from typing import Optional, List, Dict +from pydantic import BaseModel, Field, model_serializer, AliasChoices +from pydantic_core.core_schema import SerializerFunctionWrapHandler from apimatic_core.utilities.api_helper import ApiHelper -from tests.apimatic_core.mocks.models.inner_complex_type import InnerComplexType - +from typing_extensions import Annotated -class ComplexType(object): +from tests.apimatic_core.mocks.models.inner_complex_type import InnerComplexType - """Implementation of the 'ComplexType' model. - TODO: type model description here. +class ComplexType(BaseModel): + """Implementation of the 'ComplexType' model using Pydantic. Attributes: inner_complex_type (InnerComplexType): TODO: type description here. - inner_complex_list_type (list of InnerComplexType): TODO: type - description here. - inner_complex_map_type (dict): TODO: type description here. - inner_complex_list_of_map_type (list of InnerComplexType): TODO: type - description here. - inner_complex_map_of_list_type (list of InnerComplexType): TODO: type - description here. - + inner_complex_list_type (list[InnerComplexType]): TODO: type description here. + inner_complex_map_type (Optional[dict]): TODO: type description here. + inner_complex_list_of_map_type (Optional[list[InnerComplexType]]): TODO: type description here. + inner_complex_map_of_list_type (Optional[list[InnerComplexType]]): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "inner_complex_list_type": 'innerComplexListType', - "inner_complex_type": 'innerComplexType', - "inner_complex_list_of_map_type": 'innerComplexListOfMapType', - "inner_complex_map_of_list_type": 'innerComplexMapOfListType', - "inner_complex_map_type": 'innerComplexMapType' - } - - _optionals = [ - 'inner_complex_map_type', - 'inner_complex_list_of_map_type', - 'inner_complex_map_of_list_type', + inner_complex_type: Annotated[ + InnerComplexType, + Field(validation_alias=AliasChoices("inner_complex_type", "innerComplexType"), + serialization_alias="innerComplexType") ] - - def __init__(self, - inner_complex_list_type=None, - inner_complex_type=None, - inner_complex_list_of_map_type=ApiHelper.SKIP, - inner_complex_map_of_list_type=ApiHelper.SKIP, - inner_complex_map_type=ApiHelper.SKIP, - additional_properties={}): - """Constructor for the ComplexType class""" - - # Initialize members of the class - self.inner_complex_type = inner_complex_type - self.inner_complex_list_type = inner_complex_list_type - if inner_complex_map_type is not ApiHelper.SKIP: - self.inner_complex_map_type = inner_complex_map_type - if inner_complex_list_of_map_type is not ApiHelper.SKIP: - self.inner_complex_list_of_map_type = inner_complex_list_of_map_type - if inner_complex_map_of_list_type is not ApiHelper.SKIP: - self.inner_complex_map_of_list_type = inner_complex_map_of_list_type - - # Add additional model properties to the instance - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - inner_complex_list_type = None - if dictionary.get('innerComplexListType') is not None: - inner_complex_list_type = [InnerComplexType.from_dictionary(x) for x in dictionary.get('innerComplexListType')] - inner_complex_type = InnerComplexType.from_dictionary(dictionary.get('innerComplexType')) if dictionary.get('innerComplexType') else None - inner_complex_list_of_map_type = None - if dictionary.get('innerComplexListOfMapType') is not None: - inner_complex_list_of_map_type = [InnerComplexType.from_dictionary(x) for x in dictionary.get('innerComplexListOfMapType')] - else: - inner_complex_list_of_map_type = ApiHelper.SKIP - inner_complex_map_of_list_type = None - if dictionary.get('innerComplexMapOfListType') is not None: - inner_complex_map_of_list_type = [InnerComplexType.from_dictionary(x) for x in dictionary.get('innerComplexMapOfListType')] - else: - inner_complex_map_of_list_type = ApiHelper.SKIP - inner_complex_map_type = InnerComplexType.from_dictionary(dictionary.get('innerComplexMapType')) if 'innerComplexMapType' in dictionary.keys() else ApiHelper.SKIP - # Clean out expected properties from dictionary - for key in cls._names.values(): - if key in dictionary: - del dictionary[key] - # Return an object of this model - return cls(inner_complex_list_type, - inner_complex_type, - inner_complex_list_of_map_type, - inner_complex_map_of_list_type, - inner_complex_map_type, - dictionary) + inner_complex_list_type: Annotated[ + List[InnerComplexType], + Field(validation_alias=AliasChoices("inner_complex_list_type", "innerComplexListType"), + serialization_alias="innerComplexListType") + ] + inner_complex_map_type: Annotated[ + Optional[Dict], + Field(validation_alias=AliasChoices("inner_complex_map_type", "innerComplexMapType"), + serialization_alias="innerComplexMapType") + ] = None + inner_complex_list_of_map_type: Annotated[ + Optional[List[InnerComplexType]], + Field(validation_alias=AliasChoices("inner_complex_list_of_map_type", "innerComplexListOfMapType"), + serialization_alias="innerComplexListOfMapType") + ] = None + inner_complex_map_of_list_type: Annotated[ + Optional[List[InnerComplexType]], + Field(validation_alias=AliasChoices("inner_complex_map_of_list_type", "innerComplexMapOfListType"), + serialization_alias="innerComplexMapOfListType") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = { + "inner_complex_map_type", + "inner_complex_list_of_map_type", + "inner_complex_map_of_list_type" + } + return ApiHelper.sanitize_model(optional_fields=_optional_fields, model_dump=nxt(self), + model_fields=self.model_fields, model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/mocks/models/days.py b/tests/apimatic_core/mocks/models/days.py index 6b08a55..3488140 100644 --- a/tests/apimatic_core/mocks/models/days.py +++ b/tests/apimatic_core/mocks/models/days.py @@ -1,4 +1,8 @@ -class Days(object): +from enum import Enum +from typing import Optional + + +class Days(str, Enum): """Implementation of the 'Days' enum. @@ -15,8 +19,6 @@ class Days(object): """ - _all_values = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday','Saturday'] - SUNDAY = 'Sunday' MONDAY = 'Monday' @@ -32,17 +34,5 @@ class Days(object): SATURDAY = 'Saturday' @classmethod - def validate(cls, value): - """Validates value against enum. - - Args: - value: the value to be validated against. - - Returns: - boolean : if value is valid for this model. - - """ - if value is None: - return None - - return value in cls._all_values \ No newline at end of file + def is_valid(cls, value: Optional[str]) -> bool: + return value in cls._value2member_map_ diff --git a/tests/apimatic_core/mocks/models/deer.py b/tests/apimatic_core/mocks/models/deer.py index 7c9c0d2..4de5781 100644 --- a/tests/apimatic_core/mocks/models/deer.py +++ b/tests/apimatic_core/mocks/models/deer.py @@ -1,7 +1,11 @@ +from pydantic import model_serializer, BaseModel, AliasChoices, Field +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from typing_extensions import Annotated + from apimatic_core.utilities.api_helper import ApiHelper -class Deer(object): +class Deer(BaseModel): """Implementation of the 'Deer' model. @@ -21,65 +25,23 @@ class Deer(object): "mtype": 'type' } - def __init__(self, - name=None, - weight=None, - mtype=None): - """Constructor for the Deer class""" - - # Initialize members of the class - self.name = name - self.weight = weight - self.mtype = mtype - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - name = dictionary.get("name") if dictionary.get("name") else None - weight = dictionary.get("weight") if dictionary.get("weight") else None - mtype = dictionary.get("type") if dictionary.get("type") else None - # Return an object of this model - return cls(name, - weight, - mtype) - - @classmethod - def validate(cls, dictionary): - """Validates dictionary against class required properties - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - boolean : if dictionary is valid contains required properties. - - """ - - if isinstance(dictionary, cls): - return ApiHelper.is_valid_type(value=dictionary.name, type_callable=lambda value: isinstance(value, str)) \ - and ApiHelper.is_valid_type(value=dictionary.weight, type_callable=lambda value: isinstance(value, int)) \ - and ApiHelper.is_valid_type(value=dictionary.mtype, type_callable=lambda value: isinstance(value, str)) - - if not isinstance(dictionary, dict): - return False - - return ApiHelper.is_valid_type(value=dictionary.get('name'), type_callable=lambda value: isinstance(value, str)) \ - and ApiHelper.is_valid_type(value=dictionary.get('weight'), type_callable=lambda value: isinstance(value, int)) \ - and ApiHelper.is_valid_type(value=dictionary.get('type'), type_callable=lambda value: isinstance(value, str)) + name: Annotated[ + str, + Field(validation_alias=AliasChoices("name", "name"), + serialization_alias="name") + ] + weight: Annotated[ + int, + Field(validation_alias=AliasChoices("weight", "weight"), + serialization_alias="weight") + ] + mtype: Annotated[ + str, + Field(validation_alias=AliasChoices("mtype", "type"), + serialization_alias="type") + ] + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + return ApiHelper.sanitize_model(model_dump=nxt(self), model_fields=self.model_fields, + model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/mocks/models/dog_model.py b/tests/apimatic_core/mocks/models/dog_model.py index 2952f5d..ca57925 100644 --- a/tests/apimatic_core/mocks/models/dog_model.py +++ b/tests/apimatic_core/mocks/models/dog_model.py @@ -1,7 +1,12 @@ +from typing import Optional + +from pydantic import BaseModel, AliasChoices, Field +from typing_extensions import Annotated +import xml.etree.ElementTree as ET from apimatic_core.utilities.xml_helper import XmlHelper -class DogModel(object): +class DogModel(BaseModel): """Implementation of the 'Dog' model. @@ -12,20 +17,13 @@ class DogModel(object): """ - # Create a mapping from Model property names to API property names - _names = { - "barks": 'Barks' - } - - def __init__(self, - barks=None): - """Constructor for the DogModel class""" - - # Initialize members of the class - self.barks = barks + barks: Annotated[ + Optional[bool], + Field(AliasChoices("barks", "Barks"), serialization_alias="Barks") + ] = None @classmethod - def from_element(cls, root): + def from_element(cls, root: ET.Element) -> "DogModel": """Initialize an instance of this class using an xml.etree.Element. Args: @@ -37,9 +35,9 @@ def from_element(cls, root): """ barks = XmlHelper.value_from_xml_element(root.find('Barks'), bool) - return cls(barks) + return cls(barks=barks) - def to_xml_sub_element(self, root): + def to_xml_sub_element(self, root: ET.Element): """Convert this object to an instance of xml.etree.Element. Args: diff --git a/tests/apimatic_core/mocks/models/grand_parent_class_model.py b/tests/apimatic_core/mocks/models/grand_parent_class_model.py index 3a2b5b8..e203fc4 100644 --- a/tests/apimatic_core/mocks/models/grand_parent_class_model.py +++ b/tests/apimatic_core/mocks/models/grand_parent_class_model.py @@ -1,73 +1,49 @@ +from typing import Optional, List + +from pydantic import BaseModel, AliasChoices, Field, model_serializer +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from typing_extensions import Annotated + from apimatic_core.utilities.api_helper import ApiHelper -class GrandParentClassModel(object): +class GrandParentClassModel(BaseModel): """Implementation of the 'Grand Parent Class' model. TODO: type model description here. Attributes: - grand_parent_optional (string): TODO: type description here. - grand_parent_required_nullable (string): TODO: type description here. - grand_parent_required (string): TODO: type description here. + grand_parent_optional (str): TODO: type description here. + grand_parent_required_nullable (str): TODO: type description here. + grand_parent_required (str): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "grand_parent_required_nullable": 'Grand_Parent_Required_Nullable', - "grand_parent_required": 'Grand_Parent_Required', - "grand_parent_optional": 'Grand_Parent_Optional' - } - - _optionals = [ - 'grand_parent_optional', + grand_parent_required_nullable: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("grand_parent_required_nullable", "Grand_Parent_Required_Nullable"), + serialization_alias="Grand_Parent_Required_Nullable") ] - - _nullables = [ - 'grand_parent_required_nullable', - ] - - def __init__(self, - grand_parent_required_nullable=None, - grand_parent_required='not nullable and required', - grand_parent_optional=ApiHelper.SKIP): - """Constructor for the GrandParentClassModel class""" - - # Initialize members of the class - if grand_parent_optional is not ApiHelper.SKIP: - self.grand_parent_optional = grand_parent_optional - self.grand_parent_required_nullable = grand_parent_required_nullable - self.grand_parent_required = grand_parent_required - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - - grand_parent_required_nullable = dictionary.get("Grand_Parent_Required_Nullable") if dictionary.get("Grand_Parent_Required_Nullable") else None - grand_parent_required = dictionary.get("Grand_Parent_Required") if dictionary.get("Grand_Parent_Required") else 'not nullable and required' - grand_parent_optional = dictionary.get("Grand_Parent_Optional") if dictionary.get("Grand_Parent_Optional") else ApiHelper.SKIP - # Return an object of this model - return cls(grand_parent_required_nullable, - grand_parent_required, - grand_parent_optional) - + grand_parent_required: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("grand_parent_required", "Grand_Parent_Required"), + serialization_alias="Grand_Parent_Required") + ] = 'not nullable and required' + grand_parent_optional: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("grand_parent_optional", "Grand_Parent_Optional"), + serialization_alias="Grand_Parent_Optional") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _nullable_fields = {'grand_parent_required_nullable'} + _optional_fields = {'grand_parent_optional'} + + return ApiHelper.sanitize_model(nullable_fields=_nullable_fields, optional_fields=_optional_fields, + model_dump=nxt(self), model_fields=self.model_fields, + model_fields_set=self.model_fields_set) class ParentClassModel(GrandParentClassModel): @@ -80,117 +56,64 @@ class ParentClassModel(GrandParentClassModel): mclass (int): TODO: type description here. precision (float): TODO: type description here. big_decimal (float): TODO: type description here. - parent_optional_nullable_with_default_value (string): TODO: type + parent_optional_nullable_with_default_value (str): TODO: type description here. - parent_optional (string): TODO: type description here. - parent_required_nullable (string): TODO: type description here. - parent_required (string): TODO: type description here. + parent_optional (str): TODO: type description here. + parent_required_nullable (str): TODO: type description here. + parent_required (str): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "parent_required_nullable": 'Parent_Required_Nullable', - "parent_required": 'Parent_Required', - "grand_parent_required_nullable": 'Grand_Parent_Required_Nullable', - "grand_parent_required": 'Grand_Parent_Required', - "mclass": 'class', - "precision": 'precision', - "big_decimal": 'Big_Decimal', - "parent_optional_nullable_with_default_value": 'Parent_Optional_Nullable_With_Default_Value', - "parent_optional": 'Parent_Optional', - "grand_parent_optional": 'Grand_Parent_Optional' - } - - _optionals = [ - 'mclass', - 'precision', - 'big_decimal', - 'parent_optional_nullable_with_default_value', - 'parent_optional', - ] - _optionals.extend(GrandParentClassModel._optionals) - - _nullables = [ - 'mclass', - 'precision', - 'big_decimal', - 'parent_optional_nullable_with_default_value', - 'parent_required_nullable', + parent_required_nullable: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("parent_required_nullable", "Parent_Required_Nullable"), + serialization_alias="Parent_Required_Nullable") ] - _nullables.extend(GrandParentClassModel._nullables) - - def __init__(self, - parent_required_nullable=None, - parent_required='not nullable and required', - grand_parent_required_nullable=None, - grand_parent_required='not nullable and required', - mclass=23, - precision=ApiHelper.SKIP, - big_decimal=ApiHelper.SKIP, - parent_optional_nullable_with_default_value='Has default value', - parent_optional=ApiHelper.SKIP, - grand_parent_optional=ApiHelper.SKIP): - """Constructor for the ParentClassModel class""" - - # Initialize members of the class - self.mclass = mclass - if precision is not ApiHelper.SKIP: - self.precision = precision - if big_decimal is not ApiHelper.SKIP: - self.big_decimal = big_decimal - self.parent_optional_nullable_with_default_value = parent_optional_nullable_with_default_value - if parent_optional is not ApiHelper.SKIP: - self.parent_optional = parent_optional - self.parent_required_nullable = parent_required_nullable - self.parent_required = parent_required - - # Call the constructor for the base class - super(ParentClassModel, self).__init__(grand_parent_required_nullable, - grand_parent_required, - grand_parent_optional) - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - - parent_required_nullable = dictionary.get("Parent_Required_Nullable") if dictionary.get("Parent_Required_Nullable") else None - parent_required = dictionary.get("Parent_Required") if dictionary.get("Parent_Required") else 'not nullable and required' - grand_parent_required_nullable = dictionary.get("Grand_Parent_Required_Nullable") if dictionary.get("Grand_Parent_Required_Nullable") else None - grand_parent_required = dictionary.get("Grand_Parent_Required") if dictionary.get("Grand_Parent_Required") else 'not nullable and required' - mclass = dictionary.get("class") if dictionary.get("class") else 23 - precision = dictionary.get("precision") if "precision" in dictionary.keys() else ApiHelper.SKIP - big_decimal = dictionary.get("Big_Decimal") if "Big_Decimal" in dictionary.keys() else ApiHelper.SKIP - parent_optional_nullable_with_default_value = dictionary.get("Parent_Optional_Nullable_With_Default_Value") if dictionary.get("Parent_Optional_Nullable_With_Default_Value") else 'Has default value' - parent_optional = dictionary.get("Parent_Optional") if dictionary.get("Parent_Optional") else ApiHelper.SKIP - grand_parent_optional = dictionary.get("Grand_Parent_Optional") if dictionary.get("Grand_Parent_Optional") else ApiHelper.SKIP - # Return an object of this model - return cls(parent_required_nullable, - parent_required, - grand_parent_required_nullable, - grand_parent_required, - mclass, - precision, - big_decimal, - parent_optional_nullable_with_default_value, - parent_optional, - grand_parent_optional) - + mclass: Annotated[ + Optional[int], + Field(validation_alias=AliasChoices("mclass", "class"), + serialization_alias="class") + ] = 23 + precision: Annotated[ + Optional[float], + Field(validation_alias=AliasChoices("precision", "precision"), + serialization_alias="precision") + ] = None + big_decimal: Annotated[ + Optional[float], + Field(validation_alias=AliasChoices("big_decimal", "Big_Decimal"), + serialization_alias="Big_Decimal") + ] = None + parent_optional_nullable_with_default_value: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("parent_optional_nullable_with_default_value", "Parent_Optional_Nullable_With_Default_Value"), + serialization_alias="Parent_Optional_Nullable_With_Default_Value") + ] = 'Has default value' + parent_optional: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("parent_optional", "Parent_Optional"), + serialization_alias="Parent_Optional") + ] = None + parent_required: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("parent_required", "Parent_Required"), + serialization_alias="Parent_Required") + ] = 'not nullable and required' + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _nullable_fields = { + 'mclass', 'precision', 'big_decimal', 'parent_optional_nullable_with_default_value', + 'parent_required_nullable', 'grand_parent_required_nullable' + } + _optional_fields = { + 'mclass', 'precision', 'big_decimal', 'parent_optional_nullable_with_default_value', 'parent_optional', + 'grand_parent_optional' + } + + return ApiHelper.sanitize_model(nullable_fields=_nullable_fields, optional_fields=_optional_fields, + model_dump=nxt(self), model_fields=self.model_fields, + model_fields_set=self.model_fields_set) class ChildClassModel(ParentClassModel): @@ -200,147 +123,105 @@ class ChildClassModel(ParentClassModel): NOTE: This class inherits from 'ParentClassModel'. Attributes: - optional_nullable (string): TODO: type description here. - optional_nullable_with_default_value (string): TODO: type description - here. - optional (string): TODO: type description here. - required_nullable (string): TODO: type description here. - required (string): TODO: type description here. - child_class_array (list of ChildClassModel): TODO: type description + optional_nullable (str): TODO: type description here. + optional_nullable_with_default_value (str): TODO: type description here. + optional (str): TODO: type description here. + required_nullable (str): TODO: type description here. + required (str): TODO: type description here. + child_class_array (List[ChildClassModel]): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "required_nullable": 'Required_Nullable', - "required": 'Required', - "parent_required_nullable": 'Parent_Required_Nullable', - "parent_required": 'Parent_Required', - "grand_parent_required_nullable": 'Grand_Parent_Required_Nullable', - "grand_parent_required": 'Grand_Parent_Required', - "optional_nullable": 'Optional_Nullable', - "optional_nullable_with_default_value": 'Optional_Nullable_With_Default_Value', - "optional": 'Optional', - "child_class_array": 'Child_Class_Array', - "mclass": 'class', - "precision": 'precision', - "big_decimal": 'Big_Decimal', - "parent_optional_nullable_with_default_value": 'Parent_Optional_Nullable_With_Default_Value', - "parent_optional": 'Parent_Optional', - "grand_parent_optional": 'Grand_Parent_Optional' - } - - _optionals = [ - 'optional_nullable', - 'optional_nullable_with_default_value', - 'optional', - 'child_class_array', + required_nullable: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("required_nullable", "Required_Nullable"), + serialization_alias="Required_Nullable") ] - _optionals.extend(ParentClassModel._optionals) + required : Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("required", "Required"), + serialization_alias="Required") + ] = 'not nullable and required' + optional_nullable: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("optional_nullable", "Optional_Nullable"), + serialization_alias="Optional_Nullable") + ] = None + optional_nullable_with_default_value: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("optional_nullable_with_default_value", "Optional_Nullable_With_Default_Value"), + serialization_alias="Optional_Nullable_With_Default_Value") + ] = 'With default value' + optional: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("optional", "Optional"), + serialization_alias="Optional") + ] = None + child_class_array: Annotated[ + Optional[List['ChildClassModel']], + Field(validation_alias=AliasChoices("child_class_array", "Child_Class_Array"), + serialization_alias="Child_Class_Array") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _nullable_fields = { + 'optional_nullable', 'optional_nullable_with_default_value', 'required_nullable', 'child_class_array', + 'mclass', 'precision', 'big_decimal', 'parent_optional_nullable_with_default_value', + 'parent_required_nullable', 'grand_parent_required_nullable' + } + _optional_fields = { + 'optional_nullable', 'optional_nullable_with_default_value', 'optional', 'child_class_array', + 'mclass', 'precision', 'big_decimal', 'parent_optional_nullable_with_default_value', 'parent_optional', + 'grand_parent_optional' + } + + return ApiHelper.sanitize_model(nullable_fields=_nullable_fields, optional_fields=_optional_fields, + model_dump=nxt(self), model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + +class Child2ClassModel(ParentClassModel): + + """Implementation of the 'Child2 Class' model. + + TODO: type model description here. + NOTE: This class inherits from 'ParentClassModel'. + + Attributes: + optional (str): TODO: type description here. + required_nullable (str): TODO: type description here. + required (str): TODO: type description here. + + """ - _nullables = [ - 'optional_nullable', - 'optional_nullable_with_default_value', - 'required_nullable', - 'child_class_array', + required_nullable: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("required_nullable", "Required_Nullable"), + serialization_alias="Required_Nullable") ] - _nullables.extend(ParentClassModel._nullables) - - def __init__(self, - required, - required_nullable=None, - parent_required_nullable=None, - parent_required='not nullable and required', - grand_parent_required_nullable=None, - grand_parent_required='not nullable and required', - optional_nullable=ApiHelper.SKIP, - optional_nullable_with_default_value='With default value', - optional=ApiHelper.SKIP, - child_class_array=ApiHelper.SKIP, - mclass=23, - precision=ApiHelper.SKIP, - big_decimal=ApiHelper.SKIP, - parent_optional_nullable_with_default_value='Has default value', - parent_optional=ApiHelper.SKIP, - grand_parent_optional=ApiHelper.SKIP): - """Constructor for the ChildClassModel class""" - - # Initialize members of the class - if optional_nullable is not ApiHelper.SKIP: - self.optional_nullable = optional_nullable - self.optional_nullable_with_default_value = optional_nullable_with_default_value - if optional is not ApiHelper.SKIP: - self.optional = optional - self.required_nullable = required_nullable - self.required = required - if child_class_array is not ApiHelper.SKIP: - self.child_class_array = child_class_array - - # Call the constructor for the base class - super(ChildClassModel, self).__init__(parent_required_nullable, - parent_required, - grand_parent_required_nullable, - grand_parent_required, - mclass, - precision, - big_decimal, - parent_optional_nullable_with_default_value, - parent_optional, - grand_parent_optional) - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - - required_nullable = dictionary.get("Required_Nullable") if dictionary.get("Required_Nullable") else None - required = dictionary.get("Required") - parent_required_nullable = dictionary.get("Parent_Required_Nullable") if dictionary.get("Parent_Required_Nullable") else None - parent_required = dictionary.get("Parent_Required") if dictionary.get("Parent_Required") else 'not nullable and required' - grand_parent_required_nullable = dictionary.get("Grand_Parent_Required_Nullable") if dictionary.get("Grand_Parent_Required_Nullable") else None - grand_parent_required = dictionary.get("Grand_Parent_Required") if dictionary.get("Grand_Parent_Required") else 'not nullable and required' - optional_nullable = dictionary.get("Optional_Nullable") if "Optional_Nullable" in dictionary.keys() else ApiHelper.SKIP - optional_nullable_with_default_value = dictionary.get("Optional_Nullable_With_Default_Value") if dictionary.get("Optional_Nullable_With_Default_Value") else 'With default value' - optional = dictionary.get("Optional") if dictionary.get("Optional") else ApiHelper.SKIP - if 'Child_Class_Array' in dictionary.keys(): - child_class_array = [ChildClassModel.from_dictionary(x) for x in dictionary.get('Child_Class_Array')] if dictionary.get('Child_Class_Array') else None - else: - child_class_array = ApiHelper.SKIP - mclass = dictionary.get("class") if dictionary.get("class") else 23 - precision = dictionary.get("precision") if "precision" in dictionary.keys() else ApiHelper.SKIP - big_decimal = dictionary.get("Big_Decimal") if "Big_Decimal" in dictionary.keys() else ApiHelper.SKIP - parent_optional_nullable_with_default_value = dictionary.get("Parent_Optional_Nullable_With_Default_Value") if dictionary.get("Parent_Optional_Nullable_With_Default_Value") else 'Has default value' - parent_optional = dictionary.get("Parent_Optional") if dictionary.get("Parent_Optional") else ApiHelper.SKIP - grand_parent_optional = dictionary.get("Grand_Parent_Optional") if dictionary.get("Grand_Parent_Optional") else ApiHelper.SKIP - # Return an object of this model - return cls(required, - required_nullable, - parent_required_nullable, - parent_required, - grand_parent_required_nullable, - grand_parent_required, - optional_nullable, - optional_nullable_with_default_value, - optional, - child_class_array, - mclass, - precision, - big_decimal, - parent_optional_nullable_with_default_value, - parent_optional, - grand_parent_optional) \ No newline at end of file + required: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("required", "Required"), + serialization_alias="Required") + ] = 'not nullable and required' + optional: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("optional", "Optional"), + serialization_alias="Optional") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _nullable_fields = { + 'required_nullable', 'mclass', 'precision', 'big_decimal', 'parent_optional_nullable_with_default_value', + 'parent_required_nullable', 'grand_parent_required_nullable' + } + _optional_fields = { + 'optional', 'mclass', 'precision', 'big_decimal', 'parent_optional_nullable_with_default_value', + 'parent_optional', 'grand_parent_optional' + } + + return ApiHelper.sanitize_model(nullable_fields=_nullable_fields, optional_fields=_optional_fields, + model_dump=nxt(self), model_fields=self.model_fields, + model_fields_set=self.model_fields_set) \ No newline at end of file diff --git a/tests/apimatic_core/mocks/models/inner_complex_type.py b/tests/apimatic_core/mocks/models/inner_complex_type.py index 7913774..ef7a22c 100644 --- a/tests/apimatic_core/mocks/models/inner_complex_type.py +++ b/tests/apimatic_core/mocks/models/inner_complex_type.py @@ -1,83 +1,37 @@ -import dateutil -from apimatic_core.utilities.api_helper import ApiHelper +from typing import Optional, List +from pydantic import BaseModel, Field, AliasChoices +from typing_extensions import Annotated -class InnerComplexType(object): - - """Implementation of the 'InnerComplexType' model. - - TODO: type model description here. +class InnerComplexType(BaseModel): + """Implementation of the 'InnerComplexType' model using Pydantic. Attributes: string_type (str): TODO: type description here. boolean_type (bool): TODO: type description here. - long_type (long|int): TODO: type description here. + long_type (int): TODO: type description here. precision_type (float): TODO: type description here. - string_list_type (list of str): TODO: type description here. - + string_list_type (list[str]): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "boolean_type": 'booleanType', - "date_time_type": 'dateTimeType', - "date_type": 'dateType', - "long_type": 'longType', - "precision_type": 'precisionType', - "string_list_type": 'stringListType', - "string_type": 'stringType' - } - - def __init__(self, - boolean_type=None, - long_type=None, - precision_type=None, - string_list_type=None, - string_type=None, - additional_properties={}): - """Constructor for the InnerComplexType class""" - - # Initialize members of the class - self.string_type = string_type - self.boolean_type = boolean_type - self.long_type = long_type - self.precision_type = precision_type - self.string_list_type = string_list_type - - # Add additional model properties to the instance - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None + string_type: Annotated[ + Optional[str], + Field(AliasChoices("string_type", "stringType"), serialization_alias="stringType") + ] = None + boolean_type: Annotated[ + Optional[bool], + Field(AliasChoices("boolean_type", "booleanType"), serialization_alias="booleanType") + ] = None + long_type: Annotated[ + Optional[int], + Field(AliasChoices("long_type", "longType"), serialization_alias="longType") + ] = None + precision_type: Annotated[ + Optional[float], + Field(AliasChoices("precision_type", "precisionType"), serialization_alias="precisionType") + ] = None + string_list_type: Annotated[ + Optional[List[str]], + Field(AliasChoices("string_list_type", "stringListType"), serialization_alias="stringListType") + ] = None - # Extract variables from the dictionary - boolean_type = dictionary.get("booleanType") if "booleanType" in dictionary.keys() else None - long_type = dictionary.get("longType") if dictionary.get("longType") else None - precision_type = dictionary.get("precisionType") if dictionary.get("precisionType") else None - string_list_type = dictionary.get("stringListType") if dictionary.get("stringListType") else None - string_type = dictionary.get("stringType") if dictionary.get("stringType") else None - # Clean out expected properties from dictionary - for key in cls._names.values(): - if key in dictionary: - del dictionary[key] - # Return an object of this model - return cls(boolean_type, - long_type, - precision_type, - string_list_type, - string_type, - dictionary) diff --git a/tests/apimatic_core/mocks/models/lion.py b/tests/apimatic_core/mocks/models/lion.py index d3b454d..40da238 100644 --- a/tests/apimatic_core/mocks/models/lion.py +++ b/tests/apimatic_core/mocks/models/lion.py @@ -1,96 +1,50 @@ +from typing import Optional + +from pydantic import BaseModel, AliasChoices, Field, model_serializer +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from typing_extensions import Annotated + from apimatic_core.utilities.api_helper import ApiHelper -class Lion(object): +class Lion(BaseModel): """Implementation of the 'Lion' model. TODO: type model description here. Attributes: - id (int): TODO: type description here. + id (str): TODO: type description here. weight (int): TODO: type description here. mtype (str): TODO: type description here. kind (str): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "id": 'id', - "weight": 'weight', - "mtype": 'type', - "kind": 'kind' - } - - _optionals = [ - 'kind', + id: Annotated[ + str, + Field(validation_alias=AliasChoices("id", "id"), + serialization_alias="id") ] - - def __init__(self, - id=None, - weight=None, - mtype=None, - kind=ApiHelper.SKIP): - """Constructor for the Lion class""" - - # Initialize members of the class - self.id = id - self.weight = weight - self.mtype = mtype - if kind is not ApiHelper.SKIP: - self.kind = kind - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - id = dictionary.get("id") if dictionary.get("id") else None - weight = dictionary.get("weight") if dictionary.get("weight") else None - mtype = dictionary.get("type") if dictionary.get("type") else None - kind = dictionary.get("kind") if dictionary.get("kind") else ApiHelper.SKIP - # Return an object of this model - return cls(id, - weight, - mtype, - kind) - - @classmethod - def validate(cls, dictionary): - """Validates dictionary against class required properties - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - boolean : if dictionary is valid contains required properties. - - """ - - if isinstance(dictionary, cls): - return ApiHelper.is_valid_type(value=dictionary.id, type_callable=lambda value: isinstance(value, int)) \ - and ApiHelper.is_valid_type(value=dictionary.weight, type_callable=lambda value: isinstance(value, int)) \ - and ApiHelper.is_valid_type(value=dictionary.mtype, type_callable=lambda value: isinstance(value, str)) - - if not isinstance(dictionary, dict): - return False - - return ApiHelper.is_valid_type(value=dictionary.get('id'), type_callable=lambda value: isinstance(value, int)) \ - and ApiHelper.is_valid_type(value=dictionary.get('weight'), type_callable=lambda value: isinstance(value, int)) \ - and ApiHelper.is_valid_type(value=dictionary.get('type'), type_callable=lambda value: isinstance(value, str)) + weight: Annotated[ + int, + Field(validation_alias=AliasChoices("weight", "weight"), + serialization_alias="weight") + ] + mtype: Annotated[ + str, + Field(validation_alias=AliasChoices("mtype", "type"), + serialization_alias="type") + ] + kind: Annotated[ + Optional[str], + Field(validation_alias=AliasChoices("kind", "kind"), + serialization_alias="kind") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"kind"} + + return ApiHelper.sanitize_model(optional_fields=_optional_fields, model_dump=nxt(self), + model_fields=self.model_fields, model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/mocks/models/model_with_additional_properties.py b/tests/apimatic_core/mocks/models/model_with_additional_properties.py index 124aa7f..c5a1417 100644 --- a/tests/apimatic_core/mocks/models/model_with_additional_properties.py +++ b/tests/apimatic_core/mocks/models/model_with_additional_properties.py @@ -1,10 +1,15 @@ # -*- coding: utf-8 -*- -from tests.apimatic_core.mocks.union_type_lookup import UnionTypeLookUp +from typing import Optional, Dict, List, Union, Any + +from pydantic import BaseModel, Field, AliasChoices, model_validator, model_serializer +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from typing_extensions import Annotated + from tests.apimatic_core.mocks.models.lion import Lion from apimatic_core.utilities.api_helper import ApiHelper -class ModelWithAdditionalPropertiesOfPrimitiveType(object): +class ModelWithAdditionalPropertiesOfPrimitiveType(BaseModel): """Implementation of the 'NonInheritEnabledNumber' model. @@ -15,58 +20,59 @@ class ModelWithAdditionalPropertiesOfPrimitiveType(object): """ - # Create a mapping from Model property names to API property names - _names = { - "email": 'email' - } - - def __init__(self, - email=None, - additional_properties=None): - """Constructor for the NonInheritEnabledNumber class - Args: - email (str): TODO: type description here. - additional_properties (dict[str, int]): TODO: type description here. - """ + email: Annotated[ + str, + Field(validation_alias=AliasChoices("email", "email"), + serialization_alias="email") + ] - # Initialize members of the class - if additional_properties is None: - additional_properties = {} - self.email = email - self.additional_properties = additional_properties + additional_properties: Optional[Dict[str, int]] = None - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary + @model_validator(mode="before") + def populate_additional_properties(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """ + Collect all extra fields and move them into `additional_properties`. + Raise ValueError if an additional property has a name similar to a model property. Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. + values (Dict[str, Any]): The input data for the model. Returns: - object: An instance of this structure class. - + Dict[str, Any]: The modified data with additional properties populated. """ + additional_props_field = "additional_properties" - if dictionary is None: - return None + if isinstance(values.get(additional_props_field), dict): + ApiHelper.check_conflicts_with_additional_properties( + cls, values[additional_props_field], additional_props_field) + return values - # Extract variables from the dictionary - email = dictionary.get("email") if dictionary.get("email") else None + aliases = [field.serialization_alias or name for name, field in cls.model_fields.items()] - additional_properties = ApiHelper.get_additional_properties( - dictionary={k: v for k, v in dictionary.items() if k not in cls._names.values()}, - unboxing_function=lambda x: int(x)) + additional_properties = { + key: value for key, value in values.items() if key not in aliases + } - # Return an object of this model - return cls(email, additional_properties) + ApiHelper.check_conflicts_with_additional_properties(cls, additional_properties, additional_props_field) + values[additional_props_field] = additional_properties + return values -class ModelWithAdditionalPropertiesOfPrimitiveArrayType(object): + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"additional_properties"} - """Implementation of the 'NonInheritEnabledNumber' model. + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model(optional_fields=_optional_fields, + model_dump=serialized_model, model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + return {**sanitized_model, **(additional_properties or {})} + + +class ModelWithAdditionalPropertiesOfPrimitiveArrayType(BaseModel): + + """"Implementation of the 'NonInheritEnabledNumber' model. TODO: type model description here. @@ -75,57 +81,56 @@ class ModelWithAdditionalPropertiesOfPrimitiveArrayType(object): """ - # Create a mapping from Model property names to API property names - _names = { - "email": 'email' - } + email: Annotated[ + str, + Field(validation_alias=AliasChoices("email", "email"), + serialization_alias="email") + ] - def __init__(self, - email=None, - additional_properties=None): - """Constructor for the NonInheritEnabledNumber class - - Args: - email (str): TODO: type description here. - additional_properties (dict[str, list[int]]): TODO: type description here. + additional_properties: Optional[Dict[str, List[int]]] = None + @model_validator(mode="before") + def populate_additional_properties(cls, values: Dict[str, Any]) -> Dict[str, Any]: """ - - # Initialize members of the class - if additional_properties is None: - additional_properties = {} - self.email = email - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary + Collect all extra fields and move them into `additional_properties`. + Raise ValueError if an additional property has a name similar to a model property. Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. + values (Dict[str, Any]): The input data for the model. Returns: - object: An instance of this structure class. - + Dict[str, Any]: The modified data with additional properties populated. """ + additional_props_field = "additional_properties" + + if isinstance(values.get(additional_props_field), dict): + ApiHelper.check_conflicts_with_additional_properties( + cls, values[additional_props_field], additional_props_field) + return values + + aliases = [field.serialization_alias or name for name, field in cls.model_fields.items()] + + additional_properties = { + key: value for key, value in values.items() if key not in aliases + } - if dictionary is None: - return None + ApiHelper.check_conflicts_with_additional_properties(cls, additional_properties, additional_props_field) - # Extract variables from the dictionary - email = dictionary.get("email") if dictionary.get("email") else None + values[additional_props_field] = additional_properties + return values - additional_properties = ApiHelper.get_additional_properties( - dictionary={k: v for k, v in dictionary.items() if k not in cls._names.values()}, - unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: int(item), is_array=True)) + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"additional_properties"} - # Return an object of this model - return cls(email, additional_properties) + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model(optional_fields=_optional_fields, + model_dump=serialized_model, model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + return {**sanitized_model, **(additional_properties or {})} -class ModelWithAdditionalPropertiesOfPrimitiveDictType(object): +class ModelWithAdditionalPropertiesOfPrimitiveDictType(BaseModel): """Implementation of the 'NonInheritEnabledNumber' model. @@ -136,57 +141,56 @@ class ModelWithAdditionalPropertiesOfPrimitiveDictType(object): """ - # Create a mapping from Model property names to API property names - _names = { - "email": 'email' - } + email: Annotated[ + str, + Field(validation_alias=AliasChoices("email", "email"), + serialization_alias="email") + ] - def __init__(self, - email=None, - additional_properties=None): - """Constructor for the NonInheritEnabledNumber class - - Args: - email (str): TODO: type description here. - additional_properties (dict[str, dict[str, int]]): TODO: type description here. + additional_properties: Optional[Dict[str, Dict[str, int]]] = None + @model_validator(mode="before") + def populate_additional_properties(cls, values: Dict[str, Any]) -> Dict[str, Any]: """ - - # Initialize members of the class - if additional_properties is None: - additional_properties = {} - self.email = email - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary + Collect all extra fields and move them into `additional_properties`. + Raise ValueError if an additional property has a name similar to a model property. Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. + values (Dict[str, Any]): The input data for the model. Returns: - object: An instance of this structure class. - + Dict[str, Any]: The modified data with additional properties populated. """ + additional_props_field = "additional_properties" + + if isinstance(values.get(additional_props_field), dict): + ApiHelper.check_conflicts_with_additional_properties( + cls, values[additional_props_field], additional_props_field) + return values + + aliases = [field.serialization_alias or name for name, field in cls.model_fields.items()] + + additional_properties = { + key: value for key, value in values.items() if key not in aliases + } - if dictionary is None: - return None + ApiHelper.check_conflicts_with_additional_properties(cls, additional_properties, additional_props_field) - # Extract variables from the dictionary - email = dictionary.get("email") if dictionary.get("email") else None + values[additional_props_field] = additional_properties + return values - additional_properties = ApiHelper.get_additional_properties( - dictionary={k: v for k, v in dictionary.items() if k not in cls._names.values()}, - unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: int(item), is_dict=True)) + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"additional_properties"} - # Return an object of this model - return cls(email, additional_properties) + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model(optional_fields=_optional_fields, + model_dump=serialized_model, model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + return {**sanitized_model, **(additional_properties or {})} -class ModelWithAdditionalPropertiesOfModelType(object): +class ModelWithAdditionalPropertiesOfModelType(BaseModel): """Implementation of the 'NonInheritEnabledNumber' model. @@ -197,57 +201,56 @@ class ModelWithAdditionalPropertiesOfModelType(object): """ - # Create a mapping from Model property names to API property names - _names = { - "email": 'email' - } + email: Annotated[ + str, + Field(validation_alias=AliasChoices("email", "email"), + serialization_alias="email") + ] - def __init__(self, - email=None, - additional_properties=None): - """Constructor for the NonInheritEnabledNumber class - - Args: - email (str): TODO: type description here. - additional_properties (dict[str, Lion]): TODO: type description here. + additional_properties: Optional[Dict[str, Lion]] = None + @model_validator(mode="before") + def populate_additional_properties(cls, values: Dict[str, Any]) -> Dict[str, Any]: """ - - # Initialize members of the class - if additional_properties is None: - additional_properties = {} - self.email = email - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary + Collect all extra fields and move them into `additional_properties`. + Raise ValueError if an additional property has a name similar to a model property. Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. + values (Dict[str, Any]): The input data for the model. Returns: - object: An instance of this structure class. - + Dict[str, Any]: The modified data with additional properties populated. """ + additional_props_field = "additional_properties" + + if isinstance(values.get(additional_props_field), dict): + ApiHelper.check_conflicts_with_additional_properties( + cls, values[additional_props_field], additional_props_field) + return values + + aliases = [field.serialization_alias or name for name, field in cls.model_fields.items()] - if dictionary is None: - return None + additional_properties = { + key: value for key, value in values.items() if key not in aliases + } - # Extract variables from the dictionary - email = dictionary.get("email") if dictionary.get("email") else None + ApiHelper.check_conflicts_with_additional_properties(cls, additional_properties, additional_props_field) - additional_properties = ApiHelper.get_additional_properties( - dictionary={k: v for k, v in dictionary.items() if k not in cls._names.values()}, - unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: Lion.from_dictionary(item))) + values[additional_props_field] = additional_properties + return values - # Return an object of this model - return cls(email, additional_properties) + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"additional_properties"} -class ModelWithAdditionalPropertiesOfModelArrayType(object): + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model(optional_fields=_optional_fields, + model_dump=serialized_model, model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + return {**sanitized_model, **(additional_properties or {})} + +class ModelWithAdditionalPropertiesOfModelArrayType(BaseModel): """Implementation of the 'NonInheritEnabledNumber' model. @@ -258,57 +261,56 @@ class ModelWithAdditionalPropertiesOfModelArrayType(object): """ - # Create a mapping from Model property names to API property names - _names = { - "email": 'email' - } - - def __init__(self, - email=None, - additional_properties=None): - """Constructor for the NonInheritEnabledNumber class + email: Annotated[ + str, + Field(validation_alias=AliasChoices("email", "email"), + serialization_alias="email") + ] - Args: - email (str): TODO: type description here. - additional_properties (dict[str, list[Lion]]): TODO: type description here. + additional_properties: Optional[Dict[str, List[Lion]]] = None + @model_validator(mode="before") + def populate_additional_properties(cls, values: Dict[str, Any]) -> Dict[str, Any]: """ - - # Initialize members of the class - if additional_properties is None: - additional_properties = {} - self.email = email - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary + Collect all extra fields and move them into `additional_properties`. + Raise ValueError if an additional property has a name similar to a model property. Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. + values (Dict[str, Any]): The input data for the model. Returns: - object: An instance of this structure class. - + Dict[str, Any]: The modified data with additional properties populated. """ + additional_props_field = "additional_properties" + + if isinstance(values.get(additional_props_field), dict): + ApiHelper.check_conflicts_with_additional_properties( + cls, values[additional_props_field], additional_props_field) + return values + + aliases = [field.serialization_alias or name for name, field in cls.model_fields.items()] - if dictionary is None: - return None + additional_properties = { + key: value for key, value in values.items() if key not in aliases + } - # Extract variables from the dictionary - email = dictionary.get("email") if dictionary.get("email") else None + ApiHelper.check_conflicts_with_additional_properties(cls, additional_properties, additional_props_field) - additional_properties = ApiHelper.get_additional_properties( - dictionary={k: v for k, v in dictionary.items() if k not in cls._names.values()}, - unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: Lion.from_dictionary(item), is_array=True)) + values[additional_props_field] = additional_properties + return values - # Return an object of this model - return cls(email, additional_properties) + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"additional_properties"} -class ModelWithAdditionalPropertiesOfModelDictType(object): + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model(optional_fields=_optional_fields, + model_dump=serialized_model, model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + return {**sanitized_model, **(additional_properties or {})} + +class ModelWithAdditionalPropertiesOfModelDictType(BaseModel): """Implementation of the 'NonInheritEnabledNumber' model. @@ -319,59 +321,56 @@ class ModelWithAdditionalPropertiesOfModelDictType(object): """ - # Create a mapping from Model property names to API property names - _names = { - "email": 'email' - } - - def __init__(self, - email=None, - additional_properties=None): - """Constructor for the NonInheritEnabledNumber class + email: Annotated[ + str, + Field(validation_alias=AliasChoices("email", "email"), + serialization_alias="email") + ] - Args: - email (str): TODO: type description here. - additional_properties (dict[str, dict[str, Lion]]): TODO: type description here. + additional_properties: Optional[Dict[str, Dict[str, Lion]]] = None + @model_validator(mode="before") + def populate_additional_properties(cls, values: Dict[str, Any]) -> Dict[str, Any]: """ - - # Initialize members of the class - if additional_properties is None: - additional_properties = {} - self.email = email - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary + Collect all extra fields and move them into `additional_properties`. + Raise ValueError if an additional property has a name similar to a model property. Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. + values (Dict[str, Any]): The input data for the model. Returns: - object: An instance of this structure class. - + Dict[str, Any]: The modified data with additional properties populated. """ + additional_props_field = "additional_properties" + + if isinstance(values.get(additional_props_field), dict): + ApiHelper.check_conflicts_with_additional_properties( + cls, values[additional_props_field], additional_props_field) + return values - if dictionary is None: - return None + aliases = [field.serialization_alias or name for name, field in cls.model_fields.items()] - # Extract variables from the dictionary - email = dictionary.get("email") if dictionary.get("email") else None + additional_properties = { + key: value for key, value in values.items() if key not in aliases + } - additional_properties = ApiHelper.get_additional_properties( - dictionary={k: v for k, v in dictionary.items() if k not in cls._names.values()}, - unboxing_function=lambda x: ApiHelper.apply_unboxing_function( - x, lambda item: Lion.from_dictionary(item), - is_dict=True)) + ApiHelper.check_conflicts_with_additional_properties(cls, additional_properties, additional_props_field) - # Return an object of this model - return cls(email, additional_properties) + values[additional_props_field] = additional_properties + return values -class ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive(object): + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"additional_properties"} + + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model(optional_fields=_optional_fields, + model_dump=serialized_model, model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + return {**sanitized_model, **(additional_properties or {})} + +class ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive(BaseModel): """Implementation of the 'NonInheritEnabledNumber' model. @@ -382,53 +381,51 @@ class ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive(object): """ - # Create a mapping from Model property names to API property names - _names = { - "email": 'email' - } - - def __init__(self, - email=None, - additional_properties=None): - """Constructor for the NonInheritEnabledNumber class + email: Annotated[ + str, + Field(validation_alias=AliasChoices("email", "email"), + serialization_alias="email") + ] - Args: - email (str): TODO: type description here. - additional_properties (dict[str, float|bool]): TODO: type description here. + additional_properties: Optional[Dict[str, Union[float, bool]]] = None + @model_validator(mode="before") + def populate_additional_properties(cls, values: Dict[str, Any]) -> Dict[str, Any]: """ - - # Initialize members of the class - if additional_properties is None: - additional_properties = {} - self.email = email - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary + Collect all extra fields and move them into `additional_properties`. + Raise ValueError if an additional property has a name similar to a model property. Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. + values (Dict[str, Any]): The input data for the model. Returns: - object: An instance of this structure class. - + Dict[str, Any]: The modified data with additional properties populated. """ + additional_props_field = "additional_properties" + + if isinstance(values.get(additional_props_field), dict): + ApiHelper.check_conflicts_with_additional_properties( + cls, values[additional_props_field], additional_props_field) + return values + + aliases = [field.serialization_alias or name for name, field in cls.model_fields.items()] + + additional_properties = { + key: value for key, value in values.items() if key not in aliases + } - if dictionary is None: - return None + ApiHelper.check_conflicts_with_additional_properties(cls, additional_properties, additional_props_field) - # Extract variables from the dictionary - email = dictionary.get("email") if dictionary.get("email") else None + values[additional_props_field] = additional_properties + return values - additional_properties = ApiHelper.get_additional_properties( - dictionary={k: v for k, v in dictionary.items() if k not in cls._names.values()}, - unboxing_function=lambda x: ApiHelper.deserialize_union_type(UnionTypeLookUp.get('ScalarModelAnyOfRequired'), - x, False)) + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"additional_properties"} - # Return an object of this model - return cls(email, additional_properties) \ No newline at end of file + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model(optional_fields=_optional_fields, + model_dump=serialized_model, model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + return {**sanitized_model, **(additional_properties or {})} \ No newline at end of file diff --git a/tests/apimatic_core/mocks/models/months.py b/tests/apimatic_core/mocks/models/months.py index baba247..ad20a1f 100644 --- a/tests/apimatic_core/mocks/models/months.py +++ b/tests/apimatic_core/mocks/models/months.py @@ -1,5 +1,8 @@ +from enum import Enum +from typing import Optional -class Months(object): + +class Months(int, Enum): """Implementation of the 'Months' enum. @@ -19,7 +22,6 @@ class Months(object): DECEMBER: TODO: type description here. """ - _all_values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] JANUARY = 1 @@ -46,17 +48,5 @@ class Months(object): DECEMBER = 12 @classmethod - def validate(cls, value): - """Validates value against enum. - - Args: - value: the value to be validated against. - - Returns: - boolean : if value is valid for this model. - - """ - if value is None: - return None - - return value in cls._all_values \ No newline at end of file + def is_valid(cls, value: Optional[str]) -> bool: + return value in cls._value2member_map_ \ No newline at end of file diff --git a/tests/apimatic_core/mocks/models/one_of_xml.py b/tests/apimatic_core/mocks/models/one_of_xml.py index b2d73ea..0e19123 100644 --- a/tests/apimatic_core/mocks/models/one_of_xml.py +++ b/tests/apimatic_core/mocks/models/one_of_xml.py @@ -1,33 +1,31 @@ +from typing import Optional, Union, List + +from pydantic import AliasChoices, Field, BaseModel +from typing_extensions import Annotated +import xml.etree.ElementTree as ET from apimatic_core.utilities.xml_helper import XmlHelper from tests.apimatic_core.mocks.models.cat_model import CatModel from tests.apimatic_core.mocks.models.dog_model import DogModel from tests.apimatic_core.mocks.models.wolf_model import WolfModel -class OneOfXML(object): +class OneOfXML(BaseModel): """Implementation of the 'CatsOrADogOrWolves' model. Case 3 Attributes: - value (object): TODO: type description here. + value (CatModel | DogModel | WolfModel): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "value": 'value' - } - - def __init__(self, - value=None): - """Constructor for the CatsOrADogOrWolvesModel class""" - - # Initialize members of the class - self.value = value + value: Annotated[ + Optional[Union[List[CatModel], DogModel, List[WolfModel]]], + Field(AliasChoices("value", "value"), serialization_alias="value") + ] = None @classmethod - def from_element(cls, root): + def from_element(cls, root: ET.Element) -> "OneOfXML": """Initialize an instance of this class using an xml.etree.Element. Args: @@ -46,18 +44,18 @@ def from_element(cls, root): } ) - return cls(value) + return cls(value=value) - def to_xml_sub_element(self, root): + def to_xml_sub_element(self, root: ET.Element): """Convert this object to an instance of xml.etree.Element. Args: root (xml.etree.Element): The parent of this xml element. """ - if type(self.value) is list and type(self.value[0]) is CatModel: + if isinstance(self.value, list) and type(self.value[0]) is CatModel: XmlHelper.add_list_as_subelement(root, self.value, 'Cat') if type(self.value) is DogModel: XmlHelper.add_as_subelement(root, self.value, 'Dog') - if type(self.value) is list and type(self.value[0]) is WolfModel: + if isinstance(self.value, list) and type(self.value[0]) is WolfModel: XmlHelper.add_list_as_subelement(root, self.value, 'Wolf', wrapping_element_name='Items') diff --git a/tests/apimatic_core/mocks/models/orbit.py b/tests/apimatic_core/mocks/models/orbit.py index a124c09..d894c53 100644 --- a/tests/apimatic_core/mocks/models/orbit.py +++ b/tests/apimatic_core/mocks/models/orbit.py @@ -1,7 +1,11 @@ +from pydantic import BaseModel, Field, AliasChoices, model_serializer +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from typing_extensions import Annotated + from apimatic_core.utilities.api_helper import ApiHelper -class Orbit(object): +class Orbit(BaseModel): """Implementation of the 'Orbit' model. @@ -17,61 +21,13 @@ class Orbit(object): "orbit_number_of_electrons": 'OrbitNumberOfElectrons' } - def __init__(self, - orbit_number_of_electrons=None): - """Constructor for the Orbit class""" - - # Initialize members of the class - self.orbit_number_of_electrons = orbit_number_of_electrons - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - orbit_number_of_electrons = dictionary.get("OrbitNumberOfElectrons") \ - if dictionary.get("OrbitNumberOfElectrons") else None - # Return an object of this model - return cls(orbit_number_of_electrons) - - @classmethod - def validate(cls, dictionary): - """Validates dictionary against class required properties - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - boolean : if dictionary is valid contains required properties. - - """ - - if isinstance(dictionary, cls): - return ApiHelper.is_valid_type(value=dictionary.orbit_number_of_electrons, - type_callable=lambda value: isinstance(value, int)) - - if not isinstance(dictionary, dict): - return False - - return ApiHelper.is_valid_type(value=dictionary.get('OrbitNumberOfElectrons'), - type_callable=lambda value: isinstance(value, int)) + orbit_number_of_electrons: Annotated[ + int, + Field(validation_alias=AliasChoices("orbit_number_of_electrons", "OrbitNumberOfElectrons"), + serialization_alias="OrbitNumberOfElectrons") + ] - def __eq__(self, other): - if isinstance(self, other.__class__): - return self.orbit_number_of_electrons == other.orbit_number_of_electrons - return False + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + return ApiHelper.sanitize_model(model_dump=nxt(self), model_fields=self.model_fields, + model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/mocks/models/person.py b/tests/apimatic_core/mocks/models/person.py index 9724f0a..b05e174 100644 --- a/tests/apimatic_core/mocks/models/person.py +++ b/tests/apimatic_core/mocks/models/person.py @@ -1,112 +1,127 @@ -import dateutil.parser +# -*- coding: utf-8 -*- + +""" +typecombinatormoderate + +This file was automatically generated by APIMATIC v3.0 ( + https://www.apimatic.io ). +""" +from typing import Literal, Dict, Any +from typing import List, Optional, Union + +from datetime import date, datetime + from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core.utilities.datetime_helper import DateTimeHelper +from pydantic import AliasChoices, Field, model_serializer, BeforeValidator, PlainSerializer, model_validator +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from pydantic import BaseModel +from typing_extensions import Annotated +from tests.apimatic_core.mocks.models.days import Days -class Person(object): + +class Person(BaseModel): """Implementation of the 'Person' model. TODO: type model description here. Attributes: - address (string): TODO: type description here. + address (str): TODO: type description here. age (long|int): TODO: type description here. birthday (date): TODO: type description here. birthtime (datetime): TODO: type description here. - name (string): TODO: type description here. - uid (string): TODO: type description here. - person_type (string): TODO: type description here. + name (str): TODO: type description here. + uid (str): TODO: type description here. + person_type (str): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "address": 'address', - "age": 'age', - "birthday": 'birthday', - "birthtime": 'birthtime', - "name": 'name', - "uid": 'uid', - "person_type": 'personType' - } - - _optionals = [ - 'person_type', + class Config: + use_enum_values = True # Ensures JSON serialization uses enum values, not names + + address: Annotated[ + str, + Field(validation_alias=AliasChoices("address", "address"), + serialization_alias="address") + ] + age: Annotated[ + int, + Field(validation_alias=AliasChoices("age", "age"), + serialization_alias="age") + ] + birthday: Annotated[ + date, + Field(validation_alias=AliasChoices("birthday", "birthday"), + serialization_alias="birthday"), + PlainSerializer(lambda v: v.isoformat(), return_type=str) + ] + + birthtime: Annotated[ + datetime, + Field(validation_alias=AliasChoices("birthtime", "birthtime"), + serialization_alias="birthtime"), + BeforeValidator(DateTimeHelper.try_parse_from_rfc3339_date_time), + PlainSerializer(DateTimeHelper.to_rfc3339_date_time, when_used='unless-none') + ] + name: Annotated[ + str, + Field(validation_alias=AliasChoices("name", "name"), + serialization_alias="name") + ] + uid: Annotated[ + str, + Field(validation_alias=AliasChoices("uid", "uid"), + serialization_alias="uid") ] + person_type: Annotated[ + Union[Literal["Per"], Literal["Empl"], Literal["Post"]], + Field(validation_alias=AliasChoices("person_type", "personType"), + serialization_alias="personType") + ] + additional_properties: Optional[Dict[str, Any]] = None - def __init__(self, - address=None, - age=None, - birthday=None, - birthtime=None, - name=None, - uid=None, - person_type='Per', - additional_properties={}): - """Constructor for the Person class""" - - # Initialize members of the class - self.address = address - self.age = age - self.birthday = birthday - self.birthtime = ApiHelper.RFC3339DateTime(birthtime) if birthtime else None - self.name = name - self.uid = uid - self.person_type = person_type - - # Add additional model properties to the instance - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary + @model_validator(mode="before") + def populate_additional_properties(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """ + Collect all extra fields and move them into `additional_properties`. + Raise ValueError if an additional property has a name similar to a model property. Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. + values (Dict[str, Any]): The input data for the model. Returns: - object: An instance of this structure class. - + Dict[str, Any]: The modified data with additional properties populated. """ - if dictionary is None: - return None + additional_props_field = "additional_properties" + + if isinstance(values.get(additional_props_field), dict): + ApiHelper.check_conflicts_with_additional_properties( + cls, values.get(additional_props_field), additional_props_field) + return values - discriminators = { - 'Empl': Employee.from_dictionary + aliases = [field.serialization_alias or name for name, field in cls.model_fields.items()] + + additional_properties = { + key: value for key, value in values.items() if key not in aliases } - unboxer = discriminators.get(dictionary.get('personType')) - - # Delegate unboxing to another function if a discriminator - # value for a child class is present. - if unboxer: - return unboxer(dictionary) - - # Extract variables from the dictionary - - address = dictionary.get("address") if dictionary.get("address") else None - age = dictionary.get("age") if dictionary.get("age") else None - birthday = dateutil.parser.parse(dictionary.get('birthday')).date() if dictionary.get('birthday') else None - birthtime = ApiHelper.RFC3339DateTime.from_value(dictionary.get("birthtime")).datetime if dictionary.get("birthtime") else None - name = dictionary.get("name") if dictionary.get("name") else None - uid = dictionary.get("uid") if dictionary.get("uid") else None - person_type = dictionary.get("personType") if dictionary.get("personType") else 'Per' - # Clean out expected properties from dictionary - for key in cls._names.values(): - if key in dictionary: - del dictionary[key] - # Return an object of this model - return cls(address, - age, - birthday, - birthtime, - name, - uid, - person_type, - dictionary) + ApiHelper.check_conflicts_with_additional_properties(cls, additional_properties, additional_props_field) + + values[additional_props_field] = additional_properties + return values + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"additional_properties"} + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model( + optional_fields=_optional_fields, model_dump=serialized_model, + model_fields=self.model_fields, model_fields_set=self.model_fields_set) + + return {**sanitized_model, **(additional_properties or {})} class Employee(Person): @@ -116,129 +131,64 @@ class Employee(Person): NOTE: This class inherits from 'Person'. Attributes: - department (string): TODO: type description here. - dependents (list of Person): TODO: type description here. + department (str): TODO: type description here. + dependents (List[Person]): TODO: type description here. hired_at (datetime): TODO: type description here. - joining_day (Days): TODO: type description here. + joining_day (DaysEnum): TODO: type description here. salary (int): TODO: type description here. - working_days (list of Days): TODO: type description here. + working_days (List[DaysEnum]): TODO: type description here. + boss (Person): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "address": 'address', - "age": 'age', - "birthday": 'birthday', - "birthtime": 'birthtime', - "department": 'department', - "dependents": 'dependents', - "hired_at": 'hiredAt', - "joining_day": 'joiningDay', - "name": 'name', - "salary": 'salary', - "uid": 'uid', - "working_days": 'workingDays', - "person_type": 'personType' - } - - _nullables = [] - - def __init__(self, - address=None, - age=None, - birthday=None, - birthtime=None, - department=None, - dependents=None, - hired_at=None, - joining_day='Monday', - name=None, - salary=None, - uid=None, - working_days=None, - person_type='Empl', - additional_properties={}): - """Constructor for the Employee class""" - - # Initialize members of the class - self.department = department - self.dependents = dependents - self.hired_at = ApiHelper.HttpDateTime(hired_at) if hired_at else None - self.joining_day = joining_day - self.salary = salary - self.working_days = working_days - - # Add additional model properties to the instance - self.additional_properties = additional_properties - - # Call the constructor for the base class - super(Employee, self).__init__(address, - age, - birthday, - birthtime, - name, - uid, - person_type) - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - discriminators = {} - unboxer = discriminators.get(dictionary.get('personType')) - - # Delegate unboxing to another function if a discriminator - # value for a child class is present. - if unboxer: - return unboxer(dictionary) - - # Extract variables from the dictionary - - address = dictionary.get("address") if dictionary.get("address") else None - age = dictionary.get("age") if dictionary.get("age") else None - birthday = dateutil.parser.parse(dictionary.get('birthday')).date() if dictionary.get('birthday') else None - birthtime = ApiHelper.RFC3339DateTime.from_value(dictionary.get("birthtime")).datetime if dictionary.get("birthtime") else None - department = dictionary.get("department") if dictionary.get("department") else None - dependents = None - if dictionary.get('dependents') is not None: - dependents = [Person.from_dictionary(x) for x in dictionary.get('dependents')] - hired_at = ApiHelper.HttpDateTime.from_value(dictionary.get("hiredAt")).datetime if dictionary.get("hiredAt") else None - joining_day = dictionary.get("joiningDay") if dictionary.get("joiningDay") else 'Monday' - name = dictionary.get("name") if dictionary.get("name") else None - salary = dictionary.get("salary") if dictionary.get("salary") else None - uid = dictionary.get("uid") if dictionary.get("uid") else None - working_days = dictionary.get("workingDays") if dictionary.get("workingDays") else None - person_type = dictionary.get("personType") if dictionary.get("personType") else 'Empl' - # Clean out expected properties from dictionary - for key in cls._names.values(): - if key in dictionary: - del dictionary[key] - # Return an object of this model - return cls(address, - age, - birthday, - birthtime, - department, - dependents, - hired_at, - joining_day, - name, - salary, - uid, - working_days, - person_type, - dictionary) + department: Annotated[ + str, + Field(validation_alias=AliasChoices("department", "department"), + serialization_alias="department"), + ] + dependents: Annotated[ + List[Person], + Field(validation_alias=AliasChoices("dependents", "dependents"), + serialization_alias="dependents"), + ] + hired_at: Annotated[ + datetime, + Field(validation_alias=AliasChoices("hired_at", "hiredAt"), + serialization_alias="hiredAt"), + BeforeValidator(DateTimeHelper.try_parse_from_rfc1123_date_time), + PlainSerializer(DateTimeHelper.to_rfc1123_date_time, when_used='unless-none') + ] + joining_day: Annotated[ + Optional[Days], + Field(validation_alias=AliasChoices("joining_day", "joiningDay"), + serialization_alias="joiningDay") + ] = Days.MONDAY + salary: Annotated[ + int, + Field(validation_alias=AliasChoices("salary", "salary"), + serialization_alias="salary") + ] + working_days: Annotated[ + List[Days], + Field(validation_alias=AliasChoices("working_days", "workingDays"), + serialization_alias="workingDays") + ] + boss: Annotated[ + Optional[Person], + Field(validation_alias=AliasChoices("boss", "boss"), + serialization_alias="boss") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _nullable_fields = {"boss"} + _optional_fields = {"boss", "joining_day", "additional_properties"} + + serialized_model = nxt(self) + additional_properties = serialized_model.pop("additional_properties", {}) + sanitized_model = ApiHelper.sanitize_model( + nullable_fields=_nullable_fields, optional_fields=_optional_fields, + model_dump=serialized_model, model_fields=self.model_fields, + model_fields_set=self.model_fields_set) + + return {**sanitized_model, **(additional_properties or {})} diff --git a/tests/apimatic_core/mocks/models/rabbit.py b/tests/apimatic_core/mocks/models/rabbit.py index ff1cc09..7bc03fb 100644 --- a/tests/apimatic_core/mocks/models/rabbit.py +++ b/tests/apimatic_core/mocks/models/rabbit.py @@ -1,94 +1,27 @@ +from typing import Optional +from pydantic import BaseModel, Field, model_serializer +from pydantic_core.core_schema import SerializerFunctionWrapHandler from apimatic_core.utilities.api_helper import ApiHelper +from typing_extensions import Annotated -class Rabbit(object): - """Implementation of the 'Lion' model. - - TODO: type model description here. +class Rabbit(BaseModel): + """Implementation of the 'Rabbit' model using Pydantic. Attributes: - id (string): TODO: type description here. - weight (string): TODO: type description here. - mtype (string): TODO: type description here. - kind (string): TODO: type description here. - + id (str): TODO: type description here. + weight (str): TODO: type description here. + mtype (str): TODO: type description here. + kind (Optional[str]): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "id": 'id', - "weight": 'weight', - "mtype": 'type', - "kind": 'kind' - } - - _optionals = [ - 'kind', - ] - - def __init__(self, - id=None, - weight=None, - mtype=None, - kind=ApiHelper.SKIP): - """Constructor for the Lion class""" - - # Initialize members of the class - self.id = id - self.weight = weight - self.mtype = mtype - if kind is not ApiHelper.SKIP: - self.kind = kind - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - - id = dictionary.get("id") if dictionary.get("id") else None - weight = dictionary.get("weight") if dictionary.get("weight") else None - mtype = dictionary.get("type") if dictionary.get("type") else None - kind = dictionary.get("kind") if dictionary.get("kind") else ApiHelper.SKIP - # Return an object of this model - return cls(id, - weight, - mtype, - kind) - - @classmethod - def validate(cls, dictionary): - """Validates dictionary against class properties. - - Args: - dictionary: the dictionary to be validated against. - - Returns: - boolean : if value is valid for this model. - - """ - if isinstance(dictionary, cls): - return True - - if not isinstance(dictionary, dict): - return False + id: Annotated[str, Field(alias="id")] + weight: Annotated[str, Field(alias="weight")] + mtype: Annotated[str, Field(alias="type")] + kind: Annotated[Optional[str], Field(alias="kind")] = None - return dictionary.get("id") is not None and \ - ApiHelper.is_valid_type(dictionary.get("id"), lambda value: isinstance(value, str)) and \ - dictionary.get("weight") is not None and \ - ApiHelper.is_valid_type(dictionary.get("weight"), lambda value: isinstance(value, str)) and \ - dictionary.get("type") is not None and \ - ApiHelper.is_valid_type(dictionary.get("type"), lambda value: isinstance(value, str)) + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"kind"} + return ApiHelper.sanitize_model(optional_fields=_optional_fields, model_dump=nxt(self), + model_fields=self.model_fields, model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/mocks/models/union_type_scalar_model.py b/tests/apimatic_core/mocks/models/union_type_scalar_model.py index 1c8ff81..5ef6204 100644 --- a/tests/apimatic_core/mocks/models/union_type_scalar_model.py +++ b/tests/apimatic_core/mocks/models/union_type_scalar_model.py @@ -1,121 +1,47 @@ +from typing import Optional, Union +from pydantic import BaseModel, Field, model_serializer, AliasChoices +from pydantic_core.core_schema import SerializerFunctionWrapHandler from apimatic_core.utilities.api_helper import ApiHelper -from tests.apimatic_core.mocks.union_type_lookup import UnionTypeLookUp +from typing_extensions import Annotated -class UnionTypeScalarModel(object): - - """Implementation of the 'ScalarModel' model. +class UnionTypeScalarModel(BaseModel): + """Implementation of the 'ScalarModel' model using Pydantic. This class contains scalar types in oneOf/anyOf cases. Attributes: - any_of_required (float | bool): TODO: type description here. - one_of_req_nullable (int | str | None): TODO: type description here. - one_of_optional (int | float | str | None): TODO: type description - here. - any_of_opt_nullable (int | bool | None): TODO: type description here. - + any_of_required (Union[float, bool]): TODO: type description here. + one_of_req_nullable (Optional[Union[int, str]]): TODO: type description here. + one_of_optional (Optional[Union[int, float, str]]): TODO: type description here. + any_of_opt_nullable (Optional[Union[int, bool]]): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "any_of_required": 'anyOfRequired', - "one_of_req_nullable": 'oneOfReqNullable', - "one_of_optional": 'oneOfOptional', - "any_of_opt_nullable": 'anyOfOptNullable' - } - - _optionals = [ - 'one_of_optional', - 'any_of_opt_nullable', + any_of_required: Annotated[ + Union[float, bool], + Field(validation_alias=AliasChoices("any_of_required", "anyOfRequired"), + serialization_alias="anyOfRequired") ] - - _nullables = [ - 'one_of_req_nullable', - 'any_of_opt_nullable', + one_of_req_nullable: Annotated[ + Optional[Union[int, str]], + Field(validation_alias=AliasChoices("one_of_req_nullable", "oneOfReqNullable"), + serialization_alias="oneOfReqNullable") ] - - def __init__(self, - any_of_required=None, - one_of_req_nullable=None, - one_of_optional=ApiHelper.SKIP, - any_of_opt_nullable=ApiHelper.SKIP): - """Constructor for the ScalarModel class""" - - # Initialize members of the class - self.any_of_required = any_of_required - self.one_of_req_nullable = one_of_req_nullable - if one_of_optional is not ApiHelper.SKIP: - self.one_of_optional = one_of_optional - if any_of_opt_nullable is not ApiHelper.SKIP: - self.any_of_opt_nullable = any_of_opt_nullable - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary - any_of_required = ApiHelper.deserialize_union_type( - UnionTypeLookUp.get('ScalarModelAnyOfRequired'), - dictionary.get('anyOfRequired'), False) if dictionary.get('anyOfRequired') is not None else None - one_of_req_nullable = ApiHelper.deserialize_union_type( - UnionTypeLookUp.get('ScalarModelOneOfReqNullable'), - dictionary.get('oneOfReqNullable'), False) if dictionary.get('oneOfReqNullable') is not None else None - one_of_optional = ApiHelper.deserialize_union_type( - UnionTypeLookUp.get('ScalarModelOneOfOptional'), - dictionary.get('oneOfOptional'), False) if dictionary.get('oneOfOptional') is not None else ApiHelper.SKIP - if 'anyOfOptNullable' in dictionary.keys(): - any_of_opt_nullable = ApiHelper.deserialize_union_type( - UnionTypeLookUp.get('ScalarModelAnyOfOptNullable'), - dictionary.get('anyOfOptNullable'), False) if dictionary.get('anyOfOptNullable') is not None else None - else: - any_of_opt_nullable = ApiHelper.SKIP - # Return an object of this model - return cls(any_of_required, - one_of_req_nullable, - one_of_optional, - any_of_opt_nullable) - - @classmethod - def validate(cls, dictionary): - """Validates dictionary against class required properties - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - boolean : if dictionary is valid contains required properties. - - """ - if isinstance(dictionary, cls): - return ApiHelper.is_valid_type( - value=dictionary.any_of_required, - type_callable=lambda value: UnionTypeLookUp.get('ScalarModelAnyOfRequired').validate) and \ - ApiHelper.is_valid_type( - value=dictionary.one_of_req_nullable, - type_callable=lambda value: UnionTypeLookUp.get('ScalarModelOneOfReqNullable').validate) - - if not isinstance(dictionary, dict): - return False - - return ApiHelper.is_valid_type( - value=dictionary.get('anyOfRequired'), - type_callable=lambda value: UnionTypeLookUp.get('ScalarModelAnyOfRequired').validate) and \ - ApiHelper.is_valid_type( - value=dictionary.get('oneOfReqNullable'), - type_callable=lambda value: UnionTypeLookUp.get('ScalarModelOneOfReqNullable').validate) + one_of_optional: Annotated[ + Optional[Union[int, float, str]], + Field(validation_alias=AliasChoices("one_of_optional", "oneOfOptional"), + serialization_alias="oneOfOptional") + ] = None + any_of_opt_nullable: Annotated[ + Optional[Union[int, bool]], + Field(validation_alias=AliasChoices("any_of_opt_nullable", "anyOfOptNullable"), + serialization_alias="anyOfOptNullable") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"one_of_optional", "any_of_opt_nullable"} + _nullable_fields = {"one_of_req_nullable", "any_of_opt_nullable"} + return ApiHelper.sanitize_model(nullable_fields= _nullable_fields, optional_fields=_optional_fields, + model_dump=nxt(self), model_fields=self.model_fields, + model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/mocks/models/validate.py b/tests/apimatic_core/mocks/models/validate.py index 00b30eb..e295a85 100644 --- a/tests/apimatic_core/mocks/models/validate.py +++ b/tests/apimatic_core/mocks/models/validate.py @@ -1,74 +1,25 @@ +from typing import Optional +from pydantic import BaseModel, Field, model_serializer +from pydantic_core.core_schema import SerializerFunctionWrapHandler from apimatic_core.utilities.api_helper import ApiHelper +from typing_extensions import Annotated -class Validate(object): - - """Implementation of the 'validate' model. - - TODO: type model description here. +class Validate(BaseModel): + """Implementation of the 'Validate' model using Pydantic. Attributes: - field (string): TODO: type description here. - name (string): TODO: type description here. - address (string): TODO: type description here. - + field (str): TODO: type description here. + name (str): TODO: type description here. + address (Optional[str]): TODO: type description here. """ - # Create a mapping from Model property names to API property names - _names = { - "field": 'field', - "name": 'name', - "address": 'address' - } - - _optionals = [ - 'address', - ] - - def __init__(self, - field=None, - name=None, - address=ApiHelper.SKIP, - additional_properties={}): - """Constructor for the Validate class""" - - # Initialize members of the class - self.field = field - self.name = name - if address is not ApiHelper.SKIP: - self.address = address - - # Add additional model properties to the instance - self.additional_properties = additional_properties - - @classmethod - def from_dictionary(cls, - dictionary): - """Creates an instance of this model from a dictionary - - Args: - dictionary (dictionary): A dictionary representation of the object - as obtained from the deserialization of the server's response. The - keys MUST match property names in the API description. - - Returns: - object: An instance of this structure class. - - """ - if dictionary is None: - return None - - # Extract variables from the dictionary + field: Annotated[str, Field(alias="field")] + name: Annotated[str, Field(alias="name")] + address: Annotated[Optional[str], Field(alias="address")] = None - field = dictionary.get("field") if dictionary.get("field") else None - name = dictionary.get("name") if dictionary.get("name") else None - address = dictionary.get("address") if dictionary.get("address") else ApiHelper.SKIP - # Clean out expected properties from dictionary - for key in cls._names.values(): - if key in dictionary: - del dictionary[key] - # Return an object of this model - return cls(field, - name, - address, - dictionary) + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = {"address"} + return ApiHelper.sanitize_model(optional_fields=_optional_fields, model_dump=nxt(self), + model_fields=self.model_fields, model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/mocks/models/wolf_model.py b/tests/apimatic_core/mocks/models/wolf_model.py index 00762a1..9e5e918 100644 --- a/tests/apimatic_core/mocks/models/wolf_model.py +++ b/tests/apimatic_core/mocks/models/wolf_model.py @@ -1,7 +1,12 @@ +from typing import Optional + +from pydantic import AliasChoices, Field, BaseModel +from typing_extensions import Annotated +import xml.etree.ElementTree as ET from apimatic_core.utilities.xml_helper import XmlHelper -class WolfModel(object): +class WolfModel(BaseModel): """Implementation of the 'Wolf' model. @@ -17,15 +22,13 @@ class WolfModel(object): "howls": 'Howls' } - def __init__(self, - howls=None): - """Constructor for the WolfModel class""" - - # Initialize members of the class - self.howls = howls + howls: Annotated[ + Optional[bool], + Field(AliasChoices("howls", "Howls"), serialization_alias="Howls") + ] = None @classmethod - def from_element(cls, root): + def from_element(cls, root: ET.Element) -> "WolfModel": """Initialize an instance of this class using an xml.etree.Element. Args: @@ -37,9 +40,9 @@ def from_element(cls, root): """ howls = XmlHelper.value_from_xml_element(root.find('Howls'), bool) - return cls(howls) + return cls(howls=howls) - def to_xml_sub_element(self, root): + def to_xml_sub_element(self, root: ET.Element): """Convert this object to an instance of xml.etree.Element. Args: diff --git a/tests/apimatic_core/mocks/models/xml_model.py b/tests/apimatic_core/mocks/models/xml_model.py index 089b403..e64e386 100644 --- a/tests/apimatic_core/mocks/models/xml_model.py +++ b/tests/apimatic_core/mocks/models/xml_model.py @@ -1,52 +1,55 @@ from apimatic_core.utilities.xml_helper import XmlHelper +import xml.etree.ElementTree as ET +from typing import Optional +from pydantic import BaseModel, Field, AliasChoices +from typing_extensions import Annotated -class XMLModel(object): - """Implementation of the 'AttributesAndElements' model. - - TODO: type model description here. +class XMLModel(BaseModel): + """Implementation of the 'AttributesAndElements' model using Pydantic. Attributes: - string_attr (string): string attribute (attribute name "string") + string_attr (str): string attribute (attribute name "string") number_attr (int): number attribute (attribute name "number") - string_element (string): string element (element name "string") + boolean_attr (Optional[bool]): boolean attribute (attribute name "boolean") + string_element (str): string element (element name "string") number_element (int): number element (element name "number") - + boolean_element (Optional[bool]): boolean element (element name "boolean") + elements (Optional[list]): Additional elements. """ - # Create a mapping from Model property names to API property names - _names = { - "string_attr": 'string-attr', - "number_attr": 'number-attr', - "boolean_attr": 'boolean-attr', - "string_element": 'string-element', - "number_element": 'number-element', - "boolean_element": 'boolean-element', - "elements": 'elements' - } - - def __init__(self, - string_attr=None, - number_attr=None, - boolean_attr=None, - string_element=None, - number_element=None, - boolean_element=None, - elements=None): - """Constructor for the AttributesAndElementsModel class""" - - # Initialize members of the class - self.string_attr = string_attr - self.number_attr = number_attr - self.boolean_attr = boolean_attr - self.string_element = string_element - self.number_element = number_element - self.boolean_element = boolean_element - self.elements = elements + string_attr: Annotated[ + Optional[str], + Field(AliasChoices("string_attr", "string-attr"), serialization_alias="string-attr") + ] = None + number_attr: Annotated[ + Optional[int], + Field(AliasChoices("number_attr", "number-attr"), serialization_alias="number-attr") + ] = None + boolean_attr: Annotated[ + Optional[bool], + Field(AliasChoices("boolean_attr", "boolean-attr"), serialization_alias="boolean-attr") + ] = None + string_element: Annotated[ + Optional[str], + Field(AliasChoices("string_element", "string-element"), serialization_alias="string-element") + ] = None + number_element: Annotated[ + Optional[int], + Field(AliasChoices("number_element", "number-element"), serialization_alias="number-element") + ] = None + boolean_element: Annotated[ + Optional[bool], + Field(AliasChoices("boolean_element", "boolean-element"), serialization_alias="boolean-element") + ] = None + elements: Annotated[ + Optional[list], + Field(AliasChoices("elements", "elements"), serialization_alias="elements") + ] = None @classmethod - def from_element(cls, root): + def from_element(cls, root: ET.Element) -> 'XMLModel': """Initialize an instance of this class using an xml.etree.Element. Args: @@ -64,15 +67,15 @@ def from_element(cls, root): boolean_element = XmlHelper.value_from_xml_element(root.find('boolean'), bool) elements = XmlHelper.list_from_xml_element( root, 'item', str, wrapping_element_name='elements') - return cls(string_attr, - number_attr, - boolean_attr, - string_element, - number_element, - boolean_element, - elements) + return cls(string_attr=string_attr, + number_attr=number_attr, + boolean_attr=boolean_attr, + string_element=string_element, + number_element=number_element, + boolean_element=boolean_element, + elements=elements) - def to_xml_sub_element(self, root): + def to_xml_sub_element(self, root: ET.Element): """Convert this object to an instance of xml.etree.Element. Args: diff --git a/tests/apimatic_core/mocks/union_type_lookup.py b/tests/apimatic_core/mocks/union_type_lookup.py index bdcd03f..a5a77eb 100644 --- a/tests/apimatic_core/mocks/union_type_lookup.py +++ b/tests/apimatic_core/mocks/union_type_lookup.py @@ -1,34 +1,39 @@ -# -*- coding: utf-8 -*- -""" -typecombinatorsimple +from apimatic_core_interfaces.types.union_type import UnionType +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext as Context +from typing import Dict -This file was automatically generated by APIMATIC v3.0 ( - https://www.apimatic.io ). -""" +from pydantic import validate_call from apimatic_core.types.union_types.any_of import AnyOf from apimatic_core.types.union_types.leaf_type import LeafType from apimatic_core.types.union_types.one_of import OneOf -from apimatic_core.types.union_types.union_type_context import UnionTypeContext as Context class UnionTypeLookUp: - """The `UnionTypeLookUp` class serves as a utility class for - storing and managing type combinator templates.It acts as a container for the templates - used in handling various data types within the application. - """ - _union_types = { - 'ScalarModelAnyOfRequired': AnyOf([LeafType(float), LeafType(bool)]), - 'ScalarModelOneOfReqNullable': OneOf([LeafType(int), LeafType(str)], Context.create(is_nullable=True)), - 'ScalarModelOneOfOptional': OneOf([LeafType(int), LeafType(float), LeafType(str)], Context.create(is_optional=True)), - 'ScalarModelAnyOfOptNullable': AnyOf([LeafType(int), LeafType(bool)], Context.create(is_optional=True, is_nullable=True)), + The `UnionTypeLookUp` class serves as a utility class for storing and managing type combinator templates. + It acts as a container for the templates used in handling various data types within the application. + """ + _union_types: Dict[str, UnionType] = { + 'ScalarModelAnyOfRequired': AnyOf( + [LeafType(float), LeafType(bool)] + ), + 'ScalarModelOneOfReqNullable': OneOf( + [LeafType(int), LeafType(str)], Context.create(is_nullable=True) + ), + 'ScalarModelOneOfOptional': OneOf( + [LeafType(int), LeafType(float), LeafType(str)], Context.create(is_optional=True) + ), + 'ScalarModelAnyOfOptNullable': AnyOf( + [LeafType(int), LeafType(bool)], Context.create(is_optional=True, is_nullable=True) + ), 'ScalarTypes': OneOf([LeafType(float), LeafType(bool)]), } @staticmethod - def get(name): + @validate_call + def get(name: str) -> UnionType: return UnionTypeLookUp._union_types[name] diff --git a/tests/apimatic_core/request_builder_tests/test_request_builder.py b/tests/apimatic_core/request_builder_tests/test_request_builder.py index 7f84b20..3fc8c24 100644 --- a/tests/apimatic_core/request_builder_tests/test_request_builder.py +++ b/tests/apimatic_core/request_builder_tests/test_request_builder.py @@ -3,7 +3,7 @@ import sys from apimatic_core.exceptions.auth_validation_exception import AuthValidationException -from apimatic_core_interfaces.types.http_method_enum import HttpMethodEnum +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum from apimatic_core.authentication.multiple.and_auth_group import And from apimatic_core.authentication.multiple.or_auth_group import Or from apimatic_core.authentication.multiple.single_auth import Single @@ -27,29 +27,28 @@ class TestRequestBuilder(Base): (Server.AUTH_SERVER, 'http://authserver:5000/') ]) def test_base_uri(self, input_server, expected_base_uri): - http_request = self.new_request_builder.server(input_server).path('/').build(self.global_configuration) + http_request = (self.new_request_builder + .server(input_server) + .http_method(HttpMethodEnum.POST) + .path('/') + .build(self.global_configuration)) assert http_request.query_url == expected_base_uri def test_path(self): - http_request = self.new_request_builder.build(self.global_configuration) + http_request = self.new_request_builder.http_method(HttpMethodEnum.POST).build(self.global_configuration) assert http_request.query_url == 'http://localhost:3000/test' def test_required_param(self): with pytest.raises(ValueError) as validation_error: self.new_request_builder \ - .query_param(Parameter() - .key('query_param') - .value(None) - .is_required(True)) \ + .query_param(Parameter(key='query_param', value=None, is_required=True)) \ .build(self.global_configuration) assert validation_error.value.args[0] == 'Required parameter query_param cannot be None.' def test_optional_param(self): http_request = self.new_request_builder \ - .query_param(Parameter() - .key('query_param') - .value(None) - .is_required(False)) \ + .http_method(HttpMethodEnum.POST) \ + .query_param(Parameter(key='query_param', value=None, is_required=False)) \ .build(self.global_configuration) assert http_request.query_url == 'http://localhost:3000/test' @@ -85,11 +84,10 @@ def test_http_method(self, input_http_method, expected_http_method): def test_template_params_with_encoding(self, input_template_param_value, expected_template_param_value, should_encode): http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ .path('/{template_param}') \ - .template_param(Parameter() - .key('template_param') - .value(input_template_param_value) - .should_encode(should_encode)) \ + .template_param(Parameter(key='template_param', value=input_template_param_value, + should_encode=should_encode)) \ .build(self.global_configuration) assert http_request.query_url == 'http://localhost:3000/{}'.format(expected_template_param_value) @@ -136,6 +134,9 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte '&query_param[birthday]=1994-02-13' '&query_param[birthtime]={}'.format(quote( Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[name]=Bob' + '&query_param[uid]=1234567' + '&query_param[personType]=Empl' '&query_param[department]=IT' '&query_param[dependents][0][address]=street%20abc' '&query_param[dependents][0][age]=12' @@ -150,18 +151,16 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte '&query_param[hiredAt]={}'.format(quote( Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[joiningDay]=Monday' - '&query_param[name]=Bob' '&query_param[salary]=30000' - '&query_param[uid]=1234567' '&query_param[workingDays][0]=Monday' '&query_param[workingDays][1]=Tuesday' - '&query_param[personType]=Empl', SerializationFormats.INDEXED) + '&query_param[key1]=value1' + '&query_param[key2]=value2', SerializationFormats.INDEXED) ]) def test_query_params(self, input_query_param_value, expected_query_param_value, array_serialization_format): http_request = self.new_request_builder \ - .query_param(Parameter() - .key('query_param') - .value(input_query_param_value)) \ + .http_method(HttpMethodEnum.GET) \ + .query_param(Parameter(key='query_param', value=input_query_param_value)) \ .array_serialization_format(array_serialization_format) \ .build(self.global_configuration) assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_query_param_value) @@ -171,6 +170,7 @@ def test_query_params(self, input_query_param_value, expected_query_param_value, ]) def test_additional_query_params(self, input_additional_query_params_value, expected_additional_query_params_value): http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ .additional_query_params(input_additional_query_params_value) \ .build(self.global_configuration) assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_additional_query_params_value) @@ -190,9 +190,8 @@ def test_additional_query_params(self, input_additional_query_params_value, expe ]) def test_local_headers(self, input_local_header_param_value, expected_local_header_param_value): http_request = self.new_request_builder \ - .header_param(Parameter() - .key('header_param') - .value(input_local_header_param_value)) \ + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ .build(self.global_configuration) assert http_request.headers == expected_local_header_param_value @@ -210,9 +209,11 @@ def test_local_headers(self, input_local_header_param_value, expected_local_head ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) ]) def test_global_headers(self, input_global_header_param_value, expected_global_header_param_value): + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} http_request = self.new_request_builder \ - .build(self.global_configuration - .global_header('header_param', input_global_header_param_value)) + .http_method(HttpMethodEnum.GET) \ + .build(global_configuration) assert http_request.headers == expected_global_header_param_value @pytest.mark.parametrize('input_additional_header_param_value, expected_additional_header_param_value', [ @@ -229,9 +230,11 @@ def test_global_headers(self, input_global_header_param_value, expected_global_h ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) ]) def test_additional_headers(self, input_additional_header_param_value, expected_additional_header_param_value): + global_configuration = self.global_configuration + global_configuration.additional_headers = {'header_param': input_additional_header_param_value} http_request = self.new_request_builder \ - .build(self.global_configuration - .additional_header('header_param', input_additional_header_param_value)) + .http_method(HttpMethodEnum.GET) \ + .build(global_configuration) assert http_request.headers == expected_additional_header_param_value @pytest.mark.parametrize('input_global_header_param_value,' @@ -242,12 +245,12 @@ def test_additional_headers(self, input_additional_header_param_value, expected_ ]) def test_local_and_global_headers_precedence(self, input_global_header_param_value, input_local_header_param_value, expected_header_param_value): - global_headers = {'header_param': input_global_header_param_value} + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} http_request = self.new_request_builder \ - .header_param(Parameter() - .key('header_param') - .value(input_local_header_param_value)) \ - .build(self.global_configuration.global_headers(global_headers)) + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ + .build(global_configuration) assert http_request.headers == expected_header_param_value @pytest.mark.parametrize('input_global_header_param_value,' @@ -261,14 +264,13 @@ def test_local_and_global_headers_precedence(self, input_global_header_param_val ]) def test_all_headers_precedence(self, input_global_header_param_value, input_local_header_param_value, input_additional_header_param_value, expected_header_param_value): - global_headers = {'header_param': input_global_header_param_value} - additional_headers = {'header_param': input_additional_header_param_value} + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} + global_configuration.additional_headers = {'header_param': input_additional_header_param_value} http_request = self.new_request_builder \ - .header_param(Parameter() - .key('header_param') - .value(input_local_header_param_value)) \ - .build(self.global_configuration.global_headers(global_headers) - .additional_headers(additional_headers)) + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ + .build(global_configuration) assert http_request.headers == expected_header_param_value def test_useragent_header(self): @@ -276,8 +278,9 @@ def test_useragent_header(self): operating_systems = ['Linux', 'Windows', 'Darwin', 'FreeBSD', 'OpenBSD', 'macOS'] http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ .build(self.global_configuration_with_useragent) - [lang, version, engine, engineVersion, osInfo] = http_request.headers['user-agent'].split('|') + [lang, version, engine, _, osInfo] = http_request.headers['user-agent'].split('|') assert lang == 'Python' and version == '31.8.0' \ and engine in engines and osInfo in operating_systems @@ -309,42 +312,36 @@ def test_useragent_header(self): SerializationFormats.INDEXED), (Base.employee_model(), [('form_param[address]', 'street abc'), ('form_param[age]', 27), ('form_param[birthday]', '1994-02-13'), - ('form_param[birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), False)), + ('form_param[birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[name]', 'Bob'), + ('form_param[uid]', '1234567'), + ('form_param[personType]', 'Empl'), ('form_param[department]', 'IT'), ('form_param[dependents][0][address]', 'street abc'), ('form_param[dependents][0][age]', 12), ('form_param[dependents][0][birthday]', '1994-02-13'), - ('form_param[dependents][0][birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), False)), - ('form_param[dependents][0][name]', 'John'), ('form_param[dependents][0][uid]', 7654321), + ('form_param[dependents][0][birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[dependents][0][name]', 'John'), ('form_param[dependents][0][uid]', '7654321'), ('form_param[dependents][0][personType]', 'Per'), ('form_param[dependents][0][key1]', 'value1'), ('form_param[dependents][0][key2]', 'value2'), - ('form_param[hiredAt]', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15), False)), - ('form_param[joiningDay]', 'Monday'), ('form_param[name]', 'Bob'), ('form_param[salary]', 30000), - ('form_param[uid]', 1234567), ('form_param[workingDays][0]', 'Monday'), - ('form_param[workingDays][1]', 'Tuesday'), ('form_param[personType]', 'Empl')], SerializationFormats.INDEXED) + ('form_param[hiredAt]', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[joiningDay]', 'Monday'), ('form_param[salary]', 30000), ('form_param[workingDays][0]', 'Monday'), + ('form_param[workingDays][1]', 'Tuesday'), ('form_param[key1]', 'value1'), + ('form_param[key2]', 'value2')], SerializationFormats.INDEXED) ]) def test_form_params(self, input_form_param_value, expected_form_param_value, array_serialization_format): http_request = self.new_request_builder \ - .form_param(Parameter() - .key('form_param') - .value(input_form_param_value)) \ + .http_method(HttpMethodEnum.POST) \ + .form_param(Parameter(key='form_param', value=input_form_param_value)) \ .array_serialization_format(array_serialization_format) \ .build(self.global_configuration) - for index, item in enumerate(http_request.parameters): - # form encoding stores the datetime object so converting datetime to string for assertions as assertions - # do not work for objects - if isinstance(item[1], ApiHelper.CustomDate): - try: - assert item[0] == expected_form_param_value[index][0] \ - and item[1].value == expected_form_param_value[index][1].value - except: - print("here") - else: - assert item == expected_form_param_value[index] + + assert http_request.parameters == expected_form_param_value @pytest.mark.parametrize('input_additional_form_param_value, expected_additional_form_param_value', [ ({'key1': 'value1', 'key2': 'value2'}, [('key1', 'value1'), ('key2', 'value2')]) ]) def test_addition_form_params(self, input_additional_form_param_value, expected_additional_form_param_value): http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ .additional_form_params(input_additional_form_param_value) \ .build(self.global_configuration) assert http_request.parameters == expected_additional_form_param_value @@ -361,9 +358,8 @@ def test_addition_form_params(self, input_additional_form_param_value, expected_ def test_form_params_with_additional_form_params(self, input_form_param_value, input_additional_form_param_value, expected_form_param_value): http_request = self.new_request_builder \ - .form_param(Parameter() - .key('form_param') - .value(input_form_param_value)) \ + .http_method(HttpMethodEnum.POST) \ + .form_param(Parameter(key='form_param', value=input_form_param_value)) \ .additional_form_params(input_additional_form_param_value) \ .build(self.global_configuration) assert http_request.parameters == expected_form_param_value @@ -381,8 +377,8 @@ def test_form_params_with_additional_form_params(self, input_form_param_value, i ]) def test_json_body_params_without_serializer(self, input_body_param_value, expected_body_param_value): http_request = self.new_request_builder \ - .body_param(Parameter() - .value(input_body_param_value)) \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(value=input_body_param_value)) \ .build(self.global_configuration) assert http_request.parameters == expected_body_param_value @@ -394,12 +390,9 @@ def test_json_body_params_without_serializer(self, input_body_param_value, expec def test_multiple_json_body_params_with_serializer(self, input_body_param_value1, input_body_param_value2, expected_body_param_value): http_request = self.new_request_builder \ - .body_param(Parameter() - .key('param1') - .value(input_body_param_value1)) \ - .body_param(Parameter() - .key('param2') - .value(input_body_param_value2)) \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(key='param1', value=input_body_param_value1)) \ + .body_param(Parameter(key='param2', value=input_body_param_value2)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) assert http_request.parameters == expected_body_param_value @@ -410,25 +403,26 @@ def test_multiple_json_body_params_with_serializer(self, input_body_param_value1 ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, '{"key1": "value1", "key2": [1, 2, 3, 4]}'), ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), - (Base.employee_model(), ApiHelper.json_serialize(Base.get_employee_dictionary())) + (Base.employee_model(), Base.get_serialized_employee()) ]) def test_json_body_params_with_serializer(self, input_body_param_value, expected_body_param_value): http_request = self.new_request_builder \ - .body_param(Parameter() - .value(input_body_param_value)) \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(value=input_body_param_value)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_value, expected_value', [ - (100, '100'), + (100.0, '100.0'), (True, 'true') ]) def test_type_combinator_validation_in_request(self, input_value, expected_value): http_request = self.new_request_builder \ - .body_param(Parameter() - .validator(lambda value: UnionTypeLookUp.get('ScalarTypes')) - .value(input_value)) \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter( + validator=lambda value: UnionTypeLookUp.get('ScalarTypes').validate(value=value).is_valid, + value=input_value)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) assert http_request.parameters == expected_value @@ -451,9 +445,8 @@ def test_xml_body_param_with_serializer(self, input_body_param_value, expected_b 'string="String" number="10000" boolean="false">', 'boolean="false" number="10000" string="String">') http_request = self.new_request_builder \ - .xml_attributes(XmlAttributes() - .value(input_body_param_value) - .root_element_name('AttributesAndElements')) \ + .http_method(HttpMethodEnum.POST) \ + .xml_attributes(XmlAttributes(value=input_body_param_value, root_element_name='AttributesAndElements')) \ .body_serializer(XmlHelper.serialize_to_xml) \ .build(self.global_configuration) assert http_request.parameters == expected_body_param_value @@ -489,106 +482,104 @@ def test_xml_array_body_param_with_serializer(self, input_body_param_value, expe 'string="String" number="10000" boolean="false">', 'boolean="false" number="10000" string="String">') http_request = self.new_request_builder \ - .xml_attributes(XmlAttributes() - .value(input_body_param_value) - .root_element_name('arrayOfModels') - .array_item_name('item')) \ + .http_method(HttpMethodEnum.POST) \ + .xml_attributes(XmlAttributes(value=input_body_param_value, + root_element_name='arrayOfModels', + array_item_name='item')) \ .body_serializer(XmlHelper.serialize_list_to_xml) \ .build(self.global_configuration) assert http_request.parameters == expected_body_param_value - @pytest.mark.parametrize('input_body_param_value, expected_body_param_value, expected_content_type', [ + @pytest.mark.parametrize("input_body_param_value, expected_input_file, expected_content_type", [ (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), Base.read_file('apimatic.png'), 'image/png')]) - def test_file_as_body_param(self, input_body_param_value, expected_body_param_value, expected_content_type): + def test_file_as_body_param(self, input_body_param_value, expected_input_file, expected_content_type): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .header_param(Parameter(key='content-type', value='application/xml')) \ + .body_param(Parameter(value=input_body_param_value)) \ + .build(self.global_configuration) + input_file = None try: - http_request = self.new_request_builder \ - .header_param(Parameter().key('content-type').value('application/xml')) \ - .body_param(Parameter().value(input_body_param_value)) \ - .build(self.global_configuration) - - actual_body_param_value = http_request.parameters + input_file = http_request.parameters - assert actual_body_param_value.read() == expected_body_param_value.read() \ + assert input_file.read() == expected_input_file.read() \ and http_request.headers['content-type'] == expected_content_type finally: - actual_body_param_value.close() - expected_body_param_value.close() + input_file.close() + expected_input_file.close() - @pytest.mark.parametrize('input_multipart_param_value1, input_default_content_type1,' - 'input_multipart_param_value2, input_default_content_type2,' - 'expected_multipart_param_value1, expected_default_content_type1, ' - 'expected_multipart_param_value2, expected_default_content_type2', [ + @pytest.mark.parametrize( + "input_multipart_param_value1, input_default_content_type1,input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type, expected_model, expected_model_content_type", [ (Base.read_file('apimatic.png'), 'image/png', Base.employee_model(), 'application/json', Base.read_file('apimatic.png'), 'image/png', - ApiHelper.json_serialize(Base.get_employee_dictionary()), 'application/json') + Base.get_serialized_employee(), 'application/json') ]) def test_multipart_request_without_file_wrapper(self, input_multipart_param_value1, input_default_content_type1, input_multipart_param_value2, input_default_content_type2, - expected_multipart_param_value1, - expected_default_content_type1, - expected_multipart_param_value2, - expected_default_content_type2): + expected_file, + expected_file_content_type, + expected_model, + expected_model_content_type): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .multipart_param(Parameter(key='file_wrapper', value=input_multipart_param_value1, + default_content_type=input_default_content_type1)) \ + .multipart_param(Parameter(key='model', value=ApiHelper.json_serialize(input_multipart_param_value2), + default_content_type=input_default_content_type2)) \ + .build(self.global_configuration) + actual_file = None try: - http_request = self.new_request_builder \ - .multipart_param(Parameter().key('file_wrapper') - .value(input_multipart_param_value1) - .default_content_type(input_default_content_type1)) \ - .multipart_param(Parameter().key('model') - .value(ApiHelper.json_serialize(input_multipart_param_value2)) - .default_content_type(input_default_content_type2)) \ - .build(self.global_configuration) - - actual_multipart_param_value1 = http_request.files['file_wrapper'][1] - actual_multipart_param_content_type1 = http_request.files['file_wrapper'][2] - actual_multipart_param_value2 = http_request.files['model'][1] - actual_multipart_param_content_type2 = http_request.files['model'][2] - - assert actual_multipart_param_value1.read() == expected_multipart_param_value1.read() \ - and actual_multipart_param_content_type1 == expected_default_content_type1 \ - and actual_multipart_param_value2 == expected_multipart_param_value2 \ - and actual_multipart_param_content_type2 == expected_default_content_type2 + actual_file = http_request.files['file_wrapper'][1] + actual_file_content_type = http_request.files['file_wrapper'][2] + actual_model = http_request.files['model'][1] + actual_model_content_type = http_request.files['model'][2] + + assert actual_file.read() == expected_file.read() \ + and actual_file_content_type == expected_file_content_type \ + and actual_model == expected_model \ + and actual_model_content_type == expected_model_content_type finally: - actual_multipart_param_value1.close() - expected_multipart_param_value1.close() + actual_file.close() + expected_file.close() - @pytest.mark.parametrize('input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2,' - 'expected_multipart_param_value1, expected_default_content_type1,' - 'expected_multipart_param_value2, expected_default_content_type2', [ + @pytest.mark.parametrize( + "input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type,expected_model, expected_model_content_type", [ (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), Base.employee_model(), 'application/json', Base.read_file('apimatic.png'), 'image/png', - ApiHelper.json_serialize(Base.get_employee_dictionary()), 'application/json') + Base.get_serialized_employee(), 'application/json') ]) def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2, - expected_multipart_param_value1, - expected_default_content_type1, - expected_multipart_param_value2, - expected_default_content_type2): - try: - http_request = self.new_request_builder \ - .multipart_param(Parameter().key('file') - .value(input_multipart_param_value1)) \ - .multipart_param(Parameter().key('model') - .value(ApiHelper.json_serialize(input_multipart_param_value2)) - .default_content_type(input_default_content_type2)) \ - .build(self.global_configuration) + expected_file, + expected_file_content_type, + expected_model, + expected_model_content_type): - actual_multipart_param_value1 = http_request.files['file'][1] - actual_multipart_param_content_type1 = http_request.files['file'][2] - actual_multipart_param_value2 = http_request.files['model'][1] - actual_multipart_param_content_type2 = http_request.files['model'][2] + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .multipart_param(Parameter(key='file', value=input_multipart_param_value1)) \ + .multipart_param(Parameter(key='model', value=ApiHelper.json_serialize(input_multipart_param_value2), + default_content_type=input_default_content_type2)) \ + .build(self.global_configuration) - assert actual_multipart_param_value1.read() == expected_multipart_param_value1.read() \ - and actual_multipart_param_content_type1 == expected_default_content_type1 \ - and actual_multipart_param_value2 == expected_multipart_param_value2 \ - and actual_multipart_param_content_type2 == expected_default_content_type2 + actual_file = None + try: + actual_file = http_request.files['file'][1] + actual_file_content_type = http_request.files['file'][2] + actual_model = http_request.files['model'][1] + actual_model_content_type = http_request.files['model'][2] + + assert actual_file.read() == expected_file.read() \ + and actual_file_content_type == expected_file_content_type \ + and actual_model == expected_model \ + and actual_model_content_type == expected_model_content_type finally: - actual_multipart_param_value1.close() - expected_multipart_param_value1.close() + actual_file.close() + expected_file.close() @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key, expected_auth_header_value', [ (Single('basic_auth'), 'Basic-Authorization', 'Basic {}'.format( @@ -598,6 +589,7 @@ def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1, ]) def test_header_authentication(self, input_auth_scheme, expected_auth_header_key, expected_auth_header_value): http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ .build(self.global_configuration_with_auth) @@ -605,6 +597,7 @@ def test_header_authentication(self, input_auth_scheme, expected_auth_header_key def test_query_authentication(self): http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ .auth(Single('custom_query_auth')) \ .build(self.global_configuration_with_auth) @@ -618,6 +611,7 @@ def test_query_authentication(self): def test_invalid_key_authentication(self, input_invalid_auth_scheme): with pytest.raises(ValueError) as validation_error: self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ .auth(input_invalid_auth_scheme) \ .build(self.global_configuration_with_auth) assert validation_error.value.args[0] == 'Auth key is invalid.' @@ -658,12 +652,12 @@ def test_invalid_key_authentication(self, input_invalid_auth_scheme): 'Bearer-Authorization': None, 'token': None }), - (Or('basic_auth', Or(None, 'custom_header_auth')), + (Or('basic_auth', Or('custom_header_auth')), { 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', 'token': None }), - (Or('basic_auth', And(None, 'custom_header_auth')), + (Or('basic_auth', And('custom_header_auth')), { 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', 'token': None @@ -671,47 +665,46 @@ def test_invalid_key_authentication(self, input_invalid_auth_scheme): ]) def test_success_case_of_multiple_authentications(self, input_auth_scheme, expected_request_headers): http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ .build(self.global_configuration_with_auth) for key in expected_request_headers.keys(): assert http_request.headers.get(key) == expected_request_headers.get(key) @pytest.mark.parametrize('input_auth_scheme, expected_error_message', [ - (Or('basic_auth', 'bearer_auth', 'custom_header_auth'), '[BasicAuth: _basic_auth_user_name or ' - '_basic_auth_password is undefined.] or [' - 'BearerAuth: _access_token is undefined.] or [' - 'CustomHeaderAuthentication: token is undefined.]'), - (And('basic_auth', 'bearer_auth', 'custom_header_auth'), '[BasicAuth: _basic_auth_user_name or ' - '_basic_auth_password is undefined.] and [' - 'BearerAuth: _access_token is undefined.] and [' - 'CustomHeaderAuthentication: token is undefined.]'), - (Or('basic_auth', And('bearer_auth', 'custom_header_auth')), '[BasicAuth: _basic_auth_user_name or ' - '_basic_auth_password is undefined.] or [' - 'BearerAuth: _access_token is undefined.] and [' - 'CustomHeaderAuthentication: token is ' - 'undefined.]'), - (And('basic_auth', Or('bearer_auth', 'custom_header_auth')), '[BasicAuth: _basic_auth_user_name or ' - '_basic_auth_password is undefined.] and [' - 'BearerAuth: _access_token is undefined.] or [' - 'CustomHeaderAuthentication: token is ' - 'undefined.]'), - (And('basic_auth', And('bearer_auth', 'custom_header_auth')), '[BasicAuth: _basic_auth_user_name or ' - '_basic_auth_password is undefined.] and [' - 'BearerAuth: _access_token is undefined.] and [' - 'CustomHeaderAuthentication: token is ' - 'undefined.]'), - (Or('basic_auth', Or('bearer_auth', 'custom_header_auth')), '[BasicAuth: _basic_auth_user_name or ' - '_basic_auth_password is undefined.] or [' - 'BearerAuth: _access_token is undefined.] or [' - 'CustomHeaderAuthentication: token is undefined.]'), - (Or(None, None), ''), - (Or(None, 'basic_auth'), '[BasicAuth: _basic_auth_user_name or _basic_auth_password is undefined.]'), - (And(None, None), ''), - (And(None, 'basic_auth'), '[BasicAuth: _basic_auth_user_name or _basic_auth_password is undefined.]') + (Or('basic_auth', 'bearer_auth', 'custom_header_auth'), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', 'bearer_auth', 'custom_header_auth'), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or('basic_auth', And('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', Or('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', And('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or('basic_auth', Or('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or(), ''), + (Or('basic_auth'), '[BasicAuth: username or password is undefined.]'), + (And(), ''), + (And( 'basic_auth'), '[BasicAuth: username or password is undefined.]') ]) def test_failed_case_of_multiple_authentications(self, input_auth_scheme, expected_error_message): with pytest.raises(AuthValidationException) as errors: self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ .build(self.global_configuration_with_uninitialized_auth_params) assert errors.value.args[0] == expected_error_message @@ -726,6 +719,7 @@ def test_failed_case_of_multiple_authentications(self, input_auth_scheme, expect def test_case_of_multiple_authentications(self, input_auth_scheme, expected_auth_header_key, expected_auth_header_value): http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ .build(self.global_configuration_with_partially_initialized_auth_params) diff --git a/tests/apimatic_core/response_handler_tests/test_response_handler.py b/tests/apimatic_core/response_handler_tests/test_response_handler.py index 183b35a..c1109dc 100644 --- a/tests/apimatic_core/response_handler_tests/test_response_handler.py +++ b/tests/apimatic_core/response_handler_tests/test_response_handler.py @@ -1,6 +1,10 @@ from datetime import datetime, date import pytest import sys + +from apimatic_core_interfaces.http.http_response import HttpResponse +from typing import Type, Any, TypeVar, Optional, Callable, List + from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.utilities.api_helper import ApiHelper from apimatic_core.utilities.xml_helper import XmlHelper @@ -15,10 +19,13 @@ class TestResponseHandler(Base): + E = TypeVar("E", bound=BaseException) def test_nullify_404(self): - http_response = self.new_response_handler.is_nullify404(True).handle(self.response(status_code=404), - self.global_errors()) + http_response = (self.new_response_handler + .is_nullify404(True) + .handle(self.response(status_code=404), self.global_errors())) + assert http_response is None @pytest.mark.parametrize('http_response, expected_exception_type, expected_error_message', [ @@ -27,15 +34,19 @@ def test_nullify_404(self): (Base.response(status_code=429), GlobalTestException, 'Invalid response'), (Base.response(status_code=399), GlobalTestException, '3XX Global') ]) - def test_global_error(self, http_response, expected_exception_type, expected_error_message): + def test_global_error( + self, http_response: HttpResponse, expected_exception_type: Type[E], expected_error_message: str + ): with pytest.raises(expected_exception_type) as error: self.new_response_handler.handle(http_response, self.global_errors()) + assert error.value.reason == expected_error_message def test_local_error(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ .handle(self.response(status_code=404), self.global_errors()) + assert error.value.reason == 'Not Found' def test_default_local_error(self): @@ -43,6 +54,7 @@ def test_default_local_error(self): self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ .local_error('default', 'Response Not OK', LocalTestException) \ .handle(self.response(status_code=412), self.global_errors()) + assert error.value.reason == 'Response Not OK' @pytest.mark.parametrize('http_response, expected_exception_type, expected_error_message', [ @@ -50,12 +62,14 @@ def test_default_local_error(self): (Base.response(status_code=443), LocalTestException, '4XX local'), (Base.response(status_code=522), LocalTestException, '522 local') ]) - def test_default_range_local_error(self, http_response, expected_exception_type, expected_error_message): + def test_default_range_local_error( + self, http_response: HttpResponse, expected_exception_type: Type[E], expected_error_message: str): with pytest.raises(expected_exception_type) as error: self.new_response_handler.local_error(522, '522 local', LocalTestException) \ .local_error('5XX', '5XX local', APIException) \ .local_error('4XX', '4XX local', LocalTestException) \ .handle(http_response, self.global_errors()) + assert error.value.reason == expected_error_message def test_local_error_with_body(self): @@ -66,6 +80,7 @@ def test_local_error_with_body(self): '"ServerMessage": "Test message from server", ' '"SecretMessageForEndpoint": "This is test error message"}'), self.global_errors()) + assert error.value.server_code == 5001 \ and error.value.server_message == 'Test message from server' \ and error.value.secret_message_for_endpoint == 'This is test error message' @@ -83,6 +98,7 @@ def test_local_error_template_message(self): '"This is test error message"}', headers={'accept': 'application/json'}), self.global_errors_with_template_message()) + assert error.value.reason == 'error_code => 404, ' \ 'header => application/json, ' \ 'body => 5001 - Test message from server - This is test error message' @@ -100,6 +116,7 @@ def test_global_error_with_body(self): '}' '}'), self.global_errors()) + assert error.value.server_code == 5001 \ and error.value.server_message == 'Test message from server' \ and error.value.model.field == 'Test field' \ @@ -121,12 +138,14 @@ def test_local_error_precedence(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(400, '400 Local', LocalTestException) \ .handle(self.response(status_code=400), self.global_errors()) + assert error.value.reason == '400 Local' def test_global_error_precedence(self): with pytest.raises(GlobalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ .handle(self.response(status_code=400), self.global_errors()) + assert error.value.reason == '400 Global' @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -134,8 +153,9 @@ def test_global_error_precedence(self): (Base.response(text=500), 500), (Base.response(text=500.124), 500.124) ]) - def test_simple_response_body(self, input_http_response, expected_response_body): + def test_simple_response_body(self, input_http_response: HttpResponse, expected_response_body: str): http_response = self.new_response_handler.handle(input_http_response, self.global_errors()) + assert http_response == expected_response_body @pytest.mark.parametrize('input_http_response, input_response_convertor, expected_response_body_type, ' @@ -143,10 +163,13 @@ def test_simple_response_body(self, input_http_response, expected_response_body) (Base.response(text='500'), int, int, 500), (Base.response(text=500), str, str, '500') ]) - def test_simple_response_body_with_convertor(self, input_http_response, input_response_convertor, - expected_response_body_type, expected_response_body): - http_response = self.new_response_handler.convertor(input_response_convertor).handle(input_http_response, - self.global_errors()) + def test_simple_response_body_with_convertor( + self, input_http_response: HttpResponse, input_response_convertor: Optional[Callable[[Any], Any]], + expected_response_body_type: Type, expected_response_body: Any): + http_response = (self.new_response_handler + .convertor(input_response_convertor) + .handle(input_http_response, self.global_errors())) + assert type(http_response) == expected_response_body_type and http_response == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -156,10 +179,10 @@ def test_simple_response_body_with_convertor(self, input_http_response, input_re (Base.response(text=''), None), (Base.response(text=' '), None) ]) - def test_custom_type_response_body(self, input_http_response, expected_response_body): + def test_custom_type_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): http_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ - .deserialize_into(Employee.from_dictionary) \ + .deserialize_into(Employee.model_validate) \ .handle(input_http_response, self.global_errors()) assert ApiHelper.json_serialize(http_response) == expected_response_body @@ -172,7 +195,7 @@ def test_custom_type_response_body(self, input_http_response, expected_response_ (Base.response(text=''), None), (Base.response(text=' '), None) ]) - def test_json_response_body(self, input_http_response, expected_response_body): + def test_json_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): http_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .handle(input_http_response, self.global_errors()) @@ -204,17 +227,19 @@ def test_json_response_body(self, input_http_response, expected_response_body): '' ''), ]) - def test_xml_response_body_with_item_name(self, input_http_response, expected_response_body): + def test_xml_response_body_with_item_name(self, input_http_response: HttpResponse, expected_response_body: str): if sys.version_info[1] == 7: - expected_response_body = expected_response_body.replace('string="String" number="10000" boolean="false">', - 'boolean="false" number="10000" string="String">') + expected_response_body = expected_response_body.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') http_response = self.new_response_handler \ .is_xml_response(True) \ .deserializer(XmlHelper.deserialize_xml_to_list) \ .deserialize_into(XMLModel) \ .xml_item_name('item') \ .handle(input_http_response, self.global_errors()) - assert XmlHelper.serialize_list_to_xml(http_response, 'arrayOfModels', 'item') == expected_response_body + assert XmlHelper.serialize_list_to_xml( + http_response, 'arrayOfModels', 'item') == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ (Base.response(text=XmlHelper.serialize_to_xml(Base.xml_model(), 'AttributesAndElements')), @@ -229,10 +254,11 @@ def test_xml_response_body_with_item_name(self, input_http_response, expected_re '' ''), ]) - def test_xml_response_body_without_item_name(self, input_http_response, expected_response_body): + def test_xml_response_body_without_item_name(self, input_http_response: HttpResponse, expected_response_body: str): if sys.version_info[1] == 7: - expected_response_body = expected_response_body.replace('string="String" number="10000" boolean="false">', - 'boolean="false" number="10000" string="String">') + expected_response_body = expected_response_body.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') http_response = self.new_response_handler \ .is_xml_response(True) \ .deserializer(XmlHelper.deserialize_xml) \ @@ -246,7 +272,7 @@ def test_xml_response_body_without_item_name(self, input_http_response, expected (Base.response(text='{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}') ]) - def test_api_response(self, input_http_response, expected_response_body): + def test_api_response(self, input_http_response: HttpResponse, expected_response_body: str): api_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .is_api_response(True) \ @@ -254,13 +280,17 @@ def test_api_response(self, input_http_response, expected_response_body): assert ApiHelper.json_serialize(api_response.body) == expected_response_body assert api_response.is_success() is True assert api_response.is_error() is False - assert str(api_response) == ''.format(expected_response_body) + assert api_response.status_code == 200 + assert api_response.text == expected_response_body + assert api_response.reason_phrase is None + assert api_response.headers == {} @pytest.mark.parametrize('input_http_response, expected_response_body, expected_error_list', [ (Base.response(text='{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}'), '{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}', ['e1', 'e2']) ]) - def test_api_response_convertor(self, input_http_response, expected_response_body, expected_error_list): + def test_api_response_convertor( + self, input_http_response: HttpResponse, expected_response_body: str, expected_error_list: List[str]): api_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .is_api_response(True) \ @@ -268,8 +298,7 @@ def test_api_response_convertor(self, input_http_response, expected_response_bod .handle(input_http_response, self.global_errors()) assert isinstance(api_response, ApiResponse) and \ ApiHelper.json_serialize(api_response.body) == expected_response_body \ - and api_response.errors == expected_error_list \ - and api_response.cursor == "Test cursor" + and api_response.errors == expected_error_list @pytest.mark.parametrize('input_http_response, expected_response_body, expected_error_list', [ (Base.response(text='{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"]}'), @@ -277,7 +306,9 @@ def test_api_response_convertor(self, input_http_response, expected_response_bod (Base.response(text=''), None, None), (Base.response(text=' '), None, None) ]) - def test_api_response_with_errors(self, input_http_response, expected_response_body, expected_error_list): + def test_api_response_with_errors( + self, input_http_response: HttpResponse, expected_response_body: Optional[str], + expected_error_list: Optional[List[str]]): api_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .is_api_response(True) \ @@ -309,7 +340,9 @@ def test_api_response_with_errors(self, input_http_response, expected_response_b DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) ]) - def test_date_time_response_body(self, input_http_response, input_date_time_format, expected_response_body): + def test_date_time_response_body( + self, input_http_response: HttpResponse, input_date_time_format: DateTimeFormat, + expected_response_body: datetime): http_response = self.new_response_handler \ .deserializer(ApiHelper.datetime_deserialize) \ .datetime_format(input_date_time_format) \ @@ -321,7 +354,7 @@ def test_date_time_response_body(self, input_http_response, input_date_time_form (Base.response(text=ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))])), [date(1994, 2, 13), date(1994, 2, 13)]) ]) - def test_date_response_body(self, input_http_response, expected_response_body): + def test_date_response_body(self, input_http_response: HttpResponse, expected_response_body: date): http_response = self.new_response_handler \ .deserializer(ApiHelper.date_deserialize) \ .handle(input_http_response, self.global_errors()) diff --git a/tests/apimatic_core/union_type_tests/test_any_of.py b/tests/apimatic_core/union_type_tests/test_any_of.py index 690b26a..ed11e89 100644 --- a/tests/apimatic_core/union_type_tests/test_any_of.py +++ b/tests/apimatic_core/union_type_tests/test_any_of.py @@ -1,10 +1,11 @@ from datetime import datetime, date import pytest +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext + from apimatic_core.exceptions.anyof_validation_exception import AnyOfValidationException from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.types.union_types.leaf_type import LeafType from apimatic_core.types.union_types.any_of import AnyOf -from apimatic_core.types.union_types.union_type_context import UnionTypeContext from apimatic_core.utilities.api_helper import ApiHelper from apimatic_core.utilities.union_type_helper import UnionTypeHelper from tests.apimatic_core.base import Base @@ -27,262 +28,262 @@ class TestAnyOf: ('abc', [LeafType(int), LeafType(str)], UnionTypeContext(), True, 'abc'), (True, [LeafType(bool), LeafType(str)], UnionTypeContext(), True, True), (100.44, [LeafType(int), LeafType(str)], UnionTypeContext(), False, None), - (None, [LeafType(int), LeafType(str)], UnionTypeContext().nullable(True), True, None), - (None, [LeafType(int), LeafType(str)], UnionTypeContext().optional(True), True, None), + (None, [LeafType(int), LeafType(str)], UnionTypeContext(is_nullable=True), True, None), + (None, [LeafType(int), LeafType(str)], UnionTypeContext(is_optional=True), True, None), (None, [LeafType(int), LeafType(str)], UnionTypeContext(), False, None), # Outer Array Cases - (['abc', 'def'], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), True, ['abc', 'def']), - ([100, 200], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), True, [100, 200]), - ([100, 'abc'], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), True, [100, 'abc']), - (100, [LeafType(int), LeafType(str)], UnionTypeContext().array(True), False, None), - ('100', [LeafType(int), LeafType(str)], UnionTypeContext().array(True), False, None), - ([['abc', 'def']], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), False, None), - ([100, 200], [LeafType(int, UnionTypeContext().array(True)), LeafType(int)], UnionTypeContext(), True, [100, 200]), + (['abc', 'def'], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), True, ['abc', 'def']), + ([100, 200], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), True, [100, 200]), + ([100, 'abc'], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), True, [100, 'abc']), + (100, [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), False, None), + ('100', [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), False, None), + ([['abc', 'def']], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), False, None), + ([100, 200], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(int)], UnionTypeContext(), True, [100, 200]), # Inner Array Cases - (['abc', 'def'], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], + (['abc', 'def'], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], UnionTypeContext(), True, ['abc', 'def']), - ([100, 200], [LeafType(int, UnionTypeContext().array(True)), LeafType(str, UnionTypeContext().array(True))], + ([100, 200], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str, UnionTypeContext(is_array=True))], UnionTypeContext(), True, [100, 200]), ([100, 'abc'], - [LeafType(int, UnionTypeContext().array(True)), LeafType(str, UnionTypeContext().array(True))], + [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), # Partial Array Case - ('abc', [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], + ('abc', [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], UnionTypeContext(), True, 'abc'), - ([100, 200], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], + ([100, 200], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], UnionTypeContext(), True, [100, 200]), - ([100, 'abc'], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], + ([100, 'abc'], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], UnionTypeContext(), False, None), - (100, [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], + (100, [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], UnionTypeContext(), False, None), # Array of Partial Arrays Cases - (['abc', 'def'], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), True, ['abc', 'def']), - ([[100, 200]], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), True, [[100, 200]]), - ([[100, 200], 'abc'], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), True, [[100, 200], 'abc']), - ([[100, 'abc']], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), False, None), - ([100], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), False, None), + (['abc', 'def'], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), True, ['abc', 'def']), + ([[100, 200]], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), True, [[100, 200]]), + ([[100, 200], 'abc'], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), True, [[100, 200], 'abc']), + ([[100, 'abc']], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), False, None), + ([100], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), False, None), # Array of Arrays Cases - ([['abc', 'def'], ['def', 'ghi']], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [['abc', 'def'], ['def', 'ghi']]), - ([[100, 200], [300, 400]], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[100, 200], [300, 400]]), - ([[100, 200], ['abc', 'def']], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[100, 200], ['abc', 'def']]), - ([[100, 'abc'], [200, 'def']], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), - ([[100, 'abc'], ['def', 'ghi']], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), - ([[100.45, 200.45], [100, 200]], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), - ([['abc', 'def'], [100.45, 200.45]], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + ([['abc', 'def'], ['def', 'ghi']], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [['abc', 'def'], ['def', 'ghi']]), + ([[100, 200], [300, 400]], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[100, 200], [300, 400]]), + ([[100, 200], ['abc', 'def']], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[100, 200], ['abc', 'def']]), + ([[100, 'abc'], [200, 'def']], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), + ([[100, 'abc'], ['def', 'ghi']], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), + ([[100.45, 200.45], [100, 200]], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), + ([['abc', 'def'], [100.45, 200.45]], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), # Outer Dictionary Cases - ({'key0': 'abc', 'key1': 'def'}, [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), True, + ({'key0': 'abc', 'key1': 'def'}, [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 'abc', 'key1': 'def'}), - ({'key0': 100, 'key1': 200}, [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), True, + ({'key0': 100, 'key1': 200}, [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 100, 'key1': 200}), - ({'key0': 100, 'key2': 'abc'}, [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), True, + ({'key0': 100, 'key2': 'abc'}, [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 100, 'key2': 'abc'}), - (100, [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), False, None), - ('100', [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), False, None), + (100, [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), False, None), + ('100', [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': 'abc', 'key1': 'def'}}, [LeafType(int), LeafType(str)], - UnionTypeContext().dict(True), False, None), + UnionTypeContext(is_dict=True), False, None), # Inner Dictionary Cases - ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext().dict(True)), - LeafType(str, UnionTypeContext().dict(True))], + ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext(is_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True))], UnionTypeContext(), True, {'key0': 'abc', 'key1': 'def'}), - ({'key0': 100, 'key1': 200}, [LeafType(int, UnionTypeContext().dict(True)), - LeafType(str, UnionTypeContext().dict(True))], + ({'key0': 100, 'key1': 200}, [LeafType(int, UnionTypeContext(is_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True))], UnionTypeContext(), True, {'key0': 100, 'key1': 200}), - ({'key0': 100, 'key1': 'abc'}, [LeafType(int, UnionTypeContext().dict(True)), - LeafType(str, UnionTypeContext().array(True))], + ({'key0': 100, 'key1': 'abc'}, [LeafType(int, UnionTypeContext(is_dict=True)), + LeafType(str, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), # Partial Dictionary Cases - ('abc', [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], + ('abc', [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], UnionTypeContext(), True, 'abc'), - ({'key0': 100, 'key1': 200}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], + ({'key0': 100, 'key1': 200}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], UnionTypeContext(), True, {'key0': 100, 'key1': 200}), - ({'key0': 100, 'key1': 'abc'}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], + ({'key0': 100, 'key1': 'abc'}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], UnionTypeContext(), False, None), - (100, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], + (100, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], UnionTypeContext(), False, None), # Dictionary of Partial Dictionary Cases - ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), True, {'key0': 'abc', 'key1': 'def'}), - ({'key0': {'key0': 100, 'key1': 200}}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), True, {'key0': {'key0': 100, 'key1': 200}}), + ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), True, {'key0': 'abc', 'key1': 'def'}), + ({'key0': {'key0': 100, 'key1': 200}}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}}), ({'key0': {'key0': 100, 'key1': 200}, 'key1': 'abc'}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), True, {'key0': {'key0': 100, 'key1': 200}, 'key1': 'abc'}), - ({'key0': {'key0': 100, 'key1': 'abc'}}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), False, None), - ({'key0': 100}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}, 'key1': 'abc'}), + ({'key0': {'key0': 100, 'key1': 'abc'}}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), False, None), + ({'key0': 100}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), False, None), # Dictionary of Dictionary Cases ({'key0': {'key0': 'abc', 'key1': 'def'}, 'key1': {'key0': 'ghi', 'key1': 'jkl'}}, - [LeafType(int, UnionTypeContext().array(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, + [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 'abc', 'key1': 'def'}, 'key1': {'key0': 'ghi', 'key1': 'jkl'}}), ({'key0': {'key0': 100, 'key1': 200}, 'key1': {'key0': 300, 'key1': 400}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}, 'key1': {'key0': 300, 'key1': 400}}), ({'key0': {'key0': 100, 'key1': 200}, 'key1': {'key0': 'abc', 'key1': 'def'}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}, 'key1': {'key0': 'abc', 'key1': 'def'}}), ({'key0': {'key0': 100, 'key1': 'abc'}, 'key1': {'key0': 200, 'key1': 'def'}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': 100, 'key1': 'abc'}, 'key1': {'key0': 'abc', 'key1': 'def'}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': 100.45, 'key1': 200.45}, 'key1': {'key0': 100, 'key1': 200}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': 'abc', 'key1': 'def'}, 'key1': {'key0': 100.45, 'key1': 200.45}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), # Inner array of dictionary cases ([{'key0': 'abc', 'key1': 'def'}, {'key0': 'ghi', 'key1': 'jkl'}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), True, [{'key0': 'abc', 'key1': 'def'}, {'key0': 'ghi', 'key1': 'jkl'}]), ([{'key0': 100, 'key1': 200}, {'key0': 300, 'key1': 400}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), True, [{'key0': 100, 'key1': 200}, {'key0': 300, 'key1': 400}]), ([{'key0': 'abc', 'key1': 200}, {'key0': 'def', 'key1': 400}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), ([{'key0': 'abc', 'key1': 'def'}, {'key0': 100, 'key1': 200}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), ([{'key0': 'abc', 'key1': 'def'}, {'key0': 100, 'key1': 200}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), ({'key0': 100, 'key1': 200}, - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), # Outer array of dictionary cases ([{'key0': 'abc', 'key1': 'def'}, {'key0': 'ghi', 'key1': 'jkl'}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, [{'key0': 'abc', 'key1': 'def'}, {'key0': 'ghi', 'key1': 'jkl'}]), ([{'key0': 100, 'key1': 200}, {'key0': 300, 'key1': 400}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, [{'key0': 100, 'key1': 200}, {'key0': 300, 'key1': 400}]), ([{'key0': 'abc', 'key1': 200}, {'key0': 'def', 'key1': 400}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, [{'key0': 'abc', 'key1': 200}, {'key0': 'def', 'key1': 400}]), ([{'key0': 'abc', 'key1': 'def'}, {'key0': 100, 'key1': 200}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, [{'key0': 'abc', 'key1': 'def'}, {'key0': 100, 'key1': 200}]), ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), False, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), False, None), # dictionary of array cases ({'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}, - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), True, {'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}), ({'key0': [100, 200], 'key1': [300, 400]}, - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), True, {'key0': [100, 200], 'key1': [300, 400]}), ({'key0': ['abc', 200], 'key1': ['def', 400]}, - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), ({'key0': [100, 200], 'key1': ['abc', 'def']}, - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), # Outer dictionary of array cases ({'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, {'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}), + UnionTypeContext(is_dict=True, is_array=True), True, {'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}), ({'key0': [100, 200], 'key1': [300, 400]}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, {'key0': [100, 200], 'key1': [300, 400]}), + UnionTypeContext(is_dict=True, is_array=True), True, {'key0': [100, 200], 'key1': [300, 400]}), ({'key0': ['abc', 200], 'key1': ['def', 400]}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, {'key0': ['abc', 200], 'key1': ['def', 400]}), + UnionTypeContext(is_dict=True, is_array=True), True, {'key0': ['abc', 200], 'key1': ['def', 400]}), ({'key0': [100, 200], 'key1': ['abc', 'def']}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, {'key0': [100, 200], 'key1': ['abc', 'def']}), + UnionTypeContext(is_dict=True, is_array=True), True, {'key0': [100, 200], 'key1': ['abc', 'def']}), ([{'key0': [100, 200]}, {'key1': ['abc', 'def']}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), False, None), + UnionTypeContext(is_dict=True, is_array=True), False, None), # Nested oneOf cases ([[100, 200], ['abc', True]], [AnyOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[100, 200], ['abc', True]]), + UnionTypeContext(is_array=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[100, 200], ['abc', True]]), ([[100, 200], ['abc', True], None], [AnyOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True).nullable(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[100, 200], ['abc', True], None]), + UnionTypeContext(is_array=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[100, 200], ['abc', True], None]), ({'key0': {'key0': 100, 'key1': 200}, 'key2': {'key0': 'abc', 'key1': True}, 'key3': None}, [AnyOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().dict(True).nullable(True)), LeafType(int, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, + UnionTypeContext(is_dict=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}, 'key2': {'key0': 'abc', 'key1': True}, 'key3': None}), ({'key0': [100, 200], 'key2': ['abc', True], 'key3': None}, [AnyOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True).nullable(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().dict(True), True, + UnionTypeContext(is_array=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_dict=True), True, {'key0': [100, 200], 'key2': ['abc', True], 'key3': None}), ({'key0': [100, 200], 'key2': ['abc', True], 'key3': None}, [AnyOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True).nullable(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().dict(True), True, + UnionTypeContext(is_array=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_dict=True), True, {'key0': [100, 200], 'key2': ['abc', True], 'key3': None}), ([{'key0': 100, 'key1': 200}, {'key0': 'abc', 'key1': True}, None], [AnyOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().dict(True).nullable(True)), LeafType(int, UnionTypeContext().dict(True))], - UnionTypeContext().array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_array=True, is_array_of_dict=True), True, [{'key0': 100, 'key1': 200}, {'key0': 'abc', 'key1': True}, None]), ([[100, 200], None], [AnyOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + UnionTypeContext(is_array=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), ]) def test_any_of_primitive_type(self, input_value, input_types, input_context, expected_validity, expected_deserialized_value): @@ -301,19 +302,19 @@ def test_any_of_primitive_type(self, input_value, input_types, input_context, ex 'input_value, input_date, input_types, input_context, expected_validity, expected_value', [ (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37), False), Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), - [LeafType(datetime, UnionTypeContext().date_time_format(DateTimeFormat.RFC3339_DATE_TIME)), + [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), (Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37), False), Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37)), - [LeafType(datetime, UnionTypeContext().date_time_format(DateTimeFormat.HTTP_DATE_TIME)), LeafType(date)], + [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.HTTP_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), (ApiHelper.UnixDateTime(datetime(1994, 11, 6, 8, 49, 37)), 1480809600, - [LeafType(datetime, UnionTypeContext().date_time_format(DateTimeFormat.UNIX_DATE_TIME)), LeafType(date)], + [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.UNIX_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime.utcfromtimestamp(1480809600)), (datetime(1994, 11, 6, 8, 49, 37), Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), - [LeafType(datetime, UnionTypeContext().date_time_converter(ApiHelper.RFC3339DateTime).date_time_format(DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], + [LeafType(datetime, UnionTypeContext(date_time_converter=ApiHelper.RFC3339DateTime, date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - ('1994-11-06', '1994-11-06', [LeafType(date), LeafType(datetime, UnionTypeContext().date_time_format(DateTimeFormat.RFC3339_DATE_TIME))], UnionTypeContext(), + ('1994-11-06', '1994-11-06', [LeafType(date), LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME))], UnionTypeContext(), True, date(1994, 11, 6)) ]) def test_any_of_date_and_datetime(self, input_value, input_date, input_types, input_context, expected_validity, expected_value): @@ -324,18 +325,18 @@ def test_any_of_date_and_datetime(self, input_value, input_date, input_types, in @pytest.mark.parametrize( 'input_value, input_types, input_context, expected_validity, expected_value', [ - (None, [LeafType(int, UnionTypeContext().optional(True)), LeafType(str)], UnionTypeContext(), True, None), - (None, [LeafType(int, UnionTypeContext().optional(True)), LeafType(str, UnionTypeContext().optional(True))], + (None, [LeafType(int, UnionTypeContext(is_optional=True)), LeafType(str)], UnionTypeContext(), True, None), + (None, [LeafType(int, UnionTypeContext(is_optional=True)), LeafType(str, UnionTypeContext(is_optional=True))], UnionTypeContext(), True, None), - (None, [LeafType(int), LeafType(str)], UnionTypeContext().nullable(True), True, None), - (None, [LeafType(int, UnionTypeContext().nullable(True)), LeafType(str)], UnionTypeContext(), True, None), - (None, [LeafType(int, UnionTypeContext().nullable(True)), LeafType(str, UnionTypeContext().optional(True))], + (None, [LeafType(int), LeafType(str)], UnionTypeContext(is_nullable=True), True, None), + (None, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], UnionTypeContext(), True, None), + (None, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str, UnionTypeContext(is_optional=True))], UnionTypeContext(), True, None), - (None, [LeafType(int), LeafType(str)], UnionTypeContext().nullable(True), True, None), - ([1, None, 2], [LeafType(int, UnionTypeContext().nullable(True)), LeafType(str)], - UnionTypeContext().array(True), True, [1, None, 2]), - ({'key0': 1, None: None, 'key3': 2}, [LeafType(int, UnionTypeContext().nullable(True)), LeafType(str)], - UnionTypeContext().dict(True), True, {'key0': 1, None: None, 'key3': 2}) + (None, [LeafType(int), LeafType(str)], UnionTypeContext(is_nullable=True), True, None), + ([1, None, 2], [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], + UnionTypeContext(is_array=True), True, [1, None, 2]), + ({'key0': 1, 'key3': 2}, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], + UnionTypeContext(is_dict=True), True, {'key0': 1, 'key3': 2}) ]) def test_any_of_optional_nullable(self, input_value, input_types, input_context, expected_validity, expected_value): union_type_result = AnyOf(input_types, input_context).validate(input_value) @@ -347,300 +348,300 @@ def test_any_of_optional_nullable(self, input_value, input_types, input_context, 'input_value, input_types, input_context, expected_is_valid_output, expected_deserialized_value_output', [ # Simple Cases ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext(), True, Atom(2, 5)), + UnionTypeContext(), True, Atom(atom_number_of_electrons=2, atom_number_of_protons=5)), ({"OrbitNumberOfElectrons": 4}, [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext(), True, Orbit(4)), + UnionTypeContext(), True, Orbit(orbit_number_of_electrons=4)), # Outer Array Cases ([{"OrbitNumberOfElectrons": 4}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().array(True), True, [Orbit(4), Atom(2, 5)]), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_array=True), True, [Orbit(orbit_number_of_electrons=4), Atom(atom_number_of_electrons=2, atom_number_of_protons=5)]), ([{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}], - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().array(True), True, [Atom(2, 5), Atom(4, 10)]), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_array=True), True, [Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)]), ([{"OrbitNumberOfElectrons": 4}, {"OrbitNumberOfElectrons": 5}], [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext().array(True), True, [Orbit(4), Orbit(5)]), + UnionTypeContext(is_array=True), True, [Orbit(orbit_number_of_electrons=4), Orbit(orbit_number_of_electrons=5)]), ({"OrbitNumberOfElectrons": 4}, [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext().array(True), False, None), + UnionTypeContext(is_array=True), False, None), ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext().array(True), False, None), + UnionTypeContext(is_array=True), False, None), # Inner Array Cases ([{"OrbitNumberOfElectrons": 4}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), ([{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext(), True, [Atom(2, 5), Atom(4, 10)]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(), True, [Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)]), ([{"OrbitNumberOfElectrons": 4}, {"OrbitNumberOfElectrons": 5}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext(), True, [Orbit(4), Orbit(5)]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(), True, [Orbit(orbit_number_of_electrons=4), Orbit(orbit_number_of_electrons=5)]), ([{"OrbitNumberOfElectrons": 4}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), ({"OrbitNumberOfElectrons": 4}, - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), # Partial Array Case ({"OrbitNumberOfElectrons": 4}, - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), True, Orbit(4)), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), True, Orbit(orbit_number_of_electrons=4)), ([{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), True, [Atom(2, 5), Atom(4, 10)]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), True, [Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)]), ('[{"OrbitNumberOfElectrons": 4}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}]', - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext())], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), False, None), - ('{"OrbitNumberOfElectrons": 4}', [LeafType(Orbit, UnionTypeContext().array(True)), LeafType(Atom)], + ('{"OrbitNumberOfElectrons": 4}', [LeafType(Orbit, UnionTypeContext(is_array=True)), LeafType(Atom)], UnionTypeContext(), False, None), # Array of Partial Arrays Cases ([[{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit)], - UnionTypeContext().array(True), True, [[Atom(2, 5), Atom(4, 10)]]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit)], + UnionTypeContext(is_array=True), True, [[Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)]]), ([[{"OrbitNumberOfElectrons": 4}, {"OrbitNumberOfElectrons": 4}]], - [LeafType(Orbit, UnionTypeContext().array(True)), LeafType(Atom)], - UnionTypeContext().array(True), True, [[Orbit(4), Orbit(4)]]), + [LeafType(Orbit, UnionTypeContext(is_array=True)), LeafType(Atom)], + UnionTypeContext(is_array=True), True, [[Orbit(orbit_number_of_electrons=4), Orbit(orbit_number_of_electrons=4)]]), ([[{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}], {"OrbitNumberOfElectrons": 4}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext())], - UnionTypeContext().array(True), True, [[Atom(2, 5), Atom(4, 10)], Orbit(4)]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext())], + UnionTypeContext(is_array=True), True, [[Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)], Orbit(orbit_number_of_electrons=4)]), ([[{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"OrbitNumberOfElectrons": 4}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit)], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit)], + UnionTypeContext(is_array=True), False, None), ([{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit)], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit)], + UnionTypeContext(is_array=True), False, None), # Array of Arrays Cases ([[{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 3, "AtomNumberOfProtons": 6}], [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 3, "AtomNumberOfProtons": 7}]], - [LeafType(Atom, UnionTypeContext().array(True)), - LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[Atom(2, 5), Atom(3, 6)], [Atom(2, 10), Atom(3, 7)]]), + [LeafType(Atom, UnionTypeContext(is_array=True)), + LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=3, atom_number_of_protons=6)], [Atom(atom_number_of_electrons=2, atom_number_of_protons=10), Atom(atom_number_of_electrons=3, atom_number_of_protons=7)]]), ([[{"OrbitNumberOfElectrons": 4}, {"OrbitNumberOfElectrons": 6}], [{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}]], - [LeafType(Orbit, UnionTypeContext().array(True)), LeafType(Atom, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[Orbit(4), Orbit(6)], [Orbit(8), Orbit(10)]]), + [LeafType(Orbit, UnionTypeContext(is_array=True)), LeafType(Atom, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[Orbit(orbit_number_of_electrons=4), Orbit(orbit_number_of_electrons=6)], [Orbit(orbit_number_of_electrons=8), Orbit(orbit_number_of_electrons=10)]]), ([[{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}], [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}]], - [LeafType(Orbit, UnionTypeContext().array(True)), LeafType(Atom, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[Orbit(8), Orbit(10)], [Atom(2, 5), Atom(2, 10)]]), + [LeafType(Orbit, UnionTypeContext(is_array=True)), LeafType(Atom, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[Orbit(orbit_number_of_electrons=8), Orbit(orbit_number_of_electrons=10)], [Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=2, atom_number_of_protons=10)]]), ([[{"OrbitNumberOfElectrons": 8}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], [{"OrbitNumberOfElectrons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), ([[{"OrbitNumberOfElectrons": 8}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], [{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), ([[{"OrbitNumberOfElectrons": 8.5}, {"OrbitNumberOfElectrons": 10.5}], [{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), ([[{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), # Outer Dictionary Cases ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().dict(True), True, - {'key0': Atom(2, 5), 'key1': Atom(2, 10)}), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_dict=True), True, + {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}), ({'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}, - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().dict(True), True, - {'key0': Orbit(8), 'key1': Orbit(10)}), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_dict=True), True, + {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}), ({'key0': {"OrbitNumberOfElectrons": 8}, 'key2': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}, - [LeafType(Orbit), LeafType(Atom)], UnionTypeContext().dict(True), True, - {'key0': Orbit(8), 'key2': Atom(2, 5)}), - ({"OrbitNumberOfElectrons": 8}, [LeafType(Orbit), LeafType(Atom)], UnionTypeContext().dict(True), False, None), + [LeafType(Orbit), LeafType(Atom)], UnionTypeContext(is_dict=True), True, + {'key0': Orbit(orbit_number_of_electrons=8), 'key2': Atom(atom_number_of_electrons=2, atom_number_of_protons=5)}), + ({"OrbitNumberOfElectrons": 8}, [LeafType(Orbit), LeafType(Atom)], UnionTypeContext(is_dict=True), False, None), ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Orbit), LeafType(Atom)], - UnionTypeContext().dict(True), False, None), + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}}, - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().dict(True), False, None), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_dict=True), False, None), # Inner Dictionary Cases ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext(), True, {'key0': Atom(2, 5), 'key1': Atom(2, 10)}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(), True, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}), ({'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext(), True, {'key0': Orbit(8), 'key1': Orbit(10)}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(), True, {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}), ({'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}, - [LeafType(Atom, UnionTypeContext().dict(True)), - LeafType(Orbit, UnionTypeContext().dict(True))], + [LeafType(Atom, UnionTypeContext(is_dict=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True))], UnionTypeContext(), False, None), # Partial Dictionary Cases - ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Orbit, UnionTypeContext().dict(True)), + ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], UnionTypeContext(), True, - Atom(2, 5)), + Atom(atom_number_of_electrons=2, atom_number_of_protons=5)), ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit)], - UnionTypeContext(), True, {'key0': Atom(2, 5), 'key1': Atom(2, 10)}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit)], + UnionTypeContext(), True, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}), ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"OrbitNumberOfElectrons": 8}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit)], UnionTypeContext(), False, None), - ({"OrbitNumberOfElectrons": 8}, [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom)], + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit)], UnionTypeContext(), False, None), + ({"OrbitNumberOfElectrons": 8}, [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], UnionTypeContext(), False, None), # Dictionary of Partial Dictionary Cases ({'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit)], - UnionTypeContext().dict(True), True, {'key0': Orbit(8), 'key1': Orbit(10)}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit)], + UnionTypeContext(is_dict=True), True, {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}}, - [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom)], - UnionTypeContext().dict(True), True, {'key0': {'key0': Orbit(8), 'key1': Orbit(10)}}), + [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}, - [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom)], - UnionTypeContext().dict(True), True, {'key0': {'key0': Orbit(8), 'key1': Orbit(10)}, 'key1': Atom(2, 5)}), + [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}, 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=5)}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}}, - [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom)], - UnionTypeContext().dict(True), False, None), + [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], + UnionTypeContext(is_dict=True), False, None), ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit)], - UnionTypeContext().dict(True), False, None), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit)], + UnionTypeContext(is_dict=True), False, None), # Dictionary of Dictionary Cases ({'key0': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, 'key1': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, - {'key0': {'key0': Atom(2, 5), 'key1': Atom(2, 10)}, 'key1': {'key0': Atom(2, 5), 'key1': Atom(2, 10)}}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, + {'key0': {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}, 'key1': {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 4}, 'key1': {"OrbitNumberOfElectrons": 8}}, 'key1': {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 12}}}, - [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, - {'key0': {'key0': Orbit(4), 'key1': Orbit(8)}, 'key1': {'key0': Orbit(10), 'key1': Orbit(12)}}), + [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, + {'key0': {'key0': Orbit(orbit_number_of_electrons=4), 'key1': Orbit(orbit_number_of_electrons=8)}, 'key1': {'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=12)}}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 8}}, 'key1': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, - {'key0': {'key0': Orbit(10), 'key1': Orbit(8)}, 'key1': {'key0': Atom(2, 5), 'key1': Atom(2, 10)}}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, + {'key0': {'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=8)}, 'key1': {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}}), ({'key0': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"OrbitNumberOfElectrons": 10}}, 'key1': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"OrbitNumberOfElectrons": 12}}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"OrbitNumberOfElectrons": 10}}, 'key1': {'key0': {"OrbitNumberOfElectrons": 12}, 'key1': {"OrbitNumberOfElectrons": 14}}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), # Inner array of dictionary cases ([{'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 12}}, {'key0': {"OrbitNumberOfElectrons": 14}, 'key1': {"OrbitNumberOfElectrons": 8}}], - [LeafType(Orbit, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(Atom, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), True, - [{'key0': Orbit(10), 'key1': Orbit(12)}, {'key0': Orbit(14), 'key1': Orbit(8)}]), + [{'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=12)}, {'key0': Orbit(orbit_number_of_electrons=14), 'key1': Orbit(orbit_number_of_electrons=8)}]), ([{'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 15}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 20}}], - [LeafType(Atom, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(Orbit, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), True, - [{'key0': Atom(2, 5), 'key1': Atom(2, 10)}, {'key0': Atom(2, 15), 'key1': Atom(2, 20)}]), + [{'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=15), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=20)}]), ([{'key0': {"OrbitNumberOfElectrons": 12}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 15}}], - [LeafType(Orbit, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(Atom, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), ([{'key0': {"OrbitNumberOfElectrons": 12}, 'key1': {"OrbitNumberOfElectrons": 10}}, {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 20}}], - [LeafType(Orbit, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(Atom, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), # Outer array of dictionary cases ([{'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 12}}, {'key0': {"OrbitNumberOfElectrons": 14}, 'key1': {"OrbitNumberOfElectrons": 16}}], [LeafType(Orbit, UnionTypeContext()), LeafType(Atom, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, - [{'key0': Orbit(10), 'key1': Orbit(12)}, {'key0': Orbit(14), 'key1': Orbit(16)}]), + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, + [{'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=12)}, {'key0': Orbit(orbit_number_of_electrons=14), 'key1': Orbit(orbit_number_of_electrons=16)}]), ([{'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 15}}, {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 20}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}], [LeafType(Orbit, UnionTypeContext()), LeafType(Atom, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, - [{'key0': Atom(2, 10), 'key1': Atom(2, 15)}, {'key0': Atom(2, 20), 'key1': Atom(2, 5)}]), + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, + [{'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=10), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=15)}, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=20), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=5)}]), ([{'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"OrbitNumberOfElectrons": 10}}, {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"OrbitNumberOfElectrons": 12}}], [LeafType(Atom, UnionTypeContext()), LeafType(Orbit, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, - [{'key0': Atom(2, 10), 'key1': Orbit(10)}, {'key0': Atom(2, 5), 'key1': Orbit(12)}]), + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, + [{'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=10), 'key1': Orbit(orbit_number_of_electrons=10)}, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Orbit(orbit_number_of_electrons=12)}]), ([{'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}}, {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 12}}], [LeafType(Atom, UnionTypeContext()), LeafType(Orbit, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, - [{'key0': Atom(2, 10), 'key1': Atom(2, 12)}, {'key0': Orbit(10), 'key1': Orbit(12)}]), + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, + [{'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=10), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=12)}, {'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=12)}]), # dictionary of array cases ({'key0': [{"OrbitNumberOfElectrons": 10}, {"OrbitNumberOfElectrons": 12}], 'key1': [{"OrbitNumberOfElectrons": 14}, {"OrbitNumberOfElectrons": 16}]}, - [LeafType(Orbit, UnionTypeContext().dict(True).array(True)), - LeafType(Atom, UnionTypeContext().dict(True).array(True))], - UnionTypeContext(), True, {'key0': [Orbit(10), Orbit(12)], 'key1': [Orbit(14), Orbit(16)]}), + [LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True))], + UnionTypeContext(), True, {'key0': [Orbit(orbit_number_of_electrons=10), Orbit(orbit_number_of_electrons=12)], 'key1': [Orbit(orbit_number_of_electrons=14), Orbit(orbit_number_of_electrons=16)]}), ({'key0': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}], 'key1': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 14}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 16}]}, - [LeafType(Atom, UnionTypeContext().dict(True).array(True)), - LeafType(Orbit, UnionTypeContext().dict(True).array(True))], - UnionTypeContext(), True, {'key0': [Atom(2, 10), Atom(2, 12)], 'key1': [Atom(2, 14), Atom(2, 16)]}), + [LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True))], + UnionTypeContext(), True, {'key0': [Atom(atom_number_of_electrons=2, atom_number_of_protons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)], 'key1': [Atom(atom_number_of_electrons=2, atom_number_of_protons=14), Atom(atom_number_of_electrons=2, atom_number_of_protons=16)]}), ({'key0': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"OrbitNumberOfElectrons": 10}], 'key1': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}, {"OrbitNumberOfElectrons": 12}]}, - [LeafType(Atom, UnionTypeContext().dict(True).array(True)), - LeafType(Orbit, UnionTypeContext().dict(True).array(True))], + [LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), ({'key0': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}], 'key1': [{"OrbitNumberOfElectrons": 10}, {"OrbitNumberOfElectrons": 12}]}, - [LeafType(Atom, UnionTypeContext().dict(True).array(True)), - LeafType(Orbit, UnionTypeContext().dict(True).array(True))], + [LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), # Outer dictionary of array cases ({'key0': [{"OrbitNumberOfElectrons": 10}, {"OrbitNumberOfElectrons": 12}], 'key1': [{"OrbitNumberOfElectrons": 14}, {"OrbitNumberOfElectrons": 16}]}, [LeafType(Orbit, UnionTypeContext()), LeafType(Atom, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, - {'key0': [Orbit(10), Orbit(12)], 'key1': [Orbit(14), Orbit(16)]}), + UnionTypeContext(is_dict=True, is_array=True), True, + {'key0': [Orbit(orbit_number_of_electrons=10), Orbit(orbit_number_of_electrons=12)], 'key1': [Orbit(orbit_number_of_electrons=14), Orbit(orbit_number_of_electrons=16)]}), ({'key0': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}], 'key1': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}]}, [LeafType(Orbit, UnionTypeContext()), LeafType(Atom, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, - {'key0': [Atom(2, 10), Atom(2, 12)], 'key1': [Atom(2, 10), Atom(2, 12)]}), + UnionTypeContext(is_dict=True, is_array=True), True, + {'key0': [Atom(atom_number_of_electrons=2, atom_number_of_protons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)], 'key1': [Atom(atom_number_of_electrons=2, atom_number_of_protons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)]}), ({'key0': [{"OrbitNumberOfElectrons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}], 'key1': [{"OrbitNumberOfElectrons": 12}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}]}, [LeafType(Atom, UnionTypeContext()), LeafType(Orbit, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, - {'key0': [Orbit(10), Atom(2, 10)], 'key1': [Orbit(12), Atom(2, 12)]}), + UnionTypeContext(is_dict=True, is_array=True), True, + {'key0': [Orbit(orbit_number_of_electrons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=10)], 'key1': [Orbit(orbit_number_of_electrons=12), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)]}), ]) def test_any_of_custom_type(self, input_value, input_types, input_context, expected_is_valid_output, expected_deserialized_value_output): @@ -657,54 +658,54 @@ def test_any_of_custom_type(self, input_value, input_types, input_context, expec @pytest.mark.parametrize('input_value, input_types, input_context, expected_output', [ # Simple Cases - ('{"id": 123, "weight": 5, "type": "lion", "kind": "hunter"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + ('{"id": "123", "weight": 5, "type": "lion", "kind": "hunter"}', + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), - ('{"id": 123, "weight": 5, "type": "lion123", "kind": "hunter"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + ('{"id": "123", "weight": 5, "type": "lion123", "kind": "hunter"}', + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer123", "kind": "hunted"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), - ('{"id": 123, "weight": 5, "type": "lion", "kind": "hunter123"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + ('{"id": "123", "weight": 5, "type": "lion", "kind": "hunter123"}', + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunted123"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), - ('{"id": 123, "weight": 5, "type": "lion", "kind": "hunter"}', - [LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Rabbit, UnionTypeContext().discriminator('type').discriminator_value('lion'))], + ('{"id": "123", "weight": 5, "type": "lion", "kind": "hunter"}', + [LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Rabbit, UnionTypeContext(discriminator='type', discriminator_value='lion'))], UnionTypeContext(), False), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunted"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('deer')), - LeafType(Rabbit, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='deer')), + LeafType(Rabbit, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), False), - ('{"id": 123, "weight": 5, "type": "lion", "kind": "hunter"}', - [LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + ('{"id": "123", "weight": 5, "type": "lion", "kind": "hunter"}', + [LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', - [LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', - [LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('deer')), - LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='deer')), + LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', - [LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('lion'))], + [LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='lion'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', [LeafType(dict), LeafType(dict)], UnionTypeContext(), True), @@ -732,57 +733,57 @@ def test_any_of_with_discriminator_custom_type(self, input_value, input_types, i UnionTypeContext(), False, None), # Outer Array - (['Monday', 'Tuesday'], [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), True, + (['Monday', 'Tuesday'], [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), True, ['Monday', 'Tuesday']), - ([1, 2], [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), True, + ([1, 2], [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), True, [1, 2]), - ([1, 'Monday'], [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), True, [1, 'Monday']), - (2, [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), False, None), - ('Monday', [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), False, None), - ([['January', 'February']], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), False, None), + ([1, 'Monday'], [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), True, [1, 'Monday']), + (2, [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), False, None), + ('Monday', [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), False, None), + ([['January', 'February']], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), False, None), # Inner Array Cases - (['Monday', 'Tuesday'], [LeafType(Days, UnionTypeContext().array(True)), - LeafType(Months, UnionTypeContext().array(True))], + (['Monday', 'Tuesday'], [LeafType(Days, UnionTypeContext(is_array=True)), + LeafType(Months, UnionTypeContext(is_array=True))], UnionTypeContext(), True, ['Monday', 'Tuesday']), - ([1, 2], [LeafType(Days, UnionTypeContext().array(True)), LeafType(Months, UnionTypeContext().array(True))], + ([1, 2], [LeafType(Days, UnionTypeContext(is_array=True)), LeafType(Months, UnionTypeContext(is_array=True))], UnionTypeContext(), True, [1, 2]), ([1, 'Monday'], - [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days, UnionTypeContext().array(True))], + [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), # Partial Array Case - ('Monday', [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], + ('Monday', [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], UnionTypeContext(), True, 'Monday'), - ([1, 2], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], + ([1, 2], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], UnionTypeContext(), True, [1, 2]), - ([1, 'Monday'], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], + ([1, 'Monday'], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], UnionTypeContext(), False, None), - (1, [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], + (1, [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], UnionTypeContext(), False, None), # Array of Partial Arrays Cases - (['Monday', 'Tuesday'], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), True, ['Monday', 'Tuesday']), - ([[1, 2]], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), True, [[1, 2]]), - ([[1, 2], 'Monday'], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), True, [[1, 2], 'Monday']), - ([[1, 'Monday']], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), False, None), - ([1], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), False, None), + (['Monday', 'Tuesday'], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), True, ['Monday', 'Tuesday']), + ([[1, 2]], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), True, [[1, 2]]), + ([[1, 2], 'Monday'], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), True, [[1, 2], 'Monday']), + ([[1, 'Monday']], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), False, None), + ([1], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), False, None), # Array of Arrays Cases - ([['Monday', 'Tuesday'], ['Wednesday', 'Thursday']], [LeafType(Days, UnionTypeContext().array(True)), - LeafType(Months, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [['Monday', 'Tuesday'], ['Wednesday', 'Thursday']]), - ([[1, 2], [3, 4]], [LeafType(Months, UnionTypeContext().array(True)), - LeafType(Days, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[1, 2], [3, 4]]), - ([[1, 2], ['Monday', 'Tuesday']], [LeafType(Months, UnionTypeContext().array(True)), - LeafType(Days, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[1, 2], ['Monday', 'Tuesday']]), + ([['Monday', 'Tuesday'], ['Wednesday', 'Thursday']], [LeafType(Days, UnionTypeContext(is_array=True)), + LeafType(Months, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [['Monday', 'Tuesday'], ['Wednesday', 'Thursday']]), + ([[1, 2], [3, 4]], [LeafType(Months, UnionTypeContext(is_array=True)), + LeafType(Days, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[1, 2], [3, 4]]), + ([[1, 2], ['Monday', 'Tuesday']], [LeafType(Months, UnionTypeContext(is_array=True)), + LeafType(Days, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[1, 2], ['Monday', 'Tuesday']]), ]) def test_any_of_enum_type(self, input_value, input_types, input_context, expected_is_valid_output, expected_deserialized_value_output): try: @@ -803,14 +804,9 @@ def test_any_of_enum_type(self, input_value, input_types, input_context, expecte UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)), (100.5, [LeafType(int), AnyOf([LeafType(bool), LeafType(str)])], UnionTypeContext(), '{} \nActual Value: 100.5\nExpected Type: Any Of int, bool, str.'.format( - UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)), - ([[100, 200], None], [AnyOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().array(True), - '{} \nActual Value: [[100, 200], None]\nExpected Type: Any Of str, bool, int.'.format( - UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)), + UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)) ]) - def test_one_of_validation_errors(self, input_value, input_types, input_context, expected_validation_message): + def test_any_of_validation_errors(self, input_value, input_types, input_context, expected_validation_message): with pytest.raises(AnyOfValidationException) as validation_error: AnyOf(input_types, input_context).validate(input_value) assert validation_error.value.message == expected_validation_message diff --git a/tests/apimatic_core/union_type_tests/test_one_of.py b/tests/apimatic_core/union_type_tests/test_one_of.py index 23af19a..668271d 100644 --- a/tests/apimatic_core/union_type_tests/test_one_of.py +++ b/tests/apimatic_core/union_type_tests/test_one_of.py @@ -1,10 +1,11 @@ -from datetime import datetime, date +from datetime import datetime, date, timezone import pytest +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext + from apimatic_core.exceptions.oneof_validation_exception import OneOfValidationException from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.types.union_types.leaf_type import LeafType from apimatic_core.types.union_types.one_of import OneOf -from apimatic_core.types.union_types.union_type_context import UnionTypeContext from apimatic_core.utilities.api_helper import ApiHelper from apimatic_core.utilities.union_type_helper import UnionTypeHelper from tests.apimatic_core.base import Base @@ -27,265 +28,265 @@ class TestOneOf: ('abc', [LeafType(int), LeafType(str)], UnionTypeContext(), True, 'abc'), (True, [LeafType(bool), LeafType(str)], UnionTypeContext(), True, True), (100.44, [LeafType(int), LeafType(str)], UnionTypeContext(), False, None), - (None, [LeafType(int), LeafType(str)], UnionTypeContext().nullable(True), True, None), - (None, [LeafType(int), LeafType(str)], UnionTypeContext().optional(True), True, None), + (None, [LeafType(int), LeafType(str)], UnionTypeContext(is_nullable=True), True, None), + (None, [LeafType(int), LeafType(str)], UnionTypeContext(is_optional=True), True, None), (None, [LeafType(int), LeafType(str)], UnionTypeContext(), False, None), # Outer Array Cases - (['abc', 'def'], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), True, ['abc', 'def']), - ([100, 200], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), True, [100, 200]), - ([100, 'abc'], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), True, [100, 'abc']), - (100, [LeafType(int), LeafType(str)], UnionTypeContext().array(True), False, None), - ('100', [LeafType(int), LeafType(str)], UnionTypeContext().array(True), False, None), - ([['abc', 'def']], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), False, None), + (['abc', 'def'], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), True, ['abc', 'def']), + ([100, 200], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), True, [100, 200]), + ([100, 'abc'], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), True, [100, 'abc']), + (100, [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), False, None), + ('100', [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), False, None), + ([['abc', 'def']], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), False, None), # Inner Array Cases - (['abc', 'def'], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], + (['abc', 'def'], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], UnionTypeContext(), True, ['abc', 'def']), - ([100, 200], [LeafType(int, UnionTypeContext().array(True)), LeafType(str, UnionTypeContext().array(True))], + ([100, 200], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str, UnionTypeContext(is_array=True))], UnionTypeContext(), True, [100, 200]), ([100, 'abc'], - [LeafType(int, UnionTypeContext().array(True)), LeafType(str, UnionTypeContext().array(True))], + [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), # Partial Array Case - ('abc', [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], + ('abc', [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], UnionTypeContext(), True, 'abc'), - ([100, 200], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], + ([100, 200], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], UnionTypeContext(), True, [100, 200]), - ([100, 'abc'], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], + ([100, 'abc'], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], UnionTypeContext(), False, None), - (100, [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], + (100, [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], UnionTypeContext(), False, None), # Array of Partial Arrays Cases - (['abc', 'def'], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), True, ['abc', 'def']), - ([[100, 200]], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), True, [[100, 200]]), - ([[100, 200], 'abc'], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), True, [[100, 200], 'abc']), - ([[100, 'abc']], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), False, None), - ([100], [LeafType(int, UnionTypeContext().array(True)), LeafType(str)], - UnionTypeContext().array(True), False, None), + (['abc', 'def'], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), True, ['abc', 'def']), + ([[100, 200]], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), True, [[100, 200]]), + ([[100, 200], 'abc'], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), True, [[100, 200], 'abc']), + ([[100, 'abc']], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), False, None), + ([100], [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str)], + UnionTypeContext(is_array=True), False, None), # Array of Arrays Cases - ([['abc', 'def'], ['def', 'ghi']], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [['abc', 'def'], ['def', 'ghi']]), - ([[100, 200], [300, 400]], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[100, 200], [300, 400]]), - ([[100, 200], ['abc', 'def']], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[100, 200], ['abc', 'def']]), - ([[100, 'abc'], [200, 'def']], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), - ([[100, 'abc'], ['def', 'ghi']], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), - ([[100.45, 200.45], [100, 200]], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), - ([['abc', 'def'], [100.45, 200.45]], [LeafType(int, UnionTypeContext().array(True)), - LeafType(str, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + ([['abc', 'def'], ['def', 'ghi']], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [['abc', 'def'], ['def', 'ghi']]), + ([[100, 200], [300, 400]], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[100, 200], [300, 400]]), + ([[100, 200], ['abc', 'def']], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[100, 200], ['abc', 'def']]), + ([[100, 'abc'], [200, 'def']], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), + ([[100, 'abc'], ['def', 'ghi']], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), + ([[100.45, 200.45], [100, 200]], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), + ([['abc', 'def'], [100.45, 200.45]], [LeafType(int, UnionTypeContext(is_array=True)), + LeafType(str, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), # Outer Dictionary Cases - ({'key0': 'abc', 'key1': 'def'}, [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), True, + ({'key0': 'abc', 'key1': 'def'}, [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 'abc', 'key1': 'def'}), - ({'key0': 100, 'key1': 200}, [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), True, + ({'key0': 100, 'key1': 200}, [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 100, 'key1': 200}), - ({'key0': 100, 'key2': 'abc'}, [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), True, + ({'key0': 100, 'key2': 'abc'}, [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 100, 'key2': 'abc'}), - (100, [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), False, None), - ('100', [LeafType(int), LeafType(str)], UnionTypeContext().dict(True), False, None), + (100, [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), False, None), + ('100', [LeafType(int), LeafType(str)], UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': 'abc', 'key1': 'def'}}, [LeafType(int), LeafType(str)], - UnionTypeContext().dict(True), False, None), + UnionTypeContext(is_dict=True), False, None), # Inner Dictionary Cases - ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext().dict(True)), - LeafType(str, UnionTypeContext().dict(True))], + ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext(is_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True))], UnionTypeContext(), True, {'key0': 'abc', 'key1': 'def'}), - ({'key0': 100, 'key1': 200}, [LeafType(int, UnionTypeContext().dict(True)), - LeafType(str, UnionTypeContext().dict(True))], + ({'key0': 100, 'key1': 200}, [LeafType(int, UnionTypeContext(is_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True))], UnionTypeContext(), True, {'key0': 100, 'key1': 200}), - ({'key0': 100, 'key1': 'abc'}, [LeafType(int, UnionTypeContext().dict(True)), - LeafType(str, UnionTypeContext().array(True))], + ({'key0': 100, 'key1': 'abc'}, [LeafType(int, UnionTypeContext(is_dict=True)), + LeafType(str, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), # Partial Dictionary Cases - ('abc', [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], + ('abc', [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], UnionTypeContext(), True, 'abc'), - ({'key0': 100, 'key1': 200}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], + ({'key0': 100, 'key1': 200}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], UnionTypeContext(), True, {'key0': 100, 'key1': 200}), - ({'key0': 100, 'key1': 'abc'}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], + ({'key0': 100, 'key1': 'abc'}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], UnionTypeContext(), False, None), - (100, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], + (100, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], UnionTypeContext(), False, None), # Dictionary of Partial Dictionary Cases - ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), True, {'key0': 'abc', 'key1': 'def'}), - ({'key0': {'key0': 100, 'key1': 200}}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), True, {'key0': {'key0': 100, 'key1': 200}}), + ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), True, {'key0': 'abc', 'key1': 'def'}), + ({'key0': {'key0': 100, 'key1': 200}}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}}), ({'key0': {'key0': 100, 'key1': 200}, 'key1': 'abc'}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), True, {'key0': {'key0': 100, 'key1': 200}, 'key1': 'abc'}), - ({'key0': {'key0': 100, 'key1': 'abc'}}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), False, None), - ({'key0': 100}, [LeafType(int, UnionTypeContext().dict(True)), LeafType(str)], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}, 'key1': 'abc'}), + ({'key0': {'key0': 100, 'key1': 'abc'}}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), False, None), + ({'key0': 100}, [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str)], + UnionTypeContext(is_dict=True), False, None), # Dictionary of Dictionary Cases ({'key0': {'key0': 'abc', 'key1': 'def'}, 'key1': {'key0': 'ghi', 'key1': 'jkl'}}, - [LeafType(int, UnionTypeContext().array(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, + [LeafType(int, UnionTypeContext(is_array=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 'abc', 'key1': 'def'}, 'key1': {'key0': 'ghi', 'key1': 'jkl'}}), ({'key0': {'key0': 100, 'key1': 200}, 'key1': {'key0': 300, 'key1': 400}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}, 'key1': {'key0': 300, 'key1': 400}}), ({'key0': {'key0': 100, 'key1': 200}, 'key1': {'key0': 'abc', 'key1': 'def'}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}, 'key1': {'key0': 'abc', 'key1': 'def'}}), ({'key0': {'key0': 100, 'key1': 'abc'}, 'key1': {'key0': 200, 'key1': 'def'}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': 100, 'key1': 'abc'}, 'key1': {'key0': 'abc', 'key1': 'def'}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': 100.45, 'key1': 200.45}, 'key1': {'key0': 100, 'key1': 200}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': 'abc', 'key1': 'def'}, 'key1': {'key0': 100.45, 'key1': 200.45}}, - [LeafType(int, UnionTypeContext().dict(True)), LeafType(str, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(int, UnionTypeContext(is_dict=True)), LeafType(str, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), # Inner array of dictionary cases ([{'key0': 'abc', 'key1': 'def'}, {'key0': 'ghi', 'key1': 'jkl'}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), True, [{'key0': 'abc', 'key1': 'def'}, {'key0': 'ghi', 'key1': 'jkl'}]), ([{'key0': 100, 'key1': 200}, {'key0': 300, 'key1': 400}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), True, [{'key0': 100, 'key1': 200}, {'key0': 300, 'key1': 400}]), ([{'key0': 'abc', 'key1': 200}, {'key0': 'def', 'key1': 400}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), ([{'key0': 'abc', 'key1': 'def'}, {'key0': 100, 'key1': 200}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), ([{'key0': 'abc', 'key1': 'def'}, {'key0': 100, 'key1': 200}], - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), ({'key0': 100, 'key1': 200}, - [LeafType(int, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(str, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), # Outer array of dictionary cases ([{'key0': 'abc', 'key1': 'def'}, {'key0': 'ghi', 'key1': 'jkl'}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, [{'key0': 'abc', 'key1': 'def'}, {'key0': 'ghi', 'key1': 'jkl'}]), ([{'key0': 100, 'key1': 200}, {'key0': 300, 'key1': 400}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, [{'key0': 100, 'key1': 200}, {'key0': 300, 'key1': 400}]), ([{'key0': 'abc', 'key1': 200}, {'key0': 'def', 'key1': 400}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, [{'key0': 'abc', 'key1': 200}, {'key0': 'def', 'key1': 400}]), ([{'key0': 'abc', 'key1': 'def'}, {'key0': 100, 'key1': 200}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, [{'key0': 'abc', 'key1': 'def'}, {'key0': 100, 'key1': 200}]), ({'key0': 'abc', 'key1': 'def'}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), False, + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), False, None), # dictionary of array cases ({'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}, - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), True, {'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}), ({'key0': [100, 200], 'key1': [300, 400]}, - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), True, {'key0': [100, 200], 'key1': [300, 400]}), ({'key0': ['abc', 200], 'key1': ['def', 400]}, - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), ({'key0': [100, 200], 'key1': ['abc', 'def']}, - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), ([{'key0': [100, 200]}, {'key1': ['abc', 'def']}], - [LeafType(int, UnionTypeContext().dict(True).array(True)), - LeafType(str, UnionTypeContext().dict(True).array(True))], + [LeafType(int, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(str, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), # Outer dictionary of array cases ({'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, {'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}), + UnionTypeContext(is_dict=True, is_array=True), True, {'key0': ['abc', 'def'], 'key1': ['ghi', 'jkl']}), ({'key0': [100, 200], 'key1': [300, 400]}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, {'key0': [100, 200], 'key1': [300, 400]}), + UnionTypeContext(is_dict=True, is_array=True), True, {'key0': [100, 200], 'key1': [300, 400]}), ({'key0': ['abc', 200], 'key1': ['def', 400]}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, {'key0': ['abc', 200], 'key1': ['def', 400]}), + UnionTypeContext(is_dict=True, is_array=True), True, {'key0': ['abc', 200], 'key1': ['def', 400]}), ({'key0': [100, 200], 'key1': ['abc', 'def']}, [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, {'key0': [100, 200], 'key1': ['abc', 'def']}), + UnionTypeContext(is_dict=True, is_array=True), True, {'key0': [100, 200], 'key1': ['abc', 'def']}), ([{'key0': [100, 200]}, {'key1': ['abc', 'def']}], [LeafType(int, UnionTypeContext()), LeafType(str, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), False, None), + UnionTypeContext(is_dict=True, is_array=True), False, None), # Nested oneOf cases ([[100, 200], ['abc', True]], [OneOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[100, 200], ['abc', True]]), + UnionTypeContext(is_array=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[100, 200], ['abc', True]]), ([[100, 200], ['abc', True], None], [OneOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True).nullable(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[100, 200], ['abc', True], None]), + UnionTypeContext(is_array=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[100, 200], ['abc', True], None]), ({'key0': {'key0': 100, 'key1': 200}, 'key2': {'key0': 'abc', 'key1': True}, 'key3': None}, [OneOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().dict(True).nullable(True)), LeafType(int, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, + UnionTypeContext(is_dict=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': 100, 'key1': 200}, 'key2': {'key0': 'abc', 'key1': True}, 'key3': None}), ({'key0': [100, 200], 'key2': ['abc', True], 'key3': None}, [OneOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True).nullable(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().dict(True), True, + UnionTypeContext(is_array=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_dict=True), True, {'key0': [100, 200], 'key2': ['abc', True], 'key3': None}), ({'key0': [100, 200], 'key2': ['abc', True], 'key3': None}, [OneOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True).nullable(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().dict(True), True, + UnionTypeContext(is_array=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_dict=True), True, {'key0': [100, 200], 'key2': ['abc', True], 'key3': None}), ([{'key0': 100, 'key1': 200}, {'key0': 'abc', 'key1': True}, None], [OneOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().dict(True).nullable(True)), LeafType(int, UnionTypeContext().dict(True))], - UnionTypeContext().array(True).array_of_dict(True), True, + UnionTypeContext(is_dict=True, is_nullable=True)), LeafType(int, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_array=True, is_array_of_dict=True), True, [{'key0': 100, 'key1': 200}, {'key0': 'abc', 'key1': True}, None]), ([[100, 200], None], [OneOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + UnionTypeContext(is_array=True)), LeafType(int, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), ]) def test_one_of_primitive_type(self, input_value, input_types, input_context, expected_is_valid_output, @@ -302,45 +303,43 @@ def test_one_of_primitive_type(self, input_value, input_types, input_context, ex assert actual_deserialized_value == expected_deserialized_value_output @pytest.mark.parametrize( - 'input_value, input_date, input_types, input_context, expected_validity, expected_value', [ - (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37), False), - Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), - [LeafType(datetime, UnionTypeContext().date_time_format(DateTimeFormat.RFC3339_DATE_TIME)), + 'input_value, input_types, input_context, expected_validity, expected_value', [ + (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), + [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - (Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37), False), - Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37)), - [LeafType(datetime, UnionTypeContext().date_time_format(DateTimeFormat.HTTP_DATE_TIME)), LeafType(date)], + (Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37)), + [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.HTTP_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - (ApiHelper.UnixDateTime(datetime(1994, 11, 6, 8, 49, 37)), 1480809600, - [LeafType(datetime, UnionTypeContext().date_time_format(DateTimeFormat.UNIX_DATE_TIME)), LeafType(date)], + (1480809600, + [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.UNIX_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime.utcfromtimestamp(1480809600)), - (datetime(1994, 11, 6, 8, 49, 37), Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), - [LeafType(datetime, UnionTypeContext().date_time_converter(ApiHelper.RFC3339DateTime).date_time_format(DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], + (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), + [LeafType(datetime, UnionTypeContext(date_time_converter=ApiHelper.RFC3339DateTime, date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - ('1994-11-06', '1994-11-06', [LeafType(date), LeafType(datetime, UnionTypeContext().date_time_format(DateTimeFormat.RFC3339_DATE_TIME))], UnionTypeContext(), + ('1994-11-06', [LeafType(date), LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME))], UnionTypeContext(), True, date(1994, 11, 6)) ]) - def test_one_of_date_and_datetime(self, input_value, input_date, input_types, input_context, expected_validity, expected_value): + def test_one_of_date_and_datetime(self, input_value, input_types, input_context, expected_validity, expected_value): union_type = OneOf(input_types, input_context) union_type_result = union_type.validate(input_value) assert union_type_result.is_valid == expected_validity - actual_deserialized_value = union_type_result.deserialize(input_date) + actual_deserialized_value = union_type_result.deserialize(input_value) assert actual_deserialized_value == expected_value @pytest.mark.parametrize( 'input_value, input_types, input_context, expected_validity, expected_value', [ - (None, [LeafType(int, UnionTypeContext().optional(True)), LeafType(str)], UnionTypeContext(), True, None), - (None, [LeafType(int, UnionTypeContext().optional(True)), LeafType(str, UnionTypeContext().optional(True))], + (None, [LeafType(int, UnionTypeContext(is_optional=True)), LeafType(str)], UnionTypeContext(), True, None), + (None, [LeafType(int, UnionTypeContext(is_optional=True)), LeafType(str, UnionTypeContext(is_optional=True))], UnionTypeContext(), True, None), - (None, [LeafType(int), LeafType(str)], UnionTypeContext().nullable(True), True, None), - (None, [LeafType(int, UnionTypeContext().nullable(True)), LeafType(str)], UnionTypeContext(), True, None), - (None, [LeafType(int, UnionTypeContext().nullable(True)), LeafType(str, UnionTypeContext().optional(True))], + (None, [LeafType(int), LeafType(str)], UnionTypeContext(is_nullable=True), True, None), + (None, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], UnionTypeContext(), True, None), + (None, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str, UnionTypeContext(is_optional=True))], UnionTypeContext(), True, None), - (None, [LeafType(int), LeafType(str)], UnionTypeContext().nullable(True), True, None), - ([1, None, 2], [LeafType(int, UnionTypeContext().nullable(True)), LeafType(str)], - UnionTypeContext().array(True), True, [1, None, 2]), - ({'key0': 1, None: None, 'key3': 2}, [LeafType(int, UnionTypeContext().nullable(True)), LeafType(str)], - UnionTypeContext().dict(True), True, {'key0': 1, None: None, 'key3': 2}) + (None, [LeafType(int), LeafType(str)], UnionTypeContext(is_nullable=True), True, None), + ([1, None, 2], [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], + UnionTypeContext(is_array=True), True, [1, None, 2]), + ({'key0': 1, 'key3': 2}, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], + UnionTypeContext(is_dict=True), True, {'key0': 1, 'key3': 2}) ]) def test_one_of_optional_nullable(self, input_value, input_types, input_context, expected_validity, expected_value): union_type_result = OneOf(input_types, input_context).validate(input_value) @@ -352,300 +351,300 @@ def test_one_of_optional_nullable(self, input_value, input_types, input_context, 'input_value, input_types, input_context, expected_is_valid_output, expected_deserialized_value_output', [ # Simple Cases ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext(), True, Atom(2, 5)), + UnionTypeContext(), True, Atom(atom_number_of_electrons=2, atom_number_of_protons=5)), ({"OrbitNumberOfElectrons": 4}, [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext(), True, Orbit(4)), + UnionTypeContext(), True, Orbit(orbit_number_of_electrons=4)), # Outer Array Cases ([{"OrbitNumberOfElectrons": 4}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().array(True), True, [Orbit(4), Atom(2, 5)]), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_array=True), True, [Orbit(orbit_number_of_electrons=4), Atom(atom_number_of_electrons=2, atom_number_of_protons=5)]), ([{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}], - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().array(True), True, [Atom(2, 5), Atom(4, 10)]), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_array=True), True, [Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)]), ([{"OrbitNumberOfElectrons": 4}, {"OrbitNumberOfElectrons": 5}], [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext().array(True), True, [Orbit(4), Orbit(5)]), + UnionTypeContext(is_array=True), True, [Orbit(orbit_number_of_electrons=4), Orbit(orbit_number_of_electrons=5)]), ({"OrbitNumberOfElectrons": 4}, [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext().array(True), False, None), + UnionTypeContext(is_array=True), False, None), ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Atom), LeafType(Orbit)], - UnionTypeContext().array(True), False, None), + UnionTypeContext(is_array=True), False, None), # Inner Array Cases ([{"OrbitNumberOfElectrons": 4}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), ([{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext(), True, [Atom(2, 5), Atom(4, 10)]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(), True, [Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)]), ([{"OrbitNumberOfElectrons": 4}, {"OrbitNumberOfElectrons": 5}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext(), True, [Orbit(4), Orbit(5)]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(), True, [Orbit(orbit_number_of_electrons=4), Orbit(orbit_number_of_electrons=5)]), ([{"OrbitNumberOfElectrons": 4}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), ({"OrbitNumberOfElectrons": 4}, - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), # Partial Array Case ({"OrbitNumberOfElectrons": 4}, - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), True, Orbit(4)), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), True, Orbit(orbit_number_of_electrons=4)), ([{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), True, [Atom(2, 5), Atom(4, 10)]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), True, [Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)]), ('[{"OrbitNumberOfElectrons": 4}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}]', - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext())], + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext())], UnionTypeContext(), False, None), - ('{"OrbitNumberOfElectrons": 4}', [LeafType(Orbit, UnionTypeContext().array(True)), LeafType(Atom)], + ('{"OrbitNumberOfElectrons": 4}', [LeafType(Orbit, UnionTypeContext(is_array=True)), LeafType(Atom)], UnionTypeContext(), False, None), # Array of Partial Arrays Cases ([[{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit)], - UnionTypeContext().array(True), True, [[Atom(2, 5), Atom(4, 10)]]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit)], + UnionTypeContext(is_array=True), True, [[Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)]]), ([[{"OrbitNumberOfElectrons": 4}, {"OrbitNumberOfElectrons": 4}]], - [LeafType(Orbit, UnionTypeContext().array(True)), LeafType(Atom)], - UnionTypeContext().array(True), True, [[Orbit(4), Orbit(4)]]), + [LeafType(Orbit, UnionTypeContext(is_array=True)), LeafType(Atom)], + UnionTypeContext(is_array=True), True, [[Orbit(orbit_number_of_electrons=4), Orbit(orbit_number_of_electrons=4)]]), ([[{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 4, "AtomNumberOfProtons": 10}], {"OrbitNumberOfElectrons": 4}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext())], - UnionTypeContext().array(True), True, [[Atom(2, 5), Atom(4, 10)], Orbit(4)]), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext())], + UnionTypeContext(is_array=True), True, [[Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=4, atom_number_of_protons=10)], Orbit(orbit_number_of_electrons=4)]), ([[{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"OrbitNumberOfElectrons": 4}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit)], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit)], + UnionTypeContext(is_array=True), False, None), ([{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit)], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit)], + UnionTypeContext(is_array=True), False, None), # Array of Arrays Cases ([[{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 3, "AtomNumberOfProtons": 6}], [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 3, "AtomNumberOfProtons": 7}]], - [LeafType(Atom, UnionTypeContext().array(True)), - LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[Atom(2, 5), Atom(3, 6)], [Atom(2, 10), Atom(3, 7)]]), + [LeafType(Atom, UnionTypeContext(is_array=True)), + LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=3, atom_number_of_protons=6)], [Atom(atom_number_of_electrons=2, atom_number_of_protons=10), Atom(atom_number_of_electrons=3, atom_number_of_protons=7)]]), ([[{"OrbitNumberOfElectrons": 4}, {"OrbitNumberOfElectrons": 6}], [{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}]], - [LeafType(Orbit, UnionTypeContext().array(True)), LeafType(Atom, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[Orbit(4), Orbit(6)], [Orbit(8), Orbit(10)]]), + [LeafType(Orbit, UnionTypeContext(is_array=True)), LeafType(Atom, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[Orbit(orbit_number_of_electrons=4), Orbit(orbit_number_of_electrons=6)], [Orbit(orbit_number_of_electrons=8), Orbit(orbit_number_of_electrons=10)]]), ([[{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}], [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}]], - [LeafType(Orbit, UnionTypeContext().array(True)), LeafType(Atom, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[Orbit(8), Orbit(10)], [Atom(2, 5), Atom(2, 10)]]), + [LeafType(Orbit, UnionTypeContext(is_array=True)), LeafType(Atom, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[Orbit(orbit_number_of_electrons=8), Orbit(orbit_number_of_electrons=10)], [Atom(atom_number_of_electrons=2, atom_number_of_protons=5), Atom(atom_number_of_electrons=2, atom_number_of_protons=10)]]), ([[{"OrbitNumberOfElectrons": 8}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], [{"OrbitNumberOfElectrons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), ([[{"OrbitNumberOfElectrons": 8}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}], [{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), ([[{"OrbitNumberOfElectrons": 8.5}, {"OrbitNumberOfElectrons": 10.5}], [{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), ([[{"OrbitNumberOfElectrons": 8}, {"OrbitNumberOfElectrons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}]], - [LeafType(Atom, UnionTypeContext().array(True)), LeafType(Orbit, UnionTypeContext().array(True))], - UnionTypeContext().array(True), False, None), + [LeafType(Atom, UnionTypeContext(is_array=True)), LeafType(Orbit, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), False, None), # Outer Dictionary Cases ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().dict(True), True, - {'key0': Atom(2, 5), 'key1': Atom(2, 10)}), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_dict=True), True, + {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}), ({'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}, - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().dict(True), True, - {'key0': Orbit(8), 'key1': Orbit(10)}), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_dict=True), True, + {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}), ({'key0': {"OrbitNumberOfElectrons": 8}, 'key2': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}, - [LeafType(Orbit), LeafType(Atom)], UnionTypeContext().dict(True), True, - {'key0': Orbit(8), 'key2': Atom(2, 5)}), - ({"OrbitNumberOfElectrons": 8}, [LeafType(Orbit), LeafType(Atom)], UnionTypeContext().dict(True), False, None), + [LeafType(Orbit), LeafType(Atom)], UnionTypeContext(is_dict=True), True, + {'key0': Orbit(orbit_number_of_electrons=8), 'key2': Atom(atom_number_of_electrons=2, atom_number_of_protons=5)}), + ({"OrbitNumberOfElectrons": 8}, [LeafType(Orbit), LeafType(Atom)], UnionTypeContext(is_dict=True), False, None), ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Orbit), LeafType(Atom)], - UnionTypeContext().dict(True), False, None), + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}}, - [LeafType(Atom), LeafType(Orbit)], UnionTypeContext().dict(True), False, None), + [LeafType(Atom), LeafType(Orbit)], UnionTypeContext(is_dict=True), False, None), # Inner Dictionary Cases ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext(), True, {'key0': Atom(2, 5), 'key1': Atom(2, 10)}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(), True, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}), ({'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext(), True, {'key0': Orbit(8), 'key1': Orbit(10)}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(), True, {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}), ({'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}, - [LeafType(Atom, UnionTypeContext().dict(True)), - LeafType(Orbit, UnionTypeContext().dict(True))], + [LeafType(Atom, UnionTypeContext(is_dict=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True))], UnionTypeContext(), False, None), # Partial Dictionary Cases - ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Orbit, UnionTypeContext().dict(True)), + ({"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], UnionTypeContext(), True, - Atom(2, 5)), + Atom(atom_number_of_electrons=2, atom_number_of_protons=5)), ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit)], - UnionTypeContext(), True, {'key0': Atom(2, 5), 'key1': Atom(2, 10)}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit)], + UnionTypeContext(), True, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}), ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"OrbitNumberOfElectrons": 8}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit)], UnionTypeContext(), False, None), - ({"OrbitNumberOfElectrons": 8}, [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom)], + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit)], UnionTypeContext(), False, None), + ({"OrbitNumberOfElectrons": 8}, [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], UnionTypeContext(), False, None), # Dictionary of Partial Dictionary Cases ({'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit)], - UnionTypeContext().dict(True), True, {'key0': Orbit(8), 'key1': Orbit(10)}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit)], + UnionTypeContext(is_dict=True), True, {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}}, - [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom)], - UnionTypeContext().dict(True), True, {'key0': {'key0': Orbit(8), 'key1': Orbit(10)}}), + [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 8}, 'key1': {"OrbitNumberOfElectrons": 10}}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}, - [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom)], - UnionTypeContext().dict(True), True, {'key0': {'key0': Orbit(8), 'key1': Orbit(10)}, 'key1': Atom(2, 5)}), + [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], + UnionTypeContext(is_dict=True), True, {'key0': {'key0': Orbit(orbit_number_of_electrons=8), 'key1': Orbit(orbit_number_of_electrons=10)}, 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=5)}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}}, - [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom)], - UnionTypeContext().dict(True), False, None), + [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom)], + UnionTypeContext(is_dict=True), False, None), ({'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit)], - UnionTypeContext().dict(True), False, None), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit)], + UnionTypeContext(is_dict=True), False, None), # Dictionary of Dictionary Cases ({'key0': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, - 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, \ - 'key1': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, \ + 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, + 'key1': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, - {'key0': {'key0': Atom(2, 5), 'key1': Atom(2, 10)}, 'key1': {'key0': Atom(2, 5), 'key1': Atom(2, 10)}}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, + {'key0': {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}, 'key1': {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 4}, 'key1': {"OrbitNumberOfElectrons": 8}}, 'key1': {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 12}}}, - [LeafType(Orbit, UnionTypeContext().dict(True)), LeafType(Atom, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, - {'key0': {'key0': Orbit(4), 'key1': Orbit(8)}, 'key1': {'key0': Orbit(10), 'key1': Orbit(12)}}), + [LeafType(Orbit, UnionTypeContext(is_dict=True)), LeafType(Atom, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, + {'key0': {'key0': Orbit(orbit_number_of_electrons=4), 'key1': Orbit(orbit_number_of_electrons=8)}, 'key1': {'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=12)}}), ({'key0': {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 8}}, 'key1': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), True, - {'key0': {'key0': Orbit(10), 'key1': Orbit(8)}, 'key1': {'key0': Atom(2, 5), 'key1': Atom(2, 10)}}), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), True, + {'key0': {'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=8)}, 'key1': {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}}), ({'key0': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"OrbitNumberOfElectrons": 10}}, 'key1': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"OrbitNumberOfElectrons": 12}}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), ({'key0': {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"OrbitNumberOfElectrons": 10}}, 'key1': {'key0': {"OrbitNumberOfElectrons": 12}, 'key1': {"OrbitNumberOfElectrons": 14}}}, - [LeafType(Atom, UnionTypeContext().dict(True)), LeafType(Orbit, UnionTypeContext().dict(True))], - UnionTypeContext().dict(True), False, None), + [LeafType(Atom, UnionTypeContext(is_dict=True)), LeafType(Orbit, UnionTypeContext(is_dict=True))], + UnionTypeContext(is_dict=True), False, None), # Inner array of dictionary cases ([{'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 12}}, {'key0': {"OrbitNumberOfElectrons": 14}, 'key1': {"OrbitNumberOfElectrons": 8}}], - [LeafType(Orbit, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(Atom, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), True, - [{'key0': Orbit(10), 'key1': Orbit(12)}, {'key0': Orbit(14), 'key1': Orbit(8)}]), + [{'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=12)}, {'key0': Orbit(orbit_number_of_electrons=14), 'key1': Orbit(orbit_number_of_electrons=8)}]), ([{'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 15}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 20}}], - [LeafType(Atom, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(Orbit, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), True, - [{'key0': Atom(2, 5), 'key1': Atom(2, 10)}, {'key0': Atom(2, 15), 'key1': Atom(2, 20)}]), + [{'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=10)}, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=15), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=20)}]), ([{'key0': {"OrbitNumberOfElectrons": 12}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}}, {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 15}}], - [LeafType(Orbit, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(Atom, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), ([{'key0': {"OrbitNumberOfElectrons": 12}, 'key1': {"OrbitNumberOfElectrons": 10}}, {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 20}}], - [LeafType(Orbit, UnionTypeContext().dict(True).array(True).array_of_dict(True)), - LeafType(Atom, UnionTypeContext().dict(True).array(True).array_of_dict(True))], + [LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True)), + LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True))], UnionTypeContext(), False, None), # Outer array of dictionary cases ([{'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 12}}, {'key0': {"OrbitNumberOfElectrons": 14}, 'key1': {"OrbitNumberOfElectrons": 16}}], [LeafType(Orbit, UnionTypeContext()), LeafType(Atom, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, - [{'key0': Orbit(10), 'key1': Orbit(12)}, {'key0': Orbit(14), 'key1': Orbit(16)}]), + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, + [{'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=12)}, {'key0': Orbit(orbit_number_of_electrons=14), 'key1': Orbit(orbit_number_of_electrons=16)}]), ([{'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 15}}, {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 20}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}}], [LeafType(Orbit, UnionTypeContext()), LeafType(Atom, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, - [{'key0': Atom(2, 10), 'key1': Atom(2, 15)}, {'key0': Atom(2, 20), 'key1': Atom(2, 5)}]), + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, + [{'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=10), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=15)}, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=20), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=5)}]), ([{'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"OrbitNumberOfElectrons": 10}}, {'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 5}, 'key1': {"OrbitNumberOfElectrons": 12}}], [LeafType(Atom, UnionTypeContext()), LeafType(Orbit, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, - [{'key0': Atom(2, 10), 'key1': Orbit(10)}, {'key0': Atom(2, 5), 'key1': Orbit(12)}]), + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, + [{'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=10), 'key1': Orbit(orbit_number_of_electrons=10)}, {'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=5), 'key1': Orbit(orbit_number_of_electrons=12)}]), ([{'key0': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, 'key1': {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}}, {'key0': {"OrbitNumberOfElectrons": 10}, 'key1': {"OrbitNumberOfElectrons": 12}}], [LeafType(Atom, UnionTypeContext()), LeafType(Orbit, UnionTypeContext())], - UnionTypeContext().dict(True).array(True).array_of_dict(True), True, - [{'key0': Atom(2, 10), 'key1': Atom(2, 12)}, {'key0': Orbit(10), 'key1': Orbit(12)}]), + UnionTypeContext(is_dict=True, is_array=True, is_array_of_dict=True), True, + [{'key0': Atom(atom_number_of_electrons=2, atom_number_of_protons=10), 'key1': Atom(atom_number_of_electrons=2, atom_number_of_protons=12)}, {'key0': Orbit(orbit_number_of_electrons=10), 'key1': Orbit(orbit_number_of_electrons=12)}]), # dictionary of array cases ({'key0': [{"OrbitNumberOfElectrons": 10}, {"OrbitNumberOfElectrons": 12}], 'key1': [{"OrbitNumberOfElectrons": 14}, {"OrbitNumberOfElectrons": 16}]}, - [LeafType(Orbit, UnionTypeContext().dict(True).array(True)), - LeafType(Atom, UnionTypeContext().dict(True).array(True))], - UnionTypeContext(), True, {'key0': [Orbit(10), Orbit(12)], 'key1': [Orbit(14), Orbit(16)]}), + [LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True))], + UnionTypeContext(), True, {'key0': [Orbit(orbit_number_of_electrons=10), Orbit(orbit_number_of_electrons=12)], 'key1': [Orbit(orbit_number_of_electrons=14), Orbit(orbit_number_of_electrons=16)]}), ({'key0': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}], 'key1': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 14}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 16}]}, - [LeafType(Atom, UnionTypeContext().dict(True).array(True)), - LeafType(Orbit, UnionTypeContext().dict(True).array(True))], - UnionTypeContext(), True, {'key0': [Atom(2, 10), Atom(2, 12)], 'key1': [Atom(2, 14), Atom(2, 16)]}), + [LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True))], + UnionTypeContext(), True, {'key0': [Atom(atom_number_of_electrons=2, atom_number_of_protons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)], 'key1': [Atom(atom_number_of_electrons=2, atom_number_of_protons=14), Atom(atom_number_of_electrons=2, atom_number_of_protons=16)]}), ({'key0': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"OrbitNumberOfElectrons": 10}], 'key1': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}, {"OrbitNumberOfElectrons": 12}]}, - [LeafType(Atom, UnionTypeContext().dict(True).array(True)), - LeafType(Orbit, UnionTypeContext().dict(True).array(True))], + [LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), ({'key0': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}], 'key1': [{"OrbitNumberOfElectrons": 10}, {"OrbitNumberOfElectrons": 12}]}, - [LeafType(Atom, UnionTypeContext().dict(True).array(True)), - LeafType(Orbit, UnionTypeContext().dict(True).array(True))], + [LeafType(Atom, UnionTypeContext(is_dict=True, is_array=True)), + LeafType(Orbit, UnionTypeContext(is_dict=True, is_array=True))], UnionTypeContext(), False, None), # Outer dictionary of array cases ({'key0': [{"OrbitNumberOfElectrons": 10}, {"OrbitNumberOfElectrons": 12}], 'key1': [{"OrbitNumberOfElectrons": 14}, {"OrbitNumberOfElectrons": 16}]}, [LeafType(Orbit, UnionTypeContext()), LeafType(Atom, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, - {'key0': [Orbit(10), Orbit(12)], 'key1': [Orbit(14), Orbit(16)]}), + UnionTypeContext(is_dict=True, is_array=True), True, + {'key0': [Orbit(orbit_number_of_electrons=10), Orbit(orbit_number_of_electrons=12)], 'key1': [Orbit(orbit_number_of_electrons=14), Orbit(orbit_number_of_electrons=16)]}), ({'key0': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}], 'key1': [{"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}]}, [LeafType(Orbit, UnionTypeContext()), LeafType(Atom, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, - {'key0': [Atom(2, 10), Atom(2, 12)], 'key1': [Atom(2, 10), Atom(2, 12)]}), + UnionTypeContext(is_dict=True, is_array=True), True, + {'key0': [Atom(atom_number_of_electrons=2, atom_number_of_protons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)], 'key1': [Atom(atom_number_of_electrons=2, atom_number_of_protons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)]}), ({'key0': [{"OrbitNumberOfElectrons": 10}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 10}], 'key1': [{"OrbitNumberOfElectrons": 12}, {"AtomNumberOfElectrons": 2, "AtomNumberOfProtons": 12}]}, [LeafType(Atom, UnionTypeContext()), LeafType(Orbit, UnionTypeContext())], - UnionTypeContext().dict(True).array(True), True, - {'key0': [Orbit(10), Atom(2, 10)], 'key1': [Orbit(12), Atom(2, 12)]}), + UnionTypeContext(is_dict=True, is_array=True), True, + {'key0': [Orbit(orbit_number_of_electrons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=10)], 'key1': [Orbit(orbit_number_of_electrons=12), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)]}), ]) def test_one_of_custom_type(self, input_value, input_types, input_context, expected_is_valid_output, expected_deserialized_value_output): @@ -662,54 +661,54 @@ def test_one_of_custom_type(self, input_value, input_types, input_context, expec @pytest.mark.parametrize('input_value, input_types, input_context, expected_output', [ # Simple Cases - ('{"id": 123, "weight": 5, "type": "lion", "kind": "hunter"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + ('{"id": "123", "weight": 5, "type": "lion", "kind": "hunter"}', + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), - ('{"id": 123, "weight": 5, "type": "lion123", "kind": "hunter"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + ('{"id": "123", "weight": 5, "type": "lion123", "kind": "hunter"}', + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer123", "kind": "hunted"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), - ('{"id": 123, "weight": 5, "type": "lion", "kind": "hunter123"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + ('{"id": "123", "weight": 5, "type": "lion", "kind": "hunter123"}', + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunted123"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), - ('{"id": 123, "weight": 5, "type": "lion", "kind": "hunter"}', - [LeafType(Deer, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(Rabbit, UnionTypeContext().discriminator('type').discriminator_value('lion'))], + ('{"id": "123", "weight": 5, "type": "lion", "kind": "hunter"}', + [LeafType(Deer, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(Rabbit, UnionTypeContext(discriminator='type', discriminator_value='lion'))], UnionTypeContext(), False), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunted"}', - [LeafType(Lion, UnionTypeContext().discriminator('type').discriminator_value('deer')), - LeafType(Rabbit, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(Lion, UnionTypeContext(discriminator='type', discriminator_value='deer')), + LeafType(Rabbit, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), False), - ('{"id": 123, "weight": 5, "type": "lion", "kind": "hunter"}', - [LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + ('{"id": "123", "weight": 5, "type": "lion", "kind": "hunter"}', + [LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', - [LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), True), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', - [LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('deer')), - LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('deer'))], + [LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='deer')), + LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='deer'))], UnionTypeContext(), False), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', - [LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('lion')), - LeafType(dict, UnionTypeContext().discriminator('type').discriminator_value('lion'))], + [LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='lion')), + LeafType(dict, UnionTypeContext(discriminator='type', discriminator_value='lion'))], UnionTypeContext(), False), ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', [LeafType(dict), LeafType(dict)], UnionTypeContext(), False), @@ -727,7 +726,7 @@ def test_one_of_with_discriminator_custom_type(self, input_value, input_types, i @pytest.mark.parametrize('input_value, input_types, input_context, expected_is_valid_output, ' 'expected_deserialized_value_output', [ # Simple Cases - ('Monday',[LeafType(Days, UnionTypeContext()), LeafType(Months, UnionTypeContext())], + ('Monday', [LeafType(Days, UnionTypeContext()), LeafType(Months, UnionTypeContext())], UnionTypeContext(), True, 'Monday'), (1, [LeafType(Days, UnionTypeContext()), LeafType(Months, UnionTypeContext())], UnionTypeContext(), True, 1), @@ -737,57 +736,57 @@ def test_one_of_with_discriminator_custom_type(self, input_value, input_types, i UnionTypeContext(), False, None), # Outer Array - (['Monday', 'Tuesday'], [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), True, + (['Monday', 'Tuesday'], [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), True, ['Monday', 'Tuesday']), - ([1, 2], [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), True, + ([1, 2], [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), True, [1, 2]), - ([1, 'Monday'], [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), True, [1, 'Monday']), - (2, [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), False, None), - ('Monday', [LeafType(Days), LeafType(Months)], UnionTypeContext().array(True), False, None), - ([['January', 'February']], [LeafType(int), LeafType(str)], UnionTypeContext().array(True), False, None), + ([1, 'Monday'], [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), True, [1, 'Monday']), + (2, [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), False, None), + ('Monday', [LeafType(Days), LeafType(Months)], UnionTypeContext(is_array=True), False, None), + ([['January', 'February']], [LeafType(int), LeafType(str)], UnionTypeContext(is_array=True), False, None), # Inner Array Cases - (['Monday', 'Tuesday'], [LeafType(Days, UnionTypeContext().array(True)), - LeafType(Months, UnionTypeContext().array(True))], + (['Monday', 'Tuesday'], [LeafType(Days, UnionTypeContext(is_array=True)), + LeafType(Months, UnionTypeContext(is_array=True))], UnionTypeContext(), True, ['Monday', 'Tuesday']), - ([1, 2], [LeafType(Days, UnionTypeContext().array(True)), LeafType(Months, UnionTypeContext().array(True))], + ([1, 2], [LeafType(Days, UnionTypeContext(is_array=True)), LeafType(Months, UnionTypeContext(is_array=True))], UnionTypeContext(), True, [1, 2]), ([1, 'Monday'], - [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days, UnionTypeContext().array(True))], + [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days, UnionTypeContext(is_array=True))], UnionTypeContext(), False, None), # Partial Array Case - ('Monday', [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], + ('Monday', [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], UnionTypeContext(), True, 'Monday'), - ([1, 2], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], + ([1, 2], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], UnionTypeContext(), True, [1, 2]), - ([1, 'Monday'], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], + ([1, 'Monday'], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], UnionTypeContext(), False, None), - (1, [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], + (1, [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], UnionTypeContext(), False, None), # Array of Partial Arrays Cases - (['Monday', 'Tuesday'], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), True, ['Monday', 'Tuesday']), - ([[1, 2]], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), True, [[1, 2]]), - ([[1, 2], 'Monday'], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), True, [[1, 2], 'Monday']), - ([[1, 'Monday']], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), False, None), - ([1], [LeafType(Months, UnionTypeContext().array(True)), LeafType(Days)], - UnionTypeContext().array(True), False, None), + (['Monday', 'Tuesday'], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), True, ['Monday', 'Tuesday']), + ([[1, 2]], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), True, [[1, 2]]), + ([[1, 2], 'Monday'], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), True, [[1, 2], 'Monday']), + ([[1, 'Monday']], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), False, None), + ([1], [LeafType(Months, UnionTypeContext(is_array=True)), LeafType(Days)], + UnionTypeContext(is_array=True), False, None), # Array of Arrays Cases - ([['Monday', 'Tuesday'], ['Wednesday', 'Thursday']], [LeafType(Days, UnionTypeContext().array(True)), - LeafType(Months, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [['Monday', 'Tuesday'], ['Wednesday', 'Thursday']]), - ([[1, 2], [3, 4]], [LeafType(Months, UnionTypeContext().array(True)), - LeafType(Days, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[1, 2], [3, 4]]), - ([[1, 2], ['Monday', 'Tuesday']], [LeafType(Months, UnionTypeContext().array(True)), - LeafType(Days, UnionTypeContext().array(True))], - UnionTypeContext().array(True), True, [[1, 2], ['Monday', 'Tuesday']]), + ([['Monday', 'Tuesday'], ['Wednesday', 'Thursday']], [LeafType(Days, UnionTypeContext(is_array=True)), + LeafType(Months, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [['Monday', 'Tuesday'], ['Wednesday', 'Thursday']]), + ([[1, 2], [3, 4]], [LeafType(Months, UnionTypeContext(is_array=True)), + LeafType(Days, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[1, 2], [3, 4]]), + ([[1, 2], ['Monday', 'Tuesday']], [LeafType(Months, UnionTypeContext(is_array=True)), + LeafType(Days, UnionTypeContext(is_array=True))], + UnionTypeContext(is_array=True), True, [[1, 2], ['Monday', 'Tuesday']]), ]) def test_one_of_enum_type(self, input_value, input_types, input_context, expected_is_valid_output, expected_deserialized_value_output): @@ -812,11 +811,7 @@ def test_one_of_enum_type(self, input_value, input_types, input_context, expecte UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)), (100.5, [LeafType(int), OneOf([LeafType(bool), LeafType(str)])], UnionTypeContext(), '{} \nActual Value: 100.5\nExpected Type: One Of int, bool, str.'.format( - UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)), - ([[100, 200], None], [OneOf([LeafType(str, UnionTypeContext()), LeafType(bool, UnionTypeContext())], - UnionTypeContext().array(True)), LeafType(int, UnionTypeContext().array(True))], - UnionTypeContext().array(True), '{} \nActual Value: [[100, 200], None]\nExpected Type: One Of str, bool, int.'.format( - UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)), + UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)) ]) def test_one_of_validation_errors(self, input_value, input_types, input_context, expected_validation_message): with pytest.raises(OneOfValidationException) as validation_error: diff --git a/tests/apimatic_core/utility_tests/test_api_helper.py b/tests/apimatic_core/utility_tests/test_api_helper.py index e787022..1154082 100644 --- a/tests/apimatic_core/utility_tests/test_api_helper.py +++ b/tests/apimatic_core/utility_tests/test_api_helper.py @@ -1,24 +1,21 @@ from datetime import datetime, date -import jsonpickle import pytest +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import ValidationError + +from apimatic_core.types.array_serialization_format import SerializationFormats from tests.apimatic_core.mocks.models.lion import Lion -from tests.apimatic_core.mocks.models.atom import Atom from apimatic_core.types.union_types.leaf_type import LeafType from apimatic_core.types.union_types.one_of import OneOf -from apimatic_core.types.union_types.union_type_context import UnionTypeContext from dateutil.tz import tzutc -from apimatic_core.types.array_serialization_format import SerializationFormats from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.types.file_wrapper import FileWrapper from apimatic_core.utilities.api_helper import ApiHelper from tests.apimatic_core.base import Base -from tests.apimatic_core.mocks.models.days import Days - -from tests.apimatic_core.mocks.models.grand_parent_class_model import ChildClassModel from tests.apimatic_core.mocks.models.model_with_additional_properties import \ ModelWithAdditionalPropertiesOfPrimitiveType, \ ModelWithAdditionalPropertiesOfPrimitiveArrayType, ModelWithAdditionalPropertiesOfPrimitiveDictType, \ @@ -33,13 +30,14 @@ class TestApiHelper(Base): @pytest.mark.parametrize('input_value, expected_value', [ (None, None), (Base.wrapped_parameters(), '{{"bodyScalar": true, "bodyNonScalar": {{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' + '"birthday": "1994-02-13", "birthtime": "{0}", "name": "Bob", "uid": "1234567", ' + '"personType": "Empl", "department": "IT", "dependents": ' + '[{{"address": "street abc", "age": 12, "birthday": ' '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}}}'.format( + '"7654321", "personType": "Per", "key1": "value1", "key2": "value2"}}], ' + '"hiredAt": "{1}", "joiningDay": "Monday", ' + '"salary": 30000, "workingDays": ["Monday", ' + '"Tuesday"], "key1": "value1", "key2": "value2"}}}}'.format( Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), ]) @@ -50,105 +48,18 @@ def test_json_serialize_wrapped_params(self, input_value, expected_value): @pytest.mark.parametrize('input_value, expected_value', [ (None, None), ([Base.employee_model(), Base.employee_model()], - '[{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}, ' - '{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}]'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + '[{0}, {0}]'.format(Base.employee_model_str())), ([[Base.employee_model(), Base.employee_model()], [Base.employee_model(), Base.employee_model()]], - '[[{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}, ' - '{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}], [{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}, ' - '{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}]]'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + '[[{0}, {0}], [{0}, {0}]]'.format(Base.employee_model_str())), ({'key0': [Base.employee_model(), Base.employee_model()], 'key1': [Base.employee_model(), Base.employee_model()]}, - '{{"key0": [{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}, ' - '{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}], "key1": [{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}, ' - '{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}]}}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + '{{"key0": [{0}, {0}], "key1": [{0}, {0}]}}'.format(Base.employee_model_str())), ([1, 2, 3], '[1, 2, 3]'), ({'key0': 1, 'key1': 'abc'}, '{"key0": 1, "key1": "abc"}'), ([[1, 2, 3], ['abc', 'def']], '[[1, 2, 3], ["abc", "def"]]'), ([{'key0': [1, 2, 3]}, {'key1': ['abc', 'def']}], '[{"key0": [1, 2, 3]}, {"key1": ["abc", "def"]}]'), ({'key0': [1, 2, 3], 'key1': ['abc', 'def']}, '{"key0": [1, 2, 3], "key1": ["abc", "def"]}'), - (Base.employee_model(), - '{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + (Base.employee_model(), Base.employee_model_str(beautify_with_spaces=False)), (1, '1'), ('1', '1') ]) @@ -156,78 +67,75 @@ def test_json_serialize(self, input_value, expected_value): serialized_value = ApiHelper.json_serialize(input_value) assert serialized_value == expected_value - @pytest.mark.parametrize('input_value, expected_validation_message', [ - (ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( - 'test@gmail.com', {'email': 10.55}), - "An additional property key, 'email' conflicts with one of the model's properties") - ]) - def test_json_serialize_with_exception(self, input_value, expected_validation_message): - with pytest.raises(ValueError) as conflictingPropertyError: - ApiHelper.json_serialize(input_value) - - assert conflictingPropertyError.value.args[0] == expected_validation_message - @pytest.mark.parametrize('input_json_value, unboxing_function, as_dict, expected_value', [ (None, None, False, None), ('true', None, False, 'true'), ('', None, False, None), (' ', None, False, None), - (ApiHelper.json_serialize(Base.employee_model()), Employee.from_dictionary, False, + (ApiHelper.json_serialize(Base.employee_model()), Employee.model_validate, False, ApiHelper.json_serialize(Base.employee_model())), (ApiHelper.json_serialize([Base.employee_model(), Base.employee_model()]), - Employee.from_dictionary, False, + Employee.model_validate, False, ApiHelper.json_serialize([Base.employee_model(), Base.employee_model()])), (ApiHelper.json_serialize({'key1': Base.employee_model(), 'key2': Base.employee_model()}), - Employee.from_dictionary, True, - '{{"key1": {{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": "{0}", ' - '"department": "IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": "1994-02-13", ' - '"birthtime": "{0}", "name": "John", "uid": 7654321, "personType": "Per", "key1": "value1", ' - '"key2": "value2"}}], "hiredAt": "{1}", "joiningDay": "Monday", "name": "Bob", "salary": 30000, ' - '"uid": 1234567, "workingDays": ["Monday", "Tuesday"], "personType": "Empl"}}, "key2": ' - '{{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": "{0}", "department": "IT",' - ' "dependents": [{{"address": "street abc", "age": 12, "birthday": "1994-02-13", "birthtime": "{0}", ' - '"name": "John", "uid": 7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], "hiredAt": "{1}",' - ' "joiningDay": "Monday", "name": "Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday",' - ' "Tuesday"], "personType": "Empl"}}}}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), - ('{"email": "test", "prop1": 1, "prop2": 2, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveType.from_dictionary, False, - '{"email": "test", "prop1": 1, "prop2": 2}'), - ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3], "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveArrayType.from_dictionary, False, - '{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3]}'), - ('{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveDictType.from_dictionary, False, - '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}}'), - ('{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfModelType.from_dictionary, + Employee.model_validate, True, '{{"key1": {0}, "key2": {0}}}'.format(Base.employee_model_str())), + ('{"email": "test", "prop1": 1, "prop2": 2}', + ModelWithAdditionalPropertiesOfPrimitiveType.model_validate, False, + '{"email":"test","prop1":1,"prop2":2}'), + ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3]}', + ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate, False, + '{"email":"test","prop1":[1,2,3],"prop2":[1,2,3]}'), + ('{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}}', + ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate, False, + '{"email":"test","prop1":{"inner_prop1":1,"inner_prop2":2},"prop2":{"inner_prop1":1,"inner_prop2":2}}'), + ('{"email": "test", "prop1": {"id": "1", "weight": 50, "type": "Lion"}}', + ModelWithAdditionalPropertiesOfModelType.model_validate, False, - '{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}}'), - ('{"email": "test", "prop": [{"id": 1, "weight": 50, "type": "Lion"}, {"id": 2, "weight": 100, "type": "Lion"}]}', - ModelWithAdditionalPropertiesOfModelArrayType.from_dictionary, + '{"email":"test","prop1":{"id":"1","weight":50,"type":"Lion"}}'), + ('{"email": "test", "prop": [{"id": "1", "weight": 50, "type": "Lion"}, {"id": "2", "weight": 100, "type": "Lion"}]}', + ModelWithAdditionalPropertiesOfModelArrayType.model_validate, False, - '{"email": "test", "prop": [{"id": 1, "weight": 50, "type": "Lion"}, {"id": 2, "weight": 100, "type": "Lion"}]}'), - ('{"email": "test", "prop": {"inner prop 1": {"id": 1, "weight": 50, "type": "Lion"}, "inner prop 2": {"id": 2, "weight": 100, "type": "Lion"}}}', - ModelWithAdditionalPropertiesOfModelDictType.from_dictionary, + '{"email":"test","prop":[{"id":"1","weight":50,"type":"Lion"},{"id":"2","weight":100,"type":"Lion"}]}'), + ('{"email": "test", "prop": {"inner prop 1": {"id": "1", "weight": 50, "type": "Lion"}, "inner prop 2": {"id": "2", "weight": 100, "type": "Lion"}}}', + ModelWithAdditionalPropertiesOfModelDictType.model_validate, False, - '{"email": "test", "prop": {"inner prop 1": {"id": 1, "weight": 50, "type": "Lion"}, "inner prop 2": {"id": 2, "weight": 100, "type": "Lion"}}}'), + '{"email":"test","prop":{"inner prop 1":{"id":"1","weight":50,"type":"Lion"},"inner prop 2":{"id":"2","weight":100,"type":"Lion"}}}'), ('{"email": "test", "prop": true}', - ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.from_dictionary, + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, False, - '{"email": "test", "prop": true}'), + '{"email":"test","prop":true}'), ('{"email": "test", "prop": 100.65}', - ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.from_dictionary, + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, False, - '{"email": "test", "prop": 100.65}'), + '{"email":"test","prop":100.65}'), ('{"email": "test", "prop": "100.65"}', - ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.from_dictionary, + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, False, - '{"email": "test"}') + '{"email":"test","prop":100.65}') ]) def test_json_deserialize(self, input_json_value, unboxing_function, as_dict, expected_value): deserialized_value = ApiHelper.json_deserialize(input_json_value, unboxing_function, as_dict) assert ApiHelper.json_serialize(deserialized_value) == expected_value + @pytest.mark.parametrize('input_json_value, unboxing_function, expected_value', [ + ('{"email": "test", "prop1": 1, "prop2": 2, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveType.model_validate, + '{"email": "test", "prop1": 1, "prop2": 2}'), + ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3], "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate, + '{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3]}'), + ( + '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate, + '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}}'), + ('{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfModelType.model_validate, + '{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}}') + ]) + def test_json_deserialize(self, input_json_value, unboxing_function, expected_value): + with pytest.raises(ValidationError): + ApiHelper.json_deserialize(input_json_value, unboxing_function) + @pytest.mark.parametrize('input_url, input_file_value, expected_value', [ ('C:\\PYTHON_GENERIC_LIB\\Tester\\models\\test_file.py', "test_file", 'C:\\PYTHON_GENERIC_LIB\\Tester\\schemas\\TestFile.json'), @@ -322,6 +230,9 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ '&query_param[birthday]=1994-02-13' '&query_param[birthtime]={}'.format(quote( Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[name]=Bob' + '&query_param[uid]=1234567' + '&query_param[personType]=Empl' '&query_param[department]=IT' '&query_param[dependents][0][address]=street%20abc' '&query_param[dependents][0][age]=12' @@ -336,12 +247,11 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ '&query_param[hiredAt]={}'.format(quote( Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[joiningDay]=Monday' - '&query_param[name]=Bob' '&query_param[salary]=30000' - '&query_param[uid]=1234567' '&query_param[workingDays][0]=Monday' '&query_param[workingDays][1]=Tuesday' - '&query_param[personType]=Empl', SerializationFormats.INDEXED) + '&query_param[key1]=value1' + '&query_param[key2]=value2', SerializationFormats.INDEXED) ]) def test_append_url_with_query_parameters(self, input_query_param_value, expected_query_param_value, array_serialization_format): @@ -391,124 +301,6 @@ def test_clean_url_value_error(self, input_url): def test_clean_url(self, input_url, expected_url): assert ApiHelper.clean_url(input_url) == expected_url - @pytest.mark.parametrize('obj, expected_value', [ - (Base.employee_model(), - '{{"address": "street abc", "age": 27, ' - '"birthday": "1994-02-13", "birthtime": "{0}", "department": ' - '"IT", "dependents": [{{"address": "street abc", "age": 12, "birthday": ' - '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' - '7654321, "personType": "Per", "key1": "value1", "key2": "value2"}}], ' - '"hiredAt": "{1}", "joiningDay": "Monday", "name": ' - '"Bob", "salary": 30000, "uid": 1234567, "workingDays": ["Monday", ' - '"Tuesday"], "personType": "Empl"}}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), - (Base.get_complex_type(), - '{"innerComplexListType": [{"booleanType": true, "longType": 100003, "precisionType": 55.44, ' - '"stringListType": ["item1", "item2"], "stringType": "abc", "key0": "abc", "key1": 400}, ' - '{"booleanType": true, "longType": 100003, "precisionType": 55.44, "stringListType": ["item1", "item2"],' - ' "stringType": "abc", "key0": "abc", "key1": 400}], "innerComplexType": {"booleanType": true, ' - '"longType": 100003, "precisionType": 55.44, "stringListType": ["item1", "item2"], "stringType": "abc",' - ' "key0": "abc", "key1": 400}, "innerComplexListOfMapType": [{"key0": {"booleanType": true, ' - '"longType": 100003, "precisionType": 55.44, "stringListType": ["item1", "item2"], ' - '"stringType": "abc", "key0": "abc", "key1": 400}, "key1": {"booleanType": true, "longType": 100003, ' - '"precisionType": 55.44, "stringListType": ["item1", "item2"], "stringType": "abc", "key0": "abc", ' - '"key1": 400}}], "innerComplexMapOfListType": {"key0": [{"booleanType": true, "longType": 100003, ' - '"precisionType": 55.44, "stringListType": ["item1", "item2"], "stringType": "abc", "key0": "abc", ' - '"key1": 400}, {"booleanType": true, "longType": 100003, "precisionType": 55.44, "stringListType": ' - '["item1", "item2"], "stringType": "abc", "key0": "abc", "key1": 400}], "key2": [{"booleanType": true, ' - '"longType": 100003, "precisionType": 55.44, "stringListType": ["item1", "item2"], "stringType": "abc", ' - '"key0": "abc", "key1": 400}, {"booleanType": true, "longType": 100003, "precisionType": 55.44, ' - '"stringListType": ["item1", "item2"], "stringType": "abc", "key0": "abc", "key1": 400}]}, ' - '"innerComplexMapType": {"key0": {"booleanType": true, "longType": 100003, "precisionType": 55.44, ' - '"stringListType": ["item1", "item2"], "stringType": "abc", "key0": "abc", "key1": 400}, "key1": ' - '{"booleanType": true, "longType": 100003, "precisionType": 55.44, "stringListType": ["item1", "item2"], ' - '"stringType": "abc", "key0": "abc", "key1": 400}}, "prop1": [1, 2, 3], "prop2": {"key0": "abc", ' - '"key1": "def"}}'), - (ApiHelper.json_deserialize('{"Grand_Parent_Required_Nullable":{"key1": "value1", "key2": "value2"},' - '"Grand_Parent_Required":"not nullable and required","class":23,' - '"Parent_Optional_Nullable_With ' - '_Default_Value":"Has default value","Parent_Required_Nullable":nul' - 'l,"Parent_Required":"not nullable and required","Optional_Nullable' - '":"setted optionalNullable","Optional_Nullable_With_Default_Value"' - ':"With default value","Required_Nullable":null,"Required":"not nul' - 'lable and required","Optional":"not nullable and optional","Child_' - 'Class_Array":null}', ChildClassModel.from_dictionary), - '{"Required_Nullable": null, "Required": "not nullable and required", ' - '"Parent_Required_Nullable": null, "Parent_Required": "not nullable and ' - 'required", "Grand_Parent_Required_Nullable": {"key1": "value1", "key2": ' - '"value2"}, "Grand_Parent_Required": "not nullable and required", ' - '"Optional_Nullable": "setted optionalNullable", ' - '"Optional_Nullable_With_Default_Value": "With default value", "Optional": ' - '"not nullable and optional", "Child_Class_Array": null, "class": 23, ' - '"Parent_Optional_Nullable_With_Default_Value": "Has default value"}' - ), - (Base.employee_model_additional_dictionary(), - '{{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": ' - '"{0}", "department": "IT", "dependents": [{{"address": ' - '"street abc", "age": 12, "birthday": "1994-02-13", "birthtime": ' - '"{0}", "name": "John", "uid": 7654321, "personType": "Per", ' - '"key1": {{"inner_key1": "inner_val1", "inner_key2": "inner_val2"}}, "key2": ' - '["value2", "value3"]}}], "hiredAt": "{1}", ' - '"joiningDay": "Monday", "name": "Bob", "salary": 30000, "uid": 1234567, ' - '"workingDays": ["Monday", "Tuesday"], "personType": "Empl"}}'.format( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), - (Base.get_union_type_scalar_model(), - '{"anyOfRequired": 1.5, "oneOfReqNullable": "abc", "oneOfOptional": 200, "anyOfOptNullable": true}'), - - ]) - def test_to_dictionary(self, obj, expected_value): - assert jsonpickle.encode(ApiHelper.to_dictionary(obj), False) == expected_value - - @pytest.mark.parametrize('obj, should_ignore_null_values, expected_value', [ - (Base.employee_model_additional_dictionary(), True, - '{{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": ' - '"{0}", "department": "IT", "dependents": [{{"address": ' - '"street abc", "age": 12, "birthday": "1994-02-13", "birthtime": ' - '"{0}", "name": "John", "uid": 7654321, "personType": "Per", ' - '"key1": {{"inner_key1": "inner_val1", "inner_key2": "inner_val2"}}, "key2": ' - '["value2", "value3"]}}], "hiredAt": "{1}", ' - '"joiningDay": "Monday", "name": "Bob", "salary": 30000, "uid": 1234567, ' - '"workingDays": ["Monday", "Tuesday"], "personType": "Empl"}}'.format( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), - (ApiHelper.json_deserialize('{"Grand_Parent_Required_Nullable":{"key1": "value1", "key2": "value2"},' - '"Grand_Parent_Required":"not nullable and required","class":23,' - '"Parent_Optional_Nullable_With ' - '_Default_Value":"Has default value","Parent_Required_Nullable":nul' - 'l,"Parent_Required":"not nullable and required","Optional_Nullable' - '":"setted optionalNullable","Optional_Nullable_With_Default_Value"' - ':"With default value","Required_Nullable":null,"Required":"not nul' - 'lable and required","Optional":"not nullable and optional","Child_' - 'Class_Array":null}', ChildClassModel.from_dictionary), True, - '{"Required_Nullable": null, "Required": "not nullable and required", ' - '"Parent_Required_Nullable": null, "Parent_Required": "not nullable and ' - 'required", "Grand_Parent_Required_Nullable": {"key1": "value1", "key2": ' - '"value2"}, "Grand_Parent_Required": "not nullable and required", ' - '"Optional_Nullable": "setted optionalNullable", ' - '"Optional_Nullable_With_Default_Value": "With default value", "Optional": ' - '"not nullable and optional", "Child_Class_Array": null, "class": 23, ' - '"Parent_Optional_Nullable_With_Default_Value": "Has default value"}' - ), - ]) - def test_to_dictionary_for_object(self, obj, should_ignore_null_values, expected_value): - assert jsonpickle.encode(ApiHelper.to_dictionary(obj), False) == expected_value - - @pytest.mark.parametrize('obj, name', [ - (ApiHelper.json_deserialize('{"Grand_Parent_Required_Nullable":{"key1": "value1", "key2": "value2"},' - '"Grand_Parent_Required":"not nullable and required","class":23,' - '"Parent_Optional_Nullable_With ' - '_Default_Value":"Has default value","Parent_Required_Nullable":nul' - 'l,"Parent_Required":"not nullable and required","Optional_Nullable' - '":"setted optionalNullable","Optional_Nullable_With_Default_Value"' - ':"With default value","Required_Nullable":null,"Optional":"not nullable and ' - 'optional","Child_Class_Array":null}', ChildClassModel.from_dictionary), - 'required'), - ]) - def test_to_dictionary_value_error(self, obj, name): - with pytest.raises(ValueError, match=f"The value for {name} can not be None for {obj}"): - ApiHelper.to_dictionary(obj) - @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ (None, [], SerializationFormats.INDEXED), ('string', [('form_param', 'string')], SerializationFormats.INDEXED), @@ -538,39 +330,46 @@ def test_to_dictionary_value_error(self, obj, name): SerializationFormats.INDEXED), (Base.employee_model(), [('form_param[address]', 'street abc'), ('form_param[age]', 27), ('form_param[birthday]', '1994-02-13'), - ('form_param[birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), False)), + ('form_param[birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[name]', 'Bob'), ('form_param[uid]', '1234567'), ('form_param[personType]', 'Empl'), ('form_param[department]', 'IT'), ('form_param[dependents][0][address]', 'street abc'), ('form_param[dependents][0][age]', 12), ('form_param[dependents][0][birthday]', '1994-02-13'), - ('form_param[dependents][0][birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), False)), - ('form_param[dependents][0][name]', 'John'), ('form_param[dependents][0][uid]', 7654321), + ('form_param[dependents][0][birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[dependents][0][name]', 'John'), ('form_param[dependents][0][uid]', '7654321'), ('form_param[dependents][0][personType]', 'Per'), ('form_param[dependents][0][key1]', 'value1'), ('form_param[dependents][0][key2]', 'value2'), - ('form_param[hiredAt]', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15), False)), - ('form_param[joiningDay]', 'Monday'), ('form_param[name]', 'Bob'), ('form_param[salary]', 30000), - ('form_param[uid]', 1234567), ('form_param[workingDays][0]', 'Monday'), - ('form_param[workingDays][1]', 'Tuesday'), ('form_param[personType]', 'Empl')], SerializationFormats.INDEXED), - (ModelWithAdditionalPropertiesOfPrimitiveType( - 'test@gmail.com', {'prop': 20}), + ('form_param[hiredAt]', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[joiningDay]', 'Monday'), ('form_param[salary]', 30000), ('form_param[workingDays][0]', 'Monday'), + ('form_param[workingDays][1]', 'Tuesday'), ('form_param[key1]', 'value1'), + ('form_param[key2]', 'value2'),], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveType(email='test@gmail.com', additional_properties={'prop': 20}), [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 20)], SerializationFormats.INDEXED), (ModelWithAdditionalPropertiesOfPrimitiveArrayType( - 'test@gmail.com', {'prop': [20, 30]}), + email='test@gmail.com', additional_properties={'prop': [20, 30]}), [('form_param[email]', 'test@gmail.com'), ('form_param[prop][0]', 20), ('form_param[prop][1]', 30)], SerializationFormats.INDEXED), (ModelWithAdditionalPropertiesOfPrimitiveDictType( - 'test@gmail.com', {'prop': {'inner prop 1': 20, 'inner prop 2': 30}}), + email='test@gmail.com', additional_properties={'prop': {'inner prop 1': 20, 'inner prop 2': 30}}), [('form_param[email]', 'test@gmail.com'), ('form_param[prop][inner prop 1]', 20), ('form_param[prop][inner prop 2]', 30)], SerializationFormats.INDEXED), (ModelWithAdditionalPropertiesOfModelType( - 'test@gmail.com',{'prop': Lion('leo', 5, 'Lion')}), + email='test@gmail.com', additional_properties={'prop': Lion(id='leo', weight=5, mtype='Lion')}), [('form_param[email]', 'test@gmail.com'), ('form_param[prop][id]', 'leo'), ('form_param[prop][weight]', 5), ('form_param[prop][type]', 'Lion')], SerializationFormats.INDEXED), (ModelWithAdditionalPropertiesOfModelArrayType( - 'test@gmail.com', {'prop': [Lion('leo 1', 5, 'Lion'), Lion('leo 2', 10, 'Lion')]}), + email='test@gmail.com', + additional_properties={'prop': [Lion(id='leo 1', weight=5, mtype='Lion'), Lion(id='leo 2', weight=10, mtype='Lion')]}), [('form_param[email]', 'test@gmail.com'), ('form_param[prop][0][id]', 'leo 1'), ('form_param[prop][0][weight]', 5), ('form_param[prop][0][type]', 'Lion'), ('form_param[prop][1][id]', 'leo 2'), ('form_param[prop][1][weight]', 10), ('form_param[prop][1][type]', 'Lion')], SerializationFormats.INDEXED), (ModelWithAdditionalPropertiesOfModelDictType( - 'test@gmail.com', {'prop': {'leo 1': Lion('leo 1', 5, 'Lion'), 'leo 2': Lion('leo 2', 10, 'Lion')}}), + email='test@gmail.com', + additional_properties={ + 'prop': { + 'leo 1': Lion(id='leo 1', weight=5, mtype='Lion'), + 'leo 2': Lion(id='leo 2', weight=10, mtype='Lion') + } + }), [('form_param[email]', 'test@gmail.com'), ('form_param[prop][leo 1][id]', 'leo 1'), ('form_param[prop][leo 1][weight]', 5), ('form_param[prop][leo 1][type]', 'Lion'), ('form_param[prop][leo 2][id]', 'leo 2'), @@ -578,7 +377,7 @@ def test_to_dictionary_value_error(self, obj, name): ('form_param[prop][leo 2][type]', 'Lion')], SerializationFormats.INDEXED), (ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( - 'test@gmail.com', {'prop': 10.55}), + email='test@gmail.com', additional_properties={'prop': 10.55}), [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 10.55)], SerializationFormats.INDEXED) ]) @@ -594,16 +393,13 @@ def test_form_params(self, input_form_param_value, expected_form_param_value, ar else: assert item == expected_form_param_value[index] - @pytest.mark.parametrize('input_form_param_value, expected_validation_message', [ - (ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( - 'test@gmail.com', {'email': 10.55}), - "An additional property key, 'email' conflicts with one of the model's properties") - ]) - def test_form_params_with_exception(self, input_form_param_value, expected_validation_message): + def test_conflicting_additional_property(self): with pytest.raises(ValueError) as conflictingPropertyError: - ApiHelper.form_encode(input_form_param_value, 'form_param') + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( + email='test@gmail.com', additional_properties={'email': 10.55}) - assert conflictingPropertyError.value.args[0] == expected_validation_message + assert ("Invalid additional properties: {'email'}. These names conflict with existing model properties." + in str(conflictingPropertyError.value)) @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ @@ -620,17 +416,6 @@ def test_form_encode_parameters(self, input_form_param_value, expected_form_para assert ApiHelper.form_encode_parameters(input_form_param_value, array_serialization_format) == \ expected_form_param_value - @pytest.mark.parametrize('input_function, input_body, expected_value', [ - (ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime.from_value('1994-02-13T14:01:54.9571247Z').datetime, - '1994-02-13T14:01:54.957124+00:00'), - (ApiHelper.RFC3339DateTime, None, None) - ]) - def test_when_defined(self, input_function, input_body, expected_value): - if input_body is not None: - assert str(ApiHelper.when_defined(input_function, input_body)) == expected_value - else: - assert ApiHelper.when_defined(input_function, input_body) == expected_value - @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ (datetime(1994, 2, 13, 5, 30, 15), ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime), @@ -646,6 +431,13 @@ def test_apply_date_time_converter(self, input_value, input_converter, expected_ else: assert isinstance(ApiHelper.apply_datetime_converter(input_value, input_converter), expected_obj) + @pytest.mark.parametrize('input_function, input_body, expected_value', [ + (ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime.from_value('1994-02-13T14:01:54.9571247Z').datetime, + '1994-02-13T14:01:54.957124+00:00') + ]) + def test_when_defined(self, input_function, input_body, expected_value): + assert str(ApiHelper.when_defined(input_function, input_body)) == expected_value + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ ([datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.RFC3339DateTime, [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), @@ -730,7 +522,7 @@ def test_apply_date_time_converter_to_dict_of_dict(self, input_value, input_conv for index, actual_value in enumerate(actual_outer_value.values()): assert isinstance(actual_value, expected_obj[outer_key][index]) - @pytest.mark.parametrize('input_array,formatting, is_query, expected_array', [ + @pytest.mark.parametrize('input_array, formatting, is_query, expected_array', [ ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), str(date(1994, 2, 13))], SerializationFormats.INDEXED, False, [('test_array[0]', 1), ('test_array[1]', True), @@ -773,23 +565,23 @@ def test_apply_date_time_converter_to_dict_of_dict(self, input_value, input_conv ]) def test_serialize_array(self, input_array, formatting, is_query, expected_array): serialized_array = ApiHelper.serialize_array('test_array', input_array, formatting, is_query) - if hasattr(input_array[0], '_names'): + if ApiHelper.is_custom_type(input_array[0]): assert serialized_array[0][0] == 'test_array[0]' and serialized_array[1][0] == 'test_array[1]' and \ ApiHelper.json_serialize(serialized_array[0][1]) == expected_array[0] \ and ApiHelper.json_serialize(serialized_array[1][1]) == expected_array[1] else: assert ApiHelper.serialize_array('test_array', input_array, formatting, is_query) == expected_array - @pytest.mark.parametrize('input_array,formatting, is_query', [ + @pytest.mark.parametrize('input_array, formatting, is_query', [ ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - str(date(1994, 2, 13))], SerializationFormats.TSV, False), - ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - str(date(1994, 2, 13))], 'not a serialization format', True) + str(date(1994, 2, 13))], SerializationFormats.TSV, False) ]) def test_serialize_array_value_error(self, input_array, formatting, is_query): - with pytest.raises(ValueError, match="Invalid format provided."): + with pytest.raises(ValueError) as exception: ApiHelper.serialize_array('key', input_array, formatting, is_query) + assert 'Invalid format provided.' in str(exception.value) + @pytest.mark.parametrize('input_date, expected_date', [ (str(date(1994, 2, 13)), date(1994, 2, 13)), (ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))]), @@ -885,7 +677,7 @@ def test_dynamic_deserialize(self, input_value, output_value): assert ApiHelper.dynamic_deserialize(input_value) == output_value @pytest.mark.parametrize('input_placeholders, input_values, input_template, expected_message', [ - ({}, '400', 'Test template -- {$statusCode}', 'Test template -- {$statusCode}'), + (set(), '400', 'Test template -- {$statusCode}', 'Test template -- {$statusCode}'), ({'{$statusCode}'}, '400', 'Test template -- {$statusCode}', 'Test template -- 400'), ({'{$response.header.accept}'}, {'accept': 'application/json'}, 'Test template -- {$response.header.accept}', 'Test template -- application/json'), @@ -899,7 +691,7 @@ def test_resolve_template_placeholders(self, input_placeholders, input_values, i assert actual_message == expected_message @pytest.mark.parametrize('input_placeholders, input_value, input_template, expected_message', [ - ({}, + (set(), {"scalar": 123.2, "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], "arrayObjects":[{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, @@ -938,77 +730,16 @@ def test_resolve_template_placeholders(self, input_placeholders, input_values, i ]) def test_resolve_template_placeholders_using_json_pointer(self, input_placeholders, input_value, input_template, expected_message): - actual_message = ApiHelper.resolve_template_placeholders_using_json_pointer(input_placeholders, input_value, - input_template) + actual_message = ApiHelper.resolve_template_placeholders_using_json_pointer( + input_placeholders, input_value, input_template) assert actual_message == expected_message - @pytest.mark.parametrize( - 'input_value, input_callable, is_value_nullable, is_model_dict, is_inner_model_dict, expected_value', [ - (100, lambda value: isinstance(value, int), False, False, False, True), - ('100', lambda value: isinstance(value, str), False, False, False, True), - ("Sunday", lambda value: Days.validate(value), False, False, False, True), - (100.5, lambda value: isinstance(value, str), False, False, False, False), - ("Invalid", lambda value: Days.validate(value), False, False, False, False), - (None, lambda value: isinstance(value, str), False, False, False, False), - (None, lambda value: isinstance(value, str), True, False, False, True), - (None, None, False, False, False, False), - (None, None, True, False, False, True), - - ([100, 200], lambda value: isinstance(value, int), False, False, False, True), - (['100', '200'], lambda value: isinstance(value, str), False, False, False, True), - (["Sunday", "Monday"], lambda value: Days.validate(value), False, False, False, True), - ([100.5, 200], lambda value: isinstance(value, str), False, False, False, False), - (["Invalid1", "Invalid2"], lambda value: Days.validate(value), False, False, False, False), - ([None, None], lambda value: isinstance(value, str), False, False, False, False), - - ([[100, 200], [300, 400]], lambda value: isinstance(value, int), False, False, False, True), - ([['100', '200'], ['abc', 'def']], lambda value: isinstance(value, str), False, False, False, True), - ([["Sunday", "Monday"], ["Tuesday", "Friday"]], lambda value: Days.validate(value), False, False, False, - True), - ([[100.5, 200], [400, 500]], lambda value: isinstance(value, str), False, False, False, False), - ( - [["Invalid1", "Invalid2"], ["Sunday", "Invalid4"]], lambda value: Days.validate(value), False, False, False, - False), - ([[None, None], [None, None]], lambda value: isinstance(value, str), False, False, False, False), - - ({'key0': 100, 'key2': 200}, lambda value: isinstance(value, int), False, False, False, True), - ({'key0': 'abc', 'key2': 'def'}, lambda value: isinstance(value, str), False, False, False, True), - ({'key0': 'Sunday', 'key2': 'Tuesday'}, lambda value: Days.validate(value), False, False, False, True), - ({'key0': 100.5, 'key2': 200}, lambda value: isinstance(value, str), False, False, False, False), - ({'key0': "Invalid1", 'key2': "Invalid2"}, lambda value: Days.validate(value), False, False, False, False), - ({'key0': None, 'key2': None}, lambda value: isinstance(value, str), False, False, False, False), - - ({"AtomNumberOfElectrons": 3}, lambda value: Atom.validate(value), False, True, False, True), - ([{"AtomNumberOfElectrons": 3}, {"AtomNumberOfElectrons": 3}], - lambda value: Atom.validate(value), False, True, False, True), - ({"item": {"AtomNumberOfElectrons": 3}}, lambda value: Atom.validate(value), False, True, True, True), - ([{"item": {"AtomNumberOfElectrons": 3}}, {"item": {"AtomNumberOfElectrons": 3}}], - lambda value: Atom.validate(value), False, True, True, True), - ({"item": [{"AtomNumberOfElectrons": 3}, {"AtomNumberOfElectrons": 3}]}, - lambda value: Atom.validate(value), False, True, True, True), - - ({"InvalidAtomNumberOfElectrons": 3}, lambda value: Atom.validate(value), False, True, False, False), - ([{"InvalidAtomNumberOfElectrons": 3}, {"InvalidAtomNumberOfElectrons": 3}], - lambda value: Atom.validate(value), False, True, False, False), - ({"item": {"InvalidAtomNumberOfElectrons": 3}}, - lambda value: Atom.validate(value), False, True, True, False), - ([{"item": {"InvalidAtomNumberOfElectrons": 3}}, {"item": {"InvalidAtomNumberOfElectrons": 3}}], - lambda value: Atom.validate(value), False, True, True, False), - ({"item": [{"InvalidAtomNumberOfElectrons": 3}, {"AtomNumberOfElectrons": 3}]}, - lambda value: Atom.validate(value), False, True, True, False), - ]) - def test_is_valid_type(self, input_value, input_callable, is_value_nullable, is_model_dict, - is_inner_model_dict, expected_value): - actual_value = ApiHelper.is_valid_type(input_value, input_callable, is_value_nullable, is_model_dict, - is_inner_model_dict) - assert actual_value == expected_value - - @pytest.mark.parametrize('input_value, input_union_type, input_should_deserialize, expected_value', [ - (100, OneOf([LeafType(int), LeafType(str)]), False, 100), - ('[100, "200"]', OneOf([LeafType(int), LeafType(str)], UnionTypeContext.create(is_array=True)), True, + @pytest.mark.parametrize('input_union_type, input_value, input_should_deserialize, expected_value', [ + (OneOf([LeafType(int), LeafType(str)]), 100, False, 100), + (OneOf([LeafType(int), LeafType(str)], UnionTypeContext(is_array=True)), '[100, "200"]', True, [100, '200']), ]) - def test_union_type_deserialize(self, input_value, input_union_type, input_should_deserialize, expected_value): + def test_union_type_deserialize(self, input_union_type, input_value, input_should_deserialize, expected_value): actual_value = ApiHelper.deserialize_union_type(input_union_type, input_value, input_should_deserialize) assert actual_value == expected_value diff --git a/tests/apimatic_core/utility_tests/test_auth_helper.py b/tests/apimatic_core/utility_tests/test_auth_helper.py index 412c5fc..fc28c87 100644 --- a/tests/apimatic_core/utility_tests/test_auth_helper.py +++ b/tests/apimatic_core/utility_tests/test_auth_helper.py @@ -8,15 +8,6 @@ def test_base64_encoded_none_value(self): expected_base64_encoded_value = None assert actual_base64_encoded_value == expected_base64_encoded_value - def test_base64_encoded_provided_none_values(self): - actual_base64_encoded_value = AuthHelper.get_base64_encoded_value(None, None) - expected_base64_encoded_value = None - assert actual_base64_encoded_value == expected_base64_encoded_value - - actual_base64_encoded_value = AuthHelper.get_base64_encoded_value('test_username', None) - expected_base64_encoded_value = None - assert actual_base64_encoded_value == expected_base64_encoded_value - def test_base64_encoded_value(self): actual_base64_encoded_value = AuthHelper.get_base64_encoded_value('test_username', 'test_password') expected_base64_encoded_value = 'dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk' diff --git a/tests/apimatic_core/utility_tests/test_datetime_helper.py b/tests/apimatic_core/utility_tests/test_datetime_helper.py index 9514d54..51edd33 100644 --- a/tests/apimatic_core/utility_tests/test_datetime_helper.py +++ b/tests/apimatic_core/utility_tests/test_datetime_helper.py @@ -18,7 +18,7 @@ class TestDateTimeHelper: (1480809600, DateTimeFormat.RFC3339_DATE_TIME, False), ('1994-11-06T08:49:37', DateTimeFormat.UNIX_DATE_TIME, False), ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.UNIX_DATE_TIME, False), - (None, None, False) + ('Sun, 06 Nov 1994 03:49:37 GMT', None, False) ]) def test_is_valid_datetime(self, input_dt, input_datetime_format, expected_output): actual_output = DateTimeHelper.validate_datetime(input_dt, input_datetime_format) @@ -32,8 +32,7 @@ def test_is_valid_datetime(self, input_dt, input_datetime_format, expected_outpu ('19941106', False), ('941106', False), ('1941106', False), - ('1994=11=06', False), - (123, False) + ('1994=11=06', False) ]) def test_is_valid_date(self, input_date, expected_output): actual_output = DateTimeHelper.validate_date(input_date) diff --git a/tests/apimatic_core/utility_tests/test_file_helper.py b/tests/apimatic_core/utility_tests/test_file_helper.py index 598ef2f..fce8a2e 100644 --- a/tests/apimatic_core/utility_tests/test_file_helper.py +++ b/tests/apimatic_core/utility_tests/test_file_helper.py @@ -1,8 +1,7 @@ from apimatic_core.utilities.file_helper import FileHelper -from tests.apimatic_core.base import Base -class TestFileHelper(Base): +class TestFileHelper: def test_get_file(self): file_url = 'https://gist.githubusercontent.com/asadali214/' \ diff --git a/tests/apimatic_core/utility_tests/test_xml_helper.py b/tests/apimatic_core/utility_tests/test_xml_helper.py index a5373aa..8f6704f 100644 --- a/tests/apimatic_core/utility_tests/test_xml_helper.py +++ b/tests/apimatic_core/utility_tests/test_xml_helper.py @@ -332,7 +332,7 @@ def test_add_dict_as_sub_element(self, input_value, root_element_name, dictionar '' '', OneOfXML, 'Root', XmlHelper.serialize_to_xml(Base.one_of_xml_wolf_model(), 'Root')), - (None, None, None, None) + (None, int, None, None) ]) def test_deserialize_xml(self, input_value, clazz, root_element_name, expected_value): actual_value = XmlHelper.deserialize_xml(input_value, clazz) @@ -423,7 +423,7 @@ def test_deserialize_xml(self, input_value, clazz, root_element_name, expected_v Base.xml_model(), Base.xml_model()], 'Models', 'Item')), - (None, None, None, None, None) + (None, 'Item', int, 'Items', None) ]) def test_deserialize_xml_to_list(self, input_value, item_name, clazz, root_element_name, expected_value): actual_value = XmlHelper.deserialize_xml_to_list(input_value, item_name, clazz) @@ -477,11 +477,16 @@ def test_deserialize_xml_to_list(self, input_value, item_name, clazz, root_eleme '', ApiHelper.RFC3339DateTime, 'Items', XmlHelper.serialize_dict_to_xml( - {'Item1': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), - 'Item2': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), - 'Item3': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + { + 'Item1': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)) + }, 'Items')), - (None, None, None, None) + (None, int, None, None) ]) def test_deserialize_xml_to_dict(self, input_value, clazz, root_element_name, expected_value): actual_value = XmlHelper.deserialize_xml_to_dict(input_value, clazz) @@ -495,7 +500,7 @@ def test_deserialize_xml_to_dict(self, input_value, clazz, root_element_name, ex ('True', bool, True), ('70', int, 70), ('70.56443', float, 70.56443), - (None, None, None), + (None, int, None), ]) def test_value_from_xml_attribute(self, input_value, clazz, expected_value): actual_value = XmlHelper.value_from_xml_attribute(input_value, clazz) @@ -506,7 +511,7 @@ def test_value_from_xml_attribute(self, input_value, clazz, expected_value): ('True', bool, 'root', True), ('70', int, 'root', 70), ('70.56443', float, 'root', 70.56443), - (None, None, None, None), + (None, int, None, None), ]) def test_value_from_xml_element(self, input_value, root_element_name, clazz, expected_value): root_element = None @@ -523,7 +528,7 @@ def test_value_from_xml_element(self, input_value, root_element_name, clazz, exp ([50.58, 60.58, 70.58], 'root', 'item', 'items', float, [50.58, 60.58, 70.58]), ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), - (None, None, None, None, None, None), + (None, None, 'item', 'items', int, None), ]) def test_list_from_xml_element(self, input_value, root_element_name, item_name, wrapping_element_name, clazz, expected_value): @@ -558,7 +563,7 @@ def test_list_from_xml_element_with_unset_wrapper(self, input_value, root_elemen {'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}), ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), - (None, None, None, None), + (None, None, int, None), ]) def test_dict_from_xml_element(self, input_value, wrapping_element_name, clazz, expected_value): From 9ad1c0e8bd32f875dba817aaa71a952a3d363199 Mon Sep 17 00:00:00 2001 From: "DESKTOP-10GD4VQ\\Sufyan" Date: Mon, 24 Feb 2025 15:09:37 +0500 Subject: [PATCH 4/6] fixes mypy reported issue in tests package and refactors test with typing support --- .gitignore | 3 +- apimatic_core/response_handler.py | 2 +- apimatic_core/utilities/api_helper.py | 64 +- apimatic_core/utilities/api_helper.py~ | 764 +++++++++++++++ apimatic_core/utilities/comparison_helper.py | 4 +- apimatic_core/utilities/xml_helper.py | 17 +- apimatic_core/utilities/xml_helper.py~ | 419 ++++++++ mypy.ini | 3 + mypy.ini~ | 11 + .../api_call_tests/test_api_call.py | 35 +- .../api_call_tests/test_api_call.py~ | 152 +++ .../api_logger_tests/test_api_logger.py | 250 ++--- .../test_logging_configuration.py | 47 +- .../test_logging_configuration.py~ | 223 +++++ tests/apimatic_core/base.py | 91 +- tests/apimatic_core/base.py~ | 501 ++++++++++ .../mocks/callables/base_uri_callable.py | 4 +- .../mocks/callables/base_uri_callable.py~ | 42 + .../exceptions/nested_model_exception.py | 2 +- tests/apimatic_core/mocks/http/http_client.py | 22 +- .../apimatic_core/mocks/http/http_client.py~ | 52 + .../mocks/http/http_response_catcher.py | 4 +- .../mocks/http/http_response_catcher.py~ | 28 + .../mocks/models/complex_type.py | 4 +- .../mocks/models/complex_type.py~ | 55 ++ .../test_request_builder.py | 251 +++-- .../test_request_builder.py~ | 832 ++++++++++++++++ .../test_response_handler.py | 52 +- .../test_response_handler.py~ | 399 ++++++++ .../union_type_tests/test_any_of.py | 49 +- .../union_type_tests/test_one_of.py | 77 +- .../utility_tests/test_api_helper.py | 399 +++++--- .../utility_tests/test_api_helper.py~ | 922 ++++++++++++++++++ .../utility_tests/test_auth_helper.py | 38 +- .../utility_tests/test_auth_helper.py~ | 61 ++ .../utility_tests/test_comparison_helper.py | 28 +- .../utility_tests/test_datetime_helper.py | 72 +- .../utility_tests/test_datetime_helper.py~ | 104 ++ .../utility_tests/test_file_helper.py | 13 +- .../utility_tests/test_file_helper.py~ | 14 + .../utility_tests/test_xml_helper.py | 134 ++- .../utility_tests/test_xml_helper.py~ | 670 +++++++++++++ 42 files changed, 6339 insertions(+), 575 deletions(-) create mode 100644 apimatic_core/utilities/api_helper.py~ create mode 100644 apimatic_core/utilities/xml_helper.py~ create mode 100644 mypy.ini~ create mode 100644 tests/apimatic_core/api_call_tests/test_api_call.py~ create mode 100644 tests/apimatic_core/api_logger_tests/test_logging_configuration.py~ create mode 100644 tests/apimatic_core/base.py~ create mode 100644 tests/apimatic_core/mocks/callables/base_uri_callable.py~ create mode 100644 tests/apimatic_core/mocks/http/http_client.py~ create mode 100644 tests/apimatic_core/mocks/http/http_response_catcher.py~ create mode 100644 tests/apimatic_core/mocks/models/complex_type.py~ create mode 100644 tests/apimatic_core/request_builder_tests/test_request_builder.py~ create mode 100644 tests/apimatic_core/response_handler_tests/test_response_handler.py~ create mode 100644 tests/apimatic_core/utility_tests/test_api_helper.py~ create mode 100644 tests/apimatic_core/utility_tests/test_auth_helper.py~ create mode 100644 tests/apimatic_core/utility_tests/test_datetime_helper.py~ create mode 100644 tests/apimatic_core/utility_tests/test_file_helper.py~ create mode 100644 tests/apimatic_core/utility_tests/test_xml_helper.py~ diff --git a/.gitignore b/.gitignore index 93f8354..d1ade8b 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,5 @@ cython_debug/ .idea/ # Visual Studio Code -.vscode/ \ No newline at end of file +.vscode/ +.qodo diff --git a/apimatic_core/response_handler.py b/apimatic_core/response_handler.py index 1511de8..d318748 100644 --- a/apimatic_core/response_handler.py +++ b/apimatic_core/response_handler.py @@ -25,7 +25,7 @@ def __init__(self): self._xml_item_name: Optional[str] = None @validate_call - def deserializer(self, deserializer: Optional[Callable[[Union[str, bytes]], Any]]) -> 'ResponseHandler': + def deserializer(self, deserializer: Any) -> 'ResponseHandler': self._deserializer = deserializer return self diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index ad6c2f4..05e67b8 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -217,39 +217,6 @@ def json_deserialize( return unboxing_function(decoded) - @staticmethod - @validate_call - def apply_unboxing_function( - value: Any, unboxing_function: Callable[[Any], Any], is_array: bool=False, is_dict: bool=False, - is_array_of_map: bool=False, is_map_of_array: bool=False, dimension_count: int=1 - ) -> Any: - if is_dict: - if is_map_of_array: - return {k: ApiHelper.apply_unboxing_function(v, - unboxing_function, - is_array=True, - dimension_count=dimension_count) - for k, v in value.items()} - else: - return {k: unboxing_function(v) for k, v in value.items()} - elif is_array: - if is_array_of_map: - return [ - ApiHelper.apply_unboxing_function(element, - unboxing_function, - is_dict=True, - dimension_count=dimension_count) - for element in value] - elif dimension_count > 1: - return [ApiHelper.apply_unboxing_function(element, unboxing_function, - is_array=True, - dimension_count=dimension_count - 1) - for element in value] - else: - return [unboxing_function(element) for element in value] - - return unboxing_function(value) - @staticmethod @validate_call def dynamic_deserialize(dynamic_response: Optional[str]) -> Any: @@ -313,19 +280,19 @@ def datetime_deserialize( if DateTimeFormat.HTTP_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(value, ApiHelper.HttpDateTime.from_value)] + ApiHelper.json_deserialize(str(value), ApiHelper.HttpDateTime.from_value)] else: return ApiHelper.HttpDateTime.from_value(value).datetime elif DateTimeFormat.UNIX_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(value, ApiHelper.UnixDateTime.from_value)] + ApiHelper.json_deserialize(str(value), ApiHelper.UnixDateTime.from_value)] else: return ApiHelper.UnixDateTime.from_value(value).datetime elif DateTimeFormat.RFC3339_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(value, ApiHelper.RFC3339DateTime.from_value)] + ApiHelper.json_deserialize(str(value), ApiHelper.RFC3339DateTime.from_value)] else: return ApiHelper.RFC3339DateTime.from_value(value).datetime @@ -744,29 +711,6 @@ def to_lower_case(list_of_string: Optional[List[str]]) -> Optional[List[str]]: return list(map(lambda x: x.lower(), list_of_string)) - @staticmethod - @validate_call - def get_additional_properties( - dictionary: Dict[str, Any], unboxing_function: Callable[[Any], Any] - ) -> Dict[str, Any]: - """Extracts additional properties from the dictionary. - - Args: - dictionary (dict): The dictionary to extract additional properties from. - unboxing_function (callable): The deserializer to apply to each item in the dictionary. - - Returns: - dict: A dictionary containing the additional properties and their values. - """ - additional_properties = {} - for key, value in dictionary.items(): - try: - additional_properties[key] = unboxing_function(value) - except Exception: - pass - - return additional_properties - @staticmethod def sanitize_model(**kwargs: Any) -> Dict[str, Any]: _sanitized_dump: Dict[str, Any] = {} @@ -802,7 +746,7 @@ def sanitize_model(**kwargs: Any) -> Dict[str, Any]: @staticmethod def check_conflicts_with_additional_properties( - model_cls: BaseModel, properties: Dict[str, Any], additional_props_field: str + model_cls: BaseModel, properties: Any, additional_props_field: str ) -> None: """Raise ValueError if properties contain names conflicting with model fields.""" defined_fields = { diff --git a/apimatic_core/utilities/api_helper.py~ b/apimatic_core/utilities/api_helper.py~ new file mode 100644 index 0000000..e74b6f3 --- /dev/null +++ b/apimatic_core/utilities/api_helper.py~ @@ -0,0 +1,764 @@ +from __future__ import annotations +from abc import abstractmethod, ABC +from collections import abc +import re +import datetime +import calendar +import email.utils as eut +from time import mktime +from urllib.parse import urlsplit + +import jsonpickle +import dateutil.parser +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.types.union_type import UnionType +from jsonpointer import JsonPointerException, resolve_pointer +from typing import Optional, Any, Dict, Type, Callable, List, Union, Tuple, Set + +from apimatic_core.types.file_wrapper import FileWrapper +from apimatic_core.types.array_serialization_format import SerializationFormats +from urllib.parse import quote +from pydantic import BaseModel, validate_call +from pydantic.fields import FieldInfo + + +class ApiHelper(object): + """A Helper Class for various functions associated with API Calls. + + This class contains static methods for operations that need to be + performed during API requests. All of the methods inside this class are + static methods, there is no need to ever initialise an instance of this + class. + + """ + + class CustomDate(ABC): + + """ A base class for wrapper classes of datetime. + + This class contains methods which help in + appropriate serialization of datetime objects. + + """ + + def __init__(self, dtime, value=None): + self.datetime = dtime + if not value: + self.value = self.from_datetime(dtime) + else: + self.value = value + + def __repr__(self): + return str(self.value) + + def __getstate__(self): + return self.value + + def __setstate__(self, state): # pragma: no cover + pass + + @classmethod + @abstractmethod + def from_datetime(cls, date_time: datetime.datetime): + pass + + @classmethod + @abstractmethod + def from_value(cls, value: str): + pass + + class HttpDateTime(CustomDate): + + """ A wrapper class for datetime to support HTTP date format.""" + + @classmethod + def from_datetime(cls, date_time): + return eut.formatdate(timeval=mktime(date_time.timetuple()), localtime=False, usegmt=True) + + @classmethod + def from_value(cls, value): + dtime = datetime.datetime.fromtimestamp(eut.mktime_tz(eut.parsedate_tz(value))) + return cls(dtime, value) + + class UnixDateTime(CustomDate): + + """ A wrapper class for datetime to support Unix date format.""" + + @classmethod + def from_datetime(cls, date_time): + return calendar.timegm(date_time.utctimetuple()) + + @classmethod + def from_value(cls, value): + dtime = datetime.datetime.utcfromtimestamp(float(value)) + return cls(dtime, int(value)) + + class RFC3339DateTime(CustomDate): + + """ A wrapper class for datetime to support Rfc 3339 format.""" + + @classmethod + def from_datetime(cls, date_time): + return date_time.isoformat() + + @classmethod + def from_value(cls, value): + dtime = dateutil.parser.parse(value) + return cls(dtime, value) + + SKIP = '#$%^S0K1I2P3))*' + + @staticmethod + @validate_call + def json_serialize_wrapped_params(obj: Optional[Dict[str, Any]]) -> Optional[str]: + """JSON Serialization of a given wrapped object. + + Args: + obj (object): The object to serialize. + + Returns: + str: The JSON serialized string of the object. + + """ + if obj is None: + return None + val = dict() + for k, v in obj.items(): + val[k] = ApiHelper.json_serialize(v, should_encode=False) + + return jsonpickle.encode(val, False) + + @staticmethod + @validate_call + def json_serialize(obj: Any, should_encode: bool=True) -> Optional[str]: + """JSON Serialization of a given object. + + Args: + obj (object): The object to serialize. + should_encode: whether to encode at end or not + + Returns: + str: The JSON serialized string of the object. + + """ + + if obj is None: + return None + + if isinstance(obj, str): + return obj + + # Resolve any Names if it's one of our objects that needs to have this called on + if isinstance(obj, list): + value_list: List[Optional[str]] = list() + for item in obj: + if isinstance(item, dict) or isinstance(item, list): + value_list.append(ApiHelper.json_serialize(item, should_encode=False)) + elif ApiHelper.is_custom_type(item): + value_list.append(ApiHelper.json_serialize(item, should_encode=False)) + else: + value_list.append(item) + obj = value_list + elif isinstance(obj, dict): + value_dict: Dict[str, Optional[str]] = dict() + for key, item in obj.items(): + if isinstance(item, list) or isinstance(item, dict): + value_dict[key] = ApiHelper.json_serialize(item, should_encode=False) + elif ApiHelper.is_custom_type(item): + value_dict[key] = ApiHelper.json_serialize(item, should_encode=False) + else: + value_dict[key] = item + obj = value_dict + elif ApiHelper.is_custom_type(obj): + return obj.model_dump_json() if should_encode else obj.model_dump() + if not should_encode: + return obj + return jsonpickle.encode(obj, False) + + @staticmethod + @validate_call + def is_custom_type(obj_or_class: Any) -> bool: + return (isinstance(obj_or_class, type) and + issubclass(obj_or_class, BaseModel) or + isinstance(obj_or_class, BaseModel)) + + @staticmethod + @validate_call + def json_deserialize( + json: Optional[str], unboxing_function: Optional[Callable[[Any], Any]]=None, as_dict: bool=False + ) -> Any: + """JSON Deserialization of a given string. + + Args: + json (str): The JSON serialized string to deserialize. + unboxing_function (callable): The deserialization funtion to be used. + as_dict (bool): The flag to determine to deserialize json as dictionary type + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + """ + if json is None or json.strip() == '': + return None + + try: + decoded = jsonpickle.decode(json) + except ValueError: + return json + + if unboxing_function is None: + return decoded + + if as_dict: + return {k: unboxing_function(v) for k, v in decoded.items()} + elif isinstance(decoded, list): + return [unboxing_function(element) for element in decoded] + + return unboxing_function(decoded) + + @staticmethod + @validate_call + def dynamic_deserialize(dynamic_response: Optional[str]) -> Any: + """JSON Deserialization of a given string. + + Args: + dynamic_response (str): The response string to deserialize. + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + + """ + if dynamic_response is not None or not str(dynamic_response): + return ApiHelper.json_deserialize(dynamic_response) + + @staticmethod + @validate_call + def date_deserialize(json: Optional[str]) -> Union[datetime.date, List[datetime.date]]: + """JSON Deserialization of a given string. + + Args: + json (str): The JSON serialized string to deserialize. + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + """ + deserialized_response = ApiHelper.json_deserialize(json) + if isinstance(deserialized_response, list): + return [dateutil.parser.parse(element).date() for element in deserialized_response] + + return dateutil.parser.parse(deserialized_response).date() + + @staticmethod + @validate_call + def datetime_deserialize( + value: Optional[Union[str, float]], datetime_format: Optional[DateTimeFormat] + ) -> Union[None, CustomDate, Dict[str, CustomDate], List[CustomDate]]: + """JSON Deserialization of a given string. + + Args: + value: the response to deserialize + datetime_format: The date time format to deserialize into + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + """ + if value is None: + return None + + if isinstance(value, str): + deserialized_response = ApiHelper.json_deserialize(value) + else: + deserialized_response = value + + if DateTimeFormat.HTTP_DATE_TIME == datetime_format: + if isinstance(deserialized_response, list): + return [element.datetime for element in + ApiHelper.json_deserialize(str(value), ApiHelper.HttpDateTime.from_value)] + else: + return ApiHelper.HttpDateTime.from_value(value).datetime + elif DateTimeFormat.UNIX_DATE_TIME == datetime_format: + if isinstance(deserialized_response, list): + return [element.datetime for element in + ApiHelper.json_deserialize(str(value), ApiHelper.UnixDateTime.from_value)] + else: + return ApiHelper.UnixDateTime.from_value(value).datetime + elif DateTimeFormat.RFC3339_DATE_TIME == datetime_format: + if isinstance(deserialized_response, list): + return [element.datetime for element in + ApiHelper.json_deserialize(str(value), ApiHelper.RFC3339DateTime.from_value)] + else: + return ApiHelper.RFC3339DateTime.from_value(value).datetime + + return deserialized_response + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def deserialize_union_type( + union_type: UnionType, value: Any, should_deserialize: bool=True + ) -> Any: + if should_deserialize and isinstance(value, str): + value = ApiHelper.json_deserialize(value, as_dict=True) + + union_type_result = union_type.validate(value) + + return union_type_result.deserialize(value) + + @staticmethod + @validate_call + def get_content_type(value: Any) -> Optional[str]: + """Get content type header for oneof. + + Args: + value: The value passed by the user. + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + """ + if value is None: + return None + primitive = (int, str, bool, float) + + if type(value) in primitive: + return 'text/plain; charset=utf-8' + + else: + return 'application/json; charset=utf-8' + + @staticmethod + @validate_call + def get_schema_path(path: str, file_name: str) -> str: + """Return the Schema's path + + Returns: + string : returns Correct schema path + + """ + pascal_name = file_name.replace("_", " ").title().replace(" ", "") + path = path.replace('\\models', '\\schemas').replace('/models', '/schemas') \ + .replace(".py", ".json").replace(file_name, pascal_name) + return path + + @staticmethod + @validate_call + def serialize_array( + key: str, array: List[Any], formatting: SerializationFormats=SerializationFormats.INDEXED, + is_query: bool=False + ) -> List[Tuple[str, Any]]: + """Converts an array parameter to a list of key value tuples. + + Args: + key (str): The name of the parameter. + array (list): The value of the parameter. + formatting (str): The type of key formatting expected. + is_query (bool): Decides if the parameters are for query or form. + + Returns: + list: A list with key value tuples for the array elements. + + """ + tuples: List[Tuple[str, Any]] = [] + + serializable_types = (str, int, float, bool, datetime.date, ApiHelper.CustomDate) + + if isinstance(array[0], serializable_types): + if formatting == SerializationFormats.UN_INDEXED: + tuples += [("{0}[]".format(key), element) for element in array] + elif formatting == SerializationFormats.INDEXED: + tuples += [("{0}[{1}]".format(key, index), element) for index, element in enumerate(array)] + elif formatting == SerializationFormats.PLAIN: + tuples += [(key, element) for element in array] + elif is_query: + if formatting == SerializationFormats.CSV: + tuples += [(key, ",".join(str(x) for x in array))] + + elif formatting == SerializationFormats.PSV: + tuples += [(key, "|".join(str(x) for x in array))] + + elif formatting == SerializationFormats.TSV: + tuples += [(key, "\t".join(str(x) for x in array))] + else: + raise ValueError("Invalid format provided.") + else: + raise ValueError("Invalid format provided.") + else: + tuples += [("{0}[{1}]".format(key, index), element) for index, element in enumerate(array)] + + return tuples + + @staticmethod + @validate_call + def append_url_with_template_parameters(url: Optional[str], parameters: Optional[Dict[str, Dict[str, Any]]]) -> str: + """Replaces template parameters in the given url. + + Args: + url (str): The query url string to replace the template parameters. + parameters (dict): The parameters to replace in the url. + + Returns: + str: URL with replaced parameters. + + """ + # Parameter validation + if url is None: + raise ValueError("URL is None.") + + if parameters is None: + return url + + # Iterate and replace parameters + for key in parameters: + value = parameters[key]['value'] + encode = parameters[key]['encode'] + replace_value = '' + + # Load parameter value + if value is None: + replace_value = '' + elif isinstance(value, list): + replace_value = "/".join((quote(str(x), safe='') if encode else str(x)) for x in value) + else: + replace_value = quote(str(value), safe='') if encode else str(value) + + url = url.replace('{{{0}}}'.format(key), str(replace_value)) + + return url + + @staticmethod + @validate_call + def append_url_with_query_parameters( + url: Optional[str], parameters: Optional[Dict[str, Any]], + array_serialization: SerializationFormats=SerializationFormats.INDEXED + ) -> str: + """Adds query parameters to a URL. + + Args: + url (str): The URL string. + parameters (dict): The query parameters to add to the URL. + array_serialization (str): The format of array parameter serialization. + + Returns: + str: URL with added query parameters. + + """ + # Parameter validation + if url is None: + raise ValueError("URL is None.") + + if parameters is None: + return url + + query_parameters = ApiHelper.process_complex_types_parameters(parameters, array_serialization) + for value in query_parameters: + key = value[0] + val = value[1] + seperator = '&' if '?' in url else '?' + if value is not None: + url += "{0}{1}={2}".format(seperator, key, quote(str(val), safe='')) + + return url + + @staticmethod + @validate_call + def get_url_without_query(url: str) -> str: + """ + Extracts the protocol, domain, and path from a URL excluding the query parameters. + + Args: + url: The URL string. + + Returns: + A string containing the protocol, domain, and path of the URL without the query string. + + Raises: + ValueError: If the URL is invalid. + """ + try: + parsed_url = urlsplit(url) + if not parsed_url.netloc: # Check if URL has scheme and netloc (valid URL) + raise ValueError("Invalid URL format") + return f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}" + except ValueError as e: + raise ValueError(f"Error parsing URL: {e}") from e + + @staticmethod + @validate_call + def process_complex_types_parameters( + query_parameters: Dict[str, Any], array_serialization: SerializationFormats + ) -> List[Tuple[str, Any]]: + processed_params = [] + for key, value in query_parameters.items(): + processed_params.extend( + ApiHelper.form_encode(value, key, array_serialization=array_serialization, is_query=True)) + return processed_params + + @staticmethod + @validate_call + def clean_url(url: str) -> str: + """Validates and processes the given query Url to clean empty slashes. + + Args: + url (str): The given query Url to process. + + Returns: + str: Clean Url as string. + + """ + # Ensure that the urls are absolute + regex = "^https?://[^/]+" + match = re.match(regex, url) + if match is None: + raise ValueError('Invalid Url format.') + + protocol = match.group(0) + index = url.find('?') + query_url = url[len(protocol): index if index != -1 else None] + query_url = re.sub("//+", "/", query_url) + parameters = url[index:] if index != -1 else "" + + return protocol + query_url + parameters + + @staticmethod + @validate_call + def form_encode_parameters( + form_parameters: Dict[str, Any], array_serialization: SerializationFormats=SerializationFormats.INDEXED + ) -> List[Tuple[str, Any]]: + """Form encodes a dictionary of form parameters + + Args: + form_parameters (dictionary): The given dictionary which has + atleast one model to form encode. + array_serialization (str): The format of array parameter serialization. + + Returns: + dict: A dictionary of form encoded properties of the model. + + """ + encoded = [] + + for key, value in form_parameters.items(): + encoded += ApiHelper.form_encode(value, key, array_serialization) + + return encoded + + @staticmethod + @validate_call + def form_encode( + obj: Any, instance_name: str, + array_serialization: SerializationFormats=SerializationFormats.INDEXED, is_query: bool=False + ) -> List[Tuple[str, Any]]: + """Encodes a model in a form-encoded manner such as person[Name] + + Args: + obj (object): The given Object to form encode. + instance_name (string): The base name to appear before each entry + for this object. + array_serialization (string): The format of array parameter serialization. + is_query (bool): Decides if the parameters are for query or form. + + Returns: + dict: A dictionary of form encoded properties of the model. + + """ + retval = [] + # If we received an object, resolve its field names. + if ApiHelper.is_custom_type(obj): + obj = ApiHelper.json_serialize(obj, should_encode=False) + + if obj is None: + return [] + elif isinstance(obj, list): + for element in ApiHelper.serialize_array(instance_name, obj, array_serialization, is_query): + retval += ApiHelper.form_encode(element[1], element[0], array_serialization, is_query) + elif isinstance(obj, dict): + for item in obj: + retval += ApiHelper.form_encode(obj[item], instance_name + "[" + item + "]", array_serialization, + is_query) + else: + if isinstance(obj, bool): + obj = str(obj).lower() + retval.append((instance_name, obj)) + + return retval + + @staticmethod + @validate_call + def apply_datetime_converter( + value: Any, + datetime_converter_obj: Callable[[Any], Any]) -> Any: + if isinstance(value, list): + return [ApiHelper.apply_datetime_converter(item, datetime_converter_obj) for item in value] + + if isinstance(value, dict): + return {k: ApiHelper.apply_datetime_converter(v, datetime_converter_obj) for k, v in value.items()} + + if isinstance(value, datetime.datetime): + return ApiHelper.when_defined(datetime_converter_obj, value) + + return value + + @staticmethod + @validate_call + def when_defined(func: Callable[[datetime.datetime], Any], value: datetime.datetime) -> Any: + return func(value) if value else None + + @staticmethod + @validate_call + def is_file_wrapper_instance(param: Any) -> bool: + return isinstance(param, FileWrapper) + + @staticmethod + def is_valid_type( + value: Any, type_callable: Callable[[Any], bool], is_value_nullable: bool=False, is_model_dict: bool=False, + is_inner_model_dict: bool=False + ) -> bool: + if value is None and is_value_nullable: + return True + + if isinstance(value, list): + return all(ApiHelper.is_valid_type(item, type_callable, is_value_nullable, is_model_dict, + is_inner_model_dict) for item in value) + elif isinstance(value, dict) and (not is_model_dict or is_inner_model_dict): + return all(ApiHelper.is_valid_type(item, type_callable, is_value_nullable, is_model_dict) + for item in value.values()) + + return value is not None and type_callable(value) + + @staticmethod + @validate_call + def resolve_template_placeholders_using_json_pointer( + placeholders: Set[str], value: Optional[Dict[str, Any]], template: str + ) -> str: + """Updates all placeholders in the given message template with provided value. + + Args: + placeholders: The placeholders that need to be searched and replaced in the given template value. + value: The dictionary containing the actual values to replace with. + template: The template string containing placeholders. + + Returns: + string: The resolved template value. + """ + for placeholder in placeholders: + extracted_value: Optional[str] = '' + + if '#' in placeholder: + # pick the 2nd chunk then remove the last character (i.e. `}`) of the string value + node_pointer = placeholder.rsplit('#')[1].rstrip('}') + try: + extracted_value = resolve_pointer(value, node_pointer) if node_pointer else '' + extracted_value = ApiHelper.json_serialize(extracted_value) \ + if type(extracted_value) in [list, dict] else str(extracted_value) + except JsonPointerException: + pass + elif value is not None: + extracted_value = ApiHelper.json_serialize(value) + template = template.replace(placeholder, extracted_value or '') + + return template + + @staticmethod + @validate_call + def resolve_template_placeholders(placeholders: Set[str], values: Union[str, Dict[str, Any]], template: str) -> str: + """Updates all placeholders in the given message template with provided value. + + Args: + placeholders: The placeholders that need to be searched and replaced in the given template value. + values: The dictionary|string value which refers to the actual values to replace with. + template: The template string containing placeholders. + + Returns: + string: The resolved template value. + """ + for placeholder in placeholders: + if isinstance(values, abc.Mapping): + # pick the last chunk then strip the last character (i.e. `}`) of the string value + key = placeholder.rsplit('.', maxsplit=1)[-1].rstrip('}') if '.' in placeholder \ + else placeholder.lstrip('{').rstrip('}') + value_to_replace = str(values.get(key)) if values.get(key) else '' + template = template.replace(placeholder, value_to_replace) + else: + values = str(values) if values is not None else '' + template = template.replace(placeholder, values) + + return template + + @staticmethod + @validate_call + def to_lower_case(list_of_string: Optional[List[str]]) -> Optional[List[str]]: + """Converts all strings in a list to lowercase. + + Args: + list_of_string (list): A list of strings to convert to lowercase. + + Returns: + list: A new list containing the lowercase versions of the input strings. + Returns None if the input list is None. + + Raises: + TypeError: If the input is not a list. + """ + if list_of_string is None: + return None + + return list(map(lambda x: x.lower(), list_of_string)) + + @staticmethod + def sanitize_model(**kwargs: Any) -> Dict[str, Any]: + _sanitized_dump: Dict[str, Any] = {} + _nullable_fields: Set[str] = kwargs.get('nullable_fields', set()) + _optional_fields: Set[str] = kwargs.get('optional_fields', set()) + _model_dump: Dict[str, Any] = kwargs.get('model_dump', {}) + _model_fields: Dict[str, FieldInfo] = kwargs.get('model_fields', {}) + _model_fields_set: Set[str] = kwargs.get('model_fields_set', set()) + + for _name, _field_info in _model_fields.items(): + _value = _model_dump.pop(_name, None) + _alias = _field_info.serialization_alias or _name + + if _name not in _nullable_fields and _name not in _optional_fields: + # Always include required properties + _sanitized_dump[_alias] = _value + + elif _name in _nullable_fields and _name in _optional_fields: + # Include optional_nullable properties if explicitly set or not None + if _value is not None or _name in _model_fields_set: + _sanitized_dump[_alias] = _value + + elif _name in _optional_fields: + # Exclude optional properties if None + if _value is not None: + _sanitized_dump[_alias] = _value + + elif _name in _nullable_fields: + # Always include nullable properties, even if None + _sanitized_dump[_alias] = _value + + return _sanitized_dump + + @staticmethod + def check_conflicts_with_additional_properties( + model_cls: BaseModel, properties: Dict[str, Any], additional_props_field: str + ) -> None: + """Raise ValueError if properties contain names conflicting with model fields.""" + defined_fields = { + alias or name + for name, field_info in model_cls.model_fields.items() + if name != additional_props_field # Dynamically exclude additional properties field + for alias in (field_info.serialization_alias, name) + } + + conflicting_keys = defined_fields.intersection(properties) + if conflicting_keys: + raise ValueError( + f"Invalid additional properties: {conflicting_keys}. " + "These names conflict with existing model properties." + ) diff --git a/apimatic_core/utilities/comparison_helper.py b/apimatic_core/utilities/comparison_helper.py index 5c13822..d062bff 100644 --- a/apimatic_core/utilities/comparison_helper.py +++ b/apimatic_core/utilities/comparison_helper.py @@ -10,8 +10,8 @@ class ComparisonHelper: @staticmethod @validate_call def match_headers( - expected_headers: Dict[str, Optional[str]], - received_headers: Dict[str, str], + expected_headers: Dict[str, Any], + received_headers: Dict[str, Any], allow_extra: bool = True ) -> bool: """Static method to compare the received headers with the expected headers. diff --git a/apimatic_core/utilities/xml_helper.py b/apimatic_core/utilities/xml_helper.py index 700dc20..1939795 100644 --- a/apimatic_core/utilities/xml_helper.py +++ b/apimatic_core/utilities/xml_helper.py @@ -24,7 +24,7 @@ class XmlHelper: @staticmethod @validate_call - def serialize_to_xml(value: Any, root_element_name: str) -> str: + def serialize_to_xml(value: Any, root_element_name: str) -> Optional[str]: """Serializes a given value to an XML document. Args: @@ -34,6 +34,9 @@ def serialize_to_xml(value: Any, root_element_name: str) -> str: Returns: str: Serialized XML string. """ + if value is None: + return None + root = ET.Element(root_element_name) if value is not None: @@ -43,7 +46,7 @@ def serialize_to_xml(value: Any, root_element_name: str) -> str: @staticmethod @validate_call - def serialize_list_to_xml(value: List[Any], root_element_name: str, array_item_name: str) -> str: + def serialize_list_to_xml(value: Optional[List[Any]], root_element_name: str, array_item_name: str) -> Optional[str]: """Serializes a given list of values to an XML document. Args: @@ -54,6 +57,9 @@ def serialize_list_to_xml(value: List[Any], root_element_name: str, array_item_n Returns: str: Serialized XML string. """ + if value is None: + return None + root = ET.Element(root_element_name) XmlHelper.add_list_as_subelement(root, value, array_item_name) @@ -62,7 +68,7 @@ def serialize_list_to_xml(value: List[Any], root_element_name: str, array_item_n @staticmethod @validate_call - def serialize_dict_to_xml(value: Dict[str, Any], root_element_name: str) -> str: + def serialize_dict_to_xml(value: Optional[Dict[str, Any]], root_element_name: str) -> Optional[str]: """Serializes a given dictionary of values to an XML document. Args: @@ -72,6 +78,9 @@ def serialize_dict_to_xml(value: Dict[str, Any], root_element_name: str) -> str: Returns: str: Serialized XML string. """ + if value is None: + return None + root = ET.Element(root_element_name) XmlHelper.add_dict_as_subelement(root, value) @@ -126,7 +135,7 @@ def add_as_subelement(root: ET.Element, value: Any, name: str) -> None: @staticmethod @validate_call(config=dict(arbitrary_types_allowed=True)) - def add_list_as_subelement(root: ET.Element, items: List[Any], item_name: str, + def add_list_as_subelement(root: ET.Element, items: Optional[List[Any]], item_name: str, wrapping_element_name: Optional[str] = None) -> None: """Converts the given list to an ET.Element if it is not None and adds it to an existing ET.Element. diff --git a/apimatic_core/utilities/xml_helper.py~ b/apimatic_core/utilities/xml_helper.py~ new file mode 100644 index 0000000..724340d --- /dev/null +++ b/apimatic_core/utilities/xml_helper.py~ @@ -0,0 +1,419 @@ +# -*- coding: utf-8 -*- + +""" +testerxml + +This file was automatically generated by APIMATIC v3.0 ( + https://www.apimatic.io ). +""" + +import xml.etree.ElementTree as ET +import datetime +import dateutil.parser +from typing import Any, List, Dict, Optional, Type, Callable, Union + +from pydantic import validate_call + +from apimatic_core.utilities.api_helper import ApiHelper + + +class XmlHelper: + """This class hold utility methods for xml serialization and + deserialization. + """ + + @staticmethod + @validate_call + def serialize_to_xml(value: Any, root_element_name: str) -> Optional[str]: + """Serializes a given value to an XML document. + + Args: + value (Any): The value to serialize. + root_element_name (str): The name of the document's root element. + + Returns: + str: Serialized XML string. + """ + if value is None: + return None + + root = ET.Element(root_element_name) + + if value is not None: + XmlHelper.add_to_element(root, value) + + return ET.tostring(root).decode() + + @staticmethod + @validate_call + def serialize_list_to_xml(value: Optional[List[Any]], root_element_name: str, array_item_name: str) -> Optional[str]: + """Serializes a given list of values to an XML document. + + Args: + value (List[Any]): The list of values to serialize. + root_element_name (str): The name of the document's root element. + array_item_name (str): The element name to use for each item in 'values'. + + Returns: + str: Serialized XML string. + """ + if value is None: + return None + + root = ET.Element(root_element_name) + + XmlHelper.add_list_as_subelement(root, value, array_item_name) + + return ET.tostring(root).decode() + + @staticmethod + @validate_call + def serialize_dict_to_xml(value: Optional[Dict[str, Any][], root_element_name: str) -> str: + """Serializes a given dictionary of values to an XML document. + + Args: + value (Dict[str, Any]): The dictionary to serialize. + root_element_name (str): The name of the document's root element. + + Returns: + str: Serialized XML string. + """ + if value is None: + return None + + root = ET.Element(root_element_name) + + XmlHelper.add_dict_as_subelement(root, value) + + return ET.tostring(root).decode() + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_to_element(element: ET.Element, value: Any) -> None: + """Converts the given value to XML and adds it to an existing ET.Element. + + Args: + element (ET.Element): The XML tag to add the 'value' to. + value (Any): The value to add to the element. + """ + if value is None: + return + + if isinstance(value, bool): + element.text = str(value).lower() + elif isinstance(value, (int, float, str, datetime.date, ApiHelper.CustomDate)): + element.text = str(value) + else: + value.to_xml_sub_element(element) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_as_attribute(root: ET.Element, value: Any, name: str) -> None: + """Sets an attribute on an ET.Element instance if the value isn't None. + + Args: + root (ET.Element): The parent of this XML attribute. + value (Any): The value to set to the attribute. + name (str): The name of the attribute being set. + """ + if value is not None: + root.set(name, str(value).lower() if isinstance(value, bool) else str(value)) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_as_subelement(root: ET.Element, value: Any, name: str) -> None: + """Converts the given value to an ET.Element if it is not None and adds it to an existing ET.Element. + + Args: + root (ET.Element): The parent of this XML element. + value (Any): The value to add to the element. + name (str): The name of the element being added. + """ + if value is not None: + tag = ET.SubElement(root, name) + XmlHelper.add_to_element(tag, value) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_list_as_subelement(root: ET.Element, items: Optional[List[Any]], item_name: str, + wrapping_element_name: Optional[str] = None) -> None: + """Converts the given list to an ET.Element if it is not None and adds it to an existing ET.Element. + + Args: + root (ET.Element): The parent of this XML element. + items (List[Any]): The list of values to add to the element. + item_name (str): The element name to use for each item in 'items'. + wrapping_element_name (Optional[str]): The element name to use for the wrapping element, if needed. + """ + if items is not None: + parent = ET.SubElement(root, wrapping_element_name) if wrapping_element_name else root + for item in items: + sub_elem = ET.SubElement(parent, item_name) + XmlHelper.add_to_element(sub_elem, item) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_dict_as_subelement(root: ET.Element, items: Dict[str, Any], dictionary_name: Optional[str] = None) -> None: + """Converts the given dictionary to an ET.Element if it is not None and adds it to an existing ET.Element. + + Args: + root (ET.Element): The parent of this XML element. + items (Dict[str, Any]): The dictionary of values to add to the element. + dictionary_name (Optional[str]): The element name to use for the encapsulating element. + """ + if items is not None: + parent = ET.SubElement(root, dictionary_name) if dictionary_name else root + for key, value in items.items(): + if isinstance(value, list): + XmlHelper.add_list_as_subelement(parent, value, key) + else: + XmlHelper.add_as_subelement(parent, value, key) + + @staticmethod + @validate_call + def deserialize_xml(xml: Optional[str], clazz: Type[Any]) -> Optional[Any]: + """Deserializes an XML document to a Python object of the type given by 'clazz'. + + Args: + xml (Optional[str]): An XML document to deserialize. + clazz (Type[Any]): The class that the deserialized object should belong to. + + Returns: + Optional[Any]: An instance of the specified class, or None if input is empty. + """ + if not xml: + return None + + root = ET.fromstring(xml) + return XmlHelper.value_from_xml_element(root, clazz) + + @staticmethod + @validate_call + def deserialize_xml_to_list(xml: Optional[str], item_name: str, clazz: Type[Any]) -> Optional[List[Any]]: + """Deserializes an XML document to a list of Python objects, each of the type given by 'clazz'. + + Args: + xml (Optional[str]): An XML document to deserialize. + item_name (str): The name of the elements that need to be extracted into a list. + clazz (Type[Any]): The class that the deserialized objects should belong to. + + Returns: + Optional[List[Any]]: A list of instances of the specified class, or None if input is empty. + """ + if not xml: + return None + + root = ET.fromstring(xml) + return XmlHelper.list_from_xml_element(root, item_name, clazz) + + @staticmethod + @validate_call + def deserialize_xml_to_dict(xml: Optional[str], clazz: Type[Any]) -> Optional[Dict[str, Any]]: + """Deserializes an XML document to a dictionary of Python objects, each of the type given by 'clazz'. + + Args: + xml (Optional[str]): An XML document to deserialize. + clazz (Type[Any]): The class that the dictionary values should belong to. + + Returns: + Optional[Dict[str, Any]]: A dictionary with deserialized objects, or None if input is empty. + """ + if not xml: + return None + + root = ET.fromstring(xml) + return XmlHelper.dict_from_xml_element(root, clazz) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_xml_attribute(attribute: Optional[Union[ET.Element, str]], clazz: Type[Any]) -> Optional[Any]: + """Extracts the value from an attribute and converts it to the type + given by 'clazz'. + + Args: + attribute (str): The XML attribute to extract the value from. + clazz (Type[Any]): The class that the deserialized object should + belong to. + """ + if attribute is None: + return None + + conversion_function = XmlHelper.converter(clazz) + + return conversion_function(attribute)\ + if conversion_function is not None and isinstance(attribute, str) else None + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_xml_element(element: Optional[ET.Element], clazz: Type[Any]) -> Optional[Any]: + """Extracts the value from an element and converts it to the type given by 'clazz'. + + Args: + element (ET.Element): The XML element to extract the value from. + clazz (Type[Any]): The class that the deserialized object should belong to. + + Returns: + Optional[Any]: An instance of the specified class, or None if the element is None. + """ + if element is None: + return None + + # These classes can be cast directly. + if clazz in [int, float, str, bool, datetime.date] or \ + issubclass(clazz, ApiHelper.CustomDate): + conversion_function = XmlHelper.converter(clazz) + return conversion_function(element.text)\ + if conversion_function is not None and element.text is not None else None + + conversion_function = clazz.from_element if hasattr(clazz, 'from_element') else None + + return conversion_function(element) if conversion_function is not None and element is not None else None + + + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def list_from_xml_element(root: Optional[ET.Element], item_name: str, clazz: Type[Any], + wrapping_element_name: Optional[str] = None) -> Optional[List[Any]]: + """Deserializes an XML document to a list of Python objects, each of the type given by 'clazz'. + + Args: + root (ET.Element): The root XML element. + item_name (str): The name of the elements that need to be extracted into a list. + clazz (Type[Any]): The class that the deserialized objects should belong to. + wrapping_element_name (Optional[str]): The name of the wrapping element for the XML element array. + + Returns: + Optional[List[Any]]: A list of deserialized objects, or None if no matching elements are found. + """ + if root is None: + return None + + elements = XmlHelper.get_elements(root, wrapping_element_name, item_name) + + if elements is None: + return None + + return [XmlHelper.value_from_xml_element(element, clazz) for element in elements] + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def dict_from_xml_element(element: Optional[ET.Element], clazz: Type[Any]) -> Optional[Dict[str, Any]]: + """Extracts the values from an element and converts them to a dictionary with values of type 'clazz'. + + Args: + element (ET.Element): The XML element to convert. + clazz (Type[Any]): The class that the dictionary values should belong to. + + Returns: + Optional[Dict[str, Any]]: A dictionary of deserialized values, or None if the element is None. + """ + if element is None: + return None + + entries = list(element) + conversion_function = XmlHelper.converter(clazz) + return { + entry.tag: conversion_function(entry.text) if entry.text is not None else None + for entry in entries if conversion_function + } + + @staticmethod + @validate_call + def converter(clazz: Type[Any]) -> Optional[Callable[[str], Any]]: + """Provides the function to use for converting a string to the type given by 'clazz'. + + Args: + clazz (Type[Any]): The class to find the conversion function for. + + Returns: + Callable[[str], Any]: A conversion function, or None if not applicable. + """ + if clazz in [int, float, str]: + return lambda value: clazz(value) + elif clazz is bool: + return lambda value: value.lower() == 'true' + elif clazz is datetime.date: + return lambda value: dateutil.parser.parse(value).date() + # DateTime classes have their own method to convert from string. + elif issubclass(clazz, ApiHelper.CustomDate): + return clazz.from_value + + return None + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_one_of_xml_elements( + root: Optional[ET.Element], mapping_data: Optional[Dict[str, Any]] + ) -> Optional[Any]: + """Extracts the value from an element and converts it to the type given + by 'clazz'. + + Args: + root (ET.Element): The root XML element. + mapping_data (dict): A dictionary mapping possible element names + for a given field to corresponding types. + """ + if not mapping_data or root is None: + return None + + for element_name, tup in mapping_data.items(): + clazz = tup[0] + is_array = tup[1] + wrapping_element_name = tup[2] + if is_array: + elements = XmlHelper.get_elements(root, wrapping_element_name, element_name) + if elements is not None and len(elements) > 0: + return XmlHelper.list_from_xml_element( + root, element_name, clazz, wrapping_element_name) + else: + element = root.find(element_name) + if element is not None: + return XmlHelper.value_from_xml_element(element, clazz) + return None + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def list_from_multiple_one_of_xml_element(root: ET.Element, mapping_data: Dict[str, Any]) -> Optional[List[Any]]: + """Deserializes an xml document to a list of python objects + where all types of oneof schemas are allowed (when the outer + model is an array) + + Args: + root (ET.Element): An xml document to deserialize. + mapping_data (dict): A dictionary mapping possible element names + for a given field to corresponding types. + + """ + arr = [] + for elem in root.iter(): + if elem.tag in mapping_data: + arr.append(XmlHelper.value_from_xml_element( + elem, mapping_data[elem.tag][0])) + if len(arr) > 0: + return arr + else: + return None + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def get_elements(root: Optional[ET.Element], wrapping_element_name: Optional[str], item_name: str) -> Optional[List[ET.Element]]: + """Retrieves a list of XML elements based on the specified wrapping element and item name. + + Args: + root (ET.Element): The root XML element. + wrapping_element_name (Optional[str]): The name of the wrapping element. + item_name (str): The name of the desired elements. + + Returns: + Optional[List[ET.Element]]: A list of matching elements, or None if not found. + """ + if root is None: + return None + + if wrapping_element_name is None: + return root.findall(item_name) + wrapping_element = root.find(wrapping_element_name) + if wrapping_element is None: + return None + return wrapping_element.findall(item_name) diff --git a/mypy.ini b/mypy.ini index 68d6768..d2cef49 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,4 +5,7 @@ check_untyped_defs = True ignore_missing_imports = True [mypy-jsonpointer.*] +ignore_missing_imports = True + +[mypy-testfixtures.*] ignore_missing_imports = True \ No newline at end of file diff --git a/mypy.ini~ b/mypy.ini~ new file mode 100644 index 0000000..7567bb3 --- /dev/null +++ b/mypy.ini~ @@ -0,0 +1,11 @@ +[mypy] +check_untyped_defs = True + +[mypy-jsonpickle.*] +ignore_missing_imports = True + +[mypy-jsonpointer.*] +ignore_missing_imports = True + +[mypy-jsonpointer.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/tests/apimatic_core/api_call_tests/test_api_call.py b/tests/apimatic_core/api_call_tests/test_api_call.py index 95a3e4d..629db3e 100644 --- a/tests/apimatic_core/api_call_tests/test_api_call.py +++ b/tests/apimatic_core/api_call_tests/test_api_call.py @@ -1,6 +1,9 @@ +from typing import Optional + import pytest from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration from apimatic_core_interfaces.http.http_client import HttpClient +from pydantic import validate_call from apimatic_core.api_call import ApiCall from apimatic_core.configurations.global_configuration import GlobalConfiguration @@ -12,17 +15,23 @@ from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum from tests.apimatic_core.base import Base from tests.apimatic_core.mocks.callables.base_uri_callable import Server +from tests.apimatic_core.mocks.http.http_client import MockHttpClient +from tests.apimatic_core.mocks.http.http_response_catcher import HttpResponseCatcher from tests.apimatic_core.mocks.models.person import Employee class TestApiCall(Base): + @validate_call def setup_test(self, global_config: GlobalConfiguration): self.global_config: GlobalConfiguration = global_config - self.http_response_catcher: HttpCallBack = self.global_config.http_client_configuration.http_callback - self.http_client: HttpClient = self.global_config.http_client_configuration.http_client + self.http_response_catcher: HttpCallBack = (self.global_config.http_client_configuration.http_callback or + HttpResponseCatcher()) + self.http_client: HttpClient = (self.global_config.http_client_configuration.http_client or + MockHttpClient()) self.api_call_builder: ApiCall = self.new_api_call_builder(self.global_config) + @validate_call def test_end_to_end_with_uninitialized_http_client(self): self.setup_test(self.default_global_configuration) with pytest.raises(ValueError) as exception: @@ -42,6 +51,7 @@ def test_end_to_end_with_uninitialized_http_client(self): ).execute() assert exception.value.args[0] == 'An HTTP client instance is required to execute an Api call.' + @validate_call def test_end_to_end_with_uninitialized_http_callback(self): self.setup_test(self.global_configuration_without_http_callback) actual_employee_model: Employee = self.api_call_builder.new_builder.request( @@ -59,11 +69,12 @@ def test_end_to_end_with_uninitialized_http_callback(self): .deserialize_into(Employee.model_validate) ).execute() - assert self.http_client.should_retry is False - assert self.http_client.contains_binary_response is False - assert self.http_response_catcher is None + assert self.http_client.should_retry is False # type: ignore[attr-defined] + assert self.http_client.contains_binary_response is False # type: ignore[attr-defined] + assert self.http_response_catcher.response is None # type: ignore[attr-defined] assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) + @validate_call def test_end_to_end_with_not_implemented_http_callback(self): self.setup_test(self.global_configuration_unimplemented_http_callback) with pytest.raises(NotImplementedError) as not_implemented_exception: @@ -84,6 +95,7 @@ def test_end_to_end_with_not_implemented_http_callback(self): assert not_implemented_exception.value.args[0] == 'This method has not been implemented.' + @validate_call def test_end_to_end_without_endpoint_configurations(self): self.setup_test(self.global_configuration) actual_employee_model: Employee = self.api_call_builder.new_builder.request( @@ -101,9 +113,9 @@ def test_end_to_end_without_endpoint_configurations(self): .deserialize_into(Employee.model_validate) ).execute() - assert self.http_client.should_retry is False - assert self.http_client.contains_binary_response is False - assert self.http_response_catcher.response.status_code == 200 + assert self.http_client.should_retry is False # type: ignore[attr-defined] + assert self.http_client.contains_binary_response is False # type: ignore[attr-defined] + assert self.http_response_catcher.response.status_code == 200 # type: ignore[attr-defined] assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) @pytest.mark.parametrize('input_to_retry, ' @@ -114,6 +126,7 @@ def test_end_to_end_without_endpoint_configurations(self): (False, False, False, False), (True, True, True, True) ]) + @validate_call def test_end_to_end_with_endpoint_configurations(self, input_to_retry, input_contains_binary_response, expected_to_retry, expected_contains_binary_response): self.setup_test(self.global_configuration) @@ -134,7 +147,7 @@ def test_end_to_end_with_endpoint_configurations(self, input_to_retry, input_con EndpointConfiguration(should_retry=input_to_retry, has_binary_response=input_contains_binary_response) ).execute() - assert self.http_client.should_retry == expected_to_retry - assert self.http_client.contains_binary_response == expected_contains_binary_response - assert self.http_response_catcher.response.status_code == 200 + assert self.http_client.should_retry == expected_to_retry # type: ignore[attr-defined] + assert self.http_client.contains_binary_response == expected_contains_binary_response # type: ignore[attr-defined] + assert self.http_response_catcher.response.status_code == 200 # type: ignore[attr-defined] assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) diff --git a/tests/apimatic_core/api_call_tests/test_api_call.py~ b/tests/apimatic_core/api_call_tests/test_api_call.py~ new file mode 100644 index 0000000..bbabf32 --- /dev/null +++ b/tests/apimatic_core/api_call_tests/test_api_call.py~ @@ -0,0 +1,152 @@ +from typing import Optional + +import pytest +from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration +from apimatic_core_interfaces.http.http_client import HttpClient +from pydantic import validate_call + +from apimatic_core.api_call import ApiCall +from apimatic_core.configurations.global_configuration import GlobalConfiguration +from apimatic_core.http.http_callback import HttpCallBack +from apimatic_core.request_builder import RequestBuilder +from apimatic_core.response_handler import ResponseHandler +from apimatic_core.types.parameter import Parameter +from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.callables.base_uri_callable import Server +from tests.apimatic_core.mocks.http.http_client import MockHttpClient +from tests.apimatic_core.mocks.models.person import Employee + + +class TestApiCall(Base): + + @validate_call + def setup_test(self, global_config: GlobalConfiguration): + self.global_config: GlobalConfiguration = global_config + self.http_response_catcher: HttpCallBack = (self.global_config.http_client_configuration.http_callback or + HttpResponseCatcher()) + self.http_client: HttpClient = (self.global_config.http_client_configuration.http_client or + MockHttpClient()) + self.api_call_builder: ApiCall = self.new_api_call_builder(self.global_config) + + @validate_call + def test_end_to_end_with_uninitialized_http_client(self): + self.setup_test(self.default_global_configuration) + with pytest.raises(ValueError) as exception: + self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).execute() + assert exception.value.args[0] == 'An HTTP client instance is required to execute an Api call.' + + @validate_call + def test_end_to_end_with_uninitialized_http_callback(self): + self.setup_test(self.global_configuration_without_http_callback) + actual_employee_model: Employee = self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).execute() + + assert self.http_client.should_retry is False # type: ignore[attr-defined] + assert self.http_client.contains_binary_response is False # type: ignore[attr-defined] + assert self.http_response_catcher.response is None # type: ignore[attr-defined] + assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) + + @validate_call + def test_end_to_end_with_not_implemented_http_callback(self): + self.setup_test(self.global_configuration_unimplemented_http_callback) + with pytest.raises(NotImplementedError) as not_implemented_exception: + self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).execute() + + assert not_implemented_exception.value.args[0] == 'This method has not been implemented.' + + @validate_call + def test_end_to_end_without_endpoint_configurations(self): + self.setup_test(self.global_configuration) + actual_employee_model: Employee = self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).execute() + + assert self.http_client.should_retry is False # type: ignore[attr-defined] + assert self.http_client.contains_binary_response is False # type: ignore[attr-defined] + assert self.http_response_catcher.response.status_code == 200 # type: ignore[attr-defined] + assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) + + @pytest.mark.parametrize('input_to_retry, ' + 'input_contains_binary_response, ' + 'expected_to_retry, expected_contains_binary_response', [ + (True, False, True, False), + (False, True, False, True), + (False, False, False, False), + (True, True, True, True) + ]) + @validate_call + def test_end_to_end_with_endpoint_configurations(self, input_to_retry, input_contains_binary_response, + expected_to_retry, expected_contains_binary_response): + self.setup_test(self.global_configuration) + actual_employee_model: Employee = self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).endpoint_configuration( + EndpointConfiguration(should_retry=input_to_retry, has_binary_response=input_contains_binary_response) + ).execute() + + assert self.http_client.should_retry == expected_to_retry # type: ignore[attr-defined] + assert self.http_client.contains_binary_response == expected_contains_binary_response # type: ignore[attr-defined] + assert self.http_response_catcher.response.status_code == 200 # type: ignore[attr-defined] + assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) diff --git a/tests/apimatic_core/api_logger_tests/test_api_logger.py b/tests/apimatic_core/api_logger_tests/test_api_logger.py index 0f1257a..1c39ae3 100644 --- a/tests/apimatic_core/api_logger_tests/test_api_logger.py +++ b/tests/apimatic_core/api_logger_tests/test_api_logger.py @@ -1,6 +1,6 @@ import logging from logging import LogRecord -from typing import Any, Iterable, Callable, List +from typing import List, Optional import pytest @@ -9,6 +9,7 @@ from apimatic_core_interfaces.http.http_response import HttpResponse from apimatic_core_interfaces.logger.api_logger import ApiLogger from apimatic_core_interfaces.logger.logger import Logger +from pydantic import validate_call from apimatic_core.logger.configuration.api_logging_configuration import ApiRequestLoggingConfiguration, \ ApiResponseLoggingConfiguration @@ -27,16 +28,18 @@ class TestApiLogger(Base): + @validate_call @pytest.fixture def log_capture(self) -> LogCapture: """Fixture to capture logs during the test.""" with LogCapture() as capture: yield capture + @validate_call(config=dict(arbitrary_types_allowed=True)) def init_sdk_logger( self, logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, - request_logging_configuration: ApiRequestLoggingConfiguration=None, - response_logging_configuration: ApiResponseLoggingConfiguration=None): + request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, + response_logging_configuration: Optional[ApiResponseLoggingConfiguration]=None): self._api_request: HttpRequest = self.request( http_method=HttpMethodEnum.POST, query_url='http://localhost:3000/body/model?key=value', @@ -55,57 +58,65 @@ def init_sdk_logger( self._api_logger: ApiLogger = self.get_api_logger( logger, log_level, mask_sensitive_headers, request_logging_configuration, response_logging_configuration) + @validate_call(config=dict(arbitrary_types_allowed=True)) def get_api_logger( self, logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, - request_logging_configuration: ApiRequestLoggingConfiguration=None, - response_logging_configuration: ApiResponseLoggingConfiguration=None + request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, + response_logging_configuration: Optional[ApiResponseLoggingConfiguration]=None ) -> ApiLogger: return SdkLogger(self.api_logging_configuration( logger, log_level=log_level, mask_sensitive_headers=mask_sensitive_headers, request_logging_configuration=request_logging_configuration, response_logging_configuration=response_logging_configuration)) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger()) self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request_with_log_level(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), log_level=logging.WARN) self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + assert all(record.levelname == "WARNING" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request_with_include_query_in_path(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), request_logging_configuration=self.api_request_logging_configuration( include_query_in_path=True)) self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model?key=value', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model?key=value application/json', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model?key=value', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model?key=value application/json' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request_with_log_request_header(self, log_capture: LogCapture): self.init_sdk_logger( logger=MockedApiLogger(), @@ -113,68 +124,77 @@ def test_custom_logger_for_request_with_log_request_header(self, log_capture: Lo self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request Headers %s' and - record.args == {'Content-Type': 'application/json', 'Accept': 'application/json'} and - record.message == "Request Headers {'Content-Type': 'application/json', 'Accept': 'application/json'}", - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + + assert any( + record.msg == 'Request Headers %s' and + record.args == {'Content-Type': 'application/json', 'Accept': 'application/json'} and + record.message == "Request Headers {'Content-Type': 'application/json', 'Accept': 'application/json'}" + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request_with_log_request_body(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), request_logging_configuration=self.api_request_logging_configuration(log_body=True)) self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request Body %s' and - record.args == ('{"Key": "Value"}',) and - record.message == 'Request Body {"Key": "Value"}', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + + assert any( + record.msg == 'Request Body %s' and + record.args == ('{"Key": "Value"}',) and + record.message == 'Request Body {"Key": "Value"}' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_response(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger()) self._api_logger.log_response(self._api_response) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', 50) and - record.message == 'Response 200 application/json 50', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', 50) and + record.message == 'Response 200 application/json 50' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_response_with_log_level(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), log_level=logging.WARN) self._api_logger.log_response(self._api_response) + # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', 50) and - record.message == 'Response 200 application/json 50', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', 50) and + record.message == 'Response 200 application/json 50' + for record in records) + assert all(record.levelname == "WARNING" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_response_with_log_response_header(self, log_capture: LogCapture): self.init_sdk_logger( logger=MockedApiLogger(), @@ -182,61 +202,67 @@ def test_custom_logger_for_response_with_log_response_header(self, log_capture: self._api_logger.log_response(self._api_response) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', 50) and - record.message == 'Response 200 application/json 50', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response Headers %s' and - record.args == {'Content-Type': 'application/json', 'Content-Length': 50} and - record.message == "Response Headers {'Content-Type': 'application/json', 'Content-Length': 50}", - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', 50) and + record.message == 'Response 200 application/json 50' + for record in records) + + assert any( + record.msg == 'Response Headers %s' and + record.args == {'Content-Type': 'application/json', 'Content-Length': 50} and + record.message == "Response Headers {'Content-Type': 'application/json', 'Content-Length': 50}" + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_response_with_log_response_body(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), response_logging_configuration=self.api_response_logging_configuration(log_body=True)) self._api_logger.log_response(self._api_response) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', 50) and - record.message == 'Response 200 application/json 50', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response Body %s' and - record.args == ('{"Key": "Value"}',) and - record.message == 'Response Body {"Key": "Value"}', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', 50) and + record.message == 'Response 200 application/json 50' + for record in records) + + assert any( + record.msg == 'Response Body %s' and + record.args == ('{"Key": "Value"}',) and + record.message == 'Response Body {"Key": "Value"}' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_end_to_end_api_call_test(self, log_capture: LogCapture): self.execute_api_call_with_logging(logger=MockedApiLogger()) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', None) and - record.message == 'Response 200 application/json None', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', None) and + record.message == 'Response 200 application/json None' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def execute_api_call_with_logging(self, logger: Logger): _api_call_builder = self.new_api_call_builder(self.global_configuration_with_logging(logger)) _api_call_builder.new_builder \ @@ -253,17 +279,3 @@ def execute_api_call_with_logging(self, logger: Logger): .deserializer(ApiHelper.json_deserialize) .deserialize_into(Employee.model_validate)) \ .execute() - - @staticmethod - def any_with_limit(iterable: Iterable, condition: Callable[[List[LogRecord]], bool], max_checks: int): - """Checks if any element in iterable meets the condition, with a limit on checks. - - Args: - iterable: The iterable to check (list, string, etc.) - condition: A function that takes an element and returns True if it meets the criteria. - max_checks: The maximum number of elements to check before returning False (defaults to infinite). - - Returns: - True if any element meets the condition within the check limit, False otherwise. - """ - return sum(1 for element in iterable if condition(element)) == max_checks diff --git a/tests/apimatic_core/api_logger_tests/test_logging_configuration.py b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py index 3e5eac8..5acd5bb 100644 --- a/tests/apimatic_core/api_logger_tests/test_logging_configuration.py +++ b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py @@ -1,11 +1,15 @@ import pytest -from typing import Dict, List, Any +from typing import Dict, List, Any, Optional + +from pydantic import validate_call from apimatic_core.logger.configuration.api_logging_configuration import ApiRequestLoggingConfiguration class TestLogHelper: + @staticmethod + @validate_call def create_sample_headers() -> Dict[str, str]: return { "Accept": "application/json", @@ -15,166 +19,205 @@ def create_sample_headers() -> Dict[str, str]: } @staticmethod + @validate_call def create_sample_request_logging_configuration( - headers_to_include: List[str]=None, headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None + headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, + headers_to_unmask: Optional[List[str]]=None ) -> ApiRequestLoggingConfiguration: return ApiRequestLoggingConfiguration(log_body=False, log_headers=False, include_query_in_path=False, headers_to_include=headers_to_include or [], headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or []) + @validate_call def test_get_headers_to_log_include(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_include=["Authorization"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_get_headers_to_log_exclude(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_exclude=["Authorization"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() + @validate_call def test_get_headers_to_log_include_and_exclude(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_include=["Authorization"], headers_to_exclude=["Accept"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_get_headers_to_log_sensitive_masking_enabled(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) + assert "Authorization" in result.keys() assert "**Redacted**" == result.get("Authorization") + @validate_call def test_get_headers_to_log_sensitive_masking_disabled(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_get_headers_to_log_sensitive_masking_disabled_with_headers_to_unmask(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_unmask=["Authorization"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_get_headers_to_log_sensitive_masking_enabled_with_headers_to_unmask(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_unmask=["Authorization"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_extract_headers_to_log_include(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_include=["Authorization"]) result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_extract_headers_to_log_exclude(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_exclude=["Authorization"]) result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() + @validate_call def test_extract_headers_to_log_no_criteria(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + assert "Authorization" in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() + @validate_call def test_mask_sensitive_headers_masking_enabled(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) + assert "Authorization" in result.keys() assert "**Redacted**" == result.get("Authorization") + @validate_call def test_mask_sensitive_headers_masking_enabled_with_headers_to_unmask(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_unmask=["Authorization"]) result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_mask_sensitive_headers_masking_disable(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_mask_sensitive_headers_masking_disabled_with_headers_to_unmask(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_unmask=["Authorization"]) result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_filter_included_headers(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_include=["Authorization"]) result: Dict[str, Any] = logging_config._filter_included_headers(headers) + assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_filter_included_headers_no_inclusion(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._filter_included_headers(headers) + assert "Authorization" not in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_filter_excluded_headers(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_exclude=["Authorization"]) result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) + assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() + @validate_call def test_filter_excluded_headers_no_exclusion(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) + assert "Authorization" in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() diff --git a/tests/apimatic_core/api_logger_tests/test_logging_configuration.py~ b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py~ new file mode 100644 index 0000000..b5f383c --- /dev/null +++ b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py~ @@ -0,0 +1,223 @@ +import pytest +from typing import Dict, List, Any, Optional + +from pydantic import validate_call + +from apimatic_core.logger.configuration.api_logging_configuration import ApiRequestLoggingConfiguration + + +class TestLogHelper: + + @staticmethod + @validate_call + def create_sample_headers() -> Dict[str, str]: + return { + "Accept": "application/json", + "Authorization": "Bearer token", + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0" + } + + @staticmethod + @validate_call + def create_sample_request_logging_configuration( + headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, + headers_to_unmask: Optional[List[str]]=None + ) -> ApiRequestLoggingConfiguration: + return ApiRequestLoggingConfiguration(log_body=False, log_headers=False, include_query_in_path=False, + headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], + headers_to_unmask=headers_to_unmask or []) + + @validate_call + def test_get_headers_to_log_include(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_get_headers_to_log_exclude(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + + assert "Authorization" not in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() + + @validate_call + def test_get_headers_to_log_include_and_exclude(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"], headers_to_exclude=["Accept"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_get_headers_to_log_sensitive_masking_enabled(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) + + assert "Authorization" in result.keys() + assert "**Redacted**" == result.get("Authorization") + + @validate_call + def test_get_headers_to_log_sensitive_masking_disabled(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_get_headers_to_log_sensitive_masking_disabled_with_headers_to_unmask(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_get_headers_to_log_sensitive_masking_enabled_with_headers_to_unmask(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_extract_headers_to_log_include(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + + assert "Authorization" in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_extract_headers_to_log_exclude(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + + assert "Authorization" not in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() + + @validate_call + def test_extract_headers_to_log_no_criteria(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + + assert "Authorization" in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() + + @validate_call + def test_mask_sensitive_headers_masking_enabled(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) + + assert "Authorization" in result.keys() + assert "**Redacted**" == result.get("Authorization") + + @validate_call + def test_mask_sensitive_headers_masking_enabled_with_headers_to_unmask(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_mask_sensitive_headers_masking_disable(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_mask_sensitive_headers_masking_disabled_with_headers_to_unmask(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_filter_included_headers(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config._filter_included_headers(headers) + + assert "Authorization" in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_filter_included_headers_no_inclusion(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._filter_included_headers(headers) + + assert "Authorization" not in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_filter_excluded_headers(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) + + assert "Authorization" not in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() + + @validate_call + def test_filter_excluded_headers_no_exclusion(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) + + assert "Authorization" in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() diff --git a/tests/apimatic_core/base.py b/tests/apimatic_core/base.py index dae7bb7..3f23234 100644 --- a/tests/apimatic_core/base.py +++ b/tests/apimatic_core/base.py @@ -7,9 +7,10 @@ from apimatic_core_interfaces.http.http_client import HttpClient from apimatic_core_interfaces.http.http_request import HttpRequest from apimatic_core_interfaces.http.http_response import HttpResponse -from typing import Optional, Union, Dict, IO, Any, List +from typing import Optional, Union, Dict, IO, Any, List, BinaryIO from apimatic_core_interfaces.logger.logger import Logger +from pydantic import validate_call from apimatic_core.api_call import ApiCall from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration @@ -48,6 +49,7 @@ class Base: @staticmethod + @validate_call def employee_model() -> Employee: return Employee( name='Bob', uid='1234567', address='street abc', department='IT', @@ -66,6 +68,7 @@ def employee_model() -> Employee: person_type="Empl") @staticmethod + @validate_call def employee_model_str(beautify_with_spaces: bool=True) -> Union[Employee, str]: if beautify_with_spaces: return ('{{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": "{1}", "name": "Bob",' @@ -85,6 +88,7 @@ def employee_model_str(beautify_with_spaces: bool=True) -> Union[Employee, str]: Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) @staticmethod + @validate_call def employee_model_additional_dictionary() -> Employee: return Employee( name='Bob', @@ -124,7 +128,8 @@ def employee_model_additional_dictionary() -> Employee: person_type="Empl") @staticmethod - def get_employee_dictionary() -> Dict[str, Employee]: + @validate_call + def get_employee_dictionary() -> Dict[str, Any]: return { "address": "street abc", "age": 27, @@ -157,20 +162,21 @@ def get_employee_dictionary() -> Dict[str, Employee]: } @staticmethod + @validate_call def get_complex_type() -> ComplexType: inner_complex_type = InnerComplexType( boolean_type=True, long_type=100003, string_type='abc', precision_type=55.44, - string_list_type=['item1', 'item2'], additional_properties={'key0': 'abc', 'key1': 400}) + string_list_type=['item1', 'item2']) return ComplexType( inner_complex_type=inner_complex_type, inner_complex_list_type=[inner_complex_type, inner_complex_type], inner_complex_list_of_map_type=[{'key0': inner_complex_type, 'key1': inner_complex_type}], inner_complex_map_type={'key0': inner_complex_type, 'key1': inner_complex_type}, inner_complex_map_of_list_type={'key0': [inner_complex_type, inner_complex_type], - 'key2': [inner_complex_type, inner_complex_type]}, - additional_properties={'prop1': [1, 2, 3], 'prop2': {'key0': 'abc', 'key1': 'def'}}) + 'key2': [inner_complex_type, inner_complex_type]}) @staticmethod + @validate_call def get_union_type_scalar_model() -> UnionTypeScalarModel: return UnionTypeScalarModel( any_of_required=1.5, @@ -180,41 +186,46 @@ def get_union_type_scalar_model() -> UnionTypeScalarModel: ) @staticmethod + @validate_call def get_serialized_employee() -> str: employee_dictionary = Base.get_employee_dictionary() return json.dumps(employee_dictionary, separators=(',', ':')) @staticmethod + @validate_call def basic_auth() -> BasicAuth: return BasicAuth(BasicAuthCredentials(username='test_username', password='test_password')) @staticmethod + @validate_call def bearer_auth() -> BearerAuth: return BearerAuth(BearerAuthCredentials(access_token='0b79bab50daca910b000d4f1a2b675d604257e42')) @staticmethod + @validate_call def custom_header_auth() -> CustomHeaderAuthentication: return CustomHeaderAuthentication(CustomHeaderAuthenticationCredentials(token='Qaws2W233WedeRe4T56G6Vref2')) @staticmethod + @validate_call def custom_query_auth() -> CustomQueryAuthentication: return CustomQueryAuthentication( CustomQueryAuthenticationCredentials(api_key='W233WedeRe4T56G6Vref2', token='Qaws2W233WedeRe4T56G6Vref2')) @staticmethod + @validate_call def xml_model() -> XMLModel: - model = XMLModel(string_attr='String', number_attr=10000, boolean_attr=False, - string_element='Hey! I am being tested.', number_element=5000, - boolean_element=False, elements=['a', 'b', 'c']) return XMLModel(string_attr='String', number_attr=10000, boolean_attr=False, string_element='Hey! I am being tested.', number_element=5000, boolean_element=False, elements=['a', 'b', 'c']) @staticmethod + @validate_call def one_of_xml_dog_model() -> OneOfXML: return OneOfXML(value=DogModel(barks=True)) @staticmethod + @validate_call def one_of_xml_cat_model() -> OneOfXML: return OneOfXML(value=[ CatModel(meows=True), @@ -222,6 +233,7 @@ def one_of_xml_cat_model() -> OneOfXML: ]) @staticmethod + @validate_call def one_of_xml_wolf_model() -> OneOfXML: return OneOfXML(value=[ WolfModel(howls=True), @@ -229,12 +241,14 @@ def one_of_xml_wolf_model() -> OneOfXML: ]) @staticmethod - def read_file(file_name) -> IO: + @validate_call + def read_file(file_name) -> BinaryIO: real_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) file_path = os.path.join(real_path, 'apimatic_core', 'mocks/files', file_name) return open(file_path, "rb") @staticmethod + @validate_call def global_errors() -> Dict[str, ErrorCase]: return { '400': ErrorCase(message='400 Global', exception_type=GlobalTestException), @@ -244,6 +258,7 @@ def global_errors() -> Dict[str, ErrorCase]: } @staticmethod + @validate_call def global_errors_with_template_message() -> Dict[str, ErrorCase]: return { '400': ErrorCase( @@ -258,10 +273,11 @@ def global_errors_with_template_message() -> Dict[str, ErrorCase]: } @staticmethod + @validate_call def request(http_method: HttpMethodEnum=HttpMethodEnum.GET, query_url: str='http://localhost:3000/test', - headers: Dict[str, Any]=None, - query_parameters: Dict[str, Any]=None, + headers: Optional[Dict[str, Any]]=None, + query_parameters: Optional[Dict[str, Any]]=None, parameters: Any=None, files: Any=None) -> HttpRequest: if headers is None: @@ -277,8 +293,9 @@ def request(http_method: HttpMethodEnum=HttpMethodEnum.GET, files=files) @staticmethod + @validate_call def response( - status_code: int=200, reason_phrase: Optional[str]=None, headers: Dict[str, Any]=None, text: Any=None + status_code: int=200, reason_phrase: Optional[str]=None, headers: Optional[Dict[str, Any]]=None, text: Any=None ) -> HttpResponse: if headers is None: headers = {} @@ -288,14 +305,21 @@ def response( ) @staticmethod + @validate_call def get_http_datetime( datetime_value: datetime, should_return_string: bool=True ) -> Union[ApiHelper.CustomDate, str]: if should_return_string is True: - return ApiHelper.HttpDateTime.from_datetime(datetime_value) + return Base.get_http_datetime_str(datetime_value) return ApiHelper.HttpDateTime(datetime_value) @staticmethod + @validate_call + def get_http_datetime_str(datetime_value: datetime) -> str: + return ApiHelper.HttpDateTime.from_datetime(datetime_value) + + @staticmethod + @validate_call def get_rfc3339_datetime( datetime_value: datetime, should_return_string: bool=True ) -> Union[ApiHelper.CustomDate, str]: @@ -304,14 +328,22 @@ def get_rfc3339_datetime( return ApiHelper.RFC3339DateTime(datetime_value) @staticmethod + @validate_call + def get_rfc3339_datetime_str(datetime_value: datetime) -> str: + return ApiHelper.RFC3339DateTime.from_datetime(datetime_value) + + @staticmethod + @validate_call def new_api_call_builder(global_configuration: GlobalConfiguration) -> ApiCall: return ApiCall(global_configuration) @staticmethod + @validate_call def user_agent() -> str: return 'Python|31.8.0|{engine}|{engine-version}|{os-info}' @staticmethod + @validate_call def user_agent_parameters() -> Dict[str, Dict[str, Any]]: return { 'engine': {'value': platform.python_implementation(), 'encode': False}, @@ -320,6 +352,7 @@ def user_agent_parameters() -> Dict[str, Dict[str, Any]]: } @staticmethod + @validate_call def wrapped_parameters() -> Dict[str, Any]: return { 'bodyScalar': True, @@ -327,10 +360,12 @@ def wrapped_parameters() -> Dict[str, Any]: } @staticmethod + @validate_call def mocked_http_client() -> HttpClient: return MockHttpClient() @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) def http_client_configuration( http_callback: Optional[HttpCallBack]=HttpResponseCatcher(), logging_configuration: Optional[ApiLoggingConfiguration]=None @@ -341,15 +376,18 @@ def http_client_configuration( ) @property + @validate_call def new_request_builder(self) -> RequestBuilder: return RequestBuilder().path('/test') \ .server(Server.DEFAULT) @property + @validate_call def new_response_handler(self) -> ResponseHandler: return ResponseHandler() @property + @validate_call def global_configuration(self) -> GlobalConfiguration: return GlobalConfiguration( http_client_configuration=self.http_client_configuration(), @@ -357,28 +395,33 @@ def global_configuration(self) -> GlobalConfiguration: global_errors=self.global_errors()) @property + @validate_call def global_configuration_without_http_callback(self) -> GlobalConfiguration: return GlobalConfiguration( http_client_configuration=self.http_client_configuration(None), base_uri_executor=BaseUriCallable().get_base_uri) @property + @validate_call def global_configuration_unimplemented_http_callback(self) -> GlobalConfiguration: return GlobalConfiguration( http_client_configuration=self.http_client_configuration(HttpCallBack()), base_uri_executor=BaseUriCallable().get_base_uri) @property + @validate_call def default_global_configuration(self) -> GlobalConfiguration: return GlobalConfiguration() @property + @validate_call def global_configuration_with_useragent(self) -> GlobalConfiguration: global_configuration = self.global_configuration global_configuration.add_user_agent(self.user_agent(), self.user_agent_parameters()) return global_configuration @property + @validate_call def global_configuration_with_auth(self) -> GlobalConfiguration: global_configuration = self.global_configuration global_configuration.auth_managers = { @@ -390,26 +433,29 @@ def global_configuration_with_auth(self) -> GlobalConfiguration: return global_configuration @staticmethod + @validate_call def api_request_logging_configuration( - log_body: bool=False, log_headers: bool=False, headers_to_include: List[str]=None, - headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None, + log_body: bool=False, log_headers: bool=False, headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, headers_to_unmask: Optional[List[str]]=None, include_query_in_path: bool=False ) -> ApiRequestLoggingConfiguration: return ApiRequestLoggingConfiguration( - log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include, - headers_to_exclude=headers_to_exclude, headers_to_unmask=headers_to_unmask, + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or [], include_query_in_path=include_query_in_path) @staticmethod + @validate_call def api_response_logging_configuration( - log_body: bool=False, log_headers: bool=False, headers_to_include: List[str]=None, - headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None + log_body: bool=False, log_headers: bool=False, headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, headers_to_unmask: Optional[List[str]]=None ) -> ApiResponseLoggingConfiguration: return ApiResponseLoggingConfiguration( - log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include, - headers_to_exclude=headers_to_exclude, headers_to_unmask=headers_to_unmask) + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or []) @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) def api_logging_configuration( logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, @@ -427,6 +473,7 @@ def api_logging_configuration( response_logging_config=response_logging_configuration) @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) def global_configuration_with_logging(logger: Logger) -> GlobalConfiguration: return GlobalConfiguration( http_client_configuration=Base.http_client_configuration( @@ -434,6 +481,7 @@ def global_configuration_with_logging(logger: Logger) -> GlobalConfiguration: base_uri_executor=BaseUriCallable().get_base_uri) @property + @validate_call def global_configuration_with_uninitialized_auth_params(self) -> GlobalConfiguration: global_configuration = self.global_configuration global_configuration.auth_managers = { @@ -443,6 +491,7 @@ def global_configuration_with_uninitialized_auth_params(self) -> GlobalConfigura return global_configuration @property + @validate_call def global_configuration_with_partially_initialized_auth_params(self) -> GlobalConfiguration: global_configuration = self.global_configuration global_configuration.auth_managers = { diff --git a/tests/apimatic_core/base.py~ b/tests/apimatic_core/base.py~ new file mode 100644 index 0000000..47b8b8d --- /dev/null +++ b/tests/apimatic_core/base.py~ @@ -0,0 +1,501 @@ +import json +import logging +import os +import platform +from datetime import datetime, date + +from apimatic_core_interfaces.http.http_client import HttpClient +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from typing import Optional, Union, Dict, IO, Any, List, BinaryIO + +from apimatic_core_interfaces.logger.logger import Logger +from pydantic import validate_call + +from apimatic_core.api_call import ApiCall +from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration +from apimatic_core.http.http_callback import HttpCallBack +from apimatic_core.logger.configuration.api_logging_configuration import ApiLoggingConfiguration, \ + ApiRequestLoggingConfiguration, ApiResponseLoggingConfiguration +from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from apimatic_core.configurations.global_configuration import GlobalConfiguration +from apimatic_core.request_builder import RequestBuilder +from apimatic_core.response_handler import ResponseHandler +from apimatic_core.types.error_case import ErrorCase, MessageType +from tests.apimatic_core.mocks.authentications.basic_auth import BasicAuth, BasicAuthCredentials +from tests.apimatic_core.mocks.authentications.bearer_auth import BearerAuth, BearerAuthCredentials +from tests.apimatic_core.mocks.authentications.custom_header_authentication import CustomHeaderAuthentication, \ + CustomHeaderAuthenticationCredentials +from tests.apimatic_core.mocks.authentications.custom_query_authentication import CustomQueryAuthentication, \ + CustomQueryAuthenticationCredentials +from tests.apimatic_core.mocks.exceptions.global_test_exception import GlobalTestException +from tests.apimatic_core.mocks.exceptions.nested_model_exception import NestedModelException +from tests.apimatic_core.mocks.http.http_response_catcher import HttpResponseCatcher +from tests.apimatic_core.mocks.http.http_client import MockHttpClient +from tests.apimatic_core.mocks.models.cat_model import CatModel +from tests.apimatic_core.mocks.models.complex_type import ComplexType +from tests.apimatic_core.mocks.models.dog_model import DogModel +from tests.apimatic_core.mocks.models.inner_complex_type import InnerComplexType +from tests.apimatic_core.mocks.models.one_of_xml import OneOfXML +from tests.apimatic_core.mocks.models.union_type_scalar_model import UnionTypeScalarModel +from tests.apimatic_core.mocks.models.wolf_model import WolfModel +from tests.apimatic_core.mocks.models.xml_model import XMLModel +from tests.apimatic_core.mocks.models.days import Days +from tests.apimatic_core.mocks.models.person import Employee, Person +from tests.apimatic_core.mocks.callables.base_uri_callable import Server, BaseUriCallable + + +class Base: + + @staticmethod + @validate_call + def employee_model() -> Employee: + return Employee( + name='Bob', uid='1234567', address='street abc', department='IT', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=27, + additional_properties={'key1': 'value1', 'key2': 'value2'}, + hired_at=datetime(1994, 2, 13, 5, 30, 15), joining_day=Days.MONDAY, + working_days=[Days.MONDAY, Days.TUESDAY], salary=30000, + dependents=[ + Person(name='John', uid='7654321', address='street abc', birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=12, + additional_properties={ + 'key1': 'value1', + 'key2': 'value2' + }, person_type="Per")], + person_type="Empl") + + @staticmethod + @validate_call + def employee_model_str(beautify_with_spaces: bool=True) -> Union[Employee, str]: + if beautify_with_spaces: + return ('{{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": "{1}", "name": "Bob",' + ' "uid": "1234567", "personType": "Empl", "department": "IT", "dependents": [{{"address": "street abc",' + ' "age": 12, "birthday": "1994-02-13", "birthtime": "{1}", "name": "John", "uid": "7654321",' + ' "personType": "Per", "key1": "value1", "key2": "value2"}}], "hiredAt": "{0}", "joiningDay": "Monday",' + ' "salary": 30000, "workingDays": ["Monday", "Tuesday"], "key1": "value1", "key2": "value2"}}').format( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) + + return ('{{"address":"street abc","age":27,"birthday":"1994-02-13","birthtime":"{1}","name":"Bob",' + '"uid":"1234567","personType":"Empl","department":"IT","dependents":[{{"address":"street abc",' + '"age":12,"birthday":"1994-02-13","birthtime":"{1}","name":"John","uid":"7654321",' + '"personType":"Per","key1":"value1","key2":"value2"}}],"hiredAt":"{0}","joiningDay":"Monday",' + '"salary":30000,"workingDays":["Monday","Tuesday"],"key1":"value1","key2":"value2"}}').format( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) + + @staticmethod + @validate_call + def employee_model_additional_dictionary() -> Employee: + return Employee( + name='Bob', + uid='1234567', + address='street abc', + department='IT', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), + age=27, + additional_properties={ + 'key1': 'value1', + 'key2': 'value2' + }, + hired_at=datetime(1994, 2, 13, 5, 30, 15), + joining_day=Days.MONDAY, + working_days=[ + Days.MONDAY, + Days.TUESDAY + ], + salary=30000, + dependents=[ + Person( + name='John', + uid='7654321', + address='street abc', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=12, + additional_properties={ + 'key1': { + 'inner_key1': 'inner_val1', + 'inner_key2': 'inner_val2' + }, + 'key2': ['value2', 'value3'] + }, + person_type="Per") + ], + person_type="Empl") + + @staticmethod + @validate_call + def get_employee_dictionary() -> Dict[str, Any]: + return { + "address": "street abc", + "age": 27, + "birthday": "1994-02-13", + "birthtime": Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + "name": "Bob", + "uid": '1234567', + "personType": "Empl", + "department": "IT", + "dependents": [ + { + "address": "street abc", + "age": 12, + "birthday": "1994-02-13", + "birthtime": Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + "name": "John", + "uid": '7654321', + "personType": "Per", + "key1": "value1", + "key2": "value2" + } + ], + "hiredAt": Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + "joiningDay": "Monday", + "salary": 30000, + "workingDays": ["Monday", "Tuesday"], + "key1": "value1", + "key2": "value2" + } + + @staticmethod + @validate_call + def get_complex_type() -> ComplexType: + inner_complex_type = InnerComplexType( + boolean_type=True, long_type=100003, string_type='abc', precision_type=55.44, + string_list_type=['item1', 'item2']) + + return ComplexType( + inner_complex_type=inner_complex_type, inner_complex_list_type=[inner_complex_type, inner_complex_type], + inner_complex_list_of_map_type=[{'key0': inner_complex_type, 'key1': inner_complex_type}], + inner_complex_map_type={'key0': inner_complex_type, 'key1': inner_complex_type}, + inner_complex_map_of_list_type={'key0': [inner_complex_type, inner_complex_type], + 'key2': [inner_complex_type, inner_complex_type]}) + + @staticmethod + @validate_call + def get_union_type_scalar_model() -> UnionTypeScalarModel: + return UnionTypeScalarModel( + any_of_required=1.5, + one_of_req_nullable='abc', + one_of_optional=200, + any_of_opt_nullable=True + ) + + @staticmethod + @validate_call + def get_serialized_employee() -> str: + employee_dictionary = Base.get_employee_dictionary() + return json.dumps(employee_dictionary, separators=(',', ':')) + + @staticmethod + @validate_call + def basic_auth() -> BasicAuth: + return BasicAuth(BasicAuthCredentials(username='test_username', password='test_password')) + + @staticmethod + @validate_call + def bearer_auth() -> BearerAuth: + return BearerAuth(BearerAuthCredentials(access_token='0b79bab50daca910b000d4f1a2b675d604257e42')) + + @staticmethod + @validate_call + def custom_header_auth() -> CustomHeaderAuthentication: + return CustomHeaderAuthentication(CustomHeaderAuthenticationCredentials(token='Qaws2W233WedeRe4T56G6Vref2')) + + @staticmethod + @validate_call + def custom_query_auth() -> CustomQueryAuthentication: + return CustomQueryAuthentication( + CustomQueryAuthenticationCredentials(api_key='W233WedeRe4T56G6Vref2', token='Qaws2W233WedeRe4T56G6Vref2')) + + @staticmethod + @validate_call + def xml_model() -> XMLModel: + return XMLModel(string_attr='String', number_attr=10000, boolean_attr=False, + string_element='Hey! I am being tested.', number_element=5000, + boolean_element=False, elements=['a', 'b', 'c']) + + @staticmethod + @validate_call + def one_of_xml_dog_model() -> OneOfXML: + return OneOfXML(value=DogModel(barks=True)) + + @staticmethod + @validate_call + def one_of_xml_cat_model() -> OneOfXML: + return OneOfXML(value=[ + CatModel(meows=True), + CatModel(meows=False) + ]) + + @staticmethod + @validate_call + def one_of_xml_wolf_model() -> OneOfXML: + return OneOfXML(value=[ + WolfModel(howls=True), + WolfModel(howls=False) + ]) + + @staticmethod + @validate_call + def read_file(file_name) -> BinaryIO: + real_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) + file_path = os.path.join(real_path, 'apimatic_core', 'mocks/files', file_name) + return open(file_path, "rb") + + @staticmethod + @validate_call + def global_errors() -> Dict[str, ErrorCase]: + return { + '400': ErrorCase(message='400 Global', exception_type=GlobalTestException), + '412': ErrorCase(message='Precondition Failed', exception_type=NestedModelException), + '3XX': ErrorCase(message='3XX Global', exception_type=GlobalTestException), + 'default': ErrorCase(message='Invalid response', exception_type=GlobalTestException), + } + + @staticmethod + @validate_call + def global_errors_with_template_message() -> Dict[str, ErrorCase]: + return { + '400': ErrorCase( + message='error_code => {$statusCode}, header => {$response.header.accept}, ' + 'body => {$response.body#/ServerCode} - {$response.body#/ServerMessage}', + message_type=MessageType.TEMPLATE, exception_type=GlobalTestException), + '412': ErrorCase( + message='global error message -> error_code => {$statusCode}, header => ' + '{$response.header.accept}, body => {$response.body#/ServerCode} - ' + '{$response.body#/ServerMessage} - {$response.body#/model/name}', + message_type=MessageType.TEMPLATE, exception_type=NestedModelException) + } + + @staticmethod + @validate_call + def request(http_method: HttpMethodEnum=HttpMethodEnum.GET, + query_url: str='http://localhost:3000/test', + headers: Dict[str, Any]=None, + query_parameters: Optional[Dict[str, Any]]=None, + parameters: Any=None, + files: Any=None) -> HttpRequest: + if headers is None: + headers = {} + if query_parameters is None: + query_parameters = {} + + return HttpRequest(http_method=http_method, + query_url=query_url, + headers=headers, + query_parameters=query_parameters, + parameters=parameters, + files=files) + + @staticmethod + @validate_call + def response( + status_code: int=200, reason_phrase: Optional[str]=None, headers: Optional[Dict[str, Any]]=None, text: Any=None + ) -> HttpResponse: + if headers is None: + headers = {} + return HttpResponse( + status_code=status_code, reason_phrase=reason_phrase, + headers=headers, text=text, request=Base.request() + ) + + @staticmethod + @validate_call + def get_http_datetime( + datetime_value: datetime, should_return_string: bool=True + ) -> Union[ApiHelper.CustomDate, str]: + if should_return_string is True: + return Base.get_http_datetime_str(datetime_value) + return ApiHelper.HttpDateTime(datetime_value) + + @staticmethod + @validate_call + def get_http_datetime_str(datetime_value: datetime) -> str: + return ApiHelper.HttpDateTime.from_datetime(datetime_value) + + @staticmethod + @validate_call + def get_rfc3339_datetime( + datetime_value: datetime, should_return_string: bool=True + ) -> Union[ApiHelper.CustomDate, str]: + if should_return_string is True: + return ApiHelper.RFC3339DateTime.from_datetime(datetime_value) + return ApiHelper.RFC3339DateTime(datetime_value) + + @staticmethod + @validate_call + def get_rfc3339_datetime_str(datetime_value: datetime) -> str: + return ApiHelper.RFC3339DateTime.from_datetime(datetime_value) + + @staticmethod + @validate_call + def new_api_call_builder(global_configuration: GlobalConfiguration) -> ApiCall: + return ApiCall(global_configuration) + + @staticmethod + @validate_call + def user_agent() -> str: + return 'Python|31.8.0|{engine}|{engine-version}|{os-info}' + + @staticmethod + @validate_call + def user_agent_parameters() -> Dict[str, Dict[str, Any]]: + return { + 'engine': {'value': platform.python_implementation(), 'encode': False}, + 'engine-version': {'value': "", 'encode': False}, + 'os-info': {'value': platform.system(), 'encode': False}, + } + + @staticmethod + @validate_call + def wrapped_parameters() -> Dict[str, Any]: + return { + 'bodyScalar': True, + 'bodyNonScalar': Base.employee_model(), + } + + @staticmethod + @validate_call + def mocked_http_client() -> HttpClient: + return MockHttpClient() + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def http_client_configuration( + http_callback: Optional[HttpCallBack]=HttpResponseCatcher(), + logging_configuration: Optional[ApiLoggingConfiguration]=None + ) -> HttpClientConfiguration: + return HttpClientConfiguration( + http_callback=http_callback, logging_configuration=logging_configuration, + http_client=Base.mocked_http_client() + ) + + @property + @validate_call + def new_request_builder(self) -> RequestBuilder: + return RequestBuilder().path('/test') \ + .server(Server.DEFAULT) + + @property + @validate_call + def new_response_handler(self) -> ResponseHandler: + return ResponseHandler() + + @property + @validate_call + def global_configuration(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(), + base_uri_executor=BaseUriCallable().get_base_uri, + global_errors=self.global_errors()) + + @property + @validate_call + def global_configuration_without_http_callback(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(None), + base_uri_executor=BaseUriCallable().get_base_uri) + + @property + @validate_call + def global_configuration_unimplemented_http_callback(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(HttpCallBack()), + base_uri_executor=BaseUriCallable().get_base_uri) + + @property + @validate_call + def default_global_configuration(self) -> GlobalConfiguration: + return GlobalConfiguration() + + @property + @validate_call + def global_configuration_with_useragent(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.add_user_agent(self.user_agent(), self.user_agent_parameters()) + return global_configuration + + @property + @validate_call + def global_configuration_with_auth(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': self.basic_auth(), + 'bearer_auth': self.bearer_auth(), + 'custom_header_auth': self.custom_header_auth(), + 'custom_query_auth': self.custom_query_auth() + } + return global_configuration + + @staticmethod + @validate_call + def api_request_logging_configuration( + log_body: bool=False, log_headers: bool=False, headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, headers_to_unmask: Optional[List[str]]=None, + include_query_in_path: bool=False + ) -> ApiRequestLoggingConfiguration: + return ApiRequestLoggingConfiguration( + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or [], + include_query_in_path=include_query_in_path) + + @staticmethod + @validate_call + def api_response_logging_configuration( + log_body: bool=False, log_headers: bool=False, headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, headers_to_unmask: Optional[List[str]]=None + ) -> ApiResponseLoggingConfiguration: + return ApiResponseLoggingConfiguration( + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or []) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def api_logging_configuration( + logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, + request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, + response_logging_configuration: Optional[ApiResponseLoggingConfiguration]=None + ) -> ApiLoggingConfiguration: + if request_logging_configuration is None: + request_logging_configuration = Base.api_request_logging_configuration() + + if response_logging_configuration is None: + response_logging_configuration = Base.api_response_logging_configuration() + + return ApiLoggingConfiguration( + logger=logger, log_level=log_level, mask_sensitive_headers=mask_sensitive_headers, + request_logging_config=request_logging_configuration, + response_logging_config=response_logging_configuration) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def global_configuration_with_logging(logger: Logger) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=Base.http_client_configuration( + logging_configuration=Base.api_logging_configuration(logger)), + base_uri_executor=BaseUriCallable().get_base_uri) + + @property + @validate_call + def global_configuration_with_uninitialized_auth_params(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': BasicAuth(None), 'bearer_auth': BearerAuth(None), + 'custom_header_auth': CustomHeaderAuthentication(None) + } + return global_configuration + + @property + @validate_call + def global_configuration_with_partially_initialized_auth_params(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': BasicAuth(None), + 'custom_header_auth': self.custom_header_auth() + } + return global_configuration diff --git a/tests/apimatic_core/mocks/callables/base_uri_callable.py b/tests/apimatic_core/mocks/callables/base_uri_callable.py index f46b61d..5c17e41 100644 --- a/tests/apimatic_core/mocks/callables/base_uri_callable.py +++ b/tests/apimatic_core/mocks/callables/base_uri_callable.py @@ -1,6 +1,6 @@ from enum import Enum -from typing import Dict +from typing import Dict, Any from apimatic_core.utilities.api_helper import ApiHelper @@ -35,7 +35,7 @@ def get_base_uri(self, server: Server=Server.DEFAULT): String: The base URI. """ - parameters = {} + parameters: Dict[str, Dict[str, Any]] = {} return ApiHelper.append_url_with_template_parameters( self.environments[Environment.TESTING][server], parameters diff --git a/tests/apimatic_core/mocks/callables/base_uri_callable.py~ b/tests/apimatic_core/mocks/callables/base_uri_callable.py~ new file mode 100644 index 0000000..0cef368 --- /dev/null +++ b/tests/apimatic_core/mocks/callables/base_uri_callable.py~ @@ -0,0 +1,42 @@ +from enum import Enum + +from typing import Dict + +from apimatic_core.utilities.api_helper import ApiHelper + + +class Environment(int, Enum): + TESTING = 1 + + +class Server(int, Enum): + """An enum for API servers""" + DEFAULT = 0 + AUTH_SERVER = 1 + + +class BaseUriCallable: + environments: Dict[Environment, Dict[Server, str]] = { + Environment.TESTING: { + Server.DEFAULT: 'http://localhost:3000', + Server.AUTH_SERVER: 'http://authserver:5000' + } + } + + def get_base_uri(self, server: Server=Server.DEFAULT): + """Generates the appropriate base URI for the environment and the + server. + + Args: + server (Configuration.Server): The server enum for which the base + URI is required. + + Returns: + String: The base URI. + + """ + parameters: Optional[Dict[str, Dict[str, Any]]] = {} + + return ApiHelper.append_url_with_template_parameters( + self.environments[Environment.TESTING][server], parameters + ) diff --git a/tests/apimatic_core/mocks/exceptions/nested_model_exception.py b/tests/apimatic_core/mocks/exceptions/nested_model_exception.py index c1a05d9..37bec36 100644 --- a/tests/apimatic_core/mocks/exceptions/nested_model_exception.py +++ b/tests/apimatic_core/mocks/exceptions/nested_model_exception.py @@ -29,7 +29,7 @@ class NestedModelExceptionValidator(BaseModel): class NestedModelException(APIException): server_message: Optional[str] = None server_code: Optional[int] = None - model: Optional[str] = None + model: Optional[Validate] = None def __init__(self, reason: str, response: HttpResponse): """Constructor for the NestedModelException class diff --git a/tests/apimatic_core/mocks/http/http_client.py b/tests/apimatic_core/mocks/http/http_client.py index 0280780..ea740a2 100644 --- a/tests/apimatic_core/mocks/http/http_client.py +++ b/tests/apimatic_core/mocks/http/http_client.py @@ -1,3 +1,5 @@ +from typing import Any + from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration from apimatic_core_interfaces.http.http_client import HttpClient from apimatic_core_interfaces.http.http_request import HttpRequest @@ -11,7 +13,7 @@ class MockHttpClient(HttpClient): contains_binary_response: bool = False @validate_call - def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfiguration = None) -> HttpResponse: + def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfiguration) -> HttpResponse: """Execute a given CoreHttpRequest to get a string response back Args: @@ -24,16 +26,11 @@ def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfigur """ self.should_retry = endpoint_configuration.should_retry self.contains_binary_response = endpoint_configuration.has_binary_response - return HttpResponse( - status_code=200, - reason_phrase=None, - headers=request.headers, - text=str(request.parameters), - request=request) + return self.convert_response(None, self.contains_binary_response, request) @validate_call def convert_response( - self, response: HttpResponse, contains_binary_response: bool, http_request: HttpRequest + self, response: Any, contains_binary_response: bool, request: HttpRequest ) -> HttpResponse: """Converts the Response object of the CoreHttpClient into an CoreHttpResponse object. @@ -41,10 +38,15 @@ def convert_response( Args: response (dynamic): The original response object. contains_binary_response (bool): The flag to check if the response is of binary type. - http_request (HttpRequest): The original HttpRequest object. + request (HttpRequest): The original HttpRequest object. Returns: CoreHttpResponse: The converted CoreHttpResponse object. """ - pass + return HttpResponse( + status_code=200, + reason_phrase=None, + headers=request.headers, + text=str(request.parameters), + request=request) diff --git a/tests/apimatic_core/mocks/http/http_client.py~ b/tests/apimatic_core/mocks/http/http_client.py~ new file mode 100644 index 0000000..3044b6c --- /dev/null +++ b/tests/apimatic_core/mocks/http/http_client.py~ @@ -0,0 +1,52 @@ +from typing import Any + +from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration +from apimatic_core_interfaces.http.http_client import HttpClient +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call + + +class MockHttpClient(HttpClient): + + should_retry: bool = False + contains_binary_response: bool = False + + @validate_call + def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfiguration = None) -> HttpResponse: + """Execute a given CoreHttpRequest to get a string response back + + Args: + request (HttpRequest): The given HttpRequest to execute. + endpoint_configuration (EndpointConfiguration): The endpoint configurations to use. + + Returns: + HttpResponse: The response of the CoreHttpRequest. + + """ + self.should_retry = endpoint_configuration.should_retry + self.contains_binary_response = endpoint_configuration.has_binary_response + return self.convert_response(None, self.contains_binary_response, request) + + @validate_call + def convert_response( + self, response: Any, contains_binary_response: bool, request: HttpRequest + ) -> HttpResponse: + """Converts the Response object of the CoreHttpClient into an + CoreHttpResponse object. + + Args: + response (dynamic): The original response object. + contains_binary_response (bool): The flag to check if the response is of binary type. + request (HttpRequest): The original HttpRequest object. + + Returns: + CoreHttpResponse: The converted CoreHttpResponse object. + + """ + return HttpResponse( + status_code=200, + reason_phrase=None, + headers=request.headers, + text=str(request.parameters), + request=request) diff --git a/tests/apimatic_core/mocks/http/http_response_catcher.py b/tests/apimatic_core/mocks/http/http_response_catcher.py index 6d94b6a..f4b3a20 100644 --- a/tests/apimatic_core/mocks/http/http_response_catcher.py +++ b/tests/apimatic_core/mocks/http/http_response_catcher.py @@ -1,3 +1,5 @@ +from typing import Optional + from apimatic_core_interfaces.http.http_request import HttpRequest from apimatic_core_interfaces.http.http_response import HttpResponse from pydantic import validate_call @@ -14,7 +16,7 @@ class HttpResponseCatcher(HttpCallBack): after a request is executed. """ - response: HttpResponse = None + response: Optional[HttpResponse] = None @validate_call def on_before_request(self, request: HttpRequest): diff --git a/tests/apimatic_core/mocks/http/http_response_catcher.py~ b/tests/apimatic_core/mocks/http/http_response_catcher.py~ new file mode 100644 index 0000000..97d2585 --- /dev/null +++ b/tests/apimatic_core/mocks/http/http_response_catcher.py~ @@ -0,0 +1,28 @@ +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call + +from apimatic_core.http.http_callback import HttpCallBack + + +class HttpResponseCatcher(HttpCallBack): + + """A class used for catching the HttpResponse object from controllers. + + This class inherits HttpCallBack and implements the on_after_response + method to catch the HttpResponse object as returned by the HttpClient + after a request is executed. + + """ + response: Optional[HttpResponse] = None + + @validate_call + def on_before_request(self, request: HttpRequest): + pass + + @validate_call + def on_after_response(self, response: HttpResponse): + self.response = response + + + diff --git a/tests/apimatic_core/mocks/models/complex_type.py b/tests/apimatic_core/mocks/models/complex_type.py index a459083..43ca6bd 100644 --- a/tests/apimatic_core/mocks/models/complex_type.py +++ b/tests/apimatic_core/mocks/models/complex_type.py @@ -34,12 +34,12 @@ class ComplexType(BaseModel): serialization_alias="innerComplexMapType") ] = None inner_complex_list_of_map_type: Annotated[ - Optional[List[InnerComplexType]], + Optional[List[Dict[str, InnerComplexType]]], Field(validation_alias=AliasChoices("inner_complex_list_of_map_type", "innerComplexListOfMapType"), serialization_alias="innerComplexListOfMapType") ] = None inner_complex_map_of_list_type: Annotated[ - Optional[List[InnerComplexType]], + Optional[Dict[str, List[InnerComplexType]]], Field(validation_alias=AliasChoices("inner_complex_map_of_list_type", "innerComplexMapOfListType"), serialization_alias="innerComplexMapOfListType") ] = None diff --git a/tests/apimatic_core/mocks/models/complex_type.py~ b/tests/apimatic_core/mocks/models/complex_type.py~ new file mode 100644 index 0000000..5242cf0 --- /dev/null +++ b/tests/apimatic_core/mocks/models/complex_type.py~ @@ -0,0 +1,55 @@ +from typing import Optional, List, Dict +from pydantic import BaseModel, Field, model_serializer, AliasChoices +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from apimatic_core.utilities.api_helper import ApiHelper +from typing_extensions import Annotated + +from tests.apimatic_core.mocks.models.inner_complex_type import InnerComplexType + + +class ComplexType(BaseModel): + """Implementation of the 'ComplexType' model using Pydantic. + + Attributes: + inner_complex_type (InnerComplexType): TODO: type description here. + inner_complex_list_type (list[InnerComplexType]): TODO: type description here. + inner_complex_map_type (Optional[dict]): TODO: type description here. + inner_complex_list_of_map_type (Optional[list[InnerComplexType]]): TODO: type description here. + inner_complex_map_of_list_type (Optional[list[InnerComplexType]]): TODO: type description here. + """ + + inner_complex_type: Annotated[ + InnerComplexType, + Field(validation_alias=AliasChoices("inner_complex_type", "innerComplexType"), + serialization_alias="innerComplexType") + ] + inner_complex_list_type: Annotated[ + List[InnerComplexType], + Field(validation_alias=AliasChoices("inner_complex_list_type", "innerComplexListType"), + serialization_alias="innerComplexListType") + ] + inner_complex_map_type: Annotated[ + Optional[Dict], + Field(validation_alias=AliasChoices("inner_complex_map_type", "innerComplexMapType"), + serialization_alias="innerComplexMapType") + ] = None + inner_complex_list_of_map_type: Annotated[ + Optional[List[Dict[str, InnerComplexType]]], + Field(validation_alias=AliasChoices("inner_complex_list_of_map_type", "innerComplexListOfMapType"), + serialization_alias="innerComplexListOfMapType") + ] = None + inner_complex_map_of_list_type: Annotated[ + Optional[List[InnerComplexType]], + Field(validation_alias=AliasChoices("inner_complex_map_of_list_type", "innerComplexMapOfListType"), + serialization_alias="innerComplexMapOfListType") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = { + "inner_complex_map_type", + "inner_complex_list_of_map_type", + "inner_complex_map_of_list_type" + } + return ApiHelper.sanitize_model(optional_fields=_optional_fields, model_dump=nxt(self), + model_fields=self.model_fields, model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/request_builder_tests/test_request_builder.py b/tests/apimatic_core/request_builder_tests/test_request_builder.py index 3fc8c24..6a77b4d 100644 --- a/tests/apimatic_core/request_builder_tests/test_request_builder.py +++ b/tests/apimatic_core/request_builder_tests/test_request_builder.py @@ -1,14 +1,19 @@ from datetime import datetime, date +from typing import Any, Dict, Optional, List, Tuple + import pytest import sys +from apimatic_core_interfaces.authentication.authentication import Authentication +from pydantic import validate_call + from apimatic_core.exceptions.auth_validation_exception import AuthValidationException from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum from apimatic_core.authentication.multiple.and_auth_group import And from apimatic_core.authentication.multiple.or_auth_group import Or from apimatic_core.authentication.multiple.single_auth import Single from apimatic_core.types.array_serialization_format import SerializationFormats -from apimatic_core.types.file_wrapper import FileWrapper +from apimatic_core.types.file_wrapper import FileWrapper, FileType from apimatic_core.types.parameter import Parameter from apimatic_core.types.xml_attributes import XmlAttributes from apimatic_core.utilities.api_helper import ApiHelper @@ -16,7 +21,10 @@ from apimatic_core.utilities.xml_helper import XmlHelper from tests.apimatic_core.base import Base from tests.apimatic_core.mocks.callables.base_uri_callable import Server -from requests.utils import quote +from urllib.parse import quote + +from tests.apimatic_core.mocks.models.person import Employee +from tests.apimatic_core.mocks.models.xml_model import XMLModel from tests.apimatic_core.mocks.union_type_lookup import UnionTypeLookUp @@ -26,30 +34,38 @@ class TestRequestBuilder(Base): (Server.DEFAULT, 'http://localhost:3000/'), (Server.AUTH_SERVER, 'http://authserver:5000/') ]) - def test_base_uri(self, input_server, expected_base_uri): + @validate_call + def test_base_uri(self, input_server: Server, expected_base_uri: str): http_request = (self.new_request_builder .server(input_server) .http_method(HttpMethodEnum.POST) .path('/') .build(self.global_configuration)) + assert http_request.query_url == expected_base_uri + @validate_call def test_path(self): http_request = self.new_request_builder.http_method(HttpMethodEnum.POST).build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/test' + @validate_call def test_required_param(self): with pytest.raises(ValueError) as validation_error: self.new_request_builder \ .query_param(Parameter(key='query_param', value=None, is_required=True)) \ .build(self.global_configuration) + assert validation_error.value.args[0] == 'Required parameter query_param cannot be None.' + @validate_call def test_optional_param(self): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .query_param(Parameter(key='query_param', value=None, is_required=False)) \ .build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/test' @pytest.mark.parametrize('input_http_method, expected_http_method', [ @@ -59,10 +75,12 @@ def test_optional_param(self): (HttpMethodEnum.DELETE, HttpMethodEnum.DELETE), (HttpMethodEnum.GET, HttpMethodEnum.GET), ]) - def test_http_method(self, input_http_method, expected_http_method): + @validate_call + def test_http_method(self, input_http_method: HttpMethodEnum, expected_http_method: HttpMethodEnum): http_request = self.new_request_builder \ .http_method(input_http_method) \ .build(self.global_configuration) + assert http_request.http_method == expected_http_method @pytest.mark.parametrize('input_template_param_value, expected_template_param_value, should_encode', [ @@ -81,14 +99,17 @@ def test_http_method(self, input_http_method, expected_http_method): ('Basic%Test', 'Basic%Test', False), ('Basic|Test', 'Basic|Test', False), ]) - def test_template_params_with_encoding(self, input_template_param_value, expected_template_param_value, - should_encode): + @validate_call + def test_template_params_with_encoding( + self, input_template_param_value: str, expected_template_param_value: str, should_encode: bool + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .path('/{template_param}') \ .template_param(Parameter(key='template_param', value=input_template_param_value, should_encode=should_encode)) \ .build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/{}'.format(expected_template_param_value) @pytest.mark.parametrize('input_query_param_value, expected_query_param_value, array_serialization_format', [ @@ -99,10 +120,10 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), 'query_param=761117415', SerializationFormats.INDEXED), (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), - 'query_param={}'.format(quote(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + 'query_param={}'.format(quote(Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), SerializationFormats.INDEXED), (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - 'query_param={}'.format(quote(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + 'query_param={}'.format(quote(Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), SerializationFormats.INDEXED), ([1, 2, 3, 4], 'query_param[0]=1&query_param[1]=2&query_param[2]=3&query_param[3]=4', SerializationFormats.INDEXED), @@ -133,7 +154,7 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte '&query_param[age]=27' '&query_param[birthday]=1994-02-13' '&query_param[birthtime]={}'.format(quote( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[name]=Bob' '&query_param[uid]=1234567' '&query_param[personType]=Empl' @@ -142,14 +163,14 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte '&query_param[dependents][0][age]=12' '&query_param[dependents][0][birthday]=1994-02-13' '&query_param[dependents][0][birthtime]={}'.format(quote( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[dependents][0][name]=John' '&query_param[dependents][0][uid]=7654321' '&query_param[dependents][0][personType]=Per' '&query_param[dependents][0][key1]=value1' '&query_param[dependents][0][key2]=value2' '&query_param[hiredAt]={}'.format(quote( - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[joiningDay]=Monday' '&query_param[salary]=30000' '&query_param[workingDays][0]=Monday' @@ -157,22 +178,31 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte '&query_param[key1]=value1' '&query_param[key2]=value2', SerializationFormats.INDEXED) ]) - def test_query_params(self, input_query_param_value, expected_query_param_value, array_serialization_format): + @validate_call + def test_query_params( + self, input_query_param_value: Any, expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .query_param(Parameter(key='query_param', value=input_query_param_value)) \ .array_serialization_format(array_serialization_format) \ .build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_query_param_value) @pytest.mark.parametrize('input_additional_query_params_value, expected_additional_query_params_value', [ ({'key1': 'value1', 'key2': 'value2'}, 'key1=value1&key2=value2') ]) - def test_additional_query_params(self, input_additional_query_params_value, expected_additional_query_params_value): + @validate_call + def test_additional_query_params( + self, input_additional_query_params_value: Dict[str, str], expected_additional_query_params_value: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .additional_query_params(input_additional_query_params_value) \ .build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_additional_query_params_value) @pytest.mark.parametrize('input_local_header_param_value, expected_local_header_param_value', [ @@ -188,11 +218,15 @@ def test_additional_query_params(self, input_additional_query_params_value, expe {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), ([1, 2, 3, 4], {'header_param': [1, 2, 3, 4]}) ]) - def test_local_headers(self, input_local_header_param_value, expected_local_header_param_value): + @validate_call + def test_local_headers( + self, input_local_header_param_value: Any, expected_local_header_param_value: Dict[str, Any] + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ .build(self.global_configuration) + assert http_request.headers == expected_local_header_param_value @pytest.mark.parametrize('input_global_header_param_value, expected_global_header_param_value', [ @@ -208,12 +242,16 @@ def test_local_headers(self, input_local_header_param_value, expected_local_head {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) ]) - def test_global_headers(self, input_global_header_param_value, expected_global_header_param_value): + @validate_call + def test_global_headers( + self, input_global_header_param_value: Any, expected_global_header_param_value: Dict[str, Any] + ): global_configuration = self.global_configuration global_configuration.global_headers = {'header_param': input_global_header_param_value} http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .build(global_configuration) + assert http_request.headers == expected_global_header_param_value @pytest.mark.parametrize('input_additional_header_param_value, expected_additional_header_param_value', [ @@ -229,12 +267,16 @@ def test_global_headers(self, input_global_header_param_value, expected_global_h {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) ]) - def test_additional_headers(self, input_additional_header_param_value, expected_additional_header_param_value): + @validate_call + def test_additional_headers( + self, input_additional_header_param_value: Any, expected_additional_header_param_value: Dict[str, Any] + ): global_configuration = self.global_configuration global_configuration.additional_headers = {'header_param': input_additional_header_param_value} http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .build(global_configuration) + assert http_request.headers == expected_additional_header_param_value @pytest.mark.parametrize('input_global_header_param_value,' @@ -243,14 +285,18 @@ def test_additional_headers(self, input_additional_header_param_value, expected_ ('global_string', None, {'header_param': None}), ('global_string', 'local_string', {'header_param': 'local_string'}) ]) - def test_local_and_global_headers_precedence(self, input_global_header_param_value, input_local_header_param_value, - expected_header_param_value): + @validate_call + def test_local_and_global_headers_precedence( + self, input_global_header_param_value: str, input_local_header_param_value: Optional[str], + expected_header_param_value: Dict[str, Any] + ): global_configuration = self.global_configuration global_configuration.global_headers = {'header_param': input_global_header_param_value} http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ .build(global_configuration) + assert http_request.headers == expected_header_param_value @pytest.mark.parametrize('input_global_header_param_value,' @@ -262,8 +308,11 @@ def test_local_and_global_headers_precedence(self, input_global_header_param_val ('global_string', 'local_string', None, {'header_param': None}) ]) - def test_all_headers_precedence(self, input_global_header_param_value, input_local_header_param_value, - input_additional_header_param_value, expected_header_param_value): + @validate_call + def test_all_headers_precedence( + self, input_global_header_param_value: str, input_local_header_param_value: str, + input_additional_header_param_value: Optional[str], expected_header_param_value: Dict[str, Any] + ): global_configuration = self.global_configuration global_configuration.global_headers = {'header_param': input_global_header_param_value} global_configuration.additional_headers = {'header_param': input_additional_header_param_value} @@ -271,18 +320,20 @@ def test_all_headers_precedence(self, input_global_header_param_value, input_loc .http_method(HttpMethodEnum.GET) \ .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ .build(global_configuration) + assert http_request.headers == expected_header_param_value + @validate_call def test_useragent_header(self): - engines = ['CPython', 'Jython', 'JPython', 'IronPython', 'PyPy', 'RubyPython', 'AnacondaPython'] - operating_systems = ['Linux', 'Windows', 'Darwin', 'FreeBSD', 'OpenBSD', 'macOS'] + engines: List[str] = ['CPython', 'Jython', 'JPython', 'IronPython', 'PyPy', 'RubyPython', 'AnacondaPython'] + operating_systems: List[str] = ['Linux', 'Windows', 'Darwin', 'FreeBSD', 'OpenBSD', 'macOS'] http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .build(self.global_configuration_with_useragent) [lang, version, engine, _, osInfo] = http_request.headers['user-agent'].split('|') - assert lang == 'Python' and version == '31.8.0' \ - and engine in engines and osInfo in operating_systems + + assert lang == 'Python' and version == '31.8.0' and engine in engines and osInfo in operating_systems @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ ('string', [('form_param', 'string')], SerializationFormats.INDEXED), @@ -327,7 +378,11 @@ def test_useragent_header(self): ('form_param[workingDays][1]', 'Tuesday'), ('form_param[key1]', 'value1'), ('form_param[key2]', 'value2')], SerializationFormats.INDEXED) ]) - def test_form_params(self, input_form_param_value, expected_form_param_value, array_serialization_format): + @validate_call + def test_form_params( + self, input_form_param_value: Any, expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .form_param(Parameter(key='form_param', value=input_form_param_value)) \ @@ -339,11 +394,16 @@ def test_form_params(self, input_form_param_value, expected_form_param_value, ar @pytest.mark.parametrize('input_additional_form_param_value, expected_additional_form_param_value', [ ({'key1': 'value1', 'key2': 'value2'}, [('key1', 'value1'), ('key2', 'value2')]) ]) - def test_addition_form_params(self, input_additional_form_param_value, expected_additional_form_param_value): + @validate_call + def test_addition_form_params( + self, input_additional_form_param_value: Dict[str, str], + expected_additional_form_param_value: List[Tuple[str, str]] + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .additional_form_params(input_additional_form_param_value) \ .build(self.global_configuration) + assert http_request.parameters == expected_additional_form_param_value @pytest.mark.parametrize('input_form_param_value,' @@ -355,13 +415,17 @@ def test_addition_form_params(self, input_additional_form_param_value, expected_ ('additional_key1', 'additional_value1'), ('additional_key2', 'additional_value2')]) ]) - def test_form_params_with_additional_form_params(self, input_form_param_value, input_additional_form_param_value, - expected_form_param_value): + @validate_call + def test_form_params_with_additional_form_params( + self, input_form_param_value: Dict[str, str], input_additional_form_param_value: Dict[str, str], + expected_form_param_value: List[Tuple[str, str]] + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .form_param(Parameter(key='form_param', value=input_form_param_value)) \ .additional_form_params(input_additional_form_param_value) \ .build(self.global_configuration) + assert http_request.parameters == expected_form_param_value @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ @@ -375,11 +439,13 @@ def test_form_params_with_additional_form_params(self, input_form_param_value, i (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) ]) - def test_json_body_params_without_serializer(self, input_body_param_value, expected_body_param_value): + @validate_call + def test_json_body_params_without_serializer(self, input_body_param_value: Any, expected_body_param_value: Any): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .body_param(Parameter(value=input_body_param_value)) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_body_param_value1, input_body_param_value2, expected_body_param_value', [ @@ -387,14 +453,16 @@ def test_json_body_params_without_serializer(self, input_body_param_value, expec (100, 200, '{"param1": 100, "param2": 200}'), (100.12, 200.12, '{"param1": 100.12, "param2": 200.12}') ]) - def test_multiple_json_body_params_with_serializer(self, input_body_param_value1, input_body_param_value2, - expected_body_param_value): + @validate_call + def test_multiple_json_body_params_with_serializer(self, input_body_param_value1: Any, input_body_param_value2: Any, + expected_body_param_value: str): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .body_param(Parameter(key='param1', value=input_body_param_value1)) \ .body_param(Parameter(key='param2', value=input_body_param_value2)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ @@ -405,19 +473,22 @@ def test_multiple_json_body_params_with_serializer(self, input_body_param_value1 '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), (Base.employee_model(), Base.get_serialized_employee()) ]) - def test_json_body_params_with_serializer(self, input_body_param_value, expected_body_param_value): + @validate_call + def test_json_body_params_with_serializer(self, input_body_param_value: Any, expected_body_param_value: str): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .body_param(Parameter(value=input_body_param_value)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_value, expected_value', [ (100.0, '100.0'), (True, 'true') ]) - def test_type_combinator_validation_in_request(self, input_value, expected_value): + @validate_call + def test_type_combinator_validation_in_request(self, input_value: Any, expected_value: str): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .body_param(Parameter( @@ -425,6 +496,7 @@ def test_type_combinator_validation_in_request(self, input_value, expected_value value=input_value)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) + assert http_request.parameters == expected_value @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ @@ -439,7 +511,8 @@ def test_type_combinator_validation_in_request(self, input_value, expected_value '' '') ]) - def test_xml_body_param_with_serializer(self, input_body_param_value, expected_body_param_value): + @validate_call + def test_xml_body_param_with_serializer(self, input_body_param_value: XMLModel, expected_body_param_value: str): if sys.version_info[1] == 7: expected_body_param_value = expected_body_param_value.replace( 'string="String" number="10000" boolean="false">', @@ -449,6 +522,7 @@ def test_xml_body_param_with_serializer(self, input_body_param_value, expected_b .xml_attributes(XmlAttributes(value=input_body_param_value, root_element_name='AttributesAndElements')) \ .body_serializer(XmlHelper.serialize_to_xml) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ @@ -476,7 +550,10 @@ def test_xml_body_param_with_serializer(self, input_body_param_value, expected_b '' '') ]) - def test_xml_array_body_param_with_serializer(self, input_body_param_value, expected_body_param_value): + @validate_call + def test_xml_array_body_param_with_serializer( + self, input_body_param_value: List[XMLModel], expected_body_param_value: str + ): if sys.version_info[1] == 7: expected_body_param_value = expected_body_param_value.replace( 'string="String" number="10000" boolean="false">', @@ -488,12 +565,16 @@ def test_xml_array_body_param_with_serializer(self, input_body_param_value, expe array_item_name='item')) \ .body_serializer(XmlHelper.serialize_list_to_xml) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize("input_body_param_value, expected_input_file, expected_content_type", [ (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), Base.read_file('apimatic.png'), 'image/png')]) - def test_file_as_body_param(self, input_body_param_value, expected_input_file, expected_content_type): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_file_as_body_param( + self, input_body_param_value: FileWrapper, expected_input_file: FileType, expected_content_type: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .header_param(Parameter(key='content-type', value='application/xml')) \ @@ -506,23 +587,24 @@ def test_file_as_body_param(self, input_body_param_value, expected_input_file, e assert input_file.read() == expected_input_file.read() \ and http_request.headers['content-type'] == expected_content_type finally: - input_file.close() - expected_input_file.close() + self.close_file(input_file) + self.close_file(expected_input_file) @pytest.mark.parametrize( - "input_multipart_param_value1, input_default_content_type1,input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type, expected_model, expected_model_content_type", [ - (Base.read_file('apimatic.png'), 'image/png', Base.employee_model(), - 'application/json', Base.read_file('apimatic.png'), 'image/png', - Base.get_serialized_employee(), 'application/json') - ]) - def test_multipart_request_without_file_wrapper(self, input_multipart_param_value1, - input_default_content_type1, - input_multipart_param_value2, - input_default_content_type2, - expected_file, - expected_file_content_type, - expected_model, - expected_model_content_type): + "input_multipart_param_value1, input_default_content_type1,input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type, expected_model, expected_model_content_type", + [ + (Base.read_file('apimatic.png'), 'image/png', + Base.employee_model(), 'application/json', + Base.read_file('apimatic.png'), 'image/png', + Base.get_serialized_employee(), 'application/json') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_multipart_request_without_file_wrapper( + self, input_multipart_param_value1: FileType, input_default_content_type1: str, + input_multipart_param_value2: Employee, input_default_content_type2: str, + expected_file: FileType, expected_file_content_type: str, + expected_model: str, expected_model_content_type: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .multipart_param(Parameter(key='file_wrapper', value=input_multipart_param_value1, @@ -542,22 +624,24 @@ def test_multipart_request_without_file_wrapper(self, input_multipart_param_valu and actual_model == expected_model \ and actual_model_content_type == expected_model_content_type finally: - actual_file.close() - expected_file.close() + self.close_file(actual_file) + self.close_file(expected_file) @pytest.mark.parametrize( - "input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type,expected_model, expected_model_content_type", [ - (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), Base.employee_model(), - 'application/json', Base.read_file('apimatic.png'), 'image/png', - Base.get_serialized_employee(), 'application/json') - ]) - def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1, - input_multipart_param_value2, - input_default_content_type2, - expected_file, - expected_file_content_type, - expected_model, - expected_model_content_type): + "input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2, expected_file, expected_file_content_type,expected_model, expected_model_content_type", + [ + (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), + Base.employee_model(), 'application/json', + Base.read_file('apimatic.png'), 'image/png', + Base.get_serialized_employee(), 'application/json') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_multipart_request_with_file_wrapper( + self, input_multipart_param_value1: FileWrapper, + input_multipart_param_value2: Employee, input_default_content_type2: str, + expected_file: FileType, expected_file_content_type: str, + expected_model: str, expected_model_content_type: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ @@ -578,8 +662,8 @@ def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1, and actual_model == expected_model \ and actual_model_content_type == expected_model_content_type finally: - actual_file.close() - expected_file.close() + self.close_file(actual_file) + self.close_file(expected_file) @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key, expected_auth_header_value', [ (Single('basic_auth'), 'Basic-Authorization', 'Basic {}'.format( @@ -587,7 +671,10 @@ def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1, (Single('bearer_auth'), 'Bearer-Authorization', 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42'), (Single('custom_header_auth'), 'token', 'Qaws2W233WedeRe4T56G6Vref2') ]) - def test_header_authentication(self, input_auth_scheme, expected_auth_header_key, expected_auth_header_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_header_authentication( + self, input_auth_scheme: Authentication, expected_auth_header_key: str, expected_auth_header_value: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ @@ -595,6 +682,7 @@ def test_header_authentication(self, input_auth_scheme, expected_auth_header_key assert http_request.headers[expected_auth_header_key] == expected_auth_header_value + @validate_call def test_query_authentication(self): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ @@ -608,12 +696,14 @@ def test_query_authentication(self): (Or('invalid_1', 'invalid_2')), (And('invalid_1', 'invalid_2')) ]) - def test_invalid_key_authentication(self, input_invalid_auth_scheme): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_invalid_key_authentication(self, input_invalid_auth_scheme: Authentication): with pytest.raises(ValueError) as validation_error: self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_invalid_auth_scheme) \ .build(self.global_configuration_with_auth) + assert validation_error.value.args[0] == 'Auth key is invalid.' @pytest.mark.parametrize('input_auth_scheme, expected_request_headers', [ @@ -663,11 +753,15 @@ def test_invalid_key_authentication(self, input_invalid_auth_scheme): 'token': None }), ]) - def test_success_case_of_multiple_authentications(self, input_auth_scheme, expected_request_headers): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_success_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_request_headers: Dict[str, Optional[str]] + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ .build(self.global_configuration_with_auth) + for key in expected_request_headers.keys(): assert http_request.headers.get(key) == expected_request_headers.get(key) @@ -701,12 +795,16 @@ def test_success_case_of_multiple_authentications(self, input_auth_scheme, expec (And(), ''), (And( 'basic_auth'), '[BasicAuth: username or password is undefined.]') ]) - def test_failed_case_of_multiple_authentications(self, input_auth_scheme, expected_error_message): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_failed_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_error_message: str + ): with pytest.raises(AuthValidationException) as errors: self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ .build(self.global_configuration_with_uninitialized_auth_params) + assert errors.value.args[0] == expected_error_message @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key,' @@ -716,8 +814,10 @@ def test_failed_case_of_multiple_authentications(self, input_auth_scheme, expect (Or('custom_header_auth', And('basic_auth', 'custom_header_auth')), 'token', 'Qaws2W233WedeRe4T56G6Vref2') ]) - def test_case_of_multiple_authentications(self, input_auth_scheme, expected_auth_header_key, - expected_auth_header_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_auth_header_key: str, expected_auth_header_value: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ @@ -725,3 +825,8 @@ def test_case_of_multiple_authentications(self, input_auth_scheme, expected_auth assert http_request.headers[expected_auth_header_key] == expected_auth_header_value assert http_request.headers.get('Authorization') is None + + @validate_call(config=dict(arbitrary_types_allowed=True)) + def close_file(self, file: Optional[FileType]) -> None: + if file is not None: + file.close() \ No newline at end of file diff --git a/tests/apimatic_core/request_builder_tests/test_request_builder.py~ b/tests/apimatic_core/request_builder_tests/test_request_builder.py~ new file mode 100644 index 0000000..cc0fc70 --- /dev/null +++ b/tests/apimatic_core/request_builder_tests/test_request_builder.py~ @@ -0,0 +1,832 @@ +from datetime import datetime, date +from typing import Any, Dict, Optional, List, Tuple + +import pytest +import sys + +from apimatic_core_interfaces.authentication.authentication import Authentication +from pydantic import validate_call + +from apimatic_core.exceptions.auth_validation_exception import AuthValidationException +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from apimatic_core.authentication.multiple.and_auth_group import And +from apimatic_core.authentication.multiple.or_auth_group import Or +from apimatic_core.authentication.multiple.single_auth import Single +from apimatic_core.types.array_serialization_format import SerializationFormats +from apimatic_core.types.file_wrapper import FileWrapper, FileType +from apimatic_core.types.parameter import Parameter +from apimatic_core.types.xml_attributes import XmlAttributes +from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core.utilities.auth_helper import AuthHelper +from apimatic_core.utilities.xml_helper import XmlHelper +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.callables.base_uri_callable import Server +from urllib.parse import quote + +from tests.apimatic_core.mocks.models.person import Employee +from tests.apimatic_core.mocks.models.xml_model import XMLModel +from tests.apimatic_core.mocks.union_type_lookup import UnionTypeLookUp + + +class TestRequestBuilder(Base): + + @pytest.mark.parametrize('input_server, expected_base_uri', [ + (Server.DEFAULT, 'http://localhost:3000/'), + (Server.AUTH_SERVER, 'http://authserver:5000/') + ]) + @validate_call + def test_base_uri(self, input_server: Server, expected_base_uri: str): + http_request = (self.new_request_builder + .server(input_server) + .http_method(HttpMethodEnum.POST) + .path('/') + .build(self.global_configuration)) + + assert http_request.query_url == expected_base_uri + + @validate_call + def test_path(self): + http_request = self.new_request_builder.http_method(HttpMethodEnum.POST).build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/test' + + @validate_call + def test_required_param(self): + with pytest.raises(ValueError) as validation_error: + self.new_request_builder \ + .query_param(Parameter(key='query_param', value=None, is_required=True)) \ + .build(self.global_configuration) + + assert validation_error.value.args[0] == 'Required parameter query_param cannot be None.' + + @validate_call + def test_optional_param(self): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .query_param(Parameter(key='query_param', value=None, is_required=False)) \ + .build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/test' + + @pytest.mark.parametrize('input_http_method, expected_http_method', [ + (HttpMethodEnum.POST, HttpMethodEnum.POST), + (HttpMethodEnum.PUT, HttpMethodEnum.PUT), + (HttpMethodEnum.PATCH, HttpMethodEnum.PATCH), + (HttpMethodEnum.DELETE, HttpMethodEnum.DELETE), + (HttpMethodEnum.GET, HttpMethodEnum.GET), + ]) + @validate_call + def test_http_method(self, input_http_method: HttpMethodEnum, expected_http_method: HttpMethodEnum): + http_request = self.new_request_builder \ + .http_method(input_http_method) \ + .build(self.global_configuration) + + assert http_request.http_method == expected_http_method + + @pytest.mark.parametrize('input_template_param_value, expected_template_param_value, should_encode', [ + ('Basic Test', 'Basic%20Test', True), + ('Basic"Test', 'Basic%22Test', True), + ('BasicTest', 'Basic%3ETest', True), + ('Basic#Test', 'Basic%23Test', True), + ('Basic%Test', 'Basic%25Test', True), + ('Basic|Test', 'Basic%7CTest', True), + ('Basic Test', 'Basic Test', False), + ('Basic"Test', 'Basic"Test', False), + ('BasicTest', 'Basic>Test', False), + ('Basic#Test', 'Basic#Test', False), + ('Basic%Test', 'Basic%Test', False), + ('Basic|Test', 'Basic|Test', False), + ]) + @validate_call + def test_template_params_with_encoding( + self, input_template_param_value: str, expected_template_param_value: str, should_encode: bool + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .path('/{template_param}') \ + .template_param(Parameter(key='template_param', value=input_template_param_value, + should_encode=should_encode)) \ + .build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/{}'.format(expected_template_param_value) + + @pytest.mark.parametrize('input_query_param_value, expected_query_param_value, array_serialization_format', [ + ('string', 'query_param=string', SerializationFormats.INDEXED), + (500, 'query_param=500', SerializationFormats.INDEXED), + (500.12, 'query_param=500.12', SerializationFormats.INDEXED), + (date(1994, 2, 13), 'query_param=1994-02-13', SerializationFormats.INDEXED), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'query_param=761117415', SerializationFormats.INDEXED), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'query_param={}'.format(quote(Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + SerializationFormats.INDEXED), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'query_param={}'.format(quote(Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + SerializationFormats.INDEXED), + ([1, 2, 3, 4], 'query_param[0]=1&query_param[1]=2&query_param[2]=3&query_param[3]=4', + SerializationFormats.INDEXED), + ([1, 2, 3, 4], 'query_param[]=1&query_param[]=2&query_param[]=3&query_param[]=4', + SerializationFormats.UN_INDEXED), + ([1, 2, 3, 4], 'query_param=1&query_param=2&query_param=3&query_param=4', + SerializationFormats.PLAIN), + ([1, 2, 3, 4], 'query_param=1%2C2%2C3%2C4', SerializationFormats.CSV), + ([1, 2, 3, 4], 'query_param=1%7C2%7C3%7C4', SerializationFormats.PSV), + ([1, 2, 3, 4], 'query_param=1%092%093%094', SerializationFormats.TSV), + ({'key1': 'value1', 'key2': 'value2'}, 'query_param[key1]=value1&query_param[key2]=value2', + SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, + 'query_param[key1]=value1' + '&query_param[key2][0]=1' + '&query_param[key2][1]=2' + '&query_param[key2][2]=3' + '&query_param[key2][3]=4', SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, + 'query_param[key1]=value1' + '&query_param[key2][0]=1' + '&query_param[key2][1]=2' + '&query_param[key2][2]=3' + '&query_param[key2][3][key1]=value1' + '&query_param[key2][3][key2]=value2', SerializationFormats.INDEXED), + (Base.employee_model(), + 'query_param[address]=street%20abc' + '&query_param[age]=27' + '&query_param[birthday]=1994-02-13' + '&query_param[birthtime]={}'.format(quote( + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[name]=Bob' + '&query_param[uid]=1234567' + '&query_param[personType]=Empl' + '&query_param[department]=IT' + '&query_param[dependents][0][address]=street%20abc' + '&query_param[dependents][0][age]=12' + '&query_param[dependents][0][birthday]=1994-02-13' + '&query_param[dependents][0][birthtime]={}'.format(quote( + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[dependents][0][name]=John' + '&query_param[dependents][0][uid]=7654321' + '&query_param[dependents][0][personType]=Per' + '&query_param[dependents][0][key1]=value1' + '&query_param[dependents][0][key2]=value2' + '&query_param[hiredAt]={}'.format(quote( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[joiningDay]=Monday' + '&query_param[salary]=30000' + '&query_param[workingDays][0]=Monday' + '&query_param[workingDays][1]=Tuesday' + '&query_param[key1]=value1' + '&query_param[key2]=value2', SerializationFormats.INDEXED) + ]) + @validate_call + def test_query_params( + self, input_query_param_value: Any, expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .query_param(Parameter(key='query_param', value=input_query_param_value)) \ + .array_serialization_format(array_serialization_format) \ + .build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_query_param_value) + + @pytest.mark.parametrize('input_additional_query_params_value, expected_additional_query_params_value', [ + ({'key1': 'value1', 'key2': 'value2'}, 'key1=value1&key2=value2') + ]) + @validate_call + def test_additional_query_params( + self, input_additional_query_params_value: Dict[str, str], expected_additional_query_params_value: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .additional_query_params(input_additional_query_params_value) \ + .build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_additional_query_params_value) + + @pytest.mark.parametrize('input_local_header_param_value, expected_local_header_param_value', [ + ('string', {'header_param': 'string'}), + (500, {'header_param': 500}), + (500.12, {'header_param': 500.12}), + (str(date(1994, 2, 13)), {'header_param': '1994-02-13'}), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': 761117415}), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([1, 2, 3, 4], {'header_param': [1, 2, 3, 4]}) + ]) + @validate_call + def test_local_headers( + self, input_local_header_param_value: Any, expected_local_header_param_value: Dict[str, Any] + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ + .build(self.global_configuration) + + assert http_request.headers == expected_local_header_param_value + + @pytest.mark.parametrize('input_global_header_param_value, expected_global_header_param_value', [ + ('my-string', {'header_param': 'my-string'}), + (5000, {'header_param': 5000}), + (5000.12, {'header_param': 5000.12}), + (str(date(1998, 2, 13)), {'header_param': '1998-02-13'}), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': 761117415}), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) + ]) + @validate_call + def test_global_headers( + self, input_global_header_param_value: Any, expected_global_header_param_value: Dict[str, Any] + ): + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .build(global_configuration) + + assert http_request.headers == expected_global_header_param_value + + @pytest.mark.parametrize('input_additional_header_param_value, expected_additional_header_param_value', [ + ('my-string', {'header_param': 'my-string'}), + (5000, {'header_param': 5000}), + (5000.12, {'header_param': 5000.12}), + (str(date(1998, 2, 13)), {'header_param': '1998-02-13'}), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': 761117415}), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) + ]) + @validate_call + def test_additional_headers( + self, input_additional_header_param_value: Any, expected_additional_header_param_value: Dict[str, Any] + ): + global_configuration = self.global_configuration + global_configuration.additional_headers = {'header_param': input_additional_header_param_value} + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .build(global_configuration) + + assert http_request.headers == expected_additional_header_param_value + + @pytest.mark.parametrize('input_global_header_param_value,' + 'input_local_header_param_value,' + 'expected_header_param_value', [ + ('global_string', None, {'header_param': None}), + ('global_string', 'local_string', {'header_param': 'local_string'}) + ]) + @validate_call + def test_local_and_global_headers_precedence( + self, input_global_header_param_value: str, input_local_header_param_value: Optional[str], + expected_header_param_value: Dict[str, Any] + ): + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ + .build(global_configuration) + + assert http_request.headers == expected_header_param_value + + @pytest.mark.parametrize('input_global_header_param_value,' + 'input_local_header_param_value,' + 'input_additional_header_param_value,' + 'expected_header_param_value', [ + ('global_string', 'local_string', 'additional_string', + {'header_param': 'additional_string'}), + ('global_string', 'local_string', None, + {'header_param': None}) + ]) + @validate_call + def test_all_headers_precedence( + self, input_global_header_param_value: str, input_local_header_param_value: str, + input_additional_header_param_value: Optional[str], expected_header_param_value: Dict[str, Any] + ): + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} + global_configuration.additional_headers = {'header_param': input_additional_header_param_value} + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ + .build(global_configuration) + + assert http_request.headers == expected_header_param_value + + @validate_call + def test_useragent_header(self): + engines: List[str] = ['CPython', 'Jython', 'JPython', 'IronPython', 'PyPy', 'RubyPython', 'AnacondaPython'] + operating_systems: List[str] = ['Linux', 'Windows', 'Darwin', 'FreeBSD', 'OpenBSD', 'macOS'] + + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .build(self.global_configuration_with_useragent) + [lang, version, engine, _, osInfo] = http_request.headers['user-agent'].split('|') + + assert lang == 'Python' and version == '31.8.0' and engine in engines and osInfo in operating_systems + + @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ + ('string', [('form_param', 'string')], SerializationFormats.INDEXED), + (500, [('form_param', 500)], SerializationFormats.INDEXED), + (500.12, [('form_param', 500.12)], SerializationFormats.INDEXED), + (str(date(1994, 2, 13)), [('form_param', '1994-02-13')], SerializationFormats.INDEXED), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', 761117415)], SerializationFormats.INDEXED), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))], SerializationFormats.INDEXED), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))], SerializationFormats.INDEXED), + ([1, 2, 3, 4], [('form_param[0]', 1), ('form_param[1]', 2), ('form_param[2]', 3), ('form_param[3]', 4)], + SerializationFormats.INDEXED), + ([1, 2, 3, 4], [('form_param[]', 1), ('form_param[]', 2), ('form_param[]', 3), ('form_param[]', 4)], + SerializationFormats.UN_INDEXED), + ([1, 2, 3, 4], [('form_param', 1), ('form_param', 2), ('form_param', 3), ('form_param', 4)], + SerializationFormats.PLAIN), + ({'key1': 'value1', 'key2': 'value2'}, [('form_param[key1]', 'value1'), ('form_param[key2]', 'value2')], + SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, + [('form_param[key1]', 'value1'), ('form_param[key2][0]', 1), ('form_param[key2][1]', 2), + ('form_param[key2][2]', 3), ('form_param[key2][3]', 4)], SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, + [('form_param[key1]', 'value1'), ('form_param[key2][0]', 1), ('form_param[key2][1]', 2), + ('form_param[key2][2]', 3), ('form_param[key2][3][key1]', 'value1'), ('form_param[key2][3][key2]', 'value2')], + SerializationFormats.INDEXED), + (Base.employee_model(), + [('form_param[address]', 'street abc'), ('form_param[age]', 27), ('form_param[birthday]', '1994-02-13'), + ('form_param[birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[name]', 'Bob'), + ('form_param[uid]', '1234567'), + ('form_param[personType]', 'Empl'), + ('form_param[department]', 'IT'), ('form_param[dependents][0][address]', 'street abc'), + ('form_param[dependents][0][age]', 12), ('form_param[dependents][0][birthday]', '1994-02-13'), + ('form_param[dependents][0][birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[dependents][0][name]', 'John'), ('form_param[dependents][0][uid]', '7654321'), + ('form_param[dependents][0][personType]', 'Per'), ('form_param[dependents][0][key1]', 'value1'), + ('form_param[dependents][0][key2]', 'value2'), + ('form_param[hiredAt]', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[joiningDay]', 'Monday'), ('form_param[salary]', 30000), ('form_param[workingDays][0]', 'Monday'), + ('form_param[workingDays][1]', 'Tuesday'), ('form_param[key1]', 'value1'), + ('form_param[key2]', 'value2')], SerializationFormats.INDEXED) + ]) + @validate_call + def test_form_params( + self, input_form_param_value: Any, expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .form_param(Parameter(key='form_param', value=input_form_param_value)) \ + .array_serialization_format(array_serialization_format) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_form_param_value + + @pytest.mark.parametrize('input_additional_form_param_value, expected_additional_form_param_value', [ + ({'key1': 'value1', 'key2': 'value2'}, [('key1', 'value1'), ('key2', 'value2')]) + ]) + @validate_call + def test_addition_form_params( + self, input_additional_form_param_value: Dict[str, str], + expected_additional_form_param_value: List[Tuple[str, str]] + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .additional_form_params(input_additional_form_param_value) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_additional_form_param_value + + @pytest.mark.parametrize('input_form_param_value,' + 'input_additional_form_param_value,' + 'expected_form_param_value', [ + ({'key1': 'value1', 'key2': 'value2'}, + {'additional_key1': 'additional_value1', 'additional_key2': 'additional_value2'}, + [('form_param[key1]', 'value1'), ('form_param[key2]', 'value2'), + ('additional_key1', 'additional_value1'), + ('additional_key2', 'additional_value2')]) + ]) + @validate_call + def test_form_params_with_additional_form_params( + self, input_form_param_value: Dict[str, str], input_additional_form_param_value: Dict[str, str], + expected_form_param_value: List[Tuple[str, str]] + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .form_param(Parameter(key='form_param', value=input_form_param_value)) \ + .additional_form_params(input_additional_form_param_value) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_form_param_value + + @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ + ('string', 'string'), + (500, 500), + (500.12, 500.12), + (str(date(1994, 2, 13)), '1994-02-13'), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), 761117415), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) + ]) + @validate_call + def test_json_body_params_without_serializer(self, input_body_param_value: Any, expected_body_param_value: Any): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(value=input_body_param_value)) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize('input_body_param_value1, input_body_param_value2, expected_body_param_value', [ + ('string1', 'string2', '{"param1": "string1", "param2": "string2"}'), + (100, 200, '{"param1": 100, "param2": 200}'), + (100.12, 200.12, '{"param1": 100.12, "param2": 200.12}') + ]) + @validate_call + def test_multiple_json_body_params_with_serializer(self, input_body_param_value1: Any, input_body_param_value2: Any, + expected_body_param_value: str): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(key='param1', value=input_body_param_value1)) \ + .body_param(Parameter(key='param2', value=input_body_param_value2)) \ + .body_serializer(ApiHelper.json_serialize) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ + ([1, 2, 3, 4], '[1, 2, 3, 4]'), + ({'key1': 'value1', 'key2': 'value2'}, '{"key1": "value1", "key2": "value2"}'), + ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, '{"key1": "value1", "key2": [1, 2, 3, 4]}'), + ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, + '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), + (Base.employee_model(), Base.get_serialized_employee()) + ]) + @validate_call + def test_json_body_params_with_serializer(self, input_body_param_value: Any, expected_body_param_value: str): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(value=input_body_param_value)) \ + .body_serializer(ApiHelper.json_serialize) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize('input_value, expected_value', [ + (100.0, '100.0'), + (True, 'true') + ]) + @validate_call + def test_type_combinator_validation_in_request(self, input_value: Any, expected_value: str): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter( + validator=lambda value: UnionTypeLookUp.get('ScalarTypes').validate(value=value).is_valid, + value=input_value)) \ + .body_serializer(ApiHelper.json_serialize) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_value + + @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ + (Base.xml_model(), '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '') + ]) + @validate_call + def test_xml_body_param_with_serializer(self, input_body_param_value: XMLModel, expected_body_param_value: str): + if sys.version_info[1] == 7: + expected_body_param_value = expected_body_param_value.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .xml_attributes(XmlAttributes(value=input_body_param_value, root_element_name='AttributesAndElements')) \ + .body_serializer(XmlHelper.serialize_to_xml) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ + ([Base.xml_model(), Base.xml_model()], + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '') + ]) + @validate_call + def test_xml_array_body_param_with_serializer( + self, input_body_param_value: List[XMLModel], expected_body_param_value: str + ): + if sys.version_info[1] == 7: + expected_body_param_value = expected_body_param_value.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .xml_attributes(XmlAttributes(value=input_body_param_value, + root_element_name='arrayOfModels', + array_item_name='item')) \ + .body_serializer(XmlHelper.serialize_list_to_xml) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize("input_body_param_value, expected_input_file, expected_content_type", [ + (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), + Base.read_file('apimatic.png'), 'image/png')]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_file_as_body_param( + self, input_body_param_value: FileWrapper, expected_input_file: FileType, expected_content_type: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .header_param(Parameter(key='content-type', value='application/xml')) \ + .body_param(Parameter(value=input_body_param_value)) \ + .build(self.global_configuration) + input_file = None + try: + input_file = http_request.parameters + + assert input_file.read() == expected_input_file.read() \ + and http_request.headers['content-type'] == expected_content_type + finally: + self.close_file(input_file) + self.close_file(expected_input_file) + + @pytest.mark.parametrize( + "input_multipart_param_value1, input_default_content_type1,input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type, expected_model, expected_model_content_type", + [ + (Base.read_file('apimatic.png'), 'image/png', + Base.employee_model(), 'application/json', + Base.read_file('apimatic.png'), 'image/png', + Base.get_serialized_employee(), 'application/json') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_multipart_request_without_file_wrapper( + self, input_multipart_param_value1: FileType, input_default_content_type1: str, + input_multipart_param_value2: Employee, input_default_content_type2: str, + expected_file: FileType, expected_file_content_type: str, + expected_model: str, expected_model_content_type: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .multipart_param(Parameter(key='file_wrapper', value=input_multipart_param_value1, + default_content_type=input_default_content_type1)) \ + .multipart_param(Parameter(key='model', value=ApiHelper.json_serialize(input_multipart_param_value2), + default_content_type=input_default_content_type2)) \ + .build(self.global_configuration) + actual_file = None + try: + actual_file = http_request.files['file_wrapper'][1] + actual_file_content_type = http_request.files['file_wrapper'][2] + actual_model = http_request.files['model'][1] + actual_model_content_type = http_request.files['model'][2] + + assert actual_file.read() == expected_file.read() \ + and actual_file_content_type == expected_file_content_type \ + and actual_model == expected_model \ + and actual_model_content_type == expected_model_content_type + finally: + self.close_file(actual_file) + self.close_file(expected_file) + + @pytest.mark.parametrize( + "input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2, expected_file, expected_file_content_type,expected_model, expected_model_content_type", + [ + (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), + Base.employee_model(), 'application/json', + Base.read_file('apimatic.png'), 'image/png', + Base.get_serialized_employee(), 'application/json') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_multipart_request_with_file_wrapper( + self, input_multipart_param_value1: FileWrapper, + input_multipart_param_value2: Employee, input_default_content_type2: str, + expected_file: FileType, expected_file_content_type: str, + expected_model: str, expected_model_content_type: str + ): + + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .multipart_param(Parameter(key='file', value=input_multipart_param_value1)) \ + .multipart_param(Parameter(key='model', value=ApiHelper.json_serialize(input_multipart_param_value2), + default_content_type=input_default_content_type2)) \ + .build(self.global_configuration) + + actual_file = None + try: + actual_file = http_request.files['file'][1] + actual_file_content_type = http_request.files['file'][2] + actual_model = http_request.files['model'][1] + actual_model_content_type = http_request.files['model'][2] + + assert actual_file.read() == expected_file.read() \ + and actual_file_content_type == expected_file_content_type \ + and actual_model == expected_model \ + and actual_model_content_type == expected_model_content_type + finally: + self.close_file(actual_file) + self.close_file(expected_file) + + @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key, expected_auth_header_value', [ + (Single('basic_auth'), 'Basic-Authorization', 'Basic {}'.format( + AuthHelper.get_base64_encoded_value('test_username', 'test_password'))), + (Single('bearer_auth'), 'Bearer-Authorization', 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42'), + (Single('custom_header_auth'), 'token', 'Qaws2W233WedeRe4T56G6Vref2') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_header_authentication( + self, input_auth_scheme: Authentication, expected_auth_header_key: str, expected_auth_header_value: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_auth_scheme) \ + .build(self.global_configuration_with_auth) + + assert http_request.headers[expected_auth_header_key] == expected_auth_header_value + + @validate_call + def test_query_authentication(self): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(Single('custom_query_auth')) \ + .build(self.global_configuration_with_auth) + + assert http_request.query_url == 'http://localhost:3000/test?token=Qaws2W233WedeRe4T56G6Vref2&api-key=W233WedeRe4T56G6Vref2' + + @pytest.mark.parametrize('input_invalid_auth_scheme', [ + (Single('invalid')), + (Or('invalid_1', 'invalid_2')), + (And('invalid_1', 'invalid_2')) + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_invalid_key_authentication(self, input_invalid_auth_scheme: Authentication): + with pytest.raises(ValueError) as validation_error: + self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_invalid_auth_scheme) \ + .build(self.global_configuration_with_auth) + + assert validation_error.value.args[0] == 'Auth key is invalid.' + + @pytest.mark.parametrize('input_auth_scheme, expected_request_headers', [ + (Or('basic_auth', 'custom_header_auth'), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'token': None + }), + (And('basic_auth', 'bearer_auth', 'custom_header_auth'), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', + 'token': 'Qaws2W233WedeRe4T56G6Vref2' + }), + (Or('basic_auth', And('bearer_auth', 'custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': None, + 'token': None + }), + (And('basic_auth', Or('bearer_auth', 'custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', + 'token': None + }), + (And('basic_auth', And('bearer_auth', 'custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', + 'token': 'Qaws2W233WedeRe4T56G6Vref2' + }), + (Or('basic_auth', Or('bearer_auth', 'custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': None, + 'token': None + }), + (Or('basic_auth', Or('custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'token': None + }), + (Or('basic_auth', And('custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'token': None + }), + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_success_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_request_headers: Dict[str, Optional[str]] + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_auth_scheme) \ + .build(self.global_configuration_with_auth) + + for key in expected_request_headers.keys(): + assert http_request.headers.get(key) == expected_request_headers.get(key) + + @pytest.mark.parametrize('input_auth_scheme, expected_error_message', [ + (Or('basic_auth', 'bearer_auth', 'custom_header_auth'), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', 'bearer_auth', 'custom_header_auth'), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or('basic_auth', And('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', Or('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', And('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or('basic_auth', Or('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or(), ''), + (Or('basic_auth'), '[BasicAuth: username or password is undefined.]'), + (And(), ''), + (And( 'basic_auth'), '[BasicAuth: username or password is undefined.]') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_failed_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_error_message: str + ): + with pytest.raises(AuthValidationException) as errors: + self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_auth_scheme) \ + .build(self.global_configuration_with_uninitialized_auth_params) + + assert errors.value.args[0] == expected_error_message + + @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key,' + 'expected_auth_header_value', [ + (Or('basic_auth', 'custom_header_auth'), 'token', + 'Qaws2W233WedeRe4T56G6Vref2'), + (Or('custom_header_auth', And('basic_auth', 'custom_header_auth')), 'token', + 'Qaws2W233WedeRe4T56G6Vref2') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_auth_header_key: str, expected_auth_header_value: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_auth_scheme) \ + .build(self.global_configuration_with_partially_initialized_auth_params) + + assert http_request.headers[expected_auth_header_key] == expected_auth_header_value + assert http_request.headers.get('Authorization') is None + + @validate_call(config=dict(arbitrary_types_allowed=True)) + def close_file(self, file: Optional[FileType]) -> None: + if file is not None: + file.close() \ No newline at end of file diff --git a/tests/apimatic_core/response_handler_tests/test_response_handler.py b/tests/apimatic_core/response_handler_tests/test_response_handler.py index c1109dc..e1ff96a 100644 --- a/tests/apimatic_core/response_handler_tests/test_response_handler.py +++ b/tests/apimatic_core/response_handler_tests/test_response_handler.py @@ -2,10 +2,12 @@ import pytest import sys +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat from apimatic_core_interfaces.http.http_response import HttpResponse -from typing import Type, Any, TypeVar, Optional, Callable, List +from typing import Type, Any, TypeVar, Optional, Callable, List, Union + +from pydantic import validate_call -from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.utilities.api_helper import ApiHelper from apimatic_core.utilities.xml_helper import XmlHelper from tests.apimatic_core.base import Base @@ -21,6 +23,7 @@ class TestResponseHandler(Base): E = TypeVar("E", bound=BaseException) + @validate_call def test_nullify_404(self): http_response = (self.new_response_handler .is_nullify404(True) @@ -34,14 +37,16 @@ def test_nullify_404(self): (Base.response(status_code=429), GlobalTestException, 'Invalid response'), (Base.response(status_code=399), GlobalTestException, '3XX Global') ]) + @validate_call def test_global_error( - self, http_response: HttpResponse, expected_exception_type: Type[E], expected_error_message: str + self, http_response: HttpResponse, expected_exception_type: Type[APIException], expected_error_message: str ): with pytest.raises(expected_exception_type) as error: self.new_response_handler.handle(http_response, self.global_errors()) assert error.value.reason == expected_error_message + @validate_call def test_local_error(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -49,6 +54,7 @@ def test_local_error(self): assert error.value.reason == 'Not Found' + @validate_call def test_default_local_error(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -62,8 +68,9 @@ def test_default_local_error(self): (Base.response(status_code=443), LocalTestException, '4XX local'), (Base.response(status_code=522), LocalTestException, '522 local') ]) + @validate_call def test_default_range_local_error( - self, http_response: HttpResponse, expected_exception_type: Type[E], expected_error_message: str): + self, http_response: HttpResponse, expected_exception_type: Type[APIException], expected_error_message: str): with pytest.raises(expected_exception_type) as error: self.new_response_handler.local_error(522, '522 local', LocalTestException) \ .local_error('5XX', '5XX local', APIException) \ @@ -72,6 +79,7 @@ def test_default_range_local_error( assert error.value.reason == expected_error_message + @validate_call def test_local_error_with_body(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -85,6 +93,7 @@ def test_local_error_with_body(self): and error.value.server_message == 'Test message from server' \ and error.value.secret_message_for_endpoint == 'This is test error message' + @validate_call def test_local_error_template_message(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error_template(404, 'error_code => {$statusCode}, ' @@ -103,6 +112,7 @@ def test_local_error_template_message(self): 'header => application/json, ' \ 'body => 5001 - Test message from server - This is test error message' + @validate_call def test_global_error_with_body(self): with pytest.raises(NestedModelException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -119,10 +129,12 @@ def test_global_error_with_body(self): assert error.value.server_code == 5001 \ and error.value.server_message == 'Test message from server' \ + and error.value.model is not None \ and error.value.model.field == 'Test field' \ and error.value.model.name == 'Test name' \ and error.value.model.address == 'Test address' + @validate_call def test_global_error_template_message(self): with pytest.raises(NestedModelException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -134,6 +146,7 @@ def test_global_error_template_message(self): assert error.value.reason == 'global error message -> error_code => 412, header => ,' \ ' body => 5001 - Test message from server - Test name' + @validate_call def test_local_error_precedence(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(400, '400 Local', LocalTestException) \ @@ -141,6 +154,7 @@ def test_local_error_precedence(self): assert error.value.reason == '400 Local' + @validate_call def test_global_error_precedence(self): with pytest.raises(GlobalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -153,7 +167,10 @@ def test_global_error_precedence(self): (Base.response(text=500), 500), (Base.response(text=500.124), 500.124) ]) - def test_simple_response_body(self, input_http_response: HttpResponse, expected_response_body: str): + @validate_call + def test_simple_response_body( + self, input_http_response: HttpResponse, expected_response_body: Union[int, float, str] + ): http_response = self.new_response_handler.handle(input_http_response, self.global_errors()) assert http_response == expected_response_body @@ -163,6 +180,7 @@ def test_simple_response_body(self, input_http_response: HttpResponse, expected_ (Base.response(text='500'), int, int, 500), (Base.response(text=500), str, str, '500') ]) + @validate_call def test_simple_response_body_with_convertor( self, input_http_response: HttpResponse, input_response_convertor: Optional[Callable[[Any], Any]], expected_response_body_type: Type, expected_response_body: Any): @@ -179,11 +197,13 @@ def test_simple_response_body_with_convertor( (Base.response(text=''), None), (Base.response(text=' '), None) ]) + @validate_call def test_custom_type_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): http_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .deserialize_into(Employee.model_validate) \ .handle(input_http_response, self.global_errors()) + assert ApiHelper.json_serialize(http_response) == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -195,10 +215,12 @@ def test_custom_type_response_body(self, input_http_response: HttpResponse, expe (Base.response(text=''), None), (Base.response(text=' '), None) ]) + @validate_call def test_json_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): http_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .handle(input_http_response, self.global_errors()) + assert ApiHelper.json_serialize(http_response) == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -227,6 +249,7 @@ def test_json_response_body(self, input_http_response: HttpResponse, expected_re '' ''), ]) + @validate_call def test_xml_response_body_with_item_name(self, input_http_response: HttpResponse, expected_response_body: str): if sys.version_info[1] == 7: expected_response_body = expected_response_body.replace( @@ -238,6 +261,7 @@ def test_xml_response_body_with_item_name(self, input_http_response: HttpRespons .deserialize_into(XMLModel) \ .xml_item_name('item') \ .handle(input_http_response, self.global_errors()) + assert XmlHelper.serialize_list_to_xml( http_response, 'arrayOfModels', 'item') == expected_response_body @@ -254,6 +278,7 @@ def test_xml_response_body_with_item_name(self, input_http_response: HttpRespons '' ''), ]) + @validate_call def test_xml_response_body_without_item_name(self, input_http_response: HttpResponse, expected_response_body: str): if sys.version_info[1] == 7: expected_response_body = expected_response_body.replace( @@ -264,6 +289,7 @@ def test_xml_response_body_without_item_name(self, input_http_response: HttpResp .deserializer(XmlHelper.deserialize_xml) \ .deserialize_into(XMLModel) \ .handle(input_http_response, self.global_errors()) + assert XmlHelper.serialize_to_xml(http_response, 'AttributesAndElements') == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -272,11 +298,13 @@ def test_xml_response_body_without_item_name(self, input_http_response: HttpResp (Base.response(text='{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}') ]) + @validate_call def test_api_response(self, input_http_response: HttpResponse, expected_response_body: str): api_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .is_api_response(True) \ .handle(input_http_response, self.global_errors()) + assert ApiHelper.json_serialize(api_response.body) == expected_response_body assert api_response.is_success() is True assert api_response.is_error() is False @@ -289,6 +317,7 @@ def test_api_response(self, input_http_response: HttpResponse, expected_response (Base.response(text='{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}'), '{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}', ['e1', 'e2']) ]) + @validate_call def test_api_response_convertor( self, input_http_response: HttpResponse, expected_response_body: str, expected_error_list: List[str]): api_response = self.new_response_handler \ @@ -296,6 +325,7 @@ def test_api_response_convertor( .is_api_response(True) \ .convertor(ApiResponse.create) \ .handle(input_http_response, self.global_errors()) + assert isinstance(api_response, ApiResponse) and \ ApiHelper.json_serialize(api_response.body) == expected_response_body \ and api_response.errors == expected_error_list @@ -306,6 +336,7 @@ def test_api_response_convertor( (Base.response(text=''), None, None), (Base.response(text=' '), None, None) ]) + @validate_call def test_api_response_with_errors( self, input_http_response: HttpResponse, expected_response_body: Optional[str], expected_error_list: Optional[List[str]]): @@ -313,6 +344,7 @@ def test_api_response_with_errors( .deserializer(ApiHelper.json_deserialize) \ .is_api_response(True) \ .handle(input_http_response, self.global_errors()) + assert ApiHelper.json_serialize(api_response.body) == expected_response_body \ and api_response.errors == expected_error_list @@ -340,13 +372,15 @@ def test_api_response_with_errors( DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) ]) + @validate_call def test_date_time_response_body( self, input_http_response: HttpResponse, input_date_time_format: DateTimeFormat, - expected_response_body: datetime): + expected_response_body: Union[datetime, List[datetime]]): http_response = self.new_response_handler \ .deserializer(ApiHelper.datetime_deserialize) \ .datetime_format(input_date_time_format) \ .handle(input_http_response, self.global_errors()) + assert http_response == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -354,8 +388,12 @@ def test_date_time_response_body( (Base.response(text=ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))])), [date(1994, 2, 13), date(1994, 2, 13)]) ]) - def test_date_response_body(self, input_http_response: HttpResponse, expected_response_body: date): + @validate_call + def test_date_response_body( + self, input_http_response: HttpResponse, expected_response_body: Union[date, List[date]] + ): http_response = self.new_response_handler \ .deserializer(ApiHelper.date_deserialize) \ .handle(input_http_response, self.global_errors()) + assert http_response == expected_response_body diff --git a/tests/apimatic_core/response_handler_tests/test_response_handler.py~ b/tests/apimatic_core/response_handler_tests/test_response_handler.py~ new file mode 100644 index 0000000..455f905 --- /dev/null +++ b/tests/apimatic_core/response_handler_tests/test_response_handler.py~ @@ -0,0 +1,399 @@ +from datetime import datetime, date +import pytest +import sys + +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.http.http_response import HttpResponse +from typing import Type, Any, TypeVar, Optional, Callable, List, Union + +from pydantic import validate_call + +from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core.utilities.xml_helper import XmlHelper +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.exceptions.api_exception import APIException +from tests.apimatic_core.mocks.exceptions.global_test_exception import GlobalTestException +from tests.apimatic_core.mocks.exceptions.local_test_exception import LocalTestException +from tests.apimatic_core.mocks.exceptions.nested_model_exception import NestedModelException +from tests.apimatic_core.mocks.models.api_response import ApiResponse +from tests.apimatic_core.mocks.models.person import Employee +from tests.apimatic_core.mocks.models.xml_model import XMLModel + + +class TestResponseHandler(Base): + E = TypeVar("E", bound=BaseException) + + @validate_call + def test_nullify_404(self): + http_response = (self.new_response_handler + .is_nullify404(True) + .handle(self.response(status_code=404), self.global_errors())) + + assert http_response is None + + @pytest.mark.parametrize('http_response, expected_exception_type, expected_error_message', [ + (Base.response(status_code=400), GlobalTestException, '400 Global'), + (Base.response(status_code=412), NestedModelException, 'Precondition Failed'), + (Base.response(status_code=429), GlobalTestException, 'Invalid response'), + (Base.response(status_code=399), GlobalTestException, '3XX Global') + ]) + @validate_call + def test_global_error( + self, http_response: HttpResponse, expected_exception_type: Type[E], expected_error_message: str + ): + with pytest.raises(expected_exception_type) as error: + self.new_response_handler.handle(http_response, self.global_errors()) + + assert error.value.reason == expected_error_message + + @validate_call + def test_local_error(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=404), self.global_errors()) + + assert error.value.reason == 'Not Found' + + @validate_call + def test_default_local_error(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .local_error('default', 'Response Not OK', LocalTestException) \ + .handle(self.response(status_code=412), self.global_errors()) + + assert error.value.reason == 'Response Not OK' + + @pytest.mark.parametrize('http_response, expected_exception_type, expected_error_message', [ + (Base.response(status_code=501), APIException, '5XX local'), + (Base.response(status_code=443), LocalTestException, '4XX local'), + (Base.response(status_code=522), LocalTestException, '522 local') + ]) + @validate_call + def test_default_range_local_error( + self, http_response: HttpResponse, expected_exception_type: Type[APIException], expected_error_message: str): + with pytest.raises(expected_exception_type) as error: + self.new_response_handler.local_error(522, '522 local', LocalTestException) \ + .local_error('5XX', '5XX local', APIException) \ + .local_error('4XX', '4XX local', LocalTestException) \ + .handle(http_response, self.global_errors()) + + assert error.value.reason == expected_error_message + + @validate_call + def test_local_error_with_body(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=404, + text='{"ServerCode": 5001, ' + '"ServerMessage": "Test message from server", ' + '"SecretMessageForEndpoint": "This is test error message"}'), + self.global_errors()) + + assert error.value.server_code == 5001 \ + and error.value.server_message == 'Test message from server' \ + and error.value.secret_message_for_endpoint == 'This is test error message' + + @validate_call + def test_local_error_template_message(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error_template(404, 'error_code => {$statusCode}, ' + 'header => {$response.header.accept}, ' + 'body => {$response.body#/ServerCode} - ' + '{$response.body#/ServerMessage} - ' + '{$response.body#/SecretMessageForEndpoint}', + LocalTestException) \ + .handle(self.response(status_code=404, text='{"ServerCode": 5001, "ServerMessage": ' + '"Test message from server", "SecretMessageForEndpoint": ' + '"This is test error message"}', + headers={'accept': 'application/json'}), + self.global_errors_with_template_message()) + + assert error.value.reason == 'error_code => 404, ' \ + 'header => application/json, ' \ + 'body => 5001 - Test message from server - This is test error message' + + @validate_call + def test_global_error_with_body(self): + with pytest.raises(NestedModelException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=412, + text='{"ServerCode": 5001, ' + '"ServerMessage": "Test message from server", ' + '"model": { ' + '"field": "Test field", ' + '"name": "Test name", ' + '"address": "Test address"' + '}' + '}'), + self.global_errors()) + + assert error.value.server_code == 5001 \ + and error.value.server_message == 'Test message from server' \ + and error.value.model is not None \ + and error.value.model.field == 'Test field' \ + and error.value.model.name == 'Test name' \ + and error.value.model.address == 'Test address' + + @validate_call + def test_global_error_template_message(self): + with pytest.raises(NestedModelException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=412, + text='{"ServerCode": 5001, "ServerMessage": "Test message from server", "model": ' + '{ "field": "Test field", "name": "Test name", "address": "Test address"}}'), + self.global_errors_with_template_message()) + + assert error.value.reason == 'global error message -> error_code => 412, header => ,' \ + ' body => 5001 - Test message from server - Test name' + + @validate_call + def test_local_error_precedence(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error(400, '400 Local', LocalTestException) \ + .handle(self.response(status_code=400), self.global_errors()) + + assert error.value.reason == '400 Local' + + @validate_call + def test_global_error_precedence(self): + with pytest.raises(GlobalTestException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=400), self.global_errors()) + + assert error.value.reason == '400 Global' + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text='This is a string response'), 'This is a string response'), + (Base.response(text=500), 500), + (Base.response(text=500.124), 500.124) + ]) + @validate_call + def test_simple_response_body( + self, input_http_response: HttpResponse, expected_response_body: Union[int, float, str] + ): + http_response = self.new_response_handler.handle(input_http_response, self.global_errors()) + + assert http_response == expected_response_body + + @pytest.mark.parametrize('input_http_response, input_response_convertor, expected_response_body_type, ' + 'expected_response_body', [ + (Base.response(text='500'), int, int, 500), + (Base.response(text=500), str, str, '500') + ]) + @validate_call + def test_simple_response_body_with_convertor( + self, input_http_response: HttpResponse, input_response_convertor: Optional[Callable[[Any], Any]], + expected_response_body_type: Type, expected_response_body: Any): + http_response = (self.new_response_handler + .convertor(input_response_convertor) + .handle(input_http_response, self.global_errors())) + + assert type(http_response) == expected_response_body_type and http_response == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text=ApiHelper.json_serialize(Base.employee_model())), + ApiHelper.json_serialize(Base.employee_model())), + (Base.response(), None), + (Base.response(text=''), None), + (Base.response(text=' '), None) + ]) + @validate_call + def test_custom_type_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): + http_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .deserialize_into(Employee.model_validate) \ + .handle(input_http_response, self.global_errors()) + + assert ApiHelper.json_serialize(http_response) == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text='[1, 2, 3, 4]'), '[1, 2, 3, 4]'), + (Base.response(text='{"key1": "value1", "key2": "value2"}'), '{"key1": "value1", "key2": "value2"}'), + (Base.response(text='{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), + '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), + (Base.response(), None), + (Base.response(text=''), None), + (Base.response(text=' '), None) + ]) + @validate_call + def test_json_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): + http_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .handle(input_http_response, self.global_errors()) + + assert ApiHelper.json_serialize(http_response) == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text=XmlHelper.serialize_list_to_xml( + [Base.xml_model(), Base.xml_model()], 'arrayOfModels', 'item')), + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + ''), + ]) + @validate_call + def test_xml_response_body_with_item_name(self, input_http_response: HttpResponse, expected_response_body: str): + if sys.version_info[1] == 7: + expected_response_body = expected_response_body.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + http_response = self.new_response_handler \ + .is_xml_response(True) \ + .deserializer(XmlHelper.deserialize_xml_to_list) \ + .deserialize_into(XMLModel) \ + .xml_item_name('item') \ + .handle(input_http_response, self.global_errors()) + + assert XmlHelper.serialize_list_to_xml( + http_response, 'arrayOfModels', 'item') == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text=XmlHelper.serialize_to_xml(Base.xml_model(), 'AttributesAndElements')), + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + ''), + ]) + @validate_call + def test_xml_response_body_without_item_name(self, input_http_response: HttpResponse, expected_response_body: str): + if sys.version_info[1] == 7: + expected_response_body = expected_response_body.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + http_response = self.new_response_handler \ + .is_xml_response(True) \ + .deserializer(XmlHelper.deserialize_xml) \ + .deserialize_into(XMLModel) \ + .handle(input_http_response, self.global_errors()) + + assert XmlHelper.serialize_to_xml(http_response, 'AttributesAndElements') == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text='[1, 2, 3, 4]'), '[1, 2, 3, 4]'), + (Base.response(text='{"key1": "value1", "key2": "value2"}'), '{"key1": "value1", "key2": "value2"}'), + (Base.response(text='{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), + '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}') + ]) + @validate_call + def test_api_response(self, input_http_response: HttpResponse, expected_response_body: str): + api_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .is_api_response(True) \ + .handle(input_http_response, self.global_errors()) + + assert ApiHelper.json_serialize(api_response.body) == expected_response_body + assert api_response.is_success() is True + assert api_response.is_error() is False + assert api_response.status_code == 200 + assert api_response.text == expected_response_body + assert api_response.reason_phrase is None + assert api_response.headers == {} + + @pytest.mark.parametrize('input_http_response, expected_response_body, expected_error_list', [ + (Base.response(text='{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}'), + '{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}', ['e1', 'e2']) + ]) + @validate_call + def test_api_response_convertor( + self, input_http_response: HttpResponse, expected_response_body: str, expected_error_list: List[str]): + api_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .is_api_response(True) \ + .convertor(ApiResponse.create) \ + .handle(input_http_response, self.global_errors()) + + assert isinstance(api_response, ApiResponse) and \ + ApiHelper.json_serialize(api_response.body) == expected_response_body \ + and api_response.errors == expected_error_list + + @pytest.mark.parametrize('input_http_response, expected_response_body, expected_error_list', [ + (Base.response(text='{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"]}'), + '{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"]}', ['e1', 'e2']), + (Base.response(text=''), None, None), + (Base.response(text=' '), None, None) + ]) + @validate_call + def test_api_response_with_errors( + self, input_http_response: HttpResponse, expected_response_body: Optional[str], + expected_error_list: Optional[List[str]]): + api_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .is_api_response(True) \ + .handle(input_http_response, self.global_errors()) + + assert ApiHelper.json_serialize(api_response.body) == expected_response_body \ + and api_response.errors == expected_error_list + + @pytest.mark.parametrize('input_http_response, input_date_time_format, expected_response_body', [ + (Base.response(text=ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))), + DateTimeFormat.UNIX_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (Base.response(text=ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))), + DateTimeFormat.HTTP_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (Base.response(text=ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))), + DateTimeFormat.RFC3339_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + + (Base.response( + text=ApiHelper.json_serialize([ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))])), + DateTimeFormat.UNIX_DATE_TIME, [datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]), + + (Base.response( + text=ApiHelper.json_serialize([ApiHelper.HttpDateTime.from_datetime(datetime(1995, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1995, 2, 13, 5, 30, 15))])), + DateTimeFormat.HTTP_DATE_TIME, [datetime(1995, 2, 13, 5, 30, 15), datetime(1995, 2, 13, 5, 30, 15)]), + + (Base.response( + text=ApiHelper.json_serialize([ApiHelper.RFC3339DateTime.from_datetime(datetime(1996, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1996, 2, 13, 5, 30, 15))])), + DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) + + ]) + @validate_call + def test_date_time_response_body( + self, input_http_response: HttpResponse, input_date_time_format: DateTimeFormat, + expected_response_body: Union[datetime, List[datetime]]): + http_response = self.new_response_handler \ + .deserializer(ApiHelper.datetime_deserialize) \ + .datetime_format(input_date_time_format) \ + .handle(input_http_response, self.global_errors()) + + assert http_response == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text=str(date(1994, 2, 13))), date(1994, 2, 13)), + (Base.response(text=ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))])), + [date(1994, 2, 13), date(1994, 2, 13)]) + ]) + @validate_call + def test_date_response_body( + self, input_http_response: HttpResponse, expected_response_body: Union[date, List[date]] + ): + http_response = self.new_response_handler \ + .deserializer(ApiHelper.date_deserialize) \ + .handle(input_http_response, self.global_errors()) + + assert http_response == expected_response_body diff --git a/tests/apimatic_core/union_type_tests/test_any_of.py b/tests/apimatic_core/union_type_tests/test_any_of.py index ed11e89..c6a493d 100644 --- a/tests/apimatic_core/union_type_tests/test_any_of.py +++ b/tests/apimatic_core/union_type_tests/test_any_of.py @@ -1,6 +1,10 @@ from datetime import datetime, date +from typing import Any, List, Union + import pytest +from apimatic_core_interfaces.types.union_type import UnionType from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import validate_call from apimatic_core.exceptions.anyof_validation_exception import AnyOfValidationException from apimatic_core.types.datetime_format import DateTimeFormat @@ -285,8 +289,11 @@ class TestAnyOf: UnionTypeContext(is_array=True)), LeafType(int, UnionTypeContext(is_array=True))], UnionTypeContext(is_array=True), False, None), ]) - def test_any_of_primitive_type(self, input_value, input_types, input_context, expected_validity, - expected_deserialized_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_primitive_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validity: bool, expected_deserialized_value: Any + ): try: union_type_result = AnyOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -317,8 +324,13 @@ def test_any_of_primitive_type(self, input_value, input_types, input_context, ex ('1994-11-06', '1994-11-06', [LeafType(date), LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME))], UnionTypeContext(), True, date(1994, 11, 6)) ]) - def test_any_of_date_and_datetime(self, input_value, input_date, input_types, input_context, expected_validity, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_date_and_datetime( + self, input_value: Any, input_date: Union[str, float, int], input_types: List[UnionType], + input_context: UnionTypeContext, expected_validity: bool, expected_value: Union[date, datetime] + ): union_type_result = AnyOf(input_types, input_context).validate(input_value) + assert union_type_result.is_valid == expected_validity actual_deserialized_value = union_type_result.deserialize(input_date) assert actual_deserialized_value == expected_value @@ -338,8 +350,12 @@ def test_any_of_date_and_datetime(self, input_value, input_date, input_types, in ({'key0': 1, 'key3': 2}, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 1, 'key3': 2}) ]) - def test_any_of_optional_nullable(self, input_value, input_types, input_context, expected_validity, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_optional_nullable( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validity: bool, expected_value: Any): union_type_result = AnyOf(input_types, input_context).validate(input_value) + assert union_type_result.is_valid == expected_validity actual_deserialized_value = union_type_result.deserialize(input_value) assert actual_deserialized_value == expected_value @@ -643,8 +659,11 @@ def test_any_of_optional_nullable(self, input_value, input_types, input_context, UnionTypeContext(is_dict=True, is_array=True), True, {'key0': [Orbit(orbit_number_of_electrons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=10)], 'key1': [Orbit(orbit_number_of_electrons=12), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)]}), ]) - def test_any_of_custom_type(self, input_value, input_types, input_context, expected_is_valid_output, - expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_custom_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = AnyOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -710,7 +729,11 @@ def test_any_of_custom_type(self, input_value, input_types, input_context, expec ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', [LeafType(dict), LeafType(dict)], UnionTypeContext(), True), ]) - def test_any_of_with_discriminator_custom_type(self, input_value, input_types, input_context, expected_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_with_discriminator_custom_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_output: Any + ): try: deserialized_dict_input = ApiHelper.json_deserialize(input_value, as_dict=True) union_type_result = AnyOf(input_types, input_context).validate(deserialized_dict_input) @@ -785,7 +808,11 @@ def test_any_of_with_discriminator_custom_type(self, input_value, input_types, i LeafType(Days, UnionTypeContext(is_array=True))], UnionTypeContext(is_array=True), True, [[1, 2], ['Monday', 'Tuesday']]), ]) - def test_any_of_enum_type(self, input_value, input_types, input_context, expected_is_valid_output, expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_enum_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = AnyOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -806,7 +833,11 @@ def test_any_of_enum_type(self, input_value, input_types, input_context, expecte '{} \nActual Value: 100.5\nExpected Type: Any Of int, bool, str.'.format( UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)) ]) - def test_any_of_validation_errors(self, input_value, input_types, input_context, expected_validation_message): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_validation_errors( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validation_message: str + ): with pytest.raises(AnyOfValidationException) as validation_error: AnyOf(input_types, input_context).validate(input_value) assert validation_error.value.message == expected_validation_message diff --git a/tests/apimatic_core/union_type_tests/test_one_of.py b/tests/apimatic_core/union_type_tests/test_one_of.py index 668271d..564a5f9 100644 --- a/tests/apimatic_core/union_type_tests/test_one_of.py +++ b/tests/apimatic_core/union_type_tests/test_one_of.py @@ -1,6 +1,10 @@ from datetime import datetime, date, timezone +from typing import Any, List, Union + import pytest +from apimatic_core_interfaces.types.union_type import UnionType from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import validate_call from apimatic_core.exceptions.oneof_validation_exception import OneOfValidationException from apimatic_core.types.datetime_format import DateTimeFormat @@ -288,9 +292,11 @@ class TestOneOf: UnionTypeContext(is_array=True)), LeafType(int, UnionTypeContext(is_array=True))], UnionTypeContext(is_array=True), False, None), ]) - - def test_one_of_primitive_type(self, input_value, input_types, input_context, expected_is_valid_output, - expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_primitive_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = OneOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -303,27 +309,36 @@ def test_one_of_primitive_type(self, input_value, input_types, input_context, ex assert actual_deserialized_value == expected_deserialized_value_output @pytest.mark.parametrize( - 'input_value, input_types, input_context, expected_validity, expected_value', [ - (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), + 'input_value, input_date, input_types, input_context, expected_validity, expected_value', [ + (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37), False), + Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - (Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37)), + (Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37), False), + Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37)), [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.HTTP_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - (1480809600, + (ApiHelper.UnixDateTime(datetime(1994, 11, 6, 8, 49, 37)), 1480809600, [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.UNIX_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime.utcfromtimestamp(1480809600)), - (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), - [LeafType(datetime, UnionTypeContext(date_time_converter=ApiHelper.RFC3339DateTime, date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], + (datetime(1994, 11, 6, 8, 49, 37), Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), + [LeafType(datetime, UnionTypeContext(date_time_converter=ApiHelper.RFC3339DateTime, + date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - ('1994-11-06', [LeafType(date), LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME))], UnionTypeContext(), + ('1994-11-06', '1994-11-06', + [LeafType(date), LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME))], + UnionTypeContext(), True, date(1994, 11, 6)) ]) - def test_one_of_date_and_datetime(self, input_value, input_types, input_context, expected_validity, expected_value): - union_type = OneOf(input_types, input_context) - union_type_result = union_type.validate(input_value) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_date_and_datetime( + self, input_value: Any, input_date: Union[str, float, int], input_types: List[UnionType], + input_context: UnionTypeContext, expected_validity: bool, expected_value: Union[date, datetime] + ): + union_type_result = OneOf(input_types, input_context).validate(input_value) + assert union_type_result.is_valid == expected_validity - actual_deserialized_value = union_type_result.deserialize(input_value) + actual_deserialized_value = union_type_result.deserialize(input_date) assert actual_deserialized_value == expected_value @pytest.mark.parametrize( @@ -341,8 +356,13 @@ def test_one_of_date_and_datetime(self, input_value, input_types, input_context, ({'key0': 1, 'key3': 2}, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 1, 'key3': 2}) ]) - def test_one_of_optional_nullable(self, input_value, input_types, input_context, expected_validity, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_optional_nullable( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validity: bool, expected_value: Any + ): union_type_result = OneOf(input_types, input_context).validate(input_value) + assert union_type_result.is_valid == expected_validity actual_deserialized_value = union_type_result.deserialize(input_value) assert actual_deserialized_value == expected_value @@ -646,8 +666,11 @@ def test_one_of_optional_nullable(self, input_value, input_types, input_context, UnionTypeContext(is_dict=True, is_array=True), True, {'key0': [Orbit(orbit_number_of_electrons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=10)], 'key1': [Orbit(orbit_number_of_electrons=12), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)]}), ]) - def test_one_of_custom_type(self, input_value, input_types, input_context, expected_is_valid_output, - expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_custom_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = OneOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -713,7 +736,11 @@ def test_one_of_custom_type(self, input_value, input_types, input_context, expec ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', [LeafType(dict), LeafType(dict)], UnionTypeContext(), False), ]) - def test_one_of_with_discriminator_custom_type(self, input_value, input_types, input_context, expected_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_with_discriminator_custom_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_output: Any + ): try: deserialized_dict_input = ApiHelper.json_deserialize(input_value, as_dict=True) union_type_result = OneOf(input_types, input_context).validate(deserialized_dict_input) @@ -788,8 +815,11 @@ def test_one_of_with_discriminator_custom_type(self, input_value, input_types, i LeafType(Days, UnionTypeContext(is_array=True))], UnionTypeContext(is_array=True), True, [[1, 2], ['Monday', 'Tuesday']]), ]) - def test_one_of_enum_type(self, input_value, input_types, input_context, expected_is_valid_output, - expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_enum_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = OneOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -813,7 +843,12 @@ def test_one_of_enum_type(self, input_value, input_types, input_context, expecte '{} \nActual Value: 100.5\nExpected Type: One Of int, bool, str.'.format( UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)) ]) - def test_one_of_validation_errors(self, input_value, input_types, input_context, expected_validation_message): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_validation_errors( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validation_message: str + ): with pytest.raises(OneOfValidationException) as validation_error: OneOf(input_types, input_context).validate(input_value) + assert validation_error.value.message == expected_validation_message diff --git a/tests/apimatic_core/utility_tests/test_api_helper.py b/tests/apimatic_core/utility_tests/test_api_helper.py index 1154082..c88fe0a 100644 --- a/tests/apimatic_core/utility_tests/test_api_helper.py +++ b/tests/apimatic_core/utility_tests/test_api_helper.py @@ -1,8 +1,11 @@ from datetime import datetime, date +from typing import Optional, Any, Dict, Callable, Tuple, List, Union, Set import pytest +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.types.union_type import UnionType from apimatic_core_interfaces.types.union_type_context import UnionTypeContext -from pydantic import ValidationError +from pydantic import ValidationError, validate_call from apimatic_core.types.array_serialization_format import SerializationFormats from tests.apimatic_core.mocks.models.lion import Lion @@ -12,7 +15,6 @@ from apimatic_core.types.union_types.one_of import OneOf from dateutil.tz import tzutc -from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.types.file_wrapper import FileWrapper from apimatic_core.utilities.api_helper import ApiHelper from tests.apimatic_core.base import Base @@ -22,7 +24,7 @@ ModelWithAdditionalPropertiesOfModelType, ModelWithAdditionalPropertiesOfModelArrayType, \ ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive, ModelWithAdditionalPropertiesOfModelDictType from tests.apimatic_core.mocks.models.person import Employee -from requests.utils import quote +from urllib.parse import quote class TestApiHelper(Base): @@ -41,8 +43,10 @@ class TestApiHelper(Base): Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), ]) - def test_json_serialize_wrapped_params(self, input_value, expected_value): + @validate_call + def test_json_serialize_wrapped_params(self, input_value: Optional[Dict[str, Any]], expected_value: Optional[str]): request_param = ApiHelper.json_serialize_wrapped_params(input_value) + assert request_param == expected_value @pytest.mark.parametrize('input_value, expected_value', [ @@ -63,8 +67,10 @@ def test_json_serialize_wrapped_params(self, input_value, expected_value): (1, '1'), ('1', '1') ]) - def test_json_serialize(self, input_value, expected_value): + @validate_call + def test_json_serialize(self, input_value: Any, expected_value: Optional[str]): serialized_value = ApiHelper.json_serialize(input_value) + assert serialized_value == expected_value @pytest.mark.parametrize('input_json_value, unboxing_function, as_dict, expected_value', [ @@ -113,26 +119,27 @@ def test_json_serialize(self, input_value, expected_value): False, '{"email":"test","prop":100.65}') ]) - def test_json_deserialize(self, input_json_value, unboxing_function, as_dict, expected_value): + @validate_call + def test_json_deserialize( + self, input_json_value: Optional[str], unboxing_function: Optional[Callable[[Any], Any]], + as_dict: bool, expected_value: Any + ): deserialized_value = ApiHelper.json_deserialize(input_json_value, unboxing_function, as_dict) assert ApiHelper.json_serialize(deserialized_value) == expected_value - @pytest.mark.parametrize('input_json_value, unboxing_function, expected_value', [ + @pytest.mark.parametrize('input_json_value, unboxing_function', [ ('{"email": "test", "prop1": 1, "prop2": 2, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveType.model_validate, - '{"email": "test", "prop1": 1, "prop2": 2}'), + ModelWithAdditionalPropertiesOfPrimitiveType.model_validate), ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3], "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate, - '{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3]}'), + ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate), ( '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate, - '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}}'), + ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate), ('{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfModelType.model_validate, - '{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}}') + ModelWithAdditionalPropertiesOfModelType.model_validate) ]) - def test_json_deserialize(self, input_json_value, unboxing_function, expected_value): + @validate_call + def test_invalid_model_json_deserialize(self, input_json_value: Any, unboxing_function: Optional[Callable[[Any], Any]]): with pytest.raises(ValidationError): ApiHelper.json_deserialize(input_json_value, unboxing_function) @@ -140,7 +147,7 @@ def test_json_deserialize(self, input_json_value, unboxing_function, expected_va ('C:\\PYTHON_GENERIC_LIB\\Tester\\models\\test_file.py', "test_file", 'C:\\PYTHON_GENERIC_LIB\\Tester\\schemas\\TestFile.json'), ]) - def test_get_schema_path(self, input_url, input_file_value, expected_value): + def test_get_schema_path(self, input_url: str, input_file_value: str, expected_value: str): assert expected_value == ApiHelper.get_schema_path(input_url, input_file_value) @pytest.mark.parametrize('input_template_params, expected_template_params', [ @@ -161,7 +168,10 @@ def test_get_schema_path(self, input_url, input_file_value, expected_value): ({'template_param': {'value': 'Basic|Test', 'encode': False}}, 'Basic|Test'), ({'template_param': {'value': None, 'encode': False}}, ''), ]) - def test_append_url_with_template_parameters(self, input_template_params, expected_template_params): + @validate_call + def test_append_url_with_template_parameters( + self, input_template_params: Optional[Dict[str, Dict[str, Any]]], expected_template_params: str + ): assert ApiHelper.append_url_with_template_parameters('http://localhost:3000/{template_param}', input_template_params) \ == 'http://localhost:3000/{}'.format(expected_template_params) @@ -172,15 +182,21 @@ def test_append_url_with_template_parameters(self, input_template_params, expect 'template_param3': {'value': 'Basic Test', 'encode': True}, 'template_param4': {'value': 'Basic Test', 'encode': False}, })]) - def test_append_url_with_template_parameters_multiple_values(self, input_template_params): + @validate_call + def test_append_url_with_template_parameters_multiple_values( + self, input_template_params: Optional[Dict[str, Dict[str, Any]]] + ): url = 'http://localhost:3000/{template_param1}/{template_param2}/{template_param3}/{template_param4}' + assert ApiHelper.append_url_with_template_parameters(url, input_template_params) \ == 'http://localhost:3000/Basic%20Test/Basic%22Test/Basic Test/Basic"Test/Basic%20Test/Basic Test' - + @validate_call @pytest.mark.parametrize('input_url, input_template_param_value', [ (None, {'template_param1': {'value': ['Basic Test', 'Basic"Test'], 'encode': True}}) ]) - def test_append_url_with_template_parameters_value_error(self, input_url, input_template_param_value): + def test_append_url_with_template_parameters_value_error( + self, input_url: Optional[str], input_template_param_value: Optional[Dict[str, Dict[str, Any]]] + ): with pytest.raises(ValueError, match="URL is None."): ApiHelper.append_url_with_template_parameters(input_url, input_template_param_value) @@ -193,10 +209,10 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ ({'query_param': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, 'query_param=761117415', SerializationFormats.INDEXED), ({'query_param': Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}, - 'query_param={}'.format(quote(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + 'query_param={}'.format(quote(Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), SerializationFormats.INDEXED), ({'query_param': Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}, - 'query_param={}'.format(quote(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + 'query_param={}'.format(quote(Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), SerializationFormats.INDEXED), ({'query_param': [1, 2, 3, 4]}, 'query_param[0]=1&query_param[1]=2&query_param[2]=3&query_param[3]=4', SerializationFormats.INDEXED), @@ -229,7 +245,7 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ '&query_param[age]=27' '&query_param[birthday]=1994-02-13' '&query_param[birthtime]={}'.format(quote( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[name]=Bob' '&query_param[uid]=1234567' '&query_param[personType]=Empl' @@ -238,14 +254,14 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ '&query_param[dependents][0][age]=12' '&query_param[dependents][0][birthday]=1994-02-13' '&query_param[dependents][0][birthtime]={}'.format(quote( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[dependents][0][name]=John' '&query_param[dependents][0][uid]=7654321' '&query_param[dependents][0][personType]=Per' '&query_param[dependents][0][key1]=value1' '&query_param[dependents][0][key2]=value2' '&query_param[hiredAt]={}'.format(quote( - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[joiningDay]=Monday' '&query_param[salary]=30000' '&query_param[workingDays][0]=Monday' @@ -253,8 +269,11 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ '&query_param[key1]=value1' '&query_param[key2]=value2', SerializationFormats.INDEXED) ]) - def test_append_url_with_query_parameters(self, input_query_param_value, expected_query_param_value, - array_serialization_format): + @validate_call + def test_append_url_with_query_parameters( + self, input_query_param_value: Optional[Dict[str, Any]], expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): if input_query_param_value is None: assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_param_value, array_serialization_format) == 'http://localhost:3000/test' @@ -267,7 +286,9 @@ def test_append_url_with_query_parameters(self, input_query_param_value, expecte @pytest.mark.parametrize('input_url, input_query_param_value', [ (None, {'query_param': 'string'}) ]) - def test_append_url_with_query_parameters_value_error(self, input_url, input_query_param_value): + def test_append_url_with_query_parameters_value_error( + self, input_url: Optional[str], input_query_param_value: Optional[Dict[str, Any]] + ): with pytest.raises(ValueError, match="URL is None."): ApiHelper.append_url_with_query_parameters(input_url, input_query_param_value) @@ -278,15 +299,19 @@ def test_append_url_with_query_parameters_value_error(self, input_url, input_que }, 'query_param=string&query_param2=true&query_param3[0]=1&query_param3[1]=2&query_param3[2]=3', SerializationFormats.INDEXED) ]) - def test_process_complex_query_parameters(self, input_query_params, expected_query_param_value, - array_serialization_format): + @validate_call + def test_process_complex_query_parameters( + self, input_query_params: Optional[Dict[str, Any]], expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_params, array_serialization_format) == 'http://localhost:3000/test?{}'.format( expected_query_param_value) @pytest.mark.parametrize('input_url', [ 'This is not a url']) - def test_clean_url_value_error(self, input_url): + @validate_call + def test_clean_url_value_error(self, input_url: str): with pytest.raises(ValueError, match="Invalid Url format."): ApiHelper.clean_url(input_url) @@ -298,7 +323,8 @@ def test_clean_url_value_error(self, input_url): 'query_param=string&query_param2=True&query_param3[0]=1&query_param3[1]=2query_param3[2]=3',) ]) - def test_clean_url(self, input_url, expected_url): + @validate_call + def test_clean_url(self, input_url: str, expected_url: str): assert ApiHelper.clean_url(input_url) == expected_url @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ @@ -381,7 +407,11 @@ def test_clean_url(self, input_url, expected_url): [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 10.55)], SerializationFormats.INDEXED) ]) - def test_form_params(self, input_form_param_value, expected_form_param_value, array_serialization_format): + @validate_call + def test_form_params( + self, input_form_param_value: Any, expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): key = 'form_param' form_encoded_params = ApiHelper.form_encode(input_form_param_value, key, array_serialization_format) for index, item in enumerate(form_encoded_params): @@ -393,6 +423,7 @@ def test_form_params(self, input_form_param_value, expected_form_param_value, ar else: assert item == expected_form_param_value[index] + @validate_call def test_conflicting_additional_property(self): with pytest.raises(ValueError) as conflictingPropertyError: ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( @@ -411,21 +442,33 @@ def test_conflicting_additional_property(self): ('form_param2[1]', 'true'), ('form_param3[key]', 'string_val')], SerializationFormats.INDEXED) ]) - def test_form_encode_parameters(self, input_form_param_value, expected_form_param_value, - array_serialization_format): + @validate_call + def test_form_encode_parameters( + self, input_form_param_value: Dict[str, Any], expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): assert ApiHelper.form_encode_parameters(input_form_param_value, array_serialization_format) == \ expected_form_param_value @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - (datetime(1994, 2, 13, 5, 30, 15), ApiHelper.RFC3339DateTime, + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime), - (datetime(1994, 2, 13, 5, 30, 15), ApiHelper.HttpDateTime, ApiHelper.HttpDateTime), - (datetime(1994, 2, 13, 5, 30, 15), ApiHelper.UnixDateTime, ApiHelper.UnixDateTime), + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.HttpDateTime, + ApiHelper.HttpDateTime), + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.UnixDateTime, + ApiHelper.UnixDateTime), (500, ApiHelper.UnixDateTime, int), ('500', ApiHelper.UnixDateTime, str), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter(self, input_value, input_converter, expected_obj): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_apply_date_time_converter( + self, input_value: Optional[Union[datetime, int, str]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -435,22 +478,36 @@ def test_apply_date_time_converter(self, input_value, input_converter, expected_ (ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime.from_value('1994-02-13T14:01:54.9571247Z').datetime, '1994-02-13T14:01:54.957124+00:00') ]) - def test_when_defined(self, input_function, input_body, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_when_defined(self, input_function: Callable[[datetime], Any], input_body: datetime, expected_value: str): assert str(ApiHelper.when_defined(input_function, input_body)) == expected_value @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - ([datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.RFC3339DateTime, - [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), - ([datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.HttpDateTime, - [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), - ([datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.UnixDateTime, - [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.RFC3339DateTime, [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.HttpDateTime, [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.UnixDateTime, [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), ([500, 1000], ApiHelper.UnixDateTime, [int, int]), (['500', '1000'], ApiHelper.UnixDateTime, [str, str]), - (['500', datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.UnixDateTime, [str, ApiHelper.UnixDateTime]), + ([ + '500', + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.UnixDateTime, [str, ApiHelper.UnixDateTime]), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter_to_list(self, input_value, input_converter, expected_obj): + @validate_call + def test_apply_date_time_converter_to_list( + self, input_value: Optional[List[Union[datetime, int, str]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -459,18 +516,43 @@ def test_apply_date_time_converter_to_list(self, input_value, input_converter, e assert isinstance(actual_value, expected_obj[index]) @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - ([[datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]], ApiHelper.RFC3339DateTime, - [[ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]]), - ([[datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]], ApiHelper.HttpDateTime, - [[ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]]), - ([[datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]], ApiHelper.UnixDateTime, - [[ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.RFC3339DateTime, + [[ + ApiHelper.RFC3339DateTime, + ApiHelper.RFC3339DateTime + ]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.HttpDateTime, + [[ + ApiHelper.HttpDateTime, + ApiHelper.HttpDateTime + ]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.UnixDateTime, + [[ + ApiHelper.UnixDateTime, + ApiHelper.UnixDateTime + ]]), ([[500, 1000]], ApiHelper.UnixDateTime, [[int, int]]), ([['500', '1000']], ApiHelper.UnixDateTime, [[str, str]]), - ([['500', datetime(1994, 2, 13, 5, 30, 15)]], ApiHelper.UnixDateTime, [[str, ApiHelper.UnixDateTime]]), + ([[ + '500', + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.UnixDateTime, [[str, ApiHelper.UnixDateTime]]), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter_to_list_of_list(self, input_value, input_converter, expected_obj): + @validate_call + def test_apply_date_time_converter_to_list_of_list( + self, input_value: Optional[List[List[Union[datetime, int, str]]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -480,19 +562,35 @@ def test_apply_date_time_converter_to_list_of_list(self, input_value, input_conv assert isinstance(actual_value, expected_obj[outer_index][index]) @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - ({'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}, ApiHelper.RFC3339DateTime, + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.RFC3339DateTime, [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), - ({'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}, ApiHelper.HttpDateTime, + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.HttpDateTime, [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), - ({'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}, ApiHelper.UnixDateTime, + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.UnixDateTime, [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), - ({'key0': '5000', 'key1': datetime(1994, 2, 13, 5, 30, 15)}, ApiHelper.UnixDateTime, + ({ + 'key0': '5000', + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.UnixDateTime, [str, ApiHelper.UnixDateTime]), ({'key0': 5000, 'key1': 10000}, ApiHelper.UnixDateTime, [int, int]), ({'key0': '5000', 'key1': '10000'}, ApiHelper.UnixDateTime, [str, str]), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter_to_dict(self, input_value, input_converter, expected_obj): + @validate_call + def test_apply_date_time_converter_to_dict( + self, input_value: Optional[Dict[str, Union[datetime, int, str]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -501,19 +599,38 @@ def test_apply_date_time_converter_to_dict(self, input_value, input_converter, e assert isinstance(actual_value, expected_obj[index]) @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - ({'key': {'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}}, - ApiHelper.RFC3339DateTime, {'key': [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]}), - ({'key': {'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}}, ApiHelper.HttpDateTime, - {'key': [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]}), - ({'key': {'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}}, ApiHelper.UnixDateTime, - {'key': [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]}), - ({'key': {'key0': '5000', 'key1': datetime(1994, 2, 13, 5, 30, 15)}}, ApiHelper.UnixDateTime, - {'key': [str, ApiHelper.UnixDateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.RFC3339DateTime, {'key': [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.HttpDateTime, {'key': [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.UnixDateTime, {'key': [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]}), + ({ + 'key': { + 'key0': '5000', + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.UnixDateTime, {'key': [str, ApiHelper.UnixDateTime]}), ({'key': {'key0': 5000, 'key1': 10000}}, ApiHelper.UnixDateTime, {'key': [int, int]}), ({'key': {'key0': '5000', 'key1': '10000'}}, ApiHelper.UnixDateTime, {'key': [str, str]}), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter_to_dict_of_dict(self, input_value, input_converter, expected_obj): + @validate_call + def test_apply_date_time_converter_to_dict_of_dict( + self, input_value: Optional[Dict[str, Any]], input_converter: Callable[[Any], Any], expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -563,7 +680,10 @@ def test_apply_date_time_converter_to_dict_of_dict(self, input_value, input_conv [ApiHelper.json_serialize(Base.employee_model()), ApiHelper.json_serialize(Base.employee_model())]), ]) - def test_serialize_array(self, input_array, formatting, is_query, expected_array): + @validate_call + def test_serialize_array( + self, input_array: List[Any], formatting: SerializationFormats, is_query: bool, expected_array: List[Any] + ): serialized_array = ApiHelper.serialize_array('test_array', input_array, formatting, is_query) if ApiHelper.is_custom_type(input_array[0]): assert serialized_array[0][0] == 'test_array[0]' and serialized_array[1][0] == 'test_array[1]' and \ @@ -576,7 +696,10 @@ def test_serialize_array(self, input_array, formatting, is_query, expected_array ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), str(date(1994, 2, 13))], SerializationFormats.TSV, False) ]) - def test_serialize_array_value_error(self, input_array, formatting, is_query): + @validate_call + def test_serialize_array_value_error( + self, input_array: List[Any], formatting: SerializationFormats, is_query: bool + ): with pytest.raises(ValueError) as exception: ApiHelper.serialize_array('key', input_array, formatting, is_query) @@ -587,11 +710,12 @@ def test_serialize_array_value_error(self, input_array, formatting, is_query): (ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))]), [date(1994, 2, 13), date(1994, 2, 13)]) ]) - def test_date_deserialize(self, input_date, expected_date): + @validate_call + def test_date_deserialize(self, input_date: str, expected_date: Union[date, List[date]]): assert ApiHelper.date_deserialize(input_date) == expected_date @pytest.mark.parametrize('input_http_response, input_date_time_format, expected_response_body', [ - (None, None, None), + (None, DateTimeFormat.UNIX_DATE_TIME, None), (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), DateTimeFormat.UNIX_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), (ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), @@ -609,50 +733,61 @@ def test_date_deserialize(self, input_date, expected_date): DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) ]) - def test_date_time_response_body(self, input_http_response, input_date_time_format, expected_response_body): + @validate_call + def test_date_time_response_body( + self, input_http_response: Optional[Union[str, int]], input_date_time_format: DateTimeFormat, + expected_response_body: Optional[Union[datetime, List[datetime]]] + ): assert ApiHelper.datetime_deserialize(input_http_response, input_date_time_format) == expected_response_body @pytest.mark.parametrize('input_file_wrapper, is_file_instance', [ (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), True), ('I am not a file', False) ]) - def test_is_file_wrapper_instance(self, input_file_wrapper, is_file_instance): + @validate_call + def test_is_file_wrapper_instance(self, input_file_wrapper: Union[FileWrapper, str], is_file_instance: bool): assert ApiHelper.is_file_wrapper_instance(input_file_wrapper) == is_file_instance @pytest.mark.parametrize(' input_date, output_date', [ (datetime(1994, 2, 13, 5, 30, 15), Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), ]) - def test_http_datetime_from_datetime(self, input_date, output_date): + @validate_call + def test_http_datetime_from_datetime(self, input_date: datetime, output_date: str): assert ApiHelper.HttpDateTime.from_datetime(input_date) == output_date @pytest.mark.parametrize('input_date, output_date', [ (datetime(1994, 2, 13, 5, 30, 15), Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), ]) - def test_rfc3339_datetime_from_datetime(self, input_date, output_date): + @validate_call + def test_rfc3339_datetime_from_datetime(self, input_date: datetime, output_date: str): assert ApiHelper.RFC3339DateTime.from_datetime(input_date) == output_date @pytest.mark.parametrize('input_date, output_date', [ (datetime(1994, 2, 13, 5, 30, 15), 761117415), ]) - def test_unix_datetime_from_datetime(self, input_date, output_date): + @validate_call + def test_unix_datetime_from_datetime(self, input_date: datetime, output_date: int): assert ApiHelper.UnixDateTime.from_datetime(input_date) == output_date @pytest.mark.parametrize('input_date, output_date', [ (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), datetime(1994, 2, 13, 5, 30, 15)), ]) - def test_http_datetime_from_value(self, input_date, output_date): + @validate_call + def test_http_datetime_from_value(self, input_date: str, output_date: datetime): assert ApiHelper.HttpDateTime.from_value(input_date).datetime == output_date @pytest.mark.parametrize('input_date, output_date', [ ('1994-02-13T14:01:54.9571247Z', datetime(1994, 2, 13, 14, 1, 54, 957124, tzinfo=tzutc())), ]) - def test_rfc3339_from_value(self, input_date, output_date): + @validate_call + def test_rfc3339_from_value(self, input_date: str, output_date: datetime): assert ApiHelper.RFC3339DateTime.from_value(input_date).datetime == output_date @pytest.mark.parametrize('input_date, output_date', [ (1484719381, datetime(2017, 1, 18, 6, 3, 1)), ]) - def test_unix_datetime_from_value(self, input_date, output_date): + @validate_call + def test_unix_datetime_from_value(self, input_date: int, output_date: datetime): assert ApiHelper.UnixDateTime.from_value(input_date).datetime == output_date @pytest.mark.parametrize('input_value, output_value', [ @@ -663,7 +798,8 @@ def test_unix_datetime_from_value(self, input_date, output_date): ('string', 'text/plain; charset=utf-8'), (Base.employee_model(), 'application/json; charset=utf-8'), ]) - def test_get_content_type(self, input_value, output_value): + @validate_call + def test_get_content_type(self, input_value: Any, output_value: Optional[str]): assert ApiHelper.get_content_type(input_value) == output_value @pytest.mark.parametrize('input_value, output_value', [ @@ -673,7 +809,8 @@ def test_get_content_type(self, input_value, output_value): ('', None), (' ', None) ]) - def test_dynamic_deserialize(self, input_value, output_value): + @validate_call + def test_dynamic_deserialize(self, input_value: Optional[str], output_value: Any): assert ApiHelper.dynamic_deserialize(input_value) == output_value @pytest.mark.parametrize('input_placeholders, input_values, input_template, expected_message', [ @@ -686,7 +823,11 @@ def test_dynamic_deserialize(self, input_value, output_value): ({'{accept}'}, {'accept': 'application/json'}, 'Test template -- {accept}', 'Test template -- application/json') ]) - def test_resolve_template_placeholders(self, input_placeholders, input_values, input_template, expected_message): + @validate_call + def test_resolve_template_placeholders( + self, input_placeholders: Set[str], input_values: Union[str, Dict[str, Any]], + input_template: str, expected_message: str + ): actual_message = ApiHelper.resolve_template_placeholders(input_placeholders, input_values, input_template) assert actual_message == expected_message @@ -728,8 +869,11 @@ def test_resolve_template_placeholders(self, input_placeholders, input_values, i 'Test template -- {$response.body}', 'Test template -- ') ]) - def test_resolve_template_placeholders_using_json_pointer(self, input_placeholders, input_value, input_template, - expected_message): + @validate_call + def test_resolve_template_placeholders_using_json_pointer( + self, input_placeholders: Set[str], input_value: Optional[Dict[str, Any]], input_template: str, + expected_message: str + ): actual_message = ApiHelper.resolve_template_placeholders_using_json_pointer( input_placeholders, input_value, input_template) assert actual_message == expected_message @@ -739,7 +883,10 @@ def test_resolve_template_placeholders_using_json_pointer(self, input_placeholde (OneOf([LeafType(int), LeafType(str)], UnionTypeContext(is_array=True)), '[100, "200"]', True, [100, '200']), ]) - def test_union_type_deserialize(self, input_union_type, input_value, input_should_deserialize, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_union_type_deserialize( + self, input_union_type: UnionType, input_value: Any, input_should_deserialize: bool, expected_value: Any + ): actual_value = ApiHelper.deserialize_union_type(input_union_type, input_value, input_should_deserialize) assert actual_value == expected_value @@ -747,14 +894,16 @@ def test_union_type_deserialize(self, input_union_type, input_value, input_shoul ("https://www.example.com/path/to/resource?param1=value1¶m2=value2", "https://www.example.com/path/to/resource"), ("https://www.example.com/path/to/resource", "https://www.example.com/path/to/resource") ]) - def test_get_url_without_query(self, input_url, expected_url): + @validate_call + def test_get_url_without_query(self, input_url: str, expected_url: str): assert ApiHelper.get_url_without_query(input_url) == expected_url @pytest.mark.parametrize('input_url, expected_error_message', [ ("", "Error parsing URL: Invalid URL format"), ("invalid_url", "Error parsing URL: Invalid URL format") ]) - def test_get_url_without_query_with_invalid_url(self, input_url, expected_error_message): + @validate_call + def test_get_url_without_query_with_invalid_url(self, input_url: str, expected_error_message: str): with pytest.raises(ValueError) as excinfo: ApiHelper.get_url_without_query(input_url) assert str(excinfo.value) == expected_error_message @@ -765,69 +914,9 @@ def test_get_url_without_query_with_invalid_url(self, input_url, expected_error_ (["HELLO"], ["hello"]), (["hElLo", "worLd"], ["hello", "world"]) ]) - def test_to_lower_case(self, input_list, expected_output): + @validate_call + def test_to_lower_case(self, input_list: Optional[List[str]], expected_output: Optional[List[str]]): """Tests if an empty list returns an empty list.""" actual_output = ApiHelper.to_lower_case(input_list) - assert actual_output == expected_output - - @pytest.mark.parametrize( - "dictionary, expected_result, unboxing_func, is_array, is_dict", - [ - ({}, {}, lambda x: int(x), False, False), - ({"a": 1, "b": 2}, {"a": 1, "b": 2}, lambda x: int(x), False, False), - ({"a": "1", "b": "2"}, {"a": "1", "b": "2"}, lambda x: str(x), False, False), - ({"a": "Test 1", "b": "Test 2"}, {}, lambda x: int(x), False, False), - ({"a": [1, 2], "b": [3, 4]}, {"a": [1, 2], "b": [3, 4]}, lambda x: int(x), True, False), - ({"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}}, {"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}}, lambda x: int(x), False, True), - ], - ) - def test_get_additional_properties_success(self, dictionary, expected_result, unboxing_func, is_array, is_dict): - result = ApiHelper.get_additional_properties( - dictionary, lambda x: ApiHelper.apply_unboxing_function( - x, unboxing_func, is_array, is_dict)) - assert result == expected_result - - @pytest.mark.parametrize( - "dictionary", - [ - ({"a": None}), - ({"a": lambda x: x}), - ], - ) - def test_get_additional_properties_exception(self, dictionary): - result = ApiHelper.get_additional_properties(dictionary, ApiHelper.apply_unboxing_function) - assert result == {} # expected result when exception occurs - - @pytest.mark.parametrize( - "value, unboxing_function, is_array, is_dict, is_array_of_map, is_map_of_array, dimension_count, expected", - [ - # Test case 1: Simple object - (5, lambda x: x * 2, False, False, False, False, 0, 10), - # Test case 2: Array - ([1, 2, 3], lambda x: x * 2, True, False, False, False, 0, [2, 4, 6]), - # Test case 3: Dictionary - ({"a": 1, "b": 2}, lambda x: x * 2, False, True, False, False, 0, {"a": 2, "b": 4}), - # Test case 4: Array of maps - ([{"a": 1}, {"b": 2}], lambda x: x * 2, True, False, True, False, 0, [{"a": 2}, {"b": 4}]), - # Test case 5: Map of arrays - ({"a": [1, 2], "b": [3, 4]}, lambda x: x * 2, False, True, False, True, 0, {"a": [2, 4], "b": [6, 8]}), - # Test case 6: Multi-dimensional array - ([[1], [2, 3], [4]], lambda x: x * 2, True, False, False, False, 2, [[2], [4, 6], [8]]), - # Test case 7: Array of arrays - ([[1, 2], [3, 4]], lambda x: x * 2, True, False, False, False, 2, [[2, 4], [6, 8]]), - # Test case 8: Array of arrays of arrays - ([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], lambda x: x * 2, True, False, False, False, 3, - [[[2, 4], [6, 8]], [[10, 12], [14, 16]]]), - ], - ) - def test_apply_unboxing_function(self, value, unboxing_function, is_array, is_dict, - is_array_of_map, is_map_of_array, dimension_count, expected): - result = ApiHelper.apply_unboxing_function( - value, - unboxing_function, - is_array, - is_dict, - is_array_of_map, - is_map_of_array, - dimension_count) - assert result == expected \ No newline at end of file + + assert actual_output == expected_output \ No newline at end of file diff --git a/tests/apimatic_core/utility_tests/test_api_helper.py~ b/tests/apimatic_core/utility_tests/test_api_helper.py~ new file mode 100644 index 0000000..cba74c2 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_api_helper.py~ @@ -0,0 +1,922 @@ +from datetime import datetime, date +from typing import Optional, Any, Dict, Callable, Tuple, List, Union, Set + +import pytest +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.types.union_type import UnionType +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import ValidationError, validate_call + +from apimatic_core.types.array_serialization_format import SerializationFormats +from tests.apimatic_core.mocks.models.lion import Lion + + +from apimatic_core.types.union_types.leaf_type import LeafType +from apimatic_core.types.union_types.one_of import OneOf +from dateutil.tz import tzutc + +from apimatic_core.types.file_wrapper import FileWrapper +from apimatic_core.utilities.api_helper import ApiHelper +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.models.model_with_additional_properties import \ + ModelWithAdditionalPropertiesOfPrimitiveType, \ + ModelWithAdditionalPropertiesOfPrimitiveArrayType, ModelWithAdditionalPropertiesOfPrimitiveDictType, \ + ModelWithAdditionalPropertiesOfModelType, ModelWithAdditionalPropertiesOfModelArrayType, \ + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive, ModelWithAdditionalPropertiesOfModelDictType +from tests.apimatic_core.mocks.models.person import Employee +from urllib.parse import quote + + +class TestApiHelper(Base): + + @pytest.mark.parametrize('input_value, expected_value', [ + (None, None), + (Base.wrapped_parameters(), '{{"bodyScalar": true, "bodyNonScalar": {{"address": "street abc", "age": 27, ' + '"birthday": "1994-02-13", "birthtime": "{0}", "name": "Bob", "uid": "1234567", ' + '"personType": "Empl", "department": "IT", "dependents": ' + '[{{"address": "street abc", "age": 12, "birthday": ' + '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' + '"7654321", "personType": "Per", "key1": "value1", "key2": "value2"}}], ' + '"hiredAt": "{1}", "joiningDay": "Monday", ' + '"salary": 30000, "workingDays": ["Monday", ' + '"Tuesday"], "key1": "value1", "key2": "value2"}}}}'.format( + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + ]) + @validate_call + def test_json_serialize_wrapped_params(self, input_value: Optional[Dict[str, Any]], expected_value: Optional[str]): + request_param = ApiHelper.json_serialize_wrapped_params(input_value) + + assert request_param == expected_value + + @pytest.mark.parametrize('input_value, expected_value', [ + (None, None), + ([Base.employee_model(), Base.employee_model()], + '[{0}, {0}]'.format(Base.employee_model_str())), + ([[Base.employee_model(), Base.employee_model()], [Base.employee_model(), Base.employee_model()]], + '[[{0}, {0}], [{0}, {0}]]'.format(Base.employee_model_str())), + ({'key0': [Base.employee_model(), Base.employee_model()], + 'key1': [Base.employee_model(), Base.employee_model()]}, + '{{"key0": [{0}, {0}], "key1": [{0}, {0}]}}'.format(Base.employee_model_str())), + ([1, 2, 3], '[1, 2, 3]'), + ({'key0': 1, 'key1': 'abc'}, '{"key0": 1, "key1": "abc"}'), + ([[1, 2, 3], ['abc', 'def']], '[[1, 2, 3], ["abc", "def"]]'), + ([{'key0': [1, 2, 3]}, {'key1': ['abc', 'def']}], '[{"key0": [1, 2, 3]}, {"key1": ["abc", "def"]}]'), + ({'key0': [1, 2, 3], 'key1': ['abc', 'def']}, '{"key0": [1, 2, 3], "key1": ["abc", "def"]}'), + (Base.employee_model(), Base.employee_model_str(beautify_with_spaces=False)), + (1, '1'), + ('1', '1') + ]) + @validate_call + def test_json_serialize(self, input_value: Any, expected_value: Optional[str]): + serialized_value = ApiHelper.json_serialize(input_value) + + assert serialized_value == expected_value + + @pytest.mark.parametrize('input_json_value, unboxing_function, as_dict, expected_value', [ + (None, None, False, None), + ('true', None, False, 'true'), + ('', None, False, None), + (' ', None, False, None), + (ApiHelper.json_serialize(Base.employee_model()), Employee.model_validate, False, + ApiHelper.json_serialize(Base.employee_model())), + (ApiHelper.json_serialize([Base.employee_model(), Base.employee_model()]), + Employee.model_validate, False, + ApiHelper.json_serialize([Base.employee_model(), Base.employee_model()])), + (ApiHelper.json_serialize({'key1': Base.employee_model(), 'key2': Base.employee_model()}), + Employee.model_validate, True, '{{"key1": {0}, "key2": {0}}}'.format(Base.employee_model_str())), + ('{"email": "test", "prop1": 1, "prop2": 2}', + ModelWithAdditionalPropertiesOfPrimitiveType.model_validate, False, + '{"email":"test","prop1":1,"prop2":2}'), + ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3]}', + ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate, False, + '{"email":"test","prop1":[1,2,3],"prop2":[1,2,3]}'), + ('{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}}', + ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate, False, + '{"email":"test","prop1":{"inner_prop1":1,"inner_prop2":2},"prop2":{"inner_prop1":1,"inner_prop2":2}}'), + ('{"email": "test", "prop1": {"id": "1", "weight": 50, "type": "Lion"}}', + ModelWithAdditionalPropertiesOfModelType.model_validate, + False, + '{"email":"test","prop1":{"id":"1","weight":50,"type":"Lion"}}'), + ('{"email": "test", "prop": [{"id": "1", "weight": 50, "type": "Lion"}, {"id": "2", "weight": 100, "type": "Lion"}]}', + ModelWithAdditionalPropertiesOfModelArrayType.model_validate, + False, + '{"email":"test","prop":[{"id":"1","weight":50,"type":"Lion"},{"id":"2","weight":100,"type":"Lion"}]}'), + ('{"email": "test", "prop": {"inner prop 1": {"id": "1", "weight": 50, "type": "Lion"}, "inner prop 2": {"id": "2", "weight": 100, "type": "Lion"}}}', + ModelWithAdditionalPropertiesOfModelDictType.model_validate, + False, + '{"email":"test","prop":{"inner prop 1":{"id":"1","weight":50,"type":"Lion"},"inner prop 2":{"id":"2","weight":100,"type":"Lion"}}}'), + ('{"email": "test", "prop": true}', + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, + False, + '{"email":"test","prop":true}'), + ('{"email": "test", "prop": 100.65}', + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, + False, + '{"email":"test","prop":100.65}'), + ('{"email": "test", "prop": "100.65"}', + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, + False, + '{"email":"test","prop":100.65}') + ]) + @validate_call + def test_json_deserialize( + self, input_json_value: Optional[str], unboxing_function: Optional[Callable[[Any], Any]], + as_dict: bool, expected_value: Any + ): + deserialized_value = ApiHelper.json_deserialize(input_json_value, unboxing_function, as_dict) + assert ApiHelper.json_serialize(deserialized_value) == expected_value + + @pytest.mark.parametrize('input_json_value, unboxing_function', [ + ('{"email": "test", "prop1": 1, "prop2": 2, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveType.model_validate), + ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3], "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate), + ( + '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate), + ('{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfModelType.model_validate) + ]) + @validate_call + def test_invalid_model_json_deserialize(self, input_json_value: Any, unboxing_function: Optional[Callable[[Any], Any]]): + with pytest.raises(ValidationError): + ApiHelper.json_deserialize(input_json_value, unboxing_function) + + @pytest.mark.parametrize('input_url, input_file_value, expected_value', [ + ('C:\\PYTHON_GENERIC_LIB\\Tester\\models\\test_file.py', "test_file", + 'C:\\PYTHON_GENERIC_LIB\\Tester\\schemas\\TestFile.json'), + ]) + def test_get_schema_path(self, input_url: str, input_file_value: str, expected_value: str): + assert expected_value == ApiHelper.get_schema_path(input_url, input_file_value) + + @pytest.mark.parametrize('input_template_params, expected_template_params', [ + (None, '{template_param}'), + ({'template_param': {'value': 'Basic Test', 'encode': True}}, 'Basic%20Test'), + ({'template_param': {'value': 'Basic"Test', 'encode': True}}, 'Basic%22Test'), + ({'template_param': {'value': 'BasicTest', 'encode': True}}, 'Basic%3ETest'), + ({'template_param': {'value': 'Basic#Test', 'encode': True}}, 'Basic%23Test'), + ({'template_param': {'value': 'Basic%Test', 'encode': True}}, 'Basic%25Test'), + ({'template_param': {'value': 'Basic|Test', 'encode': True}}, 'Basic%7CTest'), + ({'template_param': {'value': 'Basic Test', 'encode': False}}, 'Basic Test'), + ({'template_param': {'value': 'Basic"Test', 'encode': False}}, 'Basic"Test'), + ({'template_param': {'value': 'BasicTest', 'encode': False}}, 'Basic>Test'), + ({'template_param': {'value': 'Basic#Test', 'encode': False}}, 'Basic#Test'), + ({'template_param': {'value': 'Basic%Test', 'encode': False}}, 'Basic%Test'), + ({'template_param': {'value': 'Basic|Test', 'encode': False}}, 'Basic|Test'), + ({'template_param': {'value': None, 'encode': False}}, ''), + ]) + @validate_call + def test_append_url_with_template_parameters( + self, input_template_params: Optional[Dict[str, Dict[str, Any]]], expected_template_params: str + ): + assert ApiHelper.append_url_with_template_parameters('http://localhost:3000/{template_param}', + input_template_params) \ + == 'http://localhost:3000/{}'.format(expected_template_params) + + @pytest.mark.parametrize('input_template_params', [ + ({'template_param1': {'value': ['Basic Test', 'Basic"Test'], 'encode': True}, + 'template_param2': {'value': ['Basic Test', 'Basic"Test'], 'encode': False}, + 'template_param3': {'value': 'Basic Test', 'encode': True}, + 'template_param4': {'value': 'Basic Test', 'encode': False}, + })]) + @validate_call + def test_append_url_with_template_parameters_multiple_values( + self, input_template_params: Optional[Dict[str, Dict[str, Any]]] + ): + url = 'http://localhost:3000/{template_param1}/{template_param2}/{template_param3}/{template_param4}' + + assert ApiHelper.append_url_with_template_parameters(url, input_template_params) \ + == 'http://localhost:3000/Basic%20Test/Basic%22Test/Basic Test/Basic"Test/Basic%20Test/Basic Test' + @validate_call + @pytest.mark.parametrize('input_url, input_template_param_value', [ + (None, {'template_param1': {'value': ['Basic Test', 'Basic"Test'], 'encode': True}}) + ]) + def test_append_url_with_template_parameters_value_error( + self, input_url: Optional[str], input_template_param_value: Optional[Dict[str, Dict[str, Any]]] + ): + with pytest.raises(ValueError, match="URL is None."): + ApiHelper.append_url_with_template_parameters(input_url, input_template_param_value) + + @pytest.mark.parametrize('input_query_param_value, expected_query_param_value, array_serialization_format', [ + (None, '', SerializationFormats.INDEXED), + ({'query_param': "string"}, 'query_param=string', SerializationFormats.INDEXED), + ({'query_param': 500}, 'query_param=500', SerializationFormats.INDEXED), + ({'query_param': 500.12}, 'query_param=500.12', SerializationFormats.INDEXED), + ({'query_param': date(1994, 2, 13)}, 'query_param=1994-02-13', SerializationFormats.INDEXED), + ({'query_param': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'query_param=761117415', SerializationFormats.INDEXED), + ({'query_param': Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'query_param={}'.format(quote(Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + SerializationFormats.INDEXED), + ({'query_param': Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'query_param={}'.format(quote(Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + SerializationFormats.INDEXED), + ({'query_param': [1, 2, 3, 4]}, 'query_param[0]=1&query_param[1]=2&query_param[2]=3&query_param[3]=4', + SerializationFormats.INDEXED), + ({'query_param': [1, 2, 3, 4]}, 'query_param[]=1&query_param[]=2&query_param[]=3&query_param[]=4', + SerializationFormats.UN_INDEXED), + ({'query_param': [1, 2, 3, 4]}, 'query_param=1&query_param=2&query_param=3&query_param=4', + SerializationFormats.PLAIN), + ({'query_param': [1, 2, 3, 4]}, 'query_param=1%2C2%2C3%2C4', SerializationFormats.CSV), + ({'query_param': [1, 2, 3, 4]}, 'query_param=1%7C2%7C3%7C4', SerializationFormats.PSV), + ({'query_param': [1, 2, 3, 4]}, 'query_param=1%092%093%094', SerializationFormats.TSV), + ({'query_param': {'key1': 'value1', 'key2': 'value2'}}, 'query_param[key1]=value1&query_param[key2]=value2', + SerializationFormats.INDEXED), + ({'query_param': 1, 'query_param_none': None, 'query_param2': 2}, 'query_param=1&query_param2=2', + SerializationFormats.INDEXED), + ({'query_param': {'key1': 'value1', 'key2': [1, 2, 3, 4]}}, + 'query_param[key1]=value1' + '&query_param[key2][0]=1' + '&query_param[key2][1]=2' + '&query_param[key2][2]=3' + '&query_param[key2][3]=4', SerializationFormats.INDEXED), + ({'query_param': {'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}}, + 'query_param[key1]=value1' + '&query_param[key2][0]=1' + '&query_param[key2][1]=2' + '&query_param[key2][2]=3' + '&query_param[key2][3][key1]=value1' + '&query_param[key2][3][key2]=value2', SerializationFormats.INDEXED), + ({'query_param': Base.employee_model()}, + 'query_param[address]=street%20abc' + '&query_param[age]=27' + '&query_param[birthday]=1994-02-13' + '&query_param[birthtime]={}'.format(quote( + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[name]=Bob' + '&query_param[uid]=1234567' + '&query_param[personType]=Empl' + '&query_param[department]=IT' + '&query_param[dependents][0][address]=street%20abc' + '&query_param[dependents][0][age]=12' + '&query_param[dependents][0][birthday]=1994-02-13' + '&query_param[dependents][0][birthtime]={}'.format(quote( + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[dependents][0][name]=John' + '&query_param[dependents][0][uid]=7654321' + '&query_param[dependents][0][personType]=Per' + '&query_param[dependents][0][key1]=value1' + '&query_param[dependents][0][key2]=value2' + '&query_param[hiredAt]={}'.format(quote( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[joiningDay]=Monday' + '&query_param[salary]=30000' + '&query_param[workingDays][0]=Monday' + '&query_param[workingDays][1]=Tuesday' + '&query_param[key1]=value1' + '&query_param[key2]=value2', SerializationFormats.INDEXED) + ]) + @validate_call + def test_append_url_with_query_parameters( + self, input_query_param_value: Optional[Dict[str, Any]], expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): + if input_query_param_value is None: + assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_param_value, + array_serialization_format) == 'http://localhost:3000/test' + else: + + assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_param_value, + array_serialization_format) == \ + 'http://localhost:3000/test?{}'.format(expected_query_param_value) + + @pytest.mark.parametrize('input_url, input_query_param_value', [ + (None, {'query_param': 'string'}) + ]) + def test_append_url_with_query_parameters_value_error( + self, input_url: Optional[str], input_query_param_value: Optional[Dict[str, Any]] + ): + with pytest.raises(ValueError, match="URL is None."): + ApiHelper.append_url_with_query_parameters(input_url, input_query_param_value) + + @pytest.mark.parametrize('input_query_params, expected_query_param_value, array_serialization_format', [ + ({'query_param': "string", + 'query_param2': True, + 'query_param3': [1, 2, 3] + }, 'query_param=string&query_param2=true&query_param3[0]=1&query_param3[1]=2&query_param3[2]=3', + SerializationFormats.INDEXED) + ]) + @validate_call + def test_process_complex_query_parameters( + self, input_query_params: Optional[Dict[str, Any]], expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): + assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_params, + array_serialization_format) == 'http://localhost:3000/test?{}'.format( + expected_query_param_value) + + @pytest.mark.parametrize('input_url', [ + 'This is not a url']) + @validate_call + def test_clean_url_value_error(self, input_url: str): + with pytest.raises(ValueError, match="Invalid Url format."): + ApiHelper.clean_url(input_url) + + @pytest.mark.parametrize('input_url, expected_url', [ + ('http://localhost:3000//test', 'http://localhost:3000/test'), + ('http://localhost:3000//test?' + 'query_param=string&query_param2=True&query_param3[0]=1&query_param3[1]=2query_param3[2]=3', + 'http://localhost:3000/test?' + 'query_param=string&query_param2=True&query_param3[0]=1&query_param3[1]=2query_param3[2]=3',) + + ]) + @validate_call + def test_clean_url(self, input_url: str, expected_url: str): + assert ApiHelper.clean_url(input_url) == expected_url + + @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ + (None, [], SerializationFormats.INDEXED), + ('string', [('form_param', 'string')], SerializationFormats.INDEXED), + (500, [('form_param', 500)], SerializationFormats.INDEXED), + (500.12, [('form_param', 500.12)], SerializationFormats.INDEXED), + (str(date(1994, 2, 13)), [('form_param', '1994-02-13')], SerializationFormats.INDEXED), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', 761117415)], SerializationFormats.INDEXED), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))], SerializationFormats.INDEXED), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))], SerializationFormats.INDEXED), + ([1, 2, 3, 4], [('form_param[0]', 1), ('form_param[1]', 2), ('form_param[2]', 3), ('form_param[3]', 4)], + SerializationFormats.INDEXED), + ([1, 2, 3, 4], [('form_param[]', 1), ('form_param[]', 2), ('form_param[]', 3), ('form_param[]', 4)], + SerializationFormats.UN_INDEXED), + ([1, 2, 3, 4], [('form_param', 1), ('form_param', 2), ('form_param', 3), ('form_param', 4)], + SerializationFormats.PLAIN), + ({'key1': 'value1', 'key2': 'value2'}, [('form_param[key1]', 'value1'), ('form_param[key2]', 'value2')], + SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, + [('form_param[key1]', 'value1'), ('form_param[key2][0]', 1), ('form_param[key2][1]', 2), + ('form_param[key2][2]', 3), ('form_param[key2][3]', 4)], SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, + [('form_param[key1]', 'value1'), ('form_param[key2][0]', 1), ('form_param[key2][1]', 2), + ('form_param[key2][2]', 3), ('form_param[key2][3][key1]', 'value1'), ('form_param[key2][3][key2]', 'value2')], + SerializationFormats.INDEXED), + (Base.employee_model(), + [('form_param[address]', 'street abc'), ('form_param[age]', 27), ('form_param[birthday]', '1994-02-13'), + ('form_param[birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[name]', 'Bob'), ('form_param[uid]', '1234567'), ('form_param[personType]', 'Empl'), + ('form_param[department]', 'IT'), ('form_param[dependents][0][address]', 'street abc'), + ('form_param[dependents][0][age]', 12), ('form_param[dependents][0][birthday]', '1994-02-13'), + ('form_param[dependents][0][birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[dependents][0][name]', 'John'), ('form_param[dependents][0][uid]', '7654321'), + ('form_param[dependents][0][personType]', 'Per'), ('form_param[dependents][0][key1]', 'value1'), + ('form_param[dependents][0][key2]', 'value2'), + ('form_param[hiredAt]', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[joiningDay]', 'Monday'), ('form_param[salary]', 30000), ('form_param[workingDays][0]', 'Monday'), + ('form_param[workingDays][1]', 'Tuesday'), ('form_param[key1]', 'value1'), + ('form_param[key2]', 'value2'),], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveType(email='test@gmail.com', additional_properties={'prop': 20}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 20)], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveArrayType( + email='test@gmail.com', additional_properties={'prop': [20, 30]}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][0]', 20), ('form_param[prop][1]', 30)], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveDictType( + email='test@gmail.com', additional_properties={'prop': {'inner prop 1': 20, 'inner prop 2': 30}}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][inner prop 1]', 20), ('form_param[prop][inner prop 2]', 30)], + SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfModelType( + email='test@gmail.com', additional_properties={'prop': Lion(id='leo', weight=5, mtype='Lion')}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][id]', 'leo'), ('form_param[prop][weight]', 5), ('form_param[prop][type]', 'Lion')], + SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfModelArrayType( + email='test@gmail.com', + additional_properties={'prop': [Lion(id='leo 1', weight=5, mtype='Lion'), Lion(id='leo 2', weight=10, mtype='Lion')]}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][0][id]', 'leo 1'), ('form_param[prop][0][weight]', 5), + ('form_param[prop][0][type]', 'Lion'), ('form_param[prop][1][id]', 'leo 2'), ('form_param[prop][1][weight]', 10), + ('form_param[prop][1][type]', 'Lion')], + SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfModelDictType( + email='test@gmail.com', + additional_properties={ + 'prop': { + 'leo 1': Lion(id='leo 1', weight=5, mtype='Lion'), + 'leo 2': Lion(id='leo 2', weight=10, mtype='Lion') + } + }), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][leo 1][id]', 'leo 1'), + ('form_param[prop][leo 1][weight]', 5), + ('form_param[prop][leo 1][type]', 'Lion'), ('form_param[prop][leo 2][id]', 'leo 2'), + ('form_param[prop][leo 2][weight]', 10), + ('form_param[prop][leo 2][type]', 'Lion')], + SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( + email='test@gmail.com', additional_properties={'prop': 10.55}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 10.55)], + SerializationFormats.INDEXED) + ]) + @validate_call + def test_form_params( + self, input_form_param_value: Any, expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): + key = 'form_param' + form_encoded_params = ApiHelper.form_encode(input_form_param_value, key, array_serialization_format) + for index, item in enumerate(form_encoded_params): + # form encoding stores the datetime object so converting datetime to string for assertions as assertions + # do not work for objects + if isinstance(item[1], ApiHelper.CustomDate): + assert item[0] == expected_form_param_value[index][0] \ + and item[1].value == expected_form_param_value[index][1].value + else: + assert item == expected_form_param_value[index] + + @validate_call + def test_conflicting_additional_property(self): + with pytest.raises(ValueError) as conflictingPropertyError: + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( + email='test@gmail.com', additional_properties={'email': 10.55}) + + assert ("Invalid additional properties: {'email'}. These names conflict with existing model properties." + in str(conflictingPropertyError.value)) + + + @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ + ({'form_param1': 'string', + 'form_param2': ['string', True], + 'form_param3': {'key': 'string_val'}, + }, [('form_param1', 'string'), + ('form_param2[0]', 'string'), + ('form_param2[1]', 'true'), + ('form_param3[key]', 'string_val')], SerializationFormats.INDEXED) + ]) + @validate_call + def test_form_encode_parameters( + self, input_form_param_value: Dict[str, Any], expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): + assert ApiHelper.form_encode_parameters(input_form_param_value, array_serialization_format) == \ + expected_form_param_value + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.RFC3339DateTime, + ApiHelper.RFC3339DateTime), + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.HttpDateTime, + ApiHelper.HttpDateTime), + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.UnixDateTime, + ApiHelper.UnixDateTime), + (500, ApiHelper.UnixDateTime, int), + ('500', ApiHelper.UnixDateTime, str), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_apply_date_time_converter( + self, input_value: Optional[Union[datetime, int, str]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + assert isinstance(ApiHelper.apply_datetime_converter(input_value, input_converter), expected_obj) + + @pytest.mark.parametrize('input_function, input_body, expected_value', [ + (ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime.from_value('1994-02-13T14:01:54.9571247Z').datetime, + '1994-02-13T14:01:54.957124+00:00') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_when_defined(self, input_function: Callable[[datetime], Any], input_body: datetime, expected_value: str): + assert str(ApiHelper.when_defined(input_function, input_body)) == expected_value + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.RFC3339DateTime, [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.HttpDateTime, [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.UnixDateTime, [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), + ([500, 1000], ApiHelper.UnixDateTime, [int, int]), + (['500', '1000'], ApiHelper.UnixDateTime, [str, str]), + ([ + '500', + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.UnixDateTime, [str, ApiHelper.UnixDateTime]), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call + def test_apply_date_time_converter_to_list( + self, input_value: Optional[List[Union[datetime, int, str]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + actual_converted_value = ApiHelper.apply_datetime_converter(input_value, input_converter) + for index, actual_value in enumerate(actual_converted_value): + assert isinstance(actual_value, expected_obj[index]) + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.RFC3339DateTime, + [[ + ApiHelper.RFC3339DateTime, + ApiHelper.RFC3339DateTime + ]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.HttpDateTime, + [[ + ApiHelper.HttpDateTime, + ApiHelper.HttpDateTime + ]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.UnixDateTime, + [[ + ApiHelper.UnixDateTime, + ApiHelper.UnixDateTime + ]]), + ([[500, 1000]], ApiHelper.UnixDateTime, [[int, int]]), + ([['500', '1000']], ApiHelper.UnixDateTime, [[str, str]]), + ([[ + '500', + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.UnixDateTime, [[str, ApiHelper.UnixDateTime]]), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call + def test_apply_date_time_converter_to_list_of_list( + self, input_value: Optional[List[List[Union[datetime, int, str]]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + actual_converted_value = ApiHelper.apply_datetime_converter(input_value, input_converter) + for outer_index, actual_outer_value in enumerate(actual_converted_value): + for index, actual_value in enumerate(actual_outer_value): + assert isinstance(actual_value, expected_obj[outer_index][index]) + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.RFC3339DateTime, + [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.HttpDateTime, + [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.UnixDateTime, + [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), + ({ + 'key0': '5000', + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.UnixDateTime, + [str, ApiHelper.UnixDateTime]), + ({'key0': 5000, 'key1': 10000}, ApiHelper.UnixDateTime, [int, int]), + ({'key0': '5000', 'key1': '10000'}, ApiHelper.UnixDateTime, [str, str]), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call + def test_apply_date_time_converter_to_dict( + self, input_value: Optional[Dict[str, Union[datetime, int, str]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + actual_converted_value = ApiHelper.apply_datetime_converter(input_value, input_converter) + for index, actual_value in enumerate(actual_converted_value.values()): + assert isinstance(actual_value, expected_obj[index]) + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.RFC3339DateTime, {'key': [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.HttpDateTime, {'key': [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.UnixDateTime, {'key': [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]}), + ({ + 'key': { + 'key0': '5000', + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.UnixDateTime, {'key': [str, ApiHelper.UnixDateTime]}), + ({'key': {'key0': 5000, 'key1': 10000}}, ApiHelper.UnixDateTime, {'key': [int, int]}), + ({'key': {'key0': '5000', 'key1': '10000'}}, ApiHelper.UnixDateTime, {'key': [str, str]}), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call + def test_apply_date_time_converter_to_dict_of_dict( + self, input_value: Optional[Dict[str, Any]], input_converter: Callable[[Any], Any], expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + actual_converted_value = ApiHelper.apply_datetime_converter(input_value, input_converter) + for outer_key, actual_outer_value in actual_converted_value.items(): + for index, actual_value in enumerate(actual_outer_value.values()): + assert isinstance(actual_value, expected_obj[outer_key][index]) + + @pytest.mark.parametrize('input_array, formatting, is_query, expected_array', [ + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.INDEXED, False, [('test_array[0]', 1), + ('test_array[1]', True), + ('test_array[2]', 'string'), + ('test_array[3]', 2.36), + ('test_array[4]', Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15))), + ('test_array[5]', '1994-02-13')]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.UN_INDEXED, False, [('test_array[]', 1), + ('test_array[]', True), + ('test_array[]', 'string'), + ('test_array[]', 2.36), + ('test_array[]', Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15))), + ('test_array[]', '1994-02-13')]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.PLAIN, False, [('test_array', 1), + ('test_array', True), + ('test_array', 'string'), + ('test_array', 2.36), + ('test_array', Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15))), + ('test_array', '1994-02-13')]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.CSV, True, + [('test_array', + '1,True,string,2.36,{0},1994-02-13'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))))]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.PSV, True, + [('test_array', + '1|True|string|2.36|{0}|1994-02-13'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))))]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.TSV, True, + [('test_array', '1\tTrue\tstring\t2.36\t{0}\t1994-02-13'.format( + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))))]), + ([Base.employee_model(), Base.employee_model()], SerializationFormats.INDEXED, False, + [ApiHelper.json_serialize(Base.employee_model()), ApiHelper.json_serialize(Base.employee_model())]), + + ]) + @validate_call + def test_serialize_array( + self, input_array: List[Any], formatting: SerializationFormats, is_query: bool, expected_array: List[Any] + ): + serialized_array = ApiHelper.serialize_array('test_array', input_array, formatting, is_query) + if ApiHelper.is_custom_type(input_array[0]): + assert serialized_array[0][0] == 'test_array[0]' and serialized_array[1][0] == 'test_array[1]' and \ + ApiHelper.json_serialize(serialized_array[0][1]) == expected_array[0] \ + and ApiHelper.json_serialize(serialized_array[1][1]) == expected_array[1] + else: + assert ApiHelper.serialize_array('test_array', input_array, formatting, is_query) == expected_array + + @pytest.mark.parametrize('input_array, formatting, is_query', [ + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.TSV, False) + ]) + @validate_call + def test_serialize_array_value_error( + self, input_array: List[Any], formatting: SerializationFormats, is_query: bool + ): + with pytest.raises(ValueError) as exception: + ApiHelper.serialize_array('key', input_array, formatting, is_query) + + assert 'Invalid format provided.' in str(exception.value) + + @pytest.mark.parametrize('input_date, expected_date', [ + (str(date(1994, 2, 13)), date(1994, 2, 13)), + (ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))]), + [date(1994, 2, 13), date(1994, 2, 13)]) + ]) + @validate_call + def test_date_deserialize(self, input_date: str, expected_date: Union[date, List[date]]): + assert ApiHelper.date_deserialize(input_date) == expected_date + + @pytest.mark.parametrize('input_http_response, input_date_time_format, expected_response_body', [ + (None, DateTimeFormat.UNIX_DATE_TIME, None), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + DateTimeFormat.UNIX_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + DateTimeFormat.HTTP_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + DateTimeFormat.RFC3339_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (ApiHelper.json_serialize([ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))]), + DateTimeFormat.UNIX_DATE_TIME, [datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]), + (ApiHelper.json_serialize([ApiHelper.HttpDateTime.from_datetime(datetime(1995, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1995, 2, 13, 5, 30, 15))]), + DateTimeFormat.HTTP_DATE_TIME, [datetime(1995, 2, 13, 5, 30, 15), datetime(1995, 2, 13, 5, 30, 15)]), + (ApiHelper.json_serialize([ApiHelper.RFC3339DateTime.from_datetime(datetime(1996, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1996, 2, 13, 5, 30, 15))]), + DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) + + ]) + @validate_call + def test_date_time_response_body( + self, input_http_response: Optional[Union[str, int]], input_date_time_format: DateTimeFormat, + expected_response_body: Optional[Union[datetime, List[datetime]]] + ): + assert ApiHelper.datetime_deserialize(input_http_response, input_date_time_format) == expected_response_body + + @pytest.mark.parametrize('input_file_wrapper, is_file_instance', [ + (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), True), + ('I am not a file', False) + ]) + @validate_call + def test_is_file_wrapper_instance(self, input_file_wrapper: Union[FileWrapper, str], is_file_instance: bool): + assert ApiHelper.is_file_wrapper_instance(input_file_wrapper) == is_file_instance + + @pytest.mark.parametrize(' input_date, output_date', [ + (datetime(1994, 2, 13, 5, 30, 15), Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ]) + @validate_call + def test_http_datetime_from_datetime(self, input_date: datetime, output_date: str): + assert ApiHelper.HttpDateTime.from_datetime(input_date) == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + (datetime(1994, 2, 13, 5, 30, 15), Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ]) + @validate_call + def test_rfc3339_datetime_from_datetime(self, input_date: datetime, output_date: str): + assert ApiHelper.RFC3339DateTime.from_datetime(input_date) == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + (datetime(1994, 2, 13, 5, 30, 15), 761117415), + ]) + @validate_call + def test_unix_datetime_from_datetime(self, input_date: datetime, output_date: int): + assert ApiHelper.UnixDateTime.from_datetime(input_date) == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), datetime(1994, 2, 13, 5, 30, 15)), + ]) + @validate_call + def test_http_datetime_from_value(self, input_date: str, output_date: datetime): + assert ApiHelper.HttpDateTime.from_value(input_date).datetime == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + ('1994-02-13T14:01:54.9571247Z', datetime(1994, 2, 13, 14, 1, 54, 957124, tzinfo=tzutc())), + ]) + @validate_call + def test_rfc3339_from_value(self, input_date: str, output_date: datetime): + assert ApiHelper.RFC3339DateTime.from_value(input_date).datetime == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + (1484719381, datetime(2017, 1, 18, 6, 3, 1)), + ]) + @validate_call + def test_unix_datetime_from_value(self, input_date: int, output_date: datetime): + assert ApiHelper.UnixDateTime.from_value(input_date).datetime == output_date + + @pytest.mark.parametrize('input_value, output_value', [ + (None, None), + (1, 'text/plain; charset=utf-8'), + (1.4, 'text/plain; charset=utf-8'), + (True, 'text/plain; charset=utf-8'), + ('string', 'text/plain; charset=utf-8'), + (Base.employee_model(), 'application/json; charset=utf-8'), + ]) + @validate_call + def test_get_content_type(self, input_value: Any, output_value: Optional[str]): + assert ApiHelper.get_content_type(input_value) == output_value + + @pytest.mark.parametrize('input_value, output_value', [ + ('{"method": "GET", "body": {}, "uploadCount": 0}', {'body': {}, 'method': 'GET', 'uploadCount': 0}), + ('I am a string', 'I am a string'), + (None, None), + ('', None), + (' ', None) + ]) + @validate_call + def test_dynamic_deserialize(self, input_value: Optional[str], output_value: Any): + assert ApiHelper.dynamic_deserialize(input_value) == output_value + + @pytest.mark.parametrize('input_placeholders, input_values, input_template, expected_message', [ + (set(), '400', 'Test template -- {$statusCode}', 'Test template -- {$statusCode}'), + ({'{$statusCode}'}, '400', 'Test template -- {$statusCode}', 'Test template -- 400'), + ({'{$response.header.accept}'}, {'accept': 'application/json'}, + 'Test template -- {$response.header.accept}', 'Test template -- application/json'), + ({'{$response.header.accept}'}, {'retry-after': 60}, + 'Test template -- {$response.header.accept}', 'Test template -- '), + ({'{accept}'}, {'accept': 'application/json'}, + 'Test template -- {accept}', 'Test template -- application/json') + ]) + @validate_call + def test_resolve_template_placeholders( + self, input_placeholders: Set[str], input_values: Union[str, Dict[str, Any]], + input_template: str, expected_message: str + ): + actual_message = ApiHelper.resolve_template_placeholders(input_placeholders, input_values, input_template) + assert actual_message == expected_message + + @pytest.mark.parametrize('input_placeholders, input_value, input_template, expected_message', [ + (set(), + {"scalar": 123.2, + "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects":[{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body#/scalar}, {$response.body#/object/arrayObjects/0/key2}', + 'Test template -- {$response.body#/scalar}, {$response.body#/object/arrayObjects/0/key2}'), + ({'{$response.body#/scalar}', '{$response.body#/object/arrayObjects/0/key2}'}, + {"scalar": 123.2, + "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects":[{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body#/scalar}, {$response.body#/object/arrayObjects/0/key2}', + 'Test template -- 123.2, False'), + ({'{$response.body#/scalar}', '{$response.body#/object/arrayObjects/0/key2}'}, + {"object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects": [{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body#/scalar}, {$response.body#/object/arrayObjects/0/key2}', + 'Test template -- , False'), + ({'{$response.body}'}, + {"scalar": 123.2, + "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects": [{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body}', + 'Test template -- {"scalar": 123.2, "object": {"keyA": {"keyC": true, "keyD": 34}, "keyB": "some string", ' + '"arrayScalar": ["value1", "value2"], ' + '"arrayObjects": [{"key1": 123, "key2": false}, {"key3": 1234, "key4": null}]}}'), + ({'{$response.body#/object}'}, + {"scalar": 123.2, + "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects": [{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body#/object}', + 'Test template -- {"keyA": {"keyC": true, "keyD": 34}, "keyB": "some string", "arrayScalar": ' + '["value1", "value2"], "arrayObjects": [{"key1": 123, "key2": false}, {"key3": 1234, "key4": null}]}'), + ({'{$response.body}'}, + None, + 'Test template -- {$response.body}', + 'Test template -- ') + ]) + @validate_call + def test_resolve_template_placeholders_using_json_pointer( + self, input_placeholders: Set[str], input_value: Optional[Dict[str, Any]], input_template: str, + expected_message: str + ): + actual_message = ApiHelper.resolve_template_placeholders_using_json_pointer( + input_placeholders, input_value, input_template) + assert actual_message == expected_message + + @pytest.mark.parametrize('input_union_type, input_value, input_should_deserialize, expected_value', [ + (OneOf([LeafType(int), LeafType(str)]), 100, False, 100), + (OneOf([LeafType(int), LeafType(str)], UnionTypeContext(is_array=True)), '[100, "200"]', True, + [100, '200']), + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_union_type_deserialize( + self, input_union_type: UnionType, input_value: Any, input_should_deserialize: bool, expected_value: Any + ): + actual_value = ApiHelper.deserialize_union_type(input_union_type, input_value, input_should_deserialize) + assert actual_value == expected_value + + @pytest.mark.parametrize('input_url, expected_url', [ + ("https://www.example.com/path/to/resource?param1=value1¶m2=value2", "https://www.example.com/path/to/resource"), + ("https://www.example.com/path/to/resource", "https://www.example.com/path/to/resource") + ]) + @validate_call + def test_get_url_without_query(self, input_url: str, expected_url: str): + assert ApiHelper.get_url_without_query(input_url) == expected_url + + @pytest.mark.parametrize('input_url, expected_error_message', [ + ("", "Error parsing URL: Invalid URL format"), + ("invalid_url", "Error parsing URL: Invalid URL format") + ]) + @validate_call + def test_get_url_without_query_with_invalid_url(self, input_url: str, expected_error_message: str): + with pytest.raises(ValueError) as excinfo: + ApiHelper.get_url_without_query(input_url) + assert str(excinfo.value) == expected_error_message + + @pytest.mark.parametrize('input_list, expected_output', [ + (None, None), + ([], []), + (["HELLO"], ["hello"]), + (["hElLo", "worLd"], ["hello", "world"]) + ]) + @validate_call + def test_to_lower_case(self, input_list: Optional[List[str]], expected_output: Optional[List[str]]): + """Tests if an empty list returns an empty list.""" + actual_output = ApiHelper.to_lower_case(input_list) + + assert actual_output == expected_output \ No newline at end of file diff --git a/tests/apimatic_core/utility_tests/test_auth_helper.py b/tests/apimatic_core/utility_tests/test_auth_helper.py index fc28c87..784ecfc 100644 --- a/tests/apimatic_core/utility_tests/test_auth_helper.py +++ b/tests/apimatic_core/utility_tests/test_auth_helper.py @@ -1,52 +1,62 @@ +from pydantic import validate_call + from apimatic_core.utilities.auth_helper import AuthHelper class TestAuthHelper: + @validate_call def test_base64_encoded_none_value(self): actual_base64_encoded_value = AuthHelper.get_base64_encoded_value() - expected_base64_encoded_value = None - assert actual_base64_encoded_value == expected_base64_encoded_value + assert actual_base64_encoded_value is None + + @validate_call def test_base64_encoded_value(self): actual_base64_encoded_value = AuthHelper.get_base64_encoded_value('test_username', 'test_password') - expected_base64_encoded_value = 'dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk' - assert actual_base64_encoded_value == expected_base64_encoded_value + assert actual_base64_encoded_value == 'dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk' + + @validate_call def test_token_expiry(self): current_utc_timestamp = AuthHelper.get_current_utc_timestamp() actual_token_expiry_value = AuthHelper.get_token_expiry(current_utc_timestamp, 5) expected_token_expiry_value = current_utc_timestamp + int(5) + assert actual_token_expiry_value == expected_token_expiry_value + @validate_call def test_token_is_expired(self): past_timestamp = AuthHelper.get_current_utc_timestamp() - 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp) - expected_token_expired = True - assert actual_token_expired == expected_token_expired + assert actual_token_expired is True + + @validate_call def test_token_is_expired_with_clock_skew(self): past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 10) - expected_token_expired = True - assert actual_token_expired == expected_token_expired + assert actual_token_expired is True + + @validate_call def test_token_is_not_expired_with_clock_skew(self): past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 3) - expected_token_expired = False - assert actual_token_expired == expected_token_expired + + assert actual_token_expired is False past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 5) - expected_token_expired = False - assert actual_token_expired == expected_token_expired + assert actual_token_expired is False + + @validate_call def test_token_is_not_expired(self): past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp) - expected_token_expired = False - assert actual_token_expired == expected_token_expired + + assert actual_token_expired is False diff --git a/tests/apimatic_core/utility_tests/test_auth_helper.py~ b/tests/apimatic_core/utility_tests/test_auth_helper.py~ new file mode 100644 index 0000000..2a44894 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_auth_helper.py~ @@ -0,0 +1,61 @@ +from pydantic import validate_call + +from apimatic_core.utilities.auth_helper import AuthHelper + + +class TestAuthHelper: + + @validate_call + def test_base64_encoded_none_value(self): + actual_base64_encoded_value = AuthHelper.get_base64_encoded_value() + + assert actual_base64_encoded_value is None + + @validate_call + def test_base64_encoded_value(self): + actual_base64_encoded_value = AuthHelper.get_base64_encoded_value('test_username', 'test_password') + + assert actual_base64_encoded_value == 'dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk' + + @validate_call + def test_token_expiry(self): + current_utc_timestamp = AuthHelper.get_current_utc_timestamp() + actual_token_expiry_value = AuthHelper.get_token_expiry(current_utc_timestamp, 5) + expected_token_expiry_value = current_utc_timestamp + int(5) + + assert actual_token_expiry_value == expected_token_expiry_value + + @validate_call + def test_token_is_expired(self): + past_timestamp = AuthHelper.get_current_utc_timestamp() - 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp) + + assert actual_token_expired is True + + @validate_call + def test_token_is_expired_with_clock_skew(self): + past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 10) + + assert actual_token_expired is True + + @validate_call + def test_token_is_not_expired_with_clock_skew(self): + past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 3) + + assert actual_token_expired is False + + past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 5) + + assert actual_token_expired is False + + def test_token_is_not_expired(self): + past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp) + + assert actual_token_expired is False + + + diff --git a/tests/apimatic_core/utility_tests/test_comparison_helper.py b/tests/apimatic_core/utility_tests/test_comparison_helper.py index b9456b7..ffc6768 100644 --- a/tests/apimatic_core/utility_tests/test_comparison_helper.py +++ b/tests/apimatic_core/utility_tests/test_comparison_helper.py @@ -1,9 +1,13 @@ import pytest +from typing import Dict, Any, Union, List, TypeVar + +from pydantic import validate_call from apimatic_core.utilities.comparison_helper import ComparisonHelper class TestComparisonHelper: + T = TypeVar("T") @pytest.mark.parametrize('input_expected_headers, input_received_headers, ' 'input_should_allow_extra, expected_output', @@ -26,10 +30,14 @@ class TestComparisonHelper: ({'content-type': 'application/json'}, {'content-type': 'application/json', 'accept': 'application/json'}, True, True) ]) - def test_match_headers(self, input_expected_headers, input_received_headers, - input_should_allow_extra, expected_output): - actual_output = ComparisonHelper.match_headers(input_expected_headers, input_received_headers - , input_should_allow_extra) + @validate_call + def test_match_headers( + self, input_expected_headers: Dict[str, Any], input_received_headers: Dict[str, Any], + input_should_allow_extra: bool, expected_output: bool + ): + actual_output = ComparisonHelper.match_headers( + input_expected_headers, input_received_headers, input_should_allow_extra) + assert actual_output is expected_output @pytest.mark.parametrize('input_expected_body, input_received_body, input_check_values, ' @@ -2118,8 +2126,12 @@ def test_match_headers(self, input_expected_headers, input_received_headers, } }, False, False, False, True) # 97 ]) - def test_match_body(self, input_expected_body, input_received_body, input_check_values, input_check_order, - input_check_count, expected_output): - actual_output = ComparisonHelper.match_body(input_expected_body, input_received_body, input_check_values, - input_check_order, input_check_count) + @validate_call + def test_match_body( + self, input_expected_body: Union[Dict[str, Any], List[Any], T], + input_received_body: Union[Dict[str, Any], List[Any], T], input_check_values: bool, + input_check_order: bool, input_check_count: bool, expected_output: bool): + actual_output = ComparisonHelper.match_body( + input_expected_body, input_received_body, input_check_values, input_check_order, input_check_count) + assert actual_output is expected_output diff --git a/tests/apimatic_core/utility_tests/test_datetime_helper.py b/tests/apimatic_core/utility_tests/test_datetime_helper.py index 51edd33..bda99b8 100644 --- a/tests/apimatic_core/utility_tests/test_datetime_helper.py +++ b/tests/apimatic_core/utility_tests/test_datetime_helper.py @@ -1,7 +1,10 @@ -from datetime import datetime, date +from datetime import date +from typing import Union, Optional + import pytest -from apimatic_core.types.datetime_format import DateTimeFormat -from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from pydantic import validate_call + from apimatic_core.utilities.datetime_helper import DateTimeHelper @@ -20,8 +23,13 @@ class TestDateTimeHelper: ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.UNIX_DATE_TIME, False), ('Sun, 06 Nov 1994 03:49:37 GMT', None, False) ]) - def test_is_valid_datetime(self, input_dt, input_datetime_format, expected_output): + @validate_call + def test_is_valid_datetime( + self, input_dt: Union[str, int, float], input_datetime_format: Optional[DateTimeFormat], + expected_output: bool + ): actual_output = DateTimeHelper.validate_datetime(input_dt, input_datetime_format) + assert actual_output == expected_output @pytest.mark.parametrize('input_date, expected_output', [ @@ -34,10 +42,64 @@ def test_is_valid_datetime(self, input_dt, input_datetime_format, expected_outpu ('1941106', False), ('1994=11=06', False) ]) - def test_is_valid_date(self, input_date, expected_output): + @validate_call + def test_is_valid_date(self, input_date: Union[date, str], expected_output: bool): actual_output = DateTimeHelper.validate_date(input_date) assert actual_output == expected_output + # Valid RFC 1123 datetime string returns True + @validate_call + def test_valid_rfc1123_datetime_returns_true(self): + datetime_str = "Sun, 06 Nov 1994 08:49:37 GMT" + + # Assert that valid RFC 1123 string returns True + assert DateTimeHelper.is_rfc_1123(datetime_str) is True + + # Empty string input returns False + @validate_call + def test_empty_rfc1123_string_returns_false(self): + datetime_str = "" + + # Assert that empty string returns False + assert DateTimeHelper.is_rfc_1123(datetime_str) is False + + # Valid RFC 3339 datetime string without milliseconds returns true + @validate_call + def test_valid_rfc3339_datetime_without_ms_returns_true(self): + datetime_str = "2023-12-25T10:30:00" + result = DateTimeHelper.is_rfc_3339(datetime_str) + + # Assert + assert result is True + + # Empty string input returns false + @validate_call + def test_empty_rfc3339_string_returns_false(self): + datetime_str = "" + result = DateTimeHelper.is_rfc_3339(datetime_str) + + assert result is False + + # Valid integer Unix timestamp returns True + @validate_call + def test_valid_integer_timestamp_returns_true(self): + # Current timestamp + timestamp = 1672531200 + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is True + + # Zero timestamp returns True + @validate_call + def test_zero_timestamp_returns_true(self): + timestamp = 0 + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is True + # Timestamp string with whitespace returns False + @validate_call + def test_timestamp_string_with_whitespace_returns_false(self): + timestamp = "" + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is False diff --git a/tests/apimatic_core/utility_tests/test_datetime_helper.py~ b/tests/apimatic_core/utility_tests/test_datetime_helper.py~ new file mode 100644 index 0000000..b13dd85 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_datetime_helper.py~ @@ -0,0 +1,104 @@ +from datetime import date +from typing import Union, Optional + +import pytest +from pydantic import validate_call + +from apimatic_core.utilities.datetime_helper import DateTimeHelper + + +class TestDateTimeHelper: + + @pytest.mark.parametrize('input_dt, input_datetime_format, expected_output', [ + ('1994-11-06T08:49:37', DateTimeFormat.RFC3339_DATE_TIME, True), + ('1994-02-13T14:01:54.656647Z', DateTimeFormat.RFC3339_DATE_TIME, True), + ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.HTTP_DATE_TIME, True), + (1480809600, DateTimeFormat.UNIX_DATE_TIME, True), + ('1994-11-06T08:49:37', DateTimeFormat.HTTP_DATE_TIME, False), + (1480809600, DateTimeFormat.HTTP_DATE_TIME, False), + ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.RFC3339_DATE_TIME, False), + (1480809600, DateTimeFormat.RFC3339_DATE_TIME, False), + ('1994-11-06T08:49:37', DateTimeFormat.UNIX_DATE_TIME, False), + ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.UNIX_DATE_TIME, False), + ('Sun, 06 Nov 1994 03:49:37 GMT', None, False) + ]) + @validate_call + def test_is_valid_datetime( + self, input_dt: Union[str, int, float], input_datetime_format: Optional[DateTimeFormat], + expected_output: bool + ): + actual_output = DateTimeHelper.validate_datetime(input_dt, input_datetime_format) + + assert actual_output == expected_output + + @pytest.mark.parametrize('input_date, expected_output', [ + ('1994-11-06', True), + (date(1994, 11, 6), True), + (date(94, 11, 6), True), + ('1994/11/06', False), + ('19941106', False), + ('941106', False), + ('1941106', False), + ('1994=11=06', False) + ]) + @validate_call + def test_is_valid_date(self, input_date: Union[date, str], expected_output: bool): + actual_output = DateTimeHelper.validate_date(input_date) + assert actual_output == expected_output + + # Valid RFC 1123 datetime string returns True + @validate_call + def test_valid_rfc1123_datetime_returns_true(self): + datetime_str = "Sun, 06 Nov 1994 08:49:37 GMT" + + # Assert that valid RFC 1123 string returns True + assert DateTimeHelper.is_rfc_1123(datetime_str) is True + + # Empty string input returns False + @validate_call + def test_empty_rfc1123_string_returns_false(self): + datetime_str = "" + + # Assert that empty string returns False + assert DateTimeHelper.is_rfc_1123(datetime_str) is False + + # Valid RFC 3339 datetime string without milliseconds returns true + @validate_call + def test_valid_rfc3339_datetime_without_ms_returns_true(self): + datetime_str = "2023-12-25T10:30:00" + result = DateTimeHelper.is_rfc_3339(datetime_str) + + # Assert + assert result is True + + # Empty string input returns false + @validate_call + def test_empty_rfc3339_string_returns_false(self): + datetime_str = "" + result = DateTimeHelper.is_rfc_3339(datetime_str) + + assert result is False + + # Valid integer Unix timestamp returns True + @validate_call + def test_valid_integer_timestamp_returns_true(self): + # Current timestamp + timestamp = 1672531200 + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is True + + # Zero timestamp returns True + @validate_call + def test_zero_timestamp_returns_true(self): + timestamp = 0 + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is True + + # Timestamp string with whitespace returns False + @validate_call + def test_timestamp_string_with_whitespace_returns_false(self): + timestamp = "" + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is False + + diff --git a/tests/apimatic_core/utility_tests/test_file_helper.py b/tests/apimatic_core/utility_tests/test_file_helper.py index fce8a2e..2945c24 100644 --- a/tests/apimatic_core/utility_tests/test_file_helper.py +++ b/tests/apimatic_core/utility_tests/test_file_helper.py @@ -1,12 +1,17 @@ +from typing import IO + +from pydantic import validate_call + from apimatic_core.utilities.file_helper import FileHelper class TestFileHelper: + @validate_call def test_get_file(self): - file_url = 'https://gist.githubusercontent.com/asadali214/' \ + file_url: str = 'https://gist.githubusercontent.com/asadali214/' \ '0a64efec5353d351818475f928c50767/raw/8ad3533799ecb4e01a753aaf04d248e6702d4947/testFile.txt' - actualFile = FileHelper.get_file(file_url) - assert actualFile is not None \ - and actualFile.read() == 'This test file is created to test CoreFileWrapper ' \ + actual_file: IO[bytes] = FileHelper.get_file(file_url) + assert actual_file is not None \ + and actual_file.read() == 'This test file is created to test CoreFileWrapper ' \ 'functionality'.encode('ascii') diff --git a/tests/apimatic_core/utility_tests/test_file_helper.py~ b/tests/apimatic_core/utility_tests/test_file_helper.py~ new file mode 100644 index 0000000..4f01549 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_file_helper.py~ @@ -0,0 +1,14 @@ +from typing import IO + +from apimatic_core.utilities.file_helper import FileHelper + + +class TestFileHelper: + + def test_get_file(self): + file_url: str = 'https://gist.githubusercontent.com/asadali214/' \ + '0a64efec5353d351818475f928c50767/raw/8ad3533799ecb4e01a753aaf04d248e6702d4947/testFile.txt' + actual_file: IO[bytes] = FileHelper.get_file(file_url) + assert actual_file is not None \ + and actual_file.read() == 'This test file is created to test CoreFileWrapper ' \ + 'functionality'.encode('ascii') diff --git a/tests/apimatic_core/utility_tests/test_xml_helper.py b/tests/apimatic_core/utility_tests/test_xml_helper.py index 8f6704f..dfec0e2 100644 --- a/tests/apimatic_core/utility_tests/test_xml_helper.py +++ b/tests/apimatic_core/utility_tests/test_xml_helper.py @@ -1,6 +1,11 @@ from datetime import date, datetime +from typing import Any, List, Dict, Type, Optional, Tuple, Union + import pytest import sys + +from pydantic import validate_call + from apimatic_core.utilities.api_helper import ApiHelper import xml.etree.ElementTree as ET from apimatic_core.utilities.xml_helper import XmlHelper @@ -40,12 +45,14 @@ class TestXMLHelper: '' ''), ]) - def test_serialize_to_xml(self, input_value, root_element_name, expected_value): + @validate_call + def test_serialize_to_xml(self, input_value: Any, root_element_name: str, expected_value: str): if sys.version_info[1] == 7: expected_value = expected_value.replace('string="String" number="10000" boolean="false">', 'boolean="false" number="10000" string="String">') actual_value = XmlHelper.serialize_to_xml(input_value, root_element_name) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, array_element_name, expected_value', [ @@ -121,11 +128,14 @@ def test_serialize_to_xml(self, input_value, root_element_name, expected_value): '' '') ]) - def test_serialize_list_to_xml(self, input_value, root_element_name, array_element_name, expected_value): + @validate_call + def test_serialize_list_to_xml( + self, input_value: List[Any], root_element_name: str, array_element_name: str, expected_value: str): if sys.version_info[1] == 7: expected_value = expected_value.replace('string="String" number="10000" boolean="false">', 'boolean="false" number="10000" string="String">') actual_value = XmlHelper.serialize_list_to_xml(input_value, root_element_name, array_element_name) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ @@ -202,11 +212,14 @@ def test_serialize_list_to_xml(self, input_value, root_element_name, array_eleme '' ''), ]) - def test_serialize_dictionary_to_xml(self, input_value, root_element_name, expected_value): + @validate_call + def test_serialize_dictionary_to_xml( + self, input_value: Dict[str, Any], root_element_name: str, expected_value: str): if sys.version_info[1] == 7: expected_value = expected_value.replace('string="String" number="10000" boolean="false">', 'boolean="false" number="10000" string="String">') actual_value = XmlHelper.serialize_dict_to_xml(input_value, root_element_name) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ @@ -215,10 +228,12 @@ def test_serialize_dictionary_to_xml(self, input_value, root_element_name, expec (True, 'root', 'true'), ('True', 'root', 'True'), ]) - def test_add_to_element(self, input_value, root_element_name, expected_value): + @validate_call + def test_add_to_element(self, input_value: Any, root_element_name: str, expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_to_element(root_element, input_value) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, attribute_name, expected_value', [ @@ -227,10 +242,12 @@ def test_add_to_element(self, input_value, root_element_name, expected_value): (True, 'root', 'attribute', ''), ('True', 'root', 'attribute', ''), ]) - def test_add_as_attribute(self, input_value, root_element_name, attribute_name, expected_value): + @validate_call + def test_add_as_attribute(self, input_value: Any, root_element_name: str, attribute_name: str, expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_as_attribute(root_element, input_value, attribute_name) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, element_name, expected_value', [ @@ -239,10 +256,12 @@ def test_add_as_attribute(self, input_value, root_element_name, attribute_name, (True, 'root', 'element', 'true'), ('True', 'root', 'element', 'True') ]) - def test_add_as_sub_element(self, input_value, root_element_name, element_name, expected_value): + @validate_call + def test_add_as_sub_element(self, input_value: Any, root_element_name: str, element_name: str, expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_as_subelement(root_element, input_value, element_name) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, item_name, wrapping_element_name, expected_value', [ @@ -257,11 +276,14 @@ def test_add_as_sub_element(self, input_value, root_element_name, element_name, ([True, False, True], 'root', 'Item', None, 'truefalsetrue') ]) - def test_add_list_as_sub_element(self, input_value, root_element_name, item_name, wrapping_element_name, - expected_value): + @validate_call + def test_add_list_as_sub_element( + self, input_value: List[Any], root_element_name: str, item_name: str, wrapping_element_name: Optional[str], + expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_list_as_subelement(root_element, input_value, item_name, wrapping_element_name) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, dictionary_name, expected_value', [ @@ -276,10 +298,13 @@ def test_add_list_as_sub_element(self, input_value, root_element_name, item_name ({'Item1': True, 'Item2': False, 'Item3': True}, 'root', 'Dictionary', 'truefalsetrue') ]) - def test_add_dict_as_sub_element(self, input_value, root_element_name, dictionary_name, expected_value): + @validate_call + def test_add_dict_as_sub_element( + self, input_value: Dict[str, Any], root_element_name: str, dictionary_name: str, expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_dict_as_subelement(root_element, input_value, dictionary_name) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ @@ -332,10 +357,15 @@ def test_add_dict_as_sub_element(self, input_value, root_element_name, dictionar '' '', OneOfXML, 'Root', XmlHelper.serialize_to_xml(Base.one_of_xml_wolf_model(), 'Root')), - (None, int, None, None) + (None, int, '', None) ]) - def test_deserialize_xml(self, input_value, clazz, root_element_name, expected_value): + @validate_call + def test_deserialize_xml( + self, input_value: Optional[str], clazz: Type[Any], root_element_name: str, + expected_value: Optional[str] + ): actual_value = XmlHelper.deserialize_xml(input_value, clazz) + if expected_value: assert XmlHelper.serialize_to_xml(actual_value, root_element_name) == expected_value else: @@ -425,12 +455,15 @@ def test_deserialize_xml(self, input_value, clazz, root_element_name, expected_v 'Models', 'Item')), (None, 'Item', int, 'Items', None) ]) - def test_deserialize_xml_to_list(self, input_value, item_name, clazz, root_element_name, expected_value): - actual_value = XmlHelper.deserialize_xml_to_list(input_value, item_name, clazz) - if expected_value: - assert XmlHelper.serialize_list_to_xml(actual_value, root_element_name, item_name) == expected_value - else: - assert actual_value == expected_value + @validate_call + def test_deserialize_xml_to_list( + self, input_value: Optional[str], item_name: str, clazz: Type[Any], root_element_name: str, + expected_value: Optional[str] + ): + deserialized_value = XmlHelper.deserialize_xml_to_list(input_value, item_name, clazz) + actual_value = XmlHelper.serialize_list_to_xml(deserialized_value, root_element_name, item_name) + + assert actual_value == expected_value @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ ('506070', int, 'Items', @@ -486,14 +519,15 @@ def test_deserialize_xml_to_list(self, input_value, item_name, clazz, root_eleme datetime(1994, 2, 13, 5, 30, 15)) }, 'Items')), - (None, int, None, None) + (None, int, 'Items', None) ]) - def test_deserialize_xml_to_dict(self, input_value, clazz, root_element_name, expected_value): - actual_value = XmlHelper.deserialize_xml_to_dict(input_value, clazz) - if expected_value: - assert XmlHelper.serialize_dict_to_xml(actual_value, root_element_name) == expected_value - else: - assert actual_value == expected_value + @validate_call + def test_deserialize_xml_to_dict( + self, input_value: Optional[str], clazz: Type[Any], root_element_name: str, expected_value: Optional[str]): + deserialized_value = XmlHelper.deserialize_xml_to_dict(input_value, clazz) + actual_value = XmlHelper.serialize_dict_to_xml(deserialized_value, root_element_name) + + assert actual_value == expected_value @pytest.mark.parametrize('input_value, clazz, expected_value', [ ('True', str, 'True'), @@ -502,8 +536,11 @@ def test_deserialize_xml_to_dict(self, input_value, clazz, root_element_name, ex ('70.56443', float, 70.56443), (None, int, None), ]) - def test_value_from_xml_attribute(self, input_value, clazz, expected_value): + @validate_call + def test_value_from_xml_attribute( + self, input_value: Optional[str], clazz: Type[Any], expected_value: Optional[Union[str, int, float, bool]]): actual_value = XmlHelper.value_from_xml_attribute(input_value, clazz) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ @@ -511,9 +548,11 @@ def test_value_from_xml_attribute(self, input_value, clazz, expected_value): ('True', bool, 'root', True), ('70', int, 'root', 70), ('70.56443', float, 'root', 70.56443), - (None, int, None, None), + (None, int, '', None), ]) - def test_value_from_xml_element(self, input_value, root_element_name, clazz, expected_value): + @validate_call + def test_value_from_xml_element( + self, input_value: Optional[str], root_element_name: str, clazz: Type[Any], expected_value: Optional[Any]): root_element = None if input_value: root_element = ET.Element(root_element_name) @@ -528,15 +567,17 @@ def test_value_from_xml_element(self, input_value, root_element_name, clazz, exp ([50.58, 60.58, 70.58], 'root', 'item', 'items', float, [50.58, 60.58, 70.58]), ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), - (None, None, 'item', 'items', int, None), + (None, 'root', 'item', 'items', int, None), ]) - def test_list_from_xml_element(self, input_value, root_element_name, item_name, - wrapping_element_name, clazz, expected_value): + @validate_call + def test_list_from_xml_element(self, input_value: Optional[List[Any]], root_element_name: str, item_name: str, + wrapping_element_name: str, clazz: Type[Any], expected_value: Optional[List[Any]]): root_element = None if input_value: root_element = ET.Element(root_element_name) XmlHelper.add_list_as_subelement(root_element, input_value, item_name, wrapping_element_name) actual_value = XmlHelper.list_from_xml_element(root_element, item_name, clazz, wrapping_element_name) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, item_name, ' @@ -544,16 +585,18 @@ def test_list_from_xml_element(self, input_value, root_element_name, item_name, 'clazz, expected_value', [ ([50, 60, 70], 'root', 'item', 'items', 'invalid_items', int, None), ]) - def test_list_from_xml_element_with_unset_wrapper(self, input_value, root_element_name, item_name, - serialization_wrapping_element_name, - deserialization_wrapping_element_name, - clazz, expected_value): + @validate_call + def test_list_from_xml_element_with_unset_wrapper( + self, input_value: List[Any], root_element_name: str, item_name: str, + serialization_wrapping_element_name: str, deserialization_wrapping_element_name: str, clazz: Type[Any], + expected_value: Optional[List[Any]]): root_element = None if input_value: root_element = ET.Element(root_element_name) XmlHelper.add_list_as_subelement(root_element, input_value, item_name, serialization_wrapping_element_name) - actual_value = XmlHelper.list_from_xml_element(root_element, item_name, clazz, - deserialization_wrapping_element_name) + + actual_value = XmlHelper.list_from_xml_element( + root_element, item_name, clazz, deserialization_wrapping_element_name) assert actual_value == expected_value @pytest.mark.parametrize('input_value, wrapping_element_name, clazz, expected_value', [ @@ -563,15 +606,17 @@ def test_list_from_xml_element_with_unset_wrapper(self, input_value, root_elemen {'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}), ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), - (None, None, int, None), + (None, 'root', int, None), ]) - def test_dict_from_xml_element(self, input_value, wrapping_element_name, - clazz, expected_value): + @validate_call + def test_dict_from_xml_element(self, input_value: Optional[Dict[str, Any]], wrapping_element_name: str, + clazz: Type[Any], expected_value: Optional[Dict[str, Any]]): root_element = None if input_value: root_element = ET.Element(wrapping_element_name) XmlHelper.add_dict_as_subelement(root_element, input_value) actual_value = XmlHelper.dict_from_xml_element(root_element, clazz) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, item_name, mapping_data, expected_value', [ @@ -600,12 +645,15 @@ def test_dict_from_xml_element(self, input_value, wrapping_element_name, ''), (None, 'root', 'Wolf', {}, None) ]) - def test_list_from_multiple_one_of_xml_element(self, input_value, root_element_name, item_name, - mapping_data, expected_value): + @validate_call + def test_list_from_multiple_one_of_xml_element( + self, input_value: Any, root_element_name: str, item_name: str, + mapping_data: Dict[str, Any], expected_value: Optional[str]): root_element = ET.Element(root_element_name) if input_value: XmlHelper.add_to_element(root_element, input_value) actual_value = XmlHelper.list_from_multiple_one_of_xml_element(root_element, mapping_data) + if actual_value: assert XmlHelper.serialize_list_to_xml(actual_value, root_element_name, item_name) == expected_value else: @@ -615,5 +663,7 @@ def test_list_from_multiple_one_of_xml_element(self, input_value, root_element_n ({}, None), (None, None) ]) - def test_value_from_one_of_xml_elements(self, input_mapping_data, expected_output): + @validate_call + def test_value_from_one_of_xml_elements( + self, input_mapping_data: Optional[Dict[str, Any]], expected_output: Optional[Any]): assert XmlHelper.value_from_one_of_xml_elements(None, input_mapping_data) == expected_output diff --git a/tests/apimatic_core/utility_tests/test_xml_helper.py~ b/tests/apimatic_core/utility_tests/test_xml_helper.py~ new file mode 100644 index 0000000..4b75ba1 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_xml_helper.py~ @@ -0,0 +1,670 @@ +from datetime import date, datetime +from typing import Any, List, Dict, Type, Optional, Tuple, Union + +import pytest +import sys + +from pydantic import validate_call + +from apimatic_core.utilities.api_helper import ApiHelper +import xml.etree.ElementTree as ET +from apimatic_core.utilities.xml_helper import XmlHelper +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.models.cat_model import CatModel +from tests.apimatic_core.mocks.models.dog_model import DogModel +from tests.apimatic_core.mocks.models.one_of_xml import OneOfXML +from tests.apimatic_core.mocks.models.wolf_model import WolfModel +from tests.apimatic_core.mocks.models.xml_model import XMLModel + + +class TestXMLHelper: + + @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ + (50, 'Number', '50'), + ('50', 'String', '50'), + (50.58, 'Decimal', '50.58'), + (True, 'Boolean', 'true'), + (date(1994, 2, 13), 'Date', '1994-02-13'), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'UnixDateTime', '761117415'), + (ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'HttpDateTime', '{}' + .format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + (ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'RFC3339DateTime', '{}' + .format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + (Base.xml_model(), 'Model', + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + ''), + ]) + @validate_call + def test_serialize_to_xml(self, input_value: Any, root_element_name: str, expected_value: str): + if sys.version_info[1] == 7: + expected_value = expected_value.replace('string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + + actual_value = XmlHelper.serialize_to_xml(input_value, root_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, array_element_name, expected_value', [ + ([50, 60, 70], 'Numbers', 'Item', '506070'), + (['50', '60', '70'], 'Strings', 'Item', + '506070'), + ([50.58, 60.58, 70.58], 'Decimals', 'Item', + '50.5860.5870.58'), + ([True, False, True], 'Booleans', 'Item', + 'truefalsetrue'), + ([date(1994, 2, 13), date(1994, 2, 14), date(1994, 2, 15)], + 'Dates', 'Item', '' + '1994-02-13' + '1994-02-14' + '1994-02-15' + ''), + ([ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 14, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 15, 5, 30, 15))], + 'DateTimes', 'Item', '' + '761117415' + '761203815' + '761290215' + ''), + ([ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))], + 'DateTimes', 'Item', "" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + ""), + ([ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))], + 'DateTimes', 'Item', "" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + ""), + ([Base.xml_model(), Base.xml_model(), + Base.xml_model()], 'Models', 'Item', + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '') + ]) + @validate_call + def test_serialize_list_to_xml( + self, input_value: List[Any], root_element_name: str, array_element_name: str, expected_value: str): + if sys.version_info[1] == 7: + expected_value = expected_value.replace('string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + actual_value = XmlHelper.serialize_list_to_xml(input_value, root_element_name, array_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ + ({'Item1': 50, 'Item2': 60, 'Item3': 70}, 'Dictionary', + '506070'), + ({'Item1': '50', 'Item2': '60', 'Item3': '70'}, 'Dictionary', + '506070'), + ({'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}, 'Dictionary', + '50.5860.5870.58'), + ({'Item1': True, 'Item2': False, 'Item3': True}, 'Dictionary', + 'truefalsetrue'), + ({'Item1': date(1994, 2, 13), 'Item2': date(1994, 2, 14), 'Item3': date(1994, 2, 15)}, + 'Dictionary', '' + '1994-02-13' + '1994-02-14' + '1994-02-15' + ''), + ({'Item1': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 14, 5, 30, 15)), + 'Item3': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 15, 5, 30, 15))}, + 'Dictionary', '' + '761117415' + '761203815' + '761290215' + ''), + ({'Item1': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'Dictionary', '' + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + ''), + ({'Item1': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'Dictionary', '' + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + ''), + ({'Item1': Base.xml_model(), 'Item2': Base.xml_model(), + 'Item3': Base.xml_model()}, 'Dictionary', + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + ''), + ]) + @validate_call + def test_serialize_dictionary_to_xml( + self, input_value: Dict[str, Any], root_element_name: str, expected_value: str): + if sys.version_info[1] == 7: + expected_value = expected_value.replace('string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + actual_value = XmlHelper.serialize_dict_to_xml(input_value, root_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ + (70, 'root', '70'), + (70.887, 'root', '70.887'), + (True, 'root', 'true'), + ('True', 'root', 'True'), + ]) + @validate_call + def test_add_to_element(self, input_value: Any, root_element_name: str, expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_to_element(root_element, input_value) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, attribute_name, expected_value', [ + (70, 'root', 'attribute', ''), + (70.887, 'root', 'attribute', ''), + (True, 'root', 'attribute', ''), + ('True', 'root', 'attribute', ''), + ]) + @validate_call + def test_add_as_attribute(self, input_value: Any, root_element_name: str, attribute_name: str, expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_as_attribute(root_element, input_value, attribute_name) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, element_name, expected_value', [ + (70, 'root', 'element', '70'), + (70.887, 'root', 'element', '70.887'), + (True, 'root', 'element', 'true'), + ('True', 'root', 'element', 'True') + ]) + @validate_call + def test_add_as_sub_element(self, input_value: Any, root_element_name: str, element_name: str, expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_as_subelement(root_element, input_value, element_name) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, item_name, wrapping_element_name, expected_value', [ + ([50, 60, 70], 'root', 'Item', 'Numbers', + '506070'), + (['50', '60', '70'], 'root', 'Item', 'Strings', + '506070'), + ([50.58, 60.58, 70.58], 'root', 'Item', 'Decimals', + '50.5860.5870.58'), + ([True, False, True], 'root', 'Item', 'Booleans', + 'truefalsetrue'), + ([True, False, True], 'root', 'Item', None, + 'truefalsetrue') + ]) + @validate_call + def test_add_list_as_sub_element( + self, input_value: List[Any], root_element_name: str, item_name: str, wrapping_element_name: Optional[str], + expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_list_as_subelement(root_element, input_value, item_name, wrapping_element_name) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, dictionary_name, expected_value', [ + ({'Item1': 50, 'Item2': 60, 'Item3': 70}, 'root', 'Dictionary', + '506070'), + ({'Item': [50, 60, 70]}, 'root', 'Dictionary', + '506070'), + ({'Item1': '50', 'Item2': '60', 'Item3': '70'}, 'root', 'Dictionary', + '506070'), + ({'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}, 'root', 'Dictionary', + '50.5860.5870.58'), + ({'Item1': True, 'Item2': False, 'Item3': True}, 'root', 'Dictionary', + 'truefalsetrue') + ]) + @validate_call + def test_add_dict_as_sub_element( + self, input_value: Dict[str, Any], root_element_name: str, dictionary_name: str, expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_dict_as_subelement(root_element, input_value, dictionary_name) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ + ('50', int, 'Number', XmlHelper.serialize_to_xml(50, 'Number')), + ('50', str, 'String', XmlHelper.serialize_to_xml('50', 'String')), + ('50.58', float, 'Decimal', XmlHelper.serialize_to_xml(50.58, 'Decimal')), + ('true', bool, 'Boolean', XmlHelper.serialize_to_xml(True, 'Boolean')), + ('1994-02-13', date, 'Date', XmlHelper.serialize_to_xml(date(1994, 2, 13), 'Date')), + ('761117415', + ApiHelper.UnixDateTime, 'UnixDateTime', + XmlHelper.serialize_to_xml(ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'UnixDateTime')), + ('{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ApiHelper.HttpDateTime, 'HttpDateTime', + XmlHelper.serialize_to_xml(ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'HttpDateTime')), + ('{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ApiHelper.RFC3339DateTime, 'RFC3339DateTime', + XmlHelper.serialize_to_xml(ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'RFC3339DateTime')), + ('' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '', XMLModel, 'Model', + XmlHelper.serialize_to_xml(Base.xml_model(), 'Model')), + ('' + '' + 'true' + '' + '' + 'false' + '', OneOfXML, 'Root', + XmlHelper.serialize_to_xml(Base.one_of_xml_cat_model(), 'Root')), + ('' + '' + 'true' + '' + '', OneOfXML, 'Root', + XmlHelper.serialize_to_xml(Base.one_of_xml_dog_model(), 'Root')), + ('' + '' + 'true' + 'false' + '' + '', OneOfXML, 'Root', + XmlHelper.serialize_to_xml(Base.one_of_xml_wolf_model(), 'Root')), + (None, int, '', None) + ]) + @validate_call + def test_deserialize_xml( + self, input_value: Optional[str], clazz: Type[Any], root_element_name: str, + expected_value: Optional[str] + ): + actual_value = XmlHelper.deserialize_xml(input_value, clazz) + + if expected_value: + assert XmlHelper.serialize_to_xml(actual_value, root_element_name) == expected_value + else: + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, item_name, clazz, root_element_name, expected_value', [ + ('506070', 'Item', int, 'Items', + XmlHelper.serialize_list_to_xml([50, 60, 70], 'Items', 'Item')), + ('506070', 'Item', str, 'Items', + XmlHelper.serialize_list_to_xml(['50', '60', '70'], 'Items', 'Item')), + ('50.5860.5870.58', 'Item', float, 'Items', + XmlHelper.serialize_list_to_xml([50.58, 60.58, 70.58], 'Items', 'Item')), + ('truefalsetrue', 'Item', bool, 'Items', + XmlHelper.serialize_list_to_xml([True, False, True], 'Items', 'Item')), + ('' + '1994-02-13' + '1994-02-14' + '1994-02-15' + '', 'Item', date, 'Items', + XmlHelper.serialize_list_to_xml([date(1994, 2, 13), date(1994, 2, 14), date(1994, 2, 15)], 'Items', 'Item')), + ('' + '761117415' + '761203815' + '761290215' + '', 'Item', ApiHelper.UnixDateTime, + 'Items', + XmlHelper.serialize_list_to_xml([ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 14, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 15, 5, 30, 15))], + 'Items', 'Item')), + ('' + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + '', 'Item', ApiHelper.HttpDateTime, + 'Items', + XmlHelper.serialize_list_to_xml([ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))], + 'Items', 'Item')), + ('' + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + '', 'Item', ApiHelper.RFC3339DateTime, + 'Items', + XmlHelper.serialize_list_to_xml([ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))], + 'Items', 'Item')), + ('' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '', 'Item', XMLModel, + 'Models', + XmlHelper.serialize_list_to_xml([Base.xml_model(), + Base.xml_model(), + Base.xml_model()], + 'Models', 'Item')), + (None, 'Item', int, 'Items', None) + ]) + @validate_call + def test_deserialize_xml_to_list( + self, input_value: Optional[str], item_name: str, clazz: Type[Any], root_element_name: str, + expected_value: Optional[str] + ): + deserialized_value = XmlHelper.deserialize_xml_to_list(input_value, item_name, clazz) + + actual_value = XmlHelper.serialize_list_to_xml(deserialized_value, root_element_name, item_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ + ('506070', int, 'Items', + XmlHelper.serialize_dict_to_xml({'Item1': 50, 'Item2': 60, 'Item3': 70}, 'Items')), + ('506070', str, 'Items', + XmlHelper.serialize_dict_to_xml({'Item1': '50', 'Item2': '60', 'Item3': '70'}, 'Items')), + ('50.5860.5870.58', float, 'Items', + XmlHelper.serialize_dict_to_xml({'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}, 'Items')), + ('truefalsetrue', bool, 'Items', + XmlHelper.serialize_dict_to_xml({'Item1': True, 'Item2': False, 'Item3': True}, 'Items')), + ('' + '1994-02-13' + '1994-02-14' + '1994-02-15' + '', date, 'Items', + XmlHelper.serialize_dict_to_xml( + {'Item1': date(1994, 2, 13), 'Item2': date(1994, 2, 14), 'Item3': date(1994, 2, 15)}, 'Items')), + ('' + '761117415' + '761203815' + '761290215' + '', ApiHelper.UnixDateTime, + 'Items', + XmlHelper.serialize_dict_to_xml( + {'Item1': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 14, 5, 30, 15)), + 'Item3': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 15, 5, 30, 15))}, + 'Items')), + ('' + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + '', ApiHelper.HttpDateTime, + 'Items', + XmlHelper.serialize_dict_to_xml( + {'Item1': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'Items')), + ('' + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + '', ApiHelper.RFC3339DateTime, + 'Items', + XmlHelper.serialize_dict_to_xml( + { + 'Item1': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)) + }, + 'Items')), + (None, int, 'Items', None) + ]) + @validate_call + def test_deserialize_xml_to_dict( + self, input_value: Optional[str], clazz: Type[Any], root_element_name: str, expected_value: Optional[str]): + deserialized_value = XmlHelper.deserialize_xml_to_dict(input_value, clazz) + actual_value = XmlHelper.serialize_dict_to_xml(deserialized_value, root_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, clazz, expected_value', [ + ('True', str, 'True'), + ('True', bool, True), + ('70', int, 70), + ('70.56443', float, 70.56443), + (None, int, None), + ]) + @validate_call + def test_value_from_xml_attribute( + self, input_value: Optional[str], clazz: Type[Any], expected_value: Optional[Union[str, int, float, bool]]): + actual_value = XmlHelper.value_from_xml_attribute(input_value, clazz) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ + ('True', str, 'root', 'True'), + ('True', bool, 'root', True), + ('70', int, 'root', 70), + ('70.56443', float, 'root', 70.56443), + (None, int, '', None), + ]) + @validate_call + def test_value_from_xml_element( + self, input_value: Optional[str], root_element_name: str, clazz: Type[Any], expected_value: Optional[Any]): + root_element = None + if input_value: + root_element = ET.Element(root_element_name) + XmlHelper.add_to_element(root_element, input_value) + actual_value = XmlHelper.value_from_xml_element(root_element, clazz) + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, item_name, ' + 'wrapping_element_name, clazz, expected_value', [ + ([50, 60, 70], 'root', 'item', 'items', int, [50, 60, 70]), + (['50', '60', '70'], 'root', 'item', 'items', str, ['50', '60', '70']), + ([50.58, 60.58, 70.58], 'root', 'item', 'items', float, [50.58, 60.58, 70.58]), + ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), + ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), + (None, 'root', 'item', 'items', int, None), + ]) + @validate_call + def test_list_from_xml_element(self, input_value: Optional[List[Any]], root_element_name: str, item_name: str, + wrapping_element_name: str, clazz: Type[Any], expected_value: Optional[List[Any]]): + root_element = None + if input_value: + root_element = ET.Element(root_element_name) + XmlHelper.add_list_as_subelement(root_element, input_value, item_name, wrapping_element_name) + actual_value = XmlHelper.list_from_xml_element(root_element, item_name, clazz, wrapping_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, item_name, ' + 'serialization_wrapping_element_name, deserialization_wrapping_element_name,' + 'clazz, expected_value', [ + ([50, 60, 70], 'root', 'item', 'items', 'invalid_items', int, None), + ]) + @validate_call + def test_list_from_xml_element_with_unset_wrapper( + self, input_value: List[Any], root_element_name: str, item_name: str, + serialization_wrapping_element_name: str, deserialization_wrapping_element_name: str, clazz: Type[Any], + expected_value: Optional[List[Any]]): + root_element = None + if input_value: + root_element = ET.Element(root_element_name) + XmlHelper.add_list_as_subelement(root_element, input_value, item_name, serialization_wrapping_element_name) + + actual_value = XmlHelper.list_from_xml_element( + root_element, item_name, clazz, deserialization_wrapping_element_name) + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, wrapping_element_name, clazz, expected_value', [ + ({'Item1': 50, 'Item2': 60, 'Item3': 70}, 'items', int, {'Item1': 50, 'Item2': 60, 'Item3': 70}), + ({'Item1': '50', 'Item2': '60', 'Item3': '70'}, 'items', str, {'Item1': '50', 'Item2': '60', 'Item3': '70'}), + ({'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}, 'items', float, + {'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}), + ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), + ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), + (None, 'root', int, None), + ]) + @validate_call + def test_dict_from_xml_element(self, input_value: Optional[Dict[str, Any]], wrapping_element_name: str, + clazz: Type[Any], expected_value: Optional[Dict[str, Any]]): + root_element = None + if input_value: + root_element = ET.Element(wrapping_element_name) + XmlHelper.add_dict_as_subelement(root_element, input_value) + actual_value = XmlHelper.dict_from_xml_element(root_element, clazz) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, item_name, mapping_data, expected_value', [ + (Base.one_of_xml_dog_model(), 'root', 'Dog', { + 'Cat': (CatModel, True, None), + 'Dog': (DogModel, False, None), + 'Wolf': (WolfModel, True, 'Items'), + }, '' + 'true' + ''), + (Base.one_of_xml_cat_model(), 'root', 'Cat', { + 'Cat': (CatModel, True, None), + 'Dog': (DogModel, False, None), + 'Wolf': (WolfModel, True, 'Items'), + }, '' + 'true' + 'false' + ''), + (Base.one_of_xml_wolf_model(), 'root', 'Wolf', { + 'Cat': (CatModel, True, None), + 'Dog': (DogModel, False, None), + 'Wolf': (WolfModel, True, 'Items'), + }, '' + 'true' + 'false' + ''), + (None, 'root', 'Wolf', {}, None) + ]) + @validate_call + def test_list_from_multiple_one_of_xml_element( + self, input_value: Any, root_element_name: str, item_name: str, + mapping_data: Dict[str, Any], expected_value: Optional[str]): + root_element = ET.Element(root_element_name) + if input_value: + XmlHelper.add_to_element(root_element, input_value) + actual_value = XmlHelper.list_from_multiple_one_of_xml_element(root_element, mapping_data) + + if actual_value: + assert XmlHelper.serialize_list_to_xml(actual_value, root_element_name, item_name) == expected_value + else: + assert input_value == expected_value + + @pytest.mark.parametrize('input_mapping_data, expected_output', [ + ({}, None), + (None, None) + ]) + @validate_call + def test_value_from_one_of_xml_elements( + self, input_mapping_data: Optional[Dict[str, Any]], expected_output: Optional[Any]): + assert XmlHelper.value_from_one_of_xml_elements(None, input_mapping_data) == expected_output From c7c0adde42ea50a23ea3e4225965609ba8e14d4a Mon Sep 17 00:00:00 2001 From: "DESKTOP-10GD4VQ\\Sufyan" Date: Mon, 24 Feb 2025 17:18:56 +0500 Subject: [PATCH 5/6] dropped python 3.7 support --- .github/workflows/pylint.yml | 2 +- .github/workflows/test-runner.yml | 2 +- README.md | 2 +- setup.py | 9 +++------ 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 2d67c32..a375989 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -10,7 +10,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Setup Python diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 783866f..80f8768 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Setup Python diff --git a/README.md b/README.md index cd78c1d..a8df974 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The APIMatic Core libraries provide a stable runtime that powers all the functio ## Installation -You will need Python 3.7-3.11 to support this package. +You will need Python version 3.8+ to support this package. Simply run the command below to install the core library in your SDK. The core library will be added as a dependency your SDK. diff --git a/setup.py b/setup.py index cb2c97c..3b3c2ff 100644 --- a/setup.py +++ b/setup.py @@ -3,12 +3,9 @@ import sys from setuptools import setup, find_packages -if sys.version_info[0] < 3: - with open('README.md', 'r') as fh: - long_description = fh.read() -else: - with open('README.md', 'r', encoding='utf-8') as fh: - long_description = fh.read() + +with open('README.md', 'r', encoding='utf-8') as fh: + long_description = fh.read() setup( name='apimatic-core', From f74ca778d7a5eb81f1c1f1ec1b951ecf8baa4a63 Mon Sep 17 00:00:00 2001 From: "DESKTOP-10GD4VQ\\Sufyan" Date: Mon, 24 Feb 2025 18:17:38 +0500 Subject: [PATCH 6/6] fixes the forward referencing issue --- apimatic_core/api_call.py | 14 ++++----- apimatic_core/request_builder.py | 30 +++++++++---------- apimatic_core/response_handler.py | 22 +++++++------- .../api_call_tests/test_api_call.py | 2 -- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/apimatic_core/api_call.py b/apimatic_core/api_call.py index 99a8faa..ed88808 100644 --- a/apimatic_core/api_call.py +++ b/apimatic_core/api_call.py @@ -1,9 +1,9 @@ -from __future__ import annotations - from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration from apimatic_core_interfaces.logger.api_logger import ApiLogger from pydantic import validate_call -from typing import Any +from typing import Any, __all__ + +from typing_extensions import Self from apimatic_core.configurations.global_configuration import GlobalConfiguration from apimatic_core.logger.sdk_logger import LoggerFactory @@ -14,7 +14,7 @@ class ApiCall: @property - def new_builder(self) -> 'ApiCall': + def new_builder(self) -> Self: return ApiCall(self._global_configuration) def __init__( @@ -30,17 +30,17 @@ def __init__( ) @validate_call(config=dict(arbitrary_types_allowed=True)) - def request(self, request_builder: RequestBuilder) -> 'ApiCall': + def request(self, request_builder: RequestBuilder) -> Self: self._request_builder = request_builder return self @validate_call(config=dict(arbitrary_types_allowed=True)) - def response(self, response_handler: ResponseHandler) -> 'ApiCall': + def response(self, response_handler: ResponseHandler) -> Self: self._response_handler = response_handler return self @validate_call(config=dict(arbitrary_types_allowed=True)) - def endpoint_configuration(self, endpoint_configuration: EndpointConfiguration) -> 'ApiCall': + def endpoint_configuration(self, endpoint_configuration: EndpointConfiguration) -> Self: self._endpoint_configuration = endpoint_configuration return self diff --git a/apimatic_core/request_builder.py b/apimatic_core/request_builder.py index 5eec817..5b8f591 100644 --- a/apimatic_core/request_builder.py +++ b/apimatic_core/request_builder.py @@ -1,10 +1,10 @@ -from __future__ import annotations from typing import Optional, Dict, Any, List, Union, Tuple from apimatic_core_interfaces.authentication.authentication import Authentication from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum from apimatic_core_interfaces.http.http_request import HttpRequest from pydantic import validate_call +from typing_extensions import Self from apimatic_core.configurations.global_configuration import GlobalConfiguration from apimatic_core.exceptions.auth_validation_exception import AuthValidationException @@ -36,22 +36,22 @@ def __init__(self): self._xml_attributes: Optional[XmlAttributes] = None @validate_call - def server(self, server: Any) -> 'RequestBuilder': + def server(self, server: Any) -> Self: self._server = server return self @validate_call - def path(self, path: str) -> 'RequestBuilder': + def path(self, path: str) -> Self: self._path = path return self @validate_call - def http_method(self, http_method: HttpMethodEnum) -> 'RequestBuilder': + def http_method(self, http_method: HttpMethodEnum) -> Self: self._http_method = http_method return self @validate_call - def template_param(self, template_param: Parameter) -> 'RequestBuilder': + def template_param(self, template_param: Parameter) -> Self: if template_param.is_valid_parameter() and template_param.key is not None: self._template_params[template_param.key] = { 'value': template_param.value, 'encode': template_param.should_encode @@ -59,35 +59,35 @@ def template_param(self, template_param: Parameter) -> 'RequestBuilder': return self @validate_call - def header_param(self, header_param: Parameter) -> 'RequestBuilder': + def header_param(self, header_param: Parameter) -> Self: if header_param.is_valid_parameter() and header_param.key is not None: self._header_params[header_param.key] = header_param.value return self @validate_call - def query_param(self, query_param: Parameter) -> 'RequestBuilder': + def query_param(self, query_param: Parameter) -> Self: if query_param.is_valid_parameter() and query_param.key is not None: self._query_params[query_param.key] = query_param.value return self @validate_call - def form_param(self, form_param: Parameter) -> 'RequestBuilder': + def form_param(self, form_param: Parameter) -> Self: if form_param.is_valid_parameter() and form_param.key is not None: self._form_params[form_param.key] = form_param.value return self @validate_call - def additional_form_params(self, additional_form_params: Dict[str, Any]) -> 'RequestBuilder': + def additional_form_params(self, additional_form_params: Dict[str, Any]) -> Self: self._additional_form_params = additional_form_params return self @validate_call - def additional_query_params(self, additional_query_params: Dict[str, Any]) -> 'RequestBuilder': + def additional_query_params(self, additional_query_params: Dict[str, Any]) -> Self: self._additional_query_params = additional_query_params return self @validate_call - def multipart_param(self, multipart_param: Parameter) -> 'RequestBuilder': + def multipart_param(self, multipart_param: Parameter) -> Self: if multipart_param.is_valid_parameter(): self._multipart_params.append(multipart_param) return self @@ -104,22 +104,22 @@ def body_param(self, body_param: Parameter): return self @validate_call - def body_serializer(self, body_serializer: Any) -> 'RequestBuilder': + def body_serializer(self, body_serializer: Any) -> Self: self._body_serializer = body_serializer return self @validate_call(config=dict(arbitrary_types_allowed=True)) - def auth(self, auth: Authentication) -> 'RequestBuilder': + def auth(self, auth: Authentication) -> Self: self._auth = auth return self @validate_call - def array_serialization_format(self, array_serialization_format: SerializationFormats) -> 'RequestBuilder': + def array_serialization_format(self, array_serialization_format: SerializationFormats) -> Self: self._array_serialization_format = array_serialization_format return self @validate_call - def xml_attributes(self, xml_attributes: XmlAttributes) -> 'RequestBuilder': + def xml_attributes(self, xml_attributes: XmlAttributes) -> Self: self._xml_attributes = xml_attributes return self diff --git a/apimatic_core/response_handler.py b/apimatic_core/response_handler.py index d318748..dee3473 100644 --- a/apimatic_core/response_handler.py +++ b/apimatic_core/response_handler.py @@ -1,4 +1,3 @@ -from __future__ import annotations import re from typing import Callable, Union, Any, Optional, Dict, List, Type, Literal @@ -6,6 +5,7 @@ from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat from apimatic_core_interfaces.http.http_response import HttpResponse from pydantic import validate_call +from typing_extensions import Self from apimatic_core.http.response.api_response import ApiResponse from apimatic_core.types.error_case import ErrorCase, MessageType @@ -25,34 +25,34 @@ def __init__(self): self._xml_item_name: Optional[str] = None @validate_call - def deserializer(self, deserializer: Any) -> 'ResponseHandler': + def deserializer(self, deserializer: Any) -> Self: self._deserializer = deserializer return self @validate_call - def convertor(self, convertor: Optional[Callable[[Any], Any]]) -> 'ResponseHandler': + def convertor(self, convertor: Optional[Callable[[Any], Any]]) -> Self: self._convertor = convertor return self @validate_call - def deserialize_into(self, deserialize_into: Any) -> 'ResponseHandler': + def deserialize_into(self, deserialize_into: Any) -> Self: self._deserialize_into = deserialize_into return self @validate_call - def is_api_response(self, is_api_response: bool) -> 'ResponseHandler': + def is_api_response(self, is_api_response: bool) -> Self: self._is_api_response = is_api_response return self @validate_call - def is_nullify404(self, is_nullify404: bool) -> 'ResponseHandler': + def is_nullify404(self, is_nullify404: bool) -> Self: self._is_nullify404 = is_nullify404 return self @validate_call def local_error( self, error_code: Union[int, str], error_message: str, exception_type: Type[Any] - ) -> 'ResponseHandler': + ) -> Self: self._local_errors[str(error_code)] = ErrorCase(message=error_message, message_type=MessageType.SIMPLE, exception_type=exception_type) @@ -61,24 +61,24 @@ def local_error( @validate_call def local_error_template( self, error_code: Union[int, str], error_message: str, exception_type: Type[Any] - ) -> 'ResponseHandler': + ) -> Self: self._local_errors[str(error_code)] = ErrorCase(message=error_message, message_type=MessageType.TEMPLATE, exception_type=exception_type) return self @validate_call - def datetime_format(self, datetime_format: DateTimeFormat) -> 'ResponseHandler': + def datetime_format(self, datetime_format: DateTimeFormat) -> Self: self._datetime_format = datetime_format return self @validate_call - def is_xml_response(self, is_xml_response: bool) -> 'ResponseHandler': + def is_xml_response(self, is_xml_response: bool) -> Self: self._is_xml_response = is_xml_response return self @validate_call - def xml_item_name(self, xml_item_name: Optional[str]) -> 'ResponseHandler': + def xml_item_name(self, xml_item_name: Optional[str]) -> Self: self._xml_item_name = xml_item_name return self diff --git a/tests/apimatic_core/api_call_tests/test_api_call.py b/tests/apimatic_core/api_call_tests/test_api_call.py index 629db3e..6ef5ef4 100644 --- a/tests/apimatic_core/api_call_tests/test_api_call.py +++ b/tests/apimatic_core/api_call_tests/test_api_call.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration from apimatic_core_interfaces.http.http_client import HttpClient