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

446 statements  

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.""" 

14 

15from collections import defaultdict 

16from typing import NamedTuple, Union 

17 

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 

25 

26NOT_SET = object() 

27 

28 

29class NoShapeFoundError(Exception): 

30 pass 

31 

32 

33class InvalidShapeError(Exception): 

34 pass 

35 

36 

37class OperationNotFoundError(Exception): 

38 pass 

39 

40 

41class InvalidShapeReferenceError(Exception): 

42 pass 

43 

44 

45class ServiceId(str): 

46 def hyphenize(self): 

47 return hyphenize_service_id(self) 

48 

49 

50class Shape: 

51 """Object representing a shape from the service model.""" 

52 

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 

95 

96 def __init__(self, shape_name, shape_model, shape_resolver=None): 

97 """ 

98 

99 :type shape_name: string 

100 :param shape_name: The name of the shape. 

101 

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]``) 

106 

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. 

114 

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 = {} 

129 

130 @CachedProperty 

131 def serialization(self): 

132 """Serialization information about the shape. 

133 

134 This contains information that may be needed for input serialization 

135 or response parsing. This can include: 

136 

137 * name 

138 * queryName 

139 * flattened 

140 * location 

141 * payload 

142 * streaming 

143 * xmlNamespace 

144 * resultWrapper 

145 * xmlAttribute 

146 * jsonvalue 

147 * timestampFormat 

148 

149 :rtype: dict 

150 :return: Serialization information about the shape. 

151 

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 

162 

163 @CachedProperty 

164 def metadata(self): 

165 """Metadata about the shape. 

166 

167 This requires optional information about the shape, including: 

168 

169 * min 

170 * max 

171 * pattern 

172 * enum 

173 * sensitive 

174 * required 

175 * idempotencyToken 

176 * document 

177 * union 

178 * contextParam 

179 * clientContextParams 

180 * requiresLength 

181 

182 :rtype: dict 

183 :return: Metadata about the shape. 

184 

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 

192 

193 @CachedProperty 

194 def required_members(self): 

195 """A list of members that are required. 

196 

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. 

200 

201 """ 

202 return self.metadata.get('required', []) 

203 

204 def _resolve_shape_ref(self, shape_ref): 

205 return self._shape_resolver.resolve_shape_ref(shape_ref) 

206 

207 def __repr__(self): 

208 return f"<{self.__class__.__name__}({self.name})>" 

209 

210 @property 

211 def event_stream_name(self): 

212 return None 

213 

214 

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 

229 

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 

236 

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 

247 

248 @CachedProperty 

249 def is_document_type(self): 

250 return self.metadata.get('document', False) 

251 

252 @CachedProperty 

253 def is_tagged_union(self): 

254 return self.metadata.get('union', False) 

255 

256 

257class ListShape(Shape): 

258 @CachedProperty 

259 def member(self): 

260 return self._resolve_shape_ref(self._shape_model['member']) 

261 

262 

263class MapShape(Shape): 

264 @CachedProperty 

265 def key(self): 

266 return self._resolve_shape_ref(self._shape_model['key']) 

267 

268 @CachedProperty 

269 def value(self): 

270 return self._resolve_shape_ref(self._shape_model['value']) 

271 

272 

273class StringShape(Shape): 

274 @CachedProperty 

275 def enum(self): 

276 return self.metadata.get('enum', []) 

277 

278 

279class StaticContextParameter(NamedTuple): 

280 name: str 

281 value: Union[bool, str] 

282 

283 

284class ContextParameter(NamedTuple): 

285 name: str 

286 member_name: str 

287 

288 

289class ClientContextParameter(NamedTuple): 

290 name: str 

291 type: str 

292 documentation: str 

293 

294 

295class ServiceModel: 

296 """ 

297 

298 :ivar service_description: The parsed service description dictionary. 

299 

300 """ 

301 

302 def __init__(self, service_description, service_name=None): 

