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( | |
f'Search path hits shape type {shape.type_name} from {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 | |