conveoconfi 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.
- conveoconfi/__init__.py +21 -0
- conveoconfi/config_files.py +192 -0
- conveoconfi-0.1.0.dist-info/METADATA +118 -0
- conveoconfi-0.1.0.dist-info/RECORD +5 -0
- conveoconfi-0.1.0.dist-info/WHEEL +4 -0
conveoconfi/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Public compatibility entrypoint for conveoconfi."""
|
|
2
|
+
|
|
3
|
+
from .config_files import (
|
|
4
|
+
append_config_file,
|
|
5
|
+
complete_config_file,
|
|
6
|
+
config_file_exists,
|
|
7
|
+
config_file_path,
|
|
8
|
+
create_and_read_config_file,
|
|
9
|
+
get_param,
|
|
10
|
+
overwrite_config_file,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"append_config_file",
|
|
15
|
+
"complete_config_file",
|
|
16
|
+
"config_file_exists",
|
|
17
|
+
"config_file_path",
|
|
18
|
+
"create_and_read_config_file",
|
|
19
|
+
"get_param",
|
|
20
|
+
"overwrite_config_file",
|
|
21
|
+
]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Legacy-compatible config file API surface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _not_implemented(function_name: str) -> None:
|
|
12
|
+
raise NotImplementedError(f"{function_name} is not implemented yet")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _read_yaml_file(path: Path) -> Any:
|
|
16
|
+
with path.open("r", encoding="utf-8") as file:
|
|
17
|
+
return yaml.safe_load(file)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _write_yaml_file(path: Path, data: Any) -> None:
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
with path.open("w", encoding="utf-8") as file:
|
|
23
|
+
yaml.safe_dump(data, file, sort_keys=False, allow_unicode=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _default_template_path(
|
|
27
|
+
file_name: str,
|
|
28
|
+
default_files_dir: str | Path | None = None,
|
|
29
|
+
default_template_dir: str | Path | None = None,
|
|
30
|
+
) -> Path:
|
|
31
|
+
if default_files_dir is not None and default_template_dir is not None:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"Pass only one of default_files_dir or default_template_dir, not both"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
template_dir = default_files_dir if default_files_dir is not None else default_template_dir
|
|
37
|
+
if template_dir is None:
|
|
38
|
+
raise FileNotFoundError(
|
|
39
|
+
"A default template directory is required. Pass default_files_dir "
|
|
40
|
+
"or default_template_dir."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
template_path = Path(template_dir).expanduser() / file_name
|
|
44
|
+
if not template_path.is_file():
|
|
45
|
+
raise FileNotFoundError(
|
|
46
|
+
f"Default template for {file_name!r} was not found at {template_path}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return template_path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read_default_template(
|
|
53
|
+
file_name: str,
|
|
54
|
+
default_files_dir: str | Path | None = None,
|
|
55
|
+
default_template_dir: str | Path | None = None,
|
|
56
|
+
) -> Any:
|
|
57
|
+
template_path = _default_template_path(
|
|
58
|
+
file_name,
|
|
59
|
+
default_files_dir=default_files_dir,
|
|
60
|
+
default_template_dir=default_template_dir,
|
|
61
|
+
)
|
|
62
|
+
data = _read_yaml_file(template_path)
|
|
63
|
+
if data is None:
|
|
64
|
+
raise ValueError(f"Default template {template_path} is empty")
|
|
65
|
+
return data
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _complete_config_data(current_data: Any, default_data: Any) -> Any:
|
|
69
|
+
if not isinstance(current_data, dict) or not isinstance(default_data, dict):
|
|
70
|
+
return current_data
|
|
71
|
+
|
|
72
|
+
completed_data = dict(current_data)
|
|
73
|
+
for key, default_value in default_data.items():
|
|
74
|
+
if key not in completed_data:
|
|
75
|
+
completed_data[key] = default_value
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
current_value = completed_data[key]
|
|
79
|
+
if isinstance(current_value, dict) and isinstance(default_value, dict):
|
|
80
|
+
completed_data[key] = _complete_config_data(current_value, default_value)
|
|
81
|
+
|
|
82
|
+
return completed_data
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def create_and_read_config_file(
|
|
86
|
+
file_name: str,
|
|
87
|
+
default_app_dir: str | Path,
|
|
88
|
+
force_default: bool = False,
|
|
89
|
+
complete_file: bool = True,
|
|
90
|
+
default_files_dir: str | Path | None = None,
|
|
91
|
+
default_template_dir: str | Path | None = None,
|
|
92
|
+
):
|
|
93
|
+
"""Create a missing config file from a YAML template and return parsed data."""
|
|
94
|
+
path = config_file_path(file_name, default_app_dir)
|
|
95
|
+
if force_default or not path.exists():
|
|
96
|
+
data = _read_default_template(
|
|
97
|
+
file_name,
|
|
98
|
+
default_files_dir=default_files_dir,
|
|
99
|
+
default_template_dir=default_template_dir,
|
|
100
|
+
)
|
|
101
|
+
_write_yaml_file(path, data)
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
data = _read_yaml_file(path)
|
|
105
|
+
if data is None:
|
|
106
|
+
data = _read_default_template(
|
|
107
|
+
file_name,
|
|
108
|
+
default_files_dir=default_files_dir,
|
|
109
|
+
default_template_dir=default_template_dir,
|
|
110
|
+
)
|
|
111
|
+
_write_yaml_file(path, data)
|
|
112
|
+
elif complete_file:
|
|
113
|
+
data = complete_config_file(
|
|
114
|
+
file_name,
|
|
115
|
+
default_app_dir,
|
|
116
|
+
default_files_dir=default_files_dir,
|
|
117
|
+
default_template_dir=default_template_dir,
|
|
118
|
+
)
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def complete_config_file(
|
|
123
|
+
file_name: str,
|
|
124
|
+
default_app_dir: str | Path,
|
|
125
|
+
default_files_dir: str | Path | None = None,
|
|
126
|
+
default_template_dir: str | Path | None = None,
|
|
127
|
+
):
|
|
128
|
+
"""Complete a config file with missing values from its default template."""
|
|
129
|
+
path = config_file_path(file_name, default_app_dir)
|
|
130
|
+
data = _read_yaml_file(path)
|
|
131
|
+
default_data = _read_default_template(
|
|
132
|
+
file_name,
|
|
133
|
+
default_files_dir=default_files_dir,
|
|
134
|
+
default_template_dir=default_template_dir,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if data is None:
|
|
138
|
+
_write_yaml_file(path, default_data)
|
|
139
|
+
return default_data
|
|
140
|
+
|
|
141
|
+
completed_data = _complete_config_data(data, default_data)
|
|
142
|
+
if completed_data != data:
|
|
143
|
+
_write_yaml_file(path, completed_data)
|
|
144
|
+
return completed_data
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def overwrite_config_file(file_name: str, default_app_dir: str | Path, data: Any) -> None:
|
|
148
|
+
"""Overwrite a config file with YAML data."""
|
|
149
|
+
_write_yaml_file(config_file_path(file_name, default_app_dir), data)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def append_config_file(file_name: str, default_app_dir: str | Path, data: Any) -> Any:
|
|
153
|
+
"""Append YAML data to a config file, then rewrite normalized YAML."""
|
|
154
|
+
path = config_file_path(file_name, default_app_dir)
|
|
155
|
+
with path.open("a", encoding="utf-8") as file:
|
|
156
|
+
yaml.safe_dump(data, file, sort_keys=False, allow_unicode=True)
|
|
157
|
+
|
|
158
|
+
normalized_data = _read_yaml_file(path)
|
|
159
|
+
_write_yaml_file(path, normalized_data)
|
|
160
|
+
return normalized_data
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_param(
|
|
164
|
+
parent_param: str,
|
|
165
|
+
param: str,
|
|
166
|
+
default_app_dir: str | Path,
|
|
167
|
+
default_files_dir: str | Path | None = None,
|
|
168
|
+
default_template_dir: str | Path | None = None,
|
|
169
|
+
):
|
|
170
|
+
"""Read a child parameter from the root config.yaml file."""
|
|
171
|
+
data = create_and_read_config_file(
|
|
172
|
+
"config.yaml",
|
|
173
|
+
default_app_dir,
|
|
174
|
+
default_files_dir=default_files_dir,
|
|
175
|
+
default_template_dir=default_template_dir,
|
|
176
|
+
)
|
|
177
|
+
try:
|
|
178
|
+
return data[parent_param][param]
|
|
179
|
+
except (KeyError, TypeError):
|
|
180
|
+
raise Exception(
|
|
181
|
+
f"Parameter {param!r} was not found in {parent_param!r}"
|
|
182
|
+
) from None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def config_file_exists(file_name: str, default_app_dir: str | Path) -> bool:
|
|
186
|
+
return config_file_path(file_name, default_app_dir).is_file()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def config_file_path(file_name: str, default_app_dir: str | Path) -> Path:
|
|
190
|
+
app_dir = Path(default_app_dir).expanduser()
|
|
191
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
return app_dir / file_name
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: conveoconfi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable YAML template-backed configuration file helpers.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: pyyaml>=6.0
|
|
7
|
+
Provides-Extra: test
|
|
8
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# conveoconfi
|
|
12
|
+
|
|
13
|
+
Reusable YAML template-backed configuration helpers for Python applications.
|
|
14
|
+
|
|
15
|
+
`conveoconfi` provides a small function-based API for convention-over-
|
|
16
|
+
configuration behavior:
|
|
17
|
+
|
|
18
|
+
- create missing application config directories
|
|
19
|
+
- create missing YAML config files from application-owned default templates
|
|
20
|
+
- complete existing YAML files with newly added defaults
|
|
21
|
+
- preserve existing user-provided values and extra keys
|
|
22
|
+
- read child parameters from `config.yaml`
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install the package with pip, or declare it in your project's dependency
|
|
27
|
+
metadata.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install conveoconfi
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`conveoconfi` declares `PyYAML` as a runtime dependency, so applications do not
|
|
34
|
+
need to add a separate YAML dependency for these helpers.
|
|
35
|
+
|
|
36
|
+
## Default Templates
|
|
37
|
+
|
|
38
|
+
Applications keep their own YAML default templates, such as `config.yaml`,
|
|
39
|
+
`logging.yaml`, or `feature_flags.yaml`. Pass that template directory explicitly
|
|
40
|
+
when reading or completing config files.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
from conveoconfi import create_and_read_config_file
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
APP_DIR = Path.home() / ".myapp"
|
|
49
|
+
DEFAULT_FILES_DIR = Path(__file__).parent / "default_files"
|
|
50
|
+
|
|
51
|
+
config = create_and_read_config_file(
|
|
52
|
+
"config.yaml",
|
|
53
|
+
APP_DIR,
|
|
54
|
+
default_files_dir=DEFAULT_FILES_DIR,
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The equivalent keyword `default_template_dir` is also supported. Pass only one
|
|
59
|
+
of `default_files_dir` or `default_template_dir`.
|
|
60
|
+
|
|
61
|
+
Template lookup failures are explicit. If a default directory is omitted, or the
|
|
62
|
+
requested template file is missing, `conveoconfi` raises a `FileNotFoundError`
|
|
63
|
+
that names the lookup problem.
|
|
64
|
+
|
|
65
|
+
## Template Discovery Decision
|
|
66
|
+
|
|
67
|
+
The public API requires applications to pass their template directory
|
|
68
|
+
explicitly with `default_files_dir` or `default_template_dir`. `conveoconfi`
|
|
69
|
+
does not search for templates in its own package directory because those files
|
|
70
|
+
belong to the consuming application, not to this reusable dependency.
|
|
71
|
+
|
|
72
|
+
Explicit template paths keep behavior predictable in tests, work with any
|
|
73
|
+
project layout, and fail with a direct `FileNotFoundError` when templates are
|
|
74
|
+
not configured. Projects that want a shorter call site can wrap `conveoconfi`
|
|
75
|
+
once in their own code and bind the app's template path there.
|
|
76
|
+
|
|
77
|
+
## Public API
|
|
78
|
+
|
|
79
|
+
The public API exposes these function names from the package root:
|
|
80
|
+
|
|
81
|
+
- `create_and_read_config_file(file_name, default_app_dir, force_default=False, complete_file=True, default_files_dir=None, default_template_dir=None)`
|
|
82
|
+
- `complete_config_file(file_name, default_app_dir, default_files_dir=None, default_template_dir=None)`
|
|
83
|
+
- `overwrite_config_file(file_name, default_app_dir, data)`
|
|
84
|
+
- `append_config_file(file_name, default_app_dir, data)`
|
|
85
|
+
- `get_param(parent_param, param, default_app_dir, default_files_dir=None, default_template_dir=None)`
|
|
86
|
+
- `config_file_exists(file_name, default_app_dir)`
|
|
87
|
+
- `config_file_path(file_name, default_app_dir)`
|
|
88
|
+
|
|
89
|
+
`config_file_path` creates the application config directory before returning the
|
|
90
|
+
path. `get_param` reads from `config.yaml`.
|
|
91
|
+
|
|
92
|
+
## Completion Behavior
|
|
93
|
+
|
|
94
|
+
By default, `create_and_read_config_file` completes existing config files from
|
|
95
|
+
the matching default template and writes the completed result back to disk.
|
|
96
|
+
Completion is recursive for dictionaries:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
# Existing user config
|
|
100
|
+
notifications:
|
|
101
|
+
email:
|
|
102
|
+
enabled: false
|
|
103
|
+
|
|
104
|
+
# Default template
|
|
105
|
+
notifications:
|
|
106
|
+
email:
|
|
107
|
+
enabled: true
|
|
108
|
+
sender: hello@example.com
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The completed file keeps the user's `enabled: false` value and adds only the
|
|
112
|
+
missing `sender` value. User-defined extra keys are preserved. If a
|
|
113
|
+
current value and default value disagree on shape, such as a scalar versus a
|
|
114
|
+
dictionary, the current user value is preserved.
|
|
115
|
+
|
|
116
|
+
Use `force_default=True` to deliberately replace an existing config file with
|
|
117
|
+
template defaults. Use `complete_file=False` to read an existing file without
|
|
118
|
+
mutating it.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
conveoconfi/__init__.py,sha256=7oz23y3d-zeHyTwhN60Jyuu4dESC4haP4p6V3iPbiFk,457
|
|
2
|
+
conveoconfi/config_files.py,sha256=C2NF-feI5Nlz7bXrfNR3xp5Cg9tVeVh1O1c08Ae3ZbY,6259
|
|
3
|
+
conveoconfi-0.1.0.dist-info/METADATA,sha256=mPRTkYediebZYxo8et99NjcpiNEcpSnxU19M89Zhz70,4113
|
|
4
|
+
conveoconfi-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
conveoconfi-0.1.0.dist-info/RECORD,,
|