We use cookies and similar tools to enhance your experience, provide our services, deliver relevant advertising, and make improvements. Approved third parties also use these tools to help us deliver advertising and provide certain site features.
Customize cookie preferences
We use cookies and similar tools (collectively, "cookies") for the following purposes.
Essential
Essential cookies are necessary to provide our site and services and cannot be deactivated. They are usually set in response to your actions on the site, such as setting your privacy preferences, signing in, or filling in forms.
Performance
Performance cookies provide anonymous statistics about how customers navigate our site so we can improve site experience and performance. Approved third parties may perform analytics on our behalf, but they cannot use the data for their own purposes.
Allowed
Functional
Functional cookies help us provide useful site features, remember your preferences, and display relevant content. Approved third parties may set these cookies to provide certain site features. If you do not allow these cookies, then some or all of these services may not function properly.
Allowed
Advertising
Advertising cookies may be set through our site by us or our advertising partners and help us deliver relevant marketing content. If you do not allow these cookies, you will experience less relevant advertising.
Allowed
Blocking some types of cookies may impact your experience of our sites. You may review and change your choices at any time by clicking Cookie preferences in the footer of this site. We and selected third-parties use cookies or similar technologies as specified in the AWS Cookie Notice.
# 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.importloggingfromfunctoolsimportpartialfrom..docsimportdocstringfrom..exceptionsimportResourceLoadExceptionfrom.actionimportServiceAction,WaiterActionfrom.baseimportResourceMeta,ServiceResourcefrom.collectionimportCollectionFactoryfrom.modelimportResourceModelfrom.responseimportResourceHandler,build_identifierslogger=logging.getLogger(__name__)
[docs]classResourceFactory:""" A factory to create new :py:class:`~boto3.resources.base.ServiceResource` classes from a :py:class:`~boto3.resources.model.ResourceModel`. There are two types of lookups that can be done: one on the service itself (e.g. an SQS resource) and another on models contained within the service (e.g. an SQS Queue resource). """def__init__(self,emitter):self._collection_factory=CollectionFactory()self._emitter=emitter
[docs]defload_from_definition(self,resource_name,single_resource_json_definition,service_context):""" Loads a resource from a model, creating a new :py:class:`~boto3.resources.base.ServiceResource` subclass with the correct properties and methods, named based on the service and resource name, e.g. EC2.Instance. :type resource_name: string :param resource_name: Name of the resource to look up. For services, this should match the ``service_name``. :type single_resource_json_definition: dict :param single_resource_json_definition: The loaded json of a single service resource or resource definition. :type service_context: :py:class:`~boto3.utils.ServiceContext` :param service_context: Context about the AWS service :rtype: Subclass of :py:class:`~boto3.resources.base.ServiceResource` :return: The service or resource class. """logger.debug('Loading %s:%s',service_context.service_name,resource_name)# Using the loaded JSON create a ResourceModel object.resource_model=ResourceModel(resource_name,single_resource_json_definition,service_context.resource_json_definitions,)# Do some renaming of the shape if there was a naming collision# that needed to be accounted for.shape=Noneifresource_model.shape:shape=service_context.service_model.shape_for(resource_model.shape)resource_model.load_rename_map(shape)# Set some basic infometa=ResourceMeta(service_context.service_name,resource_model=resource_model)attrs={'meta':meta,}# Create and load all of attributes of the resource class based# on the models.# Identifiersself._load_identifiers(attrs=attrs,meta=meta,resource_name=resource_name,resource_model=resource_model,)# Load/Reload actionsself._load_actions(attrs=attrs,resource_name=resource_name,resource_model=resource_model,service_context=service_context,)# Attributes that get auto-loadedself._load_attributes(attrs=attrs,meta=meta,resource_name=resource_name,resource_model=resource_model,service_context=service_context,)# Collections and their corresponding methodsself._load_collections(attrs=attrs,resource_model=resource_model,service_context=service_context,)# References and Subresourcesself._load_has_relations(attrs=attrs,resource_name=resource_name,resource_model=resource_model,service_context=service_context,)# Waiter resource actionsself._load_waiters(attrs=attrs,resource_name=resource_name,resource_model=resource_model,service_context=service_context,)# Create the name based on the requested service and resourcecls_name=resource_nameifservice_context.service_name==resource_name:cls_name='ServiceResource'cls_name=service_context.service_name+'.'+cls_namebase_classes=[ServiceResource]ifself._emitterisnotNone:self._emitter.emit(f'creating-resource-class.{cls_name}',class_attributes=attrs,base_classes=base_classes,service_context=service_context,)returntype(str(cls_name),tuple(base_classes),attrs)
def_load_identifiers(self,attrs,meta,resource_model,resource_name):""" Populate required identifiers. These are arguments without which the resource cannot be used. Identifiers become arguments for operations on the resource. """foridentifierinresource_model.identifiers:meta.identifiers.append(identifier.name)attrs[identifier.name]=self._create_identifier(identifier,resource_name)def_load_actions(self,attrs,resource_name,resource_model,service_context):""" Actions on the resource become methods, with the ``load`` method being a special case which sets internal data for attributes, and ``reload`` is an alias for ``load``. """ifresource_model.load:attrs['load']=self._create_action(action_model=resource_model.load,resource_name=resource_name,service_context=service_context,is_load=True,)attrs['reload']=attrs['load']foractioninresource_model.actions:attrs[action.name]=self._create_action(action_model=action,resource_name=resource_name,service_context=service_context,)def_load_attributes(self,attrs,meta,resource_name,resource_model,service_context):""" Load resource attributes based on the resource shape. The shape name is referenced in the resource JSON, but the shape itself is defined in the Botocore service JSON, hence the need for access to the ``service_model``. """ifnotresource_model.shape:returnshape=service_context.service_model.shape_for(resource_model.shape)identifiers={i.member_name:iforiinresource_model.identifiersifi.member_name}attributes=resource_model.get_attributes(shape)forname,(orig_name,member)inattributes.items():ifnameinidentifiers:prop=self._create_identifier_alias(resource_name=resource_name,identifier=identifiers[name],member_model=member,service_context=service_context,)else:prop=self._create_autoload_property(resource_name=resource_name,name=orig_name,snake_cased=name,member_model=member,service_context=service_context,)attrs[name]=propdef_load_collections(self,attrs,resource_model,service_context):""" Load resource collections from the model. Each collection becomes a :py:class:`~boto3.resources.collection.CollectionManager` instance on the resource instance, which allows you to iterate and filter through the collection's items. """forcollection_modelinresource_model.collections:attrs[collection_model.name]=self._create_collection(resource_name=resource_model.name,collection_model=collection_model,service_context=service_context,)def_load_has_relations(self,attrs,resource_name,resource_model,service_context):""" Load related resources, which are defined via a ``has`` relationship but conceptually come in two forms: 1. A reference, which is a related resource instance and can be ``None``, such as an EC2 instance's ``vpc``. 2. A subresource, which is a resource constructor that will always return a resource instance which shares identifiers/data with this resource, such as ``s3.Bucket('name').Object('key')``. """forreferenceinresource_model.references:# This is a dangling reference, i.e. we have all# the data we need to create the resource, so# this instance becomes an attribute on the class.attrs[reference.name]=self._create_reference(reference_model=reference,resource_name=resource_name,service_context=service_context,)forsubresourceinresource_model.subresources:# This is a sub-resource class you can create# by passing in an identifier, e.g. s3.Bucket(name).attrs[subresource.name]=self._create_class_partial(subresource_model=subresource,resource_name=resource_name,service_context=service_context,)self._create_available_subresources_command(attrs,resource_model.subresources)def_create_available_subresources_command(self,attrs,subresources):_subresources=[subresource.nameforsubresourceinsubresources]_subresources=sorted(_subresources)defget_available_subresources(factory_self):""" Returns a list of all the available sub-resources for this Resource. :returns: A list containing the name of each sub-resource for this resource :rtype: list of str """return_subresourcesattrs['get_available_subresources']=get_available_subresourcesdef_load_waiters(self,attrs,resource_name,resource_model,service_context):""" Load resource waiters from the model. Each waiter allows you to wait until a resource reaches a specific state by polling the state of the resource. """forwaiterinresource_model.waiters:attrs[waiter.name]=self._create_waiter(resource_waiter_model=waiter,resource_name=resource_name,service_context=service_context,)def_create_identifier(factory_self,identifier,resource_name):""" Creates a read-only property for identifier attributes. """defget_identifier(self):# The default value is set to ``None`` instead of# raising an AttributeError because when resources are# instantiated a check is made such that none of the# identifiers have a value ``None``. If any are ``None``,# a more informative user error than a generic AttributeError# is raised.returngetattr(self,'_'+identifier.name,None)get_identifier.__name__=str(identifier.name)get_identifier.__doc__=docstring.IdentifierDocstring(resource_name=resource_name,identifier_model=identifier,include_signature=False,)returnproperty(get_identifier)def_create_identifier_alias(factory_self,resource_name,identifier,member_model,service_context):""" Creates a read-only property that aliases an identifier. """defget_identifier(self):returngetattr(self,'_'+identifier.name,None)get_identifier.__name__=str(identifier.member_name)get_identifier.__doc__=docstring.AttributeDocstring(service_name=service_context.service_name,resource_name=resource_name,attr_name=identifier.member_name,event_emitter=factory_self._emitter,attr_model=member_model,include_signature=False,)returnproperty(get_identifier)def_create_autoload_property(factory_self,resource_name,name,snake_cased,member_model,service_context,):""" Creates a new property on the resource to lazy-load its value via the resource's ``load`` method (if it exists). """# The property loader will check to see if this resource has already# been loaded and return the cached value if possible. If not, then# it first checks to see if it CAN be loaded (raise if not), then# calls the load before returning the value.defproperty_loader(self):ifself.meta.dataisNone:ifhasattr(self,'load'):self.load()else:raiseResourceLoadException(f'{self.__class__.__name__} has no load method')returnself.meta.data.get(name)property_loader.__name__=str(snake_cased)property_loader.__doc__=docstring.AttributeDocstring(service_name=service_context.service_name,resource_name=resource_name,attr_name=snake_cased,event_emitter=factory_self._emitter,attr_model=member_model,include_signature=False,)returnproperty(property_loader)def_create_waiter(factory_self,resource_waiter_model,resource_name,service_context):""" Creates a new wait method for each resource where both a waiter and resource model is defined. """waiter=WaiterAction(resource_waiter_model,waiter_resource_name=resource_waiter_model.name,)defdo_waiter(self,*args,**kwargs):waiter(self,*args,**kwargs)do_waiter.__name__=str(resource_waiter_model.name)do_waiter.__doc__=docstring.ResourceWaiterDocstring(resource_name=resource_name,event_emitter=factory_self._emitter,service_model=service_context.service_model,resource_waiter_model=resource_waiter_model,service_waiter_model=service_context.service_waiter_model,include_signature=False,)returndo_waiterdef_create_collection(factory_self,resource_name,collection_model,service_context):""" Creates a new property on the resource to lazy-load a collection. """cls=factory_self._collection_factory.load_from_definition(resource_name=resource_name,collection_model=collection_model,service_context=service_context,event_emitter=factory_self._emitter,)defget_collection(self):returncls(collection_model=collection_model,parent=self,factory=factory_self,service_context=service_context,)get_collection.__name__=str(collection_model.name)get_collection.__doc__=docstring.CollectionDocstring(collection_model=collection_model,include_signature=False)returnproperty(get_collection)def_create_reference(factory_self,reference_model,resource_name,service_context):""" Creates a new property on the resource to lazy-load a reference. """# References are essentially an action with no request# or response, so we can re-use the response handlers to# build up resources from identifiers and data members.handler=ResourceHandler(search_path=reference_model.resource.path,factory=factory_self,resource_model=reference_model.resource,service_context=service_context,)# Are there any identifiers that need access to data members?# This is important when building the resource below since# it requires the data to be loaded.needs_data=any(i.source=='data'foriinreference_model.resource.identifiers)defget_reference(self):# We need to lazy-evaluate the reference to handle circular# references between resources. We do this by loading the class# when first accessed.# This is using a *response handler* so we need to make sure# our data is loaded (if possible) and pass that data into# the handler as if it were a response. This allows references# to have their data loaded properly.ifneeds_dataandself.meta.dataisNoneandhasattr(self,'load'):self.load()returnhandler(self,{},self.meta.data)get_reference.__name__=str(reference_model.name)get_reference.__doc__=docstring.ReferenceDocstring(reference_model=reference_model,include_signature=False)returnproperty(get_reference)def_create_class_partial(factory_self,subresource_model,resource_name,service_context):""" Creates a new method which acts as a functools.partial, passing along the instance's low-level `client` to the new resource class' constructor. """name=subresource_model.resource.typedefcreate_resource(self,*args,**kwargs):# We need a new method here because we want access to the# instance's client.positional_args=[]# We lazy-load the class to handle circular references.json_def=service_context.resource_json_definitions.get(name,{})resource_cls=factory_self.load_from_definition(resource_name=name,single_resource_json_definition=json_def,service_context=service_context,)# Assumes that identifiers are in order, which lets you do# e.g. ``sqs.Queue('foo').Message('bar')`` to create a new message# linked with the ``foo`` queue and which has a ``bar`` receipt# handle. If we did kwargs here then future positional arguments# would lead to failure.identifiers=subresource_model.resource.identifiersifidentifiersisnotNone:foridentifier,valueinbuild_identifiers(identifiers,self):positional_args.append(value)returnpartial(resource_cls,*positional_args,client=self.meta.client)(*args,**kwargs)create_resource.__name__=str(name)create_resource.__doc__=docstring.SubResourceDocstring(resource_name=resource_name,sub_resource_model=subresource_model,service_model=service_context.service_model,include_signature=False,)returncreate_resourcedef_create_action(factory_self,action_model,resource_name,service_context,is_load=False,):""" Creates a new method which makes a request to the underlying AWS service. """# Create the action in in this closure but before the ``do_action``# method below is invoked, which allows instances of the resource# to share the ServiceAction instance.action=ServiceAction(action_model,factory=factory_self,service_context=service_context)# A resource's ``load`` method is special because it sets# values on the resource instead of returning the response.ifis_load:# We need a new method here because we want access to the# instance via ``self``.defdo_action(self,*args,**kwargs):response=action(self,*args,**kwargs)self.meta.data=response# Create the docstring for the load/reload methods.lazy_docstring=docstring.LoadReloadDocstring(action_name=action_model.name,resource_name=resource_name,event_emitter=factory_self._emitter,load_model=action_model,service_model=service_context.service_model,include_signature=False,)else:# We need a new method here because we want access to the# instance via ``self``.defdo_action(self,*args,**kwargs):response=action(self,*args,**kwargs)ifhasattr(self,'load'):# Clear cached data. It will be reloaded the next# time that an attribute is accessed.# TODO: Make this configurable in the future?self.meta.data=Nonereturnresponselazy_docstring=docstring.ActionDocstring(resource_name=resource_name,event_emitter=factory_self._emitter,action_model=action_model,service_model=service_context.service_model,include_signature=False,)do_action.__name__=str(action_model.name)do_action.__doc__=lazy_docstringreturndo_action