Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/boto3/resources/factory.py: 20%
152 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# https://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
14import logging
15from functools import partial
17from ..docs import docstring
18from ..exceptions import ResourceLoadException
19from .action import ServiceAction, WaiterAction
20from .base import ResourceMeta, ServiceResource
21from .collection import CollectionFactory
22from .model import ResourceModel
23from .response import ResourceHandler, build_identifiers
25logger = logging.getLogger(__name__)
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 are
32 two types of lookups that can be done: one on the service itself (e.g. an
33 SQS resource) and another on models contained within the service (e.g. an
34 SQS Queue resource).
35 """
37 def __init__(self, emitter):
38 self._collection_factory = CollectionFactory()
39 self._emitter = emitter
41 def load_from_definition(
42 self, resource_name, single_resource_json_definition, service_context
43 ):
44 """
45 Loads a resource from a model, creating a new
46 :py:class:`~boto3.resources.base.ServiceResource` subclass
47 with the correct properties and methods, named based on the service
48 and resource name, e.g. EC2.Instance.
50 :type resource_name: string
51 :param resource_name: Name of the resource to look up. For services,
52 this should match the ``service_name``.
54 :type single_resource_json_definition: dict
55 :param single_resource_json_definition:
56 The loaded json of a single service resource or resource
57 definition.
59 :type service_context: :py:class:`~boto3.utils.ServiceContext`
60 :param service_context: Context about the AWS service
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_name
67 )
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 )
76 # Do some renaming of the shape if there was a naming collision
77 # that needed to be accounted for.
78 shape = None
79 if resource_model.shape:
80 shape = service_context.service_model.shape_for(
81 resource_model.shape
82 )
83 resource_model.load_rename_map(shape)
85 # Set some basic info
86 meta = ResourceMeta(
87 service_context.service_name, resource_model=resource_model
88 )
89 attrs = {
90 'meta': meta,
91 }
93 # Create and load all of attributes of the resource class based
94 # on the models.
96 # Identifiers
97 self._load_identifiers(
98 attrs=attrs,
99 meta=meta,
100 resource_name=resource_name,
101 resource_model=resource_model,
102 )
104 # Load/Reload actions
105 self._load_actions(
106 attrs=attrs,
107 resource_name=resource_name,
108 resource_model=resource_model,
109 service_context=service_context,
110 )
112 # Attributes that get auto-loaded
113 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 )
121 # Collections and their corresponding methods
122 self._load_collections(
123 attrs=attrs,
124 resource_model=resource_model,
125 service_context=service_context,
126 )
128 # References and Subresources
129 self._load_has_relations(
130 attrs=attrs,
131 resource_name=resource_name,
132 resource_model=resource_model,
133 service_context=service_context,
134 )
136 # Waiter resource actions
137 self._load_waiters(
138 attrs=attrs,
139 resource_name=resource_name,
140 resource_model=resource_model,
141 service_context=service_context,
142 )
144 # Create the name based on the requested service and resource
145 cls_name = resource_name
146 if service_context.service_name == resource_name:
147 cls_name = 'ServiceResource'
148 cls_name = service_context.service_name + '.' + cls_name
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)
160 def _load_identifiers(self, attrs, meta, resource_model, resource_name):
161 """
162 Populate required identifiers. These are arguments without which
163 the resource cannot be used. Identifiers become arguments for
164 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_name
170 )
172 def _load_actions(
173 self, attrs, resource_name, resource_model, service_context
174 ):
175 """
176 Actions on the resource become methods, with the ``load`` method
177 being a special case which sets internal data for attributes, and
178 ``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']
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 )
196 def _load_attributes(
197 self, attrs, meta, resource_name, resource_model, service_context
198 ):
199 """
200 Load resource attributes based on the resource shape. The shape
201 name is referenced in the resource JSON, but the shape itself
202 is defined in the Botocore service JSON, hence the need for
203 access to the ``service_model``.
204 """
205 if not resource_model.shape:
206 return
208 shape = service_context.service_model.shape_for(resource_model.shape)
210 identifiers = {
211 i.member_name: i
212 for i in resource_model.identifiers
213 if i.member_name
214 }
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] = prop
234 def _load_collections(self, attrs, resource_model, service_context):
235 """
236 Load resource collections from the model. Each collection becomes
237 a :py:class:`~boto3.resources.collection.CollectionManager` instance
238 on the resource instance, which allows you to iterate and filter
239 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 )
248 def _load_has_relations(
249 self, attrs, resource_name, resource_model, service_context
250 ):
251 """
252 Load related resources, which are defined via a ``has``
253 relationship but conceptually come in two forms:
255 1. A reference, which is a related resource instance and can be
256 ``None``, such as an EC2 instance's ``vpc``.
257 2. A subresource, which is a resource constructor that will always
258 return a resource instance which shares identifiers/data with
259 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 all
263 # the data we need to create the resource, so
264 # 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 )
271 for subresource in resource_model.subresources:
272 # This is a sub-resource class you can create
273 # 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 )
280 self._create_available_subresources_command(
281 attrs, resource_model.subresources
282 )
284 def _create_available_subresources_command(self, attrs, subresources):
285 _subresources = [subresource.name for subresource in subresources]
286 _subresources = sorted(_subresources)
288 def get_available_subresources(factory_self):
289 """
290 Returns a list of all the available sub-resources for this
291 Resource.
293 :returns: A list containing the name of each sub-resource for this
294 resource
295 :rtype: list of str
296 """
297 return _subresources
299 attrs['get_available_subresources'] = get_available_subresources
301 def _load_waiters(
302 self, attrs, resource_name, resource_model, service_context
303 ):
304 """
305 Load resource waiters from the model. Each waiter allows you to
306 wait until a resource reaches a specific state by polling the state
307 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 )
316 def _create_identifier(factory_self, identifier, resource_name):
317 """
318 Creates a read-only property for identifier attributes.
319 """
321 def get_identifier(self):
322 # The default value is set to ``None`` instead of
323 # raising an AttributeError because when resources are
324 # instantiated a check is made such that none of the
325 # identifiers have a value ``None``. If any are ``None``,
326 # a more informative user error than a generic AttributeError
327 # is raised.
328 return getattr(self, '_' + identifier.name, None)
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 )
337 return property(get_identifier)
339 def _create_identifier_alias(
340 factory_self, resource_name, identifier, member_model, service_context
341 ):
342 """
343 Creates a read-only property that aliases an identifier.
344 """
346 def get_identifier(self):
347 return getattr(self, '_' + identifier.name, None)
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 )
359 return property(get_identifier)
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 value
371 via the resource's ``load`` method (if it exists).
372 """
374 # The property loader will check to see if this resource has already
375 # been loaded and return the cached value if possible. If not, then
376 # it first checks to see if it CAN be loaded (raise if not), then
377 # 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 )
387 return self.meta.data.get(name)
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 )
399 return property(property_loader)
401 def _create_waiter(
402 factory_self, resource_waiter_model, resource_name, service_context
403 ):
404 """
405 Creates a new wait method for each resource where both a waiter and
406 resource model is defined.
407 """
408 waiter = WaiterAction(
409 resource_waiter_model,
410 waiter_resource_name=resource_waiter_model.name,
411 )
413 def do_waiter(self, *args, **kwargs):
414 waiter(self, *args, **kwargs)
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_waiter
427 def _create_collection(
428 factory_self, resource_name, collection_model, service_context
429 ):
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 )
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 )
448 get_collection.__name__ = str(collection_model.name)
449 get_collection.__doc__ = docstring.CollectionDocstring(
450 collection_model=collection_model, include_signature=False
451 )
452 return property(get_collection)
454 def _create_reference(
455 factory_self, reference_model, resource_name, service_context
456 ):
457 """
458 Creates a new property on the resource to lazy-load a reference.
459 """
460 # References are essentially an action with no request
461 # or response, so we can re-use the response handlers to
462 # 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 )
470 # Are there any identifiers that need access to data members?
471 # This is important when building the resource below since
472 # it requires the data to be loaded.
473 needs_data = any(
474 i.source == 'data' for i in reference_model.resource.identifiers
475 )
477 def get_reference(self):
478 # We need to lazy-evaluate the reference to handle circular
479 # references between resources. We do this by loading the class
480 # when first accessed.
481 # This is using a *response handler* so we need to make sure
482 # our data is loaded (if possible) and pass that data into
483 # the handler as if it were a response. This allows references
484 # 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)
489 get_reference.__name__ = str(reference_model.name)
490 get_reference.__doc__ = docstring.ReferenceDocstring(
491 reference_model=reference_model, include_signature=False
492 )
493 return property(get_reference)
495 def _create_class_partial(
496 factory_self, subresource_model, resource_name, service_context
497 ):
498 """
499 Creates a new method which acts as a functools.partial, passing
500 along the instance's low-level `client` to the new resource
501 class' constructor.
502 """
503 name = subresource_model.resource.type
505 def create_resource(self, *args, **kwargs):
506 # We need a new method here because we want access to the
507 # instance's client.
508 positional_args = []
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 )
518 # Assumes that identifiers are in order, which lets you do
519 # e.g. ``sqs.Queue('foo').Message('bar')`` to create a new message
520 # linked with the ``foo`` queue and which has a ``bar`` receipt
521 # handle. If we did kwargs here then future positional arguments
522 # would lead to failure.
523 identifiers = subresource_model.resource.identifiers
524 if identifiers is not None:
525 for identifier, value in build_identifiers(identifiers, self):
526 positional_args.append(value)
528 return partial(
529 resource_cls, *positional_args, client=self.meta.client
530 )(*args, **kwargs)
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_resource
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 underlying
550 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 resource
554 # to share the ServiceAction instance.
555 action = ServiceAction(
556 action_model, factory=factory_self, service_context=service_context
557 )
559 # A resource's ``load`` method is special because it sets
560 # 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 the
563 # instance via ``self``.
564 def do_action(self, *args, **kwargs):
565 response = action(self, *args, **kwargs)
566 self.meta.data = response
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 the
579 # instance via ``self``.
580 def do_action(self, *args, **kwargs):
581 response = action(self, *args, **kwargs)
583 if hasattr(self, 'load'):
584 # Clear cached data. It will be reloaded the next
585 # time that an attribute is accessed.
586 # TODO: Make this configurable in the future?
587 self.meta.data = None
589 return response
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 )
599 do_action.__name__ = str(action_model.name)
600 do_action.__doc__ = lazy_docstring
601 return do_action