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

133 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 copy 

15import logging 

16 

17from botocore import xform_name 

18from botocore.utils import merge_dicts 

19 

20from ..docs import docstring 

21from .action import BatchAction 

22from .params import create_request_parameters 

23from .response import ResourceHandler 

24 

25logger = logging.getLogger(__name__) 

26 

27 

28class ResourceCollection: 

29 """ 

30 Represents a collection of resources, which can be iterated through, 

31 optionally with filtering. Collections automatically handle pagination 

32 for you. 

33 

34 See :ref:`guide_collections` for a high-level overview of collections, 

35 including when remote service requests are performed. 

36 

37 :type model: :py:class:`~boto3.resources.model.Collection` 

38 :param model: Collection model 

39 :type parent: :py:class:`~boto3.resources.base.ServiceResource` 

40 :param parent: The collection's parent resource 

41 :type handler: :py:class:`~boto3.resources.response.ResourceHandler` 

42 :param handler: The resource response handler used to create resource 

43 instances 

44 """ 

45 

46 def __init__(self, model, parent, handler, **kwargs): 

47 self._model = model 

48 self._parent = parent 

49 self._py_operation_name = xform_name(model.request.operation) 

50 self._handler = handler 

51 self._params = copy.deepcopy(kwargs) 

52 

53 def __repr__(self): 

54 return '{}({}, {})'.format( 

55 self.__class__.__name__, 

56 self._parent, 

57 '{}.{}'.format( 

58 self._parent.meta.service_name, self._model.resource.type 

59 ), 

60 ) 

61 

62 def __iter__(self): 

63 """ 

64 A generator which yields resource instances after doing the 

65 appropriate service operation calls and handling any pagination 

66 on your behalf. 

67 

68 Page size, item limit, and filter parameters are applied 

69 if they have previously been set. 

70 

71 >>> bucket = s3.Bucket('boto3') 

72 >>> for obj in bucket.objects.all(): 

73 ... print(obj.key) 

74 'key1' 

75 'key2' 

76 

77 """ 

78 limit = self._params.get('limit', None) 

79 

80 count = 0 

81 for page in self.pages(): 

82 for item in page: 

83 yield item 

84 

85 # If the limit is set and has been reached, then 

86 # we stop processing items here. 

87 count += 1 

88 if limit is not None and count >= limit: 

89 return 

90 

91 def _clone(self, **kwargs): 

92 """ 

93 Create a clone of this collection. This is used by the methods 

94 below to provide a chainable interface that returns copies 

95 rather than the original. This allows things like: 

96 

97 >>> base = collection.filter(Param1=1) 

98 >>> query1 = base.filter(Param2=2) 

99 >>> query2 = base.filter(Param3=3) 

100 >>> query1.params 

101 {'Param1': 1, 'Param2': 2} 

102 >>> query2.params 

103 {'Param1': 1, 'Param3': 3} 

104 

105 :rtype: :py:class:`ResourceCollection` 

106 :return: A clone of this resource collection 

107 """ 

108 params = copy.deepcopy(self._params) 

109 merge_dicts(params, kwargs, append_lists=True) 

110 clone = self.__class__( 

111 self._model, self._parent, self._handler, **params 

112 ) 

113 return clone 

114 

115 def pages(self): 

116 """ 

117 A generator which yields pages of resource instances after 

118 doing the appropriate service operation calls and handling 

119 any pagination on your behalf. Non-paginated calls will 

120 return a single page of items. 

121 

122 Page size, item limit, and filter parameters are applied 

123 if they have previously been set. 

124 

125 >>> bucket = s3.Bucket('boto3') 

126 >>> for page in bucket.objects.pages(): 

127 ... for obj in page: 

128 ... print(obj.key) 

129 'key1' 

130 'key2' 

131 

132 :rtype: list(:py:class:`~boto3.resources.base.ServiceResource`) 

133 :return: List of resource instances 

134 """ 

135 client = self._parent.meta.client 

136 cleaned_params = self._params.copy() 

137 limit = cleaned_params.pop('limit', None) 

138 page_size = cleaned_params.pop('page_size', None) 

139 params = create_request_parameters(self._parent, self._model.request) 

140 merge_dicts(params, cleaned_params, append_lists=True) 

141 

142 # Is this a paginated operation? If so, we need to get an 

143 # iterator for the various pages. If not, then we simply 

144 # call the operation and return the result as a single 

145 # page in a list. For non-paginated results, we just ignore 

