Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/boto3/resources/model.py: 25%

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

206 statements  

1# Copyright 2014 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# https://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 

14""" 

15The models defined in this file represent the resource JSON description 

16format and provide a layer of abstraction from the raw JSON. The advantages 

17of this are: 

18 

19* Pythonic interface (e.g. ``action.request.operation``) 

20* Consumers need not change for minor JSON changes (e.g. renamed field) 

21 

22These models are used both by the resource factory to generate resource 

23classes as well as by the documentation generator. 

24""" 

25 

26import logging 

27 

28from botocore import xform_name 

29 

30logger = logging.getLogger(__name__) 

31 

32 

33class Identifier: 

34 """ 

35 A resource identifier, given by its name. 

36 

37 :type name: string 

38 :param name: The name of the identifier 

39 """ 

40 

41 def __init__(self, name, member_name=None): 

42 #: (``string``) The name of the identifier 

43 self.name = name 

44 self.member_name = member_name 

45 

46 

47class Action: 

48 """ 

49 A service operation action. 

50 

51 :type name: string 

52 :param name: The name of the action 

53 :type definition: dict 

54 :param definition: The JSON definition 

55 :type resource_defs: dict 

56 :param resource_defs: All resources defined in the service 

57 """ 

58 

59 def __init__(self, name, definition, resource_defs): 

60 self._definition = definition 

61 

62 #: (``string``) The name of the action 

63 self.name = name 

64 #: (:py:class:`Request`) This action's request or ``None`` 

65 self.request = None 

66 if 'request' in definition: 

67 self.request = Request(definition.get('request', {})) 

68 #: (:py:class:`ResponseResource`) This action's resource or ``None`` 

69 self.resource = None 

70 if 'resource' in definition: 

71 self.resource = ResponseResource( 

72 definition.get('resource', {}), resource_defs 

73 ) 

74 #: (``string``) The JMESPath search path or ``None`` 

75 self.path = definition.get('path') 

76 

77 

78class DefinitionWithParams: 

79 """ 

80 An item which has parameters exposed via the ``params`` property. 

81 A request has an operation and parameters, while a waiter has 

82 a name, a low-level waiter name and parameters. 

83 

84 :type definition: dict 

85 :param definition: The JSON definition 

86 """ 

87 

88 def __init__(self, definition): 

89 self._definition = definition 

90 

91 @property 

92 def params(self): 

93 """ 

94 Get a list of auto-filled parameters for this request. 

95 

96 :type: list(:py:class:`Parameter`) 

97 """ 

98 params = [] 

99 

100 for item in self._definition.get('params', []): 

101 params.append(Parameter(**item)) 

102 

103 return params 

104 

105 

106class Parameter: 

107 """ 

108 An auto-filled parameter which has a source and target. For example, 

109 the ``QueueUrl`` may be auto-filled from a resource's ``url`` identifier 

110 when making calls to ``queue.receive_messages``. 

111 

112 :type target: string 

113 :param target: The destination parameter name, e.g. ``QueueUrl`` 

114 :type source_type: string 

115 :param source_type: Where the source is defined. 

116 :type source: string 

117 :param source: The source name, e.g. ``Url`` 

118 """ 

119 

120 def __init__( 

121 self, target, source, name=None, path=None, value=None, **kwargs 

122 ): 

123 #: (``string``) The destination parameter name 

124 self.target = target 

125 #: (``string``) Where the source is defined 

126 self.source = source 

127 #: (``string``) The name of the source, if given 

128 self.name = name 

129 #: (``string``) The JMESPath query of the source 

130 self.path = path 

131 #: (``string|int|float|bool``) The source constant value 

132 self.value = value 

133 

134 # Complain if we encounter any unknown values. 

135 if kwargs: 

136 logger.warning('Unknown parameter options found: %s', kwargs) 

137 

138 

139class Request(DefinitionWithParams): 

140 """ 

141 A service operation action request. 

142 

143 :type definition: dict 

144 :param definition: The JSON definition 

145 """ 

146 

147 def __init__(self, definition): 

148 super().__init__(definition) 

149 

150 #: (``string``) The name of the low-level service operation 

151 self.operation = definition.get('operation') 

152 

153 

154class Waiter(DefinitionWithParams): 

155 """ 

156 An event waiter specification. 

157 

158 :type name: string 

159 :param name: Name of the waiter 

160 :type definition: dict 

161 :param definition: The JSON definition 

162 """ 

163 

164 PREFIX = 'WaitUntil' 

165 

166 def __init__(self, name, definition): 

167 super().__init__(definition) 

168 

169 #: (``string``) The name of this waiter 

170 self.name = name 

171 

172 #: (``string``) The name of the underlying event waiter 

