codekingpro/portable-devtools
114k
1# Copyright 2015 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# http://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"""Abstractions to interact with service models."""14from collections import defaultdict15from typing import NamedTuple, Union16 17from botocore.compat import OrderedDict18from botocore.exceptions import (19 MissingServiceIdError,20 UndefinedModelAttributeError,21)22from botocore.utils import CachedProperty, hyphenize_service_id, instance_cache23 24NOT_SET = object()25 26 27class NoShapeFoundError(Exception):28 pass29 30 31class InvalidShapeError(Exception):32 pass33 34 35class OperationNotFoundError(Exception):36 pass37 38 39class InvalidShapeReferenceError(Exception):40 pass41 42 43class ServiceId(str):44 def hyphenize(self):45 return hyphenize_service_id(self)46 47 48class Shape:49 """Object representing a shape from the service model."""50 51 # To simplify serialization logic, all shape params that are52 # related to serialization are moved from the top level hash into53 # a 'serialization' hash. This list below contains the names of all54 # the attributes that should be moved.55 SERIALIZED_ATTRS = [56 'locationName',57 'queryName',58 'flattened',59 'location',60 'payload',61 'streaming',62 'timestampFormat',63 'xmlNamespace',64 'resultWrapper',65 'xmlAttribute',66 'eventstream',67 'event',68 'eventheader',69 'eventpayload',70 'jsonvalue',71 'timestampFormat',72 'hostLabel',73 ]74 METADATA_ATTRS = [75 'required',76 'min',77 'max',78 'pattern',79 'sensitive',80 'enum',81 'idempotencyToken',82 'error',83 'exception',84 'endpointdiscoveryid',85 'retryable',86 'document',87 'union',88 'contextParam',89 'clientContextParams',90 'requiresLength',91 ]92 MAP_TYPE = OrderedDict93 94 def __init__(self, shape_name, shape_model, shape_resolver=None):95 """96 97 :type shape_name: string98 :param shape_name: The name of the shape.99 100 :type shape_model: dict101 :param shape_model: The shape model. This would be the value102 associated with the key in the "shapes" dict of the103 service model (i.e ``model['shapes'][shape_name]``)104 105 :type shape_resolver: botocore.model.ShapeResolver106 :param shape_resolver: A shape resolver object. This is used to107 resolve references to other shapes. For scalar shape types108 (string, integer, boolean, etc.), this argument is not109 required. If a shape_resolver is not provided for a complex110 type, then a ``ValueError`` will be raised when an attempt111 to resolve a shape is made.112 113 """114 self.name = shape_name115 self.type_name = shape_model['type']116 self.documentation = shape_model.get('documentation', '')117 self._shape_model = shape_model118 if shape_resolver is None:119 # If a shape_resolver is not provided, we create an object120 # that will throw errors if you attempt to resolve121 # a shape. This is actually ok for scalar shapes122 # because they don't need to resolve shapes and shouldn't123 # be required to provide an object they won't use.124 shape_resolver = UnresolvableShapeMap()125 self._shape_resolver = shape_resolver126 self._cache = {}127 128 @CachedProperty129 def serialization(self):130 """Serialization information about the shape.131 132 This contains information that may be needed for input serialization133 or response parsing. This can include:134 135 * name136 * queryName137 * flattened138 * location139 * payload140 * streaming141 * xmlNamespace142 * resultWrapper143 * xmlAttribute144 * jsonvalue145 * timestampFormat146 147 :rtype: dict148 :return: Serialization information about the shape.149 150 """151 model = self._shape_model152 serialization = {}153 for attr in self.SERIALIZED_ATTRS:154 if attr in self._shape_model:155 serialization[attr] = model[attr]156 # For consistency, locationName is renamed to just 'name'.157 if 'locationName' in serialization:158 serialization['name'] = serialization.pop('locationName')159 return serialization160 161 @CachedProperty162 def metadata(self):163 """Metadata about the shape.164 165 This requires optional information about the shape, including:166 167 * min168 * max169 * pattern170 * enum171 * sensitive172 * required173 * idempotencyToken174 * document175 * union176 * contextParam177 * clientContextParams178 * requiresLength179 180 :rtype: dict181 :return: Metadata about the shape.182 183 """184 model = self._shape_model185 metadata = {}186 for attr in self.METADATA_ATTRS:187 if attr in self._shape_model:188 metadata[attr] = model[attr]189 return metadata190 191 @CachedProperty192 def required_members(self):193 """A list of members that are required.194 195 A structure shape can define members that are required.196 This value will return a list of required members. If there197 are no required members an empty list is returned.198 199 """200 return self.metadata.get('required', [])201 202 def _resolve_shape_ref(self, shape_ref):203 return self._shape_resolver.resolve_shape_ref(shape_ref)204 205 def __repr__(self):206 return f"<{self.__class__.__name__}({self.name})>"207 208 @property209 def event_stream_name(self):210 return None211 212 213class StructureShape(Shape):214 @CachedProperty215 def members(self):216 members = self._shape_model.get('members', self.MAP_TYPE())217 # The members dict looks like:218 # 'members': {219 # 'MemberName': {'shape': 'shapeName'},220 # 'MemberName2': {'shape': 'shapeName'},221 # }222 # We return a dict of member name to Shape object.223 shape_members = self.MAP_TYPE()224 for name, shape_ref in members.items():225 shape_members[name] = self._resolve_shape_ref(shape_ref)226 return shape_members227 228 @CachedProperty229 def event_stream_name(self):230 for member_name, member in self.members.items():231 if member.serialization.get('eventstream'):232 return member_name233 return None234 235 @CachedProperty236 def error_code(self):237 if not self.metadata.get('exception', False):238 return None239 error_metadata = self.metadata.get("error", {})240 code = error_metadata.get("code")241 if code:242 return code243 # Use the exception name if there is no explicit code modeled244 return self.name245 246 @CachedProperty247 def is_document_type(self):248 return self.metadata.get('document', False)249 250 @CachedProperty251 def is_tagged_union(self):252 return self.metadata.get('union', False)253 254 255class ListShape(Shape):256 @CachedProperty257 def member(self):258 return self._resolve_shape_ref(self._shape_model['member'])259 260 261class MapShape(Shape):262 @CachedProperty263 def key(self):264 return self._resolve_shape_ref(self._shape_model['key'])265 266 @CachedProperty267 def value(self):268 return self._resolve_shape_ref(self._shape_model['value'])269 270 271class StringShape(Shape):272 @CachedProperty273 def enum(self):274 return self.metadata.get('enum', [])275 276 277class StaticContextParameter(NamedTuple):278 name: str279 value: Union[bool, str]280 281 282class ContextParameter(NamedTuple):283 name: str284 member_name: str285 286 287class ClientContextParameter(NamedTuple):288 name: str289 type: str290 documentation: str291 292 293class ServiceModel:294 """295 296 :ivar service_description: The parsed service description dictionary.297 298 """299 300 def __init__(self, service_description, service_name=None):301 """302 303 :type service_description: dict304 :param service_description: The service description model. This value305 is obtained from a botocore.loader.Loader, or from directly loading306 the file yourself::307 308 service_description = json.load(309 open('/path/to/service-description-model.json'))310 model = ServiceModel(service_description)311 312 :type service_name: str313 :param service_name: The name of the service. Normally this is314 the endpoint prefix defined in the service_description. However,315 you can override this value to provide a more convenient name.316 This is done in a few places in botocore (ses instead of email,317 emr instead of elasticmapreduce). If this value is not provided,318 it will default to the endpointPrefix defined in the model.319 320 """321 self._service_description = service_description322 # We want clients to be able to access metadata directly.323 self.metadata = service_description.get('metadata', {})324 self._shape_resolver = ShapeResolver(325 service_description.get('shapes', {})326 )327 self._signature_version = NOT_SET328 self._service_name = service_name329 self._instance_cache = {}330 331 def shape_for(self, shape_name, member_traits=None):332 return self._shape_resolver.get_shape_by_name(333 shape_name, member_traits334 )335 336 def shape_for_error_code(self, error_code):337 return self._error_code_cache.get(error_code, None)338 339 @CachedProperty340 def _error_code_cache(self):341 error_code_cache = {}342 for error_shape in self.error_shapes:343 code = error_shape.error_code344 error_code_cache[code] = error_shape345 return error_code_cache346 347 def resolve_shape_ref(self, shape_ref):348 return self._shape_resolver.resolve_shape_ref(shape_ref)349 350 @CachedProperty351 def shape_names(self):352 return list(self._service_description.get('shapes', {}))353 354 @CachedProperty355 def error_shapes(self):356 error_shapes = []357 for shape_name in self.shape_names:358 error_shape = self.shape_for(shape_name)359 if error_shape.metadata.get('exception', False):360 error_shapes.append(error_shape)361 return error_shapes362 363 @instance_cache364 def operation_model(self, operation_name):365 try:366 model = self._service_description['operations'][operation_name]367 except KeyError:368 raise OperationNotFoundError(operation_name)369 return OperationModel(model, self, operation_name)370 371 @CachedProperty372 def documentation(self):373 return self._service_description.get('documentation', '')374 375 @CachedProperty376 def operation_names(self):377 return list(self._service_description.get('operations', []))378 379 @CachedProperty380 def service_name(self):381 """The name of the service.382 383 This defaults to the endpointPrefix defined in the service model.384 However, this value can be overriden when a ``ServiceModel`` is385 created. If a service_name was not provided when the ``ServiceModel``386 was created and if there is no endpointPrefix defined in the387 service model, then an ``UndefinedModelAttributeError`` exception388 will be raised.389 390 """391 if self._service_name is not None:392 return self._service_name393 else:394 return self.endpoint_prefix395 396 @CachedProperty397 def service_id(self):398 try:399 return ServiceId(self._get_metadata_property('serviceId'))400 except UndefinedModelAttributeError:401 raise MissingServiceIdError(service_name=self._service_name)402 403 @CachedProperty404 def signing_name(self):405 """The name to use when computing signatures.406 407 If the model does not define a signing name, this408 value will be the endpoint prefix defined in the model.409 """410 signing_name = self.metadata.get('signingName')411 if signing_name is None:412 signing_name = self.endpoint_prefix413 return signing_name414 415 @CachedProperty416 def api_version(self):417 return self._get_metadata_property('apiVersion')418 419 @CachedProperty420 def protocol(self):421 return self._get_metadata_property('protocol')422 423 @CachedProperty424 def endpoint_prefix(self):425 return self._get_metadata_property('endpointPrefix')426 427 @CachedProperty428 def endpoint_discovery_operation(self):429 for operation in self.operation_names:430 model = self.operation_model(operation)431 if model.is_endpoint_discovery_operation:432 return model433 434 @CachedProperty435 def endpoint_discovery_required(self):436 for operation in self.operation_names:437 model = self.operation_model(operation)438 if (439 model.endpoint_discovery is not None440 and model.endpoint_discovery.get('required')441 ):442 return True443 return False444 445 @CachedProperty446 def client_context_parameters(self):447 params = self._service_description.get('clientContextParams', {})448 return [449 ClientContextParameter(450 name=param_name,451 type=param_val['type'],452 documentation=param_val['documentation'],453 )454 for param_name, param_val in params.items()455 ]456 457 def _get_metadata_property(self, name):458 try:459 return self.metadata[name]460 except KeyError:461 raise UndefinedModelAttributeError(462 f'"{name}" not defined in the metadata of the model: {self}'463 )464 465 # Signature version is one of the rare properties466 # that can be modified so a CachedProperty is not used here.467 468 @property469 def signature_version(self):470 if self._signature_version is NOT_SET:471 signature_version = self.metadata.get('signatureVersion')472 self._signature_version = signature_version473 return self._signature_version474 475 @signature_version.setter476 def signature_version(self, value):477 self._signature_version = value478 479 def __repr__(self):480 return f'{self.__class__.__name__}({self.service_name})'481 482 483class OperationModel:484 def __init__(self, operation_model, service_model, name=None):485 """486 487 :type operation_model: dict488 :param operation_model: The operation model. This comes from the489 service model, and is the value associated with the operation490 name in the service model (i.e ``model['operations'][op_name]``).491 492 :type service_model: botocore.model.ServiceModel493 :param service_model: The service model associated with the operation.494 495 :type name: string496 :param name: The operation name. This is the operation name exposed to497 the users of this model. This can potentially be different from498 the "wire_name", which is the operation name that *must* by499 provided over the wire. For example, given::500 501 "CreateCloudFrontOriginAccessIdentity":{502 "name":"CreateCloudFrontOriginAccessIdentity2014_11_06",503 ...504 }505 506 The ``name`` would be ``CreateCloudFrontOriginAccessIdentity``,507 but the ``self.wire_name`` would be508 ``CreateCloudFrontOriginAccessIdentity2014_11_06``, which is the509 value we must send in the corresponding HTTP request.510 511 """512 self._operation_model = operation_model513 self._service_model = service_model514 self._api_name = name515 # Clients can access '.name' to get the operation name516 # and '.metadata' to get the top level metdata of the service.517 self._wire_name = operation_model.get('name')518 self.metadata = service_model.metadata519 self.http = operation_model.get('http', {})520 521 @CachedProperty522 def name(self):523 if self._api_name is not None:524 return self._api_name525 else:526 return self.wire_name527 528 @property529 def wire_name(self):530 """The wire name of the operation.531 532 In many situations this is the same value as the533 ``name``, value, but in some services, the operation name534 exposed to the user is different from the operation name535 we send across the wire (e.g cloudfront).536 537 Any serialization code should use ``wire_name``.538 539 """540 return self._operation_model.get('name')541 542 @property543 def service_model(self):544 return self._service_model545 546 @CachedProperty547 def documentation(self):548 return self._operation_model.get('documentation', '')549 550 @CachedProperty551 def deprecated(self):552 return self._operation_model.get('deprecated', False)553 554 @CachedProperty555 def endpoint_discovery(self):556 # Explicit None default. An empty dictionary for this trait means it is557 # enabled but not required to be used.558 return self._operation_model.get('endpointdiscovery', None)559 560 @CachedProperty561 def is_endpoint_discovery_operation(self):562 return self._operation_model.get('endpointoperation', False)563 564 @CachedProperty565 def input_shape(self):566 if 'input' not in self._operation_model:567 # Some operations do not accept any input and do not define an568 # input shape.569 return None570 return self._service_model.resolve_shape_ref(571 self._operation_model['input']572 )573 574 @CachedProperty575 def output_shape(self):576 if 'output' not in self._operation_model:577 # Some operations do not define an output shape,578 # in which case we return None to indicate the579 # operation has no expected output.580 return None581 return self._service_model.resolve_shape_ref(582 self._operation_model['output']583 )584 585 @CachedProperty586 def idempotent_members(self):587 input_shape = self.input_shape588 if not input_shape:589 return []590 591 return [592 name593 for (name, shape) in input_shape.members.items()594 if 'idempotencyToken' in shape.metadata595 and shape.metadata['idempotencyToken']596 ]597 598 @CachedProperty599 def static_context_parameters(self):600 params = self._operation_model.get('staticContextParams', {})601 return [602 StaticContextParameter(name=name, value=props.get('value'))603 for name, props in params.items()604 ]605 606 @CachedProperty607 def context_parameters(self):608 if not self.input_shape:609 return []610 611 return [612 ContextParameter(613 name=shape.metadata['contextParam']['name'],614 member_name=name,615 )616 for name, shape in self.input_shape.members.items()617 if 'contextParam' in shape.metadata618 and 'name' in shape.metadata['contextParam']619 ]620 621 @CachedProperty622 def request_compression(self):623 return self._operation_model.get('requestcompression')624 625 @CachedProperty626 def auth_type(self):627 return self._operation_model.get('authtype')628 629 @CachedProperty630 def error_shapes(self):631 shapes = self._operation_model.get("errors", [])632 return list(self._service_model.resolve_shape_ref(s) for s in shapes)633 634 @CachedProperty635 def endpoint(self):636 return self._operation_model.get('endpoint')637 638 @CachedProperty639 def http_checksum_required(self):640 return self._operation_model.get('httpChecksumRequired', False)641 642 @CachedProperty643 def http_checksum(self):644 return self._operation_model.get('httpChecksum', {})645 646 @CachedProperty647 def has_event_stream_input(self):648 return self.get_event_stream_input() is not None649 650 @CachedProperty651 def has_event_stream_output(self):652 return self.get_event_stream_output() is not None653 654 def get_event_stream_input(self):655 return self._get_event_stream(self.input_shape)656 657 def get_event_stream_output(self):658 return self._get_event_stream(self.output_shape)659 660 def _get_event_stream(self, shape):661 """Returns the event stream member's shape if any or None otherwise."""662 if shape is None:663 return None664 event_name = shape.event_stream_name665 if event_name:666 return shape.members[event_name]667 return None668 669 @CachedProperty670 def has_streaming_input(self):671 return self.get_streaming_input() is not None672 673 @CachedProperty674 def has_streaming_output(self):675 return self.get_streaming_output() is not None676 677 def get_streaming_input(self):678 return self._get_streaming_body(self.input_shape)679 680 def get_streaming_output(self):681 return self._get_streaming_body(self.output_shape)682 683 def _get_streaming_body(self, shape):684 """Returns the streaming member's shape if any; or None otherwise."""685 if shape is None:686 return None687 payload = shape.serialization.get('payload')688 if payload is not None:689 payload_shape = shape.members[payload]690 if payload_shape.type_name == 'blob':691 return payload_shape692 return None693 694 def __repr__(self):695 return f'{self.__class__.__name__}(name={self.name})'696 697 698class ShapeResolver:699 """Resolves shape references."""700 701 # Any type not in this mapping will default to the Shape class.702 SHAPE_CLASSES = {703 'structure': StructureShape,704 'list': ListShape,705 'map': MapShape,706 'string': StringShape,707 }708 709 def __init__(self, shape_map):710 self._shape_map = shape_map711 self._shape_cache = {}712 713 def get_shape_by_name(self, shape_name, member_traits=None):714 try:715 shape_model = self._shape_map[shape_name]716 except KeyError:717 raise NoShapeFoundError(shape_name)718 try:719 shape_cls = self.SHAPE_CLASSES.get(shape_model['type'], Shape)720 except KeyError:721 raise InvalidShapeError(722 f"Shape is missing required key 'type': {shape_model}"723 )724 if member_traits:725 shape_model = shape_model.copy()726 shape_model.update(member_traits)727 result = shape_cls(shape_name, shape_model, self)728 return result729 730 def resolve_shape_ref(self, shape_ref):731 # A shape_ref is a dict that has a 'shape' key that732 # refers to a shape name as well as any additional733 # member traits that are then merged over the shape734 # definition. For example:735 # {"shape": "StringType", "locationName": "Foobar"}736 if len(shape_ref) == 1 and 'shape' in shape_ref:737 # It's just a shape ref with no member traits, we can avoid738 # a .copy(). This is the common case so it's specifically739 # called out here.740 return self.get_shape_by_name(shape_ref['shape'])741 else:742 member_traits = shape_ref.copy()743 try:744 shape_name = member_traits.pop('shape')745 except KeyError:746 raise InvalidShapeReferenceError(747 f"Invalid model, missing shape reference: {shape_ref}"748 )749 return self.get_shape_by_name(shape_name, member_traits)750 751 752class UnresolvableShapeMap:753 """A ShapeResolver that will throw ValueErrors when shapes are resolved."""754 755 def get_shape_by_name(self, shape_name, member_traits=None):756 raise ValueError(757 f"Attempted to lookup shape '{shape_name}', but no shape map was provided."758 )759 760 def resolve_shape_ref(self, shape_ref):761 raise ValueError(762 f"Attempted to resolve shape '{shape_ref}', but no shape "763 f"map was provided."764 )765 766 767class DenormalizedStructureBuilder:768 """Build a StructureShape from a denormalized model.769 770 This is a convenience builder class that makes it easy to construct771 ``StructureShape``s based on a denormalized model.772 773 It will handle the details of creating unique shape names and creating774 the appropriate shape map needed by the ``StructureShape`` class.775 776 Example usage::777 778 builder = DenormalizedStructureBuilder()779 shape = builder.with_members({780 'A': {781 'type': 'structure',782 'members': {783 'B': {784 'type': 'structure',785 'members': {786 'C': {787 'type': 'string',788 }789 }790 }791 }792 }793 }).build_model()794 # ``shape`` is now an instance of botocore.model.StructureShape795 796 :type dict_type: class797 :param dict_type: The dictionary type to use, allowing you to opt-in798 to using OrderedDict or another dict type. This can799 be particularly useful for testing when order800 matters, such as for documentation.801 802 """803 804 SCALAR_TYPES = (805 'string',806 'integer',807 'boolean',808 'blob',809 'float',810 'timestamp',811 'long',812 'double',813 'char',814 )815 816 def __init__(self, name=None):817 self.members = OrderedDict()818 self._name_generator = ShapeNameGenerator()819 if name is None:820 self.name = self._name_generator.new_shape_name('structure')821 822 def with_members(self, members):823 """824 825 :type members: dict826 :param members: The denormalized members.827 828 :return: self829 830 """831 self._members = members832 return self833 834 def build_model(self):835 """Build the model based on the provided members.836 837 :rtype: botocore.model.StructureShape838 :return: The built StructureShape object.839 840 """841 shapes = OrderedDict()842 denormalized = {843 'type': 'structure',844 'members': self._members,845 }846 self._build_model(denormalized, shapes, self.name)847 resolver = ShapeResolver(shape_map=shapes)848 return StructureShape(849 shape_name=self.name,850 shape_model=shapes[self.name],851 shape_resolver=resolver,852 )853 854 def _build_model(self, model, shapes, shape_name):855 if model['type'] == 'structure':856 shapes[shape_name] = self._build_structure(model, shapes)857 elif model['type'] == 'list':858 shapes[shape_name] = self._build_list(model, shapes)859 elif model['type'] == 'map':860 shapes[shape_name] = self._build_map(model, shapes)861 elif model['type'] in self.SCALAR_TYPES:862 shapes[shape_name] = self._build_scalar(model)863 else:864 raise InvalidShapeError(f"Unknown shape type: {model['type']}")865 866 def _build_structure(self, model, shapes):867 members = OrderedDict()868 shape = self._build_initial_shape(model)869 shape['members'] = members870 871 for name, member_model in model.get('members', OrderedDict()).items():872 member_shape_name = self._get_shape_name(member_model)873 members[name] = {'shape': member_shape_name}874 self._build_model(member_model, shapes, member_shape_name)875 return shape876 877 def _build_list(self, model, shapes):878 member_shape_name = self._get_shape_name(model)879 shape = self._build_initial_shape(model)880 shape['member'] = {'shape': member_shape_name}881 self._build_model(model['member'], shapes, member_shape_name)882 return shape883 884 def _build_map(self, model, shapes):885 key_shape_name = self._get_shape_name(model['key'])886 value_shape_name = self._get_shape_name(model['value'])887 shape = self._build_initial_shape(model)888 shape['key'] = {'shape': key_shape_name}889 shape['value'] = {'shape': value_shape_name}890 self._build_model(model['key'], shapes, key_shape_name)891 self._build_model(model['value'], shapes, value_shape_name)892 return shape893 894 def _build_initial_shape(self, model):895 shape = {896 'type': model['type'],897 }898 if 'documentation' in model:899 shape['documentation'] = model['documentation']900 for attr in Shape.METADATA_ATTRS:901 if attr in model:902 shape[attr] = model[attr]903 return shape904 905 def _build_scalar(self, model):906 return self._build_initial_shape(model)907 908 def _get_shape_name(self, model):909 if 'shape_name' in model:910 return model['shape_name']911 else:912 return self._name_generator.new_shape_name(model['type'])913 914 915class ShapeNameGenerator:916 """Generate unique shape names for a type.917 918 This class can be used in conjunction with the DenormalizedStructureBuilder919 to generate unique shape names for a given type.920 921 """922 923 def __init__(self):924 self._name_cache = defaultdict(int)925 926 def new_shape_name(self, type_name):927 """Generate a unique shape name.928 929 This method will guarantee a unique shape name each time it is930 called with the same type.931 932 ::933 934 >>> s = ShapeNameGenerator()935 >>> s.new_shape_name('structure')936 'StructureType1'937 >>> s.new_shape_name('structure')938 'StructureType2'939 >>> s.new_shape_name('list')940 'ListType1'941 >>> s.new_shape_name('list')942 'ListType2'943 944 945 :type type_name: string946 :param type_name: The type name (structure, list, map, string, etc.)947 948 :rtype: string949 :return: A unique shape name for the given type950 951 """952 self._name_cache[type_name] += 1953 current_index = self._name_cache[type_name]954 return f'{type_name.capitalize()}Type{current_index}'955 