configaroo 0.3.0__py3-none-any.whl → 0.4.1__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 CHANGED
@@ -15,4 +15,4 @@ __all__ = [
15
15
  "print_configuration",
16
16
  ]
17
17
 
18
- __version__ = "0.3.0"
18
+ __version__ = "0.4.1"
@@ -6,6 +6,7 @@ import re
6
6
  from collections import UserDict
7
7
  from collections.abc import Callable
8
8
  from pathlib import Path
9
+ from types import UnionType
9
10
  from typing import Any, Self, TypeVar
10
11
 
11
12
  from pydantic import BaseModel
@@ -36,33 +37,20 @@ class Configuration(UserDict[str, Any]):
36
37
  def from_file(
37
38
  cls,
38
39
  file_path: str | Path,
40
+ *,
39
41
  loader: str | None = None,
40
- envs: dict[str, str] | None = None,
41
- env_prefix: str = "",
42
- extra_dynamic: dict[str, Any] | None = None,
42
+ not_exist_ok: bool = False,
43
43
  ) -> Self:
44
- """Read a Configuration from a file."""
45
- config_dict = loaders.from_file(file_path, loader=loader)
46
- return cls(config_dict).initialize(
47
- envs=envs, env_prefix=env_prefix, extra_dynamic=extra_dynamic
48
- )
44
+ """Read a Configuration from a file.
49
45
 
50
- def initialize(
51
- self,
52
- envs: dict[str, str] | None = None,
53
- env_prefix: str = "",
54
- extra_dynamic: dict[str, Any] | None = None,
55
- ) -> Self:
56
- """Initialize a configuration.
57
-
58
- The initialization adds environment variables and parses dynamic values.
46
+ If not_exist_ok is True, then a missing file returns an empty
47
+ configuration. This may be useful if the configuration is potentially
48
+ populated by environment variables.
59
49
  """
60
- self = self if envs is None else self.add_envs(envs, prefix=env_prefix) # noqa: PLW0642
61
- return self.parse_dynamic(extra_dynamic)
62
-
63
- def with_model(self, model: type[ModelT]) -> ModelT:
64
- """Apply a pydantic model to a configuration."""
65
- return self.validate_model(model).convert_model(model)
50
+ config_dict = loaders.from_file(
51
+ file_path, loader=loader, not_exist_ok=not_exist_ok
52
+ )
53
+ return cls(config_dict)
66
54
 
67
55
  def __getitem__(self, key: str) -> Any: # noqa: ANN401
68
56
  """Make sure nested sections have type Configuration."""
@@ -112,30 +100,6 @@ class Configuration(UserDict[str, Any]):
112
100
  cls = type(self)
113
101
  return self | {prefix: cls(self.setdefault(prefix, {})).add(rest, value)}
114
102
 
115
- def add_envs(self, envs: dict[str, str] | None = None, prefix: str = "") -> Self:
116
- """Add environment variables to configuration.
117
-
118
- If you don't specify which environment variables to read, you'll
119
- automatically add any that matches a top-level value of the
120
- configuration.
121
- """
122
- if envs is None:
123
- # Automatically add top-level configuration items
124
- envs = {
125
- re.sub(r"\W", "_", key).upper(): key
126
- for key, value in self.data.items()
127
- if isinstance(value, str | int | float)
128
- }
129
-
130
- # Read environment variables
131
- for env, key in envs.items():
132
- env_key = f"{prefix}{env}"
133
- if env_value := os.getenv(env_key):
134
- self = self.add(key, env_value) # noqa: PLW0642
135
- elif key not in self:
136
- raise MissingEnvironmentVariableError(env_key)
137
- return self
138
-
139
103
  def parse_dynamic(
140
104
  self, extra: dict[str, Any] | None = None, *, _include_self: bool = True
141
105
  ) -> Self:
@@ -163,6 +127,60 @@ class Configuration(UserDict[str, Any]):
163
127
  # Continue parsing until no more replacements are made.
164
128
  return parsed.parse_dynamic(extra=extra, _include_self=_include_self)
165
129
 
130
+ def add_envs(self, envs: dict[str, str] | None = None, prefix: str = "") -> Self:
131
+ """Add environment variables to configuration.
132
+
133
+ If you don't specify which environment variables to read, you'll
134
+ automatically add any that matches a simple top-level value of the
135
+ configuration.
136
+ """
137
+ if envs is None:
138
+ # Automatically add top-level configuration items
139
+ envs = {
140
+ re.sub(r"\W", "_", key).upper(): key
141
+ for key, value in self.data.items()
142
+ if isinstance(value, str | int | float)
143
+ }
144
+
145
+ # Read environment variables
146
+ for env, key in envs.items():
147
+ env_key = f"{prefix}{env}"
148
+ if env_value := os.getenv(env_key):
149
+ self = self.add(key, env_value) # noqa: PLW0642
150
+ elif key not in self:
151
+ raise MissingEnvironmentVariableError(env_key)
152
+ return self
153
+
154
+ def add_envs_from_model(
155
+ self,
156
+ model: type[BaseModel],
157
+ prefix: str = "",
158
+ types: type | UnionType = str | bool | int | float,
159
+ ) -> Self:
160
+ """Add environment variables to configuration based on the given model.
161
+
162
+ Top level string, bool, integer, and float fields from the model are
163
+ looked for among environment variables.
164
+ """
165
+
166
+ def _get_class_from_annotation(annotation: type) -> type:
167
+ """Unpack generic annotations and return the underlying class."""
168
+ return (
169
+ _get_class_from_annotation(annotation.__origin__)
170
+ if hasattr(annotation, "__origin__")
171
+ else annotation
172
+ )
173
+
174
+ envs = {
175
+ re.sub(r"\W", "_", key).upper(): key
176
+ for key, field in model.model_fields.items()
177
+ if (
178
+ field.annotation is not None
179
+ and issubclass(_get_class_from_annotation(field.annotation), types)
180
+ )
181
+ }
182
+ return self.add_envs(envs, prefix=prefix)
183
+
166
184
  def validate_model(self, model: type[BaseModel]) -> Self:
167
185
  """Validate the configuration against the given model."""
168
186
  model.model_validate(self.data)
@@ -172,6 +190,10 @@ class Configuration(UserDict[str, Any]):
172
190
  """Convert data types to match the given model."""
173
191
  return model(**self.data)
174
192
 
193
+ def with_model(self, model: type[ModelT]) -> ModelT:
194
+ """Apply a pydantic model to a configuration."""
195
+ return self.validate_model(model).convert_model(model)
196
+
175
197
  def to_dict(self) -> dict[str, Any]:
176
198
  """Dump the configuration into a Python dictionary."""
177
199
  return {
@@ -217,9 +239,7 @@ def _get_rich_print() -> Callable[[str], None]: # pragma: no cover
217
239
 
218
240
  return Console().print
219
241
  except ImportError:
220
- import builtins # noqa: PLC0415
221
-
222
- return builtins.print
242
+ return print
223
243
 
224
244
 
225
245
  def _print_dict_as_tree(
@@ -26,9 +26,14 @@ def loader_names() -> list[str]:
26
26
  return sorted(pyplugs.names(PACKAGE))
27
27
 
28
28
 
29
- def from_file(path: str | Path, loader: str | None = None) -> dict[str, Any]:
29
+ def from_file(
30
+ path: str | Path, *, loader: str | None = None, not_exist_ok: bool = False
31
+ ) -> dict[str, Any]:
30
32
  """Load a file using a loader defined by the suffix if necessary."""
31
33
  path = Path(path)
34
+ if not path.exists() and not_exist_ok:
35
+ return {}
36
+
32
37
  loader = path.suffix.lstrip(".") if loader is None else loader
33
38
  try:
34
39
  return load(loader, path=path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: configaroo
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Bouncy handling of configuration files
5
5
  Author-email: Geir Arne Hjelle <geirarne@gmail.com>
6
6
  Maintainer-email: Geir Arne Hjelle <geirarne@gmail.com>
@@ -0,0 +1,12 @@
1
+ configaroo/__init__.py,sha256=yTD0mUdvhepcJSiLEBKTPDuQh8BWjrKnTB1QSfmjHRw,412
2
+ configaroo/configuration.py,sha256=1tGBLkyz703txcaGx49kWHMiVYgjTcERnikVQ69Pusc,10928
3
+ configaroo/exceptions.py,sha256=GfLf3CLfHStiQjvdS7ZAtrKF9gmGL_8biFLayue6J0M,772
4
+ configaroo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ configaroo/loaders/__init__.py,sha256=XQrFwCMWzQ71ykaZFPmYysDz12y_elPqxWhUMQCsq3s,1076
6
+ configaroo/loaders/json.py,sha256=fT2Lg4hPM2BuwqrDsP7bcJlepAdmEh2iKU-YVK4KmIA,306
7
+ configaroo/loaders/toml.py,sha256=jw9U78Lf-GMA8QmGIM8xMBqOhPaa8ITSMAhhN1ZNyng,256
8
+ configaroo-0.4.1.dist-info/licenses/LICENSE,sha256=rdeT6Y5bm0MUaERso7HRWpPj37Y1RD5li2lIQaMNJjc,1090
9
+ configaroo-0.4.1.dist-info/METADATA,sha256=U_sK60hkeYLqI5gEjv6UoG1l1r3w3pKhhADB4kfXqDQ,2672
10
+ configaroo-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ configaroo-0.4.1.dist-info/top_level.txt,sha256=JVYICl1cWSjvSOZuZMYm976z9lnZaWtHVRSt373QCxg,11
12
+ configaroo-0.4.1.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- configaroo/__init__.py,sha256=yNF3fDQx9xXJEXHtJmeYMdqy-Dnk0mHz2-EL590A858,412
2
- configaroo/configuration.py,sha256=phBUk_hHRFQb4vCcn6WHfNOnovEmdV6hnotRJkPSz2E,10255
3
- configaroo/exceptions.py,sha256=GfLf3CLfHStiQjvdS7ZAtrKF9gmGL_8biFLayue6J0M,772
4
- configaroo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- configaroo/loaders/__init__.py,sha256=l2pHeD9PJ3ZQA5xUCq9nfFqw2YhAHLeTe50wvhMmTYA,977
6
- configaroo/loaders/json.py,sha256=fT2Lg4hPM2BuwqrDsP7bcJlepAdmEh2iKU-YVK4KmIA,306
7
- configaroo/loaders/toml.py,sha256=jw9U78Lf-GMA8QmGIM8xMBqOhPaa8ITSMAhhN1ZNyng,256
8
- configaroo-0.3.0.dist-info/licenses/LICENSE,sha256=rdeT6Y5bm0MUaERso7HRWpPj37Y1RD5li2lIQaMNJjc,1090
9
- configaroo-0.3.0.dist-info/METADATA,sha256=spH6HtB45ksEBsjuCJIADFjWabsQQfzTPZvCuMfV8Fk,2672
10
- configaroo-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- configaroo-0.3.0.dist-info/top_level.txt,sha256=JVYICl1cWSjvSOZuZMYm976z9lnZaWtHVRSt373QCxg,11
12
- configaroo-0.3.0.dist-info/RECORD,,