From 28f5ff69b1319d3d94e1bd4f7931d73e67cf1b89 Mon Sep 17 00:00:00 2001 From: Fabrizio Leoni Date: Thu, 11 Sep 2025 10:59:50 +0200 Subject: [PATCH] fix/feat(Search): regenerated services with latest API definition Signed-off-by: Fabrizio Leoni --- ibm_platform_services/global_search_v2.py | 322 +++++++++++++++++----- test/unit/test_global_search_v2.py | 159 ++++++++--- 2 files changed, 374 insertions(+), 107 deletions(-) diff --git a/ibm_platform_services/global_search_v2.py b/ibm_platform_services/global_search_v2.py index d18a57e3..bd15c825 100644 --- a/ibm_platform_services/global_search_v2.py +++ b/ibm_platform_services/global_search_v2.py @@ -1,6 +1,6 @@ # coding: utf-8 -# (C) Copyright IBM Corp. 2024. +# (C) Copyright IBM Corp. 2025. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# IBM OpenAPI SDK Code Generator Version: 3.87.0-91c7c775-20240320-213027 +# IBM OpenAPI SDK Code Generator Version: 3.100.0-2ad7a784-20250212-162551 """ Search for resources with the global and shared resource properties repository that is @@ -36,7 +36,7 @@ from ibm_cloud_sdk_core import BaseService, DetailedResponse from ibm_cloud_sdk_core.authenticators.authenticator import Authenticator from ibm_cloud_sdk_core.get_authenticator import get_authenticator_from_environment -from ibm_cloud_sdk_core.utils import convert_list +from ibm_cloud_sdk_core.utils import convert_list, convert_model from .common import get_sdk_headers @@ -61,7 +61,9 @@ def new_instance( parameters and external configuration. """ authenticator = get_authenticator_from_environment(service_name) - service = cls(authenticator) + service = cls( + authenticator + ) service.configure_service(service_name) return service @@ -84,10 +86,8 @@ def __init__( def search( self, + body: 'SearchRequest', *, - query: Optional[str] = None, - fields: Optional[List[str]] = None, - search_cursor: Optional[str] = None, x_request_id: Optional[str] = None, x_correlation_id: Optional[str] = None, account_id: Optional[str] = None, @@ -96,7 +96,6 @@ def search( sort: Optional[List[str]] = None, is_deleted: Optional[str] = None, is_reclaimed: Optional[str] = None, - is_public: Optional[str] = None, impersonate_user: Optional[str] = None, can_tag: Optional[str] = None, is_project_resource: Optional[str] = None, @@ -106,33 +105,24 @@ def search( Find instances of resources (v3). Find IAM-enabled resources or storage and network resources that run on classic - infrastructure in a specific account ID. You can apply query strings if necessary. - To filter results, you can insert a string by using the Lucene syntax and the - query string is parsed into a series of terms and operators. A term can be a - single word or a phrase, in which case the search is performed for all the words, - in the same order. To filter for a specific value regardless of the property that - contains it, type the search term without specifying a field. Only resources that - belong to the account ID and that are accessible by the client are returned. + infrastructure in a specific account ID. You must use `/v3/resources/search` when you need to fetch more than `10000` resource items. On the first call, the operation returns a live cursor on the data that you must use on all the subsequent calls to get the next batch of results until you get the empty result set. - By default, the fields that are returned for every resource are `crn`, `name`, - `family`, `type`, and `account_id`. You can specify the subset of the fields you - want in your request using the `fields` request body attribute. Set `"fields": - ["*"]` to discover the set of fields which are available to request. - - :param str query: (optional) The Lucene-formatted query string. Default to - '*' if not set. - :param List[str] fields: (optional) The list of the fields returned by the - search. By default, the returned fields are the `account_id`, `name`, - `type`, `family`, and `crn`. For all queries, `crn` is always returned. You - may set `"fields": ["*"]` to discover the set of fields available to - request. - :param str search_cursor: (optional) An opaque cursor that is returned on - each call and that must be set on the subsequent call to get the next batch - of items. If the search returns no items, then the search_cursor is not - present in the response. + To filter results, you can apply query strings following the *Lucene* query + syntax. + By default, the fields that are returned for every resource are **crn**, **name**, + **family**, **type**, and **account_id**. You can specify the subset of the fields + you want in your request using the `fields` request body attribute. Set `"fields": + ["*"]` to discover the complete set of fields which are available to request. + + :param SearchRequest body: It contains the query filters on the first + operation call, or the search_cursor on next calls. On subsequent calls, + set the `search_cursor` to the value returned by the previous call. After + the first, you must set only the `search_cursor`. Any other parameter but + the `search_cursor` are ignored. The `search_cursor` encodes all the + information that needs to get the next batch of `limit` data. :param str x_request_id: (optional) An alphanumeric string that is used to trace the request. The value may include ASCII alphanumerics and any of following segment separators: space ( ), comma (,), hyphen, (-), and @@ -169,10 +159,6 @@ def search( (default), true or any. If false, only not reclaimed documents are returned; if true, only reclaimed documents are returned; If any, both reclaimed and not reclaimed documents are returned. - :param str is_public: (optional) Determines if public resources should be - included in result set or not. Possible values are false (default), true or - any. If false, do not search public resources; if true, search only public - resources; If any, search also public resources. :param str impersonate_user: (optional) The user on whose behalf the search must be performed. Only a GhoST admin can impersonate a user, so be sure you set a GhoST admin IAM token in the Authorization header if you set this @@ -194,6 +180,10 @@ def search( :rtype: DetailedResponse with `dict` result representing a `ScanResult` object """ + if body is None: + raise ValueError('body must be provided') + if isinstance(body, SearchRequest): + body = convert_model(body) headers = { 'x-request-id': x_request_id, 'x-correlation-id': x_correlation_id, @@ -212,19 +202,12 @@ def search( 'sort': convert_list(sort), 'is_deleted': is_deleted, 'is_reclaimed': is_reclaimed, - 'is_public': is_public, 'impersonate_user': impersonate_user, 'can_tag': can_tag, 'is_project_resource': is_project_resource, } - data = { - 'query': query, - 'fields': fields, - 'search_cursor': search_cursor, - } - data = {k: v for (k, v) in data.items() if v is not None} - data = json.dumps(data) + data = json.dumps(body) headers['content-type'] = 'application/json' if 'headers' in kwargs: @@ -261,7 +244,6 @@ class IsDeleted(str, Enum): TRUE = 'true' FALSE = 'false' ANY = 'any' - class IsReclaimed(str, Enum): """ Determines if reclaimed documents should be included in result set or not. @@ -273,18 +255,6 @@ class IsReclaimed(str, Enum): TRUE = 'true' FALSE = 'false' ANY = 'any' - - class IsPublic(str, Enum): - """ - Determines if public resources should be included in result set or not. Possible - values are false (default), true or any. If false, do not search public resources; - if true, search only public resources; If any, search also public resources. - """ - - TRUE = 'true' - FALSE = 'false' - ANY = 'any' - class CanTag(str, Enum): """ Determines if the result set must return the resources that the user can tag or @@ -296,7 +266,6 @@ class CanTag(str, Enum): TRUE = 'true' FALSE = 'false' - class IsProjectResource(str, Enum): """ Determines if documents belonging to Project family should be included in result @@ -322,6 +291,8 @@ class ResultItem: other properties that depend on the resource type. :param str crn: Resource identifier in CRN format. + + This type supports additional properties of type object. """ # The set of defined properties for the class @@ -330,17 +301,22 @@ class ResultItem: def __init__( self, crn: str, - **kwargs, + **kwargs: Optional[object], ) -> None: """ Initialize a ResultItem object. :param str crn: Resource identifier in CRN format. - :param **kwargs: (optional) Any additional properties. + :param object **kwargs: (optional) Additional properties of type object """ self.crn = crn - for _key, _value in kwargs.items(): - setattr(self, _key, _value) + for k, v in kwargs.items(): + if k not in ResultItem._properties: + if not isinstance(v, object): + raise ValueError('Value for additional property {} must be of type object'.format(k)) + setattr(self, k, v) + else: + raise ValueError('Property {} cannot be specified as an additional property'.format(k)) @classmethod def from_dict(cls, _dict: Dict) -> 'ResultItem': @@ -350,7 +326,11 @@ def from_dict(cls, _dict: Dict) -> 'ResultItem': args['crn'] = crn else: raise ValueError('Required property \'crn\' not present in ResultItem JSON') - args.update({k: v for (k, v) in _dict.items() if k not in cls._properties}) + for k, v in _dict.items(): + if k not in cls._properties: + if not isinstance(v, object): + raise ValueError('Value for additional property {} must be of type object'.format(k)) + args[k] = v return cls(**args) @classmethod @@ -363,8 +343,8 @@ def to_dict(self) -> Dict: _dict = {} if hasattr(self, 'crn') and self.crn is not None: _dict['crn'] = self.crn - for _key in [k for k in vars(self).keys() if k not in ResultItem._properties]: - _dict[_key] = getattr(self, _key) + for k in [_k for _k in vars(self).keys() if _k not in ResultItem._properties]: + _dict[k] = getattr(self, k) return _dict def _to_dict(self): @@ -372,21 +352,23 @@ def _to_dict(self): return self.to_dict() def get_properties(self) -> Dict: - """Return a dictionary of arbitrary properties from this instance of ResultItem""" + """Return the additional properties from this instance of ResultItem in the form of a dict.""" _dict = {} - - for _key in [k for k in vars(self).keys() if k not in ResultItem._properties]: - _dict[_key] = getattr(self, _key) + for k in [_k for _k in vars(self).keys() if _k not in ResultItem._properties]: + _dict[k] = getattr(self, k) return _dict def set_properties(self, _dict: dict): - """Set a dictionary of arbitrary properties to this instance of ResultItem""" - for _key in [k for k in vars(self).keys() if k not in ResultItem._properties]: - delattr(self, _key) - - for _key, _value in _dict.items(): - if _key not in ResultItem._properties: - setattr(self, _key, _value) + """Set a dictionary of additional properties in this instance of ResultItem""" + for k in [_k for _k in vars(self).keys() if _k not in ResultItem._properties]: + delattr(self, k) + for k, v in _dict.items(): + if k not in ResultItem._properties: + if not isinstance(v, object): + raise ValueError('Value for additional property {} must be of type object'.format(k)) + setattr(self, k, v) + else: + raise ValueError('Property {} cannot be specified as an additional property'.format(k)) def __str__(self) -> str: """Return a `str` version of this ResultItem object.""" @@ -495,3 +477,193 @@ def __eq__(self, other: 'ScanResult') -> bool: def __ne__(self, other: 'ScanResult') -> bool: """Return `true` when self and other are not equal, false otherwise.""" return not self == other + + +class SearchRequest: + """ + SearchRequest. + + """ + + def __init__( + self, + ) -> None: + """ + Initialize a SearchRequest object. + + """ + msg = "Cannot instantiate base class. Instead, instantiate one of the defined subclasses: {0}".format( + ", ".join(['SearchRequestFirstCall', 'SearchRequestNextCall']) + ) + raise Exception(msg) + + +class SearchRequestFirstCall(SearchRequest): + """ + The request body when calling the first time the v3 search. + + :param str query: The Lucene-formatted query string. Default to '*' if not set. + :param List[str] fields: (optional) The list of the fields returned by the + search. By default, the returned fields are the `account_id`, `name`, `type`, + `family`, and `crn`. For all queries, `crn` is always returned. You may set + `"fields": ["*"]` to discover the set of fields available to request. + """ + + def __init__( + self, + query: str, + *, + fields: Optional[List[str]] = None, + ) -> None: + """ + Initialize a SearchRequestFirstCall object. + + :param str query: The Lucene-formatted query string. Default to '*' if not + set. + :param List[str] fields: (optional) The list of the fields returned by the + search. By default, the returned fields are the `account_id`, `name`, + `type`, `family`, and `crn`. For all queries, `crn` is always returned. You + may set `"fields": ["*"]` to discover the set of fields available to + request. + """ + # pylint: disable=super-init-not-called + self.query = query + self.fields = fields + + @classmethod + def from_dict(cls, _dict: Dict) -> 'SearchRequestFirstCall': + """Initialize a SearchRequestFirstCall object from a json dictionary.""" + args = {} + if (query := _dict.get('query')) is not None: + args['query'] = query + else: + raise ValueError('Required property \'query\' not present in SearchRequestFirstCall JSON') + if (fields := _dict.get('fields')) is not None: + args['fields'] = fields + return cls(**args) + + @classmethod + def _from_dict(cls, _dict): + """Initialize a SearchRequestFirstCall object from a json dictionary.""" + return cls.from_dict(_dict) + + def to_dict(self) -> Dict: + """Return a json dictionary representing this model.""" + _dict = {} + if hasattr(self, 'query') and self.query is not None: + _dict['query'] = self.query + if hasattr(self, 'fields') and self.fields is not None: + _dict['fields'] = self.fields + return _dict + + def _to_dict(self): + """Return a json dictionary representing this model.""" + return self.to_dict() + + def __str__(self) -> str: + """Return a `str` version of this SearchRequestFirstCall object.""" + return json.dumps(self.to_dict(), indent=2) + + def __eq__(self, other: 'SearchRequestFirstCall') -> bool: + """Return `true` when self and other are equal, false otherwise.""" + if not isinstance(other, self.__class__): + return False + return self.__dict__ == other.__dict__ + + def __ne__(self, other: 'SearchRequestFirstCall') -> bool: + """Return `true` when self and other are not equal, false otherwise.""" + return not self == other + + +class SearchRequestNextCall(SearchRequest): + """ + The request body when calling the v3 search as second or next time, in order to + retrieve further items. + + :param str search_cursor: An opaque cursor that is returned on each call and + that must be set on the subsequent call to get the next batch of items. If the + search returns no items, then the search_cursor is not present in the response. + NOTE: any other properties present in the body will be ignored. + :param str query: (optional) The Lucene-formatted query string. Default to '*' + if not set. + :param List[str] fields: (optional) The list of the fields returned by the + search. By default, the returned fields are the `account_id`, `name`, `type`, + `family`, and `crn`. For all queries, `crn` is always returned. You may set + `"fields": ["*"]` to discover the set of fields available to request. + """ + + def __init__( + self, + search_cursor: str, + *, + query: Optional[str] = None, + fields: Optional[List[str]] = None, + ) -> None: + """ + Initialize a SearchRequestNextCall object. + + :param str search_cursor: An opaque cursor that is returned on each call + and that must be set on the subsequent call to get the next batch of items. + If the search returns no items, then the search_cursor is not present in + the response. NOTE: any other properties present in the body will be + ignored. + :param str query: (optional) The Lucene-formatted query string. Default to + '*' if not set. + :param List[str] fields: (optional) The list of the fields returned by the + search. By default, the returned fields are the `account_id`, `name`, + `type`, `family`, and `crn`. For all queries, `crn` is always returned. You + may set `"fields": ["*"]` to discover the set of fields available to + request. + """ + # pylint: disable=super-init-not-called + self.search_cursor = search_cursor + self.query = query + self.fields = fields + + @classmethod + def from_dict(cls, _dict: Dict) -> 'SearchRequestNextCall': + """Initialize a SearchRequestNextCall object from a json dictionary.""" + args = {} + if (search_cursor := _dict.get('search_cursor')) is not None: + args['search_cursor'] = search_cursor + else: + raise ValueError('Required property \'search_cursor\' not present in SearchRequestNextCall JSON') + if (query := _dict.get('query')) is not None: + args['query'] = query + if (fields := _dict.get('fields')) is not None: + args['fields'] = fields + return cls(**args) + + @classmethod + def _from_dict(cls, _dict): + """Initialize a SearchRequestNextCall object from a json dictionary.""" + return cls.from_dict(_dict) + + def to_dict(self) -> Dict: + """Return a json dictionary representing this model.""" + _dict = {} + if hasattr(self, 'search_cursor') and self.search_cursor is not None: + _dict['search_cursor'] = self.search_cursor + if hasattr(self, 'query') and self.query is not None: + _dict['query'] = self.query + if hasattr(self, 'fields') and self.fields is not None: + _dict['fields'] = self.fields + return _dict + + def _to_dict(self): + """Return a json dictionary representing this model.""" + return self.to_dict() + + def __str__(self) -> str: + """Return a `str` version of this SearchRequestNextCall object.""" + return json.dumps(self.to_dict(), indent=2) + + def __eq__(self, other: 'SearchRequestNextCall') -> bool: + """Return `true` when self and other are equal, false otherwise.""" + if not isinstance(other, self.__class__): + return False + return self.__dict__ == other.__dict__ + + def __ne__(self, other: 'SearchRequestNextCall') -> bool: + """Return `true` when self and other are not equal, false otherwise.""" + return not self == other diff --git a/test/unit/test_global_search_v2.py b/test/unit/test_global_search_v2.py index 8afa4a3c..5edf33dd 100644 --- a/test/unit/test_global_search_v2.py +++ b/test/unit/test_global_search_v2.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# (C) Copyright IBM Corp. 2024. +# (C) Copyright IBM Corp. 2025. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,7 +29,9 @@ from ibm_platform_services.global_search_v2 import * -_service = GlobalSearchV2(authenticator=NoAuthAuthenticator()) +_service = GlobalSearchV2( + authenticator=NoAuthAuthenticator() +) _base_url = 'https://api.global-search-tagging.cloud.ibm.com' _service.set_service_url(_base_url) @@ -42,15 +44,8 @@ def preprocess_url(operation_path: str): The returned request URL is used to register the mock response so it needs to match the request URL that is formed by the requests library. """ - # First, unquote the path since it might have some quoted/escaped characters in it - # due to how the generator inserts the operation paths into the unit test code. - operation_path = urllib.parse.unquote(operation_path) - # Next, quote the path using urllib so that we approximate what will - # happen during request processing. - operation_path = urllib.parse.quote(operation_path, safe='/') - - # Finally, form the request URL from the base URL and operation path. + # Form the request URL from the base URL and operation path. request_url = _base_url + operation_path # If the request url does NOT end with a /, then just return it as-is. @@ -115,10 +110,13 @@ def test_search_all_params(self): status=200, ) + # Construct a dict representation of a SearchRequestFirstCall model + search_request_model = {} + search_request_model['query'] = 'testString' + search_request_model['fields'] = ['testString'] + # Set up parameter values - query = 'testString' - fields = ['testString'] - search_cursor = 'testString' + body = search_request_model x_request_id = 'testString' x_correlation_id = 'testString' account_id = 'testString' @@ -127,16 +125,13 @@ def test_search_all_params(self): sort = ['testString'] is_deleted = 'false' is_reclaimed = 'false' - is_public = 'false' impersonate_user = 'testString' can_tag = 'false' is_project_resource = 'false' # Invoke method response = _service.search( - query=query, - fields=fields, - search_cursor=search_cursor, + body, x_request_id=x_request_id, x_correlation_id=x_correlation_id, account_id=account_id, @@ -145,7 +140,6 @@ def test_search_all_params(self): sort=sort, is_deleted=is_deleted, is_reclaimed=is_reclaimed, - is_public=is_public, impersonate_user=impersonate_user, can_tag=can_tag, is_project_resource=is_project_resource, @@ -164,15 +158,12 @@ def test_search_all_params(self): assert 'sort={}'.format(','.join(sort)) in query_string assert 'is_deleted={}'.format(is_deleted) in query_string assert 'is_reclaimed={}'.format(is_reclaimed) in query_string - assert 'is_public={}'.format(is_public) in query_string assert 'impersonate_user={}'.format(impersonate_user) in query_string assert 'can_tag={}'.format(can_tag) in query_string assert 'is_project_resource={}'.format(is_project_resource) in query_string # Validate body params req_body = json.loads(str(responses.calls[0].request.body, 'utf-8')) - assert req_body['query'] == 'testString' - assert req_body['fields'] == ['testString'] - assert req_body['search_cursor'] == 'testString' + assert req_body == body def test_search_all_params_with_retries(self): # Enable retries and run test_search_all_params. @@ -199,16 +190,17 @@ def test_search_required_params(self): status=200, ) + # Construct a dict representation of a SearchRequestFirstCall model + search_request_model = {} + search_request_model['query'] = 'testString' + search_request_model['fields'] = ['testString'] + # Set up parameter values - query = 'testString' - fields = ['testString'] - search_cursor = 'testString' + body = search_request_model # Invoke method response = _service.search( - query=query, - fields=fields, - search_cursor=search_cursor, + body, headers={}, ) @@ -217,9 +209,7 @@ def test_search_required_params(self): assert response.status_code == 200 # Validate body params req_body = json.loads(str(responses.calls[0].request.body, 'utf-8')) - assert req_body['query'] == 'testString' - assert req_body['fields'] == ['testString'] - assert req_body['search_cursor'] == 'testString' + assert req_body == body def test_search_required_params_with_retries(self): # Enable retries and run test_search_required_params. @@ -230,6 +220,48 @@ def test_search_required_params_with_retries(self): _service.disable_retries() self.test_search_required_params() + @responses.activate + def test_search_value_error(self): + """ + test_search_value_error() + """ + # Set up mock + url = preprocess_url('/v3/resources/search') + mock_response = '{"search_cursor": "search_cursor", "limit": 5, "items": [{"crn": "crn"}]}' + responses.add( + responses.POST, + url, + body=mock_response, + content_type='application/json', + status=200, + ) + + # Construct a dict representation of a SearchRequestFirstCall model + search_request_model = {} + search_request_model['query'] = 'testString' + search_request_model['fields'] = ['testString'] + + # Set up parameter values + body = search_request_model + + # Pass in all but one required param and check for a ValueError + req_param_dict = { + "body": body, + } + for param in req_param_dict.keys(): + req_copy = {key: val if key is not param else None for (key, val) in req_param_dict.items()} + with pytest.raises(ValueError): + _service.search(**req_copy) + + def test_search_value_error_with_retries(self): + # Enable retries and run test_search_value_error. + _service.enable_retries() + self.test_search_value_error() + + # Disable retries and run test_search_value_error. + _service.disable_retries() + self.test_search_value_error() + # endregion ############################################################################## @@ -281,7 +313,7 @@ def test_result_item_serialization(self): expected_dict = {'foo': 'testString'} result_item_model.set_properties(expected_dict) actual_dict = result_item_model.get_properties() - assert actual_dict == expected_dict + assert actual_dict.keys() == expected_dict.keys() class TestModel_ScanResult: @@ -322,6 +354,69 @@ def test_scan_result_serialization(self): assert scan_result_model_json2 == scan_result_model_json +class TestModel_SearchRequestFirstCall: + """ + Test Class for SearchRequestFirstCall + """ + + def test_search_request_first_call_serialization(self): + """ + Test serialization/deserialization for SearchRequestFirstCall + """ + + # Construct a json representation of a SearchRequestFirstCall model + search_request_first_call_model_json = {} + search_request_first_call_model_json['query'] = 'testString' + search_request_first_call_model_json['fields'] = ['testString'] + + # Construct a model instance of SearchRequestFirstCall by calling from_dict on the json representation + search_request_first_call_model = SearchRequestFirstCall.from_dict(search_request_first_call_model_json) + assert search_request_first_call_model != False + + # Construct a model instance of SearchRequestFirstCall by calling from_dict on the json representation + search_request_first_call_model_dict = SearchRequestFirstCall.from_dict(search_request_first_call_model_json).__dict__ + search_request_first_call_model2 = SearchRequestFirstCall(**search_request_first_call_model_dict) + + # Verify the model instances are equivalent + assert search_request_first_call_model == search_request_first_call_model2 + + # Convert model instance back to dict and verify no loss of data + search_request_first_call_model_json2 = search_request_first_call_model.to_dict() + assert search_request_first_call_model_json2 == search_request_first_call_model_json + + +class TestModel_SearchRequestNextCall: + """ + Test Class for SearchRequestNextCall + """ + + def test_search_request_next_call_serialization(self): + """ + Test serialization/deserialization for SearchRequestNextCall + """ + + # Construct a json representation of a SearchRequestNextCall model + search_request_next_call_model_json = {} + search_request_next_call_model_json['search_cursor'] = 'testString' + search_request_next_call_model_json['query'] = 'testString' + search_request_next_call_model_json['fields'] = ['testString'] + + # Construct a model instance of SearchRequestNextCall by calling from_dict on the json representation + search_request_next_call_model = SearchRequestNextCall.from_dict(search_request_next_call_model_json) + assert search_request_next_call_model != False + + # Construct a model instance of SearchRequestNextCall by calling from_dict on the json representation + search_request_next_call_model_dict = SearchRequestNextCall.from_dict(search_request_next_call_model_json).__dict__ + search_request_next_call_model2 = SearchRequestNextCall(**search_request_next_call_model_dict) + + # Verify the model instances are equivalent + assert search_request_next_call_model == search_request_next_call_model2 + + # Convert model instance back to dict and verify no loss of data + search_request_next_call_model_json2 = search_request_next_call_model.to_dict() + assert search_request_next_call_model_json2 == search_request_next_call_model_json + + # endregion ############################################################################## # End of Model Tests