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

152 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 

14import logging 

15from functools import partial 

16 

17from ..docs import docstring 

18from ..exceptions import ResourceLoadException 

19from .action import ServiceAction, WaiterAction 

20from .base import ResourceMeta, ServiceResource 

21from .collection import CollectionFactory 

22from .model import ResourceModel 

23from .response import ResourceHandler, build_identifiers 

24 

25logger = logging.getLogger(__name__) 

26 

27 

28class ResourceFactory: 

29 """ 

30 A factory to create new :py:class:`~boto3.resources.base.ServiceResource` 

31 classes from a :py:class:`~boto3.resources.model.ResourceModel`. There are 

32 two types of lookups that can be done: one on the service itself (e.g. an 

33 SQS resource) and another on models contained within the service (e.g. an 

34 SQS Queue resource). 

35 """ 

36 

37 def __init__(self, emitter): 

38 self._collection_factory = CollectionFactory() 

39 self._emitter = emitter 

40 

41 def load_from_definition( 

42 self, resource_name, single_resource_json_definition, service_context 

43 ): 

44 """ 

45 Loads a resource from a model, creating a new 

46 :py:class:`~boto3.resources.base.ServiceResource` subclass 

47 with the correct properties and methods, named based on the service 

48 and resource name, e.g. EC2.Instance. 

49 

50 :type resource_name: string 

51 :param resource_name: Name of the resource to look up. For services, 

52 this should match the ``service_name``. 

53 

54 :type single_resource_json_definition: dict 

55 :param single_resource_json_definition: 

56 The loaded json of a single service resource or resource 

57 definition. 

58 

59 :type service_context: :py:class:`~boto3.utils.ServiceContext` 

60 :param service_context: Context about the AWS service 

61 

62 :rtype: Subclass of :py:class:`~boto3.resources.base.ServiceResource` 

63 :return: The service or resource class. 

64 """ 

65 logger.debug( 

66 'Loading %s:%s', service_context.service_name, resource_name 

67 ) 

68 

69 # Using the loaded JSON create a ResourceModel object. 

70 resource_model = ResourceModel( 

71 resource_name, 

72 single_resource_json_definition, 

73 service_context.resource_json_definitions, 

74 ) 

75 

76 # Do some renaming of the shape if there was a naming collision 

77 # that needed to be accounted for. 

78 shape = None 

79 if resource_model.shape: 

80 shape = service_context.service_model.shape_for( 

81 resource_model.shape 

82 ) 

83 resource_model.load_rename_map(shape) 

84 

85 # Set some basic info 

86 meta = ResourceMeta( 

87 service_context.service_name, resource_model=resource_model 

88 ) 

89 attrs = { 

90 'meta': meta, 

91 } 

92 

93 # Create and load all of attributes of the resource class based 

94 # on the models. 

95 

96 # Identifiers 

97 self._load_identifiers( 

98 attrs=attrs, 

99 meta=meta, 

100 resource_name=resource_name, 

101 resource_model=resource_model, 

102 ) 

103 

104 # Load/Reload actions 

105 self._load_actions( 

106 attrs=attrs, 

107 resource_name=resource_name, 

108 resource_model=resource_model, 

109 service_context=service_context, 

110 ) 

111 

112 # Attributes that get auto-loaded 

113 self._load_attributes( 

114 attrs=attrs, 

115 meta=meta, 

116 resource_name=resource_name, 

117 resource_model=resource_model, 

118 service_context=service_context, 

119 ) 

120 

121 # Collections and their corresponding methods 

122 self._load_collections( 

123 attrs=attrs, 

124 resource_model=resource_model, 

125 service_context=service_context, 

126 ) 

127 

128 # References and Subresources 

129 self._load_has_relations( 

130 attrs=attrs, 

131 resource_name=resource_name, 

132 resource_model=resource_model, 

133 service_context=service_context, 

134 ) 

135 

136 # Waiter resource actions 

