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 11
"""Abstractions to interact with service models."""
14 11
from collections import defaultdict
15

16 11
from botocore.utils import CachedProperty, instance_cache, hyphenize_service_id
17 11
from botocore.compat import OrderedDict
18 11
from botocore.exceptions import MissingServiceIdError
19 11
from botocore.exceptions import UndefinedModelAttributeError
20

21 11
NOT_SET = object()
22

23

24 11
class NoShapeFoundError(Exception):
25 11
    pass
26

27

28 11
class InvalidShapeError(Exception):
29 11
    pass
30

31

32 11
class OperationNotFoundError(Exception):
33 11
    pass
34

35

36 11
class InvalidShapeReferenceError(Exception):
37 11
    pass
38

39

40 11
class ServiceId(str):
41 11
    def hyphenize(self):
42 11
        return hyphenize_service_id(self)
43

44

45 11
class Shape(object):
46
    """Object representing a shape from the service model."""
47
    # To simplify serialization logic, all shape params that are
48
    # related to serialization are moved from the top level hash into
49
    # a 'serialization' hash.  This list below contains the names of all
50
    # the attributes that should be moved.
51 11
    SERIALIZED_ATTRS = ['locationName', 'queryName', 'flattened', 'location',
52
                        'payload', 'streaming', 'timestampFormat',
53
                        'xmlNamespace', 'resultWrapper', 'xmlAttribute',
54
                        'eventstream', 'event', 'eventheader', 'eventpayload',
55
                        'jsonvalue', 'timestampFormat', 'hostLabel']
56 11
    METADATA_ATTRS = ['required', 'min', 'max', 'sensitive', 'enum',
57
                      'idempotencyToken', 'error', 'exception',
58
                      'endpointdiscoveryid', 'retryable']
59 11
    MAP_TYPE = OrderedDict
60

61 11
    def __init__(self, shape_name, shape_model, shape_resolver=None):
62
        """
63

64
        :type shape_name: string
65
        :param shape_name: The name of the shape.
66

67
        :type shape_model: dict
68
        :param shape_model: The shape model.  This would be the value
69
            associated with the key in the "shapes" dict of the
70
            service model (i.e ``model['shapes'][shape_name]``)
71

72
        :type shape_resolver: botocore.model.ShapeResolver
73
        :param shape_resolver: A shape resolver object.  This is used to
74
            resolve references to other shapes.  For scalar shape types
75
            (string, integer, boolean, etc.), this argument is not
76
            required.  If a shape_resolver is not provided for a complex
77
            type, then a ``ValueError`` will be raised when an attempt
78
            to resolve a shape is made.
79

80
        """
81 11
        self.name = shape_name
82 11
        self.type_name = shape_model['type']
83 11
        self.documentation = shape_model.get('documentation', '')
84 11
        self._shape_model = shape_model
85 11
        if shape_resolver is None:
86
            # If a shape_resolver is not provided, we create an object
87
            # that will throw errors if you attempt to resolve
88
            # a shape.  This is actually ok for scalar shapes
89
            # because they don't need to resolve shapes and shouldn't
90
            # be required to provide an object they won't use.
91 11
            shape_resolver = UnresolvableShapeMap()
92 11
        self._shape_resolver = shape_resolver
93 11
        self._cache = {}
94

95 11
    @CachedProperty
96 2
    def serialization(self):
97
        """Serialization information about the shape.
98

99
        This contains information that may be needed for input serialization
100
        or response parsing.  This can include:
101

102
            * name
103
            * queryName
104
            * flattened
105
            * location
106
            * payload
107
            * streaming
108
            * xmlNamespace
109
            * resultWrapper
110
            * xmlAttribute
111
            * jsonvalue
112
            * timestampFormat
113

114
        :rtype: dict
115
        :return: Serialization information about the shape.
116

117
        """
118 11
        model = self._shape_model
119 11
        serialization = {}
120 11
        for attr in self.SERIALIZED_ATTRS:
121 11
            if attr in self._shape_model:
122 11
                serialization[attr] = model[attr]
123
        # For consistency, locationName is renamed to just 'name'.
