Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/boto3/resources/model.py: 25%
206 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.
14"""
15The models defined in this file represent the resource JSON description
16format and provide a layer of abstraction from the raw JSON. The advantages
17of this are:
19* Pythonic interface (e.g. ``action.request.operation``)
20* Consumers need not change for minor JSON changes (e.g. renamed field)
22These models are used both by the resource factory to generate resource
23classes as well as by the documentation generator.
24"""
26import logging
28from botocore import xform_name
30logger = logging.getLogger(__name__)
33class Identifier:
34 """
35 A resource identifier, given by its name.
37 :type name: string
38 :param name: The name of the identifier
39 """
41 def __init__(self, name, member_name=None):
42 #: (``string``) The name of the identifier
43 self.name = name
44 self.member_name = member_name
47class Action:
48 """
49 A service operation action.
51 :type name: string
52 :param name: The name of the action
53 :type definition: dict
54 :param definition: The JSON definition
55 :type resource_defs: dict
56 :param resource_defs: All resources defined in the service
57 """
59 def __init__(self, name, definition, resource_defs):
60 self._definition = definition
62 #: (``string``) The name of the action
63 self.name = name
64 #: (:py:class:`Request`) This action's request or ``None``
65 self.request = None
66 if 'request' in definition:
67 self.request = Request(definition.get('request', {}))
68 #: (:py:class:`ResponseResource`) This action's resource or ``None``
69 self.resource = None
70 if 'resource' in definition:
71 self.resource = ResponseResource(
72 definition.get('resource', {}), resource_defs
73 )
74 #: (``string``) The JMESPath search path or ``None``
75 self.path = definition.get('path')
78class DefinitionWithParams:
79 """
80 An item which has parameters exposed via the ``params`` property.
81 A request has an operation and parameters, while a waiter has
82 a name, a low-level waiter name and parameters.
84 :type definition: dict
85 :param definition: The JSON definition
86 """
88 def __init__(self, definition):
89 self._definition = definition
91 @property
92 def params(self):
93 """
94 Get a list of auto-filled parameters for this request.
96 :type: list(:py:class:`Parameter`)
97 """
98 params = []
100 for item in self._definition.get('params', []):
101 params.append(Parameter(**item))
103 return params
106class Parameter:
107 """
108 An auto-filled parameter which has a source and target. For example,
109 the ``QueueUrl`` may be auto-filled from a resource's ``url`` identifier
110 when making calls to ``queue.receive_messages``.
112 :type target: string
113 :param target: The destination parameter name, e.g. ``QueueUrl``
114 :type source_type: string
115 :param source_type: Where the source is defined.
116 :type source: string
117 :param source: The source name, e.g. ``Url``
118 """
120 def __init__(
121 self, target, source, name=None, path=None, value=None, **kwargs
122 ):
123 #: (``string``) The destination parameter name
124 self.target = target
125 #: (``string``) Where the source is defined
126 self.source = source
127 #: (``string``) The name of the source, if given
128 self.name = name
129 #: (``string``) The JMESPath query of the source
130 self.path = path
131 #: (``string|int|float|bool``) The source constant value
132 self.value = value
134 # Complain if we encounter any unknown values.
135 if kwargs:
136 logger.warning('Unknown parameter options found: %s', kwargs)
139class Request(DefinitionWithParams):
140 """
141 A service operation action request.
143 :type definition: dict
144 :param definition: The JSON definition
145 """
147 def __init__(self, definition):
148 super().__init__(definition)
150 #: (``string``) The name of the low-level service operation
151 self.operation = definition.get('operation')
154class Waiter(DefinitionWithParams):
155 """
156 An event waiter specification.
158 :type name: string
159 :param name: Name of the waiter
160 :type definition: dict
161 :param definition: The JSON definition
162 """
164 PREFIX = 'WaitUntil'
166 def __init__(self, name, definition):
167 super().__init__(definition)
169 #: (``string``) The name of this waiter
170 self.name = name
172 #: (``string``) The name of the underlying event waiter
173 self.waiter_name = definition.get('waiterName')
176class ResponseResource:
177 """
178 A resource response to create after performing an action.
180 :type definition: dict
181 :param definition: The JSON definition
182 :type resource_defs: dict
183 :param resource_defs: All resources defined in the service
184 """
186 def __init__(self, definition, resource_defs):
187 self._definition = definition
188 self._resource_defs = resource_defs
190 #: (``string``) The name of the response resource type
191 self.type = definition.get('type')
193 #: (``string``) The JMESPath search query or ``None``
194 self.path = definition.get('path')
196 @property
197 def identifiers(self):
198 """
199 A list of resource identifiers.
201 :type: list(:py:class:`Identifier`)
202 """
203 identifiers = []
205 for item in self._definition.get('identifiers', []):
206 identifiers.append(Parameter(**item))
208 return identifiers
210 @property
211 def model(self):
212 """
213 Get the resource model for the response resource.
215 :type: :py:class:`ResourceModel`
216 """
217 return ResourceModel(
218 self.type, self._resource_defs[self.type], self._resource_defs
219 )
222class Collection(Action):
223 """
224 A group of resources. See :py:class:`Action`.
226 :type name: string
227 :param name: The name of the collection
228 :type definition: dict
229 :param definition: The JSON definition
230 :type resource_defs: dict
231 :param resource_defs: All resources defined in the service
232 """
234 @property
235 def batch_actions(self):
236 """
237 Get a list of batch actions supported by the resource type
238 contained in this action. This is a shortcut for accessing
239 the same information through the resource model.
241 :rtype: list(:py:class:`Action`)
242 """
243 return self.resource.model.batch_actions
246class ResourceModel:
247 """
248 A model representing a resource, defined via a JSON description
249 format. A resource has identifiers, attributes, actions,
250 sub-resources, references and collections. For more information
251 on resources, see :ref:`guide_resources`.
253 :type name: string
254 :param name: The name of this resource, e.g. ``sqs`` or ``Queue``
255 :type definition: dict
256 :param definition: The JSON definition
257 :type resource_defs: dict
258 :param resource_defs: All resources defined in the service
259 """
261 def __init__(self, name, definition, resource_defs):
262 self._definition = definition
263 self._resource_defs = resource_defs
264 self._renamed = {}
266 #: (``string``) The name of this resource
267 self.name = name
268 #: (``string``) The service shape name for this resource or ``None``
269 self.shape = definition.get('shape')
271 def load_rename_map(self, shape=None):
272 """
273 Load a name translation map given a shape. This will set
274 up renamed values for any collisions, e.g. if the shape,
275 an action, and a subresource all are all named ``foo``
276 then the resource will have an action ``foo``, a subresource
277 named ``Foo`` and a property named ``foo_attribute``.
278 This is the order of precedence, from most important to
279 least important:
281 * Load action (resource.load)
282 * Identifiers
283 * Actions
284 * Subresources
285 * References
286 * Collections
287 * Waiters
288 * Attributes (shape members)
290 Batch actions are only exposed on collections, so do not
291 get modified here. Subresources use upper camel casing, so
292 are unlikely to collide with anything but other subresources.
294 Creates a structure like this::
296 renames = {
297 ('action', 'id'): 'id_action',
298 ('collection', 'id'): 'id_collection',
299 ('attribute', 'id'): 'id_attribute'
300 }
302 # Get the final name for an action named 'id'
303 name = renames.get(('action', 'id'), 'id')
305 :type shape: botocore.model.Shape
306 :param shape: The underlying shape for this resource.
307 """
308 # Meta is a reserved name for resources
309 names = {'meta'}
310 self._renamed = {}
312 if self._definition.get('load'):
313 names.add('load')
315 for item in self._definition.get('identifiers', []):
316 self._load_name_with_category(names, item['name'], 'identifier')
318 for name in self._definition.get('actions', {}):
319 self._load_name_with_category(names, name, 'action')
321 for name, ref in self._get_has_definition().items():
322 # Subresources require no data members, just typically
323 # identifiers and user input.
324 data_required = False
325 for identifier in ref['resource']['identifiers']:
326 if identifier['source'] == 'data':
327 data_required = True
328 break
330 if not data_required:
331 self._load_name_with_category(
332 names, name, 'subresource', snake_case=False
333 )
334 else:
335 self._load_name_with_category(names, name, 'reference')
337 for name in self._definition.get('hasMany', {}):
338 self._load_name_with_category(names, name, 'collection')
340 for name in self._definition.get('waiters', {}):
341 self._load_name_with_category(
342 names, Waiter.PREFIX + name, 'waiter'
343 )
345 if shape is not None:
346 for name in shape.members.keys():
347 self._load_name_with_category(names, name, 'attribute')
349 def _load_name_with_category(self, names, name, category, snake_case=True):
350 """
351 Load a name with a given category, possibly renaming it
352 if that name is already in use. The name will be stored
353 in ``names`` and possibly be set up in ``self._renamed``.
355 :type names: set
356 :param names: Existing names (Python attributes, properties, or
357 methods) on the resource.
358 :type name: string
359 :param name: The original name of the value.
360 :type category: string
361 :param category: The value type, such as 'identifier' or 'action'
362 :type snake_case: bool
363 :param snake_case: True (default) if the name should be snake cased.
364 """
365 if snake_case:
366 name = xform_name(name)
368 if name in names:
369 logger.debug(f'Renaming {self.name} {category} {name}')
370 self._renamed[(category, name)] = name + '_' + category
371 name += '_' + category
373 if name in names:
374 # This isn't good, let's raise instead of trying to keep
375 # renaming this value.
376 raise ValueError(
377 'Problem renaming {} {} to {}!'.format(
378 self.name, category, name
379 )
380 )
382 names.add(name)
384 def _get_name(self, category, name, snake_case=True):
385 """
386 Get a possibly renamed value given a category and name. This
387 uses the rename map set up in ``load_rename_map``, so that
388 method must be called once first.
390 :type category: string
391 :param category: The value type, such as 'identifier' or 'action'
392 :type name: string
393 :param name: The original name of the value
394 :type snake_case: bool
395 :param snake_case: True (default) if the name should be snake cased.
396 :rtype: string
397 :return: Either the renamed value if it is set, otherwise the
398 original name.
399 """
400 if snake_case:
401 name = xform_name(name)
403 return self._renamed.get((category, name), name)
405 def get_attributes(self, shape):
406 """
407 Get a dictionary of attribute names to original name and shape
408 models that represent the attributes of this resource. Looks
409 like the following:
411 {
412 'some_name': ('SomeName', <Shape...>)
413 }
415 :type shape: botocore.model.Shape
416 :param shape: The underlying shape for this resource.
417 :rtype: dict
418 :return: Mapping of resource attributes.
419 """
420 attributes = {}
421 identifier_names = [i.name for i in self.identifiers]
423 for name, member in shape.members.items():
424 snake_cased = xform_name(name)
425 if snake_cased in identifier_names:
426 # Skip identifiers, these are set through other means
427 continue
428 snake_cased = self._get_name(
429 'attribute', snake_cased, snake_case=False
430 )
431 attributes[snake_cased] = (name, member)
433 return attributes
435 @property
436 def identifiers(self):
437 """
438 Get a list of resource identifiers.
440 :type: list(:py:class:`Identifier`)
441 """
442 identifiers = []
444 for item in self._definition.get('identifiers', []):
445 name = self._get_name('identifier', item['name'])
446 member_name = item.get('memberName', None)
447 if member_name:
448 member_name = self._get_name('attribute', member_name)
449 identifiers.append(Identifier(name, member_name))
451 return identifiers
453 @property
454 def load(self):
455 """
456 Get the load action for this resource, if it is defined.
458 :type: :py:class:`Action` or ``None``
459 """
460 action = self._definition.get('load')
462 if action is not None:
463 action = Action('load', action, self._resource_defs)
465 return action
467 @property
468 def actions(self):
469 """
470 Get a list of actions for this resource.
472 :type: list(:py:class:`Action`)
473 """
474 actions = []
476 for name, item in self._definition.get('actions', {}).items():
477 name = self._get_name('action', name)
478 actions.append(Action(name, item, self._resource_defs))
480 return actions
482 @property
483 def batch_actions(self):
484 """
485 Get a list of batch actions for this resource.
487 :type: list(:py:class:`Action`)
488 """
489 actions = []
491 for name, item in self._definition.get('batchActions', {}).items():
492 name = self._get_name('batch_action', name)
493 actions.append(Action(name, item, self._resource_defs))
495 return actions
497 def _get_has_definition(self):
498 """
499 Get a ``has`` relationship definition from a model, where the
500 service resource model is treated special in that it contains
501 a relationship to every resource defined for the service. This
502 allows things like ``s3.Object('bucket-name', 'key')`` to
503 work even though the JSON doesn't define it explicitly.
505 :rtype: dict
506 :return: Mapping of names to subresource and reference
507 definitions.
508 """
509 if self.name not in self._resource_defs:
510 # This is the service resource, so let us expose all of
511 # the defined resources as subresources.
512 definition = {}
514 for name, resource_def in self._resource_defs.items():
515 # It's possible for the service to have renamed a
516 # resource or to have defined multiple names that
517 # point to the same resource type, so we need to
518 # take that into account.
519 found = False
520 has_items = self._definition.get('has', {}).items()
521 for has_name, has_def in has_items:
522 if has_def.get('resource', {}).get('type') == name:
523 definition[has_name] = has_def
524 found = True
526 if not found:
527 # Create a relationship definition and attach it
528 # to the model, such that all identifiers must be
529 # supplied by the user. It will look something like:
530 #
531 # {
532 # 'resource': {
533 # 'type': 'ResourceName',
534 # 'identifiers': [
535 # {'target': 'Name1', 'source': 'input'},
536 # {'target': 'Name2', 'source': 'input'},
537 # ...
538 # ]
539 # }
540 # }
541 #
542 fake_has = {'resource': {'type': name, 'identifiers': []}}
544 for identifier in resource_def.get('identifiers', []):
545 fake_has['resource']['identifiers'].append(
546 {'target': identifier['name'], 'source': 'input'}
547 )
549 definition[name] = fake_has
550 else:
551 definition = self._definition.get('has', {})
553 return definition
555 def _get_related_resources(self, subresources):
556 """
557 Get a list of sub-resources or references.
559 :type subresources: bool
560 :param subresources: ``True`` to get sub-resources, ``False`` to
561 get references.
562 :rtype: list(:py:class:`Action`)
563 """
564 resources = []
566 for name, definition in self._get_has_definition().items():
567 if subresources:
568 name = self._get_name('subresource', name, snake_case=False)
569 else:
570 name = self._get_name('reference', name)
571 action = Action(name, definition, self._resource_defs)
573 data_required = False
574 for identifier in action.resource.identifiers:
575 if identifier.source == 'data':
576 data_required = True
577 break
579 if subresources and not data_required:
580 resources.append(action)
581 elif not subresources and data_required:
582 resources.append(action)
584 return resources
586 @property
587 def subresources(self):
588 """
589 Get a list of sub-resources.
591 :type: list(:py:class:`Action`)
592 """
593 return self._get_related_resources(True)
595 @property
596 def references(self):
597 """
598 Get a list of reference resources.
600 :type: list(:py:class:`Action`)
601 """
602 return self._get_related_resources(False)
604 @property
605 def collections(self):
606 """
607 Get a list of collections for this resource.
609 :type: list(:py:class:`Collection`)
610 """
611 collections = []
613 for name, item in self._definition.get('hasMany', {}).items():
614 name = self._get_name('collection', name)
615 collections.append(Collection(name, item, self._resource_defs))
617 return collections
619 @property
620 def waiters(self):
621 """
622 Get a list of waiters for this resource.
624 :type: list(:py:class:`Waiter`)
625 """
626 waiters = []
628 for name, item in self._definition.get('waiters', {}).items():
629 name = self._get_name('waiter', Waiter.PREFIX + name)
630 waiters.append(Waiter(name, item))
632 return waiters