Spaces:
Paused
Paused
# 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. | |
import logging | |
from functools import partial | |
from ..docs import docstring | |
from ..exceptions import ResourceLoadException | |
from .action import ServiceAction, WaiterAction | |
from .base import ResourceMeta, ServiceResource | |
from .collection import CollectionFactory | |
from .model import ResourceModel | |
from .response import ResourceHandler, build_identifiers | |
logger = logging.getLogger(__name__) | |
class ResourceFactory: | |
""" | |
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 | |
def load_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 = None | |
if resource_model.shape: | |
shape = service_context.service_model.shape_for( | |
resource_model.shape | |
) | |
resource_model.load_rename_map(shape) | |
# Set some basic info | |
meta = 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. | |
# Identifiers | |
self._load_identifiers( | |
attrs=attrs, | |
meta=meta, | |
resource_name=resource_name, | |
resource_model=resource_model, | |
) | |
# Load/Reload actions | |
self._load_actions( | |
attrs=attrs, | |
resource_name=resource_name, | |
resource_model=resource_model, | |
service_context=service_context, | |
) | |
# Attributes that get auto-loaded | |
self._load_attributes( | |
attrs=attrs, | |
meta=meta, | |
resource_name=resource_name, | |
resource_model=resource_model, | |
service_context=service_context, | |
) | |
# Collections and their corresponding methods | |
self._load_collections( | |
attrs=attrs, | |
resource_model=resource_model, | |
service_context=service_context, | |
) | |
# References and Subresources | |
self._load_has_relations( | |
attrs=attrs, | |
resource_name=resource_name, | |
resource_model=resource_model, | |
service_context=service_context, | |
) | |
# Waiter resource actions | |
self._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 resource | |
cls_name = resource_name | |
if service_context.service_name == resource_name: | |
cls_name = 'ServiceResource' | |
cls_name = service_context.service_name + '.' + cls_name | |
base_classes = [ServiceResource] | |
if self._emitter is not None: | |
self._emitter.emit( | |
f'creating-resource-class.{cls_name}', | |
class_attributes=attrs, | |
base_classes=base_classes, | |
service_context=service_context, | |
) | |
return type(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. | |
""" | |
for identifier in resource_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``. | |
""" | |
if resource_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'] | |
for action in resource_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``. | |
""" | |
if not resource_model.shape: | |
return | |
shape = service_context.service_model.shape_for(resource_model.shape) | |
identifiers = { | |
i.member_name: i | |
for i in resource_model.identifiers | |
if i.member_name | |
} | |
attributes = resource_model.get_attributes(shape) | |
for name, (orig_name, member) in attributes.items(): | |
if name in identifiers: | |
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] = prop | |
def _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. | |
""" | |
for collection_model in resource_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')``. | |
""" | |
for reference in resource_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, | |
) | |
for subresource in resource_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.name for subresource in subresources] | |
_subresources = sorted(_subresources) | |
def get_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 _subresources | |
attrs['get_available_subresources'] = get_available_subresources | |
def _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. | |
""" | |
for waiter in resource_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. | |
""" | |
def get_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. | |
return getattr(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, | |
) | |
return property(get_identifier) | |
def _create_identifier_alias( | |
factory_self, resource_name, identifier, member_model, service_context | |
): | |
""" | |
Creates a read-only property that aliases an identifier. | |
""" | |
def get_identifier(self): | |
return getattr(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, | |
) | |
return property(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. | |
def property_loader(self): | |
if self.meta.data is None: | |
if hasattr(self, 'load'): | |
self.load() | |
else: | |
raise ResourceLoadException( | |
f'{self.__class__.__name__} has no load method' | |
) | |
return self.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, | |
) | |
return property(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, | |
) | |
def do_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, | |
) | |
return do_waiter | |
def _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, | |
) | |
def get_collection(self): | |
return cls( | |
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 | |
) | |
return property(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' for i in reference_model.resource.identifiers | |
) | |
def get_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. | |
if needs_data and self.meta.data is None and hasattr(self, 'load'): | |
self.load() | |
return handler(self, {}, self.meta.data) | |
get_reference.__name__ = str(reference_model.name) | |
get_reference.__doc__ = docstring.ReferenceDocstring( | |
reference_model=reference_model, include_signature=False | |
) | |
return property(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.type | |
def create_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.identifiers | |
if identifiers is not None: | |
for identifier, value in build_identifiers(identifiers, self): | |
positional_args.append(value) | |
return partial( | |
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, | |
) | |
return create_resource | |
def _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. | |
if is_load: | |
# We need a new method here because we want access to the | |
# instance via ``self``. | |
def do_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``. | |
def do_action(self, *args, **kwargs): | |
response = action(self, *args, **kwargs) | |
if hasattr(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 = None | |
return response | |
lazy_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_docstring | |
return do_action | |