124 11
        if 'locationName' in serialization:
125 11
            serialization['name'] = serialization.pop('locationName')
126 11
        return serialization
127

128 11
    @CachedProperty
129 2
    def metadata(self):
130
        """Metadata about the shape.
131

132
        This requires optional information about the shape, including:
133

134
            * min
135
            * max
136
            * enum
137
            * sensitive
138
            * required
139
            * idempotencyToken
140

141
        :rtype: dict
142
        :return: Metadata about the shape.
143

144
        """
145 11
        model = self._shape_model
146 11
        metadata = {}
147 11
        for attr in self.METADATA_ATTRS:
148 11
            if attr in self._shape_model:
149 11
                metadata[attr] = model[attr]
150 11
        return metadata
151

152 11
    @CachedProperty
153 2
    def required_members(self):
154
        """A list of members that are required.
155

156
        A structure shape can define members that are required.
157
        This value will return a list of required members.  If there
158
        are no required members an empty list is returned.
159

160
        """
161 11
        return self.metadata.get('required', [])
162

163 11
    def _resolve_shape_ref(self, shape_ref):
164 11
        return self._shape_resolver.resolve_shape_ref(shape_ref)
165

166 11
    def __repr__(self):
167 11
        return "<%s(%s)>" % (self.__class__.__name__,
168
                             self.name)
169

170 11
    @property
171 2
    def event_stream_name(self):
172 11
        return None
173

174

175 11
class StructureShape(Shape):
176 11
    @CachedProperty
177 2
    def members(self):
178 11
        members = self._shape_model['members']
179
        # The members dict looks like:
180
        #    'members': {
181
        #        'MemberName': {'shape': 'shapeName'},
182
        #        'MemberName2': {'shape': 'shapeName'},
183
        #    }
184
        # We return a dict of member name to Shape object.
185 11
        shape_members = self.MAP_TYPE()
186 11
        for name, shape_ref in members.items():
187 11
            shape_members[name] = self._resolve_shape_ref(shape_ref)
188 11
        return shape_members
189

190 11
    @CachedProperty
191 2
    def event_stream_name(self):
192 11
        for member_name, member in self.members.items():
193 11
            if member.serialization.get('eventstream'):
194 11
                return member_name
195 11
        return None
196

197 11
    @CachedProperty
198 2
    def error_code(self):
199 11
        if not self.metadata.get('exception', False):
200 11
            return None
201 11
        error_metadata = self.metadata.get("error", {})
202 11
        code = error_metadata.get("code")
203 11
        if code:
204 11
            return code
205
        # Use the exception name if there is no explicit code modeled
206 11
        return self.name
207

208

209 11
class ListShape(Shape):
210 11
    @CachedProperty
211 2
    def member(self):
212 11
        return self._resolve_shape_ref(self._shape_model['member'])
213

214

215 11
class MapShape(Shape):
216 11
    @CachedProperty
217 2
    def key(self):
218 11
        return self._resolve_shape_ref(self._shape_model['key'])
219

220 11
    @CachedProperty
221 2
    def value(self):
222 11
        return self._resolve_shape_ref(self._shape_model['value'])
223

224

225 11
class StringShape(Shape):
226 11
    @CachedProperty
227 2
    def enum(self):
228 11
        return self.metadata.get('enum', [])
229

230

231 11
class ServiceModel(object):
232
    """
233

234
    :ivar service_description: The parsed service description dictionary.
235

236
    """
237

238 11
    def __init__(self, service_description, service_name=None):
239
        """
240

241
        :type service_description: dict
242
        :param service_description: The service description model.  This value
243
            is obtained from a botocore.loader.Loader, or from directly loading
244
            the file yourself::
245

246
                service_description = json.load(
247
                    open('/path/to/service-description-model.json'))
248
                model = ServiceModel(service_description)
249

250
        :type service_name: str
251
        :param service_name: The name of the service.  Normally this is
252
            the endpoint prefix defined in the service_description.  However,
253
            you can override this value to provide a more convenient name.
254
            This is done in a few places in botocore (ses instead of email,
255
            emr instead of elasticmapreduce).  If this value is not provided,
256
            it will default to the endpointPrefix defined in the model.
257

258
        """