146 # the page size parameter. 

147 if client.can_paginate(self._py_operation_name): 

148 logger.debug( 

149 'Calling paginated %s:%s with %r', 

150 self._parent.meta.service_name, 

151 self._py_operation_name, 

152 params, 

153 ) 

154 paginator = client.get_paginator(self._py_operation_name) 

155 pages = paginator.paginate( 

156 PaginationConfig={'MaxItems': limit, 'PageSize': page_size}, 

157 **params 

158 ) 

159 else: 

160 logger.debug( 

161 'Calling %s:%s with %r', 

162 self._parent.meta.service_name, 

163 self._py_operation_name, 

164 params, 

165 ) 

166 pages = [getattr(client, self._py_operation_name)(**params)] 

167 

168 # Now that we have a page iterator or single page of results 

169 # we start processing and yielding individual items. 

170 count = 0 

171 for page in pages: 

172 page_items = [] 

173 for item in self._handler(self._parent, params, page): 

174 page_items.append(item) 

175 

176 # If the limit is set and has been reached, then 

177 # we stop processing items here. 

178 count += 1 

179 if limit is not None and count >= limit: 

180 break 

181 

182 yield page_items 

183 

184 # Stop reading pages if we've reached out limit 

185 if limit is not None and count >= limit: 

186 break 

187 

188 def all(self): 

189 """ 

190 Get all items from the collection, optionally with a custom 

191 page size and item count limit. 

192 

193 This method returns an iterable generator which yields 

194 individual resource instances. Example use:: 

195 

196 # Iterate through items 

197 >>> for queue in sqs.queues.all(): 

198 ... print(queue.url) 

199 'https://url1' 

200 'https://url2' 

201 

202 # Convert to list 

203 >>> queues = list(sqs.queues.all()) 

204 >>> len(queues) 

205 2 

206 """ 

207 return self._clone() 

208 

209 def filter(self, **kwargs): 

210 """ 

211 Get items from the collection, passing keyword arguments along 

212 as parameters to the underlying service operation, which are 

213 typically used to filter the results. 

214 

215 This method returns an iterable generator which yields 

216 individual resource instances. Example use:: 

217 

218 # Iterate through items 

219 >>> for queue in sqs.queues.filter(Param='foo'): 

220 ... print(queue.url) 

221 'https://url1' 

222 'https://url2' 

223 

224 # Convert to list 

225 >>> queues = list(sqs.queues.filter(Param='foo')) 

226 >>> len(queues) 

227 2 

228 

229 :rtype: :py:class:`ResourceCollection` 

230 """ 

231 return self._clone(**kwargs) 

232 

233 def limit(self, count): 

234 """ 

235 Return at most this many resources. 

236 

237 >>> for bucket in s3.buckets.limit(5): 

238 ... print(bucket.name) 

239 'bucket1' 

240 'bucket2' 

241 'bucket3' 

242 'bucket4' 

243 'bucket5' 

244 

245 :type count: int 

246 :param count: Return no more than this many items 

247 :rtype: :py:class:`ResourceCollection` 

248 """ 

249 return self._clone(limit=count) 

250 

251 def page_size(self, count): 

252 """ 

253 Fetch at most this many resources per service request. 

254 

255 >>> for obj in s3.Bucket('boto3').objects.page_size(100): 

256 ... print(obj.key) 

257 

258 :type count: int 

259 :param count: Fetch this many items per request 

260 :rtype: :py:class:`ResourceCollection` 

261 """ 

262 return self._clone(page_size=count) 

263 

264 

265class CollectionManager: 

266 """ 

267 A collection manager provides access to resource collection instances, 

268 which can be iterated and filtered. The manager exposes some 

269 convenience functions that are also found on resource collections, 

270 such as :py:meth:`~ResourceCollection.all` and 

271 :py:meth:`~ResourceCollection.filter`. 

272 

273 Get all items:: 

274 

275 >>> for bucket in s3.buckets.all(): 

276 ... print(bucket.name) 

277 

278 Get only some items via filtering:: 

279 

280 >>> for queue in sqs.queues.filter(QueueNamePrefix='AWS'): 

281 ... print(queue.url) 

282 

283 Get whole pages of items: 

284 

285 >>> for page in s3.Bucket('boto3').objects.pages(): 

286 ... for obj in page: 

287 ... print(obj.key) 

288 

289 A collection manager is not iterable. You **must** call one of the 

290 methods that return a :py:class:`ResourceCollection` before trying 

291 to iterate, slice, or convert to a list. 

292 

293 See the :ref:`guide_collections` guide for a high-level overview 

294 of collections, including when remote service requests are performed. 

295 

296 :type collection_model: :py:class:`~boto3.resources.model.Collection` 

297 :param model: Collection model 

298 

299 :type parent: :py:class:`~boto3.resources.base.ServiceResource` 

300 :param parent: The collection's parent resource 

301 

302 :type factory: :py:class:`~boto3.resources.factory.ResourceFactory` 

303 :param factory: The resource factory to create new resources 

304 

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

306 :param service_context: Context about the AWS service 

307 """ 

