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# http://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( 72 'Unsupported source type: {0}'.format(source)) 73 74 results.append((xform_name(target), value)) 75 76 return results 77 78 79def build_empty_response(search_path, operation_name, service_model): 80 """ 81 Creates an appropriate empty response for the type that is expected, 82 based on the service model's shape type. For example, a value that 83 is normally a list would then return an empty list. A structure would 84 return an empty dict, and a number would return None. 85 86 :type search_path: string 87 :param search_path: JMESPath expression to search in the response 88 :type operation_name: string 89 :param operation_name: Name of the underlying service operation. 90 :type service_model: :ref:`botocore.model.ServiceModel` 91 :param service_model: The Botocore service model 92 :rtype: dict, list, or None 93 :return: An appropriate empty value 94 """ 95 response = None 96 97 operation_model = service_model.operation_model(operation_name) 98 shape = operation_model.output_shape 99 100 if search_path: 101 # Walk the search path and find the final shape. For example, given 102 # a path of ``foo.bar[0].baz``, we first find the shape for ``foo``, 103 # then the shape for ``bar`` (ignoring the indexing), and finally 104 # the shape for ``baz``. 105 for item in search_path.split('.'): 106 item = item.strip('[0123456789]$') 107 108 if shape.type_name == 'structure': 109 shape = shape.members[item] 110 elif shape.type_name == 'list': 111 shape = shape.member 112 else: 113 raise NotImplementedError( 114 'Search path hits shape type {0} from {1}'.format( 115 shape.type_name, item)) 116 117 # Anything not handled here is set to None 118 if shape.type_name == 'structure': 119 response = {} 120 elif shape.type_name == 'list': 121 response = [] 122 elif shape.type_name == 'map': 123 response = {} 124 125 return response 126 127 128class RawHandler(object): 129 """ 130 A raw action response handler. This passed through the response 131 dictionary, optionally after performing a JMESPath search if one 132 has been defined for the action. 133 134 :type search_path: string 135 :param search_path: JMESPath expression to search in the response 136 :rtype: dict 137 :return: Service response 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(object): 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 def __init__(self, search_path, factory, resource_model, 184 service_context, operation_name=None): 185 self.search_path = search_path 186 self.factory = factory 187 self.resource_model = resource_model 188 self.operation_name = operation_name 189 self.service_context = service_context 190 191 def __call__(self, parent, params, response): 192 """ 193 :type parent: ServiceResource 194 :param parent: The resource instance to which this action is attached. 195 :type params: dict 196 :param params: Request parameters sent to the service. 197 :type response: dict 198 :param response: Low-level operation response. 199 """ 200 resource_name = self.resource_model.type 201 json_definition = self.service_context.resource_json_definitions.get( 202 resource_name) 203 204 # Load the new resource class that will result from this action. 205 resource_cls = self.factory.load_from_definition( 206 resource_name=resource_name, 207 single_resource_json_definition=json_definition, 208 service_context=self.service_context 209 ) 210 raw_response = response 211 search_response = None 212 213 # Anytime a path is defined, it means the response contains the 214 # resource's attributes, so resource_data gets set here. It 215 # eventually ends up in resource.meta.data, which is where 216 # the attribute properties look for data. 217 if self.search_path: 218 search_response = jmespath.search(self.search_path, raw_response) 219 220 # First, we parse all the identifiers, then create the individual 221 # response resources using them. Any identifiers that are lists 222 # will have one item consumed from the front of the list for each 223 # resource that is instantiated. Items which are not a list will 224 # be set as the same value on each new resource instance. 225 identifiers = dict(build_identifiers( 226 self.resource_model.identifiers, parent, params, 227 raw_response)) 228 229 # If any of the identifiers is a list, then the response is plural 230 plural = [v for v in identifiers.values() if isinstance(v, list)] 231 232 if plural: 233 response = [] 234 235 # The number of items in an identifier that is a list will 236 # determine how many resource instances to create. 237 for i in range(len(plural[0])): 238 # Response item data is *only* available if a search path 239 # was given. This prevents accidentally loading unrelated 240 # data that may be in the response. 241 response_item = None 242 if search_response: 243 response_item = search_response[i] 244 response.append( 245 self.handle_response_item(resource_cls, parent, 246 identifiers, response_item)) 247 elif all_not_none(identifiers.values()): 248 # All identifiers must always exist, otherwise the resource 249 # cannot be instantiated. 250 response = self.handle_response_item( 251 resource_cls, parent, identifiers, search_response) 252 else: 253 # The response should be empty, but that may mean an 254 # empty dict, list, or None based on whether we make 255 # a remote service call and what shape it is expected 256 # to return. 257 response = None 258 if self.operation_name is not None: 259 # A remote service call was made, so try and determine 260 # its shape. 261 response = build_empty_response( 262 self.search_path, self.operation_name, 263 self.service_context.service_model) 264 265 return response 266 267 def handle_response_item(self, resource_cls, parent, identifiers, 268 resource_data): 269 """ 270 Handles the creation of a single response item by setting 271 parameters and creating the appropriate resource instance. 272 273 :type resource_cls: ServiceResource subclass 274 :param resource_cls: The resource class to instantiate. 275 :type parent: ServiceResource 276 :param parent: The resource instance to which this action is attached. 277 :type identifiers: dict 278 :param identifiers: Map of identifier names to value or values. 279 :type resource_data: dict or None 280 :param resource_data: Data for resource attributes. 281 :rtype: ServiceResource 282 :return: New resource instance. 283 """ 284 kwargs = { 285 'client': parent.meta.client, 286 } 287 288 for name, value in identifiers.items(): 289 # If value is a list, then consume the next item 290 if isinstance(value, list): 291 value = value.pop(0) 292 293 kwargs[name] = value 294 295 resource = resource_cls(**kwargs) 296 297 if resource_data is not None: 298 resource.meta.data = resource_data 299 300 return resource 301