commit 106b27e730001558331a59ba4154d01565f53d8a Author: Stefano Pigozzi Date: Wed Apr 13 17:54:53 2022 +0200 :tada: First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9585041 --- /dev/null +++ b/.gitignore @@ -0,0 +1,238 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..39102d7 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..c5fe822 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6bf142f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cfig.iml b/cfig.iml new file mode 100644 index 0000000..31ae860 --- /dev/null +++ b/cfig.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/cfig/__init__.py b/cfig/__init__.py new file mode 100644 index 0000000..c2e88c7 --- /dev/null +++ b/cfig/__init__.py @@ -0,0 +1,69 @@ +""" +This package provides a simple-to-use but featureful configuration manager for Python applications. + +A goal is to allow easy integration of an application with multiple configuration standards, such as environment +variables, dotenv files, and Docker Secrets files. + +Another goal is to provide informative error messages to the user who is configuring the application, so that they may +understand what they are doing wrong and fix it immediately. + +The final goal is for the package to be fully typed, so that useful information can be received by the developer +programming the consumption the configuration files. + +Ideally, the configuration file for an application should look like this: + +.. code-block:: python + + # Import the cfig library + import cfig + + # Create a "Configurable" object + config = cfig.Configurable() + + # Use the object to wrap configurable values + @config.value() + # Value information is determined by parameters of a function + # Name, parameters, docstring, and return annotation are used + # The function name is unusually in SCREAMING_SNAKE_CASE + def SECRET_KEY(val: str) -> str: + "Secret string used to manage tokens." + return val + + @config.value() + def ALLOWED_USERS(val: str) -> int: + "The maximum number of allowed users in the application." + # Values can be altered to become more useful to the programmer + # Errors are managed by cfig + return int(val) + + @config.value() + # If the val variable has an Optional annotation, cfig will mark that value as optional + def ACCEPTED_TERMS_AND_CONDITIONS(val: Optional[str]) -> bool: + "To accept T&C, set this to a non-blank string." + return val is not None + + if __name__ == "__main__": + # If the configuration file is executed as main, handle the call and display a user-friendly CLI interface. + config() + +Configured values can later be accessed by importing the configuration file: + +.. code-block:: python + + # Import the previously defined file + from . import myconfig + + # Function is executed once when the value is first accessed + print(f"Maximum allowed users: {myconfig.ALLOWED_USERS}") + +Configuration files of dependencies can be merged into the current + +""" + + +from .config import Configurable + + +__all__ = ( + "Configurable", +) diff --git a/cfig/config.py b/cfig/config.py new file mode 100644 index 0000000..ec2047f --- /dev/null +++ b/cfig/config.py @@ -0,0 +1,194 @@ +""" +This module defines the :class:`Configuration` class. + +The used terminology is: + +Key + The base name of a configuration value. + For example, the name of an environment variable. + +Value + A single non-processed configuration value in :class:`str` form. + For example, the raw string value of an environment variable. + +Item + A single processed value in any form. + Internally, this is a :class:`lazy_object_proxy.Proxy`: an object whose value is not retrieved until it is accessed. + +Configurable + A specially decorated function that processes a value before it is turned into an item. + +Configuration + A group of items. +""" + +import os +import lazy_object_proxy +import typing as t +import logging +from . import errors +from . import customtyping as ct +from . import sources as s + +log = logging.getLogger(__name__) + + +class Configuration: + """ + A group of configurable items. + """ + + DEFAULT_SOURCES = [ + s.EnvironmentSource(), + s.EnvironmentFileSource(), + ] + + def __init__(self, *, sources: t.Optional[t.Collection[s.Source]] = None): + log.debug(f"Initializing a new {self.__class__.__qualname__} object...") + + self.sources: t.Collection[s.Source] = sources or self.DEFAULT_SOURCES + """ + Collection of all places from where values should be retrieved from. + """ + + self.items: dict[str, t.Any] = {} + """ + :class:`dict` mapping all keys registered to this object to their respective items. + """ + + log.debug("Initialized successfully!") + + # noinspection PyMethodMayBeStatic + def _determine_configurable_key(self, f: ct.Configurable) -> str: + """ + Determine the key of a configurable. + """ + + try: + return f.__name__ + except AttributeError: + log.error(f"Could not determine key of: {f!r}") + raise errors.UnknownKeyError() + + def required(self) -> t.Callable[[ct.ConfigurableRequired], ct.TYPE]: + """ + Create the decorator to convert the decorated function into a required configurable. + """ + + def _decorator(configurable: ct.ConfigurableRequired) -> ct.TYPE: + log.debug("Determining key...") + key: str = self._determine_configurable_key(configurable) + log.debug(f"Key is: {key!r}") + + log.debug("Creating required item...") + item: ct.TYPE = self._create_item_required(key, configurable) + log.debug("Item created successfully!") + + log.debug("Registering item in the configuration...") + self._register_item(key, item) + log.debug("Registered successfully!") + + # Return the created item so it will take the place of the decorated function + return item + + return _decorator + + def optional(self) -> t.Callable[[ct.ConfigurableOptional], ct.TYPE]: + """ + Create the decorator to convert the decorated function into a required configurable. + """ + + def _decorator(configurable: ct.ConfigurableOptional) -> ct.TYPE: + log.debug("Determining key...") + key: str = self._determine_configurable_key(configurable) + log.debug(f"Key is: {key!r}") + + log.debug("Creating optional item...") + item: ct.TYPE = self._create_item_optional(key, configurable) + log.debug("Item created successfully!") + + log.debug("Registering item in the configuration...") + self._register_item(key, item) + log.debug("Registered successfully!") + + # Return the created item so it will take the place of the decorated function + return item + + return _decorator + + def _create_item_optional(self, key: str, f: ct.ConfigurableOptional) -> lazy_object_proxy.Proxy: + """ + Create a new optional item. + """ + + @lazy_object_proxy.Proxy + def _decorated(): + log.debug(f"Retrieving value with key: {key!r}") + val = self._retrieve_value_optional(key) + log.debug("Retrieved val successfully!") + + log.debug("Running user-defined configurable function...") + val = f(val) + log.info(f"{key} = {val!r}") + + return val + + return _decorated + + def _create_item_required(self, key: str, f: ct.ConfigurableRequired) -> lazy_object_proxy.Proxy: + """ + Create a new required item. + """ + + @lazy_object_proxy.Proxy + def _decorated(): + log.debug(f"Retrieving value with key: {key!r}") + val = self._retrieve_value_required(key) + log.debug("Retrieved val successfully!") + + log.debug("Running user-defined configurable function...") + val = f(val) + log.info(f"{key} = {val!r}") + + return val + + return _decorated + + def _retrieve_value_optional(self, key: str) -> t.Optional[str]: + """ + Try to retrieve a value from all :attr:`.sources` of this Configuration. + """ + + for source in self.sources: + log.debug(f"Trying to retrieve {key!r} from {source!r}...") + if value := source.get(key): + log.debug(f"Retrieved {key!r} from {source!r}: {value!r}") + return value + else: + log.debug(f"No values found for {key!r}, returning None.") + return None + + def _retrieve_value_required(self, key: str) -> str: + """ + Retrieve a new value from all supported configuration schemes in :class:`str` form. + """ + + if value := self._retrieve_value_optional(key): + return value + else: + raise errors.ConfigurationError(f"Missing {key} configuration value.") + + def _register_item(self, key, item): + """ + Register an item in this Configuration. + """ + + if key in self.items: + raise errors.DuplicateError(key) + + self.items[key] = item + + def fetch_all(self): + log.debug("Fetching now all configuration items...") + for value in self.items.values(): + _ = value.__wrapped__ diff --git a/cfig/customtyping.py b/cfig/customtyping.py new file mode 100644 index 0000000..1ed65eb --- /dev/null +++ b/cfig/customtyping.py @@ -0,0 +1,27 @@ +import typing as t + +TYPE = t.TypeVar("TYPE") + + +class Configurable(t.Protocol): + __name__: str + + def __call__(self, val: t.Any) -> TYPE: + ... + + +class ConfigurableRequired(Configurable): + def __call__(self, val: str) -> TYPE: + ... + + +class ConfigurableOptional(Configurable): + def __call__(self, val: t.Optional[str]) -> TYPE: + ... + + +__all__ = ( + "TYPE", + "ConfigurableRequired", + "ConfigurableOptional", +) diff --git a/cfig/errors.py b/cfig/errors.py new file mode 100644 index 0000000..e35876b --- /dev/null +++ b/cfig/errors.py @@ -0,0 +1,30 @@ +class DefinitionError(Exception): + """ + An error is present in the definition of a :class:`cfig.Configurable` object. + """ + + +class UnknownKeyError(DefinitionError): + """ + It was not possible to get the name of the wrapped function. + + Perhaps a call to :func:`functools.wraps` is missing? + """ + + +class RegistrationError(DefinitionError): + """ + An error occurred during the proxy registration step. + """ + + +class DuplicateError(RegistrationError): + """ + Another proxy with the same name is already registered. + """ + + +class ConfigurationError(Exception): + """ + An error is present in the configuration specified by the user. + """ diff --git a/cfig/sources.py b/cfig/sources.py new file mode 100644 index 0000000..869fa4a --- /dev/null +++ b/cfig/sources.py @@ -0,0 +1,72 @@ +import typing as t +import abc +import os + + +class Source(metaclass=abc.ABCMeta): + """ + A source of values to be tapped by configurations. + """ + + @abc.abstractmethod + def get(self, key: str) -> t.Optional[str]: + raise NotImplementedError() + + +class EnvironmentSource(Source): + """ + A source which gets values from environment variables. + """ + + def __init__(self, *, prefix: str = "", suffix: str = "", environment=os.environ): + self.prefix: str = prefix + """ + The prefix to be prepended to all environment variable names. + + For example, ``PROD_`` for production environment variables. + """ + + self.suffix: str = suffix + """ + The suffix to be appended to all environment variable names. + + For example, ``_VAL`` for raw values. + """ + + self.environment = environment + """ + The environment to retrieve variable values from. + + Defaults to :data:`os.environ`. + """ + + def _process_key(self, key: str) -> str: + return f"{self.prefix}{key}{self.suffix}" + + def get(self, key: str) -> t.Optional[str]: + key = self._process_key(key) + return self.environment.get(key) + + +class EnvironmentFileSource(EnvironmentSource): + """ + A source which gets values from files at paths specified in environment variables. + """ + + def __init__(self, *, prefix: str = "", suffix: str = "_FILE", environment=os.environ): + super().__init__(prefix=prefix, suffix=suffix, environment=environment) + + def get(self, key: str) -> t.Optional[str]: + path = super().get(key) + try: + with open(path, "r") as file: + return file.read() + except FileNotFoundError: + return None + + +__all__ = ( + "Source", + "EnvironmentSource", + "EnvironmentFileSource", +) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..bd2d487 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,202 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "lazy-object-proxy" +version = "1.7.1" +description = "A fast and thorough lazy object proxy." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyparsing" +version = "3.0.8" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "fc3a54d78dbfc06ba40cd4e23c23344a9cabd011534182d0bc4387ff9cc190c1" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, + {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyparsing = [ + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, +] +pytest = [ + {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..367ffb6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "cfig" +version = "0.1.0" +description = "The ultimate configuration manager" +authors = ["Stefano Pigozzi "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.10" +lazy-object-proxy = "^1.7.1" + +[tool.poetry.dev-dependencies] +pytest = "^7.1.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"