about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/botocore/loaders.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/botocore/loaders.py')
-rw-r--r--.venv/lib/python3.12/site-packages/botocore/loaders.py525
1 files changed, 525 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/botocore/loaders.py b/.venv/lib/python3.12/site-packages/botocore/loaders.py
new file mode 100644
index 00000000..f5072a3e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/botocore/loaders.py
@@ -0,0 +1,525 @@
+# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+"""Module for loading various model files.
+
+This module provides the classes that are used to load models used
+by botocore.  This can include:
+
+    * Service models (e.g. the model for EC2, S3, DynamoDB, etc.)
+    * Service model extras which customize the service models
+    * Other models associated with a service (pagination, waiters)
+    * Non service-specific config (Endpoint data, retry config)
+
+Loading a module is broken down into several steps:
+
+    * Determining the path to load
+    * Search the data_path for files to load
+    * The mechanics of loading the file
+    * Searching for extras and applying them to the loaded file
+
+The last item is used so that other faster loading mechanism
+besides the default JSON loader can be used.
+
+The Search Path
+===============
+
+Similar to how the PATH environment variable is to finding executables
+and the PYTHONPATH environment variable is to finding python modules
+to import, the botocore loaders have the concept of a data path exposed
+through AWS_DATA_PATH.
+
+This enables end users to provide additional search paths where we
+will attempt to load models outside of the models we ship with
+botocore.  When you create a ``Loader``, there are two paths
+automatically added to the model search path:
+
+    * <botocore root>/data/
+    * ~/.aws/models
+
+The first value is the path where all the model files shipped with
+botocore are located.
+
+The second path is so that users can just drop new model files in
+``~/.aws/models`` without having to mess around with the AWS_DATA_PATH.
+
+The AWS_DATA_PATH using the platform specific path separator to
+separate entries (typically ``:`` on linux and ``;`` on windows).
+
+
+Directory Layout
+================
+
+The Loader expects a particular directory layout.  In order for any
+directory specified in AWS_DATA_PATH to be considered, it must have
+this structure for service models::
+
+    <root>
+      |
+      |-- servicename1
+      |   |-- 2012-10-25
+      |       |-- service-2.json
+      |-- ec2
+      |   |-- 2014-01-01
+      |   |   |-- paginators-1.json
+      |   |   |-- service-2.json
+      |   |   |-- waiters-2.json
+      |   |-- 2015-03-01
+      |       |-- paginators-1.json
+      |       |-- service-2.json
+      |       |-- waiters-2.json
+      |       |-- service-2.sdk-extras.json
+
+
+That is:
+
+    * The root directory contains sub directories that are the name
+      of the services.
+    * Within each service directory, there's a sub directory for each
+      available API version.
+    * Within each API version, there are model specific files, including
+      (but not limited to): service-2.json, waiters-2.json, paginators-1.json
+
+The ``-1`` and ``-2`` suffix at the end of the model files denote which version
+schema is used within the model.  Even though this information is available in
+the ``version`` key within the model, this version is also part of the filename
+so that code does not need to load the JSON model in order to determine which
+version to use.
+
+The ``sdk-extras`` and similar files represent extra data that needs to be
+applied to the model after it is loaded. Data in these files might represent
+information that doesn't quite fit in the original models, but is still needed
+for the sdk. For instance, additional operation parameters might be added here
+which don't represent the actual service api.
+"""
+
+import logging
+import os
+
+from botocore import BOTOCORE_ROOT
+from botocore.compat import HAS_GZIP, OrderedDict, json
+from botocore.exceptions import DataNotFoundError, UnknownServiceError
+from botocore.utils import deep_merge
+
+_JSON_OPEN_METHODS = {
+    '.json': open,
+}
+
+
+if HAS_GZIP:
+    from gzip import open as gzip_open
+
+    _JSON_OPEN_METHODS['.json.gz'] = gzip_open
+
+
+logger = logging.getLogger(__name__)
+
+
+def instance_cache(func):
+    """Cache the result of a method on a per instance basis.
+
+    This is not a general purpose caching decorator.  In order
+    for this to be used, it must be used on methods on an
+    instance, and that instance *must* provide a
+    ``self._cache`` dictionary.
+
+    """
+
+    def _wrapper(self, *args, **kwargs):
+        key = (func.__name__,) + args
+        for pair in sorted(kwargs.items()):
+            key += pair
+        if key in self._cache:
+            return self._cache[key]
+        data = func(self, *args, **kwargs)
+        self._cache[key] = data
+        return data
+
+    return _wrapper
+
+
+class JSONFileLoader:
+    """Loader JSON files.
+
+    This class can load the default format of models, which is a JSON file.
+
+    """
+
+    def exists(self, file_path):
+        """Checks if the file exists.
+
+        :type file_path: str
+        :param file_path: The full path to the file to load without
+            the '.json' extension.
+
+        :return: True if file path exists, False otherwise.
+
+        """
+        for ext in _JSON_OPEN_METHODS:
+            if os.path.isfile(file_path + ext):
+                return True
+        return False
+
+    def _load_file(self, full_path, open_method):
+        if not os.path.isfile(full_path):
+            return
+
+        # By default the file will be opened with locale encoding on Python 3.
+        # We specify "utf8" here to ensure the correct behavior.
+        with open_method(full_path, 'rb') as fp:
+            payload = fp.read().decode('utf-8')
+
+        logger.debug("Loading JSON file: %s", full_path)
+        return json.loads(payload, object_pairs_hook=OrderedDict)
+
+    def load_file(self, file_path):
+        """Attempt to load the file path.
+
+        :type file_path: str
+        :param file_path: The full path to the file to load without
+            the '.json' extension.
+
+        :return: The loaded data if it exists, otherwise None.
+
+        """
+        for ext, open_method in _JSON_OPEN_METHODS.items():
+            data = self._load_file(file_path + ext, open_method)
+            if data is not None:
+                return data
+        return None
+
+
+def create_loader(search_path_string=None):
+    """Create a Loader class.
+
+    This factory function creates a loader given a search string path.
+
+    :type search_string_path: str
+    :param search_string_path: The AWS_DATA_PATH value.  A string
+        of data path values separated by the ``os.path.pathsep`` value,
+        which is typically ``:`` on POSIX platforms and ``;`` on
+        windows.
+
+    :return: A ``Loader`` instance.
+
+    """
+    if search_path_string is None:
+        return Loader()
+    paths = []
+    extra_paths = search_path_string.split(os.pathsep)
+    for path in extra_paths:
+        path = os.path.expanduser(os.path.expandvars(path))
+        paths.append(path)
+    return Loader(extra_search_paths=paths)
+
+
+class Loader:
+    """Find and load data models.
+
+    This class will handle searching for and loading data models.
+
+    The main method used here is ``load_service_model``, which is a
+    convenience method over ``load_data`` and ``determine_latest_version``.
+
+    """
+
+    FILE_LOADER_CLASS = JSONFileLoader
+    # The included models in botocore/data/ that we ship with botocore.
+    BUILTIN_DATA_PATH = os.path.join(BOTOCORE_ROOT, 'data')
+    # For convenience we automatically add ~/.aws/models to the data path.
+    CUSTOMER_DATA_PATH = os.path.join(
+        os.path.expanduser('~'), '.aws', 'models'
+    )
+    BUILTIN_EXTRAS_TYPES = ['sdk']
+
+    def __init__(
+        self,
+        extra_search_paths=None,
+        file_loader=None,
+        cache=None,
+        include_default_search_paths=True,
+        include_default_extras=True,
+    ):
+        self._cache = {}
+        if file_loader is None:
+            file_loader = self.FILE_LOADER_CLASS()
+        self.file_loader = file_loader
+        if extra_search_paths is not None:
+            self._search_paths = extra_search_paths
+        else:
+            self._search_paths = []
+        if include_default_search_paths:
+            self._search_paths.extend(
+                [self.CUSTOMER_DATA_PATH, self.BUILTIN_DATA_PATH]
+            )
+
+        self._extras_types = []
+        if include_default_extras:
+            self._extras_types.extend(self.BUILTIN_EXTRAS_TYPES)
+
+        self._extras_processor = ExtrasProcessor()
+
+    @property
+    def search_paths(self):
+        return self._search_paths
+
+    @property
+    def extras_types(self):
+        return self._extras_types
+
+    @instance_cache
+    def list_available_services(self, type_name):
+        """List all known services.
+
+        This will traverse the search path and look for all known
+        services.
+
+        :type type_name: str
+        :param type_name: The type of the service (service-2,
+            paginators-1, waiters-2, etc).  This is needed because
+            the list of available services depends on the service
+            type.  For example, the latest API version available for
+            a resource-1.json file may not be the latest API version
+            available for a services-2.json file.
+
+        :return: A list of all services.  The list of services will
+            be sorted.
+
+        """
+        services = set()
+        for possible_path in self._potential_locations():
+            # Any directory in the search path is potentially a service.
+            # We'll collect any initial list of potential services,
+            # but we'll then need to further process these directories
+            # by searching for the corresponding type_name in each
+            # potential directory.
+            possible_services = [
+                d
+                for d in os.listdir(possible_path)
+                if os.path.isdir(os.path.join(possible_path, d))
+            ]
+            for service_name in possible_services:
+                full_dirname = os.path.join(possible_path, service_name)
+                api_versions = os.listdir(full_dirname)
+                for api_version in api_versions:
+                    full_load_path = os.path.join(
+                        full_dirname, api_version, type_name
+                    )
+                    if self.file_loader.exists(full_load_path):
+                        services.add(service_name)
+                        break
+        return sorted(services)
+
+    @instance_cache
+    def determine_latest_version(self, service_name, type_name):
+        """Find the latest API version available for a service.
+
+        :type service_name: str
+        :param service_name: The name of the service.
+
+        :type type_name: str
+        :param type_name: The type of the service (service-2,
+            paginators-1, waiters-2, etc).  This is needed because
+            the latest API version available can depend on the service
+            type.  For example, the latest API version available for
+            a resource-1.json file may not be the latest API version
+            available for a services-2.json file.
+
+        :rtype: str
+        :return: The latest API version.  If the service does not exist
+            or does not have any available API data, then a
+            ``DataNotFoundError`` exception will be raised.
+
+        """
+        return max(self.list_api_versions(service_name, type_name))
+
+    @instance_cache
+    def list_api_versions(self, service_name, type_name):
+        """List all API versions available for a particular service type
+
+        :type service_name: str
+        :param service_name: The name of the service
+
+        :type type_name: str
+        :param type_name: The type name for the service (i.e service-2,
+            paginators-1, etc.)
+
+        :rtype: list
+        :return: A list of API version strings in sorted order.
+
+        """
+        known_api_versions = set()
+        for possible_path in self._potential_locations(
+            service_name, must_exist=True, is_dir=True
+        ):
+            for dirname in os.listdir(possible_path):
+                full_path = os.path.join(possible_path, dirname, type_name)
+                # Only add to the known_api_versions if the directory
+                # contains a service-2, paginators-1, etc. file corresponding
+                # to the type_name passed in.
+                if self.file_loader.exists(full_path):
+                    known_api_versions.add(dirname)
+        if not known_api_versions:
+            raise DataNotFoundError(data_path=service_name)
+        return sorted(known_api_versions)
+
+    @instance_cache
+    def load_service_model(self, service_name, type_name, api_version=None):
+        """Load a botocore service model
+
+        This is the main method for loading botocore models (e.g. a service
+        model, pagination configs, waiter configs, etc.).
+
+        :type service_name: str
+        :param service_name: The name of the service (e.g ``ec2``, ``s3``).
+
+        :type type_name: str
+        :param type_name: The model type.  Valid types include, but are not
+            limited to: ``service-2``, ``paginators-1``, ``waiters-2``.
+
+        :type api_version: str
+        :param api_version: The API version to load.  If this is not
+            provided, then the latest API version will be used.
+
+        :type load_extras: bool
+        :param load_extras: Whether or not to load the tool extras which
+            contain additional data to be added to the model.
+
+        :raises: UnknownServiceError if there is no known service with
+            the provided service_name.
+
+        :raises: DataNotFoundError if no data could be found for the
+            service_name/type_name/api_version.
+
+        :return: The loaded data, as a python type (e.g. dict, list, etc).
+        """
+        # Wrapper around the load_data.  This will calculate the path
+        # to call load_data with.
+        known_services = self.list_available_services(type_name)
+        if service_name not in known_services:
+            raise UnknownServiceError(
+                service_name=service_name,
+                known_service_names=', '.join(sorted(known_services)),
+            )
+        if api_version is None:
+            api_version = self.determine_latest_version(
+                service_name, type_name
+            )
+        full_path = os.path.join(service_name, api_version, type_name)
+        model = self.load_data(full_path)
+
+        # Load in all the extras
+        extras_data = self._find_extras(service_name, type_name, api_version)
+        self._extras_processor.process(model, extras_data)
+
+        return model
+
+    def _find_extras(self, service_name, type_name, api_version):
+        """Creates an iterator over all the extras data."""
+        for extras_type in self.extras_types:
+            extras_name = f'{type_name}.{extras_type}-extras'
+            full_path = os.path.join(service_name, api_version, extras_name)
+
+            try:
+                yield self.load_data(full_path)
+            except DataNotFoundError:
+                pass
+
+    @instance_cache
+    def load_data_with_path(self, name):
+        """Same as ``load_data`` but returns file path as second return value.
+
+        :type name: str
+        :param name: The data path, i.e ``ec2/2015-03-01/service-2``.
+
+        :return: Tuple of the loaded data and the path to the data file
+            where the data was loaded from. If no data could be found then a
+            DataNotFoundError is raised.
+        """
+        for possible_path in self._potential_locations(name):
+            found = self.file_loader.load_file(possible_path)
+            if found is not None:
+                return found, possible_path
+
+        # We didn't find anything that matched on any path.
+        raise DataNotFoundError(data_path=name)
+
+    def load_data(self, name):
+        """Load data given a data path.
+
+        This is a low level method that will search through the various
+        search paths until it's able to load a value.  This is typically
+        only needed to load *non* model files (such as _endpoints and
+        _retry).  If you need to load model files, you should prefer
+        ``load_service_model``.  Use ``load_data_with_path`` to get the
+        data path of the data file as second return value.
+
+        :type name: str
+        :param name: The data path, i.e ``ec2/2015-03-01/service-2``.
+
+        :return: The loaded data. If no data could be found then
+            a DataNotFoundError is raised.
+        """
+        data, _ = self.load_data_with_path(name)
+        return data
+
+    def _potential_locations(self, name=None, must_exist=False, is_dir=False):
+        # Will give an iterator over the full path of potential locations
+        # according to the search path.
+        for path in self.search_paths:
+            if os.path.isdir(path):
+                full_path = path
+                if name is not None:
+                    full_path = os.path.join(path, name)
+                if not must_exist:
+                    yield full_path
+                else:
+                    if is_dir and os.path.isdir(full_path):
+                        yield full_path
+                    elif os.path.exists(full_path):
+                        yield full_path
+
+    def is_builtin_path(self, path):
+        """Whether a given path is within the package's data directory.
+
+        This method can be used together with load_data_with_path(name)
+        to determine if data has been loaded from a file bundled with the
+        package, as opposed to a file in a separate location.
+
+        :type path: str
+        :param path: The file path to check.
+
+        :return: Whether the given path is within the package's data directory.
+        """
+        path = os.path.expanduser(os.path.expandvars(path))
+        return path.startswith(self.BUILTIN_DATA_PATH)
+
+
+class ExtrasProcessor:
+    """Processes data from extras files into service models."""
+
+    def process(self, original_model, extra_models):
+        """Processes data from a list of loaded extras files into a model
+
+        :type original_model: dict
+        :param original_model: The service model to load all the extras into.
+
+        :type extra_models: iterable of dict
+        :param extra_models: A list of loaded extras models.
+        """
+        for extras in extra_models:
+            self._process(original_model, extras)
+
+    def _process(self, model, extra_model):
+        """Process a single extras model into a service model."""
+        if 'merge' in extra_model:
+            deep_merge(model, extra_model['merge'])