about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/botocore/configloader.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/botocore/configloader.py')
-rw-r--r--.venv/lib/python3.12/site-packages/botocore/configloader.py287
1 files changed, 287 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/botocore/configloader.py b/.venv/lib/python3.12/site-packages/botocore/configloader.py
new file mode 100644
index 00000000..0b6c82bc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/botocore/configloader.py
@@ -0,0 +1,287 @@
+# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
+# Copyright 2012-2016 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.
+import configparser
+import copy
+import os
+import shlex
+import sys
+
+import botocore.exceptions
+
+
+def multi_file_load_config(*filenames):
+    """Load and combine multiple INI configs with profiles.
+
+    This function will take a list of filesnames and return
+    a single dictionary that represents the merging of the loaded
+    config files.
+
+    If any of the provided filenames does not exist, then that file
+    is ignored.  It is therefore ok to provide a list of filenames,
+    some of which may not exist.
+
+    Configuration files are **not** deep merged, only the top level
+    keys are merged.  The filenames should be passed in order of
+    precedence.  The first config file has precedence over the
+    second config file, which has precedence over the third config file,
+    etc.  The only exception to this is that the "profiles" key is
+    merged to combine profiles from multiple config files into a
+    single profiles mapping.  However, if a profile is defined in
+    multiple config files, then the config file with the highest
+    precedence is used.  Profile values themselves are not merged.
+    For example::
+
+        FileA              FileB                FileC
+        [foo]             [foo]                 [bar]
+        a=1               a=2                   a=3
+                          b=2
+
+        [bar]             [baz]                [profile a]
+        a=2               a=3                  region=e
+
+        [profile a]       [profile b]          [profile c]
+        region=c          region=d             region=f
+
+    The final result of ``multi_file_load_config(FileA, FileB, FileC)``
+    would be::
+
+        {"foo": {"a": 1}, "bar": {"a": 2}, "baz": {"a": 3},
+        "profiles": {"a": {"region": "c"}}, {"b": {"region": d"}},
+                    {"c": {"region": "f"}}}
+
+    Note that the "foo" key comes from A, even though it's defined in both
+    FileA and FileB.  Because "foo" was defined in FileA first, then the values
+    for "foo" from FileA are used and the values for "foo" from FileB are
+    ignored.  Also note where the profiles originate from.  Profile "a"
+    comes FileA, profile "b" comes from FileB, and profile "c" comes
+    from FileC.
+
+    """
+    configs = []
+    profiles = []
+    for filename in filenames:
+        try:
+            loaded = load_config(filename)
+        except botocore.exceptions.ConfigNotFound:
+            continue
+        profiles.append(loaded.pop('profiles'))
+        configs.append(loaded)
+    merged_config = _merge_list_of_dicts(configs)
+    merged_profiles = _merge_list_of_dicts(profiles)
+    merged_config['profiles'] = merged_profiles
+    return merged_config
+
+
+def _merge_list_of_dicts(list_of_dicts):
+    merged_dicts = {}
+    for single_dict in list_of_dicts:
+        for key, value in single_dict.items():
+            if key not in merged_dicts:
+                merged_dicts[key] = value
+    return merged_dicts
+
+
+def load_config(config_filename):
+    """Parse a INI config with profiles.
+
+    This will parse an INI config file and map top level profiles
+    into a top level "profile" key.
+
+    If you want to parse an INI file and map all section names to
+    top level keys, use ``raw_config_parse`` instead.
+
+    """
+    parsed = raw_config_parse(config_filename)
+    return build_profile_map(parsed)
+
+
+def raw_config_parse(config_filename, parse_subsections=True):
+    """Returns the parsed INI config contents.
+
+    Each section name is a top level key.
+
+    :param config_filename: The name of the INI file to parse
+
+    :param parse_subsections: If True, parse indented blocks as
+       subsections that represent their own configuration dictionary.
+       For example, if the config file had the contents::
+
+           s3 =
+              signature_version = s3v4
+              addressing_style = path
+
+        The resulting ``raw_config_parse`` would be::
+
+            {'s3': {'signature_version': 's3v4', 'addressing_style': 'path'}}
+
+       If False, do not try to parse subsections and return the indented
+       block as its literal value::
+
+            {'s3': '\nsignature_version = s3v4\naddressing_style = path'}
+
+    :returns: A dict with keys for each profile found in the config
+        file and the value of each key being a dict containing name
+        value pairs found in that profile.
+
+    :raises: ConfigNotFound, ConfigParseError
+    """
+    config = {}
+    path = config_filename
+    if path is not None:
+        path = os.path.expandvars(path)
+        path = os.path.expanduser(path)
+        if not os.path.isfile(path):
+            raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path))
+        cp = configparser.RawConfigParser()
+        try:
+            cp.read([path])
+        except (configparser.Error, UnicodeDecodeError) as e:
+            raise botocore.exceptions.ConfigParseError(
+                path=_unicode_path(path), error=e
+            ) from None
+        else:
+            for section in cp.sections():
+                config[section] = {}
+                for option in cp.options(section):
+                    config_value = cp.get(section, option)
+                    if parse_subsections and config_value.startswith('\n'):
+                        # Then we need to parse the inner contents as
+                        # hierarchical.  We support a single level
+                        # of nesting for now.
+                        try:
+                            config_value = _parse_nested(config_value)
+                        except ValueError as e:
+                            raise botocore.exceptions.ConfigParseError(
+                                path=_unicode_path(path), error=e
+                            ) from None
+                    config[section][option] = config_value
+    return config
+
+
+def _unicode_path(path):
+    if isinstance(path, str):
+        return path
+    # According to the documentation getfilesystemencoding can return None
+    # on unix in which case the default encoding is used instead.
+    filesystem_encoding = sys.getfilesystemencoding()
+    if filesystem_encoding is None:
+        filesystem_encoding = sys.getdefaultencoding()
+    return path.decode(filesystem_encoding, 'replace')
+
+
+def _parse_nested(config_value):
+    # Given a value like this:
+    # \n
+    # foo = bar
+    # bar = baz
+    # We need to parse this into
+    # {'foo': 'bar', 'bar': 'baz}
+    parsed = {}
+    for line in config_value.splitlines():
+        line = line.strip()
+        if not line:
+            continue
+        # The caller will catch ValueError
+        # and raise an appropriate error
+        # if this fails.
+        key, value = line.split('=', 1)
+        parsed[key.strip()] = value.strip()
+    return parsed
+
+
+def _parse_section(key, values):
+    result = {}
+    try:
+        parts = shlex.split(key)
+    except ValueError:
+        return result
+    if len(parts) == 2:
+        result[parts[1]] = values
+    return result
+
+
+def build_profile_map(parsed_ini_config):
+    """Convert the parsed INI config into a profile map.
+
+    The config file format requires that every profile except the
+    default to be prepended with "profile", e.g.::
+
+        [profile test]
+        aws_... = foo
+        aws_... = bar
+
+        [profile bar]
+        aws_... = foo
+        aws_... = bar
+
+        # This is *not* a profile
+        [preview]
+        otherstuff = 1
+
+        # Neither is this
+        [foobar]
+        morestuff = 2
+
+    The build_profile_map will take a parsed INI config file where each top
+    level key represents a section name, and convert into a format where all
+    the profiles are under a single top level "profiles" key, and each key in
+    the sub dictionary is a profile name.  For example, the above config file
+    would be converted from::
+
+        {"profile test": {"aws_...": "foo", "aws...": "bar"},
+         "profile bar": {"aws...": "foo", "aws...": "bar"},
+         "preview": {"otherstuff": ...},
+         "foobar": {"morestuff": ...},
+         }
+
+    into::
+
+        {"profiles": {"test": {"aws_...": "foo", "aws...": "bar"},
+                      "bar": {"aws...": "foo", "aws...": "bar"},
+         "preview": {"otherstuff": ...},
+         "foobar": {"morestuff": ...},
+        }
+
+    If there are no profiles in the provided parsed INI contents, then
+    an empty dict will be the value associated with the ``profiles`` key.
+
+    .. note::
+
+        This will not mutate the passed in parsed_ini_config.  Instead it will
+        make a deepcopy and return that value.
+
+    """
+    parsed_config = copy.deepcopy(parsed_ini_config)
+    profiles = {}
+    sso_sessions = {}
+    services = {}
+    final_config = {}
+    for key, values in parsed_config.items():
+        if key.startswith("profile"):
+            profiles.update(_parse_section(key, values))
+        elif key.startswith("sso-session"):
+            sso_sessions.update(_parse_section(key, values))
+        elif key.startswith("services"):
+            services.update(_parse_section(key, values))
+        elif key == 'default':
+            # default section is special and is considered a profile
+            # name but we don't require you use 'profile "default"'
+            # as a section.
+            profiles[key] = values
+        else:
+            final_config[key] = values
+    final_config['profiles'] = profiles
+    final_config['sso_sessions'] = sso_sessions
+    final_config['services'] = services
+    return final_config