173 self.waiter_name = definition.get('waiterName') 

174 

175 

176class ResponseResource: 

177 """ 

178 A resource response to create after performing an action. 

179 

180 :type definition: dict 

181 :param definition: The JSON definition 

182 :type resource_defs: dict 

183 :param resource_defs: All resources defined in the service 

184 """ 

185 

186 def __init__(self, definition, resource_defs): 

187 self._definition = definition 

188 self._resource_defs = resource_defs 

189 

190 #: (``string``) The name of the response resource type 

191 self.type = definition.get('type') 

192 

193 #: (``string``) The JMESPath search query or ``None`` 

194 self.path = definition.get('path') 

195 

196 @property 

197 def identifiers(self): 

198 """ 

199 A list of resource identifiers. 

200 

201 :type: list(:py:class:`Identifier`) 

202 """ 

203 identifiers = [] 

204 

205 for item in self._definition.get('identifiers', []): 

206 identifiers.append(Parameter(**item)) 

207 

208 return identifiers 

209 

210 @property 

211 def model(self): 

212 """ 

213 Get the resource model for the response resource. 

214 

215 :type: :py:class:`ResourceModel` 

216 """ 

217 return ResourceModel( 

218 self.type, self._resource_defs[self.type], self._resource_defs 

219 ) 

220 

221 

222class Collection(Action): 

223 """ 

224 A group of resources. See :py:class:`Action`. 

225 

226 :type name: string 

227 :param name: The name of the collection 

228 :type definition: dict 

229 :param definition: The JSON definition 

230 :type resource_defs: dict 

231 :param resource_defs: All resources defined in the service 

232 """ 

233 

234 @property 

235 def batch_actions(self): 

236 """ 

237 Get a list of batch actions supported by the resource type 

238 contained in this action. This is a shortcut for accessing 

239 the same information through the resource model. 

240 

241 :rtype: list(:py:class:`Action`) 

242 """ 

243 return self.resource.model.batch_actions 

244 

245 

246class ResourceModel: 

247 """ 

248 A model representing a resource, defined via a JSON description 

249 format. A resource has identifiers, attributes, actions, 

250 sub-resources, references and collections. For more information 

251 on resources, see :ref:`guide_resources`. 

252 

253 :type name: string 

254 :param name: The name of this resource, e.g. ``sqs`` or ``Queue`` 

255 :type definition: dict 

256 :param definition: The JSON definition 

257 :type resource_defs: dict 

258 :param resource_defs: All resources defined in the service 

259 """ 

260 

261 def __init__(self, name, definition, resource_defs): 

262 self._definition = definition 

263 self._resource_defs = resource_defs 

264 self._renamed = {} 

265 

266 #: (``string``) The name of this resource 

267 self.name = name 

268 #: (``string``) The service shape name for this resource or ``None`` 

269 self.shape = definition.get('shape') 

270 

271 def load_rename_map(self, shape=None): 

272 """ 

273 Load a name translation map given a shape. This will set 

274 up renamed values for any collisions, e.g. if the shape, 

275 an action, and a subresource all are all named ``foo`` 

276 then the resource will have an action ``foo``, a subresource 

277 named ``Foo`` and a property named ``foo_attribute``. 

278 This is the order of precedence, from most important to 

279 least important: 

280 

281 * Load action (resource.load) 

282 * Identifiers 

283 * Actions 

284 * Subresources 

285 * References 

286 * Collections 

287 * Waiters 

288 * Attributes (shape members) 

289 

290 Batch actions are only exposed on collections, so do not 

291 get modified here. Subresources use upper camel casing, so 

292 are unlikely to collide with anything but other subresources. 

293 

294 Creates a structure like this:: 

295 

296 renames = { 

297 ('action', 'id'): 'id_action', 

298 ('collection', 'id'): 'id_collection', 

299 ('attribute', 'id'): 'id_attribute' 

300 } 

301 

302 # Get the final name for an action named 'id' 

303 name = renames.get(('action', 'id'), 'id') 

304 

305 :type shape: botocore.model.Shape 

306 :param shape: The underlying shape for this resource. 

307 """ 

308 # Meta is a reserved name for resources 

309 names = {'meta'} 

310 self._renamed = {} 

311 

312 if self._definition.get('load'): 

313 names.add('load') 

314 

315 for item in self._definition.get('identifiers', []): 

316 self._load_name_with_category(names, item['name'], 'identifier') 

317 

318 for name in self._definition.get('actions', {}): 

319 self._load_name_with_category(names, name, 'action') 

320 

321 for name, ref in self._get_has_definition().items(): 

322 # Subresources require no data members, just typically 

323 # identifiers and user input. 

324 data_required = False 