137 self._load_waiters( 

138 attrs=attrs, 

139 resource_name=resource_name, 

140 resource_model=resource_model, 

141 service_context=service_context, 

142 ) 

143 

144 # Create the name based on the requested service and resource 

145 cls_name = resource_name 

146 if service_context.service_name == resource_name: 

147 cls_name = 'ServiceResource' 

148 cls_name = service_context.service_name + '.' + cls_name 

149 

150 base_classes = [ServiceResource] 

151 if self._emitter is not None: 

152 self._emitter.emit( 

153 f'creating-resource-class.{cls_name}', 

154 class_attributes=attrs, 

155 base_classes=base_classes, 

156 service_context=service_context, 

157 ) 

158 return type(str(cls_name), tuple(base_classes), attrs) 

159 

160 def _load_identifiers(self, attrs, meta, resource_model, resource_name): 

161 """ 

162 Populate required identifiers. These are arguments without which 

163 the resource cannot be used. Identifiers become arguments for 

164 operations on the resource. 

165 """ 

166 for identifier in resource_model.identifiers: 

167 meta.identifiers.append(identifier.name) 

168 attrs[identifier.name] = self._create_identifier( 

169 identifier, resource_name 

170 ) 

171 

172 def _load_actions( 

173 self, attrs, resource_name, resource_model, service_context 

174 ): 

175 """ 

176 Actions on the resource become methods, with the ``load`` method 

177 being a special case which sets internal data for attributes, and 

178 ``reload`` is an alias for ``load``. 

179 """ 

180 if resource_model.load: 

181 attrs['load'] = self._create_action( 

182 action_model=resource_model.load, 

183 resource_name=resource_name, 

184 service_context=service_context, 

185 is_load=True, 

186 ) 

187 attrs['reload'] = attrs['load'] 

188 

189 for action in resource_model.actions: 

190 attrs[action.name] = self._create_action( 

191 action_model=action, 

192 resource_name=resource_name, 

193 service_context=service_context, 

194 ) 

195 

196 def _load_attributes( 

197 self, attrs, meta, resource_name, resource_model, service_context 

198 ): 

199 """ 

200 Load resource attributes based on the resource shape. The shape 

201 name is referenced in the resource JSON, but the shape itself 

202 is defined in the Botocore service JSON, hence the need for 

203 access to the ``service_model``. 

204 """ 

205 if not resource_model.shape: 

206 return 

207 

208 shape = service_context.service_model.shape_for(resource_model.shape) 

209 

210 identifiers = { 

211 i.member_name: i 

212 for i in resource_model.identifiers 

213 if i.member_name 

214 } 

215 attributes = resource_model.get_attributes(shape) 

216 for name, (orig_name, member) in attributes.items(): 

217 if name in identifiers: 

218 prop = self._create_identifier_alias( 

219 resource_name=resource_name, 

220 identifier=identifiers[name], 

221 member_model=member, 

222 service_context=service_context, 

223 ) 

224 else: 

225 prop = self._create_autoload_property( 

226 resource_name=resource_name, 

227 name=orig_name, 

228 snake_cased=name, 

229 member_model=member, 

230 service_context=service_context, 

231 ) 

232 attrs[name] = prop 

233 

234 def _load_collections(self, attrs, resource_model, service_context): 

235 """ 

236 Load resource collections from the model. Each collection becomes 

237 a :py:class:`~boto3.resources.collection.CollectionManager` instance 

238 on the resource instance, which allows you to iterate and filter 

239 through the collection's items. 

240 """ 

241 for collection_model in resource_model.collections: 

242 attrs[collection_model.name] = self._create_collection( 

243 resource_name=resource_model.name, 

244 collection_model=collection_model, 

245 service_context=service_context, 

246 ) 

247 

248 def _load_has_relations( 

249 self, attrs, resource_name, resource_model, service_context 

250 ): 

