Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
factory.py602 linesDownload Raw Back to resources
1# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.2#3# Licensed under the Apache License, Version 2.0 (the "License"). You4# may not use this file except in compliance with the License. A copy of5# the License is located at6#7# https://aws.amazon.com/apache2.0/8#9# or in the "license" file accompanying this file. This file is10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF11# ANY KIND, either express or implied. See the License for the specific12# language governing permissions and limitations under the License.13 14import logging15from functools import partial16 17from ..docs import docstring18from ..exceptions import ResourceLoadException19from .action import ServiceAction, WaiterAction20from .base import ResourceMeta, ServiceResource21from .collection import CollectionFactory22from .model import ResourceModel23from .response import ResourceHandler, build_identifiers24 25logger = logging.getLogger(__name__)26 27 28class ResourceFactory:29    """30    A factory to create new :py:class:`~boto3.resources.base.ServiceResource`31    classes from a :py:class:`~boto3.resources.model.ResourceModel`. There are32    two types of lookups that can be done: one on the service itself (e.g. an33    SQS resource) and another on models contained within the service (e.g. an34    SQS Queue resource).35    """36 37    def __init__(self, emitter):38        self._collection_factory = CollectionFactory()39        self._emitter = emitter40 41    def load_from_definition(42        self, resource_name, single_resource_json_definition, service_context43    ):44        """45        Loads a resource from a model, creating a new46        :py:class:`~boto3.resources.base.ServiceResource` subclass47        with the correct properties and methods, named based on the service48        and resource name, e.g. EC2.Instance.49 50        :type resource_name: string51        :param resource_name: Name of the resource to look up. For services,52                              this should match the ``service_name``.53 54        :type single_resource_json_definition: dict55        :param single_resource_json_definition:56            The loaded json of a single service resource or resource57            definition.58 59        :type service_context: :py:class:`~boto3.utils.ServiceContext`60        :param service_context: Context about the AWS service61 62        :rtype: Subclass of :py:class:`~boto3.resources.base.ServiceResource`63        :return: The service or resource class.64        """65        logger.debug(66            'Loading %s:%s', service_context.service_name, resource_name67        )68 69        # Using the loaded JSON create a ResourceModel object.70        resource_model = ResourceModel(71            resource_name,72            single_resource_json_definition,73            service_context.resource_json_definitions,74        )75 76        # Do some renaming of the shape if there was a naming collision77        # that needed to be accounted for.78        shape = None79        if resource_model.shape:80            shape = service_context.service_model.shape_for(81                resource_model.shape82            )83        resource_model.load_rename_map(shape)84 85        # Set some basic info86        meta = ResourceMeta(87            service_context.service_name, resource_model=resource_model88        )89        attrs = {90            'meta': meta,91        }92 93        # Create and load all of attributes of the resource class based94        # on the models.95 96        # Identifiers97        self._load_identifiers(98            attrs=attrs,99            meta=meta,100            resource_name=resource_name,101            resource_model=resource_model,102        )103 104        # Load/Reload actions105        self._load_actions(106            attrs=attrs,107            resource_name=resource_name,108            resource_model=resource_model,109            service_context=service_context,110        )111 112        # Attributes that get auto-loaded113        self._load_attributes(114            attrs=attrs,115            meta=meta,116            resource_name=resource_name,117            resource_model=resource_model,118            service_context=service_context,119        )120 121        # Collections and their corresponding methods122        self._load_collections(123            attrs=attrs,124            resource_model=resource_model,125            service_context=service_context,126        )127 128        # References and Subresources129        self._load_has_relations(130            attrs=attrs,131            resource_name=resource_name,132            resource_model=resource_model,133            service_context=service_context,134        )135 136        # Waiter resource actions137        self._load_waiters(138            attrs=attrs,139            resource_name=resource_name,140            resource_model=resource_model,141            service_context=service_context,142        )143 144        # Create the name based on the requested service and resource145        cls_name = resource_name146        if service_context.service_name == resource_name:147            cls_name = 'ServiceResource'148        cls_name = service_context.service_name + '.' + cls_name149 150        base_classes = [ServiceResource]151        if self._emitter is not None:152            self._emitter.emit(153                f'creating-resource-class.{cls_name}',154                class_attributes=attrs,155                base_classes=base_classes,156                service_context=service_context,157            )158        return type(str(cls_name), tuple(base_classes), attrs)159 160    def _load_identifiers(self, attrs, meta, resource_model, resource_name):161        """162        Populate required identifiers. These are arguments without which163        the resource cannot be used. Identifiers become arguments for164        operations on the resource.165        """166        for identifier in resource_model.identifiers:167            meta.identifiers.append(identifier.name)168            attrs[identifier.name] = self._create_identifier(169                identifier, resource_name170            )171 172    def _load_actions(173        self, attrs, resource_name, resource_model, service_context174    ):175        """176        Actions on the resource become methods, with the ``load`` method177        being a special case which sets internal data for attributes, and178        ``reload`` is an alias for ``load``.179        """180        if resource_model.load:181            attrs['load'] = self._create_action(182                action_model=resource_model.load,183                resource_name=resource_name,184                service_context=service_context,185                is_load=True,186            )187            attrs['reload'] = attrs['load']188 189        for action in resource_model.actions:190            attrs[action.name] = self._create_action(191                action_model=action,192                resource_name=resource_name,193                service_context=service_context,194            )195 196    def _load_attributes(197        self, attrs, meta, resource_name, resource_model, service_context198    ):199        """200        Load resource attributes based on the resource shape. The shape201        name is referenced in the resource JSON, but the shape itself202        is defined in the Botocore service JSON, hence the need for203        access to the ``service_model``.204        """205        if not resource_model.shape:206            return207 208        shape = service_context.service_model.shape_for(resource_model.shape)209 210        identifiers = {211            i.member_name: i212            for i in resource_model.identifiers213            if i.member_name214        }215        attributes = resource_model.get_attributes(shape)216        for name, (orig_name, member) in attributes.items():217            if name in identifiers:218                prop = self._create_identifier_alias(219                    resource_name=resource_name,220                    identifier=identifiers[name],221                    member_model=member,222                    service_context=service_context,223                )224            else:225                prop = self._create_autoload_property(226                    resource_name=resource_name,227                    name=orig_name,228                    snake_cased=name,229                    member_model=member,230                    service_context=service_context,231                )232            attrs[name] = prop233 234    def _load_collections(self, attrs, resource_model, service_context):235        """236        Load resource collections from the model. Each collection becomes237        a :py:class:`~boto3.resources.collection.CollectionManager` instance238        on the resource instance, which allows you to iterate and filter239        through the collection's items.240        """241        for collection_model in resource_model.collections:242            attrs[collection_model.name] = self._create_collection(243                resource_name=resource_model.name,244                collection_model=collection_model,245                service_context=service_context,246            )247 248    def _load_has_relations(249        self, attrs, resource_name, resource_model, service_context250    ):251        """252        Load related resources, which are defined via a ``has``253        relationship but conceptually come in two forms:254 255        1. A reference, which is a related resource instance and can be256           ``None``, such as an EC2 instance's ``vpc``.257        2. A subresource, which is a resource constructor that will always258           return a resource instance which shares identifiers/data with259           this resource, such as ``s3.Bucket('name').Object('key')``.260        """261        for reference in resource_model.references:262            # This is a dangling reference, i.e. we have all263            # the data we need to create the resource, so264            # this instance becomes an attribute on the class.265            attrs[reference.name] = self._create_reference(266                reference_model=reference,267                resource_name=resource_name,268                service_context=service_context,269            )270 271        for subresource in resource_model.subresources:272            # This is a sub-resource class you can create273            # by passing in an identifier, e.g. s3.Bucket(name).274            attrs[subresource.name] = self._create_class_partial(275                subresource_model=subresource,276                resource_name=resource_name,277                service_context=service_context,278            )279 280        self._create_available_subresources_command(281            attrs, resource_model.subresources282        )283 284    def _create_available_subresources_command(self, attrs, subresources):285        _subresources = [subresource.name for subresource in subresources]286        _subresources = sorted(_subresources)287 288        def get_available_subresources(factory_self):289            """290            Returns a list of all the available sub-resources for this291            Resource.292 293            :returns: A list containing the name of each sub-resource for this294                resource295            :rtype: list of str296            """297            return _subresources298 299        attrs['get_available_subresources'] = get_available_subresources300 301    def _load_waiters(302        self, attrs, resource_name, resource_model, service_context303    ):304        """305        Load resource waiters from the model. Each waiter allows you to306        wait until a resource reaches a specific state by polling the state307        of the resource.308        """309        for waiter in resource_model.waiters:310            attrs[waiter.name] = self._create_waiter(311                resource_waiter_model=waiter,312                resource_name=resource_name,313                service_context=service_context,314            )315 316    def _create_identifier(factory_self, identifier, resource_name):317        """318        Creates a read-only property for identifier attributes.319        """320 321        def get_identifier(self):322            # The default value is set to ``None`` instead of323            # raising an AttributeError because when resources are324            # instantiated a check is made such that none of the325            # identifiers have a value ``None``. If any are ``None``,326            # a more informative user error than a generic AttributeError327            # is raised.328            return getattr(self, '_' + identifier.name, None)329 330        get_identifier.__name__ = str(identifier.name)331        get_identifier.__doc__ = docstring.IdentifierDocstring(332            resource_name=resource_name,333            identifier_model=identifier,334            include_signature=False,335        )336 337        return property(get_identifier)338 339    def _create_identifier_alias(340        factory_self, resource_name, identifier, member_model, service_context341    ):342        """343        Creates a read-only property that aliases an identifier.344        """345 346        def get_identifier(self):347            return getattr(self, '_' + identifier.name, None)348 349        get_identifier.__name__ = str(identifier.member_name)350        get_identifier.__doc__ = docstring.AttributeDocstring(351            service_name=service_context.service_name,352            resource_name=resource_name,353            attr_name=identifier.member_name,354            event_emitter=factory_self._emitter,355            attr_model=member_model,356            include_signature=False,357        )358 359        return property(get_identifier)360 361    def _create_autoload_property(362        factory_self,363        resource_name,364        name,365        snake_cased,366        member_model,367        service_context,368    ):369        """370        Creates a new property on the resource to lazy-load its value371        via the resource's ``load`` method (if it exists).372        """373 374        # The property loader will check to see if this resource has already375        # been loaded and return the cached value if possible. If not, then376        # it first checks to see if it CAN be loaded (raise if not), then377        # calls the load before returning the value.378        def property_loader(self):379            if self.meta.data is None:380                if hasattr(self, 'load'):381                    self.load()382                else:383                    raise ResourceLoadException(384                        f'{self.__class__.__name__} has no load method'385                    )386 387            return self.meta.data.get(name)388 389        property_loader.__name__ = str(snake_cased)390        property_loader.__doc__ = docstring.AttributeDocstring(391            service_name=service_context.service_name,392            resource_name=resource_name,393            attr_name=snake_cased,394            event_emitter=factory_self._emitter,395            attr_model=member_model,396            include_signature=False,397        )398 399        return property(property_loader)400 401    def _create_waiter(402        factory_self, resource_waiter_model, resource_name, service_context403    ):404        """405        Creates a new wait method for each resource where both a waiter and406        resource model is defined.407        """408        waiter = WaiterAction(409            resource_waiter_model,410            waiter_resource_name=resource_waiter_model.name,411        )412 413        def do_waiter(self, *args, **kwargs):414            waiter(self, *args, **kwargs)415 416        do_waiter.__name__ = str(resource_waiter_model.name)417        do_waiter.__doc__ = docstring.ResourceWaiterDocstring(418            resource_name=resource_name,419            event_emitter=factory_self._emitter,420            service_model=service_context.service_model,421            resource_waiter_model=resource_waiter_model,422            service_waiter_model=service_context.service_waiter_model,423            include_signature=False,424        )425        return do_waiter426 427    def _create_collection(428        factory_self, resource_name, collection_model, service_context429    ):430        """431        Creates a new property on the resource to lazy-load a collection.432        """433        cls = factory_self._collection_factory.load_from_definition(434            resource_name=resource_name,435            collection_model=collection_model,436            service_context=service_context,437            event_emitter=factory_self._emitter,438        )439 440        def get_collection(self):441            return cls(442                collection_model=collection_model,443                parent=self,444                factory=factory_self,445                service_context=service_context,446            )447 448        get_collection.__name__ = str(collection_model.name)449        get_collection.__doc__ = docstring.CollectionDocstring(450            collection_model=collection_model, include_signature=False451        )452        return property(get_collection)453 454    def _create_reference(455        factory_self, reference_model, resource_name, service_context456    ):457        """458        Creates a new property on the resource to lazy-load a reference.459        """460        # References are essentially an action with no request461        # or response, so we can re-use the response handlers to462        # build up resources from identifiers and data members.463        handler = ResourceHandler(464            search_path=reference_model.resource.path,465            factory=factory_self,466            resource_model=reference_model.resource,467            service_context=service_context,468        )469 470        # Are there any identifiers that need access to data members?471        # This is important when building the resource below since472        # it requires the data to be loaded.473        needs_data = any(474            i.source == 'data' for i in reference_model.resource.identifiers475        )476 477        def get_reference(self):478            # We need to lazy-evaluate the reference to handle circular479            # references between resources. We do this by loading the class480            # when first accessed.481            # This is using a *response handler* so we need to make sure482            # our data is loaded (if possible) and pass that data into483            # the handler as if it were a response. This allows references484            # to have their data loaded properly.485            if needs_data and self.meta.data is None and hasattr(self, 'load'):486                self.load()487            return handler(self, {}, self.meta.data)488 489        get_reference.__name__ = str(reference_model.name)490        get_reference.__doc__ = docstring.ReferenceDocstring(491            reference_model=reference_model, include_signature=False492        )493        return property(get_reference)494 495    def _create_class_partial(496        factory_self, subresource_model, resource_name, service_context497    ):498        """499        Creates a new method which acts as a functools.partial, passing500        along the instance's low-level `client` to the new resource501        class' constructor.502        """503        name = subresource_model.resource.type504 505        def create_resource(self, *args, **kwargs):506            # We need a new method here because we want access to the507            # instance's client.508            positional_args = []509 510            # We lazy-load the class to handle circular references.511            json_def = service_context.resource_json_definitions.get(name, {})512            resource_cls = factory_self.load_from_definition(513                resource_name=name,514                single_resource_json_definition=json_def,515                service_context=service_context,516            )517 518            # Assumes that identifiers are in order, which lets you do519            # e.g. ``sqs.Queue('foo').Message('bar')`` to create a new message520            # linked with the ``foo`` queue and which has a ``bar`` receipt521            # handle. If we did kwargs here then future positional arguments522            # would lead to failure.523            identifiers = subresource_model.resource.identifiers524            if identifiers is not None:525                for identifier, value in build_identifiers(identifiers, self):526                    positional_args.append(value)527 528            return partial(529                resource_cls, *positional_args, client=self.meta.client530            )(*args, **kwargs)531 532        create_resource.__name__ = str(name)533        create_resource.__doc__ = docstring.SubResourceDocstring(534            resource_name=resource_name,535            sub_resource_model=subresource_model,536            service_model=service_context.service_model,537            include_signature=False,538        )539        return create_resource540 541    def _create_action(542        factory_self,543        action_model,544        resource_name,545        service_context,546        is_load=False,547    ):548        """549        Creates a new method which makes a request to the underlying550        AWS service.551        """552        # Create the action in in this closure but before the ``do_action``553        # method below is invoked, which allows instances of the resource554        # to share the ServiceAction instance.555        action = ServiceAction(556            action_model, factory=factory_self, service_context=service_context557        )558 559        # A resource's ``load`` method is special because it sets560        # values on the resource instead of returning the response.561        if is_load:562            # We need a new method here because we want access to the563            # instance via ``self``.564            def do_action(self, *args, **kwargs):565                response = action(self, *args, **kwargs)566                self.meta.data = response567 568            # Create the docstring for the load/reload methods.569            lazy_docstring = docstring.LoadReloadDocstring(570                action_name=action_model.name,571                resource_name=resource_name,572                event_emitter=factory_self._emitter,573                load_model=action_model,574                service_model=service_context.service_model,575                include_signature=False,576            )577        else:578            # We need a new method here because we want access to the579            # instance via ``self``.580            def do_action(self, *args, **kwargs):581                response = action(self, *args, **kwargs)582 583                if hasattr(self, 'load'):584                    # Clear cached data. It will be reloaded the next585                    # time that an attribute is accessed.586                    # TODO: Make this configurable in the future?587                    self.meta.data = None588 589                return response590 591            lazy_docstring = docstring.ActionDocstring(592                resource_name=resource_name,593                event_emitter=factory_self._emitter,594                action_model=action_model,595                service_model=service_context.service_model,596                include_signature=False,597            )598 599        do_action.__name__ = str(action_model.name)600        do_action.__doc__ = lazy_docstring601        return do_action602 
codekingpro/portable-devtools · Team Ai