325 for identifier in ref['resource']['identifiers']: 

326 if identifier['source'] == 'data': 

327 data_required = True 

328 break 

329 

330 if not data_required: 

331 self._load_name_with_category( 

332 names, name, 'subresource', snake_case=False 

333 ) 

334 else: 

335 self._load_name_with_category(names, name, 'reference') 

336 

337 for name in self._definition.get('hasMany', {}): 

338 self._load_name_with_category(names, name, 'collection') 

339 

340 for name in self._definition.get('waiters', {}): 

341 self._load_name_with_category( 

342 names, Waiter.PREFIX + name, 'waiter' 

343 ) 

344 

345 if shape is not None: 

346 for name in shape.members.keys(): 

347 self._load_name_with_category(names, name, 'attribute') 

348 

349 def _load_name_with_category(self, names, name, category, snake_case=True): 

350 """ 

351 Load a name with a given category, possibly renaming it 

352 if that name is already in use. The name will be stored 

353 in ``names`` and possibly be set up in ``self._renamed``. 

354 

355 :type names: set 

356 :param names: Existing names (Python attributes, properties, or 

357 methods) on the resource. 

358 :type name: string 

359 :param name: The original name of the value. 

360 :type category: string 

361 :param category: The value type, such as 'identifier' or 'action' 

362 :type snake_case: bool 

363 :param snake_case: True (default) if the name should be snake cased. 

364 """ 

365 if snake_case: 

366 name = xform_name(name) 

367 

368 if name in names: 

369 logger.debug(f'Renaming {self.name} {category} {name}') 

370 self._renamed[(category, name)] = name + '_' + category 

371 name += '_' + category 

372 

373 if name in names: 

374 # This isn't good, let's raise instead of trying to keep 

375 # renaming this value. 

376 raise ValueError( 

377 f'Problem renaming {self.name} {category} to {name}!' 

378 ) 

379 

380 names.add(name) 

381 

382 def _get_name(self, category, name, snake_case=True): 

383 """ 

384 Get a possibly renamed value given a category and name. This 

385 uses the rename map set up in ``load_rename_map``, so that 

386 method must be called once first. 

387 

388 :type category: string 

389 :param category: The value type, such as 'identifier' or 'action' 

390 :type name: string 

391 :param name: The original name of the value 

392 :type snake_case: bool 

393 :param snake_case: True (default) if the name should be snake cased. 

394 :rtype: string 

395 :return: Either the renamed value if it is set, otherwise the 

396 original name. 

397 """ 

398 if snake_case: 

399 name = xform_name(name) 

400 

401 return self._renamed.get((category, name), name) 

402 

403 def get_attributes(self, shape): 

404 """ 

405 Get a dictionary of attribute names to original name and shape 

406 models that represent the attributes of this resource. Looks 

407 like the following: 

408 

409 { 

410 'some_name': ('SomeName', <Shape...>) 

411 } 

412 

413 :type shape: botocore.model.Shape 

414 :param shape: The underlying shape for this resource. 

415 :rtype: dict 

416 :return: Mapping of resource attributes. 

417 """ 

418 attributes = {} 

419 identifier_names = [i.name for i in self.identifiers] 

420 

421 for name, member in shape.members.items(): 

422 snake_cased = xform_name(name) 

423 if snake_cased in identifier_names: 

424 # Skip identifiers, these are set through other means 

425 continue 

426 snake_cased = self._get_name( 

427 'attribute', snake_cased, snake_case=False 

428 ) 

429 attributes[snake_cased] = (name, member) 

430 

431 return attributes 

432 

433 @property 

434 def identifiers(self): 

435 """ 

436 Get a list of resource identifiers. 

437 

438 :type: list(:py:class:`Identifier`) 

439 """ 

440 identifiers = [] 

441 

442 for item in self._definition.get('identifiers', []): 

443 name = self._get_name('identifier', item['name']) 

444 member_name = item.get('memberName', None) 

445 if member_name: 

446 member_name = self._get_name('attribute', member_name) 

447 identifiers.append(Identifier(name, member_name)) 

448 

449 return identifiers 

450 

451 @property 

452 def load(self): 

453 """ 

454 Get the load action for this resource, if it is defined. 

455 

456 :type: :py:class:`Action` or ``None`` 

457 """ 

458 action = self._definition.get('load') 

459 

460 if action is not None: 

461 action = Action('load', action, self._resource_defs) 

462 

463 return action 

464 

465 @property 

466 def actions(self): 

467 """ 

468 Get a list of actions for this resource. 

469 

470 :type: list(:py:class:`Action`) 

471 """ 

472 actions = [] 

473 

474 for name, item in self._definition.get('actions', {}).items(): 