259 11
        self._service_description = service_description
260
        # We want clients to be able to access metadata directly.
261 11
        self.metadata = service_description.get('metadata', {})
262 11
        self._shape_resolver = ShapeResolver(
263
            service_description.get('shapes', {}))
264 11
        self._signature_version = NOT_SET
265 11
        self._service_name = service_name
266 11
        self._instance_cache = {}
267

268 11
    def shape_for(self, shape_name, member_traits=None):
269 11
        return self._shape_resolver.get_shape_by_name(
270
            shape_name, member_traits)
271

272 11
    def shape_for_error_code(self, error_code):
273 11
        return self._error_code_cache.get(error_code, None)
274

275 11
    @CachedProperty
276 2
    def _error_code_cache(self):
277 11
        error_code_cache = {}
278 11
        for error_shape in self.error_shapes:
279 11
            code = error_shape.error_code
280 11
            error_code_cache[code] = error_shape
281 11
        return error_code_cache
282

283 11
    def resolve_shape_ref(self, shape_ref):
284 11
        return self._shape_resolver.resolve_shape_ref(shape_ref)
285

286 11
    @CachedProperty
287 2
    def shape_names(self):
288 11
        return list(self._service_description.get('shapes', {}))
289

290 11
    @CachedProperty
291 2
    def error_shapes(self):
292 11
        error_shapes = []
293 11
        for shape_name in self.shape_names:
294 11
            error_shape = self.shape_for(shape_name)
295 11
            if error_shape.metadata.get('exception', False):
296 11
                error_shapes.append(error_shape)
297 11
        return error_shapes
298

299 11
    @instance_cache
300 2
    def operation_model(self, operation_name):
301 11
        try:
302 11
            model = self._service_description['operations'][operation_name]
303 11
        except KeyError:
304 11
            raise OperationNotFoundError(operation_name)
305 11
        return OperationModel(model, self, operation_name)
306

307 11
    @CachedProperty
308 2
    def documentation(self):
309 11
        return self._service_description.get('documentation', '')
310

311 11
    @CachedProperty
312 2
    def operation_names(self):
313 11
        return list(self._service_description.get('operations', []))
314

315 11
    @CachedProperty
316 2
    def service_name(self):
317
        """The name of the service.
318

319
        This defaults to the endpointPrefix defined in the service model.
320
        However, this value can be overriden when a ``ServiceModel`` is
321
        created.  If a service_name was not provided when the ``ServiceModel``
322
        was created and if there is no endpointPrefix defined in the
323
        service model, then an ``UndefinedModelAttributeError`` exception
324
        will be raised.
325

326
        """
327 11
        if self._service_name is not None:
328 11
            return self._service_name
329
        else:
330 11
            return self.endpoint_prefix
331

332 11
    @CachedProperty
333 2
    def service_id(self):
334 11
        try:
335 11
            return ServiceId(self._get_metadata_property('serviceId'))
336 11
        except UndefinedModelAttributeError:
337 11
            raise MissingServiceIdError(
338
                service_name=self._service_name
339
            )
340

341 11
    @CachedProperty
342 2
    def signing_name(self):
343
        """The name to use when computing signatures.
344

345
        If the model does not define a signing name, this
346
        value will be the endpoint prefix defined in the model.
347
        """
348 11
        signing_name = self.metadata.get('signingName')
349 11
        if signing_name is None:
350 11
            signing_name = self.endpoint_prefix
351 11
        return signing_name
352

353 11
    @CachedProperty
354 2
    def api_version(self):
355 11
        return self._get_metadata_property('apiVersion')
356

357 11
    @CachedProperty
358 2
    def protocol(self):
359 11
        return self._get_metadata_property('protocol')
360

361 11
    @CachedProperty
362 2
    def endpoint_prefix(self):
363 11
        return self._get_metadata_property('endpointPrefix')
364

365 11
    @CachedProperty
366 2
    def endpoint_discovery_operation(self):
367 11
        for operation in self.operation_names:
368 11
            model = self.operation_model(operation)
369 11
            if model.is_endpoint_discovery_operation:
