codekingpro/portable-devtools
114k
1# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.2#3# Licensed under the Apache License, Version 2.0 (the "License"). You4# may not use this file except in compliance with the License. A copy of5# the License is located at6#7# https://aws.amazon.com/apache2.0/8#9# or in the "license" file accompanying this file. This file is10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF11# ANY KIND, either express or implied. See the License for the specific12# language governing permissions and limitations under the License.13 14import logging15from functools import partial16 17from ..docs import docstring18from ..exceptions import ResourceLoadException19from .action import ServiceAction, WaiterAction20from .base import ResourceMeta, ServiceResource21from .collection import CollectionFactory22from .model import ResourceModel23from .response import ResourceHandler, build_identifiers24 25logger = logging.getLogger(__name__)26 27 28class ResourceFactory:29 """30 A factory to create new :py:class:`~boto3.resources.base.ServiceResource`31 classes from a :py:class:`~boto3.resources.model.ResourceModel`. There are32 two types of lookups that can be done: one on the service itself (e.g. an33 SQS resource) and another on models contained within the service (e.g. an34 SQS Queue resource).35 """36 37 def __init__(self, emitter):38 self._collection_factory = CollectionFactory()39 self._emitter = emitter40 41 def load_from_definition(42 self, resource_name, single_resource_json_definition, service_context43 ):44 """45 Loads a resource from a model, creating a new46 :py:class:`~boto3.resources.base.ServiceResource` subclass47 with the correct properties and methods, named based on the service48 and resource name, e.g. EC2.Instance.49 50 :type resource_name: string51 :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: dict55 :param single_resource_json_definition:56 The loaded json of a single service resource or resource57 definition.58 59 :type service_context: :py:class:`~boto3.utils.ServiceContext`60 :param service_context: Context about the AWS service61 62 :rtype: Subclass of :py:class:`~boto3.resources.base.ServiceResource`63 :return: The service or resource class.64 """65 logger.debug(66 'Loading %s:%s', service_context.service_name, resource_name67 )68 69 # Using the loaded JSON create a ResourceModel object.70 resource_model = ResourceModel(71 resource_name,72 single_resource_json_definition,73 service_context.resource_json_definitions,74 )75 76 # Do some renaming of the shape if there was a naming collision77 # that needed to be accounted for.78 shape = None79 if resource_model.shape:80 shape = service_context.service_model.shape_for(81 resource_model.shape82 )83 resource_model.load_rename_map(shape)84 85 # Set some basic info86 meta = ResourceMeta(87 service_context.service_name, resource_model=resource_model88 )89 attrs = {90 'meta': meta,91 }92 93 # Create and load all of attributes of the resource class based94 # on the models.95 96 # Identifiers97 self._load_identifiers(98 attrs=attrs,99 meta=meta,100 resource_name=resource_name,101 resource_model=resource_model,102 )103 104 # Load/Reload actions105 self._load_actions(106 attrs=attrs,107 resource_name=resource_name,108 resource_model=resource_model,109 service_context=service_context,110 )111 112 # Attributes that get auto-loaded113 self._load_attributes(114 attrs=attrs,115 meta=meta,116 resource_name=resource_name,117 resource_model=resource_model,118 service_context=service_context,119 )120 121 # Collections and their corresponding methods122 self._load_collections(123 attrs=attrs,124 resource_model=resource_model,125 service_context=service_context,126 )127 128 # References and Subresources129 self._load_has_relations(130 attrs=attrs,131 resource_name=resource_name,132 resource_model=resource_model,133 service_context=service_context,134 )135 136 # Waiter resource actions137 self._load_waiters(138 attrs=attrs,139 resource_name=resource_name,140 resource_model=resource_model,141 service_context=service_context,142 )143 144 # Create the name based on the requested service and resource145 cls_name = resource_name146 if service_context.service_name == resource_name:147 cls_name = 'ServiceResource'148 cls_name = service_context.service_name + '.' + cls_name149 150 base_classes = [ServiceResource]151 if self._emitter is not None:152 self._emitter.emit(153 f'creating-resource-class.{cls_name}',154 class_attributes=attrs,155 base_classes=base_classes,156 service_context=service_context,157 )158 return type(str(cls_name), tuple(base_classes), attrs)159 160 def _load_identifiers(self, attrs, meta, resource_model, resource_name):161 """162 Populate required identifiers. These are arguments without which163 the resource cannot be used. Identifiers become arguments for164 operations on the resource.165 """166 for identifier in resource_model.identifiers:167 meta.identifiers.append(identifier.name)168 attrs[identifier.name] = self._create_identifier(169 identifier, resource_name170 )171 172 def _load_actions(173 self, attrs, resource_name, resource_model, service_context174 ):175 """176 Actions on the resource become methods, with the ``load`` method177 being a special case which sets internal data for attributes, and178 ``reload`` is an alias for ``load``.179 """180 if resource_model.load:181 attrs['load'] = self._create_action(182 action_model=resource_model.load,183 resource_name=resource_name,184 service_context=service_context,185 is_load=True,186 )187 attrs['reload'] = attrs['load']188 189 for action in resource_model.actions:190 attrs[action.name] = self._create_action(191 action_model=action,192 resource_name=resource_name,193 service_context=service_context,194 )195 196 def _load_attributes(197 self, attrs, meta, resource_name, resource_model, service_context198 ):199 """200 Load resource attributes based on the resource shape. The shape201 name is referenced in the resource JSON, but the shape itself202 is defined in the Botocore service JSON, hence the need for203 access to the ``service_model``.204 """205 if not resource_model.shape:206 return207 208 shape = service_context.service_model.shape_for(resource_model.shape)209 210 identifiers = {211 i.member_name: i212 for i in resource_model.identifiers213 if i.member_name214 }215 attributes = resource_model.get_attributes(shape)216 for name, (orig_name, member) in attributes.items():217 if name in identifiers:218 prop = self._create_identifier_alias(219 resource_name=resource_name,220 identifier=identifiers[name],221 member_model=member,222 service_context=service_context,223 )224 else:225 prop = self._create_autoload_property(226 resource_name=resource_name,227 name=orig_name,228 snake_cased=name,229 member_model=member,230 service_context=service_context,231 )232 attrs[name] = prop233 234 def _load_collections(self, attrs, resource_model, service_context):235 """236 Load resource collections from the model. Each collection becomes237 a :py:class:`~boto3.resources.collection.CollectionManager` instance238 on the resource instance, which allows you to iterate and filter239 through the collection's items.240 """241 for collection_model in resource_model.collections:242 attrs[collection_model.name] = self._create_collection(243 resource_name=resource_model.name,244 collection_model=collection_model,245 service_context=service_context,246 )247 248 def _load_has_relations(249 self, attrs, resource_name, resource_model, service_context250 ):251 """252 Load related resources, which are defined via a ``has``253 relationship but conceptually come in two forms:254 255 1. A reference, which is a related resource instance and can be256 ``None``, such as an EC2 instance's ``vpc``.257 2. A subresource, which is a resource constructor that will always258 return a resource instance which shares identifiers/data with259 this resource, such as ``s3.Bucket('name').Object('key')``.260 """261 for reference in resource_model.references:262 # This is a dangling reference, i.e. we have all263 # the data we need to create the resource, so264 # this instance becomes an attribute on the class.265 attrs[reference.name] = self._create_reference(266 reference_model=reference,267 resource_name=resource_name,268 service_context=service_context,269 )270 271 for subresource in resource_model.subresources:272 # This is a sub-resource class you can create273 # by passing in an identifier, e.g. s3.Bucket(name).274 attrs[subresource.name] = self._create_class_partial(275 subresource_model=subresource,276 resource_name=resource_name,277 service_context=service_context,278 )279 280 self._create_available_subresources_command(281 attrs, resource_model.subresources282 )283 284 def _create_available_subresources_command(self, attrs, subresources):285 _subresources = [subresource.name for subresource in subresources]286 _subresources = sorted(_subresources)287 288 def get_available_subresources(factory_self):289 """290 Returns a list of all the available sub-resources for this291 Resource.292 293 :returns: A list containing the name of each sub-resource for this294 resource295 :rtype: list of str296 """297 return _subresources298 299 attrs['get_available_subresources'] = get_available_subresources300 301 def _load_waiters(302 self, attrs, resource_name, resource_model, service_context303 ):304 """305 Load resource waiters from the model. Each waiter allows you to306 wait until a resource reaches a specific state by polling the state307 of the resource.308 """309 for waiter in resource_model.waiters:310 attrs[waiter.name] = self._create_waiter(311 resource_waiter_model=waiter,312 resource_name=resource_name,313 service_context=service_context,314 )315 316 def _create_identifier(factory_self, identifier, resource_name):317 """318 Creates a read-only property for identifier attributes.319 """320 321 def get_identifier(self):322 # The default value is set to ``None`` instead of323 # raising an AttributeError because when resources are324 # instantiated a check is made such that none of the325 # identifiers have a value ``None``. If any are ``None``,326 # a more informative user error than a generic AttributeError327 # is raised.328 return getattr(self, '_' + identifier.name, None)329 330 get_identifier.__name__ = str(identifier.name)331 get_identifier.__doc__ = docstring.IdentifierDocstring(332 resource_name=resource_name,333 identifier_model=identifier,334 include_signature=False,335 )336 337 return property(get_identifier)338 339 def _create_identifier_alias(340 factory_self, resource_name, identifier, member_model, service_context341 ):342 """343 Creates a read-only property that aliases an identifier.344 """345 346 def get_identifier(self):347 return getattr(self, '_' + identifier.name, None)348 349 get_identifier.__name__ = str(identifier.member_name)350 get_identifier.__doc__ = docstring.AttributeDocstring(351 service_name=service_context.service_name,352 resource_name=resource_name,353 attr_name=identifier.member_name,354 event_emitter=factory_self._emitter,355 attr_model=member_model,356 include_signature=False,357 )358 359 return property(get_identifier)360 361 def _create_autoload_property(362 factory_self,363 resource_name,364 name,365 snake_cased,366 member_model,367 service_context,368 ):369 """370 Creates a new property on the resource to lazy-load its value371 via the resource's ``load`` method (if it exists).372 """373 374 # The property loader will check to see if this resource has already375 # been loaded and return the cached value if possible. If not, then376 # it first checks to see if it CAN be loaded (raise if not), then377 # calls the load before returning the value.378 def property_loader(self):379 if self.meta.data is None:380 if hasattr(self, 'load'):381 self.load()382 else:383 raise ResourceLoadException(384 f'{self.__class__.__name__} has no load method'385 )386 387 return self.meta.data.get(name)388 389 property_loader.__name__ = str(snake_cased)390 property_loader.__doc__ = docstring.AttributeDocstring(391 service_name=service_context.service_name,392 resource_name=resource_name,393 attr_name=snake_cased,394 event_emitter=factory_self._emitter,395 attr_model=member_model,396 include_signature=False,397 )398 399 return property(property_loader)400 401 def _create_waiter(402 factory_self, resource_waiter_model, resource_name, service_context403 ):404 """405 Creates a new wait method for each resource where both a waiter and406 resource model is defined.407 """408 waiter = WaiterAction(409 resource_waiter_model,410 waiter_resource_name=resource_waiter_model.name,411 )412 413 def do_waiter(self, *args, **kwargs):414 waiter(self, *args, **kwargs)415 416 do_waiter.__name__ = str(resource_waiter_model.name)417 do_waiter.__doc__ = docstring.ResourceWaiterDocstring(418 resource_name=resource_name,419 event_emitter=factory_self._emitter,420 service_model=service_context.service_model,421 resource_waiter_model=resource_waiter_model,422 service_waiter_model=service_context.service_waiter_model,423 include_signature=False,424 )425 return do_waiter426 427 def _create_collection(428 factory_self, resource_name, collection_model, service_context429 ):430 """431 Creates a new property on the resource to lazy-load a collection.432 """433 cls = factory_self._collection_factory.load_from_definition(434 resource_name=resource_name,435 collection_model=collection_model,436 service_context=service_context,437 event_emitter=factory_self._emitter,438 )439 440 def get_collection(self):441 return cls(442 collection_model=collection_model,443 parent=self,444 factory=factory_self,445 service_context=service_context,446 )447 448 get_collection.__name__ = str(collection_model.name)449 get_collection.__doc__ = docstring.CollectionDocstring(450 collection_model=collection_model, include_signature=False451 )452 return property(get_collection)453 454 def _create_reference(455 factory_self, reference_model, resource_name, service_context456 ):457 """458 Creates a new property on the resource to lazy-load a reference.459 """460 # References are essentially an action with no request461 # or response, so we can re-use the response handlers to462 # build up resources from identifiers and data members.463 handler = ResourceHandler(464 search_path=reference_model.resource.path,465 factory=factory_self,466 resource_model=reference_model.resource,467 service_context=service_context,468 )469 470 # Are there any identifiers that need access to data members?471 # This is important when building the resource below since472 # it requires the data to be loaded.473 needs_data = any(474 i.source == 'data' for i in reference_model.resource.identifiers475 )476 477 def get_reference(self):478 # We need to lazy-evaluate the reference to handle circular479 # references between resources. We do this by loading the class480 # when first accessed.481 # This is using a *response handler* so we need to make sure482 # our data is loaded (if possible) and pass that data into483 # the handler as if it were a response. This allows references484 # to have their data loaded properly.485 if needs_data and self.meta.data is None and hasattr(self, 'load'):486 self.load()487 return handler(self, {}, self.meta.data)488 489 get_reference.__name__ = str(reference_model.name)490 get_reference.__doc__ = docstring.ReferenceDocstring(491 reference_model=reference_model, include_signature=False492 )493 return property(get_reference)494 495 def _create_class_partial(496 factory_self, subresource_model, resource_name, service_context497 ):498 """499 Creates a new method which acts as a functools.partial, passing500 along the instance's low-level `client` to the new resource501 class' constructor.502 """503 name = subresource_model.resource.type504 505 def create_resource(self, *args, **kwargs):506 # We need a new method here because we want access to the507 # instance's client.508 positional_args = []509 510 # We lazy-load the class to handle circular references.511 json_def = service_context.resource_json_definitions.get(name, {})512 resource_cls = factory_self.load_from_definition(513 resource_name=name,514 single_resource_json_definition=json_def,515 service_context=service_context,516 )517 518 # Assumes that identifiers are in order, which lets you do519 # e.g. ``sqs.Queue('foo').Message('bar')`` to create a new message520 # linked with the ``foo`` queue and which has a ``bar`` receipt521 # handle. If we did kwargs here then future positional arguments522 # would lead to failure.523 identifiers = subresource_model.resource.identifiers524 if identifiers is not None:525 for identifier, value in build_identifiers(identifiers, self):526 positional_args.append(value)527 528 return partial(529 resource_cls, *positional_args, client=self.meta.client530 )(*args, **kwargs)531 532 create_resource.__name__ = str(name)533 create_resource.__doc__ = docstring.SubResourceDocstring(534 resource_name=resource_name,535 sub_resource_model=subresource_model,536 service_model=service_context.service_model,537 include_signature=False,538 )539 return create_resource540 541 def _create_action(542 factory_self,543 action_model,544 resource_name,545 service_context,546 is_load=False,547 ):548 """549 Creates a new method which makes a request to the underlying550 AWS service.551 """552 # Create the action in in this closure but before the ``do_action``553 # method below is invoked, which allows instances of the resource554 # to share the ServiceAction instance.555 action = ServiceAction(556 action_model, factory=factory_self, service_context=service_context557 )558 559 # A resource's ``load`` method is special because it sets560 # values on the resource instead of returning the response.561 if is_load:562 # We need a new method here because we want access to the563 # instance via ``self``.564 def do_action(self, *args, **kwargs):565 response = action(self, *args, **kwargs)566 self.meta.data = response567 568 # Create the docstring for the load/reload methods.569 lazy_docstring = docstring.LoadReloadDocstring(570 action_name=action_model.name,571 resource_name=resource_name,572 event_emitter=factory_self._emitter,573 load_model=action_model,574 service_model=service_context.service_model,575 include_signature=False,576 )577 else:578 # We need a new method here because we want access to the579 # instance via ``self``.580 def do_action(self, *args, **kwargs):581 response = action(self, *args, **kwargs)582 583 if hasattr(self, 'load'):584 # Clear cached data. It will be reloaded the next585 # time that an attribute is accessed.586 # TODO: Make this configurable in the future?587 self.meta.data = None588 589 return response590 591 lazy_docstring = docstring.ActionDocstring(592 resource_name=resource_name,593 event_emitter=factory_self._emitter,594 action_model=action_model,595 service_model=service_context.service_model,596 include_signature=False,597 )598 599 do_action.__name__ = str(action_model.name)600 do_action.__doc__ = lazy_docstring601 return do_action602 