1# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# https://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13
14import jmespath
15from botocore import xform_name
16
17from .params import get_data_member
18
19
20def all_not_none(iterable):
21 """
22 Return True if all elements of the iterable are not None (or if the
23 iterable is empty). This is like the built-in ``all``, except checks
24 against None, so 0 and False are allowable values.
25 """
26 for element in iterable:
27 if element is None:
28 return False
29 return True
30
31
32def build_identifiers(identifiers, parent, params=None, raw_response=None):
33 """
34 Builds a mapping of identifier names to values based on the
35 identifier source location, type, and target. Identifier
36 values may be scalars or lists depending on the source type
37 and location.
38
39 :type identifiers: list
40 :param identifiers: List of :py:class:`~boto3.resources.model.Parameter`
41 definitions
42 :type parent: ServiceResource
43 :param parent: The resource instance to which this action is attached.
44 :type params: dict
45 :param params: Request parameters sent to the service.
46 :type raw_response: dict
47 :param raw_response: Low-level operation response.
48 :rtype: list
49 :return: An ordered list of ``(name, value)`` identifier tuples.
50 """
51 results = []
52
53 for identifier in identifiers:
54 source = identifier.source
55 target = identifier.target
56
57 if source == 'response':
58 value = jmespath.search(identifier.path, raw_response)
59 elif source == 'requestParameter':
60 value = jmespath.search(identifier.path, params)
61 elif source == 'identifier':
62 value = getattr(parent, xform_name(identifier.name))
63 elif source == 'data':
64 # If this is a data member then it may incur a load
65 # action before returning the value.
66 value = get_data_member(parent, identifier.path)
67 elif source == 'input':
68 # This value is set by the user, so ignore it here
69 continue
70 else:
71 raise NotImplementedError(f'Unsupported source type: {source}')
72
73 results.append((xform_name(target), value))
74
75 return results
76
77
78def build_empty_response(search_path, operation_name, service_model):
79 """
80 Creates an appropriate empty response for the type that is expected,
81 based on the service model's shape type. For example, a value that
82 is normally a list would then return an empty list. A structure would
83 return an empty dict, and a number would return None.
84
85 :type search_path: string
86 :param search_path: JMESPath expression to search in the response
87 :type operation_name: string
88 :param operation_name: Name of the underlying service operation.
89 :type service_model: :ref:`botocore.model.ServiceModel`
90 :param service_model: The Botocore service model
91 :rtype: dict, list, or None
92 :return: An appropriate empty value
93 """
94 response = None
95
96 operation_model = service_model.operation_model(operation_name)
97 shape = operation_model.output_shape
98
99 if search_path:
100 # Walk the search path and find the final shape. For example, given
101 # a path of ``foo.bar[0].baz``, we first find the shape for ``foo``,
102 # then the shape for ``bar`` (ignoring the indexing), and finally
103 # the shape for ``baz``.
104 for item in search_path.split('.'):
105 item = item.strip('[0123456789]$')
106
107 if shape.type_name == 'structure':
108 shape = shape.members[item]
109 elif shape.type_name == 'list':
110 shape = shape.member
111 else:
112 raise NotImplementedError(
113 f'Search path hits shape type {shape.type_name} from {item}'
114 )
115
116 # Anything not handled here is set to None
117 if shape.type_name == 'structure':
118 response = {}
119 elif shape.type_name == 'list':
120 response = []
121 elif shape.type_name == 'map':
122 response = {}
123
124 return response
125
126
127class RawHandler:
128 """
129 A raw action response handler. This passed through the response
130 dictionary, optionally after performing a JMESPath search if one
131 has been defined for the action.
132
133 :type search_path: string
134 :param search_path: JMESPath expression to search in the response
135 :rtype: dict
136 :return: Service response
137 """
138
139 def __init__(self, search_path):
140 self.search_path = search_path
141
142 def __call__(self, parent, params, response):
143 """
144 :type parent: ServiceResource
145 :param parent: The resource instance to which this action is attached.
146 :type params: dict
147 :param params: Request parameters sent to the service.
148 :type response: dict
149 :param response: Low-level operation response.
150 """
151 # TODO: Remove the '$' check after JMESPath supports it
152 if self.search_path and self.search_path != '$':
153 response = jmespath.search(self.search_path, response)
154
155 return response
156
157
158class ResourceHandler:
159 """
160 Creates a new resource or list of new resources from the low-level
161 response based on the given response resource definition.
162
163 :type search_path: string
164 :param search_path: JMESPath expression to search in the response
165
166 :type factory: ResourceFactory
167 :param factory: The factory that created the resource class to which
168 this action is attached.
169
170 :type resource_model: :py:class:`~boto3.resources.model.ResponseResource`
171 :param resource_model: Response resource model.
172
173 :type service_context: :py:class:`~boto3.utils.ServiceContext`
174 :param service_context: Context about the AWS service
175
176 :type operation_name: string
177 :param operation_name: Name of the underlying service operation, if it
178 exists.
179
180 :rtype: ServiceResource or list
181 :return: New resource instance(s).
182 """
183
184 def __init__(
185 self,
186 search_path,
187 factory,
188 resource_model,
189 service_context,
190 operation_name=None,
191 ):
192 self.search_path = search_path
193 self.factory = factory
194 self.resource_model = resource_model
195 self.operation_name = operation_name
196 self.service_context = service_context
197
198 def __call__(self, parent, params, response):
199 """
200 :type parent: ServiceResource
201 :param parent: The resource instance to which this action is attached.
202 :type params: dict
203 :param params: Request parameters sent to the service.
204 :type response: dict
205 :param response: Low-level operation response.
206 """
207 resource_name = self.resource_model.type
208 json_definition = self.service_context.resource_json_definitions.get(
209 resource_name
210 )
211
212 # Load the new resource class that will result from this action.
213 resource_cls = self.factory.load_from_definition(
214 resource_name=resource_name,
215 single_resource_json_definition=json_definition,
216 service_context=self.service_context,
217 )
218 raw_response = response
219 search_response = None
220
221 # Anytime a path is defined, it means the response contains the
222 # resource's attributes, so resource_data gets set here. It
223 # eventually ends up in resource.meta.data, which is where
224 # the attribute properties look for data.
225 if self.search_path:
226 search_response = jmespath.search(self.search_path, raw_response)
227
228 # First, we parse all the identifiers, then create the individual
229 # response resources using them. Any identifiers that are lists
230 # will have one item consumed from the front of the list for each
231 # resource that is instantiated. Items which are not a list will
232 # be set as the same value on each new resource instance.
233 identifiers = dict(
234 build_identifiers(
235 self.resource_model.identifiers, parent, params, raw_response
236 )
237 )
238
239 # If any of the identifiers is a list, then the response is plural
240 plural = [v for v in identifiers.values() if isinstance(v, list)]
241
242 if plural:
243 response = []
244
245 # The number of items in an identifier that is a list will
246 # determine how many resource instances to create.
247 for i in range(len(plural[0])):
248 # Response item data is *only* available if a search path
249 # was given. This prevents accidentally loading unrelated
250 # data that may be in the response.
251 response_item = None
252 if search_response:
253 response_item = search_response[i]
254 response.append(
255 self.handle_response_item(
256 resource_cls, parent, identifiers, response_item
257 )
258 )
259 elif all_not_none(identifiers.values()):
260 # All identifiers must always exist, otherwise the resource
261 # cannot be instantiated.
262 response = self.handle_response_item(
263 resource_cls, parent, identifiers, search_response
264 )
265 else:
266 # The response should be empty, but that may mean an
267 # empty dict, list, or None based on whether we make
268 # a remote service call and what shape it is expected
269 # to return.
270 response = None
271 if self.operation_name is not None:
272 # A remote service call was made, so try and determine
273 # its shape.
274 response = build_empty_response(
275 self.search_path,
276 self.operation_name,
277 self.service_context.service_model,
278 )
279
280 return response
281
282 def handle_response_item(
283 self, resource_cls, parent, identifiers, resource_data
284 ):
285 """
286 Handles the creation of a single response item by setting
287 parameters and creating the appropriate resource instance.
288
289 :type resource_cls: ServiceResource subclass
290 :param resource_cls: The resource class to instantiate.
291 :type parent: ServiceResource
292 :param parent: The resource instance to which this action is attached.
293 :type identifiers: dict
294 :param identifiers: Map of identifier names to value or values.
295 :type resource_data: dict or None
296 :param resource_data: Data for resource attributes.
297 :rtype: ServiceResource
298 :return: New resource instance.
299 """
300 kwargs = {
301 'client': parent.meta.client,
302 }
303
304 for name, value in identifiers.items():
305 # If value is a list, then consume the next item
306 if isinstance(value, list):
307 value = value.pop(0)
308
309 kwargs[name] = value
310
311 resource = resource_cls(**kwargs)
312
313 if resource_data is not None:
314 resource.meta.data = resource_data
315
316 return resource