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

206 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

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 'Problem renaming {} {} to {}!'.format( 

378 self.name, category, name 

379 ) 

380 ) 

381 

382 names.add(name) 

383 

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

385 """ 

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

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

388 method must be called once first. 

389 

390 :type category: string 

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

392 :type name: string 

393 :param name: The original name of the value 

394 :type snake_case: bool 

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

396 :rtype: string 

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

398 original name. 

399 """ 

400 if snake_case: 

401 name = xform_name(name) 

402 

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

404 

405 def get_attributes(self, shape): 

406 """ 

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

408 models that represent the attributes of this resource. Looks 

409 like the following: 

410 

411 { 

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

413 } 

414 

415 :type shape: botocore.model.Shape 

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

417 :rtype: dict 

418 :return: Mapping of resource attributes. 

419 """ 

420 attributes = {} 

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

422 

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

424 snake_cased = xform_name(name) 

425 if snake_cased in identifier_names: 

426 # Skip identifiers, these are set through other means 

427 continue 

428 snake_cased = self._get_name( 

429 'attribute', snake_cased, snake_case=False 

430 ) 

431 attributes[snake_cased] = (name, member) 

432 

433 return attributes 

434 

435 @property 

436 def identifiers(self): 

437 """ 

438 Get a list of resource identifiers. 

439 

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

441 """ 

442 identifiers = [] 

443 

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

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

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

447 if member_name: 

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

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

450 

451 return identifiers 

452 

453 @property 

454 def load(self): 

455 """ 

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

457 

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

459 """ 

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

461 

462 if action is not None: 

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

464 

465 return action 

466 

467 @property 

468 def actions(self): 

469 """ 

470 Get a list of actions for this resource. 

471 

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

473 """ 

474 actions = [] 

475 

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

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

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

479 

480 return actions 

481 

482 @property 

483 def batch_actions(self): 

484 """ 

485 Get a list of batch actions for this resource. 

486 

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

488 """ 

489 actions = [] 

490 

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

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

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

494 

495 return actions 

496 

497 def _get_has_definition(self): 

498 """ 

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

500 service resource model is treated special in that it contains 

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

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

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

504 

505 :rtype: dict 

506 :return: Mapping of names to subresource and reference 

507 definitions. 

508 """ 

509 if self.name not in self._resource_defs: 

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

511 # the defined resources as subresources. 

512 definition = {} 

513 

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

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

516 # resource or to have defined multiple names that 

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

518 # take that into account. 

519 found = False 

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

521 for has_name, has_def in has_items: 

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

523 definition[has_name] = has_def 

524 found = True 

525 

526 if not found: 

527 # Create a relationship definition and attach it 

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

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

530 # 

531 # { 

532 # 'resource': { 

533 # 'type': 'ResourceName', 

534 # 'identifiers': [ 

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

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

537 # ... 

538 # ] 

539 # } 

540 # } 

541 # 

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

543 

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

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

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

547 ) 

548 

549 definition[name] = fake_has 

550 else: 

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

552 

553 return definition 

554 

555 def _get_related_resources(self, subresources): 

556 """ 

557 Get a list of sub-resources or references. 

558 

559 :type subresources: bool 

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

561 get references. 

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

563 """ 

564 resources = [] 

565 

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

567 if subresources: 

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

569 else: 

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

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

572 

573 data_required = False 

574 for identifier in action.resource.identifiers: 

575 if identifier.source == 'data': 

576 data_required = True 

577 break 

578 

579 if subresources and not data_required: 

580 resources.append(action) 

581 elif not subresources and data_required: 

582 resources.append(action) 

583 

584 return resources 

585 

586 @property 

587 def subresources(self): 

588 """ 

589 Get a list of sub-resources. 

590 

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

592 """ 

593 return self._get_related_resources(True) 

594 

595 @property 

596 def references(self): 

597 """ 

598 Get a list of reference resources. 

599 

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

601 """ 

602 return self._get_related_resources(False) 

603 

604 @property 

605 def collections(self): 

606 """ 

607 Get a list of collections for this resource. 

608 

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

610 """ 

611 collections = [] 

612 

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

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

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

616 

617 return collections 

618 

619 @property 

620 def waiters(self): 

621 """ 

622 Get a list of waiters for this resource. 

623 

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

625 """ 

626 waiters = [] 

627 

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

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

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

631 

632 return waiters