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

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

133 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 

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 f'{self._parent.meta.service_name}.{self._model.resource.type}', 

58 ) 

59 

60 def __iter__(self): 

61 """ 

62 A generator which yields resource instances after doing the 

63 appropriate service operation calls and handling any pagination 

64 on your behalf. 

65 

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

67 if they have previously been set. 

68 

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

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

71 ... print(obj.key) 

72 'key1' 

73 'key2' 

74 

75 """ 

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

77 

78 count = 0 

79 for page in self.pages(): 

80 for item in page: 

81 yield item 

82 

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

84 # we stop processing items here. 

85 count += 1 

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

87 return 

88 

89 def _clone(self, **kwargs): 

90 """ 

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

92 below to provide a chainable interface that returns copies 

93 rather than the original. This allows things like: 

94 

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

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

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

98 >>> query1.params 

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

100 >>> query2.params 

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

102 

103 :rtype: :py:class:`ResourceCollection` 

104 :return: A clone of this resource collection 

105 """ 

106 params = copy.deepcopy(self._params) 

107 merge_dicts(params, kwargs, append_lists=True) 

108 clone = self.__class__( 

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

110 ) 

111 return clone 

112 

113 def pages(self): 

114 """ 

115 A generator which yields pages of resource instances after 

116 doing the appropriate service operation calls and handling 

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

118 return a single page of items. 

119 

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

121 if they have previously been set. 

122 

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

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

125 ... for obj in page: 

126 ... print(obj.key) 

127 'key1' 

128 'key2' 

129 

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

131 :return: List of resource instances 

132 """ 

133 client = self._parent.meta.client 

134 cleaned_params = self._params.copy() 

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

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

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

138 merge_dicts(params, cleaned_params, append_lists=True) 

139 

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

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

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

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

144 # the page size parameter. 

145 if client.can_paginate(self._py_operation_name): 

146 logger.debug( 

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

148 self._parent.meta.service_name, 

149 self._py_operation_name, 

150 params, 

151 ) 

152 paginator = client.get_paginator(self._py_operation_name) 

153 pages = paginator.paginate( 

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

155 **params, 

156 ) 

157 else: 

158 logger.debug( 

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

160 self._parent.meta.service_name, 

161 self._py_operation_name, 

162 params, 

163 ) 

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

165 

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

167 # we start processing and yielding individual items. 

168 count = 0 

169 for page in pages: 

170 page_items = [] 

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

172 page_items.append(item) 

173 

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

175 # we stop processing items here. 

176 count += 1 

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

178 break 

179 

180 yield page_items 

181 

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

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

184 break 

185 

186 def all(self): 

187 """ 

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

189 page size and item count limit. 

190 

191 This method returns an iterable generator which yields 

192 individual resource instances. Example use:: 

193 

194 # Iterate through items 

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

196 ... print(queue.url) 

197 'https://url1' 

198 'https://url2' 

199 

200 # Convert to list 

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

202 >>> len(queues) 

203 2 

204 """ 

205 return self._clone() 

206 

207 def filter(self, **kwargs): 

208 """ 

209 Get items from the collection, passing keyword arguments along 

210 as parameters to the underlying service operation, which are 

211 typically used to filter the results. 

212 

213 This method returns an iterable generator which yields 

214 individual resource instances. Example use:: 

215 

216 # Iterate through items 

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

218 ... print(queue.url) 

219 'https://url1' 

220 'https://url2' 

221 

222 # Convert to list 

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

224 >>> len(queues) 

225 2 

226 

227 :rtype: :py:class:`ResourceCollection` 

228 """ 

229 return self._clone(**kwargs) 

230 

231 def limit(self, count): 

232 """ 

233 Return at most this many resources. 

234 

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

236 ... print(bucket.name) 

237 'bucket1' 

238 'bucket2' 

239 'bucket3' 

240 'bucket4' 

241 'bucket5' 

242 

243 :type count: int 

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

245 :rtype: :py:class:`ResourceCollection` 

246 """ 

247 return self._clone(limit=count) 

248 

249 def page_size(self, count): 

