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.
573 lines
19 KiB
573 lines
19 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 copy
|
|
import logging
|
|
|
|
from botocore import xform_name
|
|
from botocore.utils import merge_dicts
|
|
|
|
from ..docs import docstring
|
|
from .action import BatchAction
|
|
from .params import create_request_parameters
|
|
from .response import ResourceHandler
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ResourceCollection:
|
|
"""
|
|
Represents a collection of resources, which can be iterated through,
|
|
optionally with filtering. Collections automatically handle pagination
|
|
for you.
|
|
|
|
See :ref:`guide_collections` for a high-level overview of collections,
|
|
including when remote service requests are performed.
|
|
|
|
:type model: :py:class:`~boto3.resources.model.Collection`
|
|
:param model: Collection model
|
|
:type parent: :py:class:`~boto3.resources.base.ServiceResource`
|
|
:param parent: The collection's parent resource
|
|
:type handler: :py:class:`~boto3.resources.response.ResourceHandler`
|
|
:param handler: The resource response handler used to create resource
|
|
instances
|
|
"""
|
|
|
|
def __init__(self, model, parent, handler, **kwargs):
|
|
self._model = model
|
|
self._parent = parent
|
|
self._py_operation_name = xform_name(model.request.operation)
|
|
self._handler = handler
|
|
self._params = copy.deepcopy(kwargs)
|
|
|
|
def __repr__(self):
|
|
return '{}({}, {})'.format(
|
|
self.__class__.__name__,
|
|
self._parent,
|
|
'{}.{}'.format(
|
|
self._parent.meta.service_name, self._model.resource.type
|
|
),
|
|
)
|
|
|
|
def __iter__(self):
|
|
"""
|
|
A generator which yields resource instances after doing the
|
|
appropriate service operation calls and handling any pagination
|
|
on your behalf.
|
|
|
|
Page size, item limit, and filter parameters are applied
|
|
if they have previously been set.
|
|
|
|
>>> bucket = s3.Bucket('boto3')
|
|
>>> for obj in bucket.objects.all():
|
|
... print(obj.key)
|
|
'key1'
|
|
'key2'
|
|
|
|
"""
|
|
limit = self._params.get('limit', None)
|
|
|
|
count = 0
|
|
for page in self.pages():
|
|
for item in page:
|
|
yield item
|
|
|
|
# If the limit is set and has been reached, then
|
|
# we stop processing items here.
|
|
count += 1
|
|
if limit is not None and count >= limit:
|
|
return
|
|
|
|
def _clone(self, **kwargs):
|
|
"""
|
|
Create a clone of this collection. This is used by the methods
|
|
below to provide a chainable interface that returns copies
|
|
rather than the original. This allows things like:
|
|
|
|
>>> base = collection.filter(Param1=1)
|
|
>>> query1 = base.filter(Param2=2)
|
|
>>> query2 = base.filter(Param3=3)
|
|
>>> query1.params
|
|
{'Param1': 1, 'Param2': 2}
|
|
>>> query2.params
|
|
{'Param1': 1, 'Param3': 3}
|
|
|
|
:rtype: :py:class:`ResourceCollection`
|
|
:return: A clone of this resource collection
|
|
"""
|
|
params = copy.deepcopy(self._params)
|
|
merge_dicts(params, kwargs, append_lists=True)
|
|
clone = self.__class__(
|
|
self._model, self._parent, self._handler, **params
|
|
)
|
|
return clone
|
|
|
|
def pages(self):
|
|
"""
|
|
A generator which yields pages of resource instances after
|
|
doing the appropriate service operation calls and handling
|
|
any pagination on your behalf. Non-paginated calls will
|
|
return a single page of items.
|
|
|
|
Page size, item limit, and filter parameters are applied
|
|
if they have previously been set.
|
|
|
|
>>> bucket = s3.Bucket('boto3')
|
|
>>> for page in bucket.objects.pages():
|
|
... for obj in page:
|
|
... print(obj.key)
|
|
'key1'
|
|
'key2'
|
|
|
|
:rtype: list(:py:class:`~boto3.resources.base.ServiceResource`)
|
|
:return: List of resource instances
|
|
"""
|
|
client = self._parent.meta.client
|
|
cleaned_params = self._params.copy()
|
|
limit = cleaned_params.pop('limit', None)
|
|
page_size = cleaned_params.pop('page_size', None)
|
|
params = create_request_parameters(self._parent, self._model.request)
|
|
merge_dicts(params, cleaned_params, append_lists=True)
|
|
|
|
# Is this a paginated operation? If so, we need to get an
|
|
# iterator for the various pages. If not, then we simply
|
|
# call the operation and return the result as a single
|
|
# page in a list. For non-paginated results, we just ignore
|
|
# the page size parameter.
|
|
if client.can_paginate(self._py_operation_name):
|
|
logger.debug(
|
|
'Calling paginated %s:%s with %r',
|
|
self._parent.meta.service_name,
|
|
self._py_operation_name,
|
|
params,
|
|
)
|
|
paginator = client.get_paginator(self._py_operation_name)
|
|
pages = paginator.paginate(
|
|
PaginationConfig={'MaxItems': limit, 'PageSize': page_size},
|
|
**params
|
|
)
|
|
else:
|
|
logger.debug(
|
|
'Calling %s:%s with %r',
|
|
self._parent.meta.service_name,
|
|
self._py_operation_name,
|
|
params,
|
|
)
|
|
pages = [getattr(client, self._py_operation_name)(**params)]
|
|
|
|
# Now that we have a page iterator or single page of results
|
|
# we start processing and yielding individual items.
|
|
count = 0
|
|
for page in pages:
|
|
page_items = []
|
|
for item in self._handler(self._parent, params, page):
|
|
page_items.append(item)
|
|
|
|
# If the limit is set and has been reached, then
|
|
# we stop processing items here.
|
|
count += 1
|
|
if limit is not None and count >= limit:
|
|
break
|
|
|
|
yield page_items
|
|
|
|
# Stop reading pages if we've reached out limit
|
|
if limit is not None and count >= limit:
|
|
break
|
|
|
|
def all(self):
|
|
"""
|
|
Get all items from the collection, optionally with a custom
|
|
page size and item count limit.
|
|
|
|
This method returns an iterable generator which yields
|
|
individual resource instances. Example use::
|
|
|
|
# Iterate through items
|
|
>>> for queue in sqs.queues.all():
|
|
... print(queue.url)
|
|
'https://url1'
|
|
'https://url2'
|
|
|
|
# Convert to list
|
|
>>> queues = list(sqs.queues.all())
|
|
>>> len(queues)
|
|
2
|
|
"""
|
|
return self._clone()
|
|
|
|
def filter(self, **kwargs):
|
|
"""
|
|
Get items from the collection, passing keyword arguments along
|
|
as parameters to the underlying service operation, which are
|
|
typically used to filter the results.
|
|
|
|
This method returns an iterable generator which yields
|
|
individual resource instances. Example use::
|
|
|
|
# Iterate through items
|
|
>>> for queue in sqs.queues.filter(Param='foo'):
|
|
... print(queue.url)
|
|
'https://url1'
|
|
'https://url2'
|
|
|
|
# Convert to list
|
|
>>> queues = list(sqs.queues.filter(Param='foo'))
|
|
>>> len(queues)
|
|
2
|
|
|
|
:rtype: :py:class:`ResourceCollection`
|
|
"""
|
|
return self._clone(**kwargs)
|
|
|
|
def limit(self, count):
|
|
"""
|
|
Return at most this many resources.
|
|
|
|
>>> for bucket in s3.buckets.limit(5):
|
|
... print(bucket.name)
|
|
'bucket1'
|
|
'bucket2'
|
|
'bucket3'
|
|
'bucket4'
|
|
'bucket5'
|
|
|
|
:type count: int
|
|
:param count: Return no more than this many items
|
|
:rtype: :py:class:`ResourceCollection`
|
|
"""
|
|
return self._clone(limit=count)
|
|
|
|
def page_size(self, count):
|
|
"""
|
|
Fetch at most this many resources per service request.
|
|
|
|
>>> for obj in s3.Bucket('boto3').objects.page_size(100):
|
|
... print(obj.key)
|
|
|
|
:type count: int
|
|
:param count: Fetch this many items per request
|
|
:rtype: :py:class:`ResourceCollection`
|
|
"""
|
|
return self._clone(page_size=count)
|
|
|
|
|
|
class CollectionManager:
|
|
"""
|
|
A collection manager provides access to resource collection instances,
|
|
which can be iterated and filtered. The manager exposes some
|
|
convenience functions that are also found on resource collections,
|
|
such as :py:meth:`~ResourceCollection.all` and
|
|
:py:meth:`~ResourceCollection.filter`.
|
|
|
|
Get all items::
|
|
|
|
>>> for bucket in s3.buckets.all():
|
|
... print(bucket.name)
|
|
|
|
Get only some items via filtering::
|
|
|
|
>>> for queue in sqs.queues.filter(QueueNamePrefix='AWS'):
|
|
... print(queue.url)
|
|
|
|
Get whole pages of items:
|
|
|
|
>>> for page in s3.Bucket('boto3').objects.pages():
|
|
... for obj in page:
|
|
... print(obj.key)
|
|
|
|
A collection manager is not iterable. You **must** call one of the
|
|
methods that return a :py:class:`ResourceCollection` before trying
|
|
to iterate, slice, or convert to a list.
|
|
|
|
See the :ref:`guide_collections` guide for a high-level overview
|
|
of collections, including when remote service requests are performed.
|
|
|
|
:type collection_model: :py:class:`~boto3.resources.model.Collection`
|
|
:param model: Collection model
|
|
|
|
:type parent: :py:class:`~boto3.resources.base.ServiceResource`
|
|
:param parent: The collection's parent resource
|
|
|
|
:type factory: :py:class:`~boto3.resources.factory.ResourceFactory`
|
|
:param factory: The resource factory to create new resources
|
|
|
|
:type service_context: :py:class:`~boto3.utils.ServiceContext`
|
|
:param service_context: Context about the AWS service
|
|
"""
|
|
|
|
# The class to use when creating an iterator
|
|
_collection_cls = ResourceCollection
|
|
|
|
def __init__(self, collection_model, parent, factory, service_context):
|
|
self._model = collection_model
|
|
operation_name = self._model.request.operation
|
|
self._parent = parent
|
|
|
|
search_path = collection_model.resource.path
|
|
self._handler = ResourceHandler(
|
|
search_path=search_path,
|
|
factory=factory,
|
|
resource_model=collection_model.resource,
|
|
service_context=service_context,
|
|
operation_name=operation_name,
|
|
)
|
|
|
|
def __repr__(self):
|
|
return '{}({}, {})'.format(
|
|
self.__class__.__name__,
|
|
self._parent,
|
|
'{}.{}'.format(
|
|
self._parent.meta.service_name, self._model.resource.type
|
|
),
|
|
)
|
|
|
|
def iterator(self, **kwargs):
|
|
"""
|
|
Get a resource collection iterator from this manager.
|
|
|
|
:rtype: :py:class:`ResourceCollection`
|
|
:return: An iterable representing the collection of resources
|
|
"""
|
|
return self._collection_cls(
|
|
self._model, self._parent, self._handler, **kwargs
|
|
)
|
|
|
|
# Set up some methods to proxy ResourceCollection methods
|
|
def all(self):
|
|
return self.iterator()
|
|
|
|
all.__doc__ = ResourceCollection.all.__doc__
|
|
|
|
def filter(self, **kwargs):
|
|
return self.iterator(**kwargs)
|
|
|
|
filter.__doc__ = ResourceCollection.filter.__doc__
|
|
|
|
def limit(self, count):
|
|
return self.iterator(limit=count)
|
|
|
|
limit.__doc__ = ResourceCollection.limit.__doc__
|
|
|
|
def page_size(self, count):
|
|
return self.iterator(page_size=count)
|
|
|
|
page_size.__doc__ = ResourceCollection.page_size.__doc__
|
|
|
|
def pages(self):
|
|
return self.iterator().pages()
|
|
|
|
pages.__doc__ = ResourceCollection.pages.__doc__
|
|
|
|
|
|
class CollectionFactory:
|
|
"""
|
|
A factory to create new
|
|
:py:class:`CollectionManager` and :py:class:`ResourceCollection`
|
|
subclasses from a :py:class:`~boto3.resources.model.Collection`
|
|
model. These subclasses include methods to perform batch operations.
|
|
"""
|
|
|
|
def load_from_definition(
|
|
self, resource_name, collection_model, service_context, event_emitter
|
|
):
|
|
"""
|
|
Loads a collection from a model, creating a new
|
|
:py:class:`CollectionManager` subclass
|
|
with the correct properties and methods, named based on the service
|
|
and resource name, e.g. ec2.InstanceCollectionManager. It also
|
|
creates a new :py:class:`ResourceCollection` subclass which is used
|
|
by the new manager class.
|
|
|
|
:type resource_name: string
|
|
:param resource_name: Name of the resource to look up. For services,
|
|
this should match the ``service_name``.
|
|
|
|
:type service_context: :py:class:`~boto3.utils.ServiceContext`
|
|
:param service_context: Context about the AWS service
|
|
|
|
:type event_emitter: :py:class:`~botocore.hooks.HierarchialEmitter`
|
|
:param event_emitter: An event emitter
|
|
|
|
:rtype: Subclass of :py:class:`CollectionManager`
|
|
:return: The collection class.
|
|
"""
|
|
attrs = {}
|
|
collection_name = collection_model.name
|
|
|
|
# Create the batch actions for a collection
|
|
self._load_batch_actions(
|
|
attrs,
|
|
resource_name,
|
|
collection_model,
|
|
service_context.service_model,
|
|
event_emitter,
|
|
)
|
|
# Add the documentation to the collection class's methods
|
|
self._load_documented_collection_methods(
|
|
attrs=attrs,
|
|
resource_name=resource_name,
|
|
collection_model=collection_model,
|
|
service_model=service_context.service_model,
|
|
event_emitter=event_emitter,
|
|
base_class=ResourceCollection,
|
|
)
|
|
|
|
if service_context.service_name == resource_name:
|
|
cls_name = '{}.{}Collection'.format(
|
|
service_context.service_name, collection_name
|
|
)
|
|
else:
|
|
cls_name = '{}.{}.{}Collection'.format(
|
|
service_context.service_name, resource_name, collection_name
|
|
)
|
|
|
|
collection_cls = type(str(cls_name), (ResourceCollection,), attrs)
|
|
|
|
# Add the documentation to the collection manager's methods
|
|
self._load_documented_collection_methods(
|
|
attrs=attrs,
|
|
resource_name=resource_name,
|
|
collection_model=collection_model,
|
|
service_model=service_context.service_model,
|
|
event_emitter=event_emitter,
|
|
base_class=CollectionManager,
|
|
)
|
|
attrs['_collection_cls'] = collection_cls
|
|
cls_name += 'Manager'
|
|
|
|
return type(str(cls_name), (CollectionManager,), attrs)
|
|
|
|
def _load_batch_actions(
|
|
self,
|
|
attrs,
|
|
resource_name,
|
|
collection_model,
|
|
service_model,
|
|
event_emitter,
|
|
):
|
|
"""
|
|
Batch actions on the collection become methods on both
|
|
the collection manager and iterators.
|
|
"""
|
|
for action_model in collection_model.batch_actions:
|
|
snake_cased = xform_name(action_model.name)
|
|
attrs[snake_cased] = self._create_batch_action(
|
|
resource_name,
|
|
snake_cased,
|
|
action_model,
|
|
collection_model,
|
|
service_model,
|
|
event_emitter,
|
|
)
|
|
|
|
def _load_documented_collection_methods(
|
|
factory_self,
|
|
attrs,
|
|
resource_name,
|
|
collection_model,
|
|
service_model,
|
|
event_emitter,
|
|
base_class,
|
|
):
|
|
# The base class already has these methods defined. However
|
|
# the docstrings are generic and not based for a particular service
|
|
# or resource. So we override these methods by proxying to the
|
|
# base class's builtin method and adding a docstring
|
|
# that pertains to the resource.
|
|
|
|
# A collection's all() method.
|
|
def all(self):
|
|
return base_class.all(self)
|
|
|
|
all.__doc__ = docstring.CollectionMethodDocstring(
|
|
resource_name=resource_name,
|
|
action_name='all',
|
|
event_emitter=event_emitter,
|
|
collection_model=collection_model,
|
|
service_model=service_model,
|
|
include_signature=False,
|
|
)
|
|
attrs['all'] = all
|
|
|
|
# The collection's filter() method.
|
|
def filter(self, **kwargs):
|
|
return base_class.filter(self, **kwargs)
|
|
|
|
filter.__doc__ = docstring.CollectionMethodDocstring(
|
|
resource_name=resource_name,
|
|
action_name='filter',
|
|
event_emitter=event_emitter,
|
|
collection_model=collection_model,
|
|
service_model=service_model,
|
|
include_signature=False,
|
|
)
|
|
attrs['filter'] = filter
|
|
|
|
# The collection's limit method.
|
|
def limit(self, count):
|
|
return base_class.limit(self, count)
|
|
|
|
limit.__doc__ = docstring.CollectionMethodDocstring(
|
|
resource_name=resource_name,
|
|
action_name='limit',
|
|
event_emitter=event_emitter,
|
|
collection_model=collection_model,
|
|
service_model=service_model,
|
|
include_signature=False,
|
|
)
|
|
attrs['limit'] = limit
|
|
|
|
# The collection's page_size method.
|
|
def page_size(self, count):
|
|
return base_class.page_size(self, count)
|
|
|
|
page_size.__doc__ = docstring.CollectionMethodDocstring(
|
|
resource_name=resource_name,
|
|
action_name='page_size',
|
|
event_emitter=event_emitter,
|
|
collection_model=collection_model,
|
|
service_model=service_model,
|
|
include_signature=False,
|
|
)
|
|
attrs['page_size'] = page_size
|
|
|
|
def _create_batch_action(
|
|
factory_self,
|
|
resource_name,
|
|
snake_cased,
|
|
action_model,
|
|
collection_model,
|
|
service_model,
|
|
event_emitter,
|
|
):
|
|
"""
|
|
Creates a new method which makes a batch operation request
|
|
to the underlying service API.
|
|
"""
|
|
action = BatchAction(action_model)
|
|
|
|
def batch_action(self, *args, **kwargs):
|
|
return action(self, *args, **kwargs)
|
|
|
|
batch_action.__name__ = str(snake_cased)
|
|
batch_action.__doc__ = docstring.BatchActionDocstring(
|
|
resource_name=resource_name,
|
|
event_emitter=event_emitter,
|
|
batch_action_model=action_model,
|
|
service_model=service_model,
|
|
collection_model=collection_model,
|
|
include_signature=False,
|
|
)
|
|
return batch_action
|