308 

309 # The class to use when creating an iterator 

310 _collection_cls = ResourceCollection 

311 

312 def __init__(self, collection_model, parent, factory, service_context): 

313 self._model = collection_model 

314 operation_name = self._model.request.operation 

315 self._parent = parent 

316 

317 search_path = collection_model.resource.path 

318 self._handler = ResourceHandler( 

319 search_path=search_path, 

320 factory=factory, 

321 resource_model=collection_model.resource, 

322 service_context=service_context, 

323 operation_name=operation_name, 

324 ) 

325 

326 def __repr__(self): 

327 return '{}({}, {})'.format( 

328 self.__class__.__name__, 

329 self._parent, 

330 '{}.{}'.format( 

331 self._parent.meta.service_name, self._model.resource.type 

332 ), 

333 ) 

334 

335 def iterator(self, **kwargs): 

336 """ 

337 Get a resource collection iterator from this manager. 

338 

339 :rtype: :py:class:`ResourceCollection` 

340 :return: An iterable representing the collection of resources 

341 """ 

342 return self._collection_cls( 

343 self._model, self._parent, self._handler, **kwargs 

344 ) 

345 

346 # Set up some methods to proxy ResourceCollection methods 

347 def all(self): 

348 return self.iterator() 

349 

350 all.__doc__ = ResourceCollection.all.__doc__ 

351 

352 def filter(self, **kwargs): 

353 return self.iterator(**kwargs) 

354 

355 filter.__doc__ = ResourceCollection.filter.__doc__ 

356 

357 def limit(self, count): 

358 return self.iterator(limit=count) 

359 

360 limit.__doc__ = ResourceCollection.limit.__doc__ 

361 

362 def page_size(self, count): 

363 return self.iterator(page_size=count) 

364 

365 page_size.__doc__ = ResourceCollection.page_size.__doc__ 

366 

367 def pages(self): 

368 return self.iterator().pages() 

369 

370 pages.__doc__ = ResourceCollection.pages.__doc__ 

371 

372 

373class CollectionFactory: 

374 """ 

375 A factory to create new 

376 :py:class:`CollectionManager` and :py:class:`ResourceCollection` 

377 subclasses from a :py:class:`~boto3.resources.model.Collection` 

378 model. These subclasses include methods to perform batch operations. 

379 """ 

380 

381 def load_from_definition( 

382 self, resource_name, collection_model, service_context, event_emitter 

383 ): 

384 """ 

385 Loads a collection from a model, creating a new 

386 :py:class:`CollectionManager` subclass 

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

388 and resource name, e.g. ec2.InstanceCollectionManager. It also 

389 creates a new :py:class:`ResourceCollection` subclass which is used 

390 by the new manager class. 

391 

392 :type resource_name: string 

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

394 this should match the ``service_name``. 

395 

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

397 :param service_context: Context about the AWS service 

398 

399 :type event_emitter: :py:class:`~botocore.hooks.HierarchialEmitter` 

400 :param event_emitter: An event emitter 

401 

402 :rtype: Subclass of :py:class:`CollectionManager` 

403 :return: The collection class. 

404 """ 

405 attrs = {} 

406 collection_name = collection_model.name 

407 

408 # Create the batch actions for a collection 

409 self._load_batch_actions( 

410 attrs, 

411 resource_name, 

412 collection_model, 

413 service_context.service_model, 

414 event_emitter, 

415 ) 

416 # Add the documentation to the collection class's methods 

417 self._load_documented_collection_methods( 

418 attrs=attrs, 

419 resource_name=resource_name, 

420 collection_model=collection_model, 

421 service_model=service_context.service_model, 

422 event_emitter=event_emitter, 

423 base_class=ResourceCollection, 

424 ) 

425 

426 if service_context.service_name == resource_name: 

427 cls_name = '{}.{}Collection'.format( 

428 service_context.service_name, collection_name 

429 ) 