370 11
                return model
371

372 11
    @CachedProperty
373 2
    def endpoint_discovery_required(self):
374 11
        for operation in self.operation_names:
375 11
            model = self.operation_model(operation)
376 11
            if (model.endpoint_discovery is not None and
377
                    model.endpoint_discovery.get('required')):
378 11
                return True
379 11
        return False
380

381 11
    def _get_metadata_property(self, name):
382 11
        try:
383 11
            return self.metadata[name]
384 11
        except KeyError:
385 11
            raise UndefinedModelAttributeError(
386
                '"%s" not defined in the metadata of the model: %s' %
387
                (name, self))
388

389
    # Signature version is one of the rare properties
390
    # than can be modified so a CachedProperty is not used here.
391

392 11
    @property
393 2
    def signature_version(self):
394 0
        if self._signature_version is NOT_SET:
395 0
            signature_version = self.metadata.get('signatureVersion')
396 0
            self._signature_version = signature_version
397 0
        return self._signature_version
398

399 11
    @signature_version.setter
400 2
    def signature_version(self, value):
401 0
        self._signature_version = value
402

403 11
    def __repr__(self):
404 11
        return '%s(%s)' % (self.__class__.__name__, self.service_name)
405

406

407

408 11
class OperationModel(object):
409 11
    def __init__(self, operation_model, service_model, name=None):
410
        """
411

412
        :type operation_model: dict
413
        :param operation_model: The operation model.  This comes from the
414
            service model, and is the value associated with the operation
415
            name in the service model (i.e ``model['operations'][op_name]``).
416

417
        :type service_model: botocore.model.ServiceModel
418
        :param service_model: The service model associated with the operation.
419

420
        :type name: string
421
        :param name: The operation name.  This is the operation name exposed to
422
            the users of this model.  This can potentially be different from
423
            the "wire_name", which is the operation name that *must* by
424
            provided over the wire.  For example, given::
425

426
               "CreateCloudFrontOriginAccessIdentity":{
427
                 "name":"CreateCloudFrontOriginAccessIdentity2014_11_06",
428
                  ...
429
              }
430

431
           The ``name`` would be ``CreateCloudFrontOriginAccessIdentity``,
432
           but the ``self.wire_name`` would be
433
           ``CreateCloudFrontOriginAccessIdentity2014_11_06``, which is the
434
           value we must send in the corresponding HTTP request.
435

436
        """
437 11
        self._operation_model = operation_model
438 11
        self._service_model = service_model
439 11
        self._api_name = name
440
        # Clients can access '.name' to get the operation name
441
        # and '.metadata' to get the top level metdata of the service.
442 11
        self._wire_name = operation_model.get('name')
443 11
        self.metadata = service_model.metadata
444 11
        self.http = operation_model.get('http', {})
445

446 11
    @CachedProperty
447 2
    def name(self):
448 11
        if self._api_name is not None:
449 11
            return self._api_name
450
        else:
451 11
            return self.wire_name
452

453 11
    @property
454 2
    def wire_name(self):
455
        """The wire name of the operation.
456

457
        In many situations this is the same value as the
458
        ``name``, value, but in some services, the operation name
459
        exposed to the user is different from the operaiton name
460
        we send across the wire (e.g cloudfront).
461

462
        Any serialization code should use ``wire_name``.
463

464
        """
465 11
        return self._operation_model.get('name')
466

467 11
    @property
468 2
    def service_model(self):
469 11
        return self._service_model
470

471 11
    @CachedProperty
472 2
    def documentation(self):
473 11
        return self._operation_model.get('documentation', '')
474

475 11
    @CachedProperty
476 2
    def deprecated(self):
477 11
        return self._operation_model.get('deprecated', False)
478

479 11
    @CachedProperty
480 2
    def endpoint_discovery(self):
481
        # Explicit None default. An empty dictionary for this trait means it is
482
        # enabled but not required to be used.
483 11
        return self._operation_model.get('endpointdiscovery', None)
484

485 11
    @CachedProperty
486 2
    def is_endpoint_discovery_operation(self):
487 11
        return self._operation_model.get('endpointoperation', False)