303 """ 

304 

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:: 

309 

310 service_description = json.load( 

311 open('/path/to/service-description-model.json')) 

312 model = ServiceModel(service_description) 

313 

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. 

321 

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 = {} 

332 

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 ) 

337 

338 def shape_for_error_code(self, error_code): 

339 return self._error_code_cache.get(error_code, None) 

340 

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 

348 

349 def resolve_shape_ref(self, shape_ref): 

350 return self._shape_resolver.resolve_shape_ref(shape_ref) 

351 

352 @CachedProperty 

353 def shape_names(self): 

354 return list(self._service_description.get('shapes', {})) 

355 

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 

364 

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) 

372 

373 @CachedProperty 

374 def documentation(self): 

375 return self._service_description.get('documentation', '') 

376 

377 @CachedProperty 

378 def operation_names(self): 

379 return list(self._service_description.get('operations', [])) 

380 

381 @CachedProperty 

382 def service_name(self): 

383 """The name of the service. 

384 

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. 

391 

392 """ 

393 if self._service_name is not None: 

394 return self._service_name 

395 else: 

396 return self.endpoint_prefix 

397 

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) 

404 

405 @CachedProperty 

406 def signing_name(self): 

407 """The name to use when computing signatures. 

408 

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 

416 

417 @CachedProperty 

418 def api_version(self): 

419 return self._get_metadata_property('apiVersion') 

420 

421 @CachedProperty 

422 def protocol(self): 

423 return self._get_metadata_property('protocol') 

424 

425 @CachedProperty 

426 def endpoint_prefix(self): 

427 return self._get_metadata_property('endpointPrefix') 

428 

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 

435 

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 

446 

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 ] 

458 

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 ) 

466 

467 # Signature version is one of the rare properties 

468 # that can be modified so a CachedProperty is not used here. 

469 

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 

476 

477 @signature_version.setter 

478 def signature_version(self, value): 

479 self._signature_version = value 

480 

481 def __repr__(self): 

482 return f'{self.__class__.__name__}({self.service_name})' 

483 

484 

485class OperationModel: 

486 def __init__(self, operation_model, service_model, name=None): 

487 """ 

488 

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]``). 

493 

494 :type service_model: botocore.model.ServiceModel 

495 :param service_model: The service model associated with the operation. 

496 

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:: 

502 

503 "CreateCloudFrontOriginAccessIdentity":{ 

504 "name":"CreateCloudFrontOriginAccessIdentity2014_11_06", 

505 ... 

506 } 

507 

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. 

512 

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', {}) 

522 

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 

529 

530 @property 

531 def wire_name(self): 

532 """The wire name of the operation. 

533 

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). 

538 

539 Any serialization code should use ``wire_name``. 

540 

