diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/boto3/resources/model.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/boto3/resources/model.py | 630 |
1 files changed, 630 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/boto3/resources/model.py b/.venv/lib/python3.12/site-packages/boto3/resources/model.py new file mode 100644 index 00000000..9d61fea9 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/boto3/resources/model.py @@ -0,0 +1,630 @@ +# 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. + +""" +The models defined in this file represent the resource JSON description +format and provide a layer of abstraction from the raw JSON. The advantages +of this are: + +* Pythonic interface (e.g. ``action.request.operation``) +* Consumers need not change for minor JSON changes (e.g. renamed field) + +These models are used both by the resource factory to generate resource +classes as well as by the documentation generator. +""" + +import logging + +from botocore import xform_name + +logger = logging.getLogger(__name__) + + +class Identifier: + """ + A resource identifier, given by its name. + + :type name: string + :param name: The name of the identifier + """ + + def __init__(self, name, member_name=None): + #: (``string``) The name of the identifier + self.name = name + self.member_name = member_name + + +class Action: + """ + A service operation action. + + :type name: string + :param name: The name of the action + :type definition: dict + :param definition: The JSON definition + :type resource_defs: dict + :param resource_defs: All resources defined in the service + """ + + def __init__(self, name, definition, resource_defs): + self._definition = definition + + #: (``string``) The name of the action + self.name = name + #: (:py:class:`Request`) This action's request or ``None`` + self.request = None + if 'request' in definition: + self.request = Request(definition.get('request', {})) + #: (:py:class:`ResponseResource`) This action's resource or ``None`` + self.resource = None + if 'resource' in definition: + self.resource = ResponseResource( + definition.get('resource', {}), resource_defs + ) + #: (``string``) The JMESPath search path or ``None`` + self.path = definition.get('path') + + +class DefinitionWithParams: + """ + An item which has parameters exposed via the ``params`` property. + A request has an operation and parameters, while a waiter has + a name, a low-level waiter name and parameters. + + :type definition: dict + :param definition: The JSON definition + """ + + def __init__(self, definition): + self._definition = definition + + @property + def params(self): + """ + Get a list of auto-filled parameters for this request. + + :type: list(:py:class:`Parameter`) + """ + params = [] + + for item in self._definition.get('params', []): + params.append(Parameter(**item)) + + return params + + +class Parameter: + """ + An auto-filled parameter which has a source and target. For example, + the ``QueueUrl`` may be auto-filled from a resource's ``url`` identifier + when making calls to ``queue.receive_messages``. + + :type target: string + :param target: The destination parameter name, e.g. ``QueueUrl`` + :type source_type: string + :param source_type: Where the source is defined. + :type source: string + :param source: The source name, e.g. ``Url`` + """ + + def __init__( + self, target, source, name=None, path=None, value=None, **kwargs + ): + #: (``string``) The destination parameter name + self.target = target + #: (``string``) Where the source is defined + self.source = source + #: (``string``) The name of the source, if given + self.name = name + #: (``string``) The JMESPath query of the source + self.path = path + #: (``string|int|float|bool``) The source constant value + self.value = value + + # Complain if we encounter any unknown values. + if kwargs: + logger.warning('Unknown parameter options found: %s', kwargs) + + +class Request(DefinitionWithParams): + """ + A service operation action request. + + :type definition: dict + :param definition: The JSON definition + """ + + def __init__(self, definition): + super().__init__(definition) + + #: (``string``) The name of the low-level service operation + self.operation = definition.get('operation') + + +class Waiter(DefinitionWithParams): + """ + An event waiter specification. + + :type name: string + :param name: Name of the waiter + :type definition: dict + :param definition: The JSON definition + """ + + PREFIX = 'WaitUntil' + + def __init__(self, name, definition): + super().__init__(definition) + + #: (``string``) The name of this waiter + self.name = name + + #: (``string``) The name of the underlying event waiter + self.waiter_name = definition.get('waiterName') + + +class ResponseResource: + """ + A resource response to create after performing an action. + + :type definition: dict + :param definition: The JSON definition + :type resource_defs: dict + :param resource_defs: All resources defined in the service + """ + + def __init__(self, definition, resource_defs): + self._definition = definition + self._resource_defs = resource_defs + + #: (``string``) The name of the response resource type + self.type = definition.get('type') + + #: (``string``) The JMESPath search query or ``None`` + self.path = definition.get('path') + + @property + def identifiers(self): + """ + A list of resource identifiers. + + :type: list(:py:class:`Identifier`) + """ + identifiers = [] + + for item in self._definition.get('identifiers', []): + identifiers.append(Parameter(**item)) + + return identifiers + + @property + def model(self): + """ + Get the resource model for the response resource. + + :type: :py:class:`ResourceModel` + """ + return ResourceModel( + self.type, self._resource_defs[self.type], self._resource_defs + ) + + +class Collection(Action): + """ + A group of resources. See :py:class:`Action`. + + :type name: string + :param name: The name of the collection + :type definition: dict + :param definition: The JSON definition + :type resource_defs: dict + :param resource_defs: All resources defined in the service + """ + + @property + def batch_actions(self): + """ + Get a list of batch actions supported by the resource type + contained in this action. This is a shortcut for accessing + the same information through the resource model. + + :rtype: list(:py:class:`Action`) + """ + return self.resource.model.batch_actions + + +class ResourceModel: + """ + A model representing a resource, defined via a JSON description + format. A resource has identifiers, attributes, actions, + sub-resources, references and collections. For more information + on resources, see :ref:`guide_resources`. + + :type name: string + :param name: The name of this resource, e.g. ``sqs`` or ``Queue`` + :type definition: dict + :param definition: The JSON definition + :type resource_defs: dict + :param resource_defs: All resources defined in the service + """ + + def __init__(self, name, definition, resource_defs): + self._definition = definition + self._resource_defs = resource_defs + self._renamed = {} + + #: (``string``) The name of this resource + self.name = name + #: (``string``) The service shape name for this resource or ``None`` + self.shape = definition.get('shape') + + def load_rename_map(self, shape=None): + """ + Load a name translation map given a shape. This will set + up renamed values for any collisions, e.g. if the shape, + an action, and a subresource all are all named ``foo`` + then the resource will have an action ``foo``, a subresource + named ``Foo`` and a property named ``foo_attribute``. + This is the order of precedence, from most important to + least important: + + * Load action (resource.load) + * Identifiers + * Actions + * Subresources + * References + * Collections + * Waiters + * Attributes (shape members) + + Batch actions are only exposed on collections, so do not + get modified here. Subresources use upper camel casing, so + are unlikely to collide with anything but other subresources. + + Creates a structure like this:: + + renames = { + ('action', 'id'): 'id_action', + ('collection', 'id'): 'id_collection', + ('attribute', 'id'): 'id_attribute' + } + + # Get the final name for an action named 'id' + name = renames.get(('action', 'id'), 'id') + + :type shape: botocore.model.Shape + :param shape: The underlying shape for this resource. + """ + # Meta is a reserved name for resources + names = {'meta'} + self._renamed = {} + + if self._definition.get('load'): + names.add('load') + + for item in self._definition.get('identifiers', []): + self._load_name_with_category(names, item['name'], 'identifier') + + for name in self._definition.get('actions', {}): + self._load_name_with_category(names, name, 'action') + + for name, ref in self._get_has_definition().items(): + # Subresources require no data members, just typically + # identifiers and user input. + data_required = False + for identifier in ref['resource']['identifiers']: + if identifier['source'] == 'data': + data_required = True + break + + if not data_required: + self._load_name_with_category( + names, name, 'subresource', snake_case=False + ) + else: + self._load_name_with_category(names, name, 'reference') + + for name in self._definition.get('hasMany', {}): + self._load_name_with_category(names, name, 'collection') + + for name in self._definition.get('waiters', {}): + self._load_name_with_category( + names, Waiter.PREFIX + name, 'waiter' + ) + + if shape is not None: + for name in shape.members.keys(): + self._load_name_with_category(names, name, 'attribute') + + def _load_name_with_category(self, names, name, category, snake_case=True): + """ + Load a name with a given category, possibly renaming it + if that name is already in use. The name will be stored + in ``names`` and possibly be set up in ``self._renamed``. + + :type names: set + :param names: Existing names (Python attributes, properties, or + methods) on the resource. + :type name: string + :param name: The original name of the value. + :type category: string + :param category: The value type, such as 'identifier' or 'action' + :type snake_case: bool + :param snake_case: True (default) if the name should be snake cased. + """ + if snake_case: + name = xform_name(name) + + if name in names: + logger.debug(f'Renaming {self.name} {category} {name}') + self._renamed[(category, name)] = name + '_' + category + name += '_' + category + + if name in names: + # This isn't good, let's raise instead of trying to keep + # renaming this value. + raise ValueError( + f'Problem renaming {self.name} {category} to {name}!' + ) + + names.add(name) + + def _get_name(self, category, name, snake_case=True): + """ + Get a possibly renamed value given a category and name. This + uses the rename map set up in ``load_rename_map``, so that + method must be called once first. + + :type category: string + :param category: The value type, such as 'identifier' or 'action' + :type name: string + :param name: The original name of the value + :type snake_case: bool + :param snake_case: True (default) if the name should be snake cased. + :rtype: string + :return: Either the renamed value if it is set, otherwise the + original name. + """ + if snake_case: + name = xform_name(name) + + return self._renamed.get((category, name), name) + + def get_attributes(self, shape): + """ + Get a dictionary of attribute names to original name and shape + models that represent the attributes of this resource. Looks + like the following: + + { + 'some_name': ('SomeName', <Shape...>) + } + + :type shape: botocore.model.Shape + :param shape: The underlying shape for this resource. + :rtype: dict + :return: Mapping of resource attributes. + """ + attributes = {} + identifier_names = [i.name for i in self.identifiers] + + for name, member in shape.members.items(): + snake_cased = xform_name(name) + if snake_cased in identifier_names: + # Skip identifiers, these are set through other means + continue + snake_cased = self._get_name( + 'attribute', snake_cased, snake_case=False + ) + attributes[snake_cased] = (name, member) + + return attributes + + @property + def identifiers(self): + """ + Get a list of resource identifiers. + + :type: list(:py:class:`Identifier`) + """ + identifiers = [] + + for item in self._definition.get('identifiers', []): + name = self._get_name('identifier', item['name']) + member_name = item.get('memberName', None) + if member_name: + member_name = self._get_name('attribute', member_name) + identifiers.append(Identifier(name, member_name)) + + return identifiers + + @property + def load(self): + """ + Get the load action for this resource, if it is defined. + + :type: :py:class:`Action` or ``None`` + """ + action = self._definition.get('load') + + if action is not None: + action = Action('load', action, self._resource_defs) + + return action + + @property + def actions(self): + """ + Get a list of actions for this resource. + + :type: list(:py:class:`Action`) + """ + actions = [] + + for name, item in self._definition.get('actions', {}).items(): + name = self._get_name('action', name) + actions.append(Action(name, item, self._resource_defs)) + + return actions + + @property + def batch_actions(self): + """ + Get a list of batch actions for this resource. + + :type: list(:py:class:`Action`) + """ + actions = [] + + for name, item in self._definition.get('batchActions', {}).items(): + name = self._get_name('batch_action', name) + actions.append(Action(name, item, self._resource_defs)) + + return actions + + def _get_has_definition(self): + """ + Get a ``has`` relationship definition from a model, where the + service resource model is treated special in that it contains + a relationship to every resource defined for the service. This + allows things like ``s3.Object('bucket-name', 'key')`` to + work even though the JSON doesn't define it explicitly. + + :rtype: dict + :return: Mapping of names to subresource and reference + definitions. + """ + if self.name not in self._resource_defs: + # This is the service resource, so let us expose all of + # the defined resources as subresources. + definition = {} + + for name, resource_def in self._resource_defs.items(): + # It's possible for the service to have renamed a + # resource or to have defined multiple names that + # point to the same resource type, so we need to + # take that into account. + found = False + has_items = self._definition.get('has', {}).items() + for has_name, has_def in has_items: + if has_def.get('resource', {}).get('type') == name: + definition[has_name] = has_def + found = True + + if not found: + # Create a relationship definition and attach it + # to the model, such that all identifiers must be + # supplied by the user. It will look something like: + # + # { + # 'resource': { + # 'type': 'ResourceName', + # 'identifiers': [ + # {'target': 'Name1', 'source': 'input'}, + # {'target': 'Name2', 'source': 'input'}, + # ... + # ] + # } + # } + # + fake_has = {'resource': {'type': name, 'identifiers': []}} + + for identifier in resource_def.get('identifiers', []): + fake_has['resource']['identifiers'].append( + {'target': identifier['name'], 'source': 'input'} + ) + + definition[name] = fake_has + else: + definition = self._definition.get('has', {}) + + return definition + + def _get_related_resources(self, subresources): + """ + Get a list of sub-resources or references. + + :type subresources: bool + :param subresources: ``True`` to get sub-resources, ``False`` to + get references. + :rtype: list(:py:class:`Action`) + """ + resources = [] + + for name, definition in self._get_has_definition().items(): + if subresources: + name = self._get_name('subresource', name, snake_case=False) + else: + name = self._get_name('reference', name) + action = Action(name, definition, self._resource_defs) + + data_required = False + for identifier in action.resource.identifiers: + if identifier.source == 'data': + data_required = True + break + + if subresources and not data_required: + resources.append(action) + elif not subresources and data_required: + resources.append(action) + + return resources + + @property + def subresources(self): + """ + Get a list of sub-resources. + + :type: list(:py:class:`Action`) + """ + return self._get_related_resources(True) + + @property + def references(self): + """ + Get a list of reference resources. + + :type: list(:py:class:`Action`) + """ + return self._get_related_resources(False) + + @property + def collections(self): + """ + Get a list of collections for this resource. + + :type: list(:py:class:`Collection`) + """ + collections = [] + + for name, item in self._definition.get('hasMany', {}).items(): + name = self._get_name('collection', name) + collections.append(Collection(name, item, self._resource_defs)) + + return collections + + @property + def waiters(self): + """ + Get a list of waiters for this resource. + + :type: list(:py:class:`Waiter`) + """ + waiters = [] + + for name, item in self._definition.get('waiters', {}).items(): + name = self._get_name('waiter', Waiter.PREFIX + name) + waiters.append(Waiter(name, item)) + + return waiters |