1
Fork 0
mirror of https://github.com/Steffo99/cfig.git synced 2024-11-21 15:34:20 +00:00

💥 Do things

This commit is contained in:
Steffo 2022-04-16 05:17:21 +02:00
parent 9e12b0f7a3
commit f3c6de8d98
Signed by: steffo
GPG key ID: 6965406171929D01
9 changed files with 721 additions and 196 deletions

View file

@ -7,6 +7,7 @@
<list>
<option value="E306" />
<option value="E711" />
<option value="E501" />
</list>
</option>
</inspection_tool>

View file

@ -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.
"""

View file

@ -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",

View file

@ -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",
)

View file

@ -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",
)

View file

@ -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):

View file

@ -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
View file

@ -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"},
]

View file

@ -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"]