Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/botocore/model.py: 53%
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 2015 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# http://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"""Abstractions to interact with service models."""
15from collections import defaultdict
16from typing import NamedTuple, Union
18from botocore.auth import resolve_auth_type
19from botocore.compat import OrderedDict
20from botocore.exceptions import (
21 MissingServiceIdError,
22 UndefinedModelAttributeError,
23)
24from botocore.utils import CachedProperty, hyphenize_service_id, instance_cache
26NOT_SET = object()
29class NoShapeFoundError(Exception):
30 pass
33class InvalidShapeError(Exception):
34 pass
37class OperationNotFoundError(Exception):
38 pass
41class InvalidShapeReferenceError(Exception):
42 pass
45class ServiceId(str):
46 def hyphenize(self):
47 return hyphenize_service_id(self)
50class Shape:
51 """Object representing a shape from the service model."""
53 # To simplify serialization logic, all shape params that are
54 # related to serialization are moved from the top level hash into
55 # a 'serialization' hash. This list below contains the names of all
56 # the attributes that should be moved.
57 SERIALIZED_ATTRS = [
58 'locationName',
59 'queryName',
60 'flattened',
61 'location',
62 'payload',
63 'streaming',
64 'timestampFormat',
65 'xmlNamespace',
66 'resultWrapper',
67 'xmlAttribute',
68 'eventstream',
69 'event',
70 'eventheader',
71 'eventpayload',
72 'jsonvalue',
73 'timestampFormat',
74 'hostLabel',
75 ]
76 METADATA_ATTRS = [
77 'required',
78 'min',
79 'max',
80 'pattern',
81 'sensitive',
82 'enum',
83 'idempotencyToken',
84 'error',
85 'exception',
86 'endpointdiscoveryid',
87 'retryable',
88 'document',
89 'union',
90 'contextParam',
91 'clientContextParams',
92 'requiresLength',
93 ]
94 MAP_TYPE = OrderedDict
96 def __init__(self, shape_name, shape_model, shape_resolver=None):
97 """
99 :type shape_name: string
100 :param shape_name: The name of the shape.
102 :type shape_model: dict
103 :param shape_model: The shape model. This would be the value
104 associated with the key in the "shapes" dict of the
105 service model (i.e ``model['shapes'][shape_name]``)
107 :type shape_resolver: botocore.model.ShapeResolver
108 :param shape_resolver: A shape resolver object. This is used to
109 resolve references to other shapes. For scalar shape types
110 (string, integer, boolean, etc.), this argument is not
111 required. If a shape_resolver is not provided for a complex
112 type, then a ``ValueError`` will be raised when an attempt
113 to resolve a shape is made.
115 """
116 self.name = shape_name
117 self.type_name = shape_model['type']
118 self.documentation = shape_model.get('documentation', '')
119 self._shape_model = shape_model
120 if shape_resolver is None:
121 # If a shape_resolver is not provided, we create an object
122 # that will throw errors if you attempt to resolve
123 # a shape. This is actually ok for scalar shapes
124 # because they don't need to resolve shapes and shouldn't
125 # be required to provide an object they won't use.
126 shape_resolver = UnresolvableShapeMap()
127 self._shape_resolver = shape_resolver
128 self._cache = {}
130 @CachedProperty
131 def serialization(self):
132 """Serialization information about the shape.
134 This contains information that may be needed for input serialization
135 or response parsing. This can include:
137 * name
138 * queryName
139 * flattened
140 * location
141 * payload
142 * streaming
143 * xmlNamespace
144 * resultWrapper
145 * xmlAttribute
146 * jsonvalue
147 * timestampFormat
149 :rtype: dict
150 :return: Serialization information about the shape.
152 """
153 model = self._shape_model
154 serialization = {}
155 for attr in self.SERIALIZED_ATTRS:
156 if attr in self._shape_model:
157 serialization[attr] = model[attr]
158 # For consistency, locationName is renamed to just 'name'.
159 if 'locationName' in serialization:
160 serialization['name'] = serialization.pop('locationName')
161 return serialization
163 @CachedProperty
164 def metadata(self):
165 """Metadata about the shape.
167 This requires optional information about the shape, including:
169 * min
170 * max
171 * pattern
172 * enum
173 * sensitive
174 * required
175 * idempotencyToken
176 * document
177 * union
178 * contextParam
179 * clientContextParams
180 * requiresLength
182 :rtype: dict
183 :return: Metadata about the shape.
185 """
186 model = self._shape_model
187 metadata = {}
188 for attr in self.METADATA_ATTRS:
189 if attr in self._shape_model:
190 metadata[attr] = model[attr]
191 return metadata
193 @CachedProperty
194 def required_members(self):
195 """A list of members that are required.
197 A structure shape can define members that are required.
198 This value will return a list of required members. If there
199 are no required members an empty list is returned.
201 """
202 return self.metadata.get('required', [])
204 def _resolve_shape_ref(self, shape_ref):
205 return self._shape_resolver.resolve_shape_ref(shape_ref)
207 def __repr__(self):
208 return f"<{self.__class__.__name__}({self.name})>"
210 @property
211 def event_stream_name(self):
212 return None
215class StructureShape(Shape):
216 @CachedProperty
217 def members(self):
218 members = self._shape_model.get('members', self.MAP_TYPE())
219 # The members dict looks like:
220 # 'members': {
221 # 'MemberName': {'shape': 'shapeName'},
222 # 'MemberName2': {'shape': 'shapeName'},
223 # }
224 # We return a dict of member name to Shape object.
225 shape_members = self.MAP_TYPE()
226 for name, shape_ref in members.items():
227 shape_members[name] = self._resolve_shape_ref(shape_ref)
228 return shape_members
230 @CachedProperty
231 def event_stream_name(self):
232 for member_name, member in self.members.items():
233 if member.serialization.get('eventstream'):
234 return member_name
235 return None
237 @CachedProperty
238 def error_code(self):
239 if not self.metadata.get('exception', False):
240 return None
241 error_metadata = self.metadata.get("error", {})
242 code = error_metadata.get("code")
243 if code:
244 return code
245 # Use the exception name if there is no explicit code modeled
246 return self.name
248 @CachedProperty
249 def is_document_type(self):
250 return self.metadata.get('document', False)
252 @CachedProperty
253 def is_tagged_union(self):
254 return self.metadata.get('union', False)
257class ListShape(Shape):
258 @CachedProperty
259 def member(self):
260 return self._resolve_shape_ref(self._shape_model['member'])
263class MapShape(Shape):
264 @CachedProperty
265 def key(self):
266 return self._resolve_shape_ref(self._shape_model['key'])
268 @CachedProperty
269 def value(self):
270 return self._resolve_shape_ref(self._shape_model['value'])
273class StringShape(Shape):
274 @CachedProperty
275 def enum(self):
276 return self.metadata.get('enum', [])
279class StaticContextParameter(NamedTuple):
280 name: str
281 value: Union[bool, str]
284class ContextParameter(NamedTuple):
285 name: str
286 member_name: str
289class ClientContextParameter(NamedTuple):
290 name: str
291 type: str
292 documentation: str
295class ServiceModel:
296 """
298 :ivar service_description: The parsed service description dictionary.
300 """
302 def __init__(self, service_description, service_name=None):
303 """
305 :type service_description: dict
306 :param service_description: The service description model. This value
307 is obtained from a botocore.loader.Loader, or from directly loading
308 the file yourself::
310 service_description = json.load(
311 open('/path/to/service-description-model.json'))
312 model = ServiceModel(service_description)
314 :type service_name: str
315 :param service_name: The name of the service. Normally this is
316 the endpoint prefix defined in the service_description. However,
317 you can override this value to provide a more convenient name.
318 This is done in a few places in botocore (ses instead of email,
319 emr instead of elasticmapreduce). If this value is not provided,
320 it will default to the endpointPrefix defined in the model.
322 """
323 self._service_description = service_description
324 # We want clients to be able to access metadata directly.
325 self.metadata = service_description.get('metadata', {})
326 self._shape_resolver = ShapeResolver(
327 service_description.get('shapes', {})
328 )
329 self._signature_version = NOT_SET
330 self._service_name = service_name
331 self._instance_cache = {}
333 def shape_for(self, shape_name, member_traits=None):
334 return self._shape_resolver.get_shape_by_name(
335 shape_name, member_traits
336 )
338 def shape_for_error_code(self, error_code):
339 return self._error_code_cache.get(error_code, None)
341 @CachedProperty
342 def _error_code_cache(self):
343 error_code_cache = {}
344 for error_shape in self.error_shapes:
345 code = error_shape.error_code
346 error_code_cache[code] = error_shape
347 return error_code_cache
349 def resolve_shape_ref(self, shape_ref):
350 return self._shape_resolver.resolve_shape_ref(shape_ref)
352 @CachedProperty
353 def shape_names(self):
354 return list(self._service_description.get('shapes', {}))
356 @CachedProperty
357 def error_shapes(self):
358 error_shapes = []
359 for shape_name in self.shape_names:
360 error_shape = self.shape_for(shape_name)
361 if error_shape.metadata.get('exception', False):
362 error_shapes.append(error_shape)
363 return error_shapes
365 @instance_cache
366 def operation_model(self, operation_name):
367 try:
368 model = self._service_description['operations'][operation_name]
369 except KeyError:
370 raise OperationNotFoundError(operation_name)
371 return OperationModel(model, self, operation_name)
373 @CachedProperty
374 def documentation(self):
375 return self._service_description.get('documentation', '')
377 @CachedProperty
378 def operation_names(self):
379 return list(self._service_description.get('operations', []))
381 @CachedProperty
382 def service_name(self):
383 """The name of the service.
385 This defaults to the endpointPrefix defined in the service model.
386 However, this value can be overriden when a ``ServiceModel`` is
387 created. If a service_name was not provided when the ``ServiceModel``
388 was created and if there is no endpointPrefix defined in the
389 service model, then an ``UndefinedModelAttributeError`` exception
390 will be raised.
392 """
393 if self._service_name is not None:
394 return self._service_name
395 else:
396 return self.endpoint_prefix
398 @CachedProperty
399 def service_id(self):
400 try:
401 return ServiceId(self._get_metadata_property('serviceId'))
402 except UndefinedModelAttributeError:
403 raise MissingServiceIdError(service_name=self._service_name)
405 @CachedProperty
406 def signing_name(self):
407 """The name to use when computing signatures.
409 If the model does not define a signing name, this
410 value will be the endpoint prefix defined in the model.
411 """
412 signing_name = self.metadata.get('signingName')
413 if signing_name is None:
414 signing_name = self.endpoint_prefix
415 return signing_name
417 @CachedProperty
418 def api_version(self):
419 return self._get_metadata_property('apiVersion')
421 @CachedProperty
422 def protocol(self):
423 return self._get_metadata_property('protocol')
425 @CachedProperty
426 def endpoint_prefix(self):
427 return self._get_metadata_property('endpointPrefix')
429 @CachedProperty
430 def endpoint_discovery_operation(self):
431 for operation in self.operation_names:
432 model = self.operation_model(operation)
433 if model.is_endpoint_discovery_operation:
434 return model
436 @CachedProperty
437 def endpoint_discovery_required(self):
438 for operation in self.operation_names:
439 model = self.operation_model(operation)
440 if (
441 model.endpoint_discovery is not None
442 and model.endpoint_discovery.get('required')
443 ):
444 return True
445 return False
447 @CachedProperty
448 def client_context_parameters(self):
449 params = self._service_description.get('clientContextParams', {})
450 return [
451 ClientContextParameter(
452 name=param_name,
453 type=param_val['type'],
454 documentation=param_val['documentation'],
455 )
456 for param_name, param_val in params.items()
457 ]
459 def _get_metadata_property(self, name):
460 try:
461 return self.metadata[name]
462 except KeyError:
463 raise UndefinedModelAttributeError(
464 f'"{name}" not defined in the metadata of the model: {self}'
465 )
467 # Signature version is one of the rare properties
468 # that can be modified so a CachedProperty is not used here.
470 @property
471 def signature_version(self):
472 if self._signature_version is NOT_SET:
473 signature_version = self.metadata.get('signatureVersion')
474 self._signature_version = signature_version
475 return self._signature_version
477 @signature_version.setter
478 def signature_version(self, value):
479 self._signature_version = value
481 def __repr__(self):
482 return f'{self.__class__.__name__}({self.service_name})'
485class OperationModel:
486 def __init__(self, operation_model, service_model, name=None):
487 """
489 :type operation_model: dict
490 :param operation_model: The operation model. This comes from the
491 service model, and is the value associated with the operation
492 name in the service model (i.e ``model['operations'][op_name]``).
494 :type service_model: botocore.model.ServiceModel
495 :param service_model: The service model associated with the operation.
497 :type name: string
498 :param name: The operation name. This is the operation name exposed to
499 the users of this model. This can potentially be different from
500 the "wire_name", which is the operation name that *must* by
501 provided over the wire. For example, given::
503 "CreateCloudFrontOriginAccessIdentity":{
504 "name":"CreateCloudFrontOriginAccessIdentity2014_11_06",
505 ...
506 }
508 The ``name`` would be ``CreateCloudFrontOriginAccessIdentity``,
509 but the ``self.wire_name`` would be
510 ``CreateCloudFrontOriginAccessIdentity2014_11_06``, which is the
511 value we must send in the corresponding HTTP request.
513 """
514 self._operation_model = operation_model
515 self._service_model = service_model
516 self._api_name = name
517 # Clients can access '.name' to get the operation name
518 # and '.metadata' to get the top level metdata of the service.
519 self._wire_name = operation_model.get('name')
520 self.metadata = service_model.metadata
521 self.http = operation_model.get('http', {})
523 @CachedProperty
524 def name(self):
525 if self._api_name is not None:
526 return self._api_name
527 else:
528 return self.wire_name
530 @property
531 def wire_name(self):
532 """The wire name of the operation.
534 In many situations this is the same value as the
535 ``name``, value, but in some services, the operation name
536 exposed to the user is different from the operation name
537 we send across the wire (e.g cloudfront).
539 Any serialization code should use ``wire_name``.
541 """
542 return self._operation_model.get('name')
544 @property
545 def service_model(self):
546 return self._service_model
548 @CachedProperty
549 def documentation(self):
550 return self._operation_model.get('documentation', '')
552 @CachedProperty
553 def deprecated(self):
554 return self._operation_model.get('deprecated', False)
556 @CachedProperty
557 def endpoint_discovery(self):
558 # Explicit None default. An empty dictionary for this trait means it is
559 # enabled but not required to be used.
560 return self._operation_model.get('endpointdiscovery', None)
562 @CachedProperty
563 def is_endpoint_discovery_operation(self):
564 return self._operation_model.get('endpointoperation', False)
566 @CachedProperty
567 def input_shape(self):
568 if 'input' not in self._operation_model:
569 # Some operations do not accept any input and do not define an
570 # input shape.
571 return None
572 return self._service_model.resolve_shape_ref(
573 self._operation_model['input']
574 )
576 @CachedProperty
577 def output_shape(self):
578 if 'output' not in self._operation_model:
579 # Some operations do not define an output shape,
580 # in which case we return None to indicate the
581 # operation has no expected output.
582 return None
583 return self._service_model.resolve_shape_ref(
584 self._operation_model['output']
585 )
587 @CachedProperty
588 def idempotent_members(self):
589 input_shape = self.input_shape
590 if not input_shape:
591 return []
593 return [
594 name
595 for (name, shape) in input_shape.members.items()
596 if 'idempotencyToken' in shape.metadata
597 and shape.metadata['idempotencyToken']
598 ]
600 @CachedProperty
601 def static_context_parameters(self):
602 params = self._operation_model.get('staticContextParams', {})
603 return [
604 StaticContextParameter(name=name, value=props.get('value'))
605 for name, props in params.items()
606 ]
608 @CachedProperty
609 def context_parameters(self):
610 if not self.input_shape:
611 return []
613 return [
614 ContextParameter(
615 name=shape.metadata['contextParam']['name'],
616 member_name=name,
617 )
618 for name, shape in self.input_shape.members.items()
619 if 'contextParam' in shape.metadata
620 and 'name' in shape.metadata['contextParam']
621 ]
623 @CachedProperty
624 def request_compression(self):
625 return self._operation_model.get('requestcompression')
627 @CachedProperty
628 def auth(self):
629 return self._operation_model.get('auth')
631 @CachedProperty
632 def auth_type(self):
633 return self._operation_model.get('authtype')
635 @CachedProperty
636 def resolved_auth_type(self):
637 if self.auth:
638 return resolve_auth_type(self.auth)
639 return self.auth_type
641 @CachedProperty
642 def unsigned_payload(self):
643 return self._operation_model.get('unsignedPayload')
645 @CachedProperty
646 def error_shapes(self):
647 shapes = self._operation_model.get("errors", [])
648 return list(self._service_model.resolve_shape_ref(s) for s in shapes)
650 @CachedProperty
651 def endpoint(self):
652 return self._operation_model.get('endpoint')
654 @CachedProperty
655 def http_checksum_required(self):
656 return self._operation_model.get('httpChecksumRequired', False)
658 @CachedProperty
659 def http_checksum(self):
660 return self._operation_model.get('httpChecksum', {})
662 @CachedProperty
663 def has_event_stream_input(self):
664 return self.get_event_stream_input() is not None
666 @CachedProperty
667 def has_event_stream_output(self):
668 return self.get_event_stream_output() is not None
670 def get_event_stream_input(self):
671 return self._get_event_stream(self.input_shape)
673 def get_event_stream_output(self):
674 return self._get_event_stream(self.output_shape)
676 def _get_event_stream(self, shape):
677 """Returns the event stream member's shape if any or None otherwise."""
678 if shape is None:
679 return None
680 event_name = shape.event_stream_name
681 if event_name:
682 return shape.members[event_name]
683 return None
685 @CachedProperty
686 def has_streaming_input(self):
687 return self.get_streaming_input() is not None
689 @CachedProperty
690 def has_streaming_output(self):
691 return self.get_streaming_output() is not None
693 def get_streaming_input(self):
694 return self._get_streaming_body(self.input_shape)
696 def get_streaming_output(self):
697 return self._get_streaming_body(self.output_shape)
699 def _get_streaming_body(self, shape):
700 """Returns the streaming member's shape if any; or None otherwise."""
701 if shape is None:
702 return None
703 payload = shape.serialization.get('payload')
704 if payload is not None:
705 payload_shape = shape.members[payload]
706 if payload_shape.type_name == 'blob':
707 return payload_shape
708 return None
710 def __repr__(self):
711 return f'{self.__class__.__name__}(name={self.name})'
714class ShapeResolver:
715 """Resolves shape references."""
717 # Any type not in this mapping will default to the Shape class.
718 SHAPE_CLASSES = {
719 'structure': StructureShape,
720 'list': ListShape,
721 'map': MapShape,
722 'string': StringShape,
723 }
725 def __init__(self, shape_map):
726 self._shape_map = shape_map
727 self._shape_cache = {}
729 def get_shape_by_name(self, shape_name, member_traits=None):
730 try:
731 shape_model = self._shape_map[shape_name]
732 except KeyError:
733 raise NoShapeFoundError(shape_name)
734 try:
735 shape_cls = self.SHAPE_CLASSES.get(shape_model['type'], Shape)
736 except KeyError:
737 raise InvalidShapeError(
738 f"Shape is missing required key 'type': {shape_model}"
739 )
740 if member_traits:
741 shape_model = shape_model.copy()
742 shape_model.update(member_traits)
743 result = shape_cls(shape_name, shape_model, self)
744 return result
746 def resolve_shape_ref(self, shape_ref):
747 # A shape_ref is a dict that has a 'shape' key that
748 # refers to a shape name as well as any additional
749 # member traits that are then merged over the shape
750 # definition. For example:
751 # {"shape": "StringType", "locationName": "Foobar"}
752 if len(shape_ref) == 1 and 'shape' in shape_ref:
753 # It's just a shape ref with no member traits, we can avoid
754 # a .copy(). This is the common case so it's specifically
755 # called out here.
756 return self.get_shape_by_name(shape_ref['shape'])
757 else:
758 member_traits = shape_ref.copy()
759 try:
760 shape_name = member_traits.pop('shape')
761 except KeyError:
762 raise InvalidShapeReferenceError(
763 f"Invalid model, missing shape reference: {shape_ref}"
764 )
765 return self.get_shape_by_name(shape_name, member_traits)
768class UnresolvableShapeMap:
769 """A ShapeResolver that will throw ValueErrors when shapes are resolved."""
771 def get_shape_by_name(self, shape_name, member_traits=None):
772 raise ValueError(
773 f"Attempted to lookup shape '{shape_name}', but no shape map was provided."
774 )
776 def resolve_shape_ref(self, shape_ref):
777 raise ValueError(
778 f"Attempted to resolve shape '{shape_ref}', but no shape "
779 f"map was provided."
780 )
783class DenormalizedStructureBuilder:
784 """Build a StructureShape from a denormalized model.
786 This is a convenience builder class that makes it easy to construct
787 ``StructureShape``s based on a denormalized model.
789 It will handle the details of creating unique shape names and creating
790 the appropriate shape map needed by the ``StructureShape`` class.
792 Example usage::
794 builder = DenormalizedStructureBuilder()
795 shape = builder.with_members({
796 'A': {
797 'type': 'structure',
798 'members': {
799 'B': {
800 'type': 'structure',
801 'members': {
802 'C': {
803 'type': 'string',
804 }
805 }
806 }
807 }
808 }
809 }).build_model()
810 # ``shape`` is now an instance of botocore.model.StructureShape
812 :type dict_type: class
813 :param dict_type: The dictionary type to use, allowing you to opt-in
814 to using OrderedDict or another dict type. This can
815 be particularly useful for testing when order
816 matters, such as for documentation.
818 """
820 SCALAR_TYPES = (
821 'string',
822 'integer',
823 'boolean',
824 'blob',
825 'float',
826 'timestamp',
827 'long',
828 'double',
829 'char',
830 )
832 def __init__(self, name=None):
833 self.members = OrderedDict()
834 self._name_generator = ShapeNameGenerator()
835 if name is None:
836 self.name = self._name_generator.new_shape_name('structure')
838 def with_members(self, members):
839 """
841 :type members: dict
842 :param members: The denormalized members.
844 :return: self
846 """
847 self._members = members
848 return self
850 def build_model(self):
851 """Build the model based on the provided members.
853 :rtype: botocore.model.StructureShape
854 :return: The built StructureShape object.
856 """
857 shapes = OrderedDict()
858 denormalized = {
859 'type': 'structure',
860 'members': self._members,
861 }
862 self._build_model(denormalized, shapes, self.name)
863 resolver = ShapeResolver(shape_map=shapes)
864 return StructureShape(
865 shape_name=self.name,
866 shape_model=shapes[self.name],
867 shape_resolver=resolver,
868 )
870 def _build_model(self, model, shapes, shape_name):
871 if model['type'] == 'structure':
872 shapes[shape_name] = self._build_structure(model, shapes)
873 elif model['type'] == 'list':
874 shapes[shape_name] = self._build_list(model, shapes)
875 elif model['type'] == 'map':
876 shapes[shape_name] = self._build_map(model, shapes)
877 elif model['type'] in self.SCALAR_TYPES:
878 shapes[shape_name] = self._build_scalar(model)
879 else:
880 raise InvalidShapeError(f"Unknown shape type: {model['type']}")
882 def _build_structure(self, model, shapes):
883 members = OrderedDict()
884 shape = self._build_initial_shape(model)
885 shape['members'] = members
887 for name, member_model in model.get('members', OrderedDict()).items():
888 member_shape_name = self._get_shape_name(member_model)
889 members[name] = {'shape': member_shape_name}
890 self._build_model(member_model, shapes, member_shape_name)
891 return shape
893 def _build_list(self, model, shapes):
894 member_shape_name = self._get_shape_name(model)
895 shape = self._build_initial_shape(model)
896 shape['member'] = {'shape': member_shape_name}
897 self._build_model(model['member'], shapes, member_shape_name)
898 return shape
900 def _build_map(self, model, shapes):
901 key_shape_name = self._get_shape_name(model['key'])
902 value_shape_name = self._get_shape_name(model['value'])
903 shape = self._build_initial_shape(model)
904 shape['key'] = {'shape': key_shape_name}
905 shape['value'] = {'shape': value_shape_name}
906 self._build_model(model['key'], shapes, key_shape_name)
907 self._build_model(model['value'], shapes, value_shape_name)
908 return shape
910 def _build_initial_shape(self, model):
911 shape = {
912 'type': model['type'],
913 }
914 if 'documentation' in model:
915 shape['documentation'] = model['documentation']
916 for attr in Shape.METADATA_ATTRS:
917 if attr in model:
918 shape[attr] = model[attr]
919 return shape
921 def _build_scalar(self, model):
922 return self._build_initial_shape(model)
924 def _get_shape_name(self, model):
925 if 'shape_name' in model:
926 return model['shape_name']
927 else:
928 return self._name_generator.new_shape_name(model['type'])
931class ShapeNameGenerator:
932 """Generate unique shape names for a type.
934 This class can be used in conjunction with the DenormalizedStructureBuilder
935 to generate unique shape names for a given type.
937 """
939 def __init__(self):
940 self._name_cache = defaultdict(int)
942 def new_shape_name(self, type_name):
943 """Generate a unique shape name.
945 This method will guarantee a unique shape name each time it is
946 called with the same type.
948 ::
950 >>> s = ShapeNameGenerator()
951 >>> s.new_shape_name('structure')
952 'StructureType1'
953 >>> s.new_shape_name('structure')
954 'StructureType2'
955 >>> s.new_shape_name('list')
956 'ListType1'
957 >>> s.new_shape_name('list')
958 'ListType2'
961 :type type_name: string
962 :param type_name: The type name (structure, list, map, string, etc.)
964 :rtype: string
965 :return: A unique shape name for the given type
967 """
968 self._name_cache[type_name] += 1
969 current_index = self._name_cache[type_name]
970 return f'{type_name.capitalize()}Type{current_index}'