From 1a8d75219949ad2c06bb2a0db305901a42c41010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Fri, 23 Jul 2021 15:56:35 +0200 Subject: [PATCH 1/9] Handle oneOf and enum errors --- .generator/templates/model.mustache | 1 + .../method_set_attribute.mustache | 2 + .generator/templates/model_utils.mustache | 78 +++++++++++++++++-- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/.generator/templates/model.mustache b/.generator/templates/model.mustache index 5e6e117f6d..c8eb7875c4 100644 --- a/.generator/templates/model.mustache +++ b/.generator/templates/model.mustache @@ -1,6 +1,7 @@ {{> partial_header }} from {{packageName}}.model_utils import ( # noqa: F401 + ApiValueError, ApiTypeError, ModelComposed, ModelNormal, diff --git a/.generator/templates/model_templates/method_set_attribute.mustache b/.generator/templates/model_templates/method_set_attribute.mustache index 55da523b2a..4595d0d887 100644 --- a/.generator/templates/model_templates/method_set_attribute.mustache +++ b/.generator/templates/model_templates/method_set_attribute.mustache @@ -49,3 +49,5 @@ self._configuration ) self.__dict__['_data_store'][name] = value + if isinstance(value, OpenApiModel) and value._unparsed: + self._unparsed = True diff --git a/.generator/templates/model_utils.mustache b/.generator/templates/model_utils.mustache index b21a5a6cea..eb0a5b35e4 100644 --- a/.generator/templates/model_utils.mustache +++ b/.generator/templates/model_utils.mustache @@ -1654,15 +1654,12 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): constant_kwargs['_check_type'], configuration=constant_kwargs['_configuration'] ) - oneof_instances.append(oneof_instance) + if not oneof_instance._unparsed: + oneof_instances.append(oneof_instance) except Exception: pass if len(oneof_instances) == 0: - raise ApiValueError( - "Invalid inputs given to generate an instance of %s. None " - "of the oneOf schemas matched the input data." % - cls.__name__ - ) + return UnparsedObject(**model_kwargs) elif len(oneof_instances) > 1: raise ApiValueError( "Invalid inputs given to generate an instance of %s. Multiple " @@ -1822,3 +1819,72 @@ def validate_get_composed_info(constant_args, model_args, self): additional_properties_model_instances, discarded_args ] + + +class UnparsedObject(ModelNormal): + """A model for an oneOf we don't know about.""" + + allowed_values = {} + + validations = {} + + additional_properties_type = None + + _nullable = False + + openapi_types = {} + + discriminator = None + + attribute_map = {} + + _composed_schemas = {} + + required_properties = set( + [ + "_data_store", + ] + ) + + @convert_js_args_to_python_args + def __init__(self, **kwargs): + + self._data_store = {} + + for var_name, var_value in kwargs.items(): + self.__dict__[var_name] = var_value + self.__dict__["_data_store"][var_name] = var_value + + +class UnparsedSimpleObject(ModelSimple): + """A model we don't know about.""" + + allowed_values = {} + + validations = {} + + additional_properties_type = None + + _nullable = False + + openapi_types = {} + + discriminator = None + + attribute_map = {} + + _composed_schemas = {} + + required_properties = set( + [ + "_data_store", + ] + ) + + @convert_js_args_to_python_args + def __init__(self, value): + + self._data_store = {} + + self.__dict__["value"] = value + self.__dict__["_data_store"]["value"] = value From 1b45fa950c4feafd10e976fa22e4b1af13791253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Mon, 26 Jul 2021 10:57:29 +0200 Subject: [PATCH 2/9] Add cassettes --- .generator/templates/model_utils.mustache | 34 ------------ ...um_and_oneof_deserialization_errors.frozen | 1 + ...enum_and_oneof_deserialization_errors.yaml | 53 +++++++++++++++++++ 3 files changed, 54 insertions(+), 34 deletions(-) create mode 100644 tests/v1/cassettes/test_scenarios/test_client_is_resilient_to_enum_and_oneof_deserialization_errors.frozen create mode 100644 tests/v1/cassettes/test_scenarios/test_client_is_resilient_to_enum_and_oneof_deserialization_errors.yaml diff --git a/.generator/templates/model_utils.mustache b/.generator/templates/model_utils.mustache index eb0a5b35e4..d10c4d39b4 100644 --- a/.generator/templates/model_utils.mustache +++ b/.generator/templates/model_utils.mustache @@ -1854,37 +1854,3 @@ class UnparsedObject(ModelNormal): for var_name, var_value in kwargs.items(): self.__dict__[var_name] = var_value self.__dict__["_data_store"][var_name] = var_value - - -class UnparsedSimpleObject(ModelSimple): - """A model we don't know about.""" - - allowed_values = {} - - validations = {} - - additional_properties_type = None - - _nullable = False - - openapi_types = {} - - discriminator = None - - attribute_map = {} - - _composed_schemas = {} - - required_properties = set( - [ - "_data_store", - ] - ) - - @convert_js_args_to_python_args - def __init__(self, value): - - self._data_store = {} - - self.__dict__["value"] = value - self.__dict__["_data_store"]["value"] = value diff --git a/tests/v1/cassettes/test_scenarios/test_client_is_resilient_to_enum_and_oneof_deserialization_errors.frozen b/tests/v1/cassettes/test_scenarios/test_client_is_resilient_to_enum_and_oneof_deserialization_errors.frozen new file mode 100644 index 0000000000..aed374a8e0 --- /dev/null +++ b/tests/v1/cassettes/test_scenarios/test_client_is_resilient_to_enum_and_oneof_deserialization_errors.frozen @@ -0,0 +1 @@ +2021-07-23T16:21:35.229591+02:00 \ No newline at end of file diff --git a/tests/v1/cassettes/test_scenarios/test_client_is_resilient_to_enum_and_oneof_deserialization_errors.yaml b/tests/v1/cassettes/test_scenarios/test_client_is_resilient_to_enum_and_oneof_deserialization_errors.yaml new file mode 100644 index 0000000000..666dadb475 --- /dev/null +++ b/tests/v1/cassettes/test_scenarios/test_client_is_resilient_to_enum_and_oneof_deserialization_errors.yaml @@ -0,0 +1,53 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Type: + - application/json + Dd-Operation-Id: + - ListTests + User-Agent: + - datadog-api-client-python/1.1.1.dev7+dirty (python 3.8.10; os Darwin; arch + x86_64) + method: GET + uri: https://api.datadoghq.com/api/v1/synthetics/tests + response: + body: + string: '{"tests":[{"status":"paused","public_id":"jv7-wfd-kvt","tags":[],"locations":["pl:pl-kevin-y-6382df0d72d4588e1817f090b131541f"],"message":"","name":"Test on www.example.com","monitor_id":28558768,"type":"api","created_at":"2021-01-12T10:11:40.802074+00:00","modified_at":"2021-01-22T16:42:10.520384+00:00","subtype":"http","config":{"request":{"url":"https://www.example.com","method":"GET","timeout":30},"assertions":[{"operator":"lessThan","type":"responseTime","target":1000},{"operator":"is","type":"statusCode","target":200},{"operator":"A non existent operator","type":"body","target":{"xPath":"//html/head/title","operator":"contains","targetValue":"Example"}}],"configVariables":[]},"options":{"monitor_options":{"notify_audit":false,"locked":false,"include_tags":true,"new_host_delay":300,"notify_no_data":false,"renotify_interval":0},"retry":{"count":0,"interval":300},"min_location_failed":1,"min_failure_duration":0,"tick_every":60}},{"status":"paused","public_id":"jv7-wfd-kvt","tags":[],"locations":["pl:pl-kevin-y-6382df0d72d4588e1817f090b131541f"],"message":"","name":"Test on www.example.com","monitor_id":28558768,"type":"api","created_at":"2021-01-12T10:11:40.802074+00:00","modified_at":"2021-01-22T16:42:10.520384+00:00","subtype":"http","config":{"request":{"url":"https://www.example.com","method":"GET","timeout":30},"assertions":[{"operator":"lessThan","type":"responseTime","target":1000},{"operator":"is","type":"A non existent assertion type","target":200}],"configVariables":[]},"options":{"monitor_options":{"notify_audit":false,"locked":false,"include_tags":true,"new_host_delay":300,"notify_no_data":false,"renotify_interval":0},"retry":{"count":0,"interval":300},"min_location_failed":1,"min_failure_duration":0,"tick_every":60}},{"status":"live","public_id":"2fx-64b-fb8","tags":["mini-website","team:synthetics","firefox","synthetics-ci-browser","edge","chrome"],"locations":["aws:ap-northeast-1","aws:eu-north-1","aws:eu-west-3","aws:eu-central-1"],"message":"This mini-website check failed, please investigate why. @slack-synthetics-ops-worker","name":"Mini Website - Click Trap","monitor_id":7647262,"type":"browser","created_at":"2018-12-20T13:19:23.734004+00:00","modified_at":"2021-06-30T15:46:49.387631+00:00","config":{"variables":[],"setCookie":"","request":{"url":"http://34.95.79.70/click-trap","headers":{},"method":"GET"},"assertions":[],"configVariables":[]},"options":{"ci":{"executionRule":"blocking"},"retry":{"count":1,"interval":1000},"min_location_failed":1,"min_failure_duration":0,"noScreenshot":false,"tick_every":300,"forwardProxy":false,"disableCors":false,"device_ids":["chrome.laptop_large","firefox.laptop_large","A non existent device ID"],"monitor_options":{"renotify_interval":360},"ignoreServerCertificateError":true}},{"status":"live","public_id":"g6d-gcm-pdq","tags":[],"locations":["aws:eu-central-1","aws:ap-northeast-1"],"message":"","name":"Check on www.10.0.0.1.xip.io","monitor_id":7464050,"type":"A non existent test type","created_at":"2018-12-07T17:30:49.785089+00:00","modified_at":"2019-09-04T17:01:09.921070+00:00","subtype":"http","config":{"request":{"url":"https://www.10.0.0.1.xip.io","method":"GET","timeout":30},"assertions":[{"operator":"is","type":"statusCode","target":200}]},"options":{"tick_every":60}},{"status":"live","public_id":"g6d-gcm-pdq","tags":[],"locations":["aws:eu-central-1","aws:ap-northeast-1"],"message":"","name":"Check on www.10.0.0.1.xip.io","monitor_id":7464050,"type":"api","created_at":"2018-12-07T17:30:49.785089+00:00","modified_at":"2019-09-04T17:01:09.921070+00:00","subtype":"http","config":{"request":{"url":"https://www.10.0.0.1.xip.io","method":"A non existent method","timeout":30},"assertions":[{"operator":"is","type":"statusCode","target":200}]},"options":{"tick_every":60}},{"status":"live","public_id":"g6d-gcm-pdq","tags":[],"locations":["aws:eu-central-1","aws:ap-northeast-1"],"message":"A fully valid test","name":"Check on www.10.0.0.1.xip.io","monitor_id":7464050,"type":"api","created_at":"2018-12-07T17:30:49.785089+00:00","modified_at":"2019-09-04T17:01:09.921070+00:00","subtype":"http","config":{"request":{"url":"https://www.10.0.0.1.xip.io","method":"GET","timeout":30},"assertions":[{"operator":"is","type":"statusCode","target":200}]},"options":{"tick_every":60}}]}' + headers: + Connection: + - keep-alive + Content-Length: + - '17244' + Content-Type: + - application/json + Date: + - Fri, 23 Jul 2021 14:21:36 GMT + cache-control: + - no-cache + content-security-policy: + - frame-ancestors 'self'; report-uri https://api.datadoghq.com/csp-report + pragma: + - no-cache + strict-transport-security: + - max-age=15724800; + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ratelimit-limit: + - '120' + x-ratelimit-period: + - '60' + x-ratelimit-remaining: + - '119' + x-ratelimit-reset: + - '24' + status: + code: 200 + message: OK +version: 1 From b8c1fde8f03f909938fc444a94173ed3c19ddcf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Mon, 26 Jul 2021 12:06:28 +0200 Subject: [PATCH 3/9] Support replay-only --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index c91d81afaf..bae718cfc5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -91,6 +91,8 @@ def pytest_bdd_apply_tag(tag, function): if not _disable_recording(): # ignore integration-only scenarios if the recording is enabled skip_tags.add("integration-only") + if os.getenv("RECORD", "false").lower() != "false": + skip_tags.add("replay-only") if tag in skip_tags: marker = pytest.mark.skip(reason=f"skipped because '{tag}' in {skip_tags}") marker(function) From 41297d882e82344ccec1320ae84bbe476f3a5008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Mon, 26 Jul 2021 13:51:56 +0200 Subject: [PATCH 4/9] Handle multiple oneOfs --- .generator/templates/model_utils.mustache | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.generator/templates/model_utils.mustache b/.generator/templates/model_utils.mustache index d10c4d39b4..ce23c1343b 100644 --- a/.generator/templates/model_utils.mustache +++ b/.generator/templates/model_utils.mustache @@ -1658,14 +1658,8 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): oneof_instances.append(oneof_instance) except Exception: pass - if len(oneof_instances) == 0: + if len(oneof_instances) != 1: return UnparsedObject(**model_kwargs) - elif len(oneof_instances) > 1: - raise ApiValueError( - "Invalid inputs given to generate an instance of %s. Multiple " - "oneOf schemas matched the inputs, but a max of one is allowed." % - cls.__name__ - ) return oneof_instances[0] @@ -1843,6 +1837,7 @@ class UnparsedObject(ModelNormal): required_properties = set( [ "_data_store", + "_unparsed", ] ) @@ -1850,6 +1845,7 @@ class UnparsedObject(ModelNormal): def __init__(self, **kwargs): self._data_store = {} + self._unparsed = True for var_name, var_value in kwargs.items(): self.__dict__[var_name] = var_value From 1da1f0266299906a9c5617befbb73dd145fdd65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Mon, 26 Jul 2021 14:50:54 +0200 Subject: [PATCH 5/9] Move the enum error handling in model utils --- .generator/templates/model.mustache | 1 - .../model_templates/method_set_attribute.mustache | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.generator/templates/model.mustache b/.generator/templates/model.mustache index c8eb7875c4..5e6e117f6d 100644 --- a/.generator/templates/model.mustache +++ b/.generator/templates/model.mustache @@ -1,7 +1,6 @@ {{> partial_header }} from {{packageName}}.model_utils import ( # noqa: F401 - ApiValueError, ApiTypeError, ModelComposed, ModelNormal, diff --git a/.generator/templates/model_templates/method_set_attribute.mustache b/.generator/templates/model_templates/method_set_attribute.mustache index 4595d0d887..3a256730e6 100644 --- a/.generator/templates/model_templates/method_set_attribute.mustache +++ b/.generator/templates/model_templates/method_set_attribute.mustache @@ -36,11 +36,12 @@ value, required_types_mixed, path_to_item, self._spec_property_naming, self._check_type, configuration=self._configuration) if (name,) in self.allowed_values: - check_allowed_values( - self.allowed_values, - (name,), - value - ) + try: + check_allowed_values(self.allowed_values, (name,), value) + except ApiValueError: + self.__dict__["_data_store"][name] = value + self._unparsed = True + return if (name,) in self.validations: check_validations( self.validations, From 00b7b29ef2ed886d29b763494966eef22fea2212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Mon, 9 Aug 2021 10:28:39 +0200 Subject: [PATCH 6/9] Add tests, remove cov options --- setup.cfg | 1 - tests/test_deserialization.py | 264 ++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 tests/test_deserialization.py diff --git a/setup.cfg b/setup.cfg index b66dda4562..61b7febe34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -67,7 +67,6 @@ exclude = tests where=src [tool:pytest] -addopts = --cov=datadog_api_client --cov-config .coveragerc # addopts = --black --cov=datadog_api_client --cov-config .coveragerc --cov-report=term-missing [flake8] diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py new file mode 100644 index 0000000000..1d284019a3 --- /dev/null +++ b/tests/test_deserialization.py @@ -0,0 +1,264 @@ +import json + +from datadog_api_client.v1.model.synthetics_api_test import SyntheticsAPITest +from datadog_api_client.v1.model.synthetics_browser_test import SyntheticsBrowserTest +from datadog_api_client.v1.model.synthetics_test_request import SyntheticsTestRequest +from datadog_api_client.v1.model_utils import validate_and_convert_types +from datadog_api_client.v1 import Configuration as Configuration +from datadog_api_client.v2.model.logs_archive import LogsArchive +from datadog_api_client.v2.model.logs_archive_destination import LogsArchiveDestination +from datadog_api_client.v2.model_utils import validate_and_convert_types as validate_and_convert_types_v2 +from datadog_api_client.v2 import Configuration as ConfigurationV2 + + +def test_unknown_nested_oneof_in_list(): + body = """{ + "status": "paused", + "public_id": "jv7-wfd-kvt", + "tags": [], + "locations": [ + "pl:pl-kevin-y-6382df0d72d4588e1817f090b131541f" + ], + "message": "", + "name": "Test on www.example.com", + "monitor_id": 28558768, + "type": "api", + "created_at": "2021-01-12T10:11:40.802074+00:00", + "modified_at": "2021-01-22T16:42:10.520384+00:00", + "subtype": "http", + "config": { + "request": { + "url": "https://www.example.com", + "method": "GET", + "timeout": 30 + }, + "assertions": [ + { + "operator": "lessThan", + "type": "responseTime", + "target": 1000 + }, + { + "operator": "is", + "type": "statusCode", + "target": 200 + }, + { + "operator": "A non existent operator", + "type": "body", + "target": { + "xPath": "//html/head/title", + "operator": "contains", + "targetValue": "Example" + } + } + ], + "configVariables": [] + }, + "options": { + "monitor_options": { + "notify_audit": false, + "locked": false, + "include_tags": true, + "new_host_delay": 300, + "notify_no_data": false, + "renotify_interval": 0 + }, + "retry": { + "count": 0, + "interval": 300 + }, + "min_location_failed": 1, + "min_failure_duration": 0, + "tick_every": 60 + } + }""" + config = Configuration() + deserialized_data = validate_and_convert_types( + json.loads(body), (SyntheticsAPITest,), ["received_data"], True, True, config) + assert isinstance(deserialized_data, SyntheticsAPITest) + assert len(deserialized_data.config.assertions) == 3 + assert deserialized_data.config.assertions[2].operator == "A non existent operator" + + +def test_unknown_nested_enum_in_list(): + body = """{ + "status": "live", + "public_id": "2fx-64b-fb8", + "tags": [ + "mini-website", + "team:synthetics", + "firefox", + "synthetics-ci-browser", + "edge", + "chrome" + ], + "locations": [ + "aws:ap-northeast-1", + "aws:eu-north-1", + "aws:eu-west-3", + "aws:eu-central-1" + ], + "message": "This mini-website check failed, please investigate why. @slack-synthetics-ops-worker", + "name": "Mini Website - Click Trap", + "monitor_id": 7647262, + "type": "browser", + "created_at": "2018-12-20T13:19:23.734004+00:00", + "modified_at": "2021-06-30T15:46:49.387631+00:00", + "config": { + "variables": [], + "setCookie": "", + "request": { + "url": "http://34.95.79.70/click-trap", + "headers": {}, + "method": "GET" + }, + "assertions": [], + "configVariables": [] + }, + "options": { + "ci": { + "executionRule": "blocking" + }, + "retry": { + "count": 1, + "interval": 1000 + }, + "min_location_failed": 1, + "min_failure_duration": 0, + "noScreenshot": false, + "tick_every": 300, + "forwardProxy": false, + "disableCors": false, + "device_ids": [ + "chrome.laptop_large", + "firefox.laptop_large", + "A non existent device ID" + ], + "monitor_options": { + "renotify_interval": 360 + }, + "ignoreServerCertificateError": true + } + }""" + config = Configuration() + deserialized_data = validate_and_convert_types( + json.loads(body), (SyntheticsBrowserTest,), ["received_data"], True, True, config) + assert isinstance(deserialized_data, SyntheticsBrowserTest) + assert len(deserialized_data.options.device_ids) == 3 + assert str(deserialized_data.options.device_ids[2]) == "A non existent device ID" + + +def test_unknown_top_level_enum(): + body = """{ + "status": "live", + "public_id": "g6d-gcm-pdq", + "tags": [], + "locations": [ + "aws:eu-central-1", + "aws:ap-northeast-1" + ], + "message": "", + "name": "Check on www.10.0.0.1.xip.io", + "monitor_id": 7464050, + "type": "A non existent test type", + "created_at": "2018-12-07T17:30:49.785089+00:00", + "modified_at": "2019-09-04T17:01:09.921070+00:00", + "subtype": "http", + "config": { + "request": { + "url": "https://www.10.0.0.1.xip.io", + "method": "GET", + "timeout": 30 + }, + "assertions": [ + { + "operator": "is", + "type": "statusCode", + "target": 200 + } + ] + }, + "options": { + "tick_every": 60 + } + }""" + config = Configuration() + deserialized_data = validate_and_convert_types( + json.loads(body), (SyntheticsBrowserTest,), ["received_data"], True, True, config) + assert isinstance(deserialized_data, SyntheticsBrowserTest) + assert str(deserialized_data.type) == "A non existent test type" + + +def test_unknown_nested_enum(): + body = """{ + "status": "live", + "public_id": "g6d-gcm-pdq", + "tags": [], + "locations": [ + "aws:eu-central-1", + "aws:ap-northeast-1" + ], + "message": "", + "name": "Check on www.10.0.0.1.xip.io", + "monitor_id": 7464050, + "type": "api", + "created_at": "2018-12-07T17:30:49.785089+00:00", + "modified_at": "2019-09-04T17:01:09.921070+00:00", + "subtype": "http", + "config": { + "request": { + "url": "https://www.10.0.0.1.xip.io", + "method": "A non existent method", + "timeout": 30 + }, + "assertions": [ + { + "operator": "is", + "type": "statusCode", + "target": 200 + } + ] + }, + "options": { + "tick_every": 60 + } + }""" + config = Configuration() + deserialized_data = validate_and_convert_types( + json.loads(body), (SyntheticsAPITest,), ["received_data"], True, True, config) + assert isinstance(deserialized_data, SyntheticsAPITest) + assert isinstance(deserialized_data.config.request, SyntheticsTestRequest) + assert str(deserialized_data.config.request.method) == "A non existent method" + + +def test_unknown_nested_one_of(): + body = """{ + "data": { + "type": "archives", + "id": "n_XDSxVpScepiBnyhysj_A", + "attributes": { + "name": "my first azure archive", + "query": "service:toto", + "state": "UNKNOWN", + "destination": { + "container": "my-container", + "storage_account": "storageaccount", + "path": "/path/blou", + "type": "A non existent destination", + "integration": { + "tenant_id": "tf-TestAccDatadogLogsArchiveAzure_basic-local-1624981538", + "client_id": "testc7f6-1234-5678-9101-3fcbf464test" + } + }, + "rehydration_tags": [], + "include_tags": false + } + } + }""" + config = ConfigurationV2() + deserialized_data = validate_and_convert_types_v2( + json.loads(body), (LogsArchive,), ["received_data"], True, True, config) + assert isinstance(deserialized_data, LogsArchive) + assert isinstance(deserialized_data.data.attributes.destination, LogsArchiveDestination) + assert deserialized_data.data.attributes.destination.type == "A non existent destination" From a7d958ab1d6a252570e28e700452919d700fb82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Mon, 9 Aug 2021 11:54:43 +0200 Subject: [PATCH 7/9] Fix rebase --- .generator/templates/model_utils.mustache | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.generator/templates/model_utils.mustache b/.generator/templates/model_utils.mustache index ce23c1343b..35e01937da 100644 --- a/.generator/templates/model_utils.mustache +++ b/.generator/templates/model_utils.mustache @@ -355,6 +355,7 @@ class OpenApiModel(object): self._path_to_item = _path_to_item self._configuration = _configuration self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + self._unparsed = False @classmethod def _from_openapi_data(cls, kwargs): @@ -396,6 +397,7 @@ class ModelSimple(OpenApiModel): '_path_to_item', '_configuration', '_visited_composed_classes', + '_unparsed', ]) {{> model_templates/methods_setattr_getattr_normal }} @@ -414,6 +416,7 @@ class ModelNormal(OpenApiModel): '_path_to_item', '_configuration', '_visited_composed_classes', + '_unparsed', ]) {{> model_templates/methods_setattr_getattr_normal }} @@ -484,6 +487,7 @@ class ModelComposed(OpenApiModel): '_composed_instances', '_var_name_to_model_instances', '_additional_properties_model_instances', + '_unparsed', ]) def __init__(self, kwargs): From 1f6a00798a7837f39c89cd0b5ef93e823d7e5560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Herv=C3=A9?= Date: Tue, 10 Aug 2021 10:25:51 +0200 Subject: [PATCH 8/9] Add unparsed check --- tests/test_deserialization.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py index 1d284019a3..f1864d93f0 100644 --- a/tests/test_deserialization.py +++ b/tests/test_deserialization.py @@ -79,6 +79,8 @@ def test_unknown_nested_oneof_in_list(): assert isinstance(deserialized_data, SyntheticsAPITest) assert len(deserialized_data.config.assertions) == 3 assert deserialized_data.config.assertions[2].operator == "A non existent operator" + assert deserialized_data.config.assertions[2]._composed_instances[0]._unparsed + assert not deserialized_data.config.assertions[1]._composed_instances[0]._unparsed def test_unknown_nested_enum_in_list(): @@ -147,6 +149,7 @@ def test_unknown_nested_enum_in_list(): assert isinstance(deserialized_data, SyntheticsBrowserTest) assert len(deserialized_data.options.device_ids) == 3 assert str(deserialized_data.options.device_ids[2]) == "A non existent device ID" + assert deserialized_data.options.device_ids[2]._unparsed def test_unknown_top_level_enum(): @@ -230,6 +233,7 @@ def test_unknown_nested_enum(): assert isinstance(deserialized_data, SyntheticsAPITest) assert isinstance(deserialized_data.config.request, SyntheticsTestRequest) assert str(deserialized_data.config.request.method) == "A non existent method" + assert deserialized_data.config.request.method._unparsed def test_unknown_nested_one_of(): @@ -262,3 +266,4 @@ def test_unknown_nested_one_of(): assert isinstance(deserialized_data, LogsArchive) assert isinstance(deserialized_data.data.attributes.destination, LogsArchiveDestination) assert deserialized_data.data.attributes.destination.type == "A non existent destination" + assert deserialized_data.data.attributes.destination._composed_instances[0]._unparsed From 7d861fbdc03f38bc5497ef052961d58e28adfebf Mon Sep 17 00:00:00 2001 From: "ci.datadog-api-spec" Date: Tue, 10 Aug 2021 08:30:18 +0000 Subject: [PATCH 9/9] Regenerate client from commit 9684c06 of spec repo --- .apigentools-info | 8 +-- src/datadog_api_client/v1/model_utils.py | 65 +++++++++++++++++++----- src/datadog_api_client/v2/model_utils.py | 65 +++++++++++++++++++----- tests/v1/features/synthetics.feature | 2 +- 4 files changed, 111 insertions(+), 29 deletions(-) diff --git a/.apigentools-info b/.apigentools-info index 8c18b37a19..33ebecebbf 100644 --- a/.apigentools-info +++ b/.apigentools-info @@ -4,13 +4,13 @@ "spec_versions": { "v1": { "apigentools_version": "1.4.1.dev11", - "regenerated": "2021-08-09 14:31:45.996631", - "spec_repo_commit": "f7db18b" + "regenerated": "2021-08-10 08:29:52.303572", + "spec_repo_commit": "9684c06" }, "v2": { "apigentools_version": "1.4.1.dev11", - "regenerated": "2021-08-09 14:32:10.644831", - "spec_repo_commit": "f7db18b" + "regenerated": "2021-08-10 08:30:15.608867", + "spec_repo_commit": "9684c06" } } } \ No newline at end of file diff --git a/src/datadog_api_client/v1/model_utils.py b/src/datadog_api_client/v1/model_utils.py index 80cbbb8759..34d36cd5a3 100644 --- a/src/datadog_api_client/v1/model_utils.py +++ b/src/datadog_api_client/v1/model_utils.py @@ -143,10 +143,17 @@ def set_attribute(self, name, value): configuration=self._configuration, ) if (name,) in self.allowed_values: - check_allowed_values(self.allowed_values, (name,), value) + try: + check_allowed_values(self.allowed_values, (name,), value) + except ApiValueError: + self.__dict__["_data_store"][name] = value + self._unparsed = True + return if (name,) in self.validations: check_validations(self.validations, (name,), value, self._configuration) self.__dict__["_data_store"][name] = value + if isinstance(value, OpenApiModel) and value._unparsed: + self._unparsed = True def __repr__(self): """For `print` and `pprint`""" @@ -383,6 +390,7 @@ def __init__(self, kwargs): self._path_to_item = _path_to_item self._configuration = _configuration self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + self._unparsed = False @classmethod def _from_openapi_data(cls, kwargs): @@ -427,6 +435,7 @@ class ModelSimple(OpenApiModel): "_path_to_item", "_configuration", "_visited_composed_classes", + "_unparsed", ] ) @@ -487,6 +496,7 @@ class ModelNormal(OpenApiModel): "_path_to_item", "_configuration", "_visited_composed_classes", + "_unparsed", ] ) @@ -611,6 +621,7 @@ class ModelComposed(OpenApiModel): "_composed_instances", "_var_name_to_model_instances", "_additional_properties_model_instances", + "_unparsed", ] ) @@ -1818,19 +1829,12 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): constant_kwargs["_check_type"], configuration=constant_kwargs["_configuration"], ) - oneof_instances.append(oneof_instance) + if not oneof_instance._unparsed: + oneof_instances.append(oneof_instance) except Exception: pass - if len(oneof_instances) == 0: - raise ApiValueError( - "Invalid inputs given to generate an instance of %s. None " - "of the oneOf schemas matched the input data." % cls.__name__ - ) - elif len(oneof_instances) > 1: - raise ApiValueError( - "Invalid inputs given to generate an instance of %s. Multiple " - "oneOf schemas matched the inputs, but a max of one is allowed." % cls.__name__ - ) + if len(oneof_instances) != 1: + return UnparsedObject(**model_kwargs) return oneof_instances[0] @@ -1978,3 +1982,40 @@ def validate_get_composed_info(constant_args, model_args, self): var_name_to_model_instances[prop_name] = [self] + composed_instances return [composed_instances, var_name_to_model_instances, additional_properties_model_instances, discarded_args] + + +class UnparsedObject(ModelNormal): + """A model for an oneOf we don't know about.""" + + allowed_values = {} + + validations = {} + + additional_properties_type = None + + _nullable = False + + openapi_types = {} + + discriminator = None + + attribute_map = {} + + _composed_schemas = {} + + required_properties = set( + [ + "_data_store", + "_unparsed", + ] + ) + + @convert_js_args_to_python_args + def __init__(self, **kwargs): + + self._data_store = {} + self._unparsed = True + + for var_name, var_value in kwargs.items(): + self.__dict__[var_name] = var_value + self.__dict__["_data_store"][var_name] = var_value diff --git a/src/datadog_api_client/v2/model_utils.py b/src/datadog_api_client/v2/model_utils.py index 446f9fb454..686612c79d 100644 --- a/src/datadog_api_client/v2/model_utils.py +++ b/src/datadog_api_client/v2/model_utils.py @@ -143,10 +143,17 @@ def set_attribute(self, name, value): configuration=self._configuration, ) if (name,) in self.allowed_values: - check_allowed_values(self.allowed_values, (name,), value) + try: + check_allowed_values(self.allowed_values, (name,), value) + except ApiValueError: + self.__dict__["_data_store"][name] = value + self._unparsed = True + return if (name,) in self.validations: check_validations(self.validations, (name,), value, self._configuration) self.__dict__["_data_store"][name] = value + if isinstance(value, OpenApiModel) and value._unparsed: + self._unparsed = True def __repr__(self): """For `print` and `pprint`""" @@ -383,6 +390,7 @@ def __init__(self, kwargs): self._path_to_item = _path_to_item self._configuration = _configuration self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + self._unparsed = False @classmethod def _from_openapi_data(cls, kwargs): @@ -427,6 +435,7 @@ class ModelSimple(OpenApiModel): "_path_to_item", "_configuration", "_visited_composed_classes", + "_unparsed", ] ) @@ -487,6 +496,7 @@ class ModelNormal(OpenApiModel): "_path_to_item", "_configuration", "_visited_composed_classes", + "_unparsed", ] ) @@ -611,6 +621,7 @@ class ModelComposed(OpenApiModel): "_composed_instances", "_var_name_to_model_instances", "_additional_properties_model_instances", + "_unparsed", ] ) @@ -1818,19 +1829,12 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None): constant_kwargs["_check_type"], configuration=constant_kwargs["_configuration"], ) - oneof_instances.append(oneof_instance) + if not oneof_instance._unparsed: + oneof_instances.append(oneof_instance) except Exception: pass - if len(oneof_instances) == 0: - raise ApiValueError( - "Invalid inputs given to generate an instance of %s. None " - "of the oneOf schemas matched the input data." % cls.__name__ - ) - elif len(oneof_instances) > 1: - raise ApiValueError( - "Invalid inputs given to generate an instance of %s. Multiple " - "oneOf schemas matched the inputs, but a max of one is allowed." % cls.__name__ - ) + if len(oneof_instances) != 1: + return UnparsedObject(**model_kwargs) return oneof_instances[0] @@ -1978,3 +1982,40 @@ def validate_get_composed_info(constant_args, model_args, self): var_name_to_model_instances[prop_name] = [self] + composed_instances return [composed_instances, var_name_to_model_instances, additional_properties_model_instances, discarded_args] + + +class UnparsedObject(ModelNormal): + """A model for an oneOf we don't know about.""" + + allowed_values = {} + + validations = {} + + additional_properties_type = None + + _nullable = False + + openapi_types = {} + + discriminator = None + + attribute_map = {} + + _composed_schemas = {} + + required_properties = set( + [ + "_data_store", + "_unparsed", + ] + ) + + @convert_js_args_to_python_args + def __init__(self, **kwargs): + + self._data_store = {} + self._unparsed = True + + for var_name, var_value in kwargs.items(): + self.__dict__[var_name] = var_value + self.__dict__["_data_store"][var_name] = var_value diff --git a/tests/v1/features/synthetics.feature b/tests/v1/features/synthetics.feature index f1cf427ba1..463bbcd734 100644 --- a/tests/v1/features/synthetics.feature +++ b/tests/v1/features/synthetics.feature @@ -15,7 +15,7 @@ Feature: Synthetics And a valid "appKeyAuth" key in the system And an instance of "Synthetics" API - @replay-only @skip-java @skip-python @skip-ruby + @replay-only @skip-java @skip-ruby Scenario: Client is resilient to enum and oneOf deserialization errors Given new "ListTests" request When the request is sent