251 """ 

252 Load related resources, which are defined via a ``has`` 

253 relationship but conceptually come in two forms: 

254 

255 1. A reference, which is a related resource instance and can be 

256 ``None``, such as an EC2 instance's ``vpc``. 

257 2. A subresource, which is a resource constructor that will always 

258 return a resource instance which shares identifiers/data with 

259 this resource, such as ``s3.Bucket('name').Object('key')``. 

260 """ 

261 for reference in resource_model.references: 

262 # This is a dangling reference, i.e. we have all 

263 # the data we need to create the resource, so 

264 # this instance becomes an attribute on the class. 

265 attrs[reference.name] = self._create_reference( 

266 reference_model=reference, 

267 resource_name=resource_name, 

268 service_context=service_context, 

269 ) 

270 

271 for subresource in resource_model.subresources: 

272 # This is a sub-resource class you can create 

273 # by passing in an identifier, e.g. s3.Bucket(name). 

274 attrs[subresource.name] = self._create_class_partial( 

275 subresource_model=subresource, 

276 resource_name=resource_name, 

277 service_context=service_context, 

278 ) 

279 

280 self._create_available_subresources_command( 

281 attrs, resource_model.subresources 

282 ) 

283 

284 def _create_available_subresources_command(self, attrs, subresources): 

285 _subresources = [subresource.name for subresource in subresources] 

286 _subresources = sorted(_subresources) 

287 

288 def get_available_subresources(factory_self): 

289 """ 

290 Returns a list of all the available sub-resources for this 

291 Resource. 

292 

293 :returns: A list containing the name of each sub-resource for this 

294 resource 

295 :rtype: list of str 

296 """ 

297 return _subresources 

298 

299 attrs['get_available_subresources'] = get_available_subresources 

300 

301 def _load_waiters( 

302 self, attrs, resource_name, resource_model, service_context 

303 ): 

304 """ 

305 Load resource waiters from the model. Each waiter allows you to 

306 wait until a resource reaches a specific state by polling the state 

307 of the resource. 

308 """ 

309 for waiter in resource_model.waiters: 

310 attrs[waiter.name] = self._create_waiter( 

311 resource_waiter_model=waiter, 

312 resource_name=resource_name, 

313 service_context=service_context, 

314 ) 

315 

316 def _create_identifier(factory_self, identifier, resource_name): 

317 """ 

318 Creates a read-only property for identifier attributes. 

319 """ 

320 

321 def get_identifier(self): 

322 # The default value is set to ``None`` instead of 

323 # raising an AttributeError because when resources are 

324 # instantiated a check is made such that none of the 

325 # identifiers have a value ``None``. If any are ``None``, 

326 # a more informative user error than a generic AttributeError 

327 # is raised. 

328 return getattr(self, '_' + identifier.name, None) 

329 

330 get_identifier.__name__ = str(identifier.name) 

331 get_identifier.__doc__ = docstring.IdentifierDocstring( 

332 resource_name=resource_name, 

333 identifier_model=identifier, 

334 include_signature=False, 

335 ) 

336 

337 return property(get_identifier) 

338 

339 def _create_identifier_alias( 

340 factory_self, resource_name, identifier, member_model, service_context 

341 ): 

342 """ 

343 Creates a read-only property that aliases an identifier. 

344 """ 

345 

346 def get_identifier(self): 

347 return getattr(self, '_' + identifier.name, None) 

348 

349 get_identifier.__name__ = str(identifier.member_name) 

350 get_identifier.__doc__ = docstring.AttributeDocstring( 

351 service_name=service_context.service_name, 

352 resource_name=resource_name, 

353 attr_name=identifier.member_name, 

354 event_emitter=factory_self._emitter, 

355 attr_model=member_model, 

356 include_signature=False, 

357 ) 

358 

359 return property(get_identifier) 

360 

361 def _create_autoload_property( 

362 factory_self, 

363 resource_name, 

364 name, 

365 snake_cased, 

366 member_model, 

367 service_context, 

368 ): 

369 """ 

370 Creates a new property on the resource to lazy-load its value 

371 via the resource's ``load`` method (if it exists). 

372 """ 

373 

374 # The property loader will check to see if this resource has already 

