configaroo 0.1.0__py3-none-any.whl

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.
configaroo/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Bouncy configuration handling"""
2
+
3
+ from configaroo.configuration import Configuration # noqa
4
+ from configaroo.exceptions import ( # noqa
5
+ ConfigarooException,
6
+ MissingEnvironmentVariableError,
7
+ UnsupportedLoaderError,
8
+ )
@@ -0,0 +1,168 @@
1
+ """A dict-like configuration with support for envvars, validation and type conversion"""
2
+
3
+ import os
4
+ from collections import UserDict
5
+ from pathlib import Path
6
+ from typing import Any, Self, Type
7
+
8
+ import dotenv
9
+ from pydantic import BaseModel
10
+
11
+ from configaroo import loaders
12
+ from configaroo.exceptions import MissingEnvironmentVariableError
13
+
14
+
15
+ class Configuration(UserDict):
16
+ """A Configuration is a dict-like structure with some conveniences"""
17
+
18
+ @classmethod
19
+ def from_file(
20
+ cls,
21
+ file_path: str | Path,
22
+ loader: str | None = None,
23
+ envs: dict[str, str] | None = None,
24
+ env_prefix: str = "",
25
+ extra_dynamic: dict[str, Any] | None = None,
26
+ model: Type[BaseModel] | None = None,
27
+ ) -> Self:
28
+ """Read a Configuration from a file"""
29
+ config_dict = loaders.from_file(file_path, loader=loader)
30
+ return cls(**config_dict).initialize(envs=envs, model=model)
31
+
32
+ def initialize(
33
+ self,
34
+ envs: dict[str, str] | None = None,
35
+ env_prefix: str = "",
36
+ extra_dynamic: dict[str, Any] | None = None,
37
+ model: Type[BaseModel] | None = None,
38
+ ) -> Self:
39
+ """Initialize a configuration.
40
+
41
+ The initialization adds environment variables, parses dynamic values,
42
+ validates against a Pydantic model, and converts value types using the
43
+ same model.
44
+ """
45
+ self = self if envs is None else self.add_envs(envs, prefix=env_prefix)
46
+ self = self.parse_dynamic(extra_dynamic)
47
+ self = self if model is None else self.validate(model).convert(model)
48
+ return self
49
+
50
+ def __getitem__(self, key: str) -> Any:
51
+ """Make sure nested sections have type Configuration"""
52
+ value = self.data[key]
53
+ if isinstance(value, dict | UserDict | Configuration):
54
+ return Configuration(**value)
55
+ else:
56
+ return value
57
+
58
+ def __getattr__(self, key: str) -> Any:
59
+ """Create attribute access for config keys for convenience"""
60
+ try:
61
+ return self[key]
62
+ except KeyError:
63
+ raise AttributeError(
64
+ f"'{type(self).__name__}' has no attribute or key '{key}'"
65
+ )
66
+
67
+ def __contains__(self, key: str) -> bool:
68
+ """Add support for dotted keys"""
69
+ if key in self.data:
70
+ return True
71
+ prefix, _, rest = key.partition(".")
72
+ try:
73
+ return rest in self[prefix]
74
+ except KeyError:
75
+ return False
76
+
77
+ def get(self, key: str, default: Any = None) -> Any:
78
+ """Allow dotted keys when using .get()"""
79
+ if key not in self.data:
80
+ prefix, _, rest = key.partition(".")
81
+ try:
82
+ return self[prefix].get(rest, default=default)
83
+ except KeyError:
84
+ return default
85
+ else:
86
+ return self[key]
87
+
88
+ def add(self, key: str, value: Any) -> Self:
89
+ """Add a value, allow dotted keys"""
90
+ prefix, _, rest = key.partition(".")
91
+ if rest:
92
+ cls = type(self)
93
+ return self | {prefix: cls(**self.setdefault(prefix, {})).add(rest, value)}
94
+ else:
95
+ return self | {key: value}
96
+
97
+ def add_envs(self, envs: dict[str, str], prefix: str = "", use_dotenv=True) -> Self:
98
+ """Add environment variables to configuration"""
99
+ if use_dotenv:
100
+ dotenv.load_dotenv()
101
+
102
+ for env, key in envs.items():
103
+ env_key = f"{prefix}{env}"
104
+ env_value = os.getenv(env_key)
105
+ if env_value:
106
+ self = self.add(key, env_value)
107
+ else:
108
+ if key not in self:
109
+ raise MissingEnvironmentVariableError(
110
+ f"required environment variable '{env_key}' not found"
111
+ )
112
+ return self
113
+
114
+ def parse_dynamic(self, extra: dict[str, Any] | None = None) -> Self:
115
+ """Parse dynamic values of the form {section.key}"""
116
+ cls = type(self)
117
+ variables = (
118
+ self.to_flat_dict()
119
+ | {"project_path": Path(__file__).parent.parent.parent}
120
+ | ({} if extra is None else extra)
121
+ )
122
+ return cls(
123
+ **{
124
+ key: (
125
+ value.parse_dynamic(extra=variables)
126
+ if isinstance(value, Configuration)
127
+ else value.format(**variables)
128
+ if isinstance(value, str)
129
+ else value
130
+ )
131
+ for key, value in self.items()
132
+ }
133
+ )
134
+
135
+ def validate(self, model: Type[BaseModel]) -> Self:
136
+ """Validate the configuration against the given model."""
137
+ model.model_validate(self.data)
138
+ return self
139
+
140
+ def convert(self, model: Type[BaseModel]) -> Self:
141
+ """Convert data types to match the given model"""
142
+ cls = type(self)
143
+ return cls(**model(**self.data).model_dump())
144
+
145
+ def to_dict(self) -> dict[str, Any]:
146
+ """Dump the configuration into a Python dictionary"""
147
+ return {
148
+ key: value.to_dict() if isinstance(value, Configuration) else value
149
+ for key, value in self.items()
150
+ }
151
+
152
+ def to_flat_dict(self, _prefix: str = "") -> dict[str, Any]:
153
+ """Dump the configuration into a flat dictionary.
154
+
155
+ Nested configurations are converted into dotted keys.
156
+ """
157
+ return {
158
+ f"{_prefix}{key}": value
159
+ for key, value in self.items()
160
+ if not isinstance(value, Configuration)
161
+ } | {
162
+ key: value
163
+ for nested_key, nested_value in self.items()
164
+ if isinstance(nested_value, Configuration)
165
+ for key, value in (
166
+ self[nested_key].to_flat_dict(_prefix=f"{_prefix}{nested_key}.").items()
167
+ )
168
+ }
@@ -0,0 +1,13 @@
1
+ """Configaroo specific exceptions"""
2
+
3
+
4
+ class ConfigarooException(Exception):
5
+ """Base exception for more specific Configaroo exceptions"""
6
+
7
+
8
+ class MissingEnvironmentVariableError(ConfigarooException, KeyError):
9
+ """A required environment variable is missing"""
10
+
11
+
12
+ class UnsupportedLoaderError(ConfigarooException, ValueError):
13
+ """An unsupported loader is called"""
@@ -0,0 +1,24 @@
1
+ """Loaders that read configuration files."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import pyplugs
7
+
8
+ from configaroo.exceptions import UnsupportedLoaderError
9
+
10
+ load = pyplugs.call_factory(__package__)
11
+ loader_names = pyplugs.names_factory(__package__)
12
+
13
+
14
+ def from_file(path: str | Path, loader: str | None = None) -> dict[str, Any]:
15
+ """Load a file using a loader defined by the suffix if necessary."""
16
+ path = Path(path)
17
+ loader = path.suffix.lstrip(".") if loader is None else loader
18
+ try:
19
+ return load(loader, path=path)
20
+ except pyplugs.UnknownPluginError:
21
+ raise UnsupportedLoaderError(
22
+ f"file type '{loader}' isn't supported. "
23
+ f"Use one of: {', '.join(loader_names())}"
24
+ ) from None
@@ -0,0 +1,13 @@
1
+ """Loader for JSON-files"""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import pyplugs
8
+
9
+
10
+ @pyplugs.register
11
+ def load_json_file(path: Path) -> dict[str, Any]:
12
+ """Read a JSON-file"""
13
+ return json.loads(path.read_text(encoding="utf-8"))
@@ -0,0 +1,13 @@
1
+ """Loader for TOML-files"""
2
+
3
+ import tomllib
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import pyplugs
8
+
9
+
10
+ @pyplugs.register
11
+ def load_toml_file(path: Path) -> dict[str, Any]:
12
+ """Read a TOML-file"""
13
+ return tomllib.loads(path.read_text(encoding="utf-8"))
configaroo/py.typed ADDED
File without changes
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: configaroo
3
+ Version: 0.1.0
4
+ Summary: Bouncy handling of configuration files
5
+ Author-email: Geir Arne Hjelle <geirarne@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Dist: pyplugs>=0.4.0
10
+ Requires-Dist: python-dotenv>=1.1.0
11
+
12
+ # Configaroo - Bouncy Configuration Handling
13
+
14
+ Configaroo is a light configuration package for Python that offers the following features:
15
+
16
+ - Access configuration settings with dotted keys: `config.nested.key`
17
+ - Use different configuration file formats, including TOML and JSON
18
+ - Override key configuration settings with environment variables
19
+ - Validate a configuration based on a Pydantic model
20
+ - Convert the type of configuration values based on a Pydantic model
21
+ - Dynamically format certain configuration values
@@ -0,0 +1,11 @@
1
+ configaroo/__init__.py,sha256=10BHeYLHiIz-oAGM1IdfaqBKyHbn2hiNnjS_zrFOtC8,232
2
+ configaroo/configuration.py,sha256=V1Dx9LdkRlUSq46Djdq12sWb9JfNi_8N4VBnkXdB11s,5821
3
+ configaroo/exceptions.py,sha256=1h-6hV7VqqKaRFnu539pjsO0ESSNDibt2kUeF7jAM2M,374
4
+ configaroo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ configaroo/loaders/__init__.py,sha256=-6GAR4g7vYW7Zagq9Ev479kpkfY7kbvDl4JPXd4wQQ0,756
6
+ configaroo/loaders/json.py,sha256=6gs_ZegUEe70vb6uSJfKBos7UjfuJnyWgPE_kjDQISw,258
7
+ configaroo/loaders/toml.py,sha256=ZVbHutZ7V-Z_jf2aHjV18jwaqCXumlr06j48rojHcBM,264
8
+ configaroo-0.1.0.dist-info/METADATA,sha256=Oy67b4ut26KSC43oh5g50fpjR_Mz5chJ0iqvo4nrWIo,827
9
+ configaroo-0.1.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
10
+ configaroo-0.1.0.dist-info/top_level.txt,sha256=JVYICl1cWSjvSOZuZMYm976z9lnZaWtHVRSt373QCxg,11
11
+ configaroo-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ configaroo