250 """ 

251 Fetch at most this many resources per service request. 

252 

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

254 ... print(obj.key) 

255 

256 :type count: int 

257 :param count: Fetch this many items per request 

258 :rtype: :py:class:`ResourceCollection` 

259 """ 

260 return self._clone(page_size=count) 

261 

262 

263class CollectionManager: 

264 """ 

265 A collection manager provides access to resource collection instances, 

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

267 convenience functions that are also found on resource collections, 

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

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

270 

271 Get all items:: 

272 

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

274 ... print(bucket.name) 

275 

276 Get only some items via filtering:: 

277 

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

279 ... print(queue.url) 

280 

281 Get whole pages of items: 

282 

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

284 ... for obj in page: 

285 ... print(obj.key) 

286 

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

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

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

290 

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

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

293 

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

295 :param model: Collection model 

296 

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

298 :param parent: The collection's parent resource 

299 

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

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

302 

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

304 :param service_context: Context about the AWS service 

305 """ 

306 

307 # The class to use when creating an iterator 

308 _collection_cls = ResourceCollection 

309 

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

311 self._model = collection_model 

312 operation_name = self._model.request.operation 

313 self._parent = parent 

314 

315 search_path = collection_model.resource.path 

316 self._handler = ResourceHandler( 

317 search_path=search_path, 

318 factory=factory, 

319 resource_model=collection_model.resource, 

320 service_context=service_context, 

321 operation_name=operation_name, 

322 ) 

323 

324 def __repr__(self): 

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

326 self.__class__.__name__, 

327 self._parent, 

328 f'{self._parent.meta.service_name}.{self._model.resource.type}', 

329 ) 

330 

331 def iterator(self, **kwargs): 

332 """ 

333 Get a resource collection iterator from this manager. 

334 

335 :rtype: :py:class:`ResourceCollection` 

336 :return: An iterable representing the collection of resources 

337 """ 

338 return self._collection_cls( 

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

340 ) 

341 

342 # Set up some methods to proxy ResourceCollection methods 

343 def all(self): 

344 return self.iterator() 

345 

346 all.__doc__ = ResourceCollection.all.__doc__ 

347 

348 def filter(self, **kwargs): 

349 return self.iterator(**kwargs) 

350 

351 filter.__doc__ = ResourceCollection.filter.__doc__ 

352 

353 def limit(self, count): 

354 return self.iterator(limit=count) 

355 

356 limit.__doc__ = ResourceCollection.limit.__doc__ 

357 

358 def page_size(self, count): 

359 return self.iterator(page_size=count) 

360 

361 page_size.__doc__ = ResourceCollection.page_size.__doc__ 

362 

363 def pages(self): 

364 return self.iterator().pages() 

365 

366 pages.__doc__ = ResourceCollection.pages.__doc__ 

367 

368 

369class CollectionFactory: 

370 """ 

371 A factory to create new 

372 :py:class:`CollectionManager` and :py:class:`ResourceCollection` 

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

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

375 """ 

376 

377 def load_from_definition( 

378 self, resource_name, collection_model, service_context, event_emitter 

379 ): 

380 """ 

381 Loads a collection from a model, creating a new 

382 :py:class:`CollectionManager` subclass 

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

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

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

386 by the new manager class. 

387 

388 :type resource_name: string 

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

390 this should match the ``service_name``. 

391 

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

393 :param service_context: Context about the AWS service 

394 

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

396 :param event_emitter: An event emitter 

397 

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

399 :return: The collection class. 

400 """ 

401 attrs = {} 

402 collection_name = collection_model.name 

403 

404 # Create the batch actions for a collection 

405 self._load_batch_actions( 

406 attrs, 

407 resource_name, 

408 collection_model, 

409 service_context.service_model, 

410 event_emitter, 

411 ) 

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

413 self._load_documented_collection_methods( 

414 attrs=attrs, 

415 resource_name=resource_name, 

416 collection_model=collection_model, 

417 service_model=service_context.service_model, 

418 event_emitter=event_emitter, 

419 base_class=ResourceCollection, 

420 ) 

