Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
stub.py430 linesDownload Raw Back to botocore
1# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.2#3# Licensed under the Apache License, Version 2.0 (the "License"). You4# may not use this file except in compliance with the License. A copy of5# the License is located at6#7# http://aws.amazon.com/apache2.0/8#9# or in the "license" file accompanying this file. This file is10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF11# ANY KIND, either express or implied. See the License for the specific12# language governing permissions and limitations under the License.13import copy14from collections import deque15from pprint import pformat16 17from botocore.awsrequest import AWSResponse18from botocore.exceptions import (19    ParamValidationError,20    StubAssertionError,21    StubResponseError,22    UnStubbedResponseError,23)24from botocore.validate import validate_parameters25 26 27class _ANY:28    """29    A helper object that compares equal to everything. Copied from30    unittest.mock31    """32 33    def __eq__(self, other):34        return True35 36    def __ne__(self, other):37        return False38 39    def __repr__(self):40        return '<ANY>'41 42 43ANY = _ANY()44 45 46class Stubber:47    """48    This class will allow you to stub out requests so you don't have to hit49    an endpoint to write tests. Responses are returned first in, first out.50    If operations are called out of order, or are called with no remaining51    queued responses, an error will be raised.52 53    **Example:**54    ::55        import datetime56        import botocore.session57        from botocore.stub import Stubber58 59 60        s3 = botocore.session.get_session().create_client('s3')61        stubber = Stubber(s3)62 63        response = {64            'IsTruncated': False,65            'Name': 'test-bucket',66            'MaxKeys': 1000, 'Prefix': '',67            'Contents': [{68                'Key': 'test.txt',69                'ETag': '"abc123"',70                'StorageClass': 'STANDARD',71                'LastModified': datetime.datetime(2016, 1, 20, 22, 9),72                'Owner': {'ID': 'abc123', 'DisplayName': 'myname'},73                'Size': 1481474            }],75            'EncodingType': 'url',76            'ResponseMetadata': {77                'RequestId': 'abc123',78                'HTTPStatusCode': 200,79                'HostId': 'abc123'80            },81            'Marker': ''82        }83 84        expected_params = {'Bucket': 'test-bucket'}85 86        stubber.add_response('list_objects', response, expected_params)87        stubber.activate()88 89        service_response = s3.list_objects(Bucket='test-bucket')90        assert service_response == response91 92 93    This class can also be called as a context manager, which will handle94    activation / deactivation for you.95 96    **Example:**97    ::98        import datetime99        import botocore.session100        from botocore.stub import Stubber101 102 103        s3 = botocore.session.get_session().create_client('s3')104 105        response = {106            "Owner": {107                "ID": "foo",108                "DisplayName": "bar"109            },110            "Buckets": [{111                "CreationDate": datetime.datetime(2016, 1, 20, 22, 9),112                "Name": "baz"113            }]114        }115 116 117        with Stubber(s3) as stubber:118            stubber.add_response('list_buckets', response, {})119            service_response = s3.list_buckets()120 121        assert service_response == response122 123 124    If you have an input parameter that is a randomly generated value, or you125    otherwise don't care about its value, you can use ``stub.ANY`` to ignore126    it in validation.127 128    **Example:**129    ::130        import datetime131        import botocore.session132        from botocore.stub import Stubber, ANY133 134 135        s3 = botocore.session.get_session().create_client('s3')136        stubber = Stubber(s3)137 138        response = {139            'IsTruncated': False,140            'Name': 'test-bucket',141            'MaxKeys': 1000, 'Prefix': '',142            'Contents': [{143                'Key': 'test.txt',144                'ETag': '"abc123"',145                'StorageClass': 'STANDARD',146                'LastModified': datetime.datetime(2016, 1, 20, 22, 9),147                'Owner': {'ID': 'abc123', 'DisplayName': 'myname'},148                'Size': 14814149            }],150            'EncodingType': 'url',151            'ResponseMetadata': {152                'RequestId': 'abc123',153                'HTTPStatusCode': 200,154                'HostId': 'abc123'155            },156            'Marker': ''157        }158 159        expected_params = {'Bucket': ANY}160        stubber.add_response('list_objects', response, expected_params)161 162        with stubber:163            service_response = s3.list_objects(Bucket='test-bucket')164 165        assert service_response == response166    """167 168    def __init__(self, client):169        """170        :param client: The client to add your stubs to.171        """172        self.client = client173        self._event_id = 'boto_stubber'174        self._expected_params_event_id = 'boto_stubber_expected_params'175        self._queue = deque()176 177    def __enter__(self):178        self.activate()179        return self180 181    def __exit__(self, exception_type, exception_value, traceback):182        self.deactivate()183 184    def activate(self):185        """186        Activates the stubber on the client187        """188        self.client.meta.events.register_first(189            'before-parameter-build.*.*',190            self._assert_expected_params,191            unique_id=self._expected_params_event_id,192        )193        self.client.meta.events.register(194            'before-call.*.*',195            self._get_response_handler,196            unique_id=self._event_id,197        )198 199    def deactivate(self):200        """201        Deactivates the stubber on the client202        """203        self.client.meta.events.unregister(204            'before-parameter-build.*.*',205            self._assert_expected_params,206            unique_id=self._expected_params_event_id,207        )208        self.client.meta.events.unregister(209            'before-call.*.*',210            self._get_response_handler,211            unique_id=self._event_id,212        )213 214    def add_response(self, method, service_response, expected_params=None):215        """216        Adds a service response to the response queue. This will be validated217        against the service model to ensure correctness. It should be noted,218        however, that while missing attributes are often considered correct,219        your code may not function properly if you leave them out. Therefore220        you should always fill in every value you see in a typical response for221        your particular request.222 223        :param method: The name of the client method to stub.224        :type method: str225 226        :param service_response: A dict response stub. Provided parameters will227            be validated against the service model.228        :type service_response: dict229 230        :param expected_params: A dictionary of the expected parameters to231            be called for the provided service response. The parameters match232            the names of keyword arguments passed to that client call. If233            any of the parameters differ a ``StubResponseError`` is thrown.234            You can use stub.ANY to indicate a particular parameter to ignore235            in validation. stub.ANY is only valid for top level params.236        """237        self._add_response(method, service_response, expected_params)238 239    def _add_response(self, method, service_response, expected_params):240        if not hasattr(self.client, method):241            raise ValueError(242                "Client %s does not have method: %s"243                % (self.client.meta.service_model.service_name, method)244            )245 246        # Create a successful http response247        http_response = AWSResponse(None, 200, {}, None)248 249        operation_name = self.client.meta.method_to_api_mapping.get(method)250        self._validate_operation_response(operation_name, service_response)251 252        # Add the service_response to the queue for returning responses253        response = {254            'operation_name': operation_name,255            'response': (http_response, service_response),256            'expected_params': expected_params,257        }258        self._queue.append(response)259 260    def add_client_error(261        self,262        method,263        service_error_code='',264        service_message='',265        http_status_code=400,266        service_error_meta=None,267        expected_params=None,268        response_meta=None,269        modeled_fields=None,270    ):271        """272        Adds a ``ClientError`` to the response queue.273 274        :param method: The name of the service method to return the error on.275        :type method: str276 277        :param service_error_code: The service error code to return,278                                   e.g. ``NoSuchBucket``279        :type service_error_code: str280 281        :param service_message: The service message to return, e.g.282                        'The specified bucket does not exist.'283        :type service_message: str284 285        :param http_status_code: The HTTP status code to return, e.g. 404, etc286        :type http_status_code: int287 288        :param service_error_meta: Additional keys to be added to the289            service Error290        :type service_error_meta: dict291 292        :param expected_params: A dictionary of the expected parameters to293            be called for the provided service response. The parameters match294            the names of keyword arguments passed to that client call. If295            any of the parameters differ a ``StubResponseError`` is thrown.296            You can use stub.ANY to indicate a particular parameter to ignore297            in validation.298 299        :param response_meta: Additional keys to be added to the300            response's ResponseMetadata301        :type response_meta: dict302 303        :param modeled_fields: Additional keys to be added to the response304            based on fields that are modeled for the particular error code.305            These keys will be validated against the particular error shape306            designated by the error code.307        :type modeled_fields: dict308 309        """310        http_response = AWSResponse(None, http_status_code, {}, None)311 312        # We don't look to the model to build this because the caller would313        # need to know the details of what the HTTP body would need to314        # look like.315        parsed_response = {316            'ResponseMetadata': {'HTTPStatusCode': http_status_code},317            'Error': {'Message': service_message, 'Code': service_error_code},318        }319 320        if service_error_meta is not None:321            parsed_response['Error'].update(service_error_meta)322 323        if response_meta is not None:324            parsed_response['ResponseMetadata'].update(response_meta)325 326        if modeled_fields is not None:327            service_model = self.client.meta.service_model328            shape = service_model.shape_for_error_code(service_error_code)329            self._validate_response(shape, modeled_fields)330            parsed_response.update(modeled_fields)331 332        operation_name = self.client.meta.method_to_api_mapping.get(method)333        # Note that we do not allow for expected_params while334        # adding errors into the queue yet.335        response = {336            'operation_name': operation_name,337            'response': (http_response, parsed_response),338            'expected_params': expected_params,339        }340        self._queue.append(response)341 342    def assert_no_pending_responses(self):343        """344        Asserts that all expected calls were made.345        """346        remaining = len(self._queue)347        if remaining != 0:348            raise AssertionError(f"{remaining} responses remaining in queue.")349 350    def _assert_expected_call_order(self, model, params):351        if not self._queue:352            raise UnStubbedResponseError(353                operation_name=model.name,354                reason=(355                    'Unexpected API Call: A call was made but no additional '356                    'calls expected. Either the API Call was not stubbed or '357                    'it was called multiple times.'358                ),359            )360 361        name = self._queue[0]['operation_name']362        if name != model.name:363            raise StubResponseError(364                operation_name=model.name,365                reason=f'Operation mismatch: found response for {name}.',366            )367 368    def _get_response_handler(self, model, params, context, **kwargs):369        self._assert_expected_call_order(model, params)370        # Pop off the entire response once everything has been validated371        return self._queue.popleft()['response']372 373    def _assert_expected_params(self, model, params, context, **kwargs):374        if self._should_not_stub(context):375            return376        self._assert_expected_call_order(model, params)377        expected_params = self._queue[0]['expected_params']378        if expected_params is None:379            return380 381        # Validate the parameters are equal382        for param, value in expected_params.items():383            if param not in params or expected_params[param] != params[param]:384                raise StubAssertionError(385                    operation_name=model.name,386                    reason='Expected parameters:\n%s,\nbut received:\n%s'387                    % (pformat(expected_params), pformat(params)),388                )389 390        # Ensure there are no extra params hanging around391        if sorted(expected_params.keys()) != sorted(params.keys()):392            raise StubAssertionError(393                operation_name=model.name,394                reason='Expected parameters:\n%s,\nbut received:\n%s'395                % (pformat(expected_params), pformat(params)),396            )397 398    def _should_not_stub(self, context):399        # Do not include presign requests when processing stubbed client calls400        # as a presign request will never have an HTTP request sent over the401        # wire for it and therefore not receive a response back.402        if context and context.get('is_presign_request'):403            return True404 405    def _validate_operation_response(self, operation_name, service_response):406        service_model = self.client.meta.service_model407        operation_model = service_model.operation_model(operation_name)408        output_shape = operation_model.output_shape409 410        # Remove ResponseMetadata so that the validator doesn't attempt to411        # perform validation on it.412        response = service_response413        if 'ResponseMetadata' in response:414            response = copy.copy(service_response)415            del response['ResponseMetadata']416 417        self._validate_response(output_shape, response)418 419    def _validate_response(self, shape, response):420        if shape is not None:421            validate_parameters(response, shape)422        elif response:423            # If the output shape is None, that means the response should be424            # empty apart from ResponseMetadata425            raise ParamValidationError(426                report=(427                    "Service response should only contain ResponseMetadata."428                )429            )430 
codekingpro/portable-devtools · Team Ai