475 name = self._get_name('action', name) 

476 actions.append(Action(name, item, self._resource_defs)) 

477 

478 return actions 

479 

480 @property 

481 def batch_actions(self): 

482 """ 

483 Get a list of batch actions for this resource. 

484 

485 :type: list(:py:class:`Action`) 

486 """ 

487 actions = [] 

488 

489 for name, item in self._definition.get('batchActions', {}).items(): 

490 name = self._get_name('batch_action', name) 

491 actions.append(Action(name, item, self._resource_defs)) 

492 

493 return actions 

494 

495 def _get_has_definition(self): 

496 """ 

497 Get a ``has`` relationship definition from a model, where the 

498 service resource model is treated special in that it contains 

499 a relationship to every resource defined for the service. This 

500 allows things like ``s3.Object('bucket-name', 'key')`` to 

501 work even though the JSON doesn't define it explicitly. 

502 

503 :rtype: dict 

504 :return: Mapping of names to subresource and reference 

505 definitions. 

506 """ 

507 if self.name not in self._resource_defs: 

508 # This is the service resource, so let us expose all of 

509 # the defined resources as subresources. 

510 definition = {} 

511 

512 for name, resource_def in self._resource_defs.items(): 

513 # It's possible for the service to have renamed a 

514 # resource or to have defined multiple names that 

515 # point to the same resource type, so we need to 

516 # take that into account. 

517 found = False 

518 has_items = self._definition.get('has', {}).items() 

519 for has_name, has_def in has_items: 

520 if has_def.get('resource', {}).get('type') == name: 

521 definition[has_name] = has_def 

522 found = True 

523 

524 if not found: 

525 # Create a relationship definition and attach it 

526 # to the model, such that all identifiers must be 

527 # supplied by the user. It will look something like: 

528 # 

529 # { 

530 # 'resource': { 

531 # 'type': 'ResourceName', 

532 # 'identifiers': [ 

533 # {'target': 'Name1', 'source': 'input'}, 

534 # {'target': 'Name2', 'source': 'input'}, 

535 # ... 

536 # ] 

537 # } 

538 # } 

539 # 

540 fake_has = {'resource': {'type': name, 'identifiers': []}} 

541 

542 for identifier in resource_def.get('identifiers', []): 

543 fake_has['resource']['identifiers'].append( 

544 {'target': identifier['name'], 'source': 'input'} 

545 ) 

546 

547 definition[name] = fake_has 

548 else: 

549 definition = self._definition.get('has', {}) 

550 

551 return definition 

552 

553 def _get_related_resources(self, subresources): 

554 """ 

555 Get a list of sub-resources or references. 

556 

557 :type subresources: bool 

558 :param subresources: ``True`` to get sub-resources, ``False`` to 

559 get references. 

560 :rtype: list(:py:class:`Action`) 

561 """ 

562 resources = [] 

563 

564 for name, definition in self._get_has_definition().items(): 

565 if subresources: 

566 name = self._get_name('subresource', name, snake_case=False) 

567 else: 

568 name = self._get_name('reference', name) 

569 action = Action(name, definition, self._resource_defs) 

570 

571 data_required = False 

572 for identifier in action.resource.identifiers: 

573 if identifier.source == 'data': 

574 data_required = True 

575 break 

576 

577 if subresources and not data_required: 

578 resources.append(action) 

579 elif not subresources and data_required: 

580 resources.append(action) 

581 

582 return resources 

583 

584 @property 

585 def subresources(self): 

586 """ 

587 Get a list of sub-resources. 

588 

589 :type: list(:py:class:`Action`) 

590 """ 

591 return self._get_related_resources(True) 

592 

593 @property 

594 def references(self): 

595 """ 

596 Get a list of reference resources. 

597 

598 :type: list(:py:class:`Action`) 

599 """ 

600 return self._get_related_resources(False) 

601 

602 @property 

603 def collections(self): 

604 """ 

605 Get a list of collections for this resource. 

606 

607 :type: list(:py:class:`Collection`) 

608 """ 

609 collections = [] 

610 

611 for name, item in self._definition.get('hasMany', {}).items(): 

612 name = self._get_name('collection', name) 

613 collections.append(Collection(name, item, self._resource_defs)) 

614 

615 return collections 

616 

617 @property 

618 def waiters(self): 

619 """ 

620 Get a list of waiters for this resource. 

621 

622 :type: list(:py:class:`Waiter`) 

623 """ 

624 waiters = [] 

625 

626 for name, item in self._definition.get('waiters', {}).items(): 

627 name = self._get_name('waiter', Waiter.PREFIX + name) 

628 waiters.append(Waiter(name, item)) 

629 

630 return waiters