mirror of
https://github.com/Steffo99/cfig.git
synced 2024-11-21 15:34:20 +00:00
💥 Do things
This commit is contained in:
parent
9e12b0f7a3
commit
f3c6de8d98
9 changed files with 721 additions and 196 deletions
|
@ -7,6 +7,7 @@
|
|||
<list>
|
||||
<option value="E306" />
|
||||
<option value="E711" />
|
||||
<option value="E501" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
This package provides a simple-to-use but featureful configuration manager for Python applications.
|
||||
This package provides a simple but powerful 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.
|
||||
|
@ -10,43 +10,46 @@ 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:
|
||||
Example
|
||||
=======
|
||||
|
||||
.. code-block:: python
|
||||
Ideally, a "config" module should be created, where the programmer defines the possible configuration options of their
|
||||
application::
|
||||
|
||||
# Import the cfig library
|
||||
import cfig
|
||||
|
||||
# Create a "Configurable" object
|
||||
config = cfig.Configurable()
|
||||
# Create a "Configuration" object
|
||||
config = cfig.Configuration()
|
||||
|
||||
# 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
|
||||
# Define configurable values by wrapping functions with the config decorators
|
||||
# Function name is used by default as the key of the variable to read
|
||||
@config.required()
|
||||
def SECRET_KEY(val: str) -> str:
|
||||
"Secret string used to manage tokens."
|
||||
return val
|
||||
|
||||
@config.value()
|
||||
@config.required()
|
||||
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
|
||||
# Values can be processed inside these functions
|
||||
return int(val)
|
||||
|
||||
@config.value()
|
||||
# If the val variable has an Optional annotation, cfig will mark that value as optional
|
||||
@config.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 heavy processing is done inside the function, it may be useful to define the configuration key manually
|
||||
@config.required(key="DATABASE_URI", doc="The URI of the database to be used.")
|
||||
def DATABASE_ENGINE(val: str):
|
||||
return sqlalchemy.create_engine(val)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If the configuration file is executed as main, handle the call and display a user-friendly CLI interface.
|
||||
# TODO: 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:
|
||||
Values can later be accessed by the program by importing the configuration file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -56,7 +59,36 @@ Configured values can later be accessed by importing the configuration file:
|
|||
# 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
|
||||
# Advanced objects can be loaded directly from the config
|
||||
Session = sessionmaker(bind=myconfig.DATABASE_ENGINE)
|
||||
|
||||
|
||||
Terminology
|
||||
===========
|
||||
|
||||
In this documentation, some terminology is used repeatedly:
|
||||
|
||||
Configuration key
|
||||
The name of a configuration value, usually in SCREAMING_SNAKE_CASE.
|
||||
For example, `PATH`, the name of the environment variable.
|
||||
|
||||
Value
|
||||
A single non-processed configuration value in :class:`str` form.
|
||||
For example, the raw string value of an environment variable.
|
||||
|
||||
Source
|
||||
A possible origin of configuration values, such as the environment, or a file.
|
||||
|
||||
Proxy
|
||||
An object used to lazily and transparently resolve and cache values.
|
||||
After resolving a value, it behaves in almost completely the same way as the object it cached.
|
||||
|
||||
Resolver
|
||||
A function taking in input a value originating from a source, and emitting in output its processed representation.
|
||||
For example, a resolver may be the :class:`int` class, which converts the value into an integer.
|
||||
|
||||
Configuration
|
||||
A collection of proxies.
|
||||
|
||||
"""
|
||||
|
||||
|
|
289
cfig/config.py
289
cfig/config.py
|
@ -1,30 +1,11 @@
|
|||
"""
|
||||
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 lazy_object_proxy
|
||||
import typing as t
|
||||
import logging
|
||||
import collections
|
||||
from . import errors
|
||||
from . import customtyping as ct
|
||||
from . import sources as s
|
||||
|
@ -34,95 +15,173 @@ log = logging.getLogger(__name__)
|
|||
|
||||
class Configuration:
|
||||
"""
|
||||
A group of configurable items.
|
||||
A collection of proxies with methods to easily define more.
|
||||
"""
|
||||
|
||||
DEFAULT_SOURCES = [
|
||||
s.EnvironmentSource(),
|
||||
s.EnvironmentFileSource(),
|
||||
]
|
||||
"""
|
||||
The sources used in :meth:`__init__` if no other source is specified.
|
||||
"""
|
||||
|
||||
class ProxyDict(collections.UserDict):
|
||||
"""
|
||||
An extended :class:`dict` with methods to perform some actions on the contained proxies.
|
||||
"""
|
||||
|
||||
def resolve(self):
|
||||
"""
|
||||
Resolve all values of the proxies inside this dictionary.
|
||||
"""
|
||||
|
||||
log.debug("Resolving and caching all values...")
|
||||
for item in self.values():
|
||||
log.debug(f"Resolving: {item!r}")
|
||||
_ = item.__wrapped__
|
||||
|
||||
def unresolve(self):
|
||||
"""
|
||||
Unresolve all values of the proxies inside this dictionary.
|
||||
"""
|
||||
|
||||
log.debug("Unresolving all cached values...")
|
||||
for item in self.values():
|
||||
log.debug(f"Unresolving: {item!r}")
|
||||
del item.__wrapped__
|
||||
|
||||
def __init__(self, *, sources: t.Optional[t.Collection[s.Source]] = None):
|
||||
"""
|
||||
Create a new :class:`Configuration`.
|
||||
"""
|
||||
|
||||
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.
|
||||
Collection of sources to use for values of this configuration.
|
||||
"""
|
||||
|
||||
self.items: dict[str, t.Any] = {}
|
||||
self.proxies: Configuration.ProxyDict = Configuration.ProxyDict()
|
||||
"""
|
||||
:class:`dict` mapping all keys registered to this object to their respective items.
|
||||
Dictionary mapping configuration keys belonging to this :class:`.Configuration` to the proxy caching their values.
|
||||
|
||||
Typed with :class:`typing.Any` so that proxies can be typed as the object they cache.
|
||||
"""
|
||||
|
||||
self.docs: dict[str, str] = {}
|
||||
"""
|
||||
:class:`dict` mapping all keys registered to this object to the description of what they should contain.
|
||||
Dictionary mapping configuration keys belonging to this :class:`.Configuration` to a description of what they should contain.
|
||||
"""
|
||||
|
||||
log.debug("Initialized successfully!")
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def _determine_configurable_key(self, f: ct.Configurable) -> str:
|
||||
def required(self, key: t.Optional[str] = None, doc: t.Optional[str] = None) -> t.Callable[[ct.ResolverRequired], ct.TYPE]:
|
||||
"""
|
||||
Determine the key of a configurable.
|
||||
Mark a function as a resolver for a required configuration value.
|
||||
|
||||
It is a decorator factory, and therefore should be used like so::
|
||||
|
||||
@config.required()
|
||||
def MY_KEY(val: str) -> str:
|
||||
return val
|
||||
|
||||
Key can be overridden manually with the ``key`` parameter.
|
||||
|
||||
Docstring can be overridden manually with the ``doc`` parameter.
|
||||
"""
|
||||
|
||||
def _decorator(configurable: ct.ResolverRequired) -> ct.TYPE:
|
||||
nonlocal key
|
||||
nonlocal doc
|
||||
|
||||
if not key:
|
||||
log.debug("Determining key...")
|
||||
key: str = self._find_resolver_key(configurable)
|
||||
log.debug(f"Key is: {key!r}")
|
||||
|
||||
log.debug("Creating required item...")
|
||||
item: ct.TYPE = self._create_proxy_required(key, configurable)
|
||||
log.debug("Item created successfully!")
|
||||
|
||||
log.debug("Registering item in the configuration...")
|
||||
self.register(key, item, doc or configurable.__doc__)
|
||||
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, key: t.Optional[str] = None, doc: t.Optional[str] = None) -> t.Callable[[ct.ResolverOptional], ct.TYPE]:
|
||||
"""
|
||||
Mark a function as a resolver for a required configuration value.
|
||||
|
||||
It is a decorator factory, and therefore should be used like so::
|
||||
|
||||
@config.optional()
|
||||
def MY_KEY(val: str) -> str:
|
||||
return val
|
||||
|
||||
Key can be overridden manually with the ``key`` parameter.
|
||||
|
||||
Docstring can be overridden manually with the ``doc`` parameter.
|
||||
"""
|
||||
|
||||
def _decorator(configurable: ct.ResolverOptional) -> ct.TYPE:
|
||||
nonlocal key
|
||||
nonlocal doc
|
||||
|
||||
if not key:
|
||||
log.debug("Determining key...")
|
||||
key: str = self._find_resolver_key(configurable)
|
||||
log.debug(f"Key is: {key!r}")
|
||||
|
||||
log.debug("Creating optional item...")
|
||||
item: ct.TYPE = self._create_proxy_optional(key, configurable)
|
||||
log.debug("Item created successfully!")
|
||||
|
||||
log.debug("Registering item in the configuration...")
|
||||
self.register(key, item, doc or configurable.__doc__)
|
||||
log.debug("Registered successfully!")
|
||||
|
||||
# Return the created item, so it will take the place of the decorated function
|
||||
return item
|
||||
|
||||
return _decorator
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def _find_resolver_key(self, resolver: ct.Resolver) -> str:
|
||||
"""
|
||||
Find the key of a resolver by accessing its ``__name__``.
|
||||
|
||||
:raises .errors.UnknownResolverNameError: If the key could not be determined, for example if the resolver had no ``__name__``.
|
||||
"""
|
||||
|
||||
try:
|
||||
return f.__name__
|
||||
return resolver.__name__
|
||||
except AttributeError:
|
||||
log.error(f"Could not determine key of: {f!r}")
|
||||
raise errors.UnknownKeyError()
|
||||
log.error(f"Could not determine key of: {resolver!r}")
|
||||
raise errors.UnknownResolverNameError()
|
||||
|
||||
def required(self) -> t.Callable[[ct.ConfigurableRequired], ct.TYPE]:
|
||||
def _retrieve_value_optional(self, key: str) -> t.Optional[str]:
|
||||
"""
|
||||
Create the decorator to convert the decorated function into a required configurable.
|
||||
Try to retrieve a value from all :attr:`.sources` of this :class:`.Configuration`, returning :data:`None` if the value is not found anywhere.
|
||||
"""
|
||||
|
||||
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}")
|
||||
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
|
||||
|
||||
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, configurable.__doc__)
|
||||
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]:
|
||||
def _create_proxy_optional(self, key: str, resolver: ct.ResolverOptional) -> lazy_object_proxy.Proxy:
|
||||
"""
|
||||
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, configurable.__doc__)
|
||||
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.
|
||||
Create, from a resolver, a proxy tolerating non-specified values.
|
||||
"""
|
||||
|
||||
@lazy_object_proxy.Proxy
|
||||
|
@ -135,16 +194,28 @@ class Configuration:
|
|||
log.debug(f"Not running user-defined configurable function since value is {val!r}.")
|
||||
else:
|
||||
log.debug("Running user-defined configurable function...")
|
||||
val = f(val)
|
||||
val = resolver(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:
|
||||
def _retrieve_value_required(self, key: str) -> str:
|
||||
"""
|
||||
Create a new required item.
|
||||
Try to retrieve a value from all :attr:`.sources` of this Configuration, raising :exc:`errors.MissingValueError` if the value is not found anywhere.
|
||||
|
||||
:raises .errors.MissingValueError: If the value with the given key is not found in any source.
|
||||
"""
|
||||
|
||||
if value := self._retrieve_value_optional(key):
|
||||
return value
|
||||
else:
|
||||
raise errors.MissingValueError(key)
|
||||
|
||||
def _create_proxy_required(self, key: str, f: ct.ResolverRequired) -> lazy_object_proxy.Proxy:
|
||||
"""
|
||||
Create, from a resolver, a proxy intolerant about non-specified values.
|
||||
"""
|
||||
|
||||
@lazy_object_proxy.Proxy
|
||||
|
@ -161,62 +232,26 @@ class Configuration:
|
|||
|
||||
return _decorated
|
||||
|
||||
def _retrieve_value_optional(self, key: str) -> t.Optional[str]:
|
||||
def register(self, key, proxy, doc):
|
||||
"""
|
||||
Try to retrieve a value from all :attr:`.sources` of this Configuration.
|
||||
Register a new proxy in this Configuration.
|
||||
|
||||
:param key: The configuration key to register the proxy to.
|
||||
:param proxy: The proxy to register in :attr:`.proxies`.
|
||||
:param doc: The docstring to register in :attr:`.docs`.
|
||||
:raises .errors.DuplicateProxyNameError` if the key already exists in either :attr:`.proxies` or :attr:`.docs`.
|
||||
"""
|
||||
|
||||
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.MissingValueError(key)
|
||||
|
||||
def _register_item(self, key, item, doc):
|
||||
"""
|
||||
Register an item in this Configuration.
|
||||
"""
|
||||
|
||||
if key in self.items:
|
||||
raise errors.DuplicateError(key)
|
||||
if key in self.proxies:
|
||||
raise errors.DuplicateProxyNameError(key)
|
||||
if key in self.docs:
|
||||
raise errors.DuplicateError(key)
|
||||
raise errors.DuplicateProxyNameError(key)
|
||||
|
||||
self.items[key] = item
|
||||
log.debug(f"Registering proxy {proxy!r} in {key!r}")
|
||||
self.proxies[key] = proxy
|
||||
log.debug(f"Registering doc {doc!r} in {key!r}")
|
||||
self.docs[key] = doc
|
||||
|
||||
def fetch_all(self, clear: bool = True):
|
||||
"""
|
||||
Fetch all configuration values.
|
||||
|
||||
If values were fetched earlier, and the ``clear`` parameter is :data:`True`,
|
||||
this will clear the cache and re-fetch them, possibly changing some items.
|
||||
"""
|
||||
|
||||
if clear:
|
||||
log.debug("Clearing the cached items...")
|
||||
for item in self.items.values():
|
||||
log.debug(f"Clearing: {item!r}")
|
||||
del item.__wrapped__
|
||||
|
||||
log.debug("Fetching items...")
|
||||
for item in self.items.values():
|
||||
log.debug(f"Fetching: {item!r}")
|
||||
_ = item.__wrapped__
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Configuration",
|
||||
|
|
|
@ -3,7 +3,7 @@ import typing as t
|
|||
TYPE = t.TypeVar("TYPE")
|
||||
|
||||
|
||||
class Configurable(t.Protocol):
|
||||
class Resolver(t.Protocol):
|
||||
__name__: str
|
||||
__doc__: str
|
||||
|
||||
|
@ -11,19 +11,19 @@ class Configurable(t.Protocol):
|
|||
...
|
||||
|
||||
|
||||
class ConfigurableRequired(Configurable):
|
||||
class ResolverRequired(Resolver):
|
||||
def __call__(self, val: str) -> TYPE:
|
||||
...
|
||||
|
||||
|
||||
class ConfigurableOptional(Configurable):
|
||||
class ResolverOptional(Resolver):
|
||||
def __call__(self, val: t.Optional[str]) -> TYPE:
|
||||
...
|
||||
|
||||
|
||||
__all__ = (
|
||||
"TYPE",
|
||||
"Configurable",
|
||||
"ConfigurableRequired",
|
||||
"ConfigurableOptional",
|
||||
"Resolver",
|
||||
"ResolverRequired",
|
||||
"ResolverOptional",
|
||||
)
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
class DefinitionError(Exception):
|
||||
"""
|
||||
An error is present in the definition of a :class:`cfig.Configurable` object.
|
||||
An error is present in the definition of a :class:`cfig.Configuration`.
|
||||
"""
|
||||
|
||||
|
||||
class UnknownKeyError(DefinitionError):
|
||||
class UnknownResolverNameError(DefinitionError):
|
||||
"""
|
||||
It was not possible to get the name of the wrapped function.
|
||||
It was not possible to get the name of the resolver.
|
||||
|
||||
Perhaps a call to :func:`functools.wraps` is missing?
|
||||
"""
|
||||
|
||||
|
||||
class RegistrationError(DefinitionError):
|
||||
class ProxyRegistrationError(DefinitionError):
|
||||
"""
|
||||
An error occurred during the proxy registration step.
|
||||
"""
|
||||
|
||||
|
||||
class DuplicateError(RegistrationError):
|
||||
class DuplicateProxyNameError(ProxyRegistrationError):
|
||||
"""
|
||||
Another proxy with the same name is already registered.
|
||||
"""
|
||||
|
@ -38,9 +38,9 @@ class MissingValueError(ConfigurationError):
|
|||
|
||||
__all__ = (
|
||||
"DefinitionError",
|
||||
"UnknownKeyError",
|
||||
"RegistrationError",
|
||||
"DuplicateError",
|
||||
"UnknownResolverNameError",
|
||||
"ProxyRegistrationError",
|
||||
"DuplicateProxyNameError",
|
||||
"ConfigurationError",
|
||||
"MissingValueError",
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import typing as t
|
||||
import abc
|
||||
import os
|
||||
import configparser
|
||||
|
||||
|
||||
class Source(metaclass=abc.ABCMeta):
|
||||
|
@ -10,7 +11,9 @@ class Source(metaclass=abc.ABCMeta):
|
|||
|
||||
@abc.abstractmethod
|
||||
def get(self, key: str) -> t.Optional[str]:
|
||||
raise NotImplementedError()
|
||||
"""
|
||||
Get the value with the given key from the source
|
||||
"""
|
||||
|
||||
|
||||
class EnvironmentSource(Source):
|
||||
|
|
|
@ -6,13 +6,17 @@ import lazy_object_proxy
|
|||
|
||||
class TestConfig:
|
||||
def test_creation(self):
|
||||
self.config = cfig.Configuration()
|
||||
config = cfig.Configuration()
|
||||
|
||||
assert isinstance(self.config, cfig.Configuration)
|
||||
assert self.config.sources == cfig.Configuration.DEFAULT_SOURCES
|
||||
assert isinstance(config, cfig.Configuration)
|
||||
assert config.sources == cfig.Configuration.DEFAULT_SOURCES
|
||||
|
||||
def test_required(self):
|
||||
@self.config.required()
|
||||
@pytest.fixture(scope="function")
|
||||
def basic_config(self):
|
||||
yield cfig.Configuration()
|
||||
|
||||
def test_registration_required(self, basic_config):
|
||||
@basic_config.required()
|
||||
def FIRST_NUMBER(val: str) -> int:
|
||||
"""The first number to sum."""
|
||||
return int(val)
|
||||
|
@ -20,14 +24,11 @@ class TestConfig:
|
|||
assert isinstance(FIRST_NUMBER, lazy_object_proxy.Proxy)
|
||||
assert callable(FIRST_NUMBER.__factory__)
|
||||
assert not FIRST_NUMBER.__resolved__
|
||||
assert basic_config.proxies["FIRST_NUMBER"] is FIRST_NUMBER
|
||||
assert basic_config.docs["FIRST_NUMBER"] == """The first number to sum."""
|
||||
|
||||
assert self.config.items["FIRST_NUMBER"] is FIRST_NUMBER
|
||||
assert self.config.docs["FIRST_NUMBER"] == """The first number to sum."""
|
||||
|
||||
self.FIRST_NUMBER = FIRST_NUMBER
|
||||
|
||||
def test_optional(self):
|
||||
@self.config.optional()
|
||||
def test_registration_optional(self, basic_config):
|
||||
@basic_config.optional()
|
||||
def SECOND_NUMBER(val: str) -> int:
|
||||
"""The second number to sum."""
|
||||
return int(val)
|
||||
|
@ -35,13 +36,24 @@ class TestConfig:
|
|||
assert isinstance(SECOND_NUMBER, lazy_object_proxy.Proxy)
|
||||
assert callable(SECOND_NUMBER.__factory__)
|
||||
assert not SECOND_NUMBER.__resolved__
|
||||
assert basic_config.proxies["SECOND_NUMBER"] is SECOND_NUMBER
|
||||
assert basic_config.docs["SECOND_NUMBER"] == """The second number to sum."""
|
||||
|
||||
assert self.config.items["SECOND_NUMBER"] is SECOND_NUMBER
|
||||
assert self.config.docs["SECOND_NUMBER"] == """The second number to sum."""
|
||||
@pytest.fixture(scope="function")
|
||||
def numbers_config(self, basic_config):
|
||||
@basic_config.required()
|
||||
def FIRST_NUMBER(val: str) -> int:
|
||||
"""The first number to sum."""
|
||||
return int(val)
|
||||
|
||||
self.SECOND_NUMBER = SECOND_NUMBER
|
||||
@basic_config.optional()
|
||||
def SECOND_NUMBER(val: str) -> int:
|
||||
"""The second number to sum."""
|
||||
return int(val)
|
||||
|
||||
def test_fetch_missing(self, monkeypatch):
|
||||
yield basic_config
|
||||
|
||||
def test_resolve_missing(self, numbers_config, monkeypatch):
|
||||
monkeypatch.setenv("FIRST_NUMBER", "")
|
||||
monkeypatch.setenv("SECOND_NUMBER", "")
|
||||
|
||||
|
@ -49,37 +61,95 @@ class TestConfig:
|
|||
assert not os.environ.get("SECOND_NUMBER")
|
||||
|
||||
with pytest.raises(cfig.MissingValueError):
|
||||
self.config.fetch_all()
|
||||
numbers_config.proxies.resolve()
|
||||
|
||||
def test_fetch_required(self, monkeypatch):
|
||||
def test_resolve_required(self, numbers_config, monkeypatch):
|
||||
monkeypatch.setenv("FIRST_NUMBER", "1")
|
||||
monkeypatch.setenv("SECOND_NUMBER", "")
|
||||
|
||||
assert os.environ.get("FIRST_NUMBER") == "1"
|
||||
assert not os.environ.get("SECOND_NUMBER")
|
||||
|
||||
self.config.fetch_all()
|
||||
first_number = numbers_config.proxies["FIRST_NUMBER"]
|
||||
second_number = numbers_config.proxies["SECOND_NUMBER"]
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
assert self.FIRST_NUMBER.__resolved__
|
||||
assert self.FIRST_NUMBER == 1
|
||||
# noinspection PyUnresolvedReferences
|
||||
assert self.SECOND_NUMBER.__resolved__
|
||||
assert self.SECOND_NUMBER == None
|
||||
assert self.SECOND_NUMBER is not None
|
||||
assert not first_number.__resolved__
|
||||
assert not second_number.__resolved__
|
||||
|
||||
def test_fetch_optional(self, monkeypatch):
|
||||
numbers_config.proxies.resolve()
|
||||
|
||||
assert first_number.__resolved__
|
||||
assert first_number == 1
|
||||
|
||||
assert second_number.__resolved__
|
||||
assert second_number == None
|
||||
assert second_number is not None
|
||||
|
||||
def test_resolve_optional(self, numbers_config, monkeypatch):
|
||||
monkeypatch.setenv("FIRST_NUMBER", "1")
|
||||
monkeypatch.setenv("SECOND_NUMBER", "2")
|
||||
|
||||
assert os.environ.get("FIRST_NUMBER") == "1"
|
||||
assert os.environ.get("FIRST_NUMBER") == "2"
|
||||
assert os.environ.get("SECOND_NUMBER") == "2"
|
||||
|
||||
self.config.fetch_all()
|
||||
first_number = numbers_config.proxies["FIRST_NUMBER"]
|
||||
second_number = numbers_config.proxies["SECOND_NUMBER"]
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
assert self.FIRST_NUMBER.__resolved__
|
||||
assert self.FIRST_NUMBER == 1
|
||||
# noinspection PyUnresolvedReferences
|
||||
assert self.SECOND_NUMBER.__resolved__
|
||||
assert self.SECOND_NUMBER == 2
|
||||
assert not first_number.__resolved__
|
||||
assert not second_number.__resolved__
|
||||
|
||||
numbers_config.proxies.resolve()
|
||||
|
||||
assert first_number.__resolved__
|
||||
assert first_number == 1
|
||||
|
||||
assert second_number.__resolved__
|
||||
assert second_number == 2
|
||||
|
||||
def test_resolve_unresolve(self, numbers_config, monkeypatch):
|
||||
monkeypatch.setenv("FIRST_NUMBER", "1")
|
||||
monkeypatch.setenv("SECOND_NUMBER", "2")
|
||||
|
||||
assert os.environ.get("FIRST_NUMBER") == "1"
|
||||
assert os.environ.get("SECOND_NUMBER") == "2"
|
||||
|
||||
first_number = numbers_config.proxies["FIRST_NUMBER"]
|
||||
second_number = numbers_config.proxies["SECOND_NUMBER"]
|
||||
|
||||
assert not first_number.__resolved__
|
||||
assert not second_number.__resolved__
|
||||
|
||||
numbers_config.proxies.resolve()
|
||||
|
||||
assert first_number.__resolved__
|
||||
assert first_number == 1
|
||||
|
||||
assert second_number.__resolved__
|
||||
assert second_number == 2
|
||||
|
||||
monkeypatch.setenv("FIRST_NUMBER", "3")
|
||||
monkeypatch.setenv("SECOND_NUMBER", "4")
|
||||
|
||||
assert os.environ.get("FIRST_NUMBER") == "3"
|
||||
assert os.environ.get("SECOND_NUMBER") == "4"
|
||||
|
||||
numbers_config.proxies.resolve()
|
||||
|
||||
assert first_number.__resolved__
|
||||
assert first_number == 1
|
||||
|
||||
assert second_number.__resolved__
|
||||
assert second_number == 2
|
||||
|
||||
numbers_config.proxies.unresolve()
|
||||
|
||||
assert not first_number.__resolved__
|
||||
assert not second_number.__resolved__
|
||||
|
||||
numbers_config.proxies.resolve()
|
||||
|
||||
assert first_number.__resolved__
|
||||
assert first_number == 3
|
||||
|
||||
assert second_number.__resolved__
|
||||
assert second_number == 4
|
||||
|
|
384
poetry.lock
generated
384
poetry.lock
generated
|
@ -1,3 +1,11 @@
|
|||
[[package]]
|
||||
name = "alabaster"
|
||||
version = "0.7.12"
|
||||
description = "A configurable sidebar-enabled Sphinx theme"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "atomicwrites"
|
||||
version = "1.4.0"
|
||||
|
@ -20,6 +28,36 @@ 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 = "babel"
|
||||
version = "2.9.1"
|
||||
description = "Internationalization utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
pytz = ">=2015.7"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2021.10.8"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.0.12"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
|
@ -28,6 +66,30 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.17.1"
|
||||
description = "Docutils -- Python Documentation Utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.3"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "1.3.0"
|
||||
description = "Getting image size from png/jpeg/jpeg2000/gif file"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
|
@ -36,6 +98,20 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.1"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-object-proxy"
|
||||
version = "1.7.1"
|
||||
|
@ -44,6 +120,14 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.1"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.3"
|
||||
|
@ -75,6 +159,14 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.11.2"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.8"
|
||||
|
@ -107,6 +199,157 @@ tomli = ">=1.0.0"
|
|||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2022.1"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.27.1"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||
|
||||
[[package]]
|
||||
name = "snowballstemmer"
|
||||
version = "2.2.0"
|
||||
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "4.5.0"
|
||||
description = "Python documentation generator"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
alabaster = ">=0.7,<0.8"
|
||||
babel = ">=1.3"
|
||||
colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
|
||||
docutils = ">=0.14,<0.18"
|
||||
imagesize = "*"
|
||||
Jinja2 = ">=2.3"
|
||||
packaging = "*"
|
||||
Pygments = ">=2.0"
|
||||
requests = ">=2.5.0"
|
||||
snowballstemmer = ">=1.1"
|
||||
sphinxcontrib-applehelp = "*"
|
||||
sphinxcontrib-devhelp = "*"
|
||||
sphinxcontrib-htmlhelp = ">=2.0.0"
|
||||
sphinxcontrib-jsmath = "*"
|
||||
sphinxcontrib-qthelp = "*"
|
||||
sphinxcontrib-serializinghtml = ">=1.1.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinxcontrib-websupport"]
|
||||
lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"]
|
||||
test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-rtd-theme"
|
||||
version = "1.0.0"
|
||||
description = "Read the Docs theme for Sphinx"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
docutils = "<0.18"
|
||||
sphinx = ">=1.6"
|
||||
|
||||
[package.extras]
|
||||
dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-applehelp"
|
||||
version = "1.0.2"
|
||||
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-devhelp"
|
||||
version = "1.0.2"
|
||||
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-htmlhelp"
|
||||
version = "2.0.0"
|
||||
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest", "html5lib"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-jsmath"
|
||||
version = "1.0.1"
|
||||
description = "A sphinx extension which renders display math in HTML via JavaScript"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "flake8", "mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-qthelp"
|
||||
version = "1.0.3"
|
||||
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-serializinghtml"
|
||||
version = "1.1.5"
|
||||
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
|
@ -115,12 +358,29 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.9"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "fc3a54d78dbfc06ba40cd4e23c23344a9cabd011534182d0bc4387ff9cc190c1"
|
||||
content-hash = "e46694c42f589afe3d3c0a8fc3b178d31f9d66970f65dfbd56eecc521a07800e"
|
||||
|
||||
[metadata.files]
|
||||
alabaster = [
|
||||
{file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
|
||||
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
|
||||
]
|
||||
atomicwrites = [
|
||||
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
|
||||
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
||||
|
@ -129,14 +389,42 @@ attrs = [
|
|||
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
||||
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||
]
|
||||
babel = [
|
||||
{file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
|
||||
{file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
||||
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
docutils = [
|
||||
{file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
|
||||
{file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
]
|
||||
imagesize = [
|
||||
{file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"},
|
||||
{file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"},
|
||||
{file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"},
|
||||
]
|
||||
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"},
|
||||
|
@ -176,6 +464,48 @@ lazy-object-proxy = [
|
|||
{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"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
|
||||
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
|
||||
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
|
||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
|
||||
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
|
@ -188,6 +518,10 @@ py = [
|
|||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pygments = [
|
||||
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
|
||||
{file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"},
|
||||
{file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"},
|
||||
|
@ -196,7 +530,55 @@ pytest = [
|
|||
{file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"},
|
||||
{file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"},
|
||||
]
|
||||
pytz = [
|
||||
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
|
||||
{file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
||||
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
|
||||
]
|
||||
snowballstemmer = [
|
||||
{file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
|
||||
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
|
||||
]
|
||||
sphinx = [
|
||||
{file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"},
|
||||
{file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"},
|
||||
]
|
||||
sphinx-rtd-theme = [
|
||||
{file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"},
|
||||
{file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"},
|
||||
]
|
||||
sphinxcontrib-applehelp = [
|
||||
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
|
||||
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
|
||||
]
|
||||
sphinxcontrib-devhelp = [
|
||||
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
|
||||
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
|
||||
]
|
||||
sphinxcontrib-htmlhelp = [
|
||||
{file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
|
||||
{file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
|
||||
]
|
||||
sphinxcontrib-jsmath = [
|
||||
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
|
||||
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
|
||||
]
|
||||
sphinxcontrib-qthelp = [
|
||||
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
|
||||
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
|
||||
]
|
||||
sphinxcontrib-serializinghtml = [
|
||||
{file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
|
||||
{file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
|
||||
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
|
||||
]
|
||||
|
|
|
@ -11,6 +11,8 @@ lazy-object-proxy = "^1.7.1"
|
|||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.1"
|
||||
Sphinx = "^4.5.0"
|
||||
sphinx-rtd-theme = "^1.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
|
Loading…
Reference in a new issue