From 0deaf89b996d45dc8e7ee229a6146131903c0f61 Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Mon, 28 Oct 2024 12:20:45 +0500 Subject: [PATCH 1/7] adds the check for conflicting model's serialized properties with additional property names --- apimatic_core/utilities/api_helper.py | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index 26d5cc2..c1ff3df 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -527,6 +527,10 @@ def to_dictionary(obj, should_ignore_null_values=False): # 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 diff --git a/setup.py b/setup.py index 1427192..359bf1c 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name='apimatic-core', - version='0.2.15', + version='0.2.16', description='A library that contains core logic and utilities for ' 'consuming REST APIs using Python SDKs generated by APIMatic.', long_description=long_description, From e9371c136e166541267dc9772e3dd9637e5513db Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Tue, 29 Oct 2024 12:55:09 +0500 Subject: [PATCH 2/7] adds implementation for the additional properties handling --- apimatic_core/utilities/api_helper.py | 41 +- .../model_with_additional_properties.py | 394 ++++++++++++++++++ .../utility_tests/test_api_helper.py | 103 ++++- 3 files changed, 527 insertions(+), 11 deletions(-) create mode 100644 tests/apimatic_core/mocks/models/model_with_additional_properties.py diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index c1ff3df..d124cc9 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -119,12 +119,16 @@ def json_deserialize(json, unboxing_function=None, as_dict=False): if unboxing_function is None: return decoded + return ApiHelper.apply_unboxing_function(decoded, unboxing_function, as_dict) + + @staticmethod + def apply_unboxing_function(obj, unboxing_function, as_dict=False): 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] - else: - return unboxing_function(decoded) + return {k: unboxing_function(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [unboxing_function(element) for element in obj] + + return unboxing_function(obj) @staticmethod def dynamic_deserialize(dynamic_response): @@ -537,17 +541,17 @@ def to_dictionary(obj, should_ignore_null_values=False): dictionary[name] = list() for item in value: dictionary[name].append( - ApiHelper.to_dictionary(item, should_ignore_null_values) if hasattr(item, "additional_properties") else item) + 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], - "additional_properties") else \ + "_names") else \ value[key] else: dictionary[name] = ApiHelper.to_dictionary(value, should_ignore_null_values) if hasattr(value, - "additional_properties") else value + "_names") else value # Return the result return dictionary @@ -672,6 +676,27 @@ 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): + """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 + + class CustomDate(object): """ A base class for wrapper classes of datetime. diff --git a/tests/apimatic_core/mocks/models/model_with_additional_properties.py b/tests/apimatic_core/mocks/models/model_with_additional_properties.py new file mode 100644 index 0000000..4646bf1 --- /dev/null +++ b/tests/apimatic_core/mocks/models/model_with_additional_properties.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +from tests.apimatic_core.mocks.union_type_lookup import UnionTypeLookUp +from tests.apimatic_core.mocks.models.lion import Lion +from apimatic_core.utilities.api_helper import ApiHelper + + +class ModelWithAdditionalPropertiesOfPrimitiveType(object): + + """Implementation of the 'NonInheritEnabledNumber' model. + + TODO: type model description here. + + Attributes: + email (str): TODO: type description here. + + """ + + # Create a mapping from Model property names to API property names + _names = { + "email": 'email' + } + + def __init__(self, + email=None, + additional_properties:dict[str, int]=None): + """Constructor for the NonInheritEnabledNumber class""" + + # 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 + + 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 + email = dictionary.pop("email", None) + + additional_properties = ApiHelper.get_additional_properties( + dictionary=dictionary, + unboxing_function=lambda x: int(x)) + + # Return an object of this model + return cls(email, additional_properties) + + +class ModelWithAdditionalPropertiesOfPrimitiveArrayType(object): + + """Implementation of the 'NonInheritEnabledNumber' model. + + TODO: type model description here. + + Attributes: + email (str): TODO: type description here. + + """ + + # Create a mapping from Model property names to API property names + _names = { + "email": 'email' + } + + def __init__(self, + email=None, + additional_properties:dict[str, list[int]]=None): + """Constructor for the NonInheritEnabledNumber class""" + + # 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 + + 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 + email = dictionary.pop("email", None) + + additional_properties = ApiHelper.get_additional_properties( + dictionary=dictionary, + unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: int(item))) + + # Return an object of this model + return cls(email, additional_properties) + +class ModelWithAdditionalPropertiesOfPrimitiveDictType(object): + + """Implementation of the 'NonInheritEnabledNumber' model. + + TODO: type model description here. + + Attributes: + email (str): TODO: type description here. + + """ + + # Create a mapping from Model property names to API property names + _names = { + "email": 'email' + } + + def __init__(self, + email=None, + additional_properties:dict[str, dict[str, int]]=None): + """Constructor for the NonInheritEnabledNumber class""" + + # 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 + + 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 + email = dictionary.pop("email", None) + + additional_properties = ApiHelper.get_additional_properties( + dictionary=dictionary, + unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: int(item), as_dict=True)) + + # Return an object of this model + return cls(email, additional_properties) + +class ModelWithAdditionalPropertiesOfModelType(object): + + """Implementation of the 'NonInheritEnabledNumber' model. + + TODO: type model description here. + + Attributes: + email (str): TODO: type description here. + + """ + + # Create a mapping from Model property names to API property names + _names = { + "email": 'email' + } + + def __init__(self, + email=None, + additional_properties:dict[str, Lion]=None): + """Constructor for the NonInheritEnabledNumber class""" + + # 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 + + 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 + email = dictionary.pop("email", None) + + additional_properties = ApiHelper.get_additional_properties( + dictionary=dictionary, + unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: Lion.from_dictionary(item))) + + # Return an object of this model + return cls(email, additional_properties) + +class ModelWithAdditionalPropertiesOfModelArrayType(object): + + """Implementation of the 'NonInheritEnabledNumber' model. + + TODO: type model description here. + + Attributes: + email (str): TODO: type description here. + + """ + + # Create a mapping from Model property names to API property names + _names = { + "email": 'email' + } + + def __init__(self, + email=None, + additional_properties:dict[str, list[Lion]]=None): + """Constructor for the NonInheritEnabledNumber class""" + + # 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 + + 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 + email = dictionary.pop("email", None) + + additional_properties = ApiHelper.get_additional_properties( + dictionary=dictionary, + unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: Lion.from_dictionary(item))) + + # Return an object of this model + return cls(email, additional_properties) + +class ModelWithAdditionalPropertiesOfModelDictType(object): + + """Implementation of the 'NonInheritEnabledNumber' model. + + TODO: type model description here. + + Attributes: + email (str): TODO: type description here. + + """ + + # Create a mapping from Model property names to API property names + _names = { + "email": 'email' + } + + def __init__(self, + email=None, + additional_properties:dict[str, dict[str, Lion]]=None): + """Constructor for the NonInheritEnabledNumber class""" + + # 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 + + 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 + email = dictionary.pop("email", None) + + additional_properties = ApiHelper.get_additional_properties( + dictionary=dictionary, + unboxing_function=lambda x: ApiHelper.apply_unboxing_function( + x, lambda item: Lion.from_dictionary(item), + as_dict=True)) + + # Return an object of this model + return cls(email, additional_properties) + +class ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive(object): + + """Implementation of the 'NonInheritEnabledNumber' model. + + TODO: type model description here. + + Attributes: + email (str): TODO: type description here. + + """ + + # Create a mapping from Model property names to API property names + _names = { + "email": 'email' + } + + def __init__(self, + email=None, + additional_properties:dict[str, float|bool]=None): + """Constructor for the NonInheritEnabledNumber class""" + + # 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 + + 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 + email = dictionary.pop("email", None) + + additional_properties = ApiHelper.get_additional_properties( + dictionary=dictionary, + unboxing_function=lambda x: ApiHelper.deserialize_union_type(UnionTypeLookUp.get('ScalarModelAnyOfRequired'), + x, False)) + + # Return an object of this model + return cls(email, additional_properties) \ 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 index d595776..3f47528 100644 --- a/tests/apimatic_core/utility_tests/test_api_helper.py +++ b/tests/apimatic_core/utility_tests/test_api_helper.py @@ -2,6 +2,8 @@ import jsonpickle import pytest +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 @@ -17,6 +19,11 @@ 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, \ + ModelWithAdditionalPropertiesOfModelType, ModelWithAdditionalPropertiesOfModelArrayType, \ + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive, ModelWithAdditionalPropertiesOfModelDictType from tests.apimatic_core.mocks.models.person import Employee from requests.utils import quote @@ -171,8 +178,40 @@ def test_json_serialize(self, input_value, expected_value): '"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_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, + 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, + 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, + 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.from_dictionary, + False, + '{"email": "test", "prop": true}'), + ('{"email": "test", "prop": 100.65}', + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.from_dictionary, + False, + '{"email": "test", "prop": 100.65}'), + ('{"email": "test", "prop": "100.65"}', + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.from_dictionary, + False, + '{"email": "test"}') ]) 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) @@ -498,7 +537,39 @@ def test_to_dictionary_value_error(self, obj, name): ('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[workingDays][1]', 'Tuesday'), ('form_param[personType]', 'Empl')], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveType( + 'test@gmail.com', {'prop': 20}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 20)], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveArrayType( + 'test@gmail.com', {'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}}), + [('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')}), + [('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')]}), + [('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')}}), + [('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( + 'test@gmail.com', {'prop': 10.55}), + [('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): key = 'form_param' @@ -944,3 +1015,29 @@ def test_to_lower_case(self, input_list, expected_output): """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_dict", + [ + ({}, {}, lambda x: int(x), False), + ({"a": 1, "b": 2}, {"a": 1, "b": 2}, lambda x: int(x), False), + ({"a": "1", "b": "2"}, {"a": "1", "b": "2"}, lambda x: str(x), False), + ({"a": "Test 1", "b": "Test 2"}, {}, lambda x: int(x), False), + ({"a": [1, 2], "b": [3, 4]}, {"a": [1, 2], "b": [3, 4]}, lambda x: int(x), 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), True), + ], + ) + def test_get_additional_properties_success(self, dictionary, expected_result, unboxing_func, is_dict): + result = ApiHelper.get_additional_properties(dictionary, lambda x: ApiHelper.apply_unboxing_function(x, unboxing_func, 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 \ No newline at end of file From 816b1f48d23adb61f1afba4a1334ad57370069f9 Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Tue, 29 Oct 2024 13:09:13 +0500 Subject: [PATCH 3/7] removes type from the constructor parameter for additional properties --- .../model_with_additional_properties.py | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) 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 4646bf1..3b94606 100644 --- a/tests/apimatic_core/mocks/models/model_with_additional_properties.py +++ b/tests/apimatic_core/mocks/models/model_with_additional_properties.py @@ -22,8 +22,12 @@ class ModelWithAdditionalPropertiesOfPrimitiveType(object): def __init__(self, email=None, - additional_properties:dict[str, int]=None): - """Constructor for the NonInheritEnabledNumber class""" + additional_properties=None): + """Constructor for the NonInheritEnabledNumber class + Args: + email (str): TODO: type description here. + additional_properties (dict[str, int]): TODO: type description here. + """ # Initialize members of the class if additional_properties is None: @@ -78,8 +82,14 @@ class ModelWithAdditionalPropertiesOfPrimitiveArrayType(object): def __init__(self, email=None, - additional_properties:dict[str, list[int]]=None): - """Constructor for the NonInheritEnabledNumber class""" + 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. + + """ # Initialize members of the class if additional_properties is None: @@ -133,8 +143,14 @@ class ModelWithAdditionalPropertiesOfPrimitiveDictType(object): def __init__(self, email=None, - additional_properties:dict[str, dict[str, int]]=None): - """Constructor for the NonInheritEnabledNumber class""" + 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. + + """ # Initialize members of the class if additional_properties is None: @@ -188,8 +204,14 @@ class ModelWithAdditionalPropertiesOfModelType(object): def __init__(self, email=None, - additional_properties:dict[str, Lion]=None): - """Constructor for the NonInheritEnabledNumber class""" + additional_properties=None): + """Constructor for the NonInheritEnabledNumber class + + Args: + email (str): TODO: type description here. + additional_properties (dict[str, Lion]): TODO: type description here. + + """ # Initialize members of the class if additional_properties is None: @@ -243,8 +265,14 @@ class ModelWithAdditionalPropertiesOfModelArrayType(object): def __init__(self, email=None, - additional_properties:dict[str, list[Lion]]=None): - """Constructor for the NonInheritEnabledNumber class""" + additional_properties=None): + """Constructor for the NonInheritEnabledNumber class + + Args: + email (str): TODO: type description here. + additional_properties (dict[str, list[Lion]]): TODO: type description here. + + """ # Initialize members of the class if additional_properties is None: @@ -298,8 +326,14 @@ class ModelWithAdditionalPropertiesOfModelDictType(object): def __init__(self, email=None, - additional_properties:dict[str, dict[str, Lion]]=None): - """Constructor for the NonInheritEnabledNumber class""" + additional_properties=None): + """Constructor for the NonInheritEnabledNumber class + + Args: + email (str): TODO: type description here. + additional_properties (dict[str, dict[str, Lion]]): TODO: type description here. + + """ # Initialize members of the class if additional_properties is None: @@ -355,8 +389,14 @@ class ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive(object): def __init__(self, email=None, - additional_properties:dict[str, float|bool]=None): - """Constructor for the NonInheritEnabledNumber class""" + additional_properties=None): + """Constructor for the NonInheritEnabledNumber class + + Args: + email (str): TODO: type description here. + additional_properties (dict[str, float|bool]): TODO: type description here. + + """ # Initialize members of the class if additional_properties is None: From eba87253d326bd2a73ab54cfa76def99dd108818 Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Tue, 29 Oct 2024 16:10:39 +0500 Subject: [PATCH 4/7] adds exception test case --- .../utility_tests/test_api_helper.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/apimatic_core/utility_tests/test_api_helper.py b/tests/apimatic_core/utility_tests/test_api_helper.py index 3f47528..643f614 100644 --- a/tests/apimatic_core/utility_tests/test_api_helper.py +++ b/tests/apimatic_core/utility_tests/test_api_helper.py @@ -156,6 +156,17 @@ 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'), @@ -583,6 +594,18 @@ 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): + with pytest.raises(ValueError) as conflictingPropertyError: + ApiHelper.form_encode(input_form_param_value, 'form_param') + + assert conflictingPropertyError.value.args[0] == expected_validation_message + + @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ ({'form_param1': 'string', 'form_param2': ['string', True], From d78ac1cb8bcf33744cd37e81fb94257eccb79124 Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Thu, 31 Oct 2024 12:03:58 +0500 Subject: [PATCH 5/7] updates the mock model with additional properties to reflect the latest changes of the SDK model --- .../model_with_additional_properties.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) 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 3b94606..603894d 100644 --- a/tests/apimatic_core/mocks/models/model_with_additional_properties.py +++ b/tests/apimatic_core/mocks/models/model_with_additional_properties.py @@ -54,10 +54,10 @@ def from_dictionary(cls, return None # Extract variables from the dictionary - email = dictionary.pop("email", None) + email = dictionary.get("email") if dictionary.get("email") else None additional_properties = ApiHelper.get_additional_properties( - dictionary=dictionary, + dictionary={k: v for k, v in dictionary.items() if k not in cls._names.values()}, unboxing_function=lambda x: int(x)) # Return an object of this model @@ -116,10 +116,10 @@ def from_dictionary(cls, return None # Extract variables from the dictionary - email = dictionary.pop("email", None) + email = dictionary.get("email") if dictionary.get("email") else None additional_properties = ApiHelper.get_additional_properties( - dictionary=dictionary, + 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))) # Return an object of this model @@ -177,10 +177,10 @@ def from_dictionary(cls, return None # Extract variables from the dictionary - email = dictionary.pop("email", None) + email = dictionary.get("email") if dictionary.get("email") else None additional_properties = ApiHelper.get_additional_properties( - dictionary=dictionary, + 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), as_dict=True)) # Return an object of this model @@ -238,10 +238,10 @@ def from_dictionary(cls, return None # Extract variables from the dictionary - email = dictionary.pop("email", None) + email = dictionary.get("email") if dictionary.get("email") else None additional_properties = ApiHelper.get_additional_properties( - dictionary=dictionary, + 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))) # Return an object of this model @@ -299,10 +299,10 @@ def from_dictionary(cls, return None # Extract variables from the dictionary - email = dictionary.pop("email", None) + email = dictionary.get("email") if dictionary.get("email") else None additional_properties = ApiHelper.get_additional_properties( - dictionary=dictionary, + 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))) # Return an object of this model @@ -360,10 +360,10 @@ def from_dictionary(cls, return None # Extract variables from the dictionary - email = dictionary.pop("email", None) + email = dictionary.get("email") if dictionary.get("email") else None additional_properties = ApiHelper.get_additional_properties( - dictionary=dictionary, + 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), as_dict=True)) @@ -423,10 +423,10 @@ def from_dictionary(cls, return None # Extract variables from the dictionary - email = dictionary.pop("email", None) + email = dictionary.get("email") if dictionary.get("email") else None additional_properties = ApiHelper.get_additional_properties( - dictionary=dictionary, + 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)) From 57fe24be7a6931bbd04a1c46c6a329244e9eef4a Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Fri, 8 Nov 2024 17:26:15 +0500 Subject: [PATCH 6/7] adds array of map and map of array handling for unboxing function in ApiHelper --- apimatic_core/utilities/api_helper.py | 26 +++++++--- .../model_with_additional_properties.py | 8 ++-- .../utility_tests/test_api_helper.py | 48 +++++++++++++++---- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index d124cc9..3a22712 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -119,16 +119,28 @@ def json_deserialize(json, unboxing_function=None, as_dict=False): if unboxing_function is None: return decoded - return ApiHelper.apply_unboxing_function(decoded, unboxing_function, as_dict) + 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 - def apply_unboxing_function(obj, unboxing_function, as_dict=False): - if as_dict: - return {k: unboxing_function(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [unboxing_function(element) for element in obj] + def apply_unboxing_function(value, unboxing_function, is_array=False, is_dict=False, is_array_of_map=False, + is_map_of_array=False): + if is_dict: + if is_map_of_array: + return {k: list(map(unboxing_function, v)) 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 list(map(lambda x: {k: unboxing_function(v) for k, v in x.items()}, value)) + else: + return list(map(unboxing_function, value)) - return unboxing_function(obj) + return unboxing_function(value) @staticmethod def dynamic_deserialize(dynamic_response): 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 603894d..124aa7f 100644 --- a/tests/apimatic_core/mocks/models/model_with_additional_properties.py +++ b/tests/apimatic_core/mocks/models/model_with_additional_properties.py @@ -120,7 +120,7 @@ def from_dictionary(cls, 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))) + unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: int(item), is_array=True)) # Return an object of this model return cls(email, additional_properties) @@ -181,7 +181,7 @@ def from_dictionary(cls, 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), as_dict=True)) + unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: int(item), is_dict=True)) # Return an object of this model return cls(email, additional_properties) @@ -303,7 +303,7 @@ def from_dictionary(cls, 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))) + unboxing_function=lambda x: ApiHelper.apply_unboxing_function(x, lambda item: Lion.from_dictionary(item), is_array=True)) # Return an object of this model return cls(email, additional_properties) @@ -366,7 +366,7 @@ def from_dictionary(cls, 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), - as_dict=True)) + is_dict=True)) # Return an object of this model return cls(email, additional_properties) diff --git a/tests/apimatic_core/utility_tests/test_api_helper.py b/tests/apimatic_core/utility_tests/test_api_helper.py index 643f614..5adf18b 100644 --- a/tests/apimatic_core/utility_tests/test_api_helper.py +++ b/tests/apimatic_core/utility_tests/test_api_helper.py @@ -1040,18 +1040,20 @@ def test_to_lower_case(self, input_list, expected_output): assert actual_output == expected_output @pytest.mark.parametrize( - "dictionary, expected_result, unboxing_func, is_dict", + "dictionary, expected_result, unboxing_func, is_array, is_dict", [ - ({}, {}, lambda x: int(x), False), - ({"a": 1, "b": 2}, {"a": 1, "b": 2}, lambda x: int(x), False), - ({"a": "1", "b": "2"}, {"a": "1", "b": "2"}, lambda x: str(x), False), - ({"a": "Test 1", "b": "Test 2"}, {}, lambda x: int(x), False), - ({"a": [1, 2], "b": [3, 4]}, {"a": [1, 2], "b": [3, 4]}, lambda x: int(x), 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), True), + ({}, {}, 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_dict): - result = ApiHelper.get_additional_properties(dictionary, lambda x: ApiHelper.apply_unboxing_function(x, unboxing_func, is_dict)) + 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( @@ -1063,4 +1065,30 @@ def test_get_additional_properties_success(self, dictionary, expected_result, un ) 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 \ No newline at end of file + 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, expected", + [ + # Test case 1: Simple object + (5, lambda x: x * 2, False, False, False, False, 10), + # Test case 2: Array + ([1, 2, 3], lambda x: x * 2, True, False, False, False, [2, 4, 6]), + # Test case 3: Dictionary + ({"a": 1, "b": 2}, lambda x: x * 2, False, True, False, False, {"a": 2, "b": 4}), + # Test case 4: Array of maps + ([{"a": 1}, {"b": 2}], lambda x: x * 2, True, False, True, False, [{"a": 2}, {"b": 4}]), + # Test case 5: Map of arrays + ({"a": [1, 2], "b": [3, 4]}, lambda x: x * 2, False, True, False, True, {"a": [2, 4], "b": [6, 8]}), + ], + ) + def test_apply_unboxing_function(self, value, unboxing_function, is_array, is_dict, + is_array_of_map, is_map_of_array, expected): + result = ApiHelper.apply_unboxing_function( + value, + unboxing_function, + is_array, + is_dict, + is_array_of_map, + is_map_of_array) + assert result == expected \ No newline at end of file From a751c05879f8a3d36beeeb14fad675b55ac7e772 Mon Sep 17 00:00:00 2001 From: Muhammad Sufyan Date: Fri, 8 Nov 2024 17:40:45 +0500 Subject: [PATCH 7/7] adds multi-dimensional array handling for additional properties --- apimatic_core/utilities/api_helper.py | 22 +++++++++++++---- .../utility_tests/test_api_helper.py | 24 ++++++++++++------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index 3a22712..d74747a 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -128,17 +128,31 @@ def json_deserialize(json, unboxing_function=None, as_dict=False): @staticmethod def apply_unboxing_function(value, unboxing_function, is_array=False, is_dict=False, is_array_of_map=False, - is_map_of_array=False): + is_map_of_array=False, dimension_count=1): if is_dict: if is_map_of_array: - return {k: list(map(unboxing_function, v)) for k, v in value.items()} + 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 list(map(lambda x: {k: unboxing_function(v) for k, v in x.items()}, value)) + 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 list(map(unboxing_function, value)) + return [unboxing_function(element) for element in value] return unboxing_function(value) diff --git a/tests/apimatic_core/utility_tests/test_api_helper.py b/tests/apimatic_core/utility_tests/test_api_helper.py index 5adf18b..e787022 100644 --- a/tests/apimatic_core/utility_tests/test_api_helper.py +++ b/tests/apimatic_core/utility_tests/test_api_helper.py @@ -1068,27 +1068,35 @@ def test_get_additional_properties_exception(self, dictionary): 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, expected", + "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, 10), + (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, [2, 4, 6]), + ([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, {"a": 2, "b": 4}), + ({"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, [{"a": 2}, {"b": 4}]), + ([{"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, {"a": [2, 4], "b": [6, 8]}), + ({"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, expected): + 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) + is_map_of_array, + dimension_count) assert result == expected \ No newline at end of file