375 # been loaded and return the cached value if possible. If not, then 

376 # it first checks to see if it CAN be loaded (raise if not), then 

377 # calls the load before returning the value. 

378 def property_loader(self): 

379 if self.meta.data is None: 

380 if hasattr(self, 'load'): 

381 self.load() 

382 else: 

383 raise ResourceLoadException( 

384 f'{self.__class__.__name__} has no load method' 

385 ) 

386 

387 return self.meta.data.get(name) 

388 

389 property_loader.__name__ = str(snake_cased) 

390 property_loader.__doc__ = docstring.AttributeDocstring( 

391 service_name=service_context.service_name, 

392 resource_name=resource_name, 

393 attr_name=snake_cased, 

394 event_emitter=factory_self._emitter, 

395 attr_model=member_model, 

396 include_signature=False, 

397 ) 

398 

399 return property(property_loader) 

400 

401 def _create_waiter( 

402 factory_self, resource_waiter_model, resource_name, service_context 

403 ): 

404 """ 

405 Creates a new wait method for each resource where both a waiter and 

406 resource model is defined. 

407 """ 

408 waiter = WaiterAction( 

409 resource_waiter_model, 

410 waiter_resource_name=resource_waiter_model.name, 

411 ) 

412 

413 def do_waiter(self, *args, **kwargs): 

414 waiter(self, *args, **kwargs) 

415 

416 do_waiter.__name__ = str(resource_waiter_model.name) 

417 do_waiter.__doc__ = docstring.ResourceWaiterDocstring( 

418 resource_name=resource_name, 

419 event_emitter=factory_self._emitter, 

420 service_model=service_context.service_model, 

421 resource_waiter_model=resource_waiter_model, 

422 service_waiter_model=service_context.service_waiter_model, 

423 include_signature=False, 

424 ) 

425 return do_waiter 

426 

427 def _create_collection( 

428 factory_self, resource_name, collection_model, service_context 

429 ): 

430 """ 

431 Creates a new property on the resource to lazy-load a collection. 

432 """ 

433 cls = factory_self._collection_factory.load_from_definition( 

434 resource_name=resource_name, 

435 collection_model=collection_model, 

436 service_context=service_context, 

437 event_emitter=factory_self._emitter, 

438 ) 

439 

440 def get_collection(self): 

441 return cls( 

442 collection_model=collection_model, 

443 parent=self, 

444 factory=factory_self, 

445 service_context=service_context, 

446 ) 

447 

448 get_collection.__name__ = str(collection_model.name) 

449 get_collection.__doc__ = docstring.CollectionDocstring( 

450 collection_model=collection_model, include_signature=False 

451 ) 

452 return property(get_collection) 

453 

454 def _create_reference( 

455 factory_self, reference_model, resource_name, service_context 

456 ): 

457 """ 

458 Creates a new property on the resource to lazy-load a reference. 

459 """ 

460 # References are essentially an action with no request 

461 # or response, so we can re-use the response handlers to 

462 # build up resources from identifiers and data members. 

463 handler = ResourceHandler( 

464 search_path=reference_model.resource.path, 

465 factory=factory_self, 

466 resource_model=reference_model.resource, 

467 service_context=service_context, 

468 ) 

469 

470 # Are there any identifiers that need access to data members? 

471 # This is important when building the resource below since 

472 # it requires the data to be loaded. 

473 needs_data = any( 

474 i.source == 'data' for i in reference_model.resource.identifiers 

475 ) 

476 

477 def get_reference(self): 

478 # We need to lazy-evaluate the reference to handle circular 

479 # references between resources. We do this by loading the class 

480 # when first accessed. 

481 # This is using a *response handler* so we need to make sure 

482 # our data is loaded (if possible) and pass that data into 

483 # the handler as if it were a response. This allows references 

484 # to have their data loaded properly. 

485 if needs_data and self.meta.data is None and hasattr(self, 'load'): 

486 self.load() 

487 return handler(self, {}, self.meta.data) 

488 

