mirror of
https://github.com/Steffo99/cfig.git
synced 2024-12-03 13:14:19 +00:00
🎉 First commit
This commit is contained in:
commit
106b27e730
14 changed files with 896 additions and 0 deletions
238
.gitignore
vendored
Normal file
238
.gitignore
vendored
Normal file
|
@ -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
|
3
.idea/.gitignore
vendored
Normal file
3
.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
12
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="E306" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
.idea/misc.xml
Normal file
6
.idea/misc.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="Python 3.10 (cfig)" project-jdk-type="Python SDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/cfig.iml" filepath="$PROJECT_DIR$/cfig.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
12
cfig.iml
Normal file
12
cfig.iml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/cfig" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/cfig/tests" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
69
cfig/__init__.py
Normal file
69
cfig/__init__.py
Normal file
|
@ -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",
|
||||
)
|
194
cfig/config.py
Normal file
194
cfig/config.py
Normal file
|
@ -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__
|
27
cfig/customtyping.py
Normal file
27
cfig/customtyping.py
Normal file
|
@ -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",
|
||||
)
|
30
cfig/errors.py
Normal file
30
cfig/errors.py
Normal file
|
@ -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.
|
||||
"""
|
72
cfig/sources.py
Normal file
72
cfig/sources.py
Normal file
|
@ -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",
|
||||
)
|
202
poetry.lock
generated
Normal file
202
poetry.lock
generated
Normal file
|
@ -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"},
|
||||
]
|
17
pyproject.toml
Normal file
17
pyproject.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[tool.poetry]
|
||||
name = "cfig"
|
||||
version = "0.1.0"
|
||||
description = "The ultimate configuration manager"
|
||||
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||
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"
|
Loading…
Reference in a new issue