From dfa94e133d3aabfecd76e2c3785a92887330fd32 Mon Sep 17 00:00:00 2001 From: Derek Kulinski Date: Wed, 6 Sep 2023 22:09:12 -0700 Subject: [PATCH 1/7] AioStubber that returns AioAWSResponse() --- CHANGES.rst | 4 ++ aiobotocore/__init__.py | 2 +- aiobotocore/stub.py | 107 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 aiobotocore/stub.py diff --git a/CHANGES.rst b/CHANGES.rst index eeab2b7b..39e5a693 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ Changes ------- +2.7.1 (2023-10-19) +^^^^^^^^^^^^^^^^^^ +* added AioStubber that return AioAWSResponse() + 2.7.0 (2023-10-17) ^^^^^^^^^^^^^^^^^^ * add support for Python 3.12 diff --git a/aiobotocore/__init__.py b/aiobotocore/__init__.py index 766ce2d0..e90ba7fa 100644 --- a/aiobotocore/__init__.py +++ b/aiobotocore/__init__.py @@ -1 +1 @@ -__version__ = '2.7.0' +__version__ = '2.7.1' diff --git a/aiobotocore/stub.py b/aiobotocore/stub.py new file mode 100644 index 00000000..e1d050f0 --- /dev/null +++ b/aiobotocore/stub.py @@ -0,0 +1,107 @@ +from .awsrequest import AioAWSResponse +from botocore.stub import Stubber + + +class AioStubber(Stubber): + def _add_response(self, method, service_response, expected_params): + if not hasattr(self.client, method): + raise ValueError( + "Client %s does not have method: %s" + % (self.client.meta.service_model.service_name, method) + ) + + # Create a successful http response + http_response = AioAWSResponse(None, 200, {}, None) + + operation_name = self.client.meta.method_to_api_mapping.get(method) + self._validate_operation_response(operation_name, service_response) + + # Add the service_response to the queue for returning responses + response = { + 'operation_name': operation_name, + 'response': (http_response, service_response), + 'expected_params': expected_params, + } + self._queue.append(response) + + def add_client_error( + self, + method, + service_error_code='', + service_message='', + http_status_code=400, + service_error_meta=None, + expected_params=None, + response_meta=None, + modeled_fields=None, + ): + """ + Adds a ``ClientError`` to the response queue. + + :param method: The name of the service method to return the error on. + :type method: str + + :param service_error_code: The service error code to return, + e.g. ``NoSuchBucket`` + :type service_error_code: str + + :param service_message: The service message to return, e.g. + 'The specified bucket does not exist.' + :type service_message: str + + :param http_status_code: The HTTP status code to return, e.g. 404, etc + :type http_status_code: int + + :param service_error_meta: Additional keys to be added to the + service Error + :type service_error_meta: dict + + :param expected_params: A dictionary of the expected parameters to + be called for the provided service response. The parameters match + the names of keyword arguments passed to that client call. If + any of the parameters differ a ``StubResponseError`` is thrown. + You can use stub.ANY to indicate a particular parameter to ignore + in validation. + + :param response_meta: Additional keys to be added to the + response's ResponseMetadata + :type response_meta: dict + + :param modeled_fields: Additional keys to be added to the response + based on fields that are modeled for the particular error code. + These keys will be validated against the particular error shape + designated by the error code. + :type modeled_fields: dict + + """ + http_response = AioAWSResponse(None, http_status_code, {}, None) + + # We don't look to the model to build this because the caller would + # need to know the details of what the HTTP body would need to + # look like. + parsed_response = { + 'ResponseMetadata': {'HTTPStatusCode': http_status_code}, + 'Error': {'Message': service_message, 'Code': service_error_code}, + } + + if service_error_meta is not None: + parsed_response['Error'].update(service_error_meta) + + if response_meta is not None: + parsed_response['ResponseMetadata'].update(response_meta) + + if modeled_fields is not None: + service_model = self.client.meta.service_model + shape = service_model.shape_for_error_code(service_error_code) + self._validate_response(shape, modeled_fields) + parsed_response.update(modeled_fields) + + operation_name = self.client.meta.method_to_api_mapping.get(method) + # Note that we do not allow for expected_params while + # adding errors into the queue yet. + response = { + 'operation_name': operation_name, + 'response': (http_response, parsed_response), + 'expected_params': expected_params, + } + self._queue.append(response) From 8d242614aa8af57eafc527a43bc1e08684bc9998 Mon Sep 17 00:00:00 2001 From: Derek Kulinski Date: Thu, 7 Sep 2023 16:48:42 -0700 Subject: [PATCH 2/7] hash for Stubber --- tests/test_patches.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_patches.py b/tests/test_patches.py index 67dbf42c..c4cff44b 100644 --- a/tests/test_patches.py +++ b/tests/test_patches.py @@ -2,7 +2,7 @@ import botocore import pytest -from botocore import retryhandler +from botocore import retryhandler, stub from botocore.args import ClientArgsCreator from botocore.awsrequest import AWSResponse from botocore.client import BaseClient, ClientCreator, Config @@ -650,6 +650,9 @@ retryhandler.CRC32Checker._check_response: { '3ee7afd0bb1a3bf53934d77e44f619962c52b0c9' }, + stub.Stubber: { + 'bccf23c3733cc656b909f5130cba80dbc9540b05' + } } From 6e87f8b66f10b16ef3b277736f2f74300a993539 Mon Sep 17 00:00:00 2001 From: Derek Kulinski Date: Mon, 11 Sep 2023 09:58:50 -0700 Subject: [PATCH 3/7] formatting to fix flake8 reported issues --- aiobotocore/stub.py | 207 ++++++++++++++++++++++---------------------- 1 file changed, 104 insertions(+), 103 deletions(-) diff --git a/aiobotocore/stub.py b/aiobotocore/stub.py index e1d050f0..ef7152de 100644 --- a/aiobotocore/stub.py +++ b/aiobotocore/stub.py @@ -1,107 +1,108 @@ -from .awsrequest import AioAWSResponse from botocore.stub import Stubber +from .awsrequest import AioAWSResponse + class AioStubber(Stubber): - def _add_response(self, method, service_response, expected_params): - if not hasattr(self.client, method): - raise ValueError( - "Client %s does not have method: %s" - % (self.client.meta.service_model.service_name, method) - ) - - # Create a successful http response - http_response = AioAWSResponse(None, 200, {}, None) - - operation_name = self.client.meta.method_to_api_mapping.get(method) - self._validate_operation_response(operation_name, service_response) - - # Add the service_response to the queue for returning responses - response = { - 'operation_name': operation_name, - 'response': (http_response, service_response), - 'expected_params': expected_params, - } - self._queue.append(response) - - def add_client_error( - self, - method, - service_error_code='', - service_message='', - http_status_code=400, - service_error_meta=None, - expected_params=None, - response_meta=None, - modeled_fields=None, - ): - """ - Adds a ``ClientError`` to the response queue. - - :param method: The name of the service method to return the error on. - :type method: str - - :param service_error_code: The service error code to return, - e.g. ``NoSuchBucket`` - :type service_error_code: str - - :param service_message: The service message to return, e.g. - 'The specified bucket does not exist.' - :type service_message: str - - :param http_status_code: The HTTP status code to return, e.g. 404, etc - :type http_status_code: int - - :param service_error_meta: Additional keys to be added to the - service Error - :type service_error_meta: dict - - :param expected_params: A dictionary of the expected parameters to - be called for the provided service response. The parameters match - the names of keyword arguments passed to that client call. If - any of the parameters differ a ``StubResponseError`` is thrown. - You can use stub.ANY to indicate a particular parameter to ignore - in validation. - - :param response_meta: Additional keys to be added to the - response's ResponseMetadata - :type response_meta: dict - - :param modeled_fields: Additional keys to be added to the response - based on fields that are modeled for the particular error code. - These keys will be validated against the particular error shape - designated by the error code. - :type modeled_fields: dict - - """ - http_response = AioAWSResponse(None, http_status_code, {}, None) - - # We don't look to the model to build this because the caller would - # need to know the details of what the HTTP body would need to - # look like. - parsed_response = { - 'ResponseMetadata': {'HTTPStatusCode': http_status_code}, - 'Error': {'Message': service_message, 'Code': service_error_code}, - } - - if service_error_meta is not None: - parsed_response['Error'].update(service_error_meta) - - if response_meta is not None: - parsed_response['ResponseMetadata'].update(response_meta) - - if modeled_fields is not None: - service_model = self.client.meta.service_model - shape = service_model.shape_for_error_code(service_error_code) - self._validate_response(shape, modeled_fields) - parsed_response.update(modeled_fields) - - operation_name = self.client.meta.method_to_api_mapping.get(method) - # Note that we do not allow for expected_params while - # adding errors into the queue yet. - response = { - 'operation_name': operation_name, - 'response': (http_response, parsed_response), - 'expected_params': expected_params, - } - self._queue.append(response) + def _add_response(self, method, service_response, expected_params): + if not hasattr(self.client, method): + raise ValueError( + "Client %s does not have method: %s" + % (self.client.meta.service_model.service_name, method) + ) + + # Create a successful http response + http_response = AioAWSResponse(None, 200, {}, None) + + operation_name = self.client.meta.method_to_api_mapping.get(method) + self._validate_operation_response(operation_name, service_response) + + # Add the service_response to the queue for returning responses + response = { + 'operation_name': operation_name, + 'response': (http_response, service_response), + 'expected_params': expected_params, + } + self._queue.append(response) + + def add_client_error( + self, + method, + service_error_code='', + service_message='', + http_status_code=400, + service_error_meta=None, + expected_params=None, + response_meta=None, + modeled_fields=None, + ): + """ + Adds a ``ClientError`` to the response queue. + + :param method: The name of the service method to return the error on. + :type method: str + + :param service_error_code: The service error code to return, + e.g. ``NoSuchBucket`` + :type service_error_code: str + + :param service_message: The service message to return, e.g. + 'The specified bucket does not exist.' + :type service_message: str + + :param http_status_code: The HTTP status code to return, e.g. 404, etc + :type http_status_code: int + + :param service_error_meta: Additional keys to be added to the + service Error + :type service_error_meta: dict + + :param expected_params: A dictionary of the expected parameters to + be called for the provided service response. The parameters match + the names of keyword arguments passed to that client call. If + any of the parameters differ a ``StubResponseError`` is thrown. + You can use stub.ANY to indicate a particular parameter to ignore + in validation. + + :param response_meta: Additional keys to be added to the + response's ResponseMetadata + :type response_meta: dict + + :param modeled_fields: Additional keys to be added to the response + based on fields that are modeled for the particular error code. + These keys will be validated against the particular error shape + designated by the error code. + :type modeled_fields: dict + + """ + http_response = AioAWSResponse(None, http_status_code, {}, None) + + # We don't look to the model to build this because the caller would + # need to know the details of what the HTTP body would need to + # look like. + parsed_response = { + 'ResponseMetadata': {'HTTPStatusCode': http_status_code}, + 'Error': {'Message': service_message, 'Code': service_error_code}, + } + + if service_error_meta is not None: + parsed_response['Error'].update(service_error_meta) + + if response_meta is not None: + parsed_response['ResponseMetadata'].update(response_meta) + + if modeled_fields is not None: + service_model = self.client.meta.service_model + shape = service_model.shape_for_error_code(service_error_code) + self._validate_response(shape, modeled_fields) + parsed_response.update(modeled_fields) + + operation_name = self.client.meta.method_to_api_mapping.get(method) + # Note that we do not allow for expected_params while + # adding errors into the queue yet. + response = { + 'operation_name': operation_name, + 'response': (http_response, parsed_response), + 'expected_params': expected_params, + } + self._queue.append(response) From 193206eabfdf5aef9b1aeaff463c2f5b12bdea66 Mon Sep 17 00:00:00 2001 From: Derek Kulinski Date: Tue, 12 Sep 2023 00:02:13 -0700 Subject: [PATCH 4/7] reformatted tests --- tests/test_patches.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_patches.py b/tests/test_patches.py index c4cff44b..80c348b7 100644 --- a/tests/test_patches.py +++ b/tests/test_patches.py @@ -650,9 +650,7 @@ retryhandler.CRC32Checker._check_response: { '3ee7afd0bb1a3bf53934d77e44f619962c52b0c9' }, - stub.Stubber: { - 'bccf23c3733cc656b909f5130cba80dbc9540b05' - } + stub.Stubber: {'bccf23c3733cc656b909f5130cba80dbc9540b05'}, } From ae38a3ec2de28af1444fa9d2caf73dab833eed8d Mon Sep 17 00:00:00 2001 From: Derek Kulinski Date: Thu, 5 Oct 2023 17:08:10 -0700 Subject: [PATCH 5/7] unit tests --- tests/test_stubber.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/test_stubber.py diff --git a/tests/test_stubber.py b/tests/test_stubber.py new file mode 100644 index 00000000..777176b9 --- /dev/null +++ b/tests/test_stubber.py @@ -0,0 +1,65 @@ +import pytest + +from aiobotocore.awsrequest import AioAWSResponse +from aiobotocore.session import AioSession +from aiobotocore.stub import AioStubber + +from .mock_server import AIOServer + + +@pytest.mark.asyncio +async def test_add_response(): + session = AioSession() + + async with AIOServer() as server, session.create_client( + 's3', + endpoint_url=server.endpoint_url, + aws_secret_access_key='xxx', + aws_access_key_id='xxx', + ) as s3_client: + stubber = AioStubber(s3_client) + operation_name = 'put_object' + service_response = dict( + ETag="6805f2cfc46c0f04559748bb039d69ae", + VersionId="psM2sYY4.o1501dSx8wMvnkOzSBB.V4a", + ) + expected_params = dict() + stubber.add_response(operation_name, service_response, expected_params) + + assert len(stubber._queue) == 1 + assert stubber._queue[0][ + 'operation_name' + ] == s3_client.meta.method_to_api_mapping.get(operation_name) + assert isinstance(stubber._queue[0]['response'][0], AioAWSResponse) + assert stubber._queue[0]['response'][1] == service_response + assert stubber._queue[0]['expected_params'] == expected_params + + +@pytest.mark.asyncio +async def test_add_client_error(): + session = AioSession() + + async with AIOServer() as server, session.create_client( + 's3', + endpoint_url=server.endpoint_url, + aws_secret_access_key='xxx', + aws_access_key_id='xxx', + ) as s3_client: + stubber = AioStubber(s3_client) + operation_name = 'put_object' + service_error_code = 'NoSuchBucket' + service_message = 'The specified bucket does not exist.' + http_status_code = 404 + stubber.add_client_error( + operation_name, + service_error_code, + service_message, + http_status_code, + ) + + assert len(stubber._queue) == 1 + assert stubber._queue[0][ + 'operation_name' + ] == s3_client.meta.method_to_api_mapping.get(operation_name) + assert isinstance(stubber._queue[0]['response'][0], AioAWSResponse) + assert stubber._queue[0]['response'][1] From a2848f653dd7dadcc114421b506a31329afe9dcc Mon Sep 17 00:00:00 2001 From: Derek Kulinski Date: Fri, 6 Oct 2023 16:48:33 -0700 Subject: [PATCH 6/7] 100% coverage; bumped patchlevel number --- aiobotocore/stub.py | 2 +- tests/test_stubber.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/aiobotocore/stub.py b/aiobotocore/stub.py index ef7152de..9423a822 100644 --- a/aiobotocore/stub.py +++ b/aiobotocore/stub.py @@ -9,7 +9,7 @@ def _add_response(self, method, service_response, expected_params): raise ValueError( "Client %s does not have method: %s" % (self.client.meta.service_model.service_name, method) - ) + ) # pragma: no cover # Create a successful http response http_response = AioAWSResponse(None, 200, {}, None) diff --git a/tests/test_stubber.py b/tests/test_stubber.py index 777176b9..b60683db 100644 --- a/tests/test_stubber.py +++ b/tests/test_stubber.py @@ -47,14 +47,21 @@ async def test_add_client_error(): ) as s3_client: stubber = AioStubber(s3_client) operation_name = 'put_object' - service_error_code = 'NoSuchBucket' - service_message = 'The specified bucket does not exist.' - http_status_code = 404 + service_error_code = 'InvalidObjectState' + service_message = 'Object is in invalid state' + http_status_code = 400 + service_error_meta = {"AdditionalInfo": "value"} + response_meta = {"AdditionalResponseInfo": "value"} + modeled_fields = {'StorageClass': 'foo', 'AccessTier': 'bar'} + stubber.add_client_error( operation_name, service_error_code, service_message, http_status_code, + service_error_meta, + response_meta=response_meta, + modeled_fields=modeled_fields, ) assert len(stubber._queue) == 1 From b174eac518518be4725f17d0a90c675dea19b222 Mon Sep 17 00:00:00 2001 From: Derek Kulinski Date: Thu, 19 Oct 2023 16:41:35 -0700 Subject: [PATCH 7/7] adding moto marker to stubber --- tests/test_stubber.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_stubber.py b/tests/test_stubber.py index b60683db..786d8ac6 100644 --- a/tests/test_stubber.py +++ b/tests/test_stubber.py @@ -7,6 +7,7 @@ from .mock_server import AIOServer +@pytest.mark.moto @pytest.mark.asyncio async def test_add_response(): session = AioSession() @@ -35,6 +36,7 @@ async def test_add_response(): assert stubber._queue[0]['expected_params'] == expected_params +@pytest.mark.moto @pytest.mark.asyncio async def test_add_client_error(): session = AioSession()