488

489 11
    @CachedProperty
490 2
    def input_shape(self):
491 11
        if 'input' not in self._operation_model:
492
            # Some operations do not accept any input and do not define an
493
            # input shape.
494 11
            return None
495 11
        return self._service_model.resolve_shape_ref(
496
            self._operation_model['input'])
497

498 11
    @CachedProperty
499 2
    def output_shape(self):
500 11
        if 'output' not in self._operation_model:
501
            # Some operations do not define an output shape,
502
            # in which case we return None to indicate the
503
            # operation has no expected output.
504 11
            return None
505 11
        return self._service_model.resolve_shape_ref(
506
            self._operation_model['output'])
507

508 11
    @CachedProperty
509 2
    def idempotent_members(self):
510 11
        input_shape = self.input_shape
511 11
        if not input_shape:
512 11
            return []
513

514 11
        return [name for (name, shape) in input_shape.members.items()
515
                if 'idempotencyToken' in shape.metadata and
516
                shape.metadata['idempotencyToken']]
517

518 11
    @CachedProperty
519 2
    def auth_type(self):
520 11
        return self._operation_model.get('authtype')
521

522 11
    @CachedProperty
523 2
    def error_shapes(self):
524 11
        shapes = self._operation_model.get("errors", [])
525 11
        return list(self._service_model.resolve_shape_ref(s) for s in shapes)
526

527 11
    @CachedProperty
528 2
    def endpoint(self):
529 11
        return self._operation_model.get('endpoint')
530

531 11
    @CachedProperty
532 2
    def http_checksum_required(self):
533 11
        return self._operation_model.get('httpChecksumRequired', False)
534

535 11
    @CachedProperty
536 2
    def has_event_stream_input(self):
537 11
        return self.get_event_stream_input() is not None
538

539 11
    @CachedProperty
540 2
    def has_event_stream_output(self):
541 11
        return self.get_event_stream_output() is not None
542

543 11
    def get_event_stream_input(self):
544 11
        return self._get_event_stream(self.input_shape)
545

546 11
    def get_event_stream_output(self):
547 11
        return self._get_event_stream(self.output_shape)
548

549 11
    def _get_event_stream(self, shape):
550
        """Returns the event stream member's shape if any or None otherwise."""
551 11
        if shape is None:
552 11
            return None
553 11
        event_name = shape.event_stream_name
554 11
        if event_name:
555 11
            return shape.members[event_name]
556 11
        return None
557

558 11
    @CachedProperty
559 2
    def has_streaming_input(self):
560 11
        return self.get_streaming_input() is not None
561

562 11
    @CachedProperty
563 2
    def has_streaming_output(self):
564 11
        return self.get_streaming_output() is not None
565

566 11
    def get_streaming_input(self):
567 11
        return self._get_streaming_body(self.input_shape)
568

569 11
    def get_streaming_output(self):
570 11
        return self._get_streaming_body(self.output_shape)
571

572 11
    def _get_streaming_body(self, shape):
573
        """Returns the streaming member's shape if any; or None otherwise."""
574 11
        if shape is None:
575 11
            return None
576 11
        payload = shape.serialization.get('payload')
577 11
        if payload is not None:
578 11
            payload_shape = shape.members[payload]
579 11
            if payload_shape.type_name == 'blob':
580 11
                return payload_shape
581 11
        return None
582

583 11
    def __repr__(self):
584 11
        return '%s(name=%s)' % (self.__class__.__name__, self.name)
585

586

587 11
class ShapeResolver(object):
588
    """Resolves shape references."""
589

590
    # Any type not in this mapping will default to the Shape class.
591 11
    SHAPE_CLASSES = {
592
        'structure': StructureShape,
593
        'list': ListShape,
594
        'map': MapShape,
595
        'string': StringShape
596
    }
597

598 11
    def __init__(self, shape_map):
599 11
        self._shape_map = shape_map
600 11
        self._shape_cache = {}
601

602 11
    def get_shape_by_name(self, shape_name, member_traits=None):
603 11
        try:
604 11
            shape_model = self._shape_map[shape_name]
605 11
        except KeyError:
606 11
            raise NoShapeFoundError(shape_name)
607 11
        try:
608 11
            shape_cls = self.SHAPE_CLASSES.get(shape_model['type'], Shape)
609 11
        except KeyError:
610 11
            raise InvalidShapeError("Shape is missing required key 'type': %s"
611
                                    % shape_model)
612 11
        if member_traits:
613 11
            shape_model = shape_model.copy()
614 11
            shape_model.update(member_traits)
615 11
        result = shape_cls(shape_name, shape_model, self)
616 11
        return result
617

618 11
    def resolve_shape_ref(self, shape_ref):
619
        # A shape_ref is a dict that has a 'shape' key that
620
        # refers to a shape name as well as any additional
621
        # member traits that are then merged over the shape
622
        # definition.  For example:
623
        # {"shape": "StringType", "locationName": "Foobar"}
624 11
        if len(shape_ref) == 1 and 'shape' in shape_ref:
625
            # It's just a shape ref with no member traits, we can avoid
626
            # a .copy().  This is the common case so it's specifically
627
            # called out here.
628 11
            return self.get_shape_by_name(shape_ref['shape'])
629
        else:
630 11
            member_traits = shape_ref.copy()
631 11
            try:
632 11
                shape_name = member_traits.pop('shape')
633 11
            except KeyError:
634 11
                raise InvalidShapeReferenceError(
635
                    "Invalid model, missing shape reference: %s" % shape_ref)
636 11
            return self.get_shape_by_name(shape_name, member_traits)
637

638

639 11
class UnresolvableShapeMap(object):
640
    """A ShapeResolver that will throw ValueErrors when shapes are resolved.
641
    """
642 11
    def get_shape_by_name(self, shape_name, member_traits=None):
643 0
        raise ValueError("Attempted to lookup shape '%s', but no shape "
644
                         "map was provided.")
645

646 11
    def resolve_shape_ref(self, shape_ref):
647 0
        raise ValueError("Attempted to resolve shape '%s', but no shape "
648
                         "map was provided.")
649

650

651 11
class DenormalizedStructureBuilder(object):
652
    """Build a StructureShape from a denormalized model.
653

654
    This is a convenience builder class that makes it easy to construct
655
    ``StructureShape``s based on a denormalized model.
656

657
    It will handle the details of creating unique shape names and creating
658
    the appropriate shape map needed by the ``StructureShape`` class.
659

660
    Example usage::
661

662
        builder = DenormalizedStructureBuilder()
663
        shape = builder.with_members({
664
            'A': {
665
                'type': 'structure',
666
                'members': {
667
                    'B': {
668
                        'type': 'structure',
669
                        'members': {
670
                            'C': {
671
                                'type': 'string',
672
                            }
673
                        }
674
                    }
675
                }
676
            }
677
        }).build_model()
678
        # ``shape`` is now an instance of botocore.model.StructureShape
679

680
    :type dict_type: class
681
    :param dict_type: The dictionary type to use, allowing you to opt-in
682
                      to using OrderedDict or another dict type. This can
683
                      be particularly useful for testing when order
684
                      matters, such as for documentation.
685

686
    """
687 11
    def __init__(self, name=None):
688 11
        self.members = OrderedDict()
689 11
        self._name_generator = ShapeNameGenerator()
690 11
        if name is None:
691 11
            self.name = self._name_generator.new_shape_name('structure')
692

693 11
    def with_members(self, members):
694
        """
695

696
        :type members: dict
697
        :param members: The denormalized members.
698

699
        :return: self
700

701
        """
702 11
        self._members = members
703 11
        return self
704

705 11
    def build_model(self):
706
        """Build the model based on the provided members.
707

708
        :rtype: botocore.model.StructureShape
709
        :return: The built StructureShape object.
710

711
        """
712 11
        shapes = OrderedDict()
713 11
        denormalized = {
714
            'type': 'structure',
715
            'members': self._members,
716
        }
717 11
        self._build_model(denormalized, shapes, self.name)
718 11
        resolver = ShapeResolver(shape_map=shapes)