421 

422 if service_context.service_name == resource_name: 

423 cls_name = ( 

424 f'{service_context.service_name}.{collection_name}Collection' 

425 ) 

426 else: 

427 cls_name = f'{service_context.service_name}.{resource_name}.{collection_name}Collection' 

428 

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

430 

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

432 self._load_documented_collection_methods( 

433 attrs=attrs, 

434 resource_name=resource_name, 

435 collection_model=collection_model, 

436 service_model=service_context.service_model, 

437 event_emitter=event_emitter, 

438 base_class=CollectionManager, 

439 ) 

440 attrs['_collection_cls'] = collection_cls 

441 cls_name += 'Manager' 

442 

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

444 

445 def _load_batch_actions( 

446 self, 

447 attrs, 

448 resource_name, 

449 collection_model, 

450 service_model, 

451 event_emitter, 

452 ): 

453 """ 

454 Batch actions on the collection become methods on both 

455 the collection manager and iterators. 

456 """ 

457 for action_model in collection_model.batch_actions: 

458 snake_cased = xform_name(action_model.name) 

459 attrs[snake_cased] = self._create_batch_action( 

460 resource_name, 

461 snake_cased, 

462 action_model, 

463 collection_model, 

464 service_model, 

465 event_emitter, 

466 ) 

467 

468 def _load_documented_collection_methods( 

469 factory_self, 

470 attrs, 

471 resource_name, 

472 collection_model, 

473 service_model, 

474 event_emitter, 

475 base_class, 

476 ): 

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

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

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

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

481 # that pertains to the resource. 

482 

483 # A collection's all() method. 

484 def all(self): 

485 return base_class.all(self) 

486 

487 all.__doc__ = docstring.CollectionMethodDocstring( 

488 resource_name=resource_name, 

489 action_name='all', 

490 event_emitter=event_emitter, 

491 collection_model=collection_model, 

492 service_model=service_model, 

493 include_signature=False, 

494 ) 

495 attrs['all'] = all 

496 

497 # The collection's filter() method. 

498 def filter(self, **kwargs): 

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

500 

501 filter.__doc__ = docstring.CollectionMethodDocstring( 

502 resource_name=resource_name, 

503 action_name='filter', 

504 event_emitter=event_emitter, 

505 collection_model=collection_model, 

506 service_model=service_model, 

507 include_signature=False, 

508 ) 

509 attrs['filter'] = filter 

510 

511 # The collection's limit method. 

512 def limit(self, count): 

513 return base_class.limit(self, count) 

514 

515 limit.__doc__ = docstring.CollectionMethodDocstring( 

516 resource_name=resource_name, 

517 action_name='limit', 

518 event_emitter=event_emitter, 

519 collection_model=collection_model, 

520 service_model=service_model, 

521 include_signature=False, 

522 ) 

523 attrs['limit'] = limit 

524 

525 # The collection's page_size method. 

526 def page_size(self, count): 

527 return base_class.page_size(self, count) 

528 

529 page_size.__doc__ = docstring.CollectionMethodDocstring( 

530 resource_name=resource_name, 

531 action_name='page_size', 

532 event_emitter=event_emitter, 

533 collection_model=collection_model, 

534 service_model=service_model, 

535 include_signature=False, 

536 ) 

537 attrs['page_size'] = page_size 

538 

539 def _create_batch_action( 

540 factory_self, 

541 resource_name, 

542 snake_cased, 

543 action_model, 

544 collection_model, 

545 service_model, 

546 event_emitter, 

547 ): 

548 """ 

549 Creates a new method which makes a batch operation request 

550 to the underlying service API. 

551 """ 

552 action = BatchAction(action_model) 

553 

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

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

556 

557 batch_action.__name__ = str(snake_cased) 

558 batch_action.__doc__ = docstring.BatchActionDocstring( 

559 resource_name=resource_name, 

560 event_emitter=event_emitter, 

561 batch_action_model=action_model, 

562 service_model=service_model, 

563 collection_model=collection_model, 

564 include_signature=False, 

565 ) 

566 return batch_action