diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 50b49b7..1a2217a 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -7,6 +7,7 @@ diff --git a/cfig/__init__.py b/cfig/__init__.py index edaf9a5..196487e 100644 --- a/cfig/__init__.py +++ b/cfig/__init__.py @@ -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. """ diff --git a/cfig/config.py b/cfig/config.py index 077072a..30bb756 100644 --- a/cfig/config.py +++ b/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", diff --git a/cfig/customtyping.py b/cfig/customtyping.py index 6a694b0..7bc64a3 100644 --- a/cfig/customtyping.py +++ b/cfig/customtyping.py @@ -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", ) diff --git a/cfig/errors.py b/cfig/errors.py index 8b25d3b..e47d0d3 100644 --- a/cfig/errors.py +++ b/cfig/errors.py @@ -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", ) diff --git a/cfig/sources.py b/cfig/sources.py index cfc9438..1332577 100644 --- a/cfig/sources.py +++ b/cfig/sources.py @@ -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): diff --git a/cfig/tests/test_all.py b/cfig/tests/test_all.py index b1b5b7c..4280851 100644 --- a/cfig/tests/test_all.py +++ b/cfig/tests/test_all.py @@ -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 diff --git a/poetry.lock b/poetry.lock index bd2d487..8140e54 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, +] diff --git a/pyproject.toml b/pyproject.toml index 367ffb6..f52793d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"]