541 """ 

542 return self._operation_model.get('name') 

543 

544 @property 

545 def service_model(self): 

546 return self._service_model 

547 

548 @CachedProperty 

549 def documentation(self): 

550 return self._operation_model.get('documentation', '') 

551 

552 @CachedProperty 

553 def deprecated(self): 

554 return self._operation_model.get('deprecated', False) 

555 

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) 

561 

562 @CachedProperty 

563 def is_endpoint_discovery_operation(self): 

564 return self._operation_model.get('endpointoperation', False) 

565 

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 ) 

575 

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 ) 

586 

587 @CachedProperty 

588 def idempotent_members(self): 

589 input_shape = self.input_shape 

590 if not input_shape: 

591 return [] 

592 

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 ] 

599 

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 ] 

607 

608 @CachedProperty 

609 def context_parameters(self): 

610 if not self.input_shape: 

611 return [] 

612 

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 ] 

622 

623 @CachedProperty 

624 def request_compression(self): 

625 return self._operation_model.get('requestcompression') 

626 

627 @CachedProperty 

628 def auth(self): 

629 return self._operation_model.get('auth') 

630 

631 @CachedProperty 

632 def auth_type(self): 

633 return self._operation_model.get('authtype') 

634 

635 @CachedProperty 

636 def resolved_auth_type(self): 

637 if self.auth: 

638 return resolve_auth_type(self.auth) 

639 return self.auth_type 

640 

641 @CachedProperty 

642 def unsigned_payload(self): 

643 return self._operation_model.get('unsignedPayload') 

644 

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) 

649 

650 @CachedProperty 

651 def endpoint(self): 

652 return self._operation_model.get('endpoint') 

653 

654 @CachedProperty 

655 def http_checksum_required(self): 

656 return self._operation_model.get('httpChecksumRequired', False) 

657 

658 @CachedProperty 

659 def http_checksum(self): 

660 return self._operation_model.get('httpChecksum', {}) 

661 

662 @CachedProperty 

663 def has_event_stream_input(self): 

664 return self.get_event_stream_input() is not None 

665 

666 @CachedProperty 

667 def has_event_stream_output(self): 

668 return self.get_event_stream_output() is not None 

669 

670 def get_event_stream_input(self): 

671 return self._get_event_stream(self.input_shape) 

672 

673 def get_event_stream_output(self): 

674 return self._get_event_stream(self.output_shape) 

675 

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 

684 

685 @CachedProperty 

686 def has_streaming_input(self): 

687 return self.get_streaming_input() is not None 

688 

689 @CachedProperty 

690 def has_streaming_output(self): 

691 return self.get_streaming_output() is not None 

692 

693 def get_streaming_input(self): 

694 return self._get_streaming_body(self.input_shape) 

695 

696 def get_streaming_output(self): 

697 return self._get_streaming_body(self.output_shape) 

698 

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 

709 

710 def __repr__(self): 

711 return f'{self.__class__.__name__}(name={self.name})' 

712 

713 

714class ShapeResolver: 

715 """Resolves shape references.""" 

716 

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 } 

724 

725 def __init__(self, shape_map): 

726 self._shape_map = shape_map 

727 self._shape_cache = {} 

728 

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 

745 

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) 

766 

767 

768class UnresolvableShapeMap: 

769 """A ShapeResolver that will throw ValueErrors when shapes are resolved.""" 

770 

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 ) 

775 

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 ) 

781 

782 

783class DenormalizedStructureBuilder: 

784 """Build a StructureShape from a denormalized model. 

785 

786 This is a convenience builder class that makes it easy to construct 

787 ``StructureShape``s based on a denormalized model. 

788 

789 It will handle the details of creating unique shape names and creating 

790 the appropriate shape map needed by the ``StructureShape`` class. 

791 

792 Example usage:: 

793 

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 

811 

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. 

817 

818 """ 

819 

820 SCALAR_TYPES = ( 

821 'string', 

822 'integer', 

823 'boolean', 

824 'blob', 

825 'float', 

826 'timestamp', 

827 'long', 

828 'double', 

829 'char', 

830 ) 

831 

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') 

837 

838 def with_members(self, members): 

839 """ 

840 

841 :type members: dict 

842 :param members: The denormalized members. 

843 

844 :return: self 

845 

846 """ 

847 self._members = members 

848 return self 

849 

850 def build_model(self): 

851 """Build the model based on the provided members. 

852 

853 :rtype: botocore.model.StructureShape 

854 :return: The built StructureShape object. 

855 

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 ) 

869 

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']}") 

881 

882 def _build_structure(self, model, shapes): 

883 members = OrderedDict() 

884 shape = self._build_initial_shape(model) 

885 shape['members'] = members 

886 

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 

892 

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 

899 

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 

909 

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 

920 

921 def _build_scalar(self, model): 

922 return self._build_initial_shape(model) 

923 

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']) 

929 

930 

931class ShapeNameGenerator: 

932 """Generate unique shape names for a type. 

933 

934 This class can be used in conjunction with the DenormalizedStructureBuilder 

935 to generate unique shape names for a given type. 

936 

937 """ 

938 

939 def __init__(self): 

940 self._name_cache = defaultdict(int) 

941 

942 def new_shape_name(self, type_name): 

943 """Generate a unique shape name. 

944 

945 This method will guarantee a unique shape name each time it is 

946 called with the same type. 

947 

948 :: 

949 

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' 

959 

960 

961 :type type_name: string 

962 :param type_name: The type name (structure, list, map, string, etc.) 

963 

964 :rtype: string 

965 :return: A unique shape name for the given type 

966 

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}'