From 1153c5ff1756b68a14e46e1668a272be720a609b Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:24:55 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20Feature:=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D=E7=BD=AE=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=20`pydantic-settings`=20(#2521)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: uy/sun Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- nonebot/__init__.py | 5 +- nonebot/config.py | 335 ++++++++++++++++++++++++++++++++++--------- poetry.lock | 222 ++++++++++++++-------------- pyproject.toml | 3 +- tests/.env.example | 17 +++ tests/test_config.py | 118 +++++++++++++++ 6 files changed, 520 insertions(+), 180 deletions(-) create mode 100644 tests/.env.example create mode 100644 tests/test_config.py diff --git a/nonebot/__init__.py b/nonebot/__init__.py index a232a655..55366625 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -47,11 +47,10 @@ from importlib.metadata import version from typing import Any, Dict, Type, Union, TypeVar, Optional, overload import loguru -from pydantic.env_settings import DotenvType -from nonebot.config import Env, Config from nonebot.log import logger as logger from nonebot.adapters import Bot, Adapter +from nonebot.config import DOTENV_TYPE, Env, Config from nonebot.utils import escape_tag, resolve_dot_notation from nonebot.drivers import Driver, ASGIMixin, combine_driver @@ -273,7 +272,7 @@ def _log_patcher(record: "loguru.Record"): ) -def init(*, _env_file: Optional[DotenvType] = None, **kwargs: Any) -> None: +def init(*, _env_file: Optional[DOTENV_TYPE] = None, **kwargs: Any) -> None: """初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。 NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。 diff --git a/nonebot/config.py b/nonebot/config.py index 89bdc55a..bd6f4c8c 100644 --- a/nonebot/config.py +++ b/nonebot/config.py @@ -12,76 +12,257 @@ FrontMatter: """ import os +import abc +from pathlib import Path from datetime import timedelta from ipaddress import IPv4Address -from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional - -from pydantic.utils import deep_update -from pydantic.fields import Undefined, UndefinedType -from pydantic import Extra, Field, BaseSettings, IPvAnyAddress -from pydantic.env_settings import ( - DotenvType, - SettingsError, - EnvSettingsSource, - InitSettingsSource, - SettingsSourceCallable, +from typing_extensions import TypeAlias, get_origin +from typing import ( + TYPE_CHECKING, + Any, + Set, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + ClassVar, + Optional, ) +from dotenv import dotenv_values +from pydantic.typing import is_union +from pydantic.utils import deep_update +from pydantic.fields import Undefined, ModelField, UndefinedType +from pydantic import Extra, Field, BaseModel, BaseConfig, JsonWrapper, IPvAnyAddress + from nonebot.log import logger +DOTENV_TYPE: TypeAlias = Union[ + Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...] +] -class CustomEnvSettings(EnvSettingsSource): - def __call__(self, settings: BaseSettings) -> Dict[str, Any]: +ENV_FILE_SENTINEL = Path("") + + +class SettingsError(ValueError): + ... + + +class BaseSettingsSource(abc.ABC): + def __init__(self, settings_cls: Type["BaseSettings"]) -> None: + self.settings_cls = settings_cls + + @property + def config(self) -> Type["SettingsConfig"]: + return self.settings_cls.__config__ + + @abc.abstractmethod + def __call__(self) -> Dict[str, Any]: + raise NotImplementedError + + +class InitSettingsSource(BaseSettingsSource): + __slots__ = ("init_kwargs",) + + def __init__( + self, settings_cls: Type["BaseSettings"], init_kwargs: Dict[str, Any] + ) -> None: + self.init_kwargs = init_kwargs + super().__init__(settings_cls) + + def __call__(self) -> Dict[str, Any]: + return self.init_kwargs + + def __repr__(self) -> str: + return f"InitSettingsSource(init_kwargs={self.init_kwargs!r})" + + +class DotEnvSettingsSource(BaseSettingsSource): + def __init__( + self, + settings_cls: Type["BaseSettings"], + env_file: Optional[DOTENV_TYPE] = ENV_FILE_SENTINEL, + env_file_encoding: Optional[str] = None, + case_sensitive: Optional[bool] = None, + env_nested_delimiter: Optional[str] = None, + ) -> None: + super().__init__(settings_cls) + self.env_file = ( + env_file if env_file is not ENV_FILE_SENTINEL else self.config.env_file + ) + self.env_file_encoding = ( + env_file_encoding + if env_file_encoding is not None + else self.config.env_file_encoding + ) + self.case_sensitive = ( + case_sensitive if case_sensitive is not None else self.config.case_sensitive + ) + self.env_nested_delimiter = ( + env_nested_delimiter + if env_nested_delimiter is not None + else self.config.env_nested_delimiter + ) + + def _apply_case_sensitive(self, var_name: str) -> str: + return var_name if self.case_sensitive else var_name.lower() + + def _field_is_complex(self, field: ModelField) -> Tuple[bool, bool]: + try: + if isinstance(field.annotation, type) and issubclass( + field.annotation, JsonWrapper + ): + return True, False + except TypeError: + pass + + if field.is_complex(): + return True, False + elif ( + is_union(get_origin(field.type_)) + and field.sub_fields + and any(f.is_complex() for f in field.sub_fields) + ): + return True, True + return False, False + + def _parse_env_vars( + self, env_vars: Mapping[str, Optional[str]] + ) -> Dict[str, Optional[str]]: + return { + self._apply_case_sensitive(key): value for key, value in env_vars.items() + } + + def _read_env_file(self, file_path: Path) -> Dict[str, Optional[str]]: + file_vars = dotenv_values(file_path, encoding=self.env_file_encoding) + return self._parse_env_vars(file_vars) + + def _read_env_files(self) -> Dict[str, Optional[str]]: + env_files = self.env_file + if env_files is None: + return {} + + if isinstance(env_files, (str, os.PathLike)): + env_files = [env_files] + + dotenv_vars: Dict[str, Optional[str]] = {} + for env_file in env_files: + env_path = Path(env_file).expanduser() + if env_path.is_file(): + dotenv_vars.update(self._read_env_file(env_path)) + return dotenv_vars + + def _next_field( + self, field: Optional[ModelField], key: str + ) -> Optional[ModelField]: + if not field or is_union(get_origin(field.annotation)): + return None + elif ( + field.annotation + and isinstance( + (fields := getattr(field.annotation, "__fields__", None)), dict + ) + and (field := fields.get(key)) + ): + return field + return None + + def _explode_env_vars( + self, + field: ModelField, + env_vars: Dict[str, Optional[str]], + env_file_vars: Dict[str, Optional[str]], + ) -> Dict[str, Any]: + if self.env_nested_delimiter is None: + return {} + + prefix = f"{field.name}{self.env_nested_delimiter}" + result: Dict[str, Any] = {} + for env_name, env_val in env_vars.items(): + if not env_name.startswith(prefix): + continue + + # delete from file vars when used + if env_name in env_file_vars: + del env_file_vars[env_name] + + _, *keys, last_key = env_name.split(self.env_nested_delimiter) + env_var = result + target_field: Optional[ModelField] = field + for key in keys: + target_field = self._next_field(target_field, key) + env_var = env_var.setdefault(key, {}) + + target_field = self._next_field(target_field, last_key) + if target_field and env_val: + is_complex, allow_parse_failure = self._field_is_complex(target_field) + if is_complex: + try: + env_val = self.settings_cls.__config__.json_loads(env_val) + except ValueError as e: + if not allow_parse_failure: + raise SettingsError( + f'error parsing env var "{env_name}"' + ) from e + + env_var[last_key] = env_val + + return result + + def __call__(self) -> Dict[str, Any]: """从环境变量和 dotenv 配置文件中读取配置项。""" d: Dict[str, Any] = {} - if settings.__config__.case_sensitive: - env_vars: Mapping[str, Optional[str]] = os.environ # pragma: no cover - else: - env_vars = {k.lower(): v for k, v in os.environ.items()} - - env_file_vars = self._read_env_files(settings.__config__.case_sensitive) + env_vars = self._parse_env_vars(os.environ) + env_file_vars = self._read_env_files() env_vars = {**env_file_vars, **env_vars} - for field in settings.__fields__.values(): - env_val: Union[str, None, UndefinedType] = Undefined - for env_name in field.field_info.extra["env_names"]: - env_val = env_vars.get(env_name, Undefined) - if env_name in env_file_vars: - del env_file_vars[env_name] - if env_val is not Undefined: - break + for field in self.settings_cls.__fields__.values(): + field_key = field.name + env_name = self._apply_case_sensitive(field_key) + # try get values from env vars + env_val = env_vars.get(env_name, Undefined) + # delete from file vars when used + if env_name in env_file_vars: + del env_file_vars[env_name] - is_complex, allow_parse_failure = self.field_is_complex(field) + is_complex, allow_parse_failure = self._field_is_complex(field) if is_complex: if isinstance(env_val, UndefinedType): # field is complex but no value found so far, try explode_env_vars - if env_val_built := self.explode_env_vars(field, env_vars): - d[field.alias] = env_val_built + if env_val_built := self._explode_env_vars( + field, env_vars, env_file_vars + ): + d[field_key] = env_val_built elif env_val is None: - d[field.alias] = env_val + d[field_key] = env_val else: # field is complex and there's a value # decode that as JSON, then add explode_env_vars try: - env_val = settings.__config__.parse_env_var(field.name, env_val) + env_val = self.settings_cls.__config__.json_loads(env_val) except ValueError as e: if not allow_parse_failure: raise SettingsError( - f'error parsing env var "{env_name}"' # type: ignore + f'error parsing env var "{env_name}"' ) from e if isinstance(env_val, dict): - d[field.alias] = deep_update( - env_val, self.explode_env_vars(field, env_vars) + # field value is a dict + # try explode_env_vars to find more sub-values + d[field_key] = deep_update( + env_val, + self._explode_env_vars(field, env_vars, env_file_vars), ) else: - d[field.alias] = env_val + d[field_key] = env_val elif not isinstance(env_val, UndefinedType): # simplest case, field is not complex # we only need to add the value if it was found - d[field.alias] = env_val + d[field_key] = env_val # remain user custom config for env_name in env_file_vars: @@ -89,7 +270,7 @@ class CustomEnvSettings(EnvSettingsSource): if env_val and (val_striped := env_val.strip()): # there's a value, decode that as JSON try: - env_val = settings.__config__.parse_env_var(env_name, val_striped) + env_val = self.settings_cls.__config__.json_loads(val_striped) except ValueError: logger.trace( "Error while parsing JSON for " @@ -113,38 +294,58 @@ class CustomEnvSettings(EnvSettingsSource): return d -class BaseConfig(BaseSettings): +class SettingsConfig(BaseConfig): + extra = Extra.allow + env_file: Optional[DOTENV_TYPE] = None + env_file_encoding: str = "utf-8" + case_sensitive: bool = False + env_nested_delimiter: Optional[str] = "__" + + +class BaseSettings(BaseModel): if TYPE_CHECKING: + __config__: ClassVar[Type[SettingsConfig]] + # dummy getattr for pylance checking, actually not used def __getattr__(self, name: str) -> Any: # pragma: no cover return self.__dict__.get(name) - class Config: - extra = Extra.allow - env_nested_delimiter = "__" + Config = SettingsConfig - @classmethod - def customise_sources( - cls, - init_settings: InitSettingsSource, - env_settings: EnvSettingsSource, - file_secret_settings: SettingsSourceCallable, - ) -> Tuple[SettingsSourceCallable, ...]: - common_config = init_settings.init_kwargs.pop("_common_config", {}) - return ( - init_settings, - CustomEnvSettings( - env_settings.env_file, - env_settings.env_file_encoding, - env_settings.env_nested_delimiter, - env_settings.env_prefix_len, - ), - InitSettingsSource(common_config), - file_secret_settings, + def __init__( + __settings_self__, # pyright: ignore[reportSelfClsParameterName] + _env_file: Optional[DOTENV_TYPE] = ENV_FILE_SENTINEL, + _env_file_encoding: Optional[str] = None, + _env_nested_delimiter: Optional[str] = None, + **values: Any, + ) -> None: + super().__init__( + **__settings_self__._settings_build_values( + values, + env_file=_env_file, + env_file_encoding=_env_file_encoding, + env_nested_delimiter=_env_nested_delimiter, ) + ) + + def _settings_build_values( + self, + init_kwargs: Dict[str, Any], + env_file: Optional[DOTENV_TYPE] = None, + env_file_encoding: Optional[str] = None, + env_nested_delimiter: Optional[str] = None, + ) -> Dict[str, Any]: + init_settings = InitSettingsSource(self.__class__, init_kwargs=init_kwargs) + env_settings = DotEnvSettingsSource( + self.__class__, + env_file=env_file, + env_file_encoding=env_file_encoding, + env_nested_delimiter=env_nested_delimiter, + ) + return deep_update(env_settings(), init_settings()) -class Env(BaseConfig): +class Env(BaseSettings): """运行环境配置。大小写不敏感。 将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。 @@ -160,7 +361,7 @@ class Env(BaseConfig): env_file = ".env" -class Config(BaseConfig): +class Config(BaseSettings): """NoneBot 主要配置。大小写不敏感。 除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。 @@ -169,7 +370,7 @@ class Config(BaseConfig): 配置方法参考: [配置](https://nonebot.dev/docs/appendices/config) """ - _env_file: DotenvType = ".env", ".env.prod" + _env_file: Optional[DOTENV_TYPE] = ".env", ".env.prod" # nonebot configs driver: str = "~fastapi" @@ -259,6 +460,10 @@ class Config(BaseConfig): __autodoc__ = { - "CustomEnvSettings": False, - "BaseConfig": False, + "SettingsError": False, + "BaseSettingsSource": False, + "InitSettingsSource": False, + "DotEnvSettingsSource": False, + "SettingsConfig": False, + "BaseSettings": False, } diff --git a/poetry.lock b/poetry.lock index c0761c8f..1ca194d9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -140,24 +140,25 @@ frozenlist = ">=1.1.0" [[package]] name = "anyio" -version = "3.7.1" +version = "4.2.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, ] [package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "asgiref" @@ -203,21 +204,22 @@ files = [ [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" optional = true python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "black" @@ -619,63 +621,63 @@ files = [ [[package]] name = "coverage" -version = "7.3.4" +version = "7.4.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aff2bd3d585969cc4486bfc69655e862028b689404563e6b549e6a8244f226df"}, - {file = "coverage-7.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4353923f38d752ecfbd3f1f20bf7a3546993ae5ecd7c07fd2f25d40b4e54571"}, - {file = "coverage-7.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea473c37872f0159294f7073f3fa72f68b03a129799f3533b2bb44d5e9fa4f82"}, - {file = "coverage-7.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5214362abf26e254d749fc0c18af4c57b532a4bfde1a057565616dd3b8d7cc94"}, - {file = "coverage-7.3.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99b7d3f7a7adfa3d11e3a48d1a91bb65739555dd6a0d3fa68aa5852d962e5b1"}, - {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:74397a1263275bea9d736572d4cf338efaade2de9ff759f9c26bcdceb383bb49"}, - {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f154bd866318185ef5865ace5be3ac047b6d1cc0aeecf53bf83fe846f4384d5d"}, - {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e0d84099ea7cba9ff467f9c6f747e3fc3906e2aadac1ce7b41add72e8d0a3712"}, - {file = "coverage-7.3.4-cp310-cp310-win32.whl", hash = "sha256:3f477fb8a56e0c603587b8278d9dbd32e54bcc2922d62405f65574bd76eba78a"}, - {file = "coverage-7.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:c75738ce13d257efbb6633a049fb2ed8e87e2e6c2e906c52d1093a4d08d67c6b"}, - {file = "coverage-7.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:997aa14b3e014339d8101b9886063c5d06238848905d9ad6c6eabe533440a9a7"}, - {file = "coverage-7.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a9c5bc5db3eb4cd55ecb8397d8e9b70247904f8eca718cc53c12dcc98e59fc8"}, - {file = "coverage-7.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27ee94f088397d1feea3cb524e4313ff0410ead7d968029ecc4bc5a7e1d34fbf"}, - {file = "coverage-7.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ce03e25e18dd9bf44723e83bc202114817f3367789052dc9e5b5c79f40cf59d"}, - {file = "coverage-7.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85072e99474d894e5df582faec04abe137b28972d5e466999bc64fc37f564a03"}, - {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a877810ef918d0d345b783fc569608804f3ed2507bf32f14f652e4eaf5d8f8d0"}, - {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9ac17b94ab4ca66cf803f2b22d47e392f0977f9da838bf71d1f0db6c32893cb9"}, - {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:36d75ef2acab74dc948d0b537ef021306796da551e8ac8b467810911000af66a"}, - {file = "coverage-7.3.4-cp311-cp311-win32.whl", hash = "sha256:47ee56c2cd445ea35a8cc3ad5c8134cb9bece3a5cb50bb8265514208d0a65928"}, - {file = "coverage-7.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:11ab62d0ce5d9324915726f611f511a761efcca970bd49d876cf831b4de65be5"}, - {file = "coverage-7.3.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:33e63c578f4acce1b6cd292a66bc30164495010f1091d4b7529d014845cd9bee"}, - {file = "coverage-7.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:782693b817218169bfeb9b9ba7f4a9f242764e180ac9589b45112571f32a0ba6"}, - {file = "coverage-7.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c4277ddaad9293454da19121c59f2d850f16bcb27f71f89a5c4836906eb35ef"}, - {file = "coverage-7.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d892a19ae24b9801771a5a989fb3e850bd1ad2e2b6e83e949c65e8f37bc67a1"}, - {file = "coverage-7.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3024ec1b3a221bd10b5d87337d0373c2bcaf7afd86d42081afe39b3e1820323b"}, - {file = "coverage-7.3.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1c3e9d2bbd6f3f79cfecd6f20854f4dc0c6e0ec317df2b265266d0dc06535f1"}, - {file = "coverage-7.3.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e91029d7f151d8bf5ab7d8bfe2c3dbefd239759d642b211a677bc0709c9fdb96"}, - {file = "coverage-7.3.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6879fe41c60080aa4bb59703a526c54e0412b77e649a0d06a61782ecf0853ee1"}, - {file = "coverage-7.3.4-cp312-cp312-win32.whl", hash = "sha256:fd2f8a641f8f193968afdc8fd1697e602e199931012b574194052d132a79be13"}, - {file = "coverage-7.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:d1d0ce6c6947a3a4aa5479bebceff2c807b9f3b529b637e2b33dea4468d75fc7"}, - {file = "coverage-7.3.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:36797b3625d1da885b369bdaaa3b0d9fb8865caed3c2b8230afaa6005434aa2f"}, - {file = "coverage-7.3.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfed0ec4b419fbc807dec417c401499ea869436910e1ca524cfb4f81cf3f60e7"}, - {file = "coverage-7.3.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f97ff5a9fc2ca47f3383482858dd2cb8ddbf7514427eecf5aa5f7992d0571429"}, - {file = "coverage-7.3.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:607b6c6b35aa49defaebf4526729bd5238bc36fe3ef1a417d9839e1d96ee1e4c"}, - {file = "coverage-7.3.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8e258dcc335055ab59fe79f1dec217d9fb0cdace103d6b5c6df6b75915e7959"}, - {file = "coverage-7.3.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a02ac7c51819702b384fea5ee033a7c202f732a2a2f1fe6c41e3d4019828c8d3"}, - {file = "coverage-7.3.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b710869a15b8caf02e31d16487a931dbe78335462a122c8603bb9bd401ff6fb2"}, - {file = "coverage-7.3.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6a23ae9348a7a92e7f750f9b7e828448e428e99c24616dec93a0720342f241d"}, - {file = "coverage-7.3.4-cp38-cp38-win32.whl", hash = "sha256:758ebaf74578b73f727acc4e8ab4b16ab6f22a5ffd7dd254e5946aba42a4ce76"}, - {file = "coverage-7.3.4-cp38-cp38-win_amd64.whl", hash = "sha256:309ed6a559bc942b7cc721f2976326efbfe81fc2b8f601c722bff927328507dc"}, - {file = "coverage-7.3.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:aefbb29dc56317a4fcb2f3857d5bce9b881038ed7e5aa5d3bcab25bd23f57328"}, - {file = "coverage-7.3.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:183c16173a70caf92e2dfcfe7c7a576de6fa9edc4119b8e13f91db7ca33a7923"}, - {file = "coverage-7.3.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a4184dcbe4f98d86470273e758f1d24191ca095412e4335ff27b417291f5964"}, - {file = "coverage-7.3.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93698ac0995516ccdca55342599a1463ed2e2d8942316da31686d4d614597ef9"}, - {file = "coverage-7.3.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb220b3596358a86361139edce40d97da7458412d412e1e10c8e1970ee8c09ab"}, - {file = "coverage-7.3.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d5b14abde6f8d969e6b9dd8c7a013d9a2b52af1235fe7bebef25ad5c8f47fa18"}, - {file = "coverage-7.3.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:610afaf929dc0e09a5eef6981edb6a57a46b7eceff151947b836d869d6d567c1"}, - {file = "coverage-7.3.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed790728fb71e6b8247bd28e77e99d0c276dff952389b5388169b8ca7b1c28"}, - {file = "coverage-7.3.4-cp39-cp39-win32.whl", hash = "sha256:c15fdfb141fcf6a900e68bfa35689e1256a670db32b96e7a931cab4a0e1600e5"}, - {file = "coverage-7.3.4-cp39-cp39-win_amd64.whl", hash = "sha256:38d0b307c4d99a7aca4e00cad4311b7c51b7ac38fb7dea2abe0d182dd4008e05"}, - {file = "coverage-7.3.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b1e0f25ae99cf247abfb3f0fac7ae25739e4cd96bf1afa3537827c576b4847e5"}, - {file = "coverage-7.3.4.tar.gz", hash = "sha256:020d56d2da5bc22a0e00a5b0d54597ee91ad72446fa4cf1b97c35022f6b6dbf0"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, + {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, + {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, + {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, + {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, + {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, + {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, + {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, + {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, + {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, + {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, + {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, + {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, ] [package.dependencies] @@ -741,19 +743,18 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "fastapi" -version = "0.105.0" +version = "0.108.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = true python-versions = ">=3.8" files = [ - {file = "fastapi-0.105.0-py3-none-any.whl", hash = "sha256:f19ebf6fdc82a3281d10f2cb4774bdfa90238e3b40af3525a0c09fd08ad1c480"}, - {file = "fastapi-0.105.0.tar.gz", hash = "sha256:4d12838819aa52af244580675825e750ad67c9df4614f557a769606af902cf22"}, + {file = "fastapi-0.108.0-py3-none-any.whl", hash = "sha256:8c7bc6d315da963ee4cdb605557827071a9a7f95aeb8fcdd3bde48cdc8764dd7"}, + {file = "fastapi-0.108.0.tar.gz", hash = "sha256:5056e504ac6395bf68493d71fcfc5352fdbd5fda6f88c21f6420d80d81163296"}, ] [package.dependencies] -anyio = ">=3.7.1,<4.0.0" pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.27.0,<0.28.0" +starlette = ">=0.29.0,<0.33.0" typing-extensions = ">=4.8.0" [package.extras] @@ -1017,13 +1018,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "hypercorn" -version = "0.15.0" +version = "0.16.0" description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "hypercorn-0.15.0-py3-none-any.whl", hash = "sha256:5008944999612fd188d7a1ca02e89d20065642b89503020ac392dfed11840730"}, - {file = "hypercorn-0.15.0.tar.gz", hash = "sha256:d517f68d5dc7afa9a9d50ecefb0f769f466ebe8c1c18d2c2f447a24e763c9a63"}, + {file = "hypercorn-0.16.0-py3-none-any.whl", hash = "sha256:929e45c4acde3fbf7c58edf55336d30a009d2b4cb1f1eb96e6a515d61b663f58"}, + {file = "hypercorn-0.16.0.tar.gz", hash = "sha256:3b17d1dcf4992c1f262d9f9dd799c374125d0b9a8e40e1e2d11e2938b0adfe03"}, ] [package.dependencies] @@ -1133,13 +1134,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." optional = true python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -1622,7 +1623,6 @@ files = [ ] [package.dependencies] -python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""} typing-extensions = ">=4.2.0" [package.extras] @@ -1642,13 +1642,13 @@ files = [ [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -1664,13 +1664,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.23.2" +version = "0.23.3" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.2.tar.gz", hash = "sha256:c16052382554c7b22d48782ab3438d5b10f8cf7a4bdcae7f0f67f097d95beecc"}, - {file = "pytest_asyncio-0.23.2-py3-none-any.whl", hash = "sha256:ea9021364e32d58f0be43b91c6233fb8d2224ccef2398d6837559e587682808f"}, + {file = "pytest-asyncio-0.23.3.tar.gz", hash = "sha256:af313ce900a62fbe2b1aed18e37ad757f1ef9940c6b6a88e2954de38d6b1fb9f"}, + {file = "pytest_asyncio-0.23.3-py3-none-any.whl", hash = "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba"}, ] [package.dependencies] @@ -1842,28 +1842,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.9" +version = "0.1.11" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.9-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e6a212f436122ac73df851f0cf006e0c6612fe6f9c864ed17ebefce0eff6a5fd"}, - {file = "ruff-0.1.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:28d920e319783d5303333630dae46ecc80b7ba294aeffedf946a02ac0b7cc3db"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:104aa9b5e12cb755d9dce698ab1b97726b83012487af415a4512fedd38b1459e"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e63bf5a4a91971082a4768a0aba9383c12392d0d6f1e2be2248c1f9054a20da"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d0738917c203246f3e275b37006faa3aa96c828b284ebfe3e99a8cb413c8c4b"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69dac82d63a50df2ab0906d97a01549f814b16bc806deeac4f064ff95c47ddf5"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2aec598fb65084e41a9c5d4b95726173768a62055aafb07b4eff976bac72a592"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:744dfe4b35470fa3820d5fe45758aace6269c578f7ddc43d447868cfe5078bcb"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479ca4250cab30f9218b2e563adc362bd6ae6343df7c7b5a7865300a5156d5a6"}, - {file = "ruff-0.1.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:aa8344310f1ae79af9ccd6e4b32749e93cddc078f9b5ccd0e45bd76a6d2e8bb6"}, - {file = "ruff-0.1.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:837c739729394df98f342319f5136f33c65286b28b6b70a87c28f59354ec939b"}, - {file = "ruff-0.1.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e6837202c2859b9f22e43cb01992373c2dbfeae5c0c91ad691a4a2e725392464"}, - {file = "ruff-0.1.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:331aae2cd4a0554667ac683243b151c74bd60e78fb08c3c2a4ac05ee1e606a39"}, - {file = "ruff-0.1.9-py3-none-win32.whl", hash = "sha256:8151425a60878e66f23ad47da39265fc2fad42aed06fb0a01130e967a7a064f4"}, - {file = "ruff-0.1.9-py3-none-win_amd64.whl", hash = "sha256:c497d769164df522fdaf54c6eba93f397342fe4ca2123a2e014a5b8fc7df81c7"}, - {file = "ruff-0.1.9-py3-none-win_arm64.whl", hash = "sha256:0e17f53bcbb4fff8292dfd84cf72d767b5e146f009cccd40c2fad27641f8a7a9"}, - {file = "ruff-0.1.9.tar.gz", hash = "sha256:b041dee2734719ddbb4518f762c982f2e912e7f28b8ee4fe1dee0b15d1b6e800"}, + {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, + {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, + {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, + {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, + {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, + {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, ] [[package]] @@ -1895,13 +1895,13 @@ files = [ [[package]] name = "starlette" -version = "0.27.0" +version = "0.32.0.post1" description = "The little ASGI library that shines." optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, - {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, + {file = "starlette-0.32.0.post1-py3-none-any.whl", hash = "sha256:cd0cb10ddb49313f609cedfac62c8c12e56c7314b66d89bb077ba228bada1b09"}, + {file = "starlette-0.32.0.post1.tar.gz", hash = "sha256:e54e2b7e2fb06dff9eac40133583f10dfa05913f5a85bf26f427c7a40a9a3d02"}, ] [package.dependencies] @@ -2142,13 +2142,13 @@ anyio = ">=3.0.0" [[package]] name = "wcwidth" -version = "0.2.12" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, - {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [[package]] @@ -2406,4 +2406,4 @@ websockets = ["websockets"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "0435932f8ea0fb58e19a9c14120bb15fd44515e6304dd3967fb1b6ce7c531617" +content-hash = "e7bd1c1b070f1a46d94022047f2b76dbf90751f49086a099139f2ade4ad07a65" diff --git a/pyproject.toml b/pyproject.toml index 38cb7015..3cbde147 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,10 +28,11 @@ include = ["nonebot/py.typed"] python = "^3.8" yarl = "^1.7.2" pygtrie = "^2.4.1" +pydantic = "^1.10.0" loguru = ">=0.6.0,<1.0.0" +python-dotenv = ">=0.21.0,<2.0.0" typing-extensions = ">=4.4.0,<5.0.0" tomli = { version = "^2.0.1", python = "<3.11" } -pydantic = { version = "^1.10.0", extras = ["dotenv"] } websockets = { version = ">=10.0", optional = true } Quart = { version = ">=0.18.0,<1.0.0", optional = true } diff --git a/tests/.env.example b/tests/.env.example new file mode 100644 index 00000000..5d416fd5 --- /dev/null +++ b/tests/.env.example @@ -0,0 +1,17 @@ +SIMPLE=simple +COMPLEX=' +[1, 2, 3] +' +COMPLEX_NONE +COMPLEX_UNION=[1, 2, 3] +NESTED={"a": 1} +NESTED__B=2 +NESTED__C__C=3 +NESTED__COMPLEX=[1, 2, 3] +NESTED_INNER__A=1 +NESTED_INNER__B=2 +OTHER_SIMPLE=simple +OTHER_NESTED={"a": 1} +OTHER_NESTED__B=2 +OTHER_NESTED_INNER__A=1 +OTHER_NESTED_INNER__B=2 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..15748502 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,118 @@ +from typing import List, Union, Optional + +import pytest +from pydantic import BaseModel + +from nonebot.config import DOTENV_TYPE, BaseSettings, SettingsError + + +class Simple(BaseModel): + a: int = 0 + b: int = 0 + c: dict = {} + complex: list = [] + + +class Example(BaseSettings): + _env_file: Optional[DOTENV_TYPE] = ".env", ".env.example" + _env_nested_delimiter: Optional[str] = "__" + + simple: str = "" + complex: List[int] = [1] + complex_none: Optional[List[int]] = None + complex_union: Union[int, List[int]] = 1 + nested: Simple = Simple() + nested_inner: Simple = Simple() + + class Config: + env_file = ".env", ".env.example" + env_nested_delimiter = "__" + + +class ExampleWithoutDelimiter(Example): + class Config: + env_nested_delimiter = None + + +@pytest.mark.asyncio +async def test_config_no_env(): + config = Example(_env_file=None) + assert config.simple == "" + with pytest.raises(AttributeError): + config.common_config + + +@pytest.mark.asyncio +async def test_config_with_env(): + config = Example(_env_file=(".env", ".env.example")) + assert config.simple == "simple" + + assert config.complex == [1, 2, 3] + + assert config.complex_none is None + + assert config.complex_union == [1, 2, 3] + + assert config.nested.a == 1 + assert config.nested.b == 2 + assert config.nested.c == {"c": "3"} + assert config.nested.complex == [1, 2, 3] + with pytest.raises(AttributeError): + config.nested__b + with pytest.raises(AttributeError): + config.nested__c__c + with pytest.raises(AttributeError): + config.nested__complex + + assert config.nested_inner.a == 1 + assert config.nested_inner.b == 2 + with pytest.raises(AttributeError): + config.nested_inner__a + with pytest.raises(AttributeError): + config.nested_inner__b + + assert config.common_config == "common" + + assert config.other_simple == "simple" + + assert config.other_nested == {"a": 1, "b": 2} + with pytest.raises(AttributeError): + config.other_nested__b + + assert config.other_nested_inner == {"a": 1, "b": 2} + with pytest.raises(AttributeError): + config.other_nested_inner__a + with pytest.raises(AttributeError): + config.other_nested_inner__b + + +@pytest.mark.asyncio +async def test_config_error_env(): + with pytest.MonkeyPatch().context() as m: + m.setenv("COMPLEX", "not json") + + with pytest.raises(SettingsError): + Example(_env_file=(".env", ".env.example")) + + +@pytest.mark.asyncio +async def test_config_without_delimiter(): + config = ExampleWithoutDelimiter() + assert config.nested.a == 1 + assert config.nested.b == 0 + assert config.nested__b == 2 + assert config.nested.c == {} + assert config.nested__c__c == 3 + assert config.nested.complex == [] + assert config.nested__complex == [1, 2, 3] + + assert config.nested_inner.a == 0 + assert config.nested_inner.b == 0 + + assert config.other_nested == {"a": 1} + assert config.other_nested__b == 2 + + with pytest.raises(AttributeError): + config.other_nested_inner + assert config.other_nested_inner__a == 1 + assert config.other_nested_inner__b == 2