the1conf 1.0.0__tar.gz → 1.3.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.
- the1conf-1.3.0/PKG-INFO +794 -0
- the1conf-1.3.0/README.md +769 -0
- {the1conf-1.0.0 → the1conf-1.3.0}/pyproject.toml +13 -8
- the1conf-1.3.0/python/main/the1conf/__init__.py +14 -0
- the1conf-1.3.0/python/main/the1conf/app_config.py +511 -0
- the1conf-1.3.0/python/main/the1conf/app_config_meta.py +202 -0
- {the1conf-1.0.0 → the1conf-1.3.0}/python/main/the1conf/attr_dict.py +38 -25
- the1conf-1.3.0/python/main/the1conf/auto_prolog.py +177 -0
- the1conf-1.3.0/python/main/the1conf/click_option.py +72 -0
- the1conf-1.3.0/python/main/the1conf/config_var.py +542 -0
- the1conf-1.3.0/python/main/the1conf/flags.py +143 -0
- the1conf-1.3.0/python/main/the1conf/solver.py +346 -0
- the1conf-1.3.0/python/main/the1conf/utils.py +110 -0
- the1conf-1.3.0/python/main/the1conf/var_substitution.py +59 -0
- the1conf-1.0.0/PKG-INFO +0 -516
- the1conf-1.0.0/README.md +0 -493
- the1conf-1.0.0/python/main/the1conf/__init__.py +0 -4
- the1conf-1.0.0/python/main/the1conf/app_config.py +0 -643
- the1conf-1.0.0/python/main/the1conf/click_option.py +0 -35
- the1conf-1.0.0/python/main/the1conf/config_var.py +0 -626
- {the1conf-1.0.0 → the1conf-1.3.0}/LICENSE +0 -0
- {the1conf-1.0.0 → the1conf-1.3.0}/python/main/the1conf/py.typed +0 -0
the1conf-1.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: the1conf
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: All in one configuration management tool for your python applications.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: configuration,settings,cli,click,pydantic
|
|
8
|
+
Author: Eric CHASTAN
|
|
9
|
+
Author-email: eric@chastan.consulting
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Dist: click (>=8.3.1)
|
|
18
|
+
Requires-Dist: jinja2 (>=3.1.6)
|
|
19
|
+
Requires-Dist: pydantic (>=2.12.5)
|
|
20
|
+
Requires-Dist: toml (>=0.10.2)
|
|
21
|
+
Project-URL: Issues, https://gitlab.com/eric-chastan/the1conf/-/issues
|
|
22
|
+
Project-URL: Repository, https://gitlab.com/eric-chastan/the1conf
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# TheOneConf
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
Define app configuration in plain Python classes.
|
|
33
|
+
|
|
34
|
+
**TheOneConf** lets Python developers declare configuration variables in a single line of **plain Python** (name, type, default, help text) and then resolve values from **CLI > env vars > config files > variable substitution > computed values > static defaults** — without writing any manual parsing code.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install the1conf
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# Setup: create a dummy yaml config file whose content is:
|
|
42
|
+
#
|
|
43
|
+
# host-dev: localhost
|
|
44
|
+
# host: myapp.dot.com
|
|
45
|
+
#
|
|
46
|
+
old_env = os.environ.get("XDG_CONFIG_HOME")
|
|
47
|
+
os.environ["XDG_CONFIG_HOME"] = str(tmp_path)
|
|
48
|
+
conf_file = Path(os.environ["XDG_CONFIG_HOME"]) / "myapp/conf.yaml"
|
|
49
|
+
conf_file.parent.mkdir(parents=True)
|
|
50
|
+
conf_file.write_text("""
|
|
51
|
+
host-dev: localhost
|
|
52
|
+
host: myapp.dot.com
|
|
53
|
+
port: 80
|
|
54
|
+
""")
|
|
55
|
+
|
|
56
|
+
class MyConfig(AppConfig):
|
|
57
|
+
host: str = configvar(
|
|
58
|
+
file_key=["host-{{exec_stage}}", "host"],
|
|
59
|
+
)
|
|
60
|
+
"""Server host"""
|
|
61
|
+
|
|
62
|
+
port: int = configvar(default=8080)
|
|
63
|
+
"""Server port"""
|
|
64
|
+
|
|
65
|
+
url: str = configvar(
|
|
66
|
+
default="http://{{host}}:{{port}}",
|
|
67
|
+
no_search=True,
|
|
68
|
+
)
|
|
69
|
+
"""Computed base URL"""
|
|
70
|
+
|
|
71
|
+
# case 1: dev stage
|
|
72
|
+
cfg = MyConfig()
|
|
73
|
+
# Override port via CLI args for demonstration
|
|
74
|
+
cfg.resolve_vars(conffile_path="{{xdg_config_home}}/myapp/conf.yaml",values={"stage":"dev","port": "9090"})
|
|
75
|
+
assert cfg.url == "http://localhost:9090"
|
|
76
|
+
|
|
77
|
+
# case 2: prod stage
|
|
78
|
+
cfg = MyConfig()
|
|
79
|
+
cfg.resolve_vars(conffile_path="{{xdg_config_home}}/myapp/conf.yaml",values={"stage":"prod"})
|
|
80
|
+
assert cfg.url == "http://myapp.dot.com:80"
|
|
81
|
+
|
|
82
|
+
if old_env is not None:
|
|
83
|
+
os.environ["XDG_CONFIG_HOME"] = old_env
|
|
84
|
+
else:
|
|
85
|
+
del os.environ["XDG_CONFIG_HOME"]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Why you might like it**
|
|
89
|
+
- **Plain Python Declarations**: Define variables, types, defaults, and docs in a single line of standard Python.
|
|
90
|
+
- **Variable subsitution**: Use Jinja2 templates to define dynamic defaults and search keys.
|
|
91
|
+
- **Deep Computation**: Resolution chain goes: **CLI > Env > Files > Substitution > Computed > Defaults**.
|
|
92
|
+
- **Smart Fallbacks**: Automatically search for multiple key variants (e.g. `host-dev` then `host`) and first matching config file.
|
|
93
|
+
- **Structured & Modular**: Supports **Namespaces**, **Scopes**, and **Decentralized** definitions via inheritance.
|
|
94
|
+
- **Robust & Integrated**: Built-in **Pydantic** validation and zero-boilerplate **Click** CLI generation.
|
|
95
|
+
- **predefined variables**: `config_home`, `user_home`, `os_type`, `exec-stage` are available out of the box for use in templates and can be used in variable substitution to define dynamic defaults and search keys.
|
|
96
|
+
|
|
97
|
+
**Why not X? (quick comparison)**
|
|
98
|
+
- **pydantic-settings**: great for env-driven settings models; choose TheOneConf if you want a complete **resolution order** (CLI > Env > Files > Defaults) with built-in **variable substitution**.
|
|
99
|
+
- **Dynaconf**: great for flexible layered configs; choose TheOneConf if you prefer a **plain Python** approach where definitions are just class attributes with standard type hints.
|
|
100
|
+
- **Hydra / OmegaConf**: great for complex composition; choose TheOneConf if you want a **lighter** solution that manages **smart fallbacks** (like searching `key-prod` then `key`) natively.
|
|
101
|
+
- **Hand-rolled `argparse`/`click` + `os.environ` + file parsing**: fine for small scripts; choose TheOneConf when you want to stop rewriting the same precedence/typing/validation glue.
|
|
102
|
+
|
|
103
|
+
## Table of Contents
|
|
104
|
+
|
|
105
|
+
- [✨ Key Features](#-key-features)
|
|
106
|
+
- [🚀 Quick Start](#-quick-start)
|
|
107
|
+
- [🧩 First-Class IDE Support](#-first-class-ide-support)
|
|
108
|
+
- [🔌 Multiple Configuration Sources](#-multiple-configuration-sources)
|
|
109
|
+
- [🧠 Dynamic Computed Values (Eval Forms)](#-dynamic-computed-values-eval-forms)
|
|
110
|
+
- [🔄 Data Transformation](#-data-transformation)
|
|
111
|
+
- [📂 Path Management](#-path-management)
|
|
112
|
+
- [📦 Nested Configurations (Namespaces)](#-nested-configurations-namespaces)
|
|
113
|
+
- [🌍 Scopes (Environment Awareness)](#-scopes-environment-awareness)
|
|
114
|
+
- [🏗️ Inheritance and Extensibility (Decentralization)](#-inheritance-and-extensibility-decentralization)
|
|
115
|
+
- [🛡️ Type Casting and Validation](#-type-casting-and-validation)
|
|
116
|
+
- [🖱️ Click Integration](#-click-integration)
|
|
117
|
+
|
|
118
|
+
## ✨ Key Features
|
|
119
|
+
|
|
120
|
+
- 🧠 **First-Class IDE Support**: Leverage standard Python type hints for out-of-the-box autocompletion, type checking, and navigation.
|
|
121
|
+
- 🔌 **Multi-Source Loading**: Seamlessly unify configuration from CLI arguments, environment variables, config files (YAML/JSON/TOML), and defaults.
|
|
122
|
+
- � **Predefined Variables**: Access useful built-in variables like `os_type`, `user_home`, `config_home`, and `exec_stage` immediately.
|
|
123
|
+
- �🔎 **Smart Fallbacks**: Automatically search for multiple key variants (e.g. `host-dev` then `host`) and first matching config file.
|
|
124
|
+
- 🔄 **Variable Substitution**: Use Jinja2 templates to define dynamic defaults and search keys using the values of other variables.
|
|
125
|
+
- 🧮 **Dynamic Computed Values**: Define smart variables that automatically update based on other resolved values using Python callables.
|
|
126
|
+
- 🧹 **Data Transformation**: Sanitized and format your inputs on the fly (e.g. trimming, case conversion) before they hit your application logic.
|
|
127
|
+
- 📂 **Path Management**: Handle file system paths elegantly with auto-creation of directories and relative path resolution.
|
|
128
|
+
- 🏗️ **Cascading Configuration Files**: Load and merge multiple configuration files (e.g. system, user, local) by resolving configuration variables sequentially from different files.
|
|
129
|
+
- 🧩 **Nested Namespaces**: Organize complex configurations into logical, hierarchical groups (e.g. `db.host`, `server.timeout`) using nested classes.
|
|
130
|
+
- 🎭 **Scopes**: Activate different sets of variables for different execution scopes (e.g. 'bd', 'server') or usages within a single config class.
|
|
131
|
+
- 🌐 **Decentralized Configuration**: Modularize your settings by splitting definitions across multiple files or mixins and combining them effortlessly.
|
|
132
|
+
- 🛡️ **Robust Validation**: Eliminate startup errors with strict type enforcement and powerful constraints (ranges, regex) powered by **Pydantic**.
|
|
133
|
+
- � **Configuration Saving**: Persist configuration back to files with filtering by scope/namespaces, merging updates smartly to preserve unrelated existing values.
|
|
134
|
+
- �🖱️ **Seamless Click Integration**: Auto-generate your CLI interface directly from your configuration schema with zero boilerplate.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## 🚀 Quick Start
|
|
138
|
+
|
|
139
|
+
A minimal example showing how to define and use configuration variables with default values.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from the1conf import AppConfig, configvar
|
|
143
|
+
|
|
144
|
+
class MyConfig(AppConfig):
|
|
145
|
+
host: str = configvar(default="localhost")
|
|
146
|
+
"""Server host"""
|
|
147
|
+
|
|
148
|
+
port: int = configvar(default=8080,env_name="APP_PORT")
|
|
149
|
+
"""Server port"""
|
|
150
|
+
|
|
151
|
+
debug: bool = configvar(default=False)
|
|
152
|
+
|
|
153
|
+
# Instantiate and resolve
|
|
154
|
+
conf = MyConfig()
|
|
155
|
+
conf.resolve_vars()
|
|
156
|
+
|
|
157
|
+
print(f"Host: {conf.host}, Port: {conf.port}")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Why it's easier:**
|
|
161
|
+
TheOneConf drastically reduces boilerplate. In a single line, you define the variable name, its type, its default value, and its documentation. No separate schema files, no manual parsing—just standard Python code that works out of the box.
|
|
162
|
+
|
|
163
|
+
## 🧩 First-Class IDE Support
|
|
164
|
+
|
|
165
|
+
Because TheOneConf uses standard Python type hints, modern IDEs (VS Code, PyCharm) provide excellent support out of the box.
|
|
166
|
+
|
|
167
|
+
- **Autocompletion**: You get instant suggestions for resolved configuration variables as you type `config.`.
|
|
168
|
+
- **Type Checking**: Static analysis tools (mypy, pylance) can catch type errors in your configuration usage.
|
|
169
|
+
- **Go to Definition**: Easily navigate to where a configuration variable is defined.
|
|
170
|
+
- **Documentation**: Hover over a variable to see its help string.
|
|
171
|
+
> **Note**: This requires defining a standard python docstring just below the variable definition (which is also used for the CLI help message when using a click_option as explained below).
|
|
172
|
+
|
|
173
|
+
## 🔌 Multiple Configuration Sources
|
|
174
|
+
|
|
175
|
+
TheOneConf resolves values in this priority order: **CLI > Environment > Config File > Variable Substitution > Computed Values > Defaults**.
|
|
176
|
+
|
|
177
|
+
### Detailed Resolution Order
|
|
178
|
+
|
|
179
|
+
1. **CLI Arguments**: Explicit values passed to `resolve_vars` (usually from command line args) have the highest priority.
|
|
180
|
+
2. **Environment Variables**: Checks for associated environment variables (e.g., `APP_PORT`).
|
|
181
|
+
3. **Config Files**: Looks for keys in loaded configuration files (JSON, YAML, TOML).
|
|
182
|
+
4. **Variable Substitution**: Substitue already resolved variables into values (e.g. `{{ home }}/param`) with Jinja2 templating.
|
|
183
|
+
5. **Computed Values**: Executes Python callables to derive values dynamically.
|
|
184
|
+
6. **Static Defaults**: Falls back to the hardcoded default value if nothing else is found.
|
|
185
|
+
|
|
186
|
+
Here is a comprehensive example showing resolution from all sources (Click, Env, File, Default), integration with Click will be explain in detail later.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
import os
|
|
190
|
+
import json
|
|
191
|
+
import click
|
|
192
|
+
import the1conf
|
|
193
|
+
from pathlib import Path
|
|
194
|
+
|
|
195
|
+
# 1. Setup Environment Variable
|
|
196
|
+
os.environ["APP_KEY_ENV"] = "value_from_env"
|
|
197
|
+
|
|
198
|
+
# 2. Create a dummy JSON config file
|
|
199
|
+
with open("config.json", "w") as f:
|
|
200
|
+
json.dump({"key_file": "value_from_file","key_file_alt2": "value_from_file_alt2"}, f)
|
|
201
|
+
|
|
202
|
+
class AppConfig(the1conf.AppConfig):
|
|
203
|
+
# Variables with different priorities
|
|
204
|
+
key_cli: str = the1conf.configvar(default="default")
|
|
205
|
+
key_env: str = the1conf.configvar(default="default", env_name="APP_KEY_ENV")
|
|
206
|
+
key_file: str = the1conf.configvar(default="default")
|
|
207
|
+
key_sub: str = the1conf.configvar(
|
|
208
|
+
default="sub_{{ key_env }}_{{ key_file }}"
|
|
209
|
+
)
|
|
210
|
+
key_computed: str = the1conf.configvar(
|
|
211
|
+
default=lambda _, c, __: f"computed_{c.key_env}_{c.key_file}",
|
|
212
|
+
no_search=True
|
|
213
|
+
)
|
|
214
|
+
key_default: str = the1conf.configvar(default="value_from_default")
|
|
215
|
+
|
|
216
|
+
@click.command()
|
|
217
|
+
@the1conf.click_option(AppConfig.key_cli)
|
|
218
|
+
def main(**kwargs):
|
|
219
|
+
conf = AppConfig()
|
|
220
|
+
|
|
221
|
+
# Resolve vars: CLI (kwargs) > Env > File > Default
|
|
222
|
+
conf.resolve_vars(
|
|
223
|
+
values=kwargs,
|
|
224
|
+
conffile_path="config.json"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Verify sources
|
|
228
|
+
assert conf.key_cli == "value_from_cli" # Source: CLI Argument
|
|
229
|
+
assert conf.key_env == "value_from_env" # Source: Environment Variable
|
|
230
|
+
assert conf.key_file == "value_from_file" # Source: Config File (JSON)
|
|
231
|
+
assert conf.key_sub == "sub_value_from_env_value_from_file" # Source: Variable Substitution
|
|
232
|
+
assert conf.key_computed == "computed_value_from_env_value_from_file" # Source: Computed Value
|
|
233
|
+
assert conf.key_default == "value_from_default" # Source: Default Value
|
|
234
|
+
print("All assertions passed!")
|
|
235
|
+
|
|
236
|
+
# Simulate CLI execution: python app.py --key-cli "value_from_cli"
|
|
237
|
+
# We invoke the underlying function directly for demonstration
|
|
238
|
+
runner = CliRunner()
|
|
239
|
+
result = runner.invoke(main, ["--key-cli", "value_from_cli"])
|
|
240
|
+
if result.exception:
|
|
241
|
+
raise result.exception
|
|
242
|
+
assert result.exit_code == 0
|
|
243
|
+
|
|
244
|
+
# Cleanup
|
|
245
|
+
del os.environ["APP_KEY_ENV"]
|
|
246
|
+
if os.path.exists("config.json"):
|
|
247
|
+
os.remove("config.json")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Focus on Logic, Not Plumbing**
|
|
251
|
+
Notice how clean the `main` function is? You don't verify if a file exists, you don't manually parse environment variables, and you don't write complex `if/else` chains to handle priorities. TheOneConf abstracts all this complexity away, allowing you to focus entirely on your application's business logic.
|
|
252
|
+
|
|
253
|
+
## Predefined Variables
|
|
254
|
+
TheOneConf provides several useful built-in variables that you can use directly in your configuration definitions, especially for variable substitution and dynamic paths.
|
|
255
|
+
There are two groups of predefined variables:
|
|
256
|
+
1. **Standard base directory variables**: These are variables derived from standard environment variables that define common user directories. There are os dependent variables based on:
|
|
257
|
+
- XGD base directories for linux and macOS.
|
|
258
|
+
- standard windows APPDATA and APPCONFIG directories.
|
|
259
|
+
|
|
260
|
+
All these path are unified into OS independent variables:
|
|
261
|
+
- `data_home`: The path to the user-specific data directory.
|
|
262
|
+
- `config_home`: The path to the user-specific configuration directory.
|
|
263
|
+
- `cache_home`: The path to the user-specific cache directory.
|
|
264
|
+
|
|
265
|
+
2. **OS Variables**: These provide information about the operating system and user environment.
|
|
266
|
+
|
|
267
|
+
- `os_type`: The operating system type (e.g., 'windows', 'linux', 'darwin').
|
|
268
|
+
- `user_home`: The path to the current user's home directory.
|
|
269
|
+
|
|
270
|
+
3. **Execution Stage Variable**: The `exec_stage` variable is particularly useful for differentiating configurations based on the execution stage (ie. environment) like development, production, test, etc.
|
|
271
|
+
This variable is predefined but users needs to set its value either through the command line or in an environment variable. You have different options to pass it value:
|
|
272
|
+
- **Command Line Argument**: Using the Click integration, you can pass `--stage`,`--env` or `--exec-stage` argument to set the execution stage.
|
|
273
|
+
- **Environment Variable**: You can set the `EXEC_STAGE`, `STAGE` or `ENV` environment variable to define the execution stage.
|
|
274
|
+
- **Pass a value to resolve_vars()** : You can also directly pass the execution stage value in the `values` dictionary when calling `resolve_vars()`.
|
|
275
|
+
- **Redefine the variable**: You can redefine the `exec_stage` variable in your configuration class if you want to set a different default , different keys or change its behavior.
|
|
276
|
+
|
|
277
|
+
## Key Lookup with Fallback
|
|
278
|
+
|
|
279
|
+
When searching for a variable name (like `server_port`), TheOneConf uses a smart fallback mechanism:
|
|
280
|
+
|
|
281
|
+
1. **Exact Key**: Checks for the exact key defined in `ConfigVarDef`.
|
|
282
|
+
2. **Fallback List**: If `env_name`, `value_key` or `file_key` is defined as a list, it searches each candidate in order.
|
|
283
|
+
* Example: `env_name=["MYAPP_PORT", "PORT"]` checks `MYAPP_PORT` first, then `PORT`.
|
|
284
|
+
3. **Click-Style Conversion**: If enabled (`click_key_conversion=True`), it attempts to normalize keys (e.g. converting `server_port` to `server-port`).
|
|
285
|
+
4. **Variable Substitution in keys**: Keys themselves can be templates (e.g. `env_name="APP_{{ env }}_PORT"`), allowing context-dependent variable names.
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
This example demonstrates how to use `exec_stage` and `os_type` (predefined variables) to implement context-aware defaults.
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
import click
|
|
292
|
+
import the1conf
|
|
293
|
+
from the1conf import AppConfig, configvar
|
|
294
|
+
|
|
295
|
+
# 1. Create a dummy config file
|
|
296
|
+
with open("config.yaml", "w") as f:
|
|
297
|
+
f.write("""
|
|
298
|
+
browser-linux-prod: "/usr/bin/google-chrome-stable"
|
|
299
|
+
browser-linux-dev: "/usr/bin/google-chrome-beta"
|
|
300
|
+
browser-linux: "google-chrome"
|
|
301
|
+
browser-windows-prod: "C:/Program Files/Google/Chrome/Application/chrome.exe"
|
|
302
|
+
browser-windows-dev: "C:/Program Files/Google/Chrome Beta/Application/chrome.exe"
|
|
303
|
+
browser-windows: "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
|
304
|
+
browser: "google-chrome"
|
|
305
|
+
""")
|
|
306
|
+
|
|
307
|
+
class MyConfig(AppConfig):
|
|
308
|
+
# This variable will look for keys in this order:
|
|
309
|
+
# 1. 'browser-{{os_type}}-{{exec_stage}}' (e.g. browser-linux-prod)
|
|
310
|
+
# 2. 'browser-{{os_type}}' (e.g. browser-linux)
|
|
311
|
+
# 3. 'browser' (fallback)
|
|
312
|
+
browser_cmd: str = configvar(
|
|
313
|
+
file_key=[
|
|
314
|
+
"browser-{{os_type}}-{{exec_stage}}",
|
|
315
|
+
"browser-{{os_type}}",
|
|
316
|
+
"browser"
|
|
317
|
+
]
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@click.command()
|
|
321
|
+
# Allow user to specify stage via CLI (e.g. --env prod)
|
|
322
|
+
# exec_stage is a predefined variable in AppConfig
|
|
323
|
+
@the1conf.click_option(AppConfig.exec_stage)
|
|
324
|
+
def main(**kwargs):
|
|
325
|
+
# 'exec_stage' and 'os_type' are automatically available
|
|
326
|
+
cfg = MyConfig()
|
|
327
|
+
cfg.resolve_vars(conffile_path="config.yaml", values=kwargs)
|
|
328
|
+
|
|
329
|
+
print(f"Stage: {cfg.exec_stage}")
|
|
330
|
+
print(f"OS: {cfg.os_type}")
|
|
331
|
+
print(f"Browser: {cfg.browser_cmd}")
|
|
332
|
+
|
|
333
|
+
# simulate CLI execution like: python app.py --env prod
|
|
334
|
+
runner = CliRunner()
|
|
335
|
+
result = runner.invoke(main, ["--env", "prod"])
|
|
336
|
+
if result.exception:
|
|
337
|
+
raise result.exception
|
|
338
|
+
assert result.exit_code == 0
|
|
339
|
+
assert "Browser: /usr/bin/google-chrome-stable" in result.output
|
|
340
|
+
if os.path.exists("config.yaml"):
|
|
341
|
+
os.remove("config.yaml")
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Config File Lookup with Fallback
|
|
345
|
+
|
|
346
|
+
You can specify multiple potential locations for configuration files. TheOneConf will use the first one that exists.
|
|
347
|
+
|
|
348
|
+
* **Candidate List**: Pass a list of paths to `conffile_path` (e.g. `['./config.json', '~/.myapp/config.json', '/etc/myapp/config.json']`).
|
|
349
|
+
* **Template Support**: Paths can contain Jinja templates (e.g. `~/.{{ app_name }}/config.yaml`), which are rendered using currently resolved variables before checking for existence.
|
|
350
|
+
|
|
351
|
+
Configuration files can be in **YAML**, **JSON**, or **TOML** format.
|
|
352
|
+
|
|
353
|
+
**Important**: When using variables in `conffile_path`, these variables must be resolved **before** TheOneConf attempts to load the file. This is typically achieved by:
|
|
354
|
+
1. Using **Predefined Variables** (like `exec_stage` or `os_type`) which are available automatically.
|
|
355
|
+
2. Using variables marked with `auto_prolog=True`.
|
|
356
|
+
3. **Two-Step Resolution**: Resolving the path-dependent variables first, then resolving the rest.
|
|
357
|
+
|
|
358
|
+
### Example: Two-Step Resolution
|
|
359
|
+
|
|
360
|
+
If your config file path depends on a custom variable (like `app_name`), you can resolve it first.
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
import os
|
|
364
|
+
import json
|
|
365
|
+
import tempfile
|
|
366
|
+
from the1conf import AppConfig, configvar
|
|
367
|
+
|
|
368
|
+
# Setup: Create a specific config file for 'superapp' in a temp dir
|
|
369
|
+
tmp_dir = tempfile.TemporaryDirectory()
|
|
370
|
+
config_file = os.path.join(tmp_dir.name, "superapp.json")
|
|
371
|
+
with open(config_file, "w") as f:
|
|
372
|
+
json.dump({"timeout": 120}, f)
|
|
373
|
+
|
|
374
|
+
class MyConfig(AppConfig):
|
|
375
|
+
app_name: str = configvar(
|
|
376
|
+
default="myapp",
|
|
377
|
+
env_name="APP_NAME"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
timeout: int = configvar(default=30)
|
|
381
|
+
|
|
382
|
+
conf = MyConfig()
|
|
383
|
+
|
|
384
|
+
# Step 1: Resolve 'app_name' (from Defaults, Env, or CLI variables passed here)
|
|
385
|
+
# We haven't specified a conffile_path yet.
|
|
386
|
+
conf.resolve_vars(values={"app_name": "superapp"})
|
|
387
|
+
|
|
388
|
+
# Step 2: Use the resolved 'app_name' to locate the config file
|
|
389
|
+
# We use f-string to inject the temp dir path, but let TheOneConf inject 'app_name'
|
|
390
|
+
conf_path_template = os.path.join(tmp_dir.name, "{{app_name}}.json")
|
|
391
|
+
conf.resolve_vars(conffile_path=conf_path_template)
|
|
392
|
+
|
|
393
|
+
assert conf.timeout == 120
|
|
394
|
+
|
|
395
|
+
# Cleanup
|
|
396
|
+
tmp_dir.cleanup()
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## 🏗️ Cascading Configuration Files
|
|
400
|
+
|
|
401
|
+
Complex applications effectively need to load configuration from multiples files depending on the context (e.g. system-wide, user-specific, project-specific).
|
|
402
|
+
Since TheOneConf loads only one configuration file per call to `resolve_vars()`, you can easily chain multiple calls to achieve **Cascading Configuration**.
|
|
403
|
+
|
|
404
|
+
Normally `resolve_vars()` protects already set variables. By setting `allow_override=True`, you instruct TheOneConf to overwrite existing values with new ones found in the subsequent file, effectively implementing a "last-one-wins" merge logic. (Note: If a specific variable definition has `allow_override=False` (default value if not specified anywhere in the Configuration instantiation , the call to resolve_vars or the variable definition), it will only be resolved the first time a value is found, ignoring subsequent calls even if the global flag is set).
|
|
405
|
+
|
|
406
|
+
This feature combines powerfully with other mechanisms:
|
|
407
|
+
1. **Fallback**: If a variable is missing in a higher-priority file, the value from the lower-priority file (or default) is preserved.
|
|
408
|
+
2. **Variable Substitution**: You can use variables (like `exec_stage` or `os_type`) to dynamically construct paths for each layer of configuration.
|
|
409
|
+
|
|
410
|
+
### Example: Multi-layer loading
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
conf = MyAppConfig()
|
|
414
|
+
|
|
415
|
+
# 1. Load system defaults (lowest priority)
|
|
416
|
+
conf.resolve_vars(conffile_path="/etc/myapp/config.yaml")
|
|
417
|
+
|
|
418
|
+
# 2. Load user preferences (override system defaults)
|
|
419
|
+
conf.resolve_vars(conffile_path="~/.config/myapp/user_config.yaml", allow_override=True)
|
|
420
|
+
|
|
421
|
+
# 3. Load stage specific config (highest priority)
|
|
422
|
+
# Using substitution: loads 'config-dev.yaml' or 'config-prod.yaml'
|
|
423
|
+
conf.resolve_vars(conffile_path="config-{{exec_stage}}.yaml", allow_override=True)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## 🧠 Dynamic Computed Values (Eval Forms)
|
|
427
|
+
|
|
428
|
+
TheOneConf allows variables to be computed dynamically based on the values of **already resolved** variables. This is realized using **Eval Forms**: callables that receive the configuration context.
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
from the1conf import AppConfig, configvar
|
|
433
|
+
|
|
434
|
+
class DBConfig(AppConfig):
|
|
435
|
+
host: str = configvar(default="localhost")
|
|
436
|
+
port: int = configvar(default=5432)
|
|
437
|
+
name: str = configvar(default="app_db")
|
|
438
|
+
|
|
439
|
+
# Eval Form signature: (variable_name, config, current_value)
|
|
440
|
+
# We use the 'config' (c) to access previously resolved 'host', 'port', and 'name'
|
|
441
|
+
|
|
442
|
+
dsn: str = configvar(
|
|
443
|
+
default=lambda _, c, __: f"postgresql://{c.host}:{c.port}/{c.name}",
|
|
444
|
+
no_search=True
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
conf = DBConfig()
|
|
448
|
+
# Override host via CLI args style for demonstration
|
|
449
|
+
conf.resolve_vars(values={"host": "db.internal"})
|
|
450
|
+
|
|
451
|
+
assert conf.dsn == "postgresql://db.internal:5432/app_db"
|
|
452
|
+
```
|
|
453
|
+
**Note**: `no_search=True` for `dns` ensures we don't look for 'dsn' in env vars or config file, purely computed.
|
|
454
|
+
|
|
455
|
+
**Decouple Configuration Logic**:
|
|
456
|
+
By moving the logic for computation on variables (like URLs, paths, or connection strings) out of your application code and into the configuration definition, you keep your business logic clean. Your application simply requests the final value (e.g. `conf.dsn`) without needing to know how it was constructed from `host`, `port`, and `name`.
|
|
457
|
+
|
|
458
|
+
This is extremely useful to avoid duplication, for example constructing a Database URL from host and port.
|
|
459
|
+
|
|
460
|
+
## 🔄 Data Transformation
|
|
461
|
+
|
|
462
|
+
You can also use **Eval Forms** to transform a value after it has been resolved but before it is cast to its final type. This is done using the `transform` directive.
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from the1conf import AppConfig, configvar
|
|
466
|
+
|
|
467
|
+
class App(AppConfig):
|
|
468
|
+
# transform: takes the found value (e.g. from env or CLI) and modifies it.
|
|
469
|
+
# Here we ensure the API key is always uppercase and stripped of whitespace.
|
|
470
|
+
api_key: str = configvar(
|
|
471
|
+
default=" default_key ",
|
|
472
|
+
transform=lambda _, __, val: val.strip().upper() if val else val
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
conf = App()
|
|
476
|
+
# Pass a value that needs cleaning (whitespace, lowercase)
|
|
477
|
+
conf.resolve_vars(values={"api_key": " my_custom_key "})
|
|
478
|
+
|
|
479
|
+
assert conf.api_key == "MY_CUSTOM_KEY" # Result has been stripped and uppercased
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## 📂 Path Management
|
|
483
|
+
|
|
484
|
+
TheOneConf simplifies handling file system paths with built-in directives for resolution and directory creation.
|
|
485
|
+
|
|
486
|
+
- **`can_be_relative_to`**: If a path is relative, it is resolved against a base directory (which can be another configuration variable or a fixed path).
|
|
487
|
+
- **`make_dirs`**: Automatically creates the directory hierarchy if it doesn't exist.
|
|
488
|
+
|
|
489
|
+
```python
|
|
490
|
+
import os
|
|
491
|
+
import shutil
|
|
492
|
+
from pathlib import Path
|
|
493
|
+
from the1conf import AppConfig, configvar
|
|
494
|
+
from the1conf.app_config import PathType
|
|
495
|
+
|
|
496
|
+
# Set environment variable for the example
|
|
497
|
+
os.environ["APP_BASE_DIR"] = "/tmp/my_app_from_env"
|
|
498
|
+
|
|
499
|
+
class IOConfig(AppConfig):
|
|
500
|
+
# Validates that 'base_dir' is a path
|
|
501
|
+
# no_value_search=True means we ignore values passed in resolve_vars() dict
|
|
502
|
+
base_dir: Path = configvar(
|
|
503
|
+
default="/tmp/default_data",
|
|
504
|
+
env_name="APP_BASE_DIR",
|
|
505
|
+
no_value_search=True
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# If 'log_dir' is relative (e.g. "logs"), it becomes "{base_dir}/logs"
|
|
509
|
+
# make_dirs=PathType.Dir ensures the directory is created on resolution
|
|
510
|
+
log_dir: Path = configvar(
|
|
511
|
+
default="logs",
|
|
512
|
+
can_be_relative_to="base_dir",
|
|
513
|
+
make_dirs=PathType.Dir
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Resolves relative to 'base_dir'. Creates parent directory of the file.
|
|
517
|
+
cache_file: Path = configvar(
|
|
518
|
+
default="cache/db.sqlite",
|
|
519
|
+
can_be_relative_to="base_dir",
|
|
520
|
+
make_dirs=PathType.File
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
conf = IOConfig()
|
|
524
|
+
# Pass a value for base_dir, but it will be IGNORED due to no_value_search=True
|
|
525
|
+
conf.resolve_vars(values={"base_dir": "/tmp/ignored_path"})
|
|
526
|
+
|
|
527
|
+
# Verification:
|
|
528
|
+
# 1. base_dir came from ENV: "APP_BASE_DIR" => "/tmp/my_app_from_env"
|
|
529
|
+
# 2. log_dir is resolved relative to base_dir
|
|
530
|
+
assert conf.log_dir == Path("/tmp/my_app_from_env/logs") # Value from Env used, runtime value ignored
|
|
531
|
+
assert conf.log_dir.is_dir() # Directory was automatically created
|
|
532
|
+
|
|
533
|
+
# Clean up for the example
|
|
534
|
+
if conf.base_dir.exists():
|
|
535
|
+
shutil.rmtree(conf.base_dir)
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## 📦 Nested Configurations (Namespaces)
|
|
539
|
+
|
|
540
|
+
For larger applications, flat configuration structures can become unmanageable. TheOneConf supports **Nested Namespaces** using inner classes inheriting from `NameSpace`. This allows you to group related settings logically (e.g., `db`, `logging`, `server`).
|
|
541
|
+
|
|
542
|
+
Important: Namespaces are also used in searching configuration files (e.g. `db.host` looks for `{"db": {"host": ...}}` in YAML/JSON).
|
|
543
|
+
|
|
544
|
+
```yaml
|
|
545
|
+
# config.yaml
|
|
546
|
+
env: "prod"
|
|
547
|
+
db:
|
|
548
|
+
host: "db.prod"
|
|
549
|
+
port: 5432
|
|
550
|
+
auth:
|
|
551
|
+
username: "admin"
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
from pathlib import Path
|
|
556
|
+
from the1conf import AppConfig, NameSpace, configvar
|
|
557
|
+
|
|
558
|
+
class MyApp(AppConfig):
|
|
559
|
+
env: str = configvar(default="dev")
|
|
560
|
+
|
|
561
|
+
# Define a 'db' namespace
|
|
562
|
+
class db(NameSpace):
|
|
563
|
+
host: str = configvar(default="localhost")
|
|
564
|
+
port: int = configvar(default=5432)
|
|
565
|
+
|
|
566
|
+
# Nested namespaces can be infinitely deep
|
|
567
|
+
class auth(NameSpace):
|
|
568
|
+
username: str = configvar(default="admin")
|
|
569
|
+
password: str = configvar(env_name="DB_PASSWORD")
|
|
570
|
+
|
|
571
|
+
conf = MyApp()
|
|
572
|
+
# Resolve variables loading the config.yaml file defined above
|
|
573
|
+
conf.resolve_vars(conffile_path=Path("config.yaml"))
|
|
574
|
+
|
|
575
|
+
# Check that values are loaded from the file
|
|
576
|
+
assert conf.env == "prod"
|
|
577
|
+
assert conf.db.host == "db.prod"
|
|
578
|
+
assert conf.db.auth.username == "admin"
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## 🌍 Scopes (Environment Awareness)
|
|
582
|
+
|
|
583
|
+
In complex applications, you often need to adapt the configuration schema based on the runtime environment.
|
|
584
|
+
By tagging variables with `scope`, you can define which settings are relevant for a specific mode (e.g. "server", "client", "test"). When resolving variables, you specify the active scope(s), and TheOneConf will ignore any variable not belonging to it.
|
|
585
|
+
|
|
586
|
+
**Why it matters**:
|
|
587
|
+
This prevents errors and avoids unnecessary computation. Variables specific to one scope often depend on data (like CLI arguments or config sections) that are absent in others. By skipping unconnected scope, you ensure the application doesn't crash trying to resolve or validate settings it doesn't need.
|
|
588
|
+
|
|
589
|
+
```python
|
|
590
|
+
import the1conf
|
|
591
|
+
|
|
592
|
+
class ToolConfig(the1conf.AppConfig):
|
|
593
|
+
# Common variable (available in all scope)
|
|
594
|
+
verbose: bool = the1conf.configvar(default=False)
|
|
595
|
+
"""Enable verbose logging"""
|
|
596
|
+
|
|
597
|
+
# Variable specific to 'server' scope
|
|
598
|
+
port: int = the1conf.configvar(
|
|
599
|
+
default=8080,
|
|
600
|
+
scope=["server"]
|
|
601
|
+
)
|
|
602
|
+
"""Port to listen on"""
|
|
603
|
+
|
|
604
|
+
# Variable specific to 'client' scope
|
|
605
|
+
timeout: int = the1conf.configvar(
|
|
606
|
+
default=30,
|
|
607
|
+
scope=["client"]
|
|
608
|
+
)
|
|
609
|
+
"""Connection timeout"""
|
|
610
|
+
|
|
611
|
+
conf = ToolConfig()
|
|
612
|
+
|
|
613
|
+
# 1. Resolve for SERVER scope
|
|
614
|
+
# Only 'verbose' and 'port' are resolved. 'timeout' is ignored.
|
|
615
|
+
conf.resolve_vars(scope=["server"])
|
|
616
|
+
|
|
617
|
+
assert conf.port == 8080 # Available in 'server' scope
|
|
618
|
+
assert conf.verbose is False # Common variable
|
|
619
|
+
assert conf.timeout is None # Ignored variable
|
|
620
|
+
|
|
621
|
+
# 2. Resolve for CLIENT scope (simulating a fresh run for clarity)
|
|
622
|
+
# In a real CLI, you'd likely instantiate a new config or reuse one cleanly.
|
|
623
|
+
conf2 = ToolConfig()
|
|
624
|
+
conf2.resolve_vars(scope=["client"])
|
|
625
|
+
|
|
626
|
+
assert conf2.timeout == 30 # Available in 'client' scope
|
|
627
|
+
assert conf2.verbose is False # Common variable
|
|
628
|
+
assert conf2.port is None # Ignored variable
|
|
629
|
+
print("Scope assertions passed!")
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## 🏗️ Inheritance and Extensibility (Decentralization)
|
|
633
|
+
|
|
634
|
+
One of the strengths of TheOneConf is its support for standard Python inheritance to achieve **decentralized configuration**.
|
|
635
|
+
You can split your configuration definitions across multiple classes (e.g., one per module or component) or create specialized versions for environments.
|
|
636
|
+
|
|
637
|
+
When you instantiate the final class, TheOneConf **merges** all variables found in the class hierarchy into a single, unified configuration object. This means valid variables are the union of all parent content and the local content.
|
|
638
|
+
|
|
639
|
+
```python
|
|
640
|
+
import the1conf
|
|
641
|
+
|
|
642
|
+
# Base Component Configuration
|
|
643
|
+
class LogConfig(the1conf.AppConfig):
|
|
644
|
+
verbose: bool = the1conf.configvar(default=False)
|
|
645
|
+
log_file: str = the1conf.configvar(default="app.log")
|
|
646
|
+
|
|
647
|
+
# App Configuration extends the Component Config
|
|
648
|
+
class AppConfig(LogConfig):
|
|
649
|
+
# We inherit 'verbose' and 'log_file'
|
|
650
|
+
# And we add new specific variables
|
|
651
|
+
port: int = the1conf.configvar(default=8080)
|
|
652
|
+
|
|
653
|
+
# We can also override defaults
|
|
654
|
+
log_file: str = the1conf.configvar(default="server.log")
|
|
655
|
+
|
|
656
|
+
conf = AppConfig()
|
|
657
|
+
conf.resolve_vars()
|
|
658
|
+
|
|
659
|
+
# variable from Base
|
|
660
|
+
assert conf.verbose is False
|
|
661
|
+
# overwritten variable
|
|
662
|
+
assert conf.log_file == "server.log"
|
|
663
|
+
# new variable
|
|
664
|
+
assert conf.port == 8080
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Modular & Independent Configuration**
|
|
668
|
+
You can organize your configuration by "application domain" (e.g., Database, Network), where each domain has its own configuration file, a dedicated **Namespace**, and one or more specific **Scopes**. Thanks to the ability to call `resolve_vars()` multiple times, each domain can trigger its own resolution process targeting only its relevant scopes. This ensures strict variable segregation via namespaces and allows different parts of your application to load and validate their specific settings independently.
|
|
669
|
+
|
|
670
|
+
## 🛡️ Type Casting and Validation
|
|
671
|
+
|
|
672
|
+
TheOneConf leverages **Pydantic** to ensure that configuration values are not only of the correct type but also adhere to specific constraints.
|
|
673
|
+
This is particularly useful when loading values from typeless sources like environment variables or CLI arguments (which are always strings). TheOneConf automatically **casts** them to your target Python types and **validates** them against any defined constraints.
|
|
674
|
+
|
|
675
|
+
```python
|
|
676
|
+
from typing import Annotated
|
|
677
|
+
from datetime import date
|
|
678
|
+
from pydantic import PositiveInt, BaseModel, model_validator, Field
|
|
679
|
+
from the1conf import AppConfig, configvar
|
|
680
|
+
from the1conf.app_config import AppConfigException
|
|
681
|
+
|
|
682
|
+
# Identify a Pydantic Model for complex validation
|
|
683
|
+
class DateRange(BaseModel):
|
|
684
|
+
start_date: date
|
|
685
|
+
end_date: date
|
|
686
|
+
|
|
687
|
+
@model_validator(mode='after')
|
|
688
|
+
def check_dates(self):
|
|
689
|
+
if self.end_date <= self.start_date:
|
|
690
|
+
raise ValueError("end_date must be after start_date")
|
|
691
|
+
return self
|
|
692
|
+
|
|
693
|
+
class ServerConfig(AppConfig):
|
|
694
|
+
# Annotated[int, Field(...)] enforces value constraints (1024 < port < 65536)
|
|
695
|
+
port: Annotated[int, Field(gt=1024, lt=65536)] = configvar(default=8080)
|
|
696
|
+
|
|
697
|
+
# Pydantic types enforce strict constraints
|
|
698
|
+
max_workers: PositiveInt = configvar(default=4) # Must be > 0
|
|
699
|
+
|
|
700
|
+
# Complex validation using Pydantic Model
|
|
701
|
+
period: DateRange = configvar(
|
|
702
|
+
default={"start_date": "2024-01-01", "end_date": "2024-12-31"}
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
conf = ServerConfig()
|
|
706
|
+
|
|
707
|
+
# Simulate loading values from environment variables (strings)
|
|
708
|
+
conf.resolve_vars(values={
|
|
709
|
+
"port": "9000", # Cast: "9000" -> 9000 (int)
|
|
710
|
+
"max_workers": "10", # Cast & Validate: "10" -> 10 (int)
|
|
711
|
+
"period": {"start_date": "2024-03-01", "end_date": "2024-03-31"}
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
assert conf.port == 9000
|
|
715
|
+
assert conf.max_workers == 10
|
|
716
|
+
assert conf.period.start_date == date(2024, 3, 1)
|
|
717
|
+
|
|
718
|
+
# Validation ensures integrity
|
|
719
|
+
try:
|
|
720
|
+
# Use a new instance to ensure we don't skip the variable because it's already set
|
|
721
|
+
conf_invalid = ServerConfig()
|
|
722
|
+
# Invalid: end_date before start_date
|
|
723
|
+
conf_invalid.resolve_vars(values={
|
|
724
|
+
"period": {"start_date": "2024-02-01", "end_date": "2024-01-01"}
|
|
725
|
+
})
|
|
726
|
+
except AppConfigException:
|
|
727
|
+
print("Validation correctly rejected invalid date range")
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**Powerful & Safe Configuration**
|
|
731
|
+
This combination of strict typing and advanced validation ensures your application starts only with a valid configuration state. By catching errors early (at startup) and providing clear feedback, you prevent subtle runtime bugs and make your application more robust. You can define complex rules (ranges, dependencies between fields, regex patterns) declaratively, keeping your initialization logic clean and simple.
|
|
732
|
+
|
|
733
|
+
## Saving Configuration
|
|
734
|
+
|
|
735
|
+
`the1conf` allows you to save the current configuration state back to a file using the `store_conf_infile` method. This is useful for persisting user preferences or updating configuration files programmatically.
|
|
736
|
+
|
|
737
|
+
### `store_conf_infile`
|
|
738
|
+
|
|
739
|
+
This method writes the configuration variables to a specified file. It merges the current configuration with the existing file content (if any), updating values for the provided variables while preserving other data in the file (at the top level).
|
|
740
|
+
|
|
741
|
+
```python
|
|
742
|
+
def store_conf_infile(
|
|
743
|
+
self,
|
|
744
|
+
file: Union[Path, str],
|
|
745
|
+
*,
|
|
746
|
+
namespaces: Sequence[str] = [],
|
|
747
|
+
scopes: Sequence[str] = [],
|
|
748
|
+
type: str = "yaml",
|
|
749
|
+
)
|
|
750
|
+
```
|
|
751
|
+
## 🖱️ Click Integration
|
|
752
|
+
|
|
753
|
+
TheOneConf integrates seamlessly with [Click](https://click.palletsprojects.com/) to inject configuration variables into your CLI.
|
|
754
|
+
The `@the1conf.click_option` decorator creates a click option from a `ConfigVarDef`.
|
|
755
|
+
|
|
756
|
+
**Benefits:**
|
|
757
|
+
1. **Zero Boilerplate**: No need to manually define `click.option('--port', default=8080, help='...')`. TheOneConf infers everything from your class definition.
|
|
758
|
+
2. **Single Source of Truth**: Change the default value or help text in your Config class, and the CLI updates automatically.
|
|
759
|
+
3. **Complex Features Support**: It works seamlessly with TheOneConf's features like type casting, validation, and multi-source resolution.
|
|
760
|
+
|
|
761
|
+
```python
|
|
762
|
+
import click
|
|
763
|
+
import the1conf
|
|
764
|
+
|
|
765
|
+
class MyConfig(the1conf.AppConfig):
|
|
766
|
+
name: str = the1conf.configvar(default="World")
|
|
767
|
+
"""Name to greet. Help text is automatically exposed in --help"""
|
|
768
|
+
|
|
769
|
+
port: int = the1conf.configvar(default=8080)
|
|
770
|
+
"""Server port"""
|
|
771
|
+
|
|
772
|
+
@click.command()
|
|
773
|
+
# Automatically generate --name and --port options
|
|
774
|
+
@the1conf.click_option(MyConfig.name)
|
|
775
|
+
@the1conf.click_option(MyConfig.port)
|
|
776
|
+
def main(**kwargs):
|
|
777
|
+
cfg = MyConfig()
|
|
778
|
+
# Apply CLI args (highest priority) -> Env -> Files -> Defaults
|
|
779
|
+
cfg.resolve_vars(values=kwargs)
|
|
780
|
+
|
|
781
|
+
assert cfg.name == "Alice"
|
|
782
|
+
assert cfg.port == 9000
|
|
783
|
+
print(f"Server starting on {cfg.port} for {cfg.name}...")
|
|
784
|
+
|
|
785
|
+
if __name__ == "__main__":
|
|
786
|
+
from click.testing import CliRunner
|
|
787
|
+
|
|
788
|
+
# Simulate CLI execution: app.py --name Alice --port 9000
|
|
789
|
+
runner = CliRunner()
|
|
790
|
+
result = runner.invoke(main, ["--name", "Alice", "--port", "9000"])
|
|
791
|
+
print(result.output)
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
|