Spaces:
Paused
Paused
| # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"). You | |
| # may not use this file except in compliance with the License. A copy of | |
| # the License is located at | |
| # | |
| # https://aws.amazon.com/apache2.0/ | |
| # | |
| # or in the "license" file accompanying this file. This file is | |
| # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF | |
| # ANY KIND, either express or implied. See the License for the specific | |
| # language governing permissions and limitations under the License. | |
| import jmespath | |
| from botocore import xform_name | |
| from .params import get_data_member | |
| def all_not_none(iterable): | |
| """ | |
| Return True if all elements of the iterable are not None (or if the | |
| iterable is empty). This is like the built-in ``all``, except checks | |
| against None, so 0 and False are allowable values. | |
| """ | |
| for element in iterable: | |
| if element is None: | |
| return False | |
| return True | |
| def build_identifiers(identifiers, parent, params=None, raw_response=None): | |
| """ | |
| Builds a mapping of identifier names to values based on the | |
| identifier source location, type, and target. Identifier | |
| values may be scalars or lists depending on the source type | |
| and location. | |
| :type identifiers: list | |
| :param identifiers: List of :py:class:`~boto3.resources.model.Parameter` | |
| definitions | |
| :type parent: ServiceResource | |
| :param parent: The resource instance to which this action is attached. | |
| :type params: dict | |
| :param params: Request parameters sent to the service. | |
| :type raw_response: dict | |
| :param raw_response: Low-level operation response. | |
| :rtype: list | |
| :return: An ordered list of ``(name, value)`` identifier tuples. | |
| """ | |
| results = [] | |
| for identifier in identifiers: | |
| source = identifier.source | |
| target = identifier.target | |
| if source == 'response': | |
| value = jmespath.search(identifier.path, raw_response) | |
| elif source == 'requestParameter': | |
| value = jmespath.search(identifier.path, params) | |
| elif source == 'identifier': | |
| value = getattr(parent, xform_name(identifier.name)) | |
| elif source == 'data': | |
| # If this is a data member then it may incur a load | |
| # action before returning the value. | |
| value = get_data_member(parent, identifier.path) | |
| elif source == 'input': | |
| # This value is set by the user, so ignore it here | |
| continue | |
| else: | |
| raise NotImplementedError(f'Unsupported source type: {source}') | |
| results.append((xform_name(target), value)) | |
| return results | |
| def build_empty_response(search_path, operation_name, service_model): | |
| """ | |
| Creates an appropriate empty response for the type that is expected, | |
| based on the service model's shape type. For example, a value that | |
| is normally a list would then return an empty list. A structure would | |
| return an empty dict, and a number would return None. | |
| :type search_path: string | |
| :param search_path: JMESPath expression to search in the response | |
| :type operation_name: string | |
| :param operation_name: Name of the underlying service operation. | |
| :type service_model: :ref:`botocore.model.ServiceModel` | |
| :param service_model: The Botocore service model | |
| :rtype: dict, list, or None | |
| :return: An appropriate empty value | |
| """ | |
| response = None | |
| operation_model = service_model.operation_model(operation_name) | |
| shape = operation_model.output_shape | |
| if search_path: | |
| # Walk the search path and find the final shape. For example, given | |
| # a path of ``foo.bar[0].baz``, we first find the shape for ``foo``, | |
| # then the shape for ``bar`` (ignoring the indexing), and finally | |
| # the shape for ``baz``. | |
| for item in search_path.split('.'): | |
| item = item.strip('[0123456789]$') | |
| if shape.type_name == 'structure': | |
| shape = shape.members[item] | |
| elif shape.type_name == 'list': | |
| shape = shape.member | |
| else: | |
| raise NotImplementedError( | |
| 'Search path hits shape type {} from {}'.format( | |
| shape.type_name, item | |
| ) | |
| ) | |
| # Anything not handled here is set to None | |
| if shape.type_name == 'structure': | |
| response = {} | |
| elif shape.type_name == 'list': | |
| response = [] | |
| elif shape.type_name == 'map': | |
| response = {} | |
| return response | |
| class RawHandler: | |
| """ | |
| A raw action response handler. This passed through the response | |
| dictionary, optionally after performing a JMESPath search if one | |
| has been defined for the action. | |
| :type search_path: string | |
| :param search_path: JMESPath expression to search in the response | |
| :rtype: dict | |
| :return: Service response | |
| """ | |
| def __init__(self, search_path): | |
| self.search_path = search_path | |
| def __call__(self, parent, params, response): | |
| """ | |
| :type parent: ServiceResource | |
| :param parent: The resource instance to which this action is attached. | |
| :type params: dict | |
| :param params: Request parameters sent to the service. | |
| :type response: dict | |
| :param response: Low-level operation response. | |
| """ | |
| # TODO: Remove the '$' check after JMESPath supports it | |
| if self.search_path and self.search_path != '$': | |
| response = jmespath.search(self.search_path, response) | |
| return response | |
| class ResourceHandler: | |
| """ | |
| Creates a new resource or list of new resources from the low-level | |
| response based on the given response resource definition. | |
| :type search_path: string | |
| :param search_path: JMESPath expression to search in the response | |
| :type factory: ResourceFactory | |
| :param factory: The factory that created the resource class to which | |
| this action is attached. | |
| :type resource_model: :py:class:`~boto3.resources.model.ResponseResource` | |
| :param resource_model: Response resource model. | |
| :type service_context: :py:class:`~boto3.utils.ServiceContext` | |
| :param service_context: Context about the AWS service | |
| :type operation_name: string | |
| :param operation_name: Name of the underlying service operation, if it | |
| exists. | |
| :rtype: ServiceResource or list | |
| :return: New resource instance(s). | |
| """ | |
| def __init__( | |
| self, | |
| search_path, | |
| factory, | |
| resource_model, | |
| service_context, | |
| operation_name=None, | |
| ): | |
| self.search_path = search_path | |
| self.factory = factory | |
| self.resource_model = resource_model | |
| self.operation_name = operation_name | |
| self.service_context = service_context | |
| def __call__(self, parent, params, response): | |
| """ | |
| :type parent: ServiceResource | |
| :param parent: The resource instance to which this action is attached. | |
| :type params: dict | |
| :param params: Request parameters sent to the service. | |
| :type response: dict | |
| :param response: Low-level operation response. | |
| """ | |
| resource_name = self.resource_model.type | |
| json_definition = self.service_context.resource_json_definitions.get( | |
| resource_name | |
| ) | |
| # Load the new resource class that will result from this action. | |
| resource_cls = self.factory.load_from_definition( | |
| resource_name=resource_name, | |
| single_resource_json_definition=json_definition, | |
| service_context=self.service_context, | |
| ) | |
| raw_response = response | |
| search_response = None | |
| # Anytime a path is defined, it means the response contains the | |
| # resource's attributes, so resource_data gets set here. It | |
| # eventually ends up in resource.meta.data, which is where | |
| # the attribute properties look for data. | |
| if self.search_path: | |
| search_response = jmespath.search(self.search_path, raw_response) | |
| # First, we parse all the identifiers, then create the individual | |
| # response resources using them. Any identifiers that are lists | |
| # will have one item consumed from the front of the list for each | |
| # resource that is instantiated. Items which are not a list will | |
| # be set as the same value on each new resource instance. | |
| identifiers = dict( | |
| build_identifiers( | |
| self.resource_model.identifiers, parent, params, raw_response | |
| ) | |
| ) | |
| # If any of the identifiers is a list, then the response is plural | |
| plural = [v for v in identifiers.values() if isinstance(v, list)] | |
| if plural: | |
| response = [] | |
| # The number of items in an identifier that is a list will | |
| # determine how many resource instances to create. | |
| for i in range(len(plural[0])): | |
| # Response item data is *only* available if a search path | |
| # was given. This prevents accidentally loading unrelated | |
| # data that may be in the response. | |
| response_item = None | |
| if search_response: | |
| response_item = search_response[i] | |
| response.append( | |
| self.handle_response_item( | |
| resource_cls, parent, identifiers, response_item | |
| ) | |
| ) | |
| elif all_not_none(identifiers.values()): | |
| # All identifiers must always exist, otherwise the resource | |
| # cannot be instantiated. | |
| response = self.handle_response_item( | |
| resource_cls, parent, identifiers, search_response | |
| ) | |
| else: | |
| # The response should be empty, but that may mean an | |
| # empty dict, list, or None based on whether we make | |
| # a remote service call and what shape it is expected | |
| # to return. | |
| response = None | |
| if self.operation_name is not None: | |
| # A remote service call was made, so try and determine | |
| # its shape. | |
| response = build_empty_response( | |
| self.search_path, | |
| self.operation_name, | |
| self.service_context.service_model, | |
| ) | |
| return response | |
| def handle_response_item( | |
| self, resource_cls, parent, identifiers, resource_data | |
| ): | |
| """ | |
| Handles the creation of a single response item by setting | |
| parameters and creating the appropriate resource instance. | |
| :type resource_cls: ServiceResource subclass | |
| :param resource_cls: The resource class to instantiate. | |
| :type parent: ServiceResource | |
| :param parent: The resource instance to which this action is attached. | |
| :type identifiers: dict | |
| :param identifiers: Map of identifier names to value or values. | |
| :type resource_data: dict or None | |
| :param resource_data: Data for resource attributes. | |
| :rtype: ServiceResource | |
| :return: New resource instance. | |
| """ | |
| kwargs = { | |
| 'client': parent.meta.client, | |
| } | |
| for name, value in identifiers.items(): | |
| # If value is a list, then consume the next item | |
| if isinstance(value, list): | |
| value = value.pop(0) | |
| kwargs[name] = value | |
| resource = resource_cls(**kwargs) | |
| if resource_data is not None: | |
| resource.meta.data = resource_data | |
| return resource | |