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 logging 15from functools import partial 16 17from .action import ServiceAction 18from .action import WaiterAction 19from .base import ResourceMeta, ServiceResource 20from .collection import CollectionFactory 21from .model import ResourceModel 22from .response import build_identifiers, ResourceHandler 23from ..exceptions import ResourceLoadException 24from ..docs import docstring 25 26 27logger = logging.getLogger(__name__) 28 29 30class ResourceFactory(object): 31 """ 32 A factory to create new :py:class:`~boto3.resources.base.ServiceResource` 33 classes from a :py:class:`~boto3.resources.model.ResourceModel`. There are 34 two types of lookups that can be done: one on the service itself (e.g. an 35 SQS resource) and another on models contained within the service (e.g. an 36 SQS Queue resource). 37 """ 38 def __init__(self, emitter): 39 self._collection_factory = CollectionFactory() 40 self._emitter = emitter 41 42 def load_from_definition(self, resource_name, 43 single_resource_json_definition, service_context): 44 """ 45 Loads a resource from a model, creating a new 46 :py:class:`~boto3.resources.base.ServiceResource` subclass 47 with the correct properties and methods, named based on the service 48 and resource name, e.g. EC2.Instance. 49 50 :type resource_name: string 51 :param resource_name: Name of the resource to look up. For services, 52 this should match the ``service_name``. 53 54 :type single_resource_json_definition: dict 55 :param single_resource_json_definition: 56 The loaded json of a single service resource or resource 57 definition. 58 59 :type service_context: :py:class:`~boto3.utils.ServiceContext` 60 :param service_context: Context about the AWS service 61 62 :rtype: Subclass of :py:class:`~boto3.resources.base.ServiceResource` 63 :return: The service or resource class. 64 """ 65 logger.debug('Loading %s:%s', service_context.service_name, 66 resource_name) 67 68 # Using the loaded JSON create a ResourceModel object. 69 resource_model = ResourceModel( 70 resource_name, single_resource_json_definition, 71 service_context.resource_json_definitions 72 ) 73 74 # Do some renaming of the shape if there was a naming collision 75 # that needed to be accounted for. 76 shape = None 77 if resource_model.shape: 78 shape = service_context.service_model.shape_for( 79 resource_model.shape) 80 resource_model.load_rename_map(shape) 81 82 # Set some basic info 83 meta = ResourceMeta( 84 service_context.service_name, resource_model=resource_model) 85 attrs = { 86 'meta': meta, 87 } 88 89 # Create and load all of attributes of the resource class based 90 # on the models. 91 92 # Identifiers 93 self._load_identifiers( 94 attrs=attrs, meta=meta, resource_name=resource_name, 95 resource_model=resource_model 96 ) 97 98 # Load/Reload actions 99 self._load_actions( 100 attrs=attrs, resource_name=resource_name, 101 resource_model=resource_model, service_context=service_context 102 ) 103 104 # Attributes that get auto-loaded 105 self._load_attributes( 106 attrs=attrs, meta=meta, resource_name=resource_name, 107 resource_model=resource_model, 108 service_context=service_context) 109 110 # Collections and their corresponding methods 111 self._load_collections( 112 attrs=attrs, resource_model=resource_model, 113 service_context=service_context) 114 115 # References and Subresources 116 self._load_has_relations( 117 attrs=attrs, resource_name=resource_name, 118 resource_model=resource_model, service_context=service_context 119 ) 120 121 # Waiter resource actions 122 self._load_waiters( 123 attrs=attrs, resource_name=resource_name, 124 resource_model=resource_model, service_context=service_context 125 ) 126 127 # Create the name based on the requested service and resource 128 cls_name = resource_name 129 if service_context.service_name == resource_name: 130 cls_name = 'ServiceResource' 131 cls_name = service_context.service_name + '.' + cls_name 132 133 base_classes = [ServiceResource] 134 if self._emitter is not None: 135 self._emitter.emit( 136 'creating-resource-class.%s' % cls_name, 137 class_attributes=attrs, base_classes=base_classes, 138 service_context=service_context) 139 return type(str(cls_name), tuple(base_classes), attrs) 140 141 def _load_identifiers(self, attrs, meta, resource_model, resource_name): 142 """ 143 Populate required identifiers. These are arguments without which 144 the resource cannot be used. Identifiers become arguments for 145 operations on the resource. 146 """ 147 for identifier in resource_model.identifiers: 148 meta.identifiers.append(identifier.name) 149 attrs[identifier.name] = self._create_identifier( 150 identifier, resource_name) 151 152 def _load_actions(self, attrs, resource_name, resource_model, 153 service_context): 154 """ 155 Actions on the resource become methods, with the ``load`` method 156 being a special case which sets internal data for attributes, and 157 ``reload`` is an alias for ``load``. 158 """ 159 if resource_model.load: 160 attrs['load'] = self._create_action( 161 action_model=resource_model.load, resource_name=resource_name, 162 service_context=service_context, is_load=True) 163 attrs['reload'] = attrs['load'] 164 165 for action in resource_model.actions: 166 attrs[action.name] = self._create_action( 167 action_model=action, resource_name=resource_name, 168 service_context=service_context) 169 170 def _load_attributes(self, attrs, meta, resource_name, resource_model, 171 service_context): 172 """ 173 Load resource attributes based on the resource shape. The shape 174 name is referenced in the resource JSON, but the shape itself 175 is defined in the Botocore service JSON, hence the need for 176 access to the ``service_model``. 177 """ 178 if not resource_model.shape: 179 return 180 181 shape = service_context.service_model.shape_for( 182 resource_model.shape) 183 184 identifiers = dict( 185 (i.member_name, i) 186 for i in resource_model.identifiers if i.member_name) 187 attributes = resource_model.get_attributes(shape) 188 for name, (orig_name, member) in attributes.items(): 189 if name in identifiers: 190 prop = self._create_identifier_alias( 191 resource_name=resource_name, 192 identifier=identifiers[name], 193 member_model=member, 194 service_context=service_context 195 ) 196 else: 197 prop = self._create_autoload_property( 198 resource_name=resource_name, 199 name=orig_name, snake_cased=name, 200 member_model=member, 201 service_context=service_context 202 ) 203 attrs[name] = prop 204 205 def _load_collections(self, attrs, resource_model, service_context): 206 """ 207 Load resource collections from the model. Each collection becomes 208 a :py:class:`~boto3.resources.collection.CollectionManager` instance 209 on the resource instance, which allows you to iterate and filter 210 through the collection's items. 211 """ 212 for collection_model in resource_model.collections: 213 attrs[collection_model.name] = self._create_collection( 214 resource_name=resource_model.name, 215 collection_model=collection_model, 216 service_context=service_context 217 ) 218 219 def _load_has_relations(self, attrs, resource_name, resource_model, 220 service_context): 221 """ 222 Load related resources, which are defined via a ``has`` 223 relationship but conceptually come in two forms: 224 225 1. A reference, which is a related resource instance and can be 226 ``None``, such as an EC2 instance's ``vpc``. 227 2. A subresource, which is a resource constructor that will always 228 return a resource instance which shares identifiers/data with 229 this resource, such as ``s3.Bucket('name').Object('key')``. 230 """ 231 for reference in resource_model.references: 232 # This is a dangling reference, i.e. we have all 233 # the data we need to create the resource, so 234 # this instance becomes an attribute on the class. 235 attrs[reference.name] = self._create_reference( 236 reference_model=reference, 237 resource_name=resource_name, 238 service_context=service_context 239 ) 240 241 for subresource in resource_model.subresources: 242 # This is a sub-resource class you can create 243 # by passing in an identifier, e.g. s3.Bucket(name). 244 attrs[subresource.name] = self._create_class_partial( 245 subresource_model=subresource, 246 resource_name=resource_name, 247 service_context=service_context 248 ) 249 250 self._create_available_subresources_command( 251 attrs, resource_model.subresources) 252 253 def _create_available_subresources_command(self, attrs, subresources): 254 _subresources = [subresource.name for subresource in subresources] 255 _subresources = sorted(_subresources) 256 257 def get_available_subresources(factory_self): 258 """ 259 Returns a list of all the available sub-resources for this 260 Resource. 261 262 :returns: A list containing the name of each sub-resource for this 263 resource 264 :rtype: list of str 265 """ 266 return _subresources 267 268 attrs['get_available_subresources'] = get_available_subresources 269 270 def _load_waiters(self, attrs, resource_name, resource_model, 271 service_context): 272 """ 273 Load resource waiters from the model. Each waiter allows you to 274 wait until a resource reaches a specific state by polling the state 275 of the resource. 276 """ 277 for waiter in resource_model.waiters: 278 attrs[waiter.name] = self._create_waiter( 279 resource_waiter_model=waiter, 280 resource_name=resource_name, 281 service_context=service_context 282 ) 283 284 def _create_identifier(factory_self, identifier, resource_name): 285 """ 286 Creates a read-only property for identifier attributes. 287 """ 288 def get_identifier(self): 289 # The default value is set to ``None`` instead of 290 # raising an AttributeError because when resources are 291 # instantiated a check is made such that none of the 292 # identifiers have a value ``None``. If any are ``None``, 293 # a more informative user error than a generic AttributeError 294 # is raised. 295 return getattr(self, '_' + identifier.name, None) 296 297 get_identifier.__name__ = str(identifier.name) 298 get_identifier.__doc__ = docstring.IdentifierDocstring( 299 resource_name=resource_name, 300 identifier_model=identifier, 301 include_signature=False 302 ) 303 304 return property(get_identifier) 305 306 def _create_identifier_alias(factory_self, resource_name, identifier, 307 member_model, service_context): 308 """ 309 Creates a read-only property that aliases an identifier. 310 """ 311 def get_identifier(self): 312 return getattr(self, '_' + identifier.name, None) 313 314 get_identifier.__name__ = str(identifier.member_name) 315 get_identifier.__doc__ = docstring.AttributeDocstring( 316 service_name=service_context.service_name, 317 resource_name=resource_name, 318 attr_name=identifier.member_name, 319 event_emitter=factory_self._emitter, 320 attr_model=member_model, 321 include_signature=False 322 ) 323 324 return property(get_identifier) 325 326 def _create_autoload_property(factory_self, resource_name, name, 327 snake_cased, member_model, service_context): 328 """ 329 Creates a new property on the resource to lazy-load its value 330 via the resource's ``load`` method (if it exists). 331 """ 332 # The property loader will check to see if this resource has already 333 # been loaded and return the cached value if possible. If not, then 334 # it first checks to see if it CAN be loaded (raise if not), then 335 # calls the load before returning the value. 336 def property_loader(self): 337 if self.meta.data is None: 338 if hasattr(self, 'load'): 339 self.load() 340 else: 341 raise ResourceLoadException( 342 '{0} has no load method'.format( 343 self.__class__.__name__)) 344 345 return self.meta.data.get(name) 346 347 property_loader.__name__ = str(snake_cased) 348 property_loader.__doc__ = docstring.AttributeDocstring( 349 service_name=service_context.service_name, 350 resource_name=resource_name, 351 attr_name=snake_cased, 352 event_emitter=factory_self._emitter, 353 attr_model=member_model, 354 include_signature=False 355 ) 356 357 return property(property_loader) 358 359 def _create_waiter(factory_self, resource_waiter_model, resource_name, 360 service_context): 361 """ 362 Creates a new wait method for each resource where both a waiter and 363 resource model is defined. 364 """ 365 waiter = WaiterAction(resource_waiter_model, 366 waiter_resource_name=resource_waiter_model.name) 367 368 def do_waiter(self, *args, **kwargs): 369 waiter(self, *args, **kwargs) 370 371 do_waiter.__name__ = str(resource_waiter_model.name) 372 do_waiter.__doc__ = docstring.ResourceWaiterDocstring( 373 resource_name=resource_name, 374 event_emitter=factory_self._emitter, 375 service_model=service_context.service_model, 376 resource_waiter_model=resource_waiter_model, 377 service_waiter_model=service_context.service_waiter_model, 378 include_signature=False 379 ) 380 return do_waiter 381 382 def _create_collection(factory_self, resource_name, collection_model, 383 service_context): 384 """ 385 Creates a new property on the resource to lazy-load a collection. 386 """ 387 cls = factory_self._collection_factory.load_from_definition( 388 resource_name=resource_name, collection_model=collection_model, 389 service_context=service_context, 390 event_emitter=factory_self._emitter) 391 392 def get_collection(self): 393 return cls( 394 collection_model=collection_model, parent=self, 395 factory=factory_self, service_context=service_context) 396 397 get_collection.__name__ = str(collection_model.name) 398 get_collection.__doc__ = docstring.CollectionDocstring( 399 collection_model=collection_model, include_signature=False) 400 return property(get_collection) 401 402 def _create_reference(factory_self, reference_model, resource_name, 403 service_context): 404 """ 405 Creates a new property on the resource to lazy-load a reference. 406 """ 407 # References are essentially an action with no request 408 # or response, so we can re-use the response handlers to 409 # build up resources from identifiers and data members. 410 handler = ResourceHandler( 411 search_path=reference_model.resource.path, factory=factory_self, 412 resource_model=reference_model.resource, 413 service_context=service_context 414 ) 415 416 # Are there any identifiers that need access to data members? 417 # This is important when building the resource below since 418 # it requires the data to be loaded. 419 needs_data = any(i.source == 'data' for i in 420 reference_model.resource.identifiers) 421 422 def get_reference(self): 423 # We need to lazy-evaluate the reference to handle circular 424 # references between resources. We do this by loading the class 425 # when first accessed. 426 # This is using a *response handler* so we need to make sure 427 # our data is loaded (if possible) and pass that data into 428 # the handler as if it were a response. This allows references 429 # to have their data loaded properly. 430 if needs_data and self.meta.data is None and hasattr(self, 'load'): 431 self.load() 432 return handler(self, {}, self.meta.data) 433 434 get_reference.__name__ = str(reference_model.name) 435 get_reference.__doc__ = docstring.ReferenceDocstring( 436 reference_model=reference_model, 437 include_signature=False 438 ) 439 return property(get_reference) 440 441 def _create_class_partial(factory_self, subresource_model, resource_name, 442 service_context): 443 """ 444 Creates a new method which acts as a functools.partial, passing 445 along the instance's low-level `client` to the new resource 446 class' constructor. 447 """ 448 name = subresource_model.resource.type 449 450 def create_resource(self, *args, **kwargs): 451 # We need a new method here because we want access to the 452 # instance's client. 453 positional_args = [] 454 455 # We lazy-load the class to handle circular references. 456 json_def = service_context.resource_json_definitions.get(name, {}) 457 resource_cls = factory_self.load_from_definition( 458 resource_name=name, 459 single_resource_json_definition=json_def, 460 service_context=service_context 461 ) 462 463 # Assumes that identifiers are in order, which lets you do 464 # e.g. ``sqs.Queue('foo').Message('bar')`` to create a new message 465 # linked with the ``foo`` queue and which has a ``bar`` receipt 466 # handle. If we did kwargs here then future positional arguments 467 # would lead to failure. 468 identifiers = subresource_model.resource.identifiers 469 if identifiers is not None: 470 for identifier, value in build_identifiers(identifiers, self): 471 positional_args.append(value) 472 473 return partial(resource_cls, *positional_args, 474 client=self.meta.client)(*args, **kwargs) 475 476 create_resource.__name__ = str(name) 477 create_resource.__doc__ = docstring.SubResourceDocstring( 478 resource_name=resource_name, 479 sub_resource_model=subresource_model, 480 service_model=service_context.service_model, 481 include_signature=False 482 ) 483 return create_resource 484 485 def _create_action(factory_self, action_model, resource_name, 486 service_context, is_load=False): 487 """ 488 Creates a new method which makes a request to the underlying 489 AWS service. 490 """ 491 # Create the action in in this closure but before the ``do_action`` 492 # method below is invoked, which allows instances of the resource 493 # to share the ServiceAction instance. 494 action = ServiceAction( 495 action_model, factory=factory_self, 496 service_context=service_context 497 ) 498 499 # A resource's ``load`` method is special because it sets 500 # values on the resource instead of returning the response. 501 if is_load: 502 # We need a new method here because we want access to the 503 # instance via ``self``. 504 def do_action(self, *args, **kwargs): 505 response = action(self, *args, **kwargs) 506 self.meta.data = response 507 # Create the docstring for the load/reload mehtods. 508 lazy_docstring = docstring.LoadReloadDocstring( 509 action_name=action_model.name, 510 resource_name=resource_name, 511 event_emitter=factory_self._emitter, 512 load_model=action_model, 513 service_model=service_context.service_model, 514 include_signature=False 515 ) 516 else: 517 # We need a new method here because we want access to the 518 # instance via ``self``. 519 def do_action(self, *args, **kwargs): 520 response = action(self, *args, **kwargs) 521 522 if hasattr(self, 'load'): 523 # Clear cached data. It will be reloaded the next 524 # time that an attribute is accessed. 525 # TODO: Make this configurable in the future? 526 self.meta.data = None 527 528 return response 529 lazy_docstring = docstring.ActionDocstring( 530 resource_name=resource_name, 531 event_emitter=factory_self._emitter, 532 action_model=action_model, 533 service_model=service_context.service_model, 534 include_signature=False 535 ) 536 537 do_action.__name__ = str(action_model.name) 538 do_action.__doc__ = lazy_docstring 539 return do_action 540