Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/botocore/loaders.py: 30%

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

156 statements  

1# Copyright 2012-2015 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# http://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"""Module for loading various model files. 

14 

15This module provides the classes that are used to load models used 

16by botocore. This can include: 

17 

18 * Service models (e.g. the model for EC2, S3, DynamoDB, etc.) 

19 * Service model extras which customize the service models 

20 * Other models associated with a service (pagination, waiters) 

21 * Non service-specific config (Endpoint data, retry config) 

22 

23Loading a module is broken down into several steps: 

24 

25 * Determining the path to load 

26 * Search the data_path for files to load 

27 * The mechanics of loading the file 

28 * Searching for extras and applying them to the loaded file 

29 

30The last item is used so that other faster loading mechanism 

31besides the default JSON loader can be used. 

32 

33The Search Path 

34=============== 

35 

36Similar to how the PATH environment variable is to finding executables 

37and the PYTHONPATH environment variable is to finding python modules 

38to import, the botocore loaders have the concept of a data path exposed 

39through AWS_DATA_PATH. 

40 

41This enables end users to provide additional search paths where we 

42will attempt to load models outside of the models we ship with 

43botocore. When you create a ``Loader``, there are two paths 

44automatically added to the model search path: 

45 

46 * <botocore root>/data/ 

47 * ~/.aws/models 

48 

49The first value is the path where all the model files shipped with 

50botocore are located. 

51 

52The second path is so that users can just drop new model files in 

53``~/.aws/models`` without having to mess around with the AWS_DATA_PATH. 

54 

55The AWS_DATA_PATH using the platform specific path separator to 

56separate entries (typically ``:`` on linux and ``;`` on windows). 

57 

58 

59Directory Layout 

60================ 

61 

62The Loader expects a particular directory layout. In order for any 

63directory specified in AWS_DATA_PATH to be considered, it must have 

64this structure for service models:: 

65 

66 <root> 

67 | 

68 |-- servicename1 

69 | |-- 2012-10-25 

70 | |-- service-2.json 

71 |-- ec2 

72 | |-- 2014-01-01 

73 | | |-- paginators-1.json 

74 | | |-- service-2.json 

75 | | |-- waiters-2.json 

76 | |-- 2015-03-01 

77 | |-- paginators-1.json 

78 | |-- service-2.json 

79 | |-- waiters-2.json 

80 | |-- service-2.sdk-extras.json 

81 

82 

83That is: 

84 

85 * The root directory contains sub directories that are the name 

86 of the services. 

87 * Within each service directory, there's a sub directory for each 

88 available API version. 

89 * Within each API version, there are model specific files, including 

90 (but not limited to): service-2.json, waiters-2.json, paginators-1.json 

91 

92The ``-1`` and ``-2`` suffix at the end of the model files denote which version 

93schema is used within the model. Even though this information is available in 

94the ``version`` key within the model, this version is also part of the filename 

95so that code does not need to load the JSON model in order to determine which 

96version to use. 

97 

98The ``sdk-extras`` and similar files represent extra data that needs to be 

99applied to the model after it is loaded. Data in these files might represent 

100information that doesn't quite fit in the original models, but is still needed 

101for the sdk. For instance, additional operation parameters might be added here 

102which don't represent the actual service api. 

103""" 

104 

105import logging 

106import os 

107 

108from botocore import BOTOCORE_ROOT 

109from botocore.compat import HAS_GZIP, OrderedDict, json 

110from botocore.exceptions import DataNotFoundError, UnknownServiceError 

111from botocore.utils import deep_merge 

112 

113_JSON_OPEN_METHODS = { 

114 '.json': open, 

115} 

116 

117 

118if HAS_GZIP: 

119 from gzip import open as gzip_open 

120 

121 _JSON_OPEN_METHODS['.json.gz'] = gzip_open 

122 

123 

124logger = logging.getLogger(__name__) 

125 

126 

127def instance_cache(func): 

128 """Cache the result of a method on a per instance basis. 

129 

130 This is not a general purpose caching decorator. In order 

131 for this to be used, it must be used on methods on an 

132 instance, and that instance *must* provide a 

133 ``self._cache`` dictionary. 

134 

