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.
13
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:
18
19* Pythonic interface (e.g. ``action.request.operation``)
20* Consumers need not change for minor JSON changes (e.g. renamed field)
21
22These models are used both by the resource factory to generate resource
23classes as well as by the documentation generator.
24"""
25
26import logging
27
28from botocore import xform_name
29
30logger = logging.getLogger(__name__)
31
32
33class Identifier:
34 """
35 A resource identifier, given by its name.
36
37 :type name: string
38 :param name: The name of the identifier
39 """
40
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
45
46
47class Action:
48 """
49 A service operation action.
50
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 """
58
59 def __init__(self, name, definition, resource_defs):
60 self._definition = definition
61
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')
76
77
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.
83
84 :type definition: dict
85 :param definition: The JSON definition
86 """
87
88 def __init__(self, definition):
89 self._definition = definition
90
91 @property
92 def params(self):
93 """
94 Get a list of auto-filled parameters for this request.
95
96 :type: list(:py:class:`Parameter`)
97 """
98 params = []
99
100 for item in self._definition.get('params', []):
101 params.append(Parameter(**item))
102
103 return params
104
105
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``.
111
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 """
119
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
133
134 # Complain if we encounter any unknown values.
135 if kwargs:
136 logger.warning('Unknown parameter options found: %s', kwargs)
137
138
139class Request(DefinitionWithParams):
140 """
141 A service operation action request.
142
143 :type definition: dict
144 :param definition: The JSON definition
145 """
146
147 def __init__(self, definition):
148 super().__init__(definition)
149
150 #: (``string``) The name of the low-level service operation
151 self.operation = definition.get('operation')
152
153
154class Waiter(DefinitionWithParams):
155 """
156 An event waiter specification.
157
158 :type name: string
159 :param name: Name of the waiter
160 :type definition: dict
161 :param definition: The JSON definition
162 """
163
164 PREFIX = 'WaitUntil'
165
166 def __init__(self, name, definition):
167 super().__init__(definition)
168
169 #: (``string``) The name of this waiter
170 self.name = name
171
172 #: (``string``) The name of the underlying event waiter
173 self.waiter_name = definition.get('waiterName')
174
175
176class ResponseResource:
177 """
178 A resource response to create after performing an action.
179
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 """
185
186 def __init__(self, definition, resource_defs):
187 self._definition = definition
188 self._resource_defs = resource_defs
189
190 #: (``string``) The name of the response resource type
191 self.type = definition.get('type')
192
193 #: (``string``) The JMESPath search query or ``None``
194 self.path = definition.get('path')
195
196 @property
197 def identifiers(self):
198 """
199 A list of resource identifiers.
200
201 :type: list(:py:class:`Identifier`)
202 """
203 identifiers = []
204
205 for item in self._definition.get('identifiers', []):
206 identifiers.append(Parameter(**item))
207
208 return identifiers
209
210 @property
211 def model(self):
212 """
213 Get the resource model for the response resource.
214
215 :type: :py:class:`ResourceModel`
216 """
217 return ResourceModel(
218 self.type, self._resource_defs[self.type], self._resource_defs
219 )
220
221
222class Collection(Action):
223 """
224 A group of resources. See :py:class:`Action`.
225
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 """
233
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.
240
241 :rtype: list(:py:class:`Action`)
242 """
243 return self.resource.model.batch_actions
244
245
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`.
252
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 """
260
261 def __init__(self, name, definition, resource_defs):
262 self._definition = definition
263 self._resource_defs = resource_defs
264 self._renamed = {}
265
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')
270
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:
280
281 * Load action (resource.load)
282 * Identifiers
283 * Actions
284 * Subresources
285 * References
286 * Collections
287 * Waiters
288 * Attributes (shape members)
289
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.
293
294 Creates a structure like this::
295
296 renames = {
297 ('action', 'id'): 'id_action',
298 ('collection', 'id'): 'id_collection',
299 ('attribute', 'id'): 'id_attribute'
300 }
301
302 # Get the final name for an action named 'id'
303 name = renames.get(('action', 'id'), 'id')
304
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 = {}
311
312 if self._definition.get('load'):
313 names.add('load')
314
315 for item in self._definition.get('identifiers', []):
316 self._load_name_with_category(names, item['name'], 'identifier')
317
318 for name in self._definition.get('actions', {}):
319 self._load_name_with_category(names, name, 'action')
320
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
329
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')
336
337 for name in self._definition.get('hasMany', {}):
338 self._load_name_with_category(names, name, 'collection')
339
340 for name in self._definition.get('waiters', {}):
341 self._load_name_with_category(
342 names, Waiter.PREFIX + name, 'waiter'
343 )
344
345 if shape is not None:
346 for name in shape.members.keys():
347 self._load_name_with_category(names, name, 'attribute')
348
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``.
354
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)
367
368 if name in names:
369 logger.debug(f'Renaming {self.name} {category} {name}')
370 self._renamed[(category, name)] = name + '_' + category
371 name += '_' + category
372
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 )
379
380 names.add(name)
381
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.
387
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)
400
401 return self._renamed.get((category, name), name)
402
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:
408
409 {
410 'some_name': ('SomeName', <Shape...>)
411 }
412
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]
420
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)
430
431 return attributes
432
433 @property
434 def identifiers(self):
435 """
436 Get a list of resource identifiers.
437
438 :type: list(:py:class:`Identifier`)
439 """
440 identifiers = []
441
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))
448
449 return identifiers
450
451 @property
452 def load(self):
453 """
454 Get the load action for this resource, if it is defined.
455
456 :type: :py:class:`Action` or ``None``
457 """
458 action = self._definition.get('load')
459
460 if action is not None:
461 action = Action('load', action, self._resource_defs)
462
463 return action
464
465 @property
466 def actions(self):
467 """
468 Get a list of actions for this resource.
469
470 :type: list(:py:class:`Action`)
471 """
472 actions = []
473
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))
477
478 return actions
479
480 @property
481 def batch_actions(self):
482 """
483 Get a list of batch actions for this resource.
484
485 :type: list(:py:class:`Action`)
486 """
487 actions = []
488
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))
492
493 return actions
494
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.
502
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 = {}
511
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
523
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': []}}
541
542 for identifier in resource_def.get('identifiers', []):
543 fake_has['resource']['identifiers'].append(
544 {'target': identifier['name'], 'source': 'input'}
545 )
546
547 definition[name] = fake_has
548 else:
549 definition = self._definition.get('has', {})
550
551 return definition
552
553 def _get_related_resources(self, subresources):
554 """
555 Get a list of sub-resources or references.
556
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 = []
563
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)
570
571 data_required = False
572 for identifier in action.resource.identifiers:
573 if identifier.source == 'data':
574 data_required = True
575 break
576
577 if subresources and not data_required:
578 resources.append(action)
579 elif not subresources and data_required:
580 resources.append(action)
581
582 return resources
583
584 @property
585 def subresources(self):
586 """
587 Get a list of sub-resources.
588
589 :type: list(:py:class:`Action`)
590 """
591 return self._get_related_resources(True)
592
593 @property
594 def references(self):
595 """
596 Get a list of reference resources.
597
598 :type: list(:py:class:`Action`)
599 """
600 return self._get_related_resources(False)
601
602 @property
603 def collections(self):
604 """
605 Get a list of collections for this resource.
606
607 :type: list(:py:class:`Collection`)
608 """
609 collections = []
610
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))
614
615 return collections
616
617 @property
618 def waiters(self):
619 """
620 Get a list of waiters for this resource.
621
622 :type: list(:py:class:`Waiter`)
623 """
624 waiters = []
625
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))
629
630 return waiters