codekingpro/portable-devtools
114k
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 14"""15The models defined in this file represent the resource JSON description16format and provide a layer of abstraction from the raw JSON. The advantages17of 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 resource23classes as well as by the documentation generator.24"""25 26import logging27 28from botocore import xform_name29 30logger = logging.getLogger(__name__)31 32 33class Identifier:34 """35 A resource identifier, given by its name.36 37 :type name: string38 :param name: The name of the identifier39 """40 41 def __init__(self, name, member_name=None):42 #: (``string``) The name of the identifier43 self.name = name44 self.member_name = member_name45 46 47class Action:48 """49 A service operation action.50 51 :type name: string52 :param name: The name of the action53 :type definition: dict54 :param definition: The JSON definition55 :type resource_defs: dict56 :param resource_defs: All resources defined in the service57 """58 59 def __init__(self, name, definition, resource_defs):60 self._definition = definition61 62 #: (``string``) The name of the action63 self.name = name64 #: (:py:class:`Request`) This action's request or ``None``65 self.request = None66 if 'request' in definition:67 self.request = Request(definition.get('request', {}))68 #: (:py:class:`ResponseResource`) This action's resource or ``None``69 self.resource = None70 if 'resource' in definition:71 self.resource = ResponseResource(72 definition.get('resource', {}), resource_defs73 )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 has82 a name, a low-level waiter name and parameters.83 84 :type definition: dict85 :param definition: The JSON definition86 """87 88 def __init__(self, definition):89 self._definition = definition90 91 @property92 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 params104 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`` identifier110 when making calls to ``queue.receive_messages``.111 112 :type target: string113 :param target: The destination parameter name, e.g. ``QueueUrl``114 :type source_type: string115 :param source_type: Where the source is defined.116 :type source: string117 :param source: The source name, e.g. ``Url``118 """119 120 def __init__(121 self, target, source, name=None, path=None, value=None, **kwargs122 ):123 #: (``string``) The destination parameter name124 self.target = target125 #: (``string``) Where the source is defined126 self.source = source127 #: (``string``) The name of the source, if given128 self.name = name129 #: (``string``) The JMESPath query of the source130 self.path = path131 #: (``string|int|float|bool``) The source constant value132 self.value = value133 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: dict144 :param definition: The JSON definition145 """146 147 def __init__(self, definition):148 super().__init__(definition)149 150 #: (``string``) The name of the low-level service operation151 self.operation = definition.get('operation')152 153 154class Waiter(DefinitionWithParams):155 """156 An event waiter specification.157 158 :type name: string159 :param name: Name of the waiter160 :type definition: dict161 :param definition: The JSON definition162 """163 164 PREFIX = 'WaitUntil'165 166 def __init__(self, name, definition):167 super().__init__(definition)168 169 #: (``string``) The name of this waiter170 self.name = name171 172 #: (``string``) The name of the underlying event waiter173 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: dict181 :param definition: The JSON definition182 :type resource_defs: dict183 :param resource_defs: All resources defined in the service184 """185 186 def __init__(self, definition, resource_defs):187 self._definition = definition188 self._resource_defs = resource_defs189 190 #: (``string``) The name of the response resource type191 self.type = definition.get('type')192 193 #: (``string``) The JMESPath search query or ``None``194 self.path = definition.get('path')195 196 @property197 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 identifiers209 210 @property211 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_defs219 )220 221 222class Collection(Action):223 """224 A group of resources. See :py:class:`Action`.225 226 :type name: string227 :param name: The name of the collection228 :type definition: dict229 :param definition: The JSON definition230 :type resource_defs: dict231 :param resource_defs: All resources defined in the service232 """233 234 @property235 def batch_actions(self):236 """237 Get a list of batch actions supported by the resource type238 contained in this action. This is a shortcut for accessing239 the same information through the resource model.240 241 :rtype: list(:py:class:`Action`)242 """243 return self.resource.model.batch_actions244 245 246class ResourceModel:247 """248 A model representing a resource, defined via a JSON description249 format. A resource has identifiers, attributes, actions,250 sub-resources, references and collections. For more information251 on resources, see :ref:`guide_resources`.252 253 :type name: string254 :param name: The name of this resource, e.g. ``sqs`` or ``Queue``255 :type definition: dict256 :param definition: The JSON definition257 :type resource_defs: dict258 :param resource_defs: All resources defined in the service259 """260 261 def __init__(self, name, definition, resource_defs):262 self._definition = definition263 self._resource_defs = resource_defs264 self._renamed = {}265 266 #: (``string``) The name of this resource267 self.name = name268 #: (``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 set274 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 subresource277 named ``Foo`` and a property named ``foo_attribute``.278 This is the order of precedence, from most important to279 least important:280 281 * Load action (resource.load)282 * Identifiers283 * Actions284 * Subresources285 * References286 * Collections287 * Waiters288 * Attributes (shape members)289 290 Batch actions are only exposed on collections, so do not291 get modified here. Subresources use upper camel casing, so292 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.Shape306 :param shape: The underlying shape for this resource.307 """308 # Meta is a reserved name for resources309 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 typically323 # identifiers and user input.324 data_required = False325 for identifier in ref['resource']['identifiers']:326 if identifier['source'] == 'data':327 data_required = True328 break329 330 if not data_required:331 self._load_name_with_category(332 names, name, 'subresource', snake_case=False333 )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 it352 if that name is already in use. The name will be stored353 in ``names`` and possibly be set up in ``self._renamed``.354 355 :type names: set356 :param names: Existing names (Python attributes, properties, or357 methods) on the resource.358 :type name: string359 :param name: The original name of the value.360 :type category: string361 :param category: The value type, such as 'identifier' or 'action'362 :type snake_case: bool363 :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 + '_' + category371 name += '_' + category372 373 if name in names:374 # This isn't good, let's raise instead of trying to keep375 # renaming this value.376 raise ValueError(377 'Problem renaming {} {} to {}!'.format(378 self.name, category, name379 )380 )381 382 names.add(name)383 384 def _get_name(self, category, name, snake_case=True):385 """386 Get a possibly renamed value given a category and name. This387 uses the rename map set up in ``load_rename_map``, so that388 method must be called once first.389 390 :type category: string391 :param category: The value type, such as 'identifier' or 'action'392 :type name: string393 :param name: The original name of the value394 :type snake_case: bool395 :param snake_case: True (default) if the name should be snake cased.396 :rtype: string397 :return: Either the renamed value if it is set, otherwise the398 original name.399 """400 if snake_case:401 name = xform_name(name)402 403 return self._renamed.get((category, name), name)404 405 def get_attributes(self, shape):406 """407 Get a dictionary of attribute names to original name and shape408 models that represent the attributes of this resource. Looks409 like the following:410 411 {412 'some_name': ('SomeName', <Shape...>)413 }414 415 :type shape: botocore.model.Shape416 :param shape: The underlying shape for this resource.417 :rtype: dict418 :return: Mapping of resource attributes.419 """420 attributes = {}421 identifier_names = [i.name for i in self.identifiers]422 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 means427 continue428 snake_cased = self._get_name(429 'attribute', snake_cased, snake_case=False430 )431 attributes[snake_cased] = (name, member)432 433 return attributes434 435 @property436 def identifiers(self):437 """438 Get a list of resource identifiers.439 440 :type: list(:py:class:`Identifier`)441 """442 identifiers = []443 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))450 451 return identifiers452 453 @property454 def load(self):455 """456 Get the load action for this resource, if it is defined.457 458 :type: :py:class:`Action` or ``None``459 """460 action = self._definition.get('load')461 462 if action is not None:463 action = Action('load', action, self._resource_defs)464 465 return action466 467 @property468 def actions(self):469 """470 Get a list of actions for this resource.471 472 :type: list(:py:class:`Action`)473 """474 actions = []475 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))479 480 return actions481 482 @property483 def batch_actions(self):484 """485 Get a list of batch actions for this resource.486 487 :type: list(:py:class:`Action`)488 """489 actions = []490 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))494 495 return actions496 497 def _get_has_definition(self):498 """499 Get a ``has`` relationship definition from a model, where the500 service resource model is treated special in that it contains501 a relationship to every resource defined for the service. This502 allows things like ``s3.Object('bucket-name', 'key')`` to503 work even though the JSON doesn't define it explicitly.504 505 :rtype: dict506 :return: Mapping of names to subresource and reference507 definitions.508 """509 if self.name not in self._resource_defs:510 # This is the service resource, so let us expose all of511 # the defined resources as subresources.512 definition = {}513 514 for name, resource_def in self._resource_defs.items():515 # It's possible for the service to have renamed a516 # resource or to have defined multiple names that517 # point to the same resource type, so we need to518 # take that into account.519 found = False520 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_def524 found = True525 526 if not found:527 # Create a relationship definition and attach it528 # to the model, such that all identifiers must be529 # 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': []}}543 544 for identifier in resource_def.get('identifiers', []):545 fake_has['resource']['identifiers'].append(546 {'target': identifier['name'], 'source': 'input'}547 )548 549 definition[name] = fake_has550 else:551 definition = self._definition.get('has', {})552 553 return definition554 555 def _get_related_resources(self, subresources):556 """557 Get a list of sub-resources or references.558 559 :type subresources: bool560 :param subresources: ``True`` to get sub-resources, ``False`` to561 get references.562 :rtype: list(:py:class:`Action`)563 """564 resources = []565 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)572 573 data_required = False574 for identifier in action.resource.identifiers:575 if identifier.source == 'data':576 data_required = True577 break578 579 if subresources and not data_required:580 resources.append(action)581 elif not subresources and data_required:582 resources.append(action)583 584 return resources585 586 @property587 def subresources(self):588 """589 Get a list of sub-resources.590 591 :type: list(:py:class:`Action`)592 """593 return self._get_related_resources(True)594 595 @property596 def references(self):597 """598 Get a list of reference resources.599 600 :type: list(:py:class:`Action`)601 """602 return self._get_related_resources(False)603 604 @property605 def collections(self):606 """607 Get a list of collections for this resource.608 609 :type: list(:py:class:`Collection`)610 """611 collections = []612 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))616 617 return collections618 619 @property620 def waiters(self):621 """622 Get a list of waiters for this resource.623 624 :type: list(:py:class:`Waiter`)625 """626 waiters = []627 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))631 632 return waiters633 