135 """ 

136 

137 def _wrapper(self, *args, **kwargs): 

138 key = (func.__name__,) + args 

139 for pair in sorted(kwargs.items()): 

140 key += pair 

141 if key in self._cache: 

142 return self._cache[key] 

143 data = func(self, *args, **kwargs) 

144 self._cache[key] = data 

145 return data 

146 

147 return _wrapper 

148 

149 

150class JSONFileLoader: 

151 """Loader JSON files. 

152 

153 This class can load the default format of models, which is a JSON file. 

154 

155 """ 

156 

157 def exists(self, file_path): 

158 """Checks if the file exists. 

159 

160 :type file_path: str 

161 :param file_path: The full path to the file to load without 

162 the '.json' extension. 

163 

164 :return: True if file path exists, False otherwise. 

165 

166 """ 

167 for ext in _JSON_OPEN_METHODS: 

168 if os.path.isfile(file_path + ext): 

169 return True 

170 return False 

171 

172 def _load_file(self, full_path, open_method): 

173 if not os.path.isfile(full_path): 

174 return 

175 

176 # By default the file will be opened with locale encoding on Python 3. 

177 # We specify "utf8" here to ensure the correct behavior. 

178 with open_method(full_path, 'rb') as fp: 

179 payload = fp.read().decode('utf-8') 

180 

181 logger.debug("Loading JSON file: %s", full_path) 

182 return json.loads(payload, object_pairs_hook=OrderedDict) 

183 

184 def load_file(self, file_path): 

185 """Attempt to load the file path. 

186 

187 :type file_path: str 

188 :param file_path: The full path to the file to load without 

189 the '.json' extension. 

190 

191 :return: The loaded data if it exists, otherwise None. 

192 

193 """ 

194 for ext, open_method in _JSON_OPEN_METHODS.items(): 

195 data = self._load_file(file_path + ext, open_method) 

196 if data is not None: 

197 return data 

198 return None 

199 

200 

201def create_loader(search_path_string=None): 

202 """Create a Loader class. 

203 

204 This factory function creates a loader given a search string path. 

205 

206 :type search_string_path: str 

207 :param search_string_path: The AWS_DATA_PATH value. A string 

208 of data path values separated by the ``os.path.pathsep`` value, 

209 which is typically ``:`` on POSIX platforms and ``;`` on 

210 windows. 

211 

212 :return: A ``Loader`` instance. 

213 

214 """ 

215 if search_path_string is None: 

216 return Loader() 

217 paths = [] 

218 extra_paths = search_path_string.split(os.pathsep) 

219 for path in extra_paths: 

220 path = os.path.expanduser(os.path.expandvars(path)) 

221 paths.append(path) 

222 return Loader(extra_search_paths=paths) 

223 

224 

225class Loader: 

226 """Find and load data models. 

227 

228 This class will handle searching for and loading data models. 

229 

230 The main method used here is ``load_service_model``, which is a 

231 convenience method over ``load_data`` and ``determine_latest_version``. 

232 

233 """ 

234 

235 FILE_LOADER_CLASS = JSONFileLoader 

236 # The included models in botocore/data/ that we ship with botocore. 

237 BUILTIN_DATA_PATH = os.path.join(BOTOCORE_ROOT, 'data') 

238 # For convenience we automatically add ~/.aws/models to the data path. 

239 CUSTOMER_DATA_PATH = os.path.join( 

240 os.path.expanduser('~'), '.aws', 'models' 

241 ) 

242 BUILTIN_EXTRAS_TYPES = ['sdk'] 

243 

244 def __init__( 

245 self, 

246 extra_search_paths=None, 

247 file_loader=None, 

248 cache=None, 

249 include_default_search_paths=True, 

250 include_default_extras=True, 

251 ): 

252 self._cache = {} 

253 if file_loader is None: 

254 file_loader = self.FILE_LOADER_CLASS() 

255 self.file_loader = file_loader 

256 if extra_search_paths is not None: 

257 self._search_paths = extra_search_paths 

258 else: 

259 self._search_paths = [] 

260 if include_default_search_paths: 

261 self._search_paths.extend( 

262 [self.CUSTOMER_DATA_PATH, self.BUILTIN_DATA_PATH] 

263 ) 

264 

265 self._extras_types = [] 

266 if include_default_extras: 

267 self._extras_types.extend(self.BUILTIN_EXTRAS_TYPES) 

268 

269 self._extras_processor = ExtrasProcessor() 

270 

271 @property 

272 def search_paths(self): 

273 return self._search_paths 

274 

275 @property 

276 def extras_types(self): 

277 return self._extras_types 

278 

279 @instance_cache 

280 def list_available_services(self, type_name): 

281 """List all known services. 

