liblaf-conf 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- liblaf_conf-0.1.0/.gitignore +40 -0
- liblaf_conf-0.1.0/LICENSE +21 -0
- liblaf_conf-0.1.0/PKG-INFO +31 -0
- liblaf_conf-0.1.0/README.md +1 -0
- liblaf_conf-0.1.0/pyproject.toml +99 -0
- liblaf_conf-0.1.0/src/liblaf/conf/__init__.py +4 -0
- liblaf_conf-0.1.0/src/liblaf/conf/__init__.pyi +45 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_config.py +118 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_field.py +43 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_field_specifiers.py +171 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_group.py +33 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_sentinel.py +8 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_types.py +11 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_var.py +86 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_version.py +34 -0
- liblaf_conf-0.1.0/src/liblaf/conf/_version.pyi +8 -0
- liblaf_conf-0.1.0/src/liblaf/conf/converters/__init__.py +4 -0
- liblaf_conf-0.1.0/src/liblaf/conf/converters/__init__.pyi +19 -0
- liblaf_conf-0.1.0/src/liblaf/conf/converters/_misc.py +2 -0
- liblaf_conf-0.1.0/src/liblaf/conf/converters/_pydantic.py +34 -0
- liblaf_conf-0.1.0/src/liblaf/conf/py.typed +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ref: <https://github.com/github/gitignore/blob/main/Python.gitignore>
|
|
2
|
+
|
|
3
|
+
# Globally Useful gitignores
|
|
4
|
+
*.log
|
|
5
|
+
*.log.*
|
|
6
|
+
|
|
7
|
+
# Byte-compiled / optimized / DLL files
|
|
8
|
+
__pycache__/
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
dist/
|
|
12
|
+
|
|
13
|
+
# Unit test / coverage reports
|
|
14
|
+
.nox/
|
|
15
|
+
.cache/
|
|
16
|
+
.coverage
|
|
17
|
+
.coverage.*
|
|
18
|
+
.hypothesis/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
|
|
21
|
+
# pixi
|
|
22
|
+
.pixi/
|
|
23
|
+
|
|
24
|
+
# Environments
|
|
25
|
+
.venv/
|
|
26
|
+
|
|
27
|
+
# mkdocs documentation
|
|
28
|
+
/site/
|
|
29
|
+
|
|
30
|
+
# Ruff stuff:
|
|
31
|
+
.ruff_cache/
|
|
32
|
+
|
|
33
|
+
# [Codecov](https://docs.codecov.com/docs/test-analytics#1-output-a-junit-xml-file-in-your-ci)
|
|
34
|
+
junit.xml
|
|
35
|
+
|
|
36
|
+
# [CodSpeed](https://codspeed.io/)
|
|
37
|
+
.codspeed/
|
|
38
|
+
|
|
39
|
+
# [hatch-vcs](https://github.com/ofek/hatch-vcs)
|
|
40
|
+
_version.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 liblaf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: liblaf-conf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Project-URL: Changelog, https://github.com/liblaf/conf/blob/main/CHANGELOG.md
|
|
5
|
+
Project-URL: Documentation, https://liblaf.github.io/conf/
|
|
6
|
+
Project-URL: Funding, https://github.com/liblaf/conf?sponsor=1
|
|
7
|
+
Project-URL: Homepage, https://github.com/liblaf/conf
|
|
8
|
+
Project-URL: Issue Tracker, https://github.com/liblaf/conf/issues
|
|
9
|
+
Project-URL: Release Notes, https://github.com/liblaf/conf/releases
|
|
10
|
+
Project-URL: Source Code, https://github.com/liblaf/conf
|
|
11
|
+
Author-email: liblaf <30631553+liblaf@users.noreply.github.com>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Requires-Dist: lazy-loader>=0.1
|
|
27
|
+
Requires-Dist: pydantic-extra-types[pendulum]>=2.11
|
|
28
|
+
Requires-Dist: pydantic>=2.12
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# conf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# conf
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#:schema https://json.schemastore.org/pyproject.json
|
|
2
|
+
# ref: <https://packaging.python.org/en/latest/specifications/pyproject-toml/>
|
|
3
|
+
|
|
4
|
+
[project]
|
|
5
|
+
name = "liblaf-conf"
|
|
6
|
+
description = ""
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "liblaf", email = "30631553+liblaf@users.noreply.github.com" }
|
|
12
|
+
]
|
|
13
|
+
keywords = []
|
|
14
|
+
classifiers = [
|
|
15
|
+
# ref: <https://pypi.org/classifiers/>
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Typing :: Typed"
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"lazy-loader>=0.1",
|
|
30
|
+
"pydantic>=2.12",
|
|
31
|
+
"pydantic-extra-types[pendulum]>=2.11"
|
|
32
|
+
]
|
|
33
|
+
dynamic = ["version"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
# ref: <https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels>
|
|
37
|
+
"Changelog" = "https://github.com/liblaf/conf/blob/main/CHANGELOG.md"
|
|
38
|
+
"Documentation" = "https://liblaf.github.io/conf/"
|
|
39
|
+
"Funding" = "https://github.com/liblaf/conf?sponsor=1"
|
|
40
|
+
"Homepage" = "https://github.com/liblaf/conf"
|
|
41
|
+
"Issue Tracker" = "https://github.com/liblaf/conf/issues"
|
|
42
|
+
"Release Notes" = "https://github.com/liblaf/conf/releases"
|
|
43
|
+
"Source Code" = "https://github.com/liblaf/conf"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = []
|
|
47
|
+
build = ["check-wheel-contents>=0.6", "hatch>=1", "twine>=6"]
|
|
48
|
+
docs = ["liblaf-mkdocs-preset>=0.3", "mkdocs>=1,<2"]
|
|
49
|
+
nox = ["liblaf-nox-recipes>=0.1", "nox>=2026"]
|
|
50
|
+
test = [
|
|
51
|
+
"pytest>=9",
|
|
52
|
+
"pytest-cov>=7",
|
|
53
|
+
"pytest-randomly>=4",
|
|
54
|
+
"pytest-xdist>=3",
|
|
55
|
+
{ include-group = "nox" }
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[build-system]
|
|
59
|
+
requires = ["hatch-vcs", "hatchling"]
|
|
60
|
+
build-backend = "hatchling.build"
|
|
61
|
+
|
|
62
|
+
[tool.check-wheel-contents]
|
|
63
|
+
ignore = ["W002"]
|
|
64
|
+
|
|
65
|
+
[tool.coverage.run]
|
|
66
|
+
branch = true
|
|
67
|
+
source = ["src"]
|
|
68
|
+
|
|
69
|
+
[tool.hatch.build.hooks.vcs]
|
|
70
|
+
version-file = "src/liblaf/conf/_version.py"
|
|
71
|
+
|
|
72
|
+
[tool.hatch.build.targets.sdist]
|
|
73
|
+
only-include = ["src"]
|
|
74
|
+
|
|
75
|
+
[tool.hatch.build.targets.wheel]
|
|
76
|
+
packages = ["src/liblaf"]
|
|
77
|
+
|
|
78
|
+
[tool.hatch.version]
|
|
79
|
+
source = "vcs"
|
|
80
|
+
|
|
81
|
+
[tool.pytest]
|
|
82
|
+
addopts = [
|
|
83
|
+
"--doctest-modules",
|
|
84
|
+
"--import-mode=importlib",
|
|
85
|
+
"--numprocesses=auto",
|
|
86
|
+
"--showlocals"
|
|
87
|
+
]
|
|
88
|
+
consider_namespace_packages = true
|
|
89
|
+
strict = true
|
|
90
|
+
testpaths = ["benches", "src", "tests"]
|
|
91
|
+
|
|
92
|
+
[tool.ruff]
|
|
93
|
+
extend = ".config/linters/.ruff.toml"
|
|
94
|
+
|
|
95
|
+
[tool.ty.environment]
|
|
96
|
+
python = ".venv" # ty-vscode does not work without this
|
|
97
|
+
|
|
98
|
+
[tool.uv]
|
|
99
|
+
default-groups = "all"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from ._config import BaseConfig, ConfigMeta
|
|
2
|
+
from ._field import Converter, Factory, Field
|
|
3
|
+
from ._field_specifiers import (
|
|
4
|
+
field_bool,
|
|
5
|
+
field_date,
|
|
6
|
+
field_datetime,
|
|
7
|
+
field_decimal,
|
|
8
|
+
field_float,
|
|
9
|
+
field_int,
|
|
10
|
+
field_json,
|
|
11
|
+
field_list_str,
|
|
12
|
+
field_path,
|
|
13
|
+
field_str,
|
|
14
|
+
field_time,
|
|
15
|
+
field_timedelta,
|
|
16
|
+
)
|
|
17
|
+
from ._group import Group, group
|
|
18
|
+
from ._var import Var
|
|
19
|
+
from ._version import __commit_id__, __version__, __version_tuple__
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"BaseConfig",
|
|
23
|
+
"ConfigMeta",
|
|
24
|
+
"Converter",
|
|
25
|
+
"Factory",
|
|
26
|
+
"Field",
|
|
27
|
+
"Group",
|
|
28
|
+
"Var",
|
|
29
|
+
"__commit_id__",
|
|
30
|
+
"__version__",
|
|
31
|
+
"__version_tuple__",
|
|
32
|
+
"field_bool",
|
|
33
|
+
"field_date",
|
|
34
|
+
"field_datetime",
|
|
35
|
+
"field_decimal",
|
|
36
|
+
"field_float",
|
|
37
|
+
"field_int",
|
|
38
|
+
"field_json",
|
|
39
|
+
"field_list_str",
|
|
40
|
+
"field_path",
|
|
41
|
+
"field_str",
|
|
42
|
+
"field_time",
|
|
43
|
+
"field_timedelta",
|
|
44
|
+
"group",
|
|
45
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import types
|
|
5
|
+
from collections.abc import Generator, Mapping
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
|
|
7
|
+
|
|
8
|
+
from pydantic import alias_generators
|
|
9
|
+
|
|
10
|
+
from ._field import Field
|
|
11
|
+
from ._group import Group
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ._var import Var
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigMeta(type):
|
|
18
|
+
def __new__(
|
|
19
|
+
mcs,
|
|
20
|
+
name: str,
|
|
21
|
+
bases: tuple[type, ...],
|
|
22
|
+
namespace: dict[str, Any],
|
|
23
|
+
/,
|
|
24
|
+
**_kwargs: Any,
|
|
25
|
+
) -> type:
|
|
26
|
+
if "name" not in namespace:
|
|
27
|
+
namespace["name"] = alias_generators.to_snake(name).removesuffix("_config")
|
|
28
|
+
if "env_prefix" not in namespace:
|
|
29
|
+
namespace["env_prefix"] = namespace["name"].upper() + "_"
|
|
30
|
+
cls: type[BaseConfig] = cast(
|
|
31
|
+
"type[BaseConfig]", super().__new__(mcs, name, bases, namespace)
|
|
32
|
+
)
|
|
33
|
+
fields: dict[str, Field[Any]] = {}
|
|
34
|
+
groups: dict[str, Group[Any]] = {}
|
|
35
|
+
for base in reversed(cls.__mro__[1:]):
|
|
36
|
+
fields.update(getattr(base, "_fields", {}))
|
|
37
|
+
groups.update(getattr(base, "_groups", {}))
|
|
38
|
+
for key, value in namespace.items():
|
|
39
|
+
match value:
|
|
40
|
+
case Field():
|
|
41
|
+
groups.pop(key, None)
|
|
42
|
+
fields[key] = value
|
|
43
|
+
case Group():
|
|
44
|
+
fields.pop(key, None)
|
|
45
|
+
groups[key] = value
|
|
46
|
+
case _:
|
|
47
|
+
fields.pop(key, None)
|
|
48
|
+
groups.pop(key, None)
|
|
49
|
+
cls._fields = fields
|
|
50
|
+
cls._groups = groups
|
|
51
|
+
return cls
|
|
52
|
+
|
|
53
|
+
def __call__[T: BaseConfig](cls: type[T], *args, **kwargs) -> T:
|
|
54
|
+
instance: T | None = cls.__dict__.get("_instance")
|
|
55
|
+
if instance is None:
|
|
56
|
+
instance = super().__call__(*args, **kwargs) # ty:ignore[invalid-super-argument]
|
|
57
|
+
cls._instance = instance
|
|
58
|
+
return instance
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BaseConfig(metaclass=ConfigMeta):
|
|
62
|
+
name: ClassVar[str]
|
|
63
|
+
env_prefix: ClassVar[str]
|
|
64
|
+
_fields: ClassVar[dict[str, Field[Any]]]
|
|
65
|
+
_groups: ClassVar[dict[str, Group[Any]]]
|
|
66
|
+
_instance: ClassVar[Self | None] = None
|
|
67
|
+
|
|
68
|
+
def load_env(self) -> None:
|
|
69
|
+
for name in self._fields:
|
|
70
|
+
var: Var[Any] = self._get_field(name)
|
|
71
|
+
var.load_env()
|
|
72
|
+
for name in self._groups:
|
|
73
|
+
group: BaseConfig = self._get_group(name)
|
|
74
|
+
group.load_env()
|
|
75
|
+
|
|
76
|
+
def set(self, changes: Mapping[str, Any] | None = None, /, **kwargs: Any) -> None:
|
|
77
|
+
if changes is not None:
|
|
78
|
+
kwargs.update(changes)
|
|
79
|
+
for name, value in kwargs.items():
|
|
80
|
+
var: BaseConfig | Var[Any] = self._get_field_or_group(name)
|
|
81
|
+
var.set(value)
|
|
82
|
+
|
|
83
|
+
@contextlib.contextmanager
|
|
84
|
+
def override(
|
|
85
|
+
self, changes: Mapping[str, Any] | None = None, /, **kwargs: Any
|
|
86
|
+
) -> Generator[None]:
|
|
87
|
+
if changes is not None:
|
|
88
|
+
kwargs.update(changes)
|
|
89
|
+
with contextlib.ExitStack() as stack:
|
|
90
|
+
for name, value in kwargs.items():
|
|
91
|
+
var: BaseConfig | Var[Any] = self._get_field_or_group(name)
|
|
92
|
+
stack.enter_context(var.override(value))
|
|
93
|
+
yield
|
|
94
|
+
|
|
95
|
+
def to_dict(self) -> dict[str, Any]:
|
|
96
|
+
result: dict[str, Any] = {}
|
|
97
|
+
for name in self._fields:
|
|
98
|
+
result[name] = self._get_field(name).get()
|
|
99
|
+
for name in self._groups:
|
|
100
|
+
result[name] = self._get_group(name).to_dict()
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def to_namespace(self) -> types.SimpleNamespace:
|
|
104
|
+
result: types.SimpleNamespace = types.SimpleNamespace()
|
|
105
|
+
for name in self._fields:
|
|
106
|
+
setattr(result, name, self._get_field(name).get())
|
|
107
|
+
for name in self._groups:
|
|
108
|
+
setattr(result, name, self._get_group(name).to_namespace())
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
def _get_field(self, name: str) -> Var[Any]:
|
|
112
|
+
return getattr(self, name)
|
|
113
|
+
|
|
114
|
+
def _get_field_or_group(self, name: str) -> BaseConfig | Var[Any]:
|
|
115
|
+
return getattr(self, name)
|
|
116
|
+
|
|
117
|
+
def _get_group(self, name: str) -> BaseConfig:
|
|
118
|
+
return getattr(self, name)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Self, overload
|
|
3
|
+
|
|
4
|
+
from . import converters
|
|
5
|
+
from ._sentinel import MISSING, MissingType
|
|
6
|
+
from ._types import ConfigProtocol, Converter, Factory
|
|
7
|
+
from ._var import Var
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass(frozen=True, slots=True, weakref_slot=True)
|
|
11
|
+
class Field[T]:
|
|
12
|
+
env: str | None = None
|
|
13
|
+
default: T | MissingType = MISSING
|
|
14
|
+
factory: Factory[T] | None = None
|
|
15
|
+
converter: Converter[T] = converters.identity
|
|
16
|
+
name: str = dataclasses.field(init=False)
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
def __get__(self, instance: None, owner: type | None = None) -> Self: ...
|
|
20
|
+
@overload
|
|
21
|
+
def __get__(self, instance: object, owner: type | None = None) -> Var[T]: ...
|
|
22
|
+
def __get__(
|
|
23
|
+
self, instance: ConfigProtocol | None, owner: type | None = None
|
|
24
|
+
) -> Self | Var[T]:
|
|
25
|
+
if instance is None:
|
|
26
|
+
return self
|
|
27
|
+
if self.name not in instance.__dict__:
|
|
28
|
+
instance.__dict__[self.name] = self._bind(instance)
|
|
29
|
+
return instance.__dict__[self.name]
|
|
30
|
+
|
|
31
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
32
|
+
object.__setattr__(self, "name", name)
|
|
33
|
+
|
|
34
|
+
def _bind(self, instance: ConfigProtocol) -> Var[T]:
|
|
35
|
+
name: str = instance.name + "." + self.name
|
|
36
|
+
env: str = self.env or instance.env_prefix + self.name.upper()
|
|
37
|
+
return Var(
|
|
38
|
+
name,
|
|
39
|
+
default=self.default,
|
|
40
|
+
factory=self.factory,
|
|
41
|
+
env=env,
|
|
42
|
+
converter=self.converter,
|
|
43
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import json
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from pydantic_extra_types.pendulum_dt import Date, DateTime, Duration, Time
|
|
10
|
+
|
|
11
|
+
from . import converters
|
|
12
|
+
from ._field import Field
|
|
13
|
+
from ._sentinel import MISSING, MissingType
|
|
14
|
+
from ._types import Converter, Factory
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from _typeshed import StrPath
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def field_bool(
|
|
21
|
+
env: str | None = None,
|
|
22
|
+
default: bool | MissingType = MISSING, # noqa: FBT001
|
|
23
|
+
factory: Factory[bool] | None = None,
|
|
24
|
+
converter: Converter[bool] | None = None,
|
|
25
|
+
) -> Field[bool]:
|
|
26
|
+
if converter is None:
|
|
27
|
+
converter: Converter[bool] = converters.pydantic_type_adapter_validate_strings(
|
|
28
|
+
bool
|
|
29
|
+
)
|
|
30
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def field_date(
|
|
34
|
+
env: str | None = None,
|
|
35
|
+
default: Any | MissingType = MISSING,
|
|
36
|
+
factory: Factory[Any] | None = None,
|
|
37
|
+
converter: Converter[Any] | None = None,
|
|
38
|
+
) -> Field[Any]:
|
|
39
|
+
if converter is None:
|
|
40
|
+
converter: Converter[datetime.date] = (
|
|
41
|
+
converters.pydantic_type_adapter_validate_strings(Date)
|
|
42
|
+
)
|
|
43
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def field_datetime(
|
|
47
|
+
env: str | None = None,
|
|
48
|
+
default: datetime.datetime | MissingType = MISSING,
|
|
49
|
+
factory: Factory[datetime.datetime] | None = None,
|
|
50
|
+
converter: Converter[datetime.datetime] | None = None,
|
|
51
|
+
) -> Field[datetime.datetime]:
|
|
52
|
+
if converter is None:
|
|
53
|
+
converter: Converter[datetime.datetime] = (
|
|
54
|
+
converters.pydantic_type_adapter_validate_strings(DateTime)
|
|
55
|
+
)
|
|
56
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def field_decimal(
|
|
60
|
+
env: str | None = None,
|
|
61
|
+
default: Decimal | MissingType = MISSING,
|
|
62
|
+
factory: Factory[Decimal] | None = None,
|
|
63
|
+
converter: Converter[Decimal] | None = None,
|
|
64
|
+
) -> Field[Decimal]:
|
|
65
|
+
if converter is None:
|
|
66
|
+
converter: Converter[Decimal] = Decimal
|
|
67
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def field_float(
|
|
71
|
+
env: str | None = None,
|
|
72
|
+
default: float | MissingType = MISSING,
|
|
73
|
+
factory: Factory[float] | None = None,
|
|
74
|
+
converter: Converter[float] | None = None,
|
|
75
|
+
) -> Field[float]:
|
|
76
|
+
if converter is None:
|
|
77
|
+
converter: Converter[float] = float
|
|
78
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def field_int(
|
|
82
|
+
env: str | None = None,
|
|
83
|
+
default: int | MissingType = MISSING,
|
|
84
|
+
factory: Factory[int] | None = None,
|
|
85
|
+
converter: Converter[int] | None = None,
|
|
86
|
+
) -> Field[int]:
|
|
87
|
+
if converter is None:
|
|
88
|
+
converter: Converter[int] = int
|
|
89
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def field_json(
|
|
93
|
+
env: str | None = None,
|
|
94
|
+
default: Any | MissingType = MISSING,
|
|
95
|
+
factory: Factory[Any] | None = None,
|
|
96
|
+
converter: Converter[Any] | None = None,
|
|
97
|
+
) -> Field[Any]:
|
|
98
|
+
if converter is None:
|
|
99
|
+
converter: Converter[Any] = json.loads
|
|
100
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def field_list_str(
|
|
104
|
+
env: str | None = None,
|
|
105
|
+
default: list[str] | MissingType = MISSING,
|
|
106
|
+
factory: Factory[list[str]] | None = None,
|
|
107
|
+
converter: Converter[list[str]] | None = None,
|
|
108
|
+
delimiter: str = ",",
|
|
109
|
+
) -> Field[list[str]]:
|
|
110
|
+
if converter is None:
|
|
111
|
+
|
|
112
|
+
def converter(value: str) -> list[str]:
|
|
113
|
+
return [item.strip() for item in value.split(delimiter)]
|
|
114
|
+
|
|
115
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def field_path(
|
|
119
|
+
env: str | None = None,
|
|
120
|
+
default: StrPath | MissingType = MISSING,
|
|
121
|
+
factory: Factory[StrPath] | None = None,
|
|
122
|
+
converter: Converter[Path] | None = None,
|
|
123
|
+
) -> Field[Path]:
|
|
124
|
+
if converter is None:
|
|
125
|
+
converter: Converter[Path] = Path
|
|
126
|
+
if default is not MISSING:
|
|
127
|
+
default = Path(default)
|
|
128
|
+
if factory is not None:
|
|
129
|
+
wrapped: Factory[StrPath] = factory
|
|
130
|
+
|
|
131
|
+
def factory() -> Path:
|
|
132
|
+
return Path(wrapped())
|
|
133
|
+
|
|
134
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def field_str(
|
|
138
|
+
env: str | None = None,
|
|
139
|
+
default: str | MissingType = MISSING,
|
|
140
|
+
factory: Factory[str] | None = None,
|
|
141
|
+
converter: Converter[str] | None = None,
|
|
142
|
+
) -> Field[str]:
|
|
143
|
+
if converter is None:
|
|
144
|
+
converter: Converter[str] = converters.identity
|
|
145
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def field_time(
|
|
149
|
+
env: str | None = None,
|
|
150
|
+
default: datetime.time | MissingType = MISSING,
|
|
151
|
+
factory: Factory[datetime.time] | None = None,
|
|
152
|
+
converter: Converter[datetime.time] | None = None,
|
|
153
|
+
) -> Field[datetime.time]:
|
|
154
|
+
if converter is None:
|
|
155
|
+
converter: Converter[datetime.time] = (
|
|
156
|
+
converters.pydantic_type_adapter_validate_strings(Time)
|
|
157
|
+
)
|
|
158
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def field_timedelta(
|
|
162
|
+
env: str | None = None,
|
|
163
|
+
default: datetime.timedelta | MissingType = MISSING,
|
|
164
|
+
factory: Factory[datetime.timedelta] | None = None,
|
|
165
|
+
converter: Converter[datetime.timedelta] | None = None,
|
|
166
|
+
) -> Field[datetime.timedelta]:
|
|
167
|
+
if converter is None:
|
|
168
|
+
converter: Converter[datetime.timedelta] = (
|
|
169
|
+
converters.pydantic_type_adapter_validate_strings(Duration)
|
|
170
|
+
)
|
|
171
|
+
return Field(env=env, default=default, factory=factory, converter=converter)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Self, overload
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass(frozen=True, slots=True, weakref_slot=True)
|
|
9
|
+
class Group[T]:
|
|
10
|
+
factory: Callable[[], T]
|
|
11
|
+
name: str = dataclasses.field(init=False)
|
|
12
|
+
|
|
13
|
+
@overload
|
|
14
|
+
def __get__(self, instance: None, owner: type | None = None) -> Self: ...
|
|
15
|
+
@overload
|
|
16
|
+
def __get__(self, instance: object, owner: type | None = None) -> T: ...
|
|
17
|
+
def __get__(self, instance: object | None, owner: type | None = None) -> Self | T:
|
|
18
|
+
if instance is None:
|
|
19
|
+
return self
|
|
20
|
+
if self.name not in instance.__dict__:
|
|
21
|
+
instance.__dict__[self.name] = self.factory()
|
|
22
|
+
return instance.__dict__[self.name]
|
|
23
|
+
|
|
24
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
25
|
+
object.__setattr__(self, "name", name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@overload
|
|
29
|
+
def group[T](factory: type[T]) -> Group[T]: ...
|
|
30
|
+
@overload
|
|
31
|
+
def group[T](factory: Callable[[], T]) -> Group[T]: ...
|
|
32
|
+
def group[T](factory: Callable[[], T]) -> Group[T]:
|
|
33
|
+
return Group(factory)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import contextvars
|
|
3
|
+
import dataclasses
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
from typing import Any, cast, overload
|
|
7
|
+
|
|
8
|
+
from . import converters
|
|
9
|
+
from ._sentinel import MISSING, MissingType
|
|
10
|
+
from ._types import Converter, Factory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclasses.dataclass(init=False, frozen=True, slots=True, weakref_slot=True)
|
|
14
|
+
class Var[T]:
|
|
15
|
+
env: str | None
|
|
16
|
+
converter: Converter[T]
|
|
17
|
+
_var: contextvars.ContextVar[T]
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
name: str,
|
|
22
|
+
default: T | MissingType = MISSING,
|
|
23
|
+
factory: Factory[T] | None = None,
|
|
24
|
+
env: str | None = None,
|
|
25
|
+
converter: Converter[T] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
if converter is None:
|
|
28
|
+
converter: Converter[T] = converters.identity
|
|
29
|
+
if env is not None:
|
|
30
|
+
value: str | None = os.getenv(env)
|
|
31
|
+
if value is not None:
|
|
32
|
+
default: T = cast("T", converter(value))
|
|
33
|
+
if default is MISSING and factory is not None:
|
|
34
|
+
default: T = factory()
|
|
35
|
+
if default is MISSING:
|
|
36
|
+
var: contextvars.ContextVar[T] = contextvars.ContextVar(name)
|
|
37
|
+
else:
|
|
38
|
+
var: contextvars.ContextVar[T] = contextvars.ContextVar(
|
|
39
|
+
name, default=default
|
|
40
|
+
)
|
|
41
|
+
object.__setattr__(self, "_var", var)
|
|
42
|
+
object.__setattr__(self, "env", env)
|
|
43
|
+
object.__setattr__(self, "converter", converter)
|
|
44
|
+
|
|
45
|
+
def __hash__(self) -> int:
|
|
46
|
+
return hash(self._var)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def name(self) -> str:
|
|
50
|
+
return self._var.name
|
|
51
|
+
|
|
52
|
+
@overload
|
|
53
|
+
def get(self) -> T: ...
|
|
54
|
+
@overload
|
|
55
|
+
def get(self, default: T, /) -> T: ...
|
|
56
|
+
@overload
|
|
57
|
+
def get[D](self, default: D, /) -> D | T: ...
|
|
58
|
+
def get(self, default: Any = MISSING) -> T:
|
|
59
|
+
if default is MISSING:
|
|
60
|
+
return self._var.get()
|
|
61
|
+
return self._var.get(default)
|
|
62
|
+
|
|
63
|
+
def set(self, value: T) -> contextvars.Token[T]:
|
|
64
|
+
return self._var.set(value)
|
|
65
|
+
|
|
66
|
+
def reset(self, token: contextvars.Token[T]) -> None:
|
|
67
|
+
self._var.reset(token)
|
|
68
|
+
|
|
69
|
+
def load_env(self) -> None:
|
|
70
|
+
if self.env is None:
|
|
71
|
+
return
|
|
72
|
+
value: str | None = os.getenv(self.env)
|
|
73
|
+
if value is None:
|
|
74
|
+
return
|
|
75
|
+
if self.converter is None:
|
|
76
|
+
self.set(cast("T", value))
|
|
77
|
+
else:
|
|
78
|
+
self.set(self.converter(value))
|
|
79
|
+
|
|
80
|
+
@contextlib.contextmanager
|
|
81
|
+
def override(self, value: T) -> Generator[None]:
|
|
82
|
+
token: contextvars.Token[T] = self._var.set(value)
|
|
83
|
+
try:
|
|
84
|
+
yield
|
|
85
|
+
finally:
|
|
86
|
+
self._var.reset(token)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# ref: <https://github.com/ofek/hatch-vcs>
|
|
2
|
+
# ref: <https://github.com/pypa/setuptools-scm/blob/58ed7d5dec0e3bc80190f73543cf4d8a6876ba8e/src/setuptools_scm/_integration/dump_version.py#L16-L51>
|
|
3
|
+
__commit_id__: str | None
|
|
4
|
+
__version__: str
|
|
5
|
+
__version_tuple__: tuple[int | str, ...]
|
|
6
|
+
commit_id: str | None
|
|
7
|
+
version_tuple: tuple[int | str, ...]
|
|
8
|
+
version: str
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ._misc import identity
|
|
2
|
+
from ._pydantic import (
|
|
3
|
+
pydantic_model_validate,
|
|
4
|
+
pydantic_model_validate_json,
|
|
5
|
+
pydantic_model_validate_strings,
|
|
6
|
+
pydantic_type_adapter_validate_json,
|
|
7
|
+
pydantic_type_adapter_validate_python,
|
|
8
|
+
pydantic_type_adapter_validate_strings,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"identity",
|
|
13
|
+
"pydantic_model_validate",
|
|
14
|
+
"pydantic_model_validate_json",
|
|
15
|
+
"pydantic_model_validate_strings",
|
|
16
|
+
"pydantic_type_adapter_validate_json",
|
|
17
|
+
"pydantic_type_adapter_validate_python",
|
|
18
|
+
"pydantic_type_adapter_validate_strings",
|
|
19
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from liblaf.conf._field import Converter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pydantic_model_validate[T: pydantic.BaseModel](model: type[T]) -> Converter[T]:
|
|
12
|
+
return model.model_validate
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def pydantic_model_validate_json[T: pydantic.BaseModel](model: type[T]) -> Converter[T]:
|
|
16
|
+
return model.model_validate_json
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def pydantic_model_validate_strings[T: pydantic.BaseModel](
|
|
20
|
+
model: type[T],
|
|
21
|
+
) -> Converter[T]:
|
|
22
|
+
return model.model_validate_strings
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def pydantic_type_adapter_validate_json[T](type_: type[T]) -> Converter[T]:
|
|
26
|
+
return pydantic.TypeAdapter(type_).validate_json
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def pydantic_type_adapter_validate_python[T](type_: type[T]) -> Converter[T]:
|
|
30
|
+
return pydantic.TypeAdapter(type_).validate_python
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def pydantic_type_adapter_validate_strings[T](type_: type[T]) -> Converter[T]:
|
|
34
|
+
return pydantic.TypeAdapter(type_).validate_strings
|
|
File without changes
|