430 else: 

431 cls_name = '{}.{}.{}Collection'.format( 

432 service_context.service_name, resource_name, collection_name 

433 ) 

434 

435 collection_cls = type(str(cls_name), (ResourceCollection,), attrs) 

436 

437 # Add the documentation to the collection manager's methods 

438 self._load_documented_collection_methods( 

439 attrs=attrs, 

440 resource_name=resource_name, 

441 collection_model=collection_model, 

442 service_model=service_context.service_model, 

443 event_emitter=event_emitter, 

444 base_class=CollectionManager, 

445 ) 

446 attrs['_collection_cls'] = collection_cls 

447 cls_name += 'Manager' 

448 

449 return type(str(cls_name), (CollectionManager,), attrs) 

450 

451 def _load_batch_actions( 

452 self, 

453 attrs, 

454 resource_name, 

455 collection_model, 

456 service_model, 

457 event_emitter, 

458 ): 

459 """ 

460 Batch actions on the collection become methods on both 

461 the collection manager and iterators. 

462 """ 

463 for action_model in collection_model.batch_actions: 

464 snake_cased = xform_name(action_model.name) 

465 attrs[snake_cased] = self._create_batch_action( 

466 resource_name, 

467 snake_cased, 

468 action_model, 

469 collection_model, 

470 service_model, 

471 event_emitter, 

472 ) 

473 

474 def _load_documented_collection_methods( 

475 factory_self, 

476 attrs, 

477 resource_name, 

478 collection_model, 

479 service_model, 

480 event_emitter, 

481 base_class, 

482 ): 

483 # The base class already has these methods defined. However 

484 # the docstrings are generic and not based for a particular service 

485 # or resource. So we override these methods by proxying to the 

486 # base class's builtin method and adding a docstring 

487 # that pertains to the resource. 

488 

489 # A collection's all() method. 

490 def all(self): 

491 return base_class.all(self) 

492 

493 all.__doc__ = docstring.CollectionMethodDocstring( 

494 resource_name=resource_name, 

495 action_name='all', 

496 event_emitter=event_emitter, 

497 collection_model=collection_model, 

498 service_model=service_model, 

499 include_signature=False, 

500 ) 

501 attrs['all'] = all 

502 

503 # The collection's filter() method. 

504 def filter(self, **kwargs): 

505 return base_class.filter(self, **kwargs) 

506 

507 filter.__doc__ = docstring.CollectionMethodDocstring( 

508 resource_name=resource_name, 

509 action_name='filter', 

510 event_emitter=event_emitter, 

511 collection_model=collection_model, 

512 service_model=service_model, 

513 include_signature=False, 

514 ) 

515 attrs['filter'] = filter 

516 

517 # The collection's limit method. 

518 def limit(self, count): 

519 return base_class.limit(self, count) 

520 

521 limit.__doc__ = docstring.CollectionMethodDocstring( 

522 resource_name=resource_name, 

523 action_name='limit', 

524 event_emitter=event_emitter, 

525 collection_model=collection_model, 

526 service_model=service_model, 

527 include_signature=False, 

528 ) 

529 attrs['limit'] = limit 

530 

531 # The collection's page_size method. 

532 def page_size(self, count): 

533 return base_class.page_size(self, count) 

534 

535 page_size.__doc__ = docstring.CollectionMethodDocstring( 

536 resource_name=resource_name, 

537 action_name='page_size', 

538 event_emitter=event_emitter, 

539 collection_model=collection_model, 

540 service_model=service_model, 

541 include_signature=False, 

542 ) 

543 attrs['page_size'] = page_size 

544 

545 def _create_batch_action( 

546 factory_self, 

547 resource_name, 

548 snake_cased, 

549 action_model, 

550 collection_model, 

551 service_model, 

552 event_emitter, 

553 ): 

554 """ 

555 Creates a new method which makes a batch operation request 

556 to the underlying service API. 

557 """ 

558 action = BatchAction(action_model) 

559 

560 def batch_action(self, *args, **kwargs): 

561 return action(self, *args, **kwargs) 

562 

563 batch_action.__name__ = str(snake_cased) 

564 batch_action.__doc__ = docstring.BatchActionDocstring( 

565 resource_name=resource_name, 

566 event_emitter=event_emitter, 

567 batch_action_model=action_model, 

568 service_model=service_model, 

569 collection_model=collection_model, 

570 include_signature=False, 

571 ) 

572 return batch_action