labd / commercetools-python-sdk

Commercetools Python SDK
https://commercetools-python-sdk.readthedocs.io/en/latest/
MIT License
17 stars 16 forks source link

fix: mocking errors for /product-projections/search #146

Open pshiu opened 9 months ago

pshiu commented 9 months ago

Hi! Don't know if this project accepts PRs, but in case it does:

Description

On tests of commercetools-python-sdk SDK calls to Product Projection Search, we see error:

marshmallow.exceptions.ValidationError: {'filter': ['Unknown field.'], 'markmatchingvariants': ['Unknown field.']}

In the first 2 commits, this PR attempts to correct the schema and urls used in commercetools.testing.product_projections's ProductProjectionsBackend.search() to squash this error.

In the last 2 commits, this PR proposes minor changes to the coverage GitHub Action job to squash bugs from the breaking changes in tooling dependencies, such as one in actions/upload-artifact@3 to actions/upload-artifact@4 of not being able to upload artifacts with the same name.

Additional Information

See commit messages for detailed explanation of changes.

Full error logs that prompted change are below. (But see commit messages first.)

Output of error resolved by a5b5f273d259cfde32e1620c5f32c2b04d2264c8 Note in the full logs below how this mocker matches to to `get_by_id()` instead of `search()`: ``` <...> ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/product_projections.py:85: in get_by_id params = utils.parse_request_params(_ProductProjectionQuerySchema, request) <...> ``` Full logs, starting with where `search()` is called: ``` % pytest <...truncated...> commerce_coordinator/apps/commercetools/clients.py:240: in get_product_variant_by_course_run results = self.base_client.product_projections.search(False, filter=f"variants.sku:\"{cr_id}\"").results ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/services/product_projections.py:294: in search return self._client._get( ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/client.py:36: in _get response = self._http_client.get(self._base_url + endpoint, params=params) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests/sessions.py:602: in get return self.request("GET", url, **kwargs) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests_oauthlib/oauth2_session.py:521: in request return super(OAuth2Session, self).request( ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests/sessions.py:589: in request resp = self.send(prep, **send_kwargs) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests_mock/mocker.py:185: in _fake_send return _original_send(session, request, **kwargs) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests/sessions.py:703: in send r = adapter.send(request, **kwargs) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests_mock/adapter.py:248: in send resp = matcher(request) ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/abstract.py:174: in _matcher response = callback(request, **path_match.groupdict()) ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/product_projections.py:85: in get_by_id params = utils.parse_request_params(_ProductProjectionQuerySchema, request) ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/utils.py:32: in parse_request_params obj = schema().load(params) ../../.virtualenvs/workarea/lib/python3.8/site-packages/marshmallow/schema.py:722: in load return self._do_load( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <_ProductProjectionQuerySchema(many=False)> data = {'filter': 'variants.sku:"course-v1:michiganx+injurypreventionx+1t2021"', 'markmatchingvariants': 'false', 'predicate_var': {}} def _do_load( self, data: ( typing.Mapping[str, typing.Any] | typing.Iterable[typing.Mapping[str, typing.Any]] ), *, many: bool | None = None, partial: bool | types.StrSequenceOrSet | None = None, unknown: str | None = None, postprocess: bool = True, ): """Deserialize `data`, returning the deserialized result. This method is private API. :param data: The data to deserialize. :param many: Whether to deserialize `data` as a collection. If `None`, the value for `self.many` is used. :param partial: Whether to validate required fields. If its value is an iterable, only fields listed in that iterable will be ignored will be allowed missing. If `True`, all fields will be allowed missing. If `None`, the value for `self.partial` is used. :param unknown: Whether to exclude, include, or raise an error for unknown fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. If `None`, the value for `self.unknown` is used. :param postprocess: Whether to run post_load methods.. :return: Deserialized data """ error_store = ErrorStore() errors = {} # type: dict[str, list[str]] many = self.many if many is None else bool(many) unknown = ( self.unknown if unknown is None else validate_unknown_parameter_value(unknown) ) if partial is None: partial = self.partial # Run preprocessors if self._has_processors(PRE_LOAD): try: processed_data = self._invoke_load_processors( PRE_LOAD, data, many=many, original_data=data, partial=partial ) except ValidationError as err: errors = err.normalized_messages() result = None # type: list | dict | None else: processed_data = data if not errors: # Deserialize data result = self._deserialize( processed_data, error_store=error_store, many=many, partial=partial, unknown=unknown, ) # Run field-level validation self._invoke_field_validators( error_store=error_store, data=result, many=many ) # Run schema-level validation if self._has_processors(VALIDATES_SCHEMA): field_errors = bool(error_store.errors) self._invoke_schema_validators( error_store=error_store, pass_many=True, data=result, original_data=data, many=many, partial=partial, field_errors=field_errors, ) self._invoke_schema_validators( error_store=error_store, pass_many=False, data=result, original_data=data, many=many, partial=partial, field_errors=field_errors, ) errors = error_store.errors # Run post processors if not errors and postprocess and self._has_processors(POST_LOAD): try: result = self._invoke_load_processors( POST_LOAD, result, many=many, original_data=data, partial=partial, ) except ValidationError as err: errors = err.normalized_messages() if errors: exc = ValidationError(errors, data=data, valid_data=result) self.handle_error(exc, data, many=many, partial=partial) > raise exc E marshmallow.exceptions.ValidationError: {'filter': ['Unknown field.'], 'markmatchingvariants': ['Unknown field.']} ../../.virtualenvs/workarea/lib/python3.8/site-packages/marshmallow/schema.py:909: ValidationError ```
Output of error resolved by 59c762312d71732d5b8de5e67767c6d056ab0e49 Note in full logs below that now call is to `search()`: ``` <...> ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/product_projections.py:57: in search params = utils.parse_request_params(_ProductProjectionQuerySchema, request) <...> ``` but `_ProductProjectionQuerySchema` is used instead of `_ProductProjectionSearchSchema`: ``` <...> ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/product_projections.py:57: in search params = utils.parse_request_params(_ProductProjectionQuerySchema, request) <...> ``` Full logs: ``` % pytest <...truncated...> commerce_coordinator/apps/commercetools/clients.py:240: in get_product_variant_by_course_run results = self.base_client.product_projections.search(False, filter=f"variants.sku:\"{cr_id}\"").results ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/services/product_projections.py:294: in search return self._client._get( ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/client.py:36: in _get response = self._http_client.get(self._base_url + endpoint, params=params) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests/sessions.py:602: in get return self.request("GET", url, **kwargs) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests_oauthlib/oauth2_session.py:521: in request return super(OAuth2Session, self).request( ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests/sessions.py:589: in request resp = self.send(prep, **send_kwargs) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests_mock/mocker.py:185: in _fake_send return _original_send(session, request, **kwargs) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests/sessions.py:703: in send r = adapter.send(request, **kwargs) ../../.virtualenvs/workarea/lib/python3.8/site-packages/requests_mock/adapter.py:248: in send resp = matcher(request) ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/abstract.py:174: in _matcher response = callback(request, **path_match.groupdict()) ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/product_projections.py:57: in search params = utils.parse_request_params(_ProductProjectionQuerySchema, request) ../../.virtualenvs/workarea/lib/python3.8/site-packages/commercetools/testing/utils.py:32: in parse_request_params obj = schema().load(params) ../../.virtualenvs/workarea/lib/python3.8/site-packages/marshmallow/schema.py:722: in load return self._do_load( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <_ProductProjectionQuerySchema(many=False)> data = {'filter': 'variants.sku:"course-v1:michiganx+injurypreventionx+1t2021"', 'markmatchingvariants': 'false', 'predicate_var': {}} def _do_load( self, data: ( typing.Mapping[str, typing.Any] | typing.Iterable[typing.Mapping[str, typing.Any]] ), *, many: bool | None = None, partial: bool | types.StrSequenceOrSet | None = None, unknown: str | None = None, postprocess: bool = True, ): """Deserialize `data`, returning the deserialized result. This method is private API. :param data: The data to deserialize. :param many: Whether to deserialize `data` as a collection. If `None`, the value for `self.many` is used. :param partial: Whether to validate required fields. If its value is an iterable, only fields listed in that iterable will be ignored will be allowed missing. If `True`, all fields will be allowed missing. If `None`, the value for `self.partial` is used. :param unknown: Whether to exclude, include, or raise an error for unknown fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. If `None`, the value for `self.unknown` is used. :param postprocess: Whether to run post_load methods.. :return: Deserialized data """ error_store = ErrorStore() errors = {} # type: dict[str, list[str]] many = self.many if many is None else bool(many) unknown = ( self.unknown if unknown is None else validate_unknown_parameter_value(unknown) ) if partial is None: partial = self.partial # Run preprocessors if self._has_processors(PRE_LOAD): try: processed_data = self._invoke_load_processors( PRE_LOAD, data, many=many, original_data=data, partial=partial ) except ValidationError as err: errors = err.normalized_messages() result = None # type: list | dict | None else: processed_data = data if not errors: # Deserialize data result = self._deserialize( processed_data, error_store=error_store, many=many, partial=partial, unknown=unknown, ) # Run field-level validation self._invoke_field_validators( error_store=error_store, data=result, many=many ) # Run schema-level validation if self._has_processors(VALIDATES_SCHEMA): field_errors = bool(error_store.errors) self._invoke_schema_validators( error_store=error_store, pass_many=True, data=result, original_data=data, many=many, partial=partial, field_errors=field_errors, ) self._invoke_schema_validators( error_store=error_store, pass_many=False, data=result, original_data=data, many=many, partial=partial, field_errors=field_errors, ) errors = error_store.errors # Run post processors if not errors and postprocess and self._has_processors(POST_LOAD): try: result = self._invoke_load_processors( POST_LOAD, result, many=many, original_data=data, partial=partial, ) except ValidationError as err: errors = err.normalized_messages() if errors: exc = ValidationError(errors, data=data, valid_data=result) self.handle_error(exc, data, many=many, partial=partial) > raise exc E marshmallow.exceptions.ValidationError: {'filter': ['Unknown field.'], 'markmatchingvariants': ['Unknown field.']} ../../.virtualenvs/workarea/lib/python3.8/site-packages/marshmallow/schema.py:909: ValidationError ```

Testing Information

We tested these changes by verifying that the test errors in https://github.com/edx/commerce-coordinator/pull/135 (see this comment) turned green on our locals when manually loading this branch of the SDK in our code.

We verified format/tests/coverage jobs ran successfully in https://github.com/edx/commercetools-python-sdk/pull/1.