489 get_reference.__name__ = str(reference_model.name) 

490 get_reference.__doc__ = docstring.ReferenceDocstring( 

491 reference_model=reference_model, include_signature=False 

492 ) 

493 return property(get_reference) 

494 

495 def _create_class_partial( 

496 factory_self, subresource_model, resource_name, service_context 

497 ): 

498 """ 

499 Creates a new method which acts as a functools.partial, passing 

500 along the instance's low-level `client` to the new resource 

501 class' constructor. 

502 """ 

503 name = subresource_model.resource.type 

504 

505 def create_resource(self, *args, **kwargs): 

506 # We need a new method here because we want access to the 

507 # instance's client. 

508 positional_args = [] 

509 

510 # We lazy-load the class to handle circular references. 

511 json_def = service_context.resource_json_definitions.get(name, {}) 

512 resource_cls = factory_self.load_from_definition( 

513 resource_name=name, 

514 single_resource_json_definition=json_def, 

515 service_context=service_context, 

516 ) 

517 

518 # Assumes that identifiers are in order, which lets you do 

519 # e.g. ``sqs.Queue('foo').Message('bar')`` to create a new message 

520 # linked with the ``foo`` queue and which has a ``bar`` receipt 

521 # handle. If we did kwargs here then future positional arguments 

522 # would lead to failure. 

523 identifiers = subresource_model.resource.identifiers 

524 if identifiers is not None: 

525 for identifier, value in build_identifiers(identifiers, self): 

526 positional_args.append(value) 

527 

528 return partial( 

529 resource_cls, *positional_args, client=self.meta.client 

530 )(*args, **kwargs) 

531 

532 create_resource.__name__ = str(name) 

533 create_resource.__doc__ = docstring.SubResourceDocstring( 

534 resource_name=resource_name, 

535 sub_resource_model=subresource_model, 

536 service_model=service_context.service_model, 

537 include_signature=False, 

538 ) 

539 return create_resource 

540 

541 def _create_action( 

542 factory_self, 

543 action_model, 

544 resource_name, 

545 service_context, 

546 is_load=False, 

547 ): 

548 """ 

549 Creates a new method which makes a request to the underlying 

550 AWS service. 

551 """ 

552 # Create the action in in this closure but before the ``do_action`` 

553 # method below is invoked, which allows instances of the resource 

554 # to share the ServiceAction instance. 

555 action = ServiceAction( 

556 action_model, factory=factory_self, service_context=service_context 

557 ) 

558 

559 # A resource's ``load`` method is special because it sets 

560 # values on the resource instead of returning the response. 

561 if is_load: 

562 # We need a new method here because we want access to the 

563 # instance via ``self``. 

564 def do_action(self, *args, **kwargs): 

565 response = action(self, *args, **kwargs) 

566 self.meta.data = response 

567 

568 # Create the docstring for the load/reload methods. 

569 lazy_docstring = docstring.LoadReloadDocstring( 

570 action_name=action_model.name, 

571 resource_name=resource_name, 

572 event_emitter=factory_self._emitter, 

573 load_model=action_model, 

574 service_model=service_context.service_model, 

575 include_signature=False, 

576 ) 

577 else: 

578 # We need a new method here because we want access to the 

579 # instance via ``self``. 

580 def do_action(self, *args, **kwargs): 

581 response = action(self, *args, **kwargs) 

582 

583 if hasattr(self, 'load'): 

584 # Clear cached data. It will be reloaded the next 

585 # time that an attribute is accessed. 

586 # TODO: Make this configurable in the future? 

587 self.meta.data = None 

588 

589 return response 

590 

591 lazy_docstring = docstring.ActionDocstring( 

592 resource_name=resource_name, 

593 event_emitter=factory_self._emitter, 

594 action_model=action_model, 

595 service_model=service_context.service_model, 

596 include_signature=False, 

597 ) 

598 

599 do_action.__name__ = str(action_model.name) 

600 do_action.__doc__ = lazy_docstring 

601 return do_action