719 11
        return StructureShape(shape_name=self.name,
720
                              shape_model=shapes[self.name],
721
                              shape_resolver=resolver)
722

723 11
    def _build_model(self, model, shapes, shape_name):
724 11
        if model['type'] == 'structure':
725 11
            shapes[shape_name] = self._build_structure(model, shapes)
726 11
        elif model['type'] == 'list':
727 11
            shapes[shape_name] = self._build_list(model, shapes)
728 11
        elif model['type'] == 'map':
729 11
            shapes[shape_name] = self._build_map(model, shapes)
730 11
        elif model['type'] in ['string', 'integer', 'boolean', 'blob', 'float',
731
                               'timestamp', 'long', 'double', 'char']:
732 11
            shapes[shape_name] = self._build_scalar(model)
733
        else:
734 11
            raise InvalidShapeError("Unknown shape type: %s" % model['type'])
735

736 11
    def _build_structure(self, model, shapes):
737 11
        members = OrderedDict()
738 11
        shape = self._build_initial_shape(model)
739 11
        shape['members'] = members
740

741 11
        for name, member_model in model['members'].items():
742 11
            member_shape_name = self._get_shape_name(member_model)
743 11
            members[name] = {'shape': member_shape_name}
744 11
            self._build_model(member_model, shapes, member_shape_name)
745 11
        return shape
746

747 11
    def _build_list(self, model, shapes):
748 11
        member_shape_name = self._get_shape_name(model)
749 11
        shape = self._build_initial_shape(model)
750 11
        shape['member'] = {'shape': member_shape_name}
751 11
        self._build_model(model['member'], shapes, member_shape_name)
752 11
        return shape
753

754 11
    def _build_map(self, model, shapes):
755 11
        key_shape_name = self._get_shape_name(model['key'])
756 11
        value_shape_name = self._get_shape_name(model['value'])
757 11
        shape = self._build_initial_shape(model)
758 11
        shape['key'] = {'shape': key_shape_name}
759 11
        shape['value'] = {'shape': value_shape_name}
760 11
        self._build_model(model['key'], shapes, key_shape_name)
761 11
        self._build_model(model['value'], shapes, value_shape_name)
762 11
        return shape
763

764 11
    def _build_initial_shape(self, model):
765 11
        shape = {
766
            'type': model['type'],
767
        }
768 11
        if 'documentation' in model:
769 11
            shape['documentation'] = model['documentation']
770 11
        for attr in Shape.METADATA_ATTRS:
771 11
            if attr in model:
772 11
                shape[attr] = model[attr]
773 11
        return shape
774

775 11
    def _build_scalar(self, model):
776 11
        return self._build_initial_shape(model)
777

778 11
    def _get_shape_name(self, model):
779 11
        if 'shape_name' in model:
780 11
            return model['shape_name']
781
        else:
782 11
            return self._name_generator.new_shape_name(model['type'])
783

784

785 11
class ShapeNameGenerator(object):
786
    """Generate unique shape names for a type.
787

788
    This class can be used in conjunction with the DenormalizedStructureBuilder
789
    to generate unique shape names for a given type.
790

791
    """
792 11
    def __init__(self):
793 11
        self._name_cache = defaultdict(int)
794

795 11
    def new_shape_name(self, type_name):
796
        """Generate a unique shape name.
797

798
        This method will guarantee a unique shape name each time it is
799
        called with the same type.
800

801
        ::
802

803
            >>> s = ShapeNameGenerator()
804
            >>> s.new_shape_name('structure')
805
            'StructureType1'
806
            >>> s.new_shape_name('structure')
807
            'StructureType2'
808
            >>> s.new_shape_name('list')
809
            'ListType1'
810
            >>> s.new_shape_name('list')
811
            'ListType2'
812

813

814
        :type type_name: string
815
        :param type_name: The type name (structure, list, map, string, etc.)
816

817
        :rtype: string
818
        :return: A unique shape name for the given type
819

820
        """
821 11
        self._name_cache[type_name] += 1
822 11
        current_index = self._name_cache[type_name]
823 11
        return '%sType%s' % (type_name.capitalize(),
824
                             current_index)

Read our documentation on viewing source code .

Loading