282 

283 This will traverse the search path and look for all known 

284 services. 

285 

286 :type type_name: str 

287 :param type_name: The type of the service (service-2, 

288 paginators-1, waiters-2, etc). This is needed because 

289 the list of available services depends on the service 

290 type. For example, the latest API version available for 

291 a resource-1.json file may not be the latest API version 

292 available for a services-2.json file. 

293 

294 :return: A list of all services. The list of services will 

295 be sorted. 

296 

297 """ 

298 services = set() 

299 for possible_path in self._potential_locations(): 

300 # Any directory in the search path is potentially a service. 

301 # We'll collect any initial list of potential services, 

302 # but we'll then need to further process these directories 

303 # by searching for the corresponding type_name in each 

304 # potential directory. 

305 possible_services = [ 

306 d 

307 for d in os.listdir(possible_path) 

308 if os.path.isdir(os.path.join(possible_path, d)) 

309 ] 

310 for service_name in possible_services: 

311 full_dirname = os.path.join(possible_path, service_name) 

312 api_versions = os.listdir(full_dirname) 

313 for api_version in api_versions: 

314 full_load_path = os.path.join( 

315 full_dirname, api_version, type_name 

316 ) 

317 if self.file_loader.exists(full_load_path): 

318 services.add(service_name) 

319 break 

320 return sorted(services) 

321 

322 @instance_cache 

323 def determine_latest_version(self, service_name, type_name): 

324 """Find the latest API version available for a service. 

325 

326 :type service_name: str 

327 :param service_name: The name of the service. 

328 

329 :type type_name: str 

330 :param type_name: The type of the service (service-2, 

331 paginators-1, waiters-2, etc). This is needed because 

332 the latest API version available can depend on the service 

333 type. For example, the latest API version available for 

334 a resource-1.json file may not be the latest API version 

335 available for a services-2.json file. 

336 

337 :rtype: str 

338 :return: The latest API version. If the service does not exist 

339 or does not have any available API data, then a 

340 ``DataNotFoundError`` exception will be raised. 

341 

342 """ 

343 return max(self.list_api_versions(service_name, type_name)) 

344 

345 @instance_cache 

346 def list_api_versions(self, service_name, type_name): 

347 """List all API versions available for a particular service type 

348 

349 :type service_name: str 

350 :param service_name: The name of the service 

351 

352 :type type_name: str 

353 :param type_name: The type name for the service (i.e service-2, 

354 paginators-1, etc.) 

355 

356 :rtype: list 

357 :return: A list of API version strings in sorted order. 

358 

359 """ 

360 known_api_versions = set() 

361 for possible_path in self._potential_locations( 

362 service_name, must_exist=True, is_dir=True 

363 ): 

364 for dirname in os.listdir(possible_path): 

365 full_path = os.path.join(possible_path, dirname, type_name) 

366 # Only add to the known_api_versions if the directory 

367 # contains a service-2, paginators-1, etc. file corresponding 

368 # to the type_name passed in. 

369 if self.file_loader.exists(full_path): 

370 known_api_versions.add(dirname) 

371 if not known_api_versions: 

372 raise DataNotFoundError(data_path=service_name) 

373 return sorted(known_api_versions) 

374 

375 @instance_cache 

376 def load_service_model(self, service_name, type_name, api_version=None): 

377 """Load a botocore service model 

378 

379 This is the main method for loading botocore models (e.g. a service 

380 model, pagination configs, waiter configs, etc.). 

381 

382 :type service_name: str 

383 :param service_name: The name of the service (e.g ``ec2``, ``s3``). 

384 

385 :type type_name: str 

386 :param type_name: The model type. Valid types include, but are not 

387 limited to: ``service-2``, ``paginators-1``, ``waiters-2``. 

388 

389 :type api_version: str 

390 :param api_version: The API version to load. If this is not 

391 provided, then the latest API version will be used. 

392 

393 :type load_extras: bool 

394 :param load_extras: Whether or not to load the tool extras which 

395 contain additional data to be added to the model. 

396 

397 :raises: UnknownServiceError if there is no known service with 

398 the provided service_name. 

399 

400 :raises: DataNotFoundError if no data could be found for the 

401 service_name/type_name/api_version. 

402 

403 :return: The loaded data, as a python type (e.g. dict, list, etc). 

404 """ 

405 # Wrapper around the load_data. This will calculate the path 

406 # to call load_data with. 

407 known_services = self.list_available_services(type_name) 

408 if service_name not in known_services: 

409 raise UnknownServiceError( 

410 service_name=service_name, 

411 known_service_names=', '.join(sorted(known_services)), 

412 ) 

413 if api_version is None: 

414 api_version = self.determine_latest_version( 

415 service_name, type_name 

416 ) 

417 full_path = os.path.join(service_name, api_version, type_name) 

418 model = self.load_data(full_path) 

419 

420 # Load in all the extras 

421 extras_data = self._find_extras(service_name, type_name, api_version) 

422 self._extras_processor.process(model, extras_data) 

423 

424 return model 

425 

426 def _find_extras(self, service_name, type_name, api_version): 

427 """Creates an iterator over all the extras data.""" 

428 for extras_type in self.extras_types: 

429 extras_name = f'{type_name}.{extras_type}-extras' 

430 full_path = os.path.join(service_name, api_version, extras_name) 

431 

432 try: 

433 yield self.load_data(full_path) 

434 except DataNotFoundError: 

435 pass 

436 

437 @instance_cache 

438 def load_data_with_path(self, name): 

439 """Same as ``load_data`` but returns file path as second return value. 

440 

441 :type name: str 

442 :param name: The data path, i.e ``ec2/2015-03-01/service-2``. 

443 

444 :return: Tuple of the loaded data and the path to the data file 

445 where the data was loaded from. If no data could be found then a 

446 DataNotFoundError is raised. 

447 """ 

448 for possible_path in self._potential_locations(name): 

449 found = self.file_loader.load_file(possible_path) 

450 if found is not None: 

451 return found, possible_path 

452 

453 # We didn't find anything that matched on any path. 

454 raise DataNotFoundError(data_path=name) 

455 

456 def load_data(self, name): 

457 """Load data given a data path. 

458 

459 This is a low level method that will search through the various 

460 search paths until it's able to load a value. This is typically 

461 only needed to load *non* model files (such as _endpoints and 

462 _retry). If you need to load model files, you should prefer 

463 ``load_service_model``. Use ``load_data_with_path`` to get the 

464 data path of the data file as second return value. 

465 

466 :type name: str 

467 :param name: The data path, i.e ``ec2/2015-03-01/service-2``. 

468 

469 :return: The loaded data. If no data could be found then 

470 a DataNotFoundError is raised. 

471 """ 

472 data, _ = self.load_data_with_path(name) 

473 return data 

474 

475 def _potential_locations(self, name=None, must_exist=False, is_dir=False): 

476 # Will give an iterator over the full path of potential locations 

477 # according to the search path. 

478 for path in self.search_paths: 

479 if os.path.isdir(path): 

480 full_path = path 

481 if name is not None: 

482 full_path = os.path.join(path, name) 

483 if not must_exist: 

484 yield full_path 

485 else: 

486 if is_dir and os.path.isdir(full_path): 

487 yield full_path 

488 elif os.path.exists(full_path): 

489 yield full_path 

490 

491 def is_builtin_path(self, path): 

492 """Whether a given path is within the package's data directory. 

493 

494 This method can be used together with load_data_with_path(name) 

495 to determine if data has been loaded from a file bundled with the 

496 package, as opposed to a file in a separate location. 

497 

498 :type path: str 

499 :param path: The file path to check. 

500 

501 :return: Whether the given path is within the package's data directory. 

502 """ 

503 path = os.path.expanduser(os.path.expandvars(path)) 

504 return path.startswith(self.BUILTIN_DATA_PATH) 

505 

506 

507class ExtrasProcessor: 

508 """Processes data from extras files into service models.""" 

509 

510 def process(self, original_model, extra_models): 

511 """Processes data from a list of loaded extras files into a model 

512 

513 :type original_model: dict 

514 :param original_model: The service model to load all the extras into. 

515 

516 :type extra_models: iterable of dict 

517 :param extra_models: A list of loaded extras models. 

518 """ 

519 for extras in extra_models: 

520 self._process(original_model, extras) 

521 

522 def _process(self, model, extra_model): 

523 """Process a single extras model into a service model.""" 

524 if 'merge' in extra_model: 

525 deep_merge(model, extra_model['merge'])