Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/boto3/resources/model.py: 25%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 f'Problem renaming {self.name} {category} to {name}!'
378 )
380 names.add(name)
382 def _get_name(self, category, name, snake_case=True):
383 """
384 Get a possibly renamed value given a category and name. This
385 uses the rename map set up in ``load_rename_map``, so that
386 method must be called once first.
388 :type category: string
389 :param category: The value type, such as 'identifier' or 'action'
390 :type name: string
391 :param name: The original name of the value
392 :type snake_case: bool
393 :param snake_case: True (default) if the name should be snake cased.
394 :rtype: string
395 :return: Either the renamed value if it is set, otherwise the
396 original name.
397 """
398 if snake_case:
399 name = xform_name(name)
401 return self._renamed.get((category, name), name)
403 def get_attributes(self, shape):
404 """
405 Get a dictionary of attribute names to original name and shape
406 models that represent the attributes of this resource. Looks
407 like the following:
409 {
410 'some_name': ('SomeName', <Shape...>)
411 }
413 :type shape: botocore.model.Shape
414 :param shape: The underlying shape for this resource.
415 :rtype: dict
416 :return: Mapping of resource attributes.
417 """
418 attributes = {}
419 identifier_names = [i.name for i in self.identifiers]
421 for name, member in shape.members.items():
422 snake_cased = xform_name(name)
423 if snake_cased in identifier_names:
424 # Skip identifiers, these are set through other means
425 continue
426 snake_cased = self._get_name(
427 'attribute', snake_cased, snake_case=False
428 )
429 attributes[snake_cased] = (name, member)
431 return attributes
433 @property
434 def identifiers(self):
435 """
436 Get a list of resource identifiers.
438 :type: list(:py:class:`Identifier`)
439 """
440 identifiers = []
442 for item in self._definition.get('identifiers', []):
443 name = self._get_name('identifier', item['name'])
444 member_name = item.get('memberName', None)
445 if member_name:
446 member_name = self._get_name('attribute', member_name)
447 identifiers.append(Identifier(name, member_name))
449 return identifiers
451 @property
452 def load(self):
453 """
454 Get the load action for this resource, if it is defined.
456 :type: :py:class:`Action` or ``None``
457 """
458 action = self._definition.get('load')
460 if action is not None:
461 action = Action('load', action, self._resource_defs)
463 return action
465 @property
466 def actions(self):
467 """
468 Get a list of actions for this resource.
470 :type: list(:py:class:`Action`)
471 """
472 actions = []
474 for name, item in self._definition.get('actions', {}).items():
475 name = self._get_name('action', name)
476 actions.append(Action(name, item, self._resource_defs))
478 return actions
480 @property
481 def batch_actions(self):
482 """
483 Get a list of batch actions for this resource.
485 :type: list(:py:class:`Action`)
486 """
487 actions = []
489 for name, item in self._definition.get('batchActions', {}).items():
490 name = self._get_name('batch_action', name)
491 actions.append(Action(name, item, self._resource_defs))
493 return actions
495 def _get_has_definition(self):
496 """
497 Get a ``has`` relationship definition from a model, where the
498 service resource model is treated special in that it contains
499 a relationship to every resource defined for the service. This
500 allows things like ``s3.Object('bucket-name', 'key')`` to
501 work even though the JSON doesn't define it explicitly.
503 :rtype: dict
504 :return: Mapping of names to subresource and reference
505 definitions.
506 """
507 if self.name not in self._resource_defs:
508 # This is the service resource, so let us expose all of
509 # the defined resources as subresources.
510 definition = {}
512 for name, resource_def in self._resource_defs.items():
513 # It's possible for the service to have renamed a
514 # resource or to have defined multiple names that
515 # point to the same resource type, so we need to
516 # take that into account.
517 found = False
518 has_items = self._definition.get('has', {}).items()
519 for has_name, has_def in has_items:
520 if has_def.get('resource', {}).get('type') == name:
521 definition[has_name] = has_def
522 found = True
524 if not found:
525 # Create a relationship definition and attach it
526 # to the model, such that all identifiers must be
527 # supplied by the user. It will look something like:
528 #
529 # {
530 # 'resource': {
531 # 'type': 'ResourceName',
532 # 'identifiers': [
533 # {'target': 'Name1', 'source': 'input'},
534 # {'target': 'Name2', 'source': 'input'},
535 # ...
536 # ]
537 # }
538 # }
539 #
540 fake_has = {'resource': {'type': name, 'identifiers': []}}
542 for identifier in resource_def.get('identifiers', []):
543 fake_has['resource']['identifiers'].append(
544 {'target': identifier['name'], 'source': 'input'}
545 )
547 definition[name] = fake_has
548 else:
549 definition = self._definition.get('has', {})
551 return definition
553 def _get_related_resources(self, subresources):
554 """
555 Get a list of sub-resources or references.
557 :type subresources: bool
558 :param subresources: ``True`` to get sub-resources, ``False`` to
559 get references.
560 :rtype: list(:py:class:`Action`)
561 """
562 resources = []
564 for name, definition in self._get_has_definition().items():
565 if subresources:
566 name = self._get_name('subresource', name, snake_case=False)
567 else:
568 name = self._get_name('reference', name)
569 action = Action(name, definition, self._resource_defs)
571 data_required = False
572 for identifier in action.resource.identifiers:
573 if identifier.source == 'data':
574 data_required = True
575 break
577 if subresources and not data_required:
578 resources.append(action)
579 elif not subresources and data_required:
580 resources.append(action)
582 return resources
584 @property
585 def subresources(self):
586 """
587 Get a list of sub-resources.
589 :type: list(:py:class:`Action`)
590 """
591 return self._get_related_resources(True)
593 @property
594 def references(self):
595 """
596 Get a list of reference resources.
598 :type: list(:py:class:`Action`)
599 """
600 return self._get_related_resources(False)
602 @property
603 def collections(self):
604 """
605 Get a list of collections for this resource.
607 :type: list(:py:class:`Collection`)
608 """
609 collections = []
611 for name, item in self._definition.get('hasMany', {}).items():
612 name = self._get_name('collection', name)
613 collections.append(Collection(name, item, self._resource_defs))
615 return collections
617 @property
618 def waiters(self):
619 """
620 Get a list of waiters for this resource.
622 :type: list(:py:class:`Waiter`)
623 """
624 waiters = []
626 for name, item in self._definition.get('waiters', {}).items():
627 name = self._get_name('waiter', Waiter.PREFIX + name)
628 waiters.append(Waiter(name, item))
630 return waiters