You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
602 lines
22 KiB
602 lines
22 KiB
# 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
|