Metadata-Version: 2.4
Name: the1conf
Version: 2.2.0
Summary: All in one configuration management tool for your python applications.
License-Expression: MIT
License-File: LICENSE
Keywords: configuration,settings,cli,click
Author: Eric CHASTAN
Author-email: eric@chastan.consulting
Requires-Python: >=3.12
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Requires-Dist: click (>=8.4.2)
Requires-Dist: jinja2 (>=3.1.6)
Requires-Dist: toml (>=0.10.2)
Project-URL: Issues, https://gitlab.com/eric-chastan/the1conf/-/issues
Project-URL: Repository, https://gitlab.com/eric-chastan/the1conf
Description-Content-Type: text/markdown

# TheOneConf

![Status](https://img.shields.io/badge/status-active-success.svg)
![Python](https://img.shields.io/badge/python-3.12%2B-blue.svg)
![PyPI](https://img.shields.io/pypi/v/the1conf.svg)
![License](https://img.shields.io/badge/license-MIT-green.svg)

Define app configuration in plain Python classes.

**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.

```bash
pip install the1conf
```

```python
import os
import tempfile
from pathlib import Path

from the1conf import AppConfig, configvar

 # Setup: create a dummy yaml config file
tmp_dir = tempfile.TemporaryDirectory()
os.environ["XDG_CONFIG_HOME"] = tmp_dir.name
conf_file = Path(os.environ["XDG_CONFIG_HOME"]) / "myapp" / "conf.yaml"
conf_file.parent.mkdir(parents=True, exist_ok=True)
conf_file.write_text(
    """
host-dev: localhost
host: myapp.dot.com
port: 80
"""
)

class MyConfig(AppConfig):
    host: str = configvar(
        file_keys=["host-" + AppConfig.exec_stage, "host"],
    )
    """Server host"""

    port: int = configvar(default=8080)
    """Server port"""

    url: str = configvar(
        default="http://" + host + ":" + port,
        no_search=True,
    )
    """Computed base URL"""

try:
    # case 1: dev stage
    cfg = MyConfig()
    cfg.resolve_vars(
        conffile_path=AppConfig.config_home / "myapp" / "conf.yaml",
        values={"exec_stage": "dev", "port": "9090"},
    )
    assert cfg.url == "http://localhost:9090"

    # case 2: prod stage
    cfg = MyConfig()
    cfg.resolve_vars(
        conffile_path=AppConfig.config_home / "myapp" / "conf.yaml",
        values={"exec_stage": "prod"},
    )
    assert cfg.url == "http://myapp.dot.com:80"
finally:
    tmp_dir.cleanup()
```

**Why you might like it**
- **Plain Python Declarations**: Define variables, types, defaults, and docs in a single line of standard Python.
- **Variable Composition**: Combine variables using either Jinja templates or ConfigVar expressions
  - **ConfigVar expressions** are Python expressions that combine configuration variables with standard Python operators like `+`, `-`, `*`, `/` (e.g. `"host-" + AppConfig.exec_stage`, `"base_timeout * 2"`).
- **Deep Computation**: Resolution chain goes: **CLI > Env > Files > Substitution > Computed > Defaults**.
- **Smart Fallbacks**: Automatically search for multiple key variants (e.g. `host-dev` then `host`) and first matching config file.
- **Structured & Modular**: Supports **Namespaces**, **Scopes**, and **Inheritance**.
- **Robust & Integrated**: Built-in type conversion, value validation, integration with **Click** for command line interface with zero-boilerplate.
- **Predefined variables**: `config_home`, `user_home`, `os_type`, `exec_stage` are available out of the box.

**Why not X? (quick comparison)**
- **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**.
- **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.
- **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.
- **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.

## Table of Contents

- [✨ Key Features](#-key-features)
- [🚀 Quick Start](#-quick-start)
- [🧩 First-Class IDE Support](#-first-class-ide-support)
- [🔌 Multiple Configuration Sources](#-multiple-configuration-sources)
- [🧠 Dynamic Computed Values (Eval Forms)](#-dynamic-computed-values-eval-forms)
- [🧮 ConfigVar Expressions](#-configvar-expressions)
- [🔄 Data Transformation](#-data-transformation)
- [📂 Path Management](#-path-management)
- [📦 Nested Configurations (Namespaces)](#-nested-configurations-namespaces)
- [🌍 Scopes (Environment Awareness)](#-scopes-environment-awareness)
- [🏗️ Inheritance and Extensibility (Decentralization)](#-inheritance-and-extensibility-decentralization)
- [🛡️ Type Casting and Validation](#-type-casting-and-validation)
- [🖱️ Click Integration](#-click-integration)

## ✨ Key Features

- 🧠 **First-Class IDE Support**: Leverage standard Python type hints for out-of-the-box autocompletion, type checking, and navigation.
- 🔌 **Multi-Source Loading**: Seamlessly unify configuration from CLI arguments, environment variables, config files (YAML/JSON/TOML), and defaults.
- 🏷️ **Predefined Variables**: Access useful built-in variables like `os_type`, `user_home`, `config_home`, and `exec_stage` immediately.
- �🔎 **Smart Fallbacks**: Automatically search for multiple key variants (e.g. `host-dev` then `host`) and first matching config file.
- 🔄 **Variable Composition**: Combine resolved variables in default values, paths, and search keys using either Jinja2 templates or ConfigVar expressions.
- 🧮 **Dynamic Computed Values**: Compute values based on other resolved values using Python callables.
- 🧹 **Data Transformation**: Sanitize and format your inputs on the fly (e.g. trimming, case conversion) before they hit your application logic.
- 📂 **Path Management**: Handle file system paths elegantly with auto-creation of directories and relative path resolution.
- 🏗️ **Cascading Configuration Files**: Load and merge multiple configuration files (e.g. system, user, local) by resolving configuration variables sequentially from different files.
- 🧩 **Nested Namespaces**: Organize complex configurations into logical, hierarchical groups (e.g. `db.host`, `server.timeout`) using nested classes.
- 🎭 **Scopes**: Activate different sets of variables for different execution scopes (e.g. 'bd', 'server') or usages within a single config class.
- 🌐 **Decentralized Configuration**: Modularize your settings by splitting definitions across multiple files or mixins and combining them with inheritance.
- 🛡️ **Robust Validation**: Convert standard Python type hints at runtime and enforce application constraints with plain callables.
- � **Configuration Saving**: Persist configuration back to files with filtering by scope/namespaces, merging updates smartly to preserve unrelated existing values.
- �🖱️ **Seamless Click Integration**: Auto-generate your CLI interface directly from your configuration schema with zero boilerplate.


## 🚀 Quick Start

A minimal example showing how to define and use configuration variables with default values.

```python
from the1conf import AppConfig, configvar

class MyConfig(AppConfig):
    host: str = configvar(default="localhost")
    """Server host"""

    port: int = configvar(default=8080, env_keys="APP_PORT")
    """Server port"""

    debug: bool = configvar(default=False)
    """Enable debug mode"""

# Instantiate and resolve
conf = MyConfig()
conf.resolve_vars()

print(f"Host: {conf.host}, Port: {conf.port}")
assert conf.host == "localhost"
assert conf.port == 8080
assert conf.debug is False

```

**Why it's easier:**

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.

## 🧩 First-Class IDE Support

Because TheOneConf uses standard Python type hints, modern IDEs (VS Code, PyCharm) provide excellent support out of the box.

- **Autocompletion**: You get instant suggestions for resolved configuration variables as you type `config.`.
- **Type Checking**: Static analysis tools (mypy, pylance) can catch type errors in your configuration usage.
- **Go to Definition**: Easily navigate to where a configuration variable is defined.
- **Documentation**: Hover over a variable to see its help string.
  > **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).

## 🔌 Multiple Configuration Sources

TheOneConf resolves values in this priority order: **CLI > Environment > Config File > Variable Substitution > Computed Values > Defaults**.

### Detailed Resolution Order

1.  **CLI Arguments**: Explicit values passed to `resolve_vars` (usually from command line args) have the highest priority.
2.  **Environment Variables**: Checks for associated environment variables (e.g., `APP_PORT`).
3.  **Config Files**: Looks for keys in loaded configuration files (JSON, YAML, TOML).
4.  **Variable Composition**: Combine already resolved variables using Jinja2 templates or ConfigVar expressions.
5.  **Computed Values**: Executes Python callables to derive values dynamically.
6.  **Static Defaults**: Falls back to the hardcoded default value if nothing else is found.

Here is a comprehensive example showing resolution from all sources (Click, Env, File, Default). Click integration is explained in detail later.

```python
import os
import json
import click
import the1conf
from typing import Any
from pathlib import Path
import tempfile
from click.testing import CliRunner

from the1conf import AppConfig, configvar

# 1. Setup Environment Variable
os.environ["APP_KEY_ENV"] = "value_from_env"

# 2. Create a dummy JSON config file
tmp_dir = tempfile.TemporaryDirectory()
conf_file = Path(tmp_dir.name) / "myapp" / "conf.json"
conf_file.parent.mkdir(parents=True, exist_ok=True)
conf_file.write_text(
    json.dumps(
        {"key_file": "value_from_file", "key_file_alt2": "value_from_file_alt2"}
    )
)

class AppConfigExample(AppConfig):
    # Variables with different priorities
    key_cli: str = configvar(default="default")
    key_env: str = configvar(default="default", env_keys="APP_KEY_ENV")
    key_file: str = configvar(default="default")
    key_sub: str = configvar(default="sub_" + key_env + "_" + key_file)
    key_computed: str = configvar(
        default="computed_" + key_env + "_" + key_file,
        no_search=True,
    )
    key_default: str = configvar(default="value_from_default")

try:
    # Simulate CLI execution: python app.py --key_cli "value_from_cli"
    @click.command()
    @the1conf.click_option(AppConfigExample.key_cli)
    def main(**kwargs: Any) -> None:
        conf = AppConfigExample()

        # Resolve vars: CLI (kwargs) > Env > File > Default
        conf.resolve_vars(values=kwargs, conffile_path=conf_file)

        # Verify sources
        assert conf.key_cli == "value_from_cli"  # Source: CLI Argument
        assert conf.key_env == "value_from_env"  # Source: Environment Variable
        assert conf.key_file == "value_from_file"  # Source: Config File (JSON)
        assert (
            conf.key_sub == "sub_value_from_env_value_from_file"
        )  # Source: ConfigVar Expression
        assert (
            conf.key_computed == "computed_value_from_env_value_from_file"
        )  # Source: Computed Value
        assert conf.key_default == "value_from_default"  # Source: Default Value
        
    # Simulate CLI execution: python app.py --key_cli "value_from_cli"
    runner = CliRunner()
    result = runner.invoke(main, ["--key_cli", "value_from_cli"])
    if result.exception:
        raise result.exception
    assert result.exit_code == 0
finally:
    tmp_dir.cleanup()
```

**Focus on Logic, Not Plumbing**

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.

## Predefined Variables

TheOneConf provides several useful built-in variables that you can use directly in your configuration definitions, especially for variable substitution and dynamic paths. There are two groups of predefined variables:
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:
- XDG base directories for Linux and macOS.
- Standard Windows APPDATA and LOCALAPPDATA directories.

All these paths are unified into OS-independent variables:
- `data_home`: The path to the user-specific data directory.
- `config_home`: The path to the user-specific configuration directory.
- `cache_home`: The path to the user-specific cache directory.

2.  **OS Variables**: These provide information about the operating system and user environment.

- `os_type`: The operating system type (e.g., 'windows', 'linux', 'darwin').
- `user_home`: The path to the current user's home directory.

3. **Execution Stage Variable**: The `exec_stage` variable is particularly useful for differentiating configurations based on the execution stage (i.e. environment), like development, production, or test.
This variable is predefined, but users need to set its value either through the command line or an environment variable. You have different options to pass its value:
- **Command Line Argument**: Using the Click integration, you can pass `--stage`,`--env` or `--exec_stage` argument to set the execution stage.
- **Environment Variable**: You can set the `EXEC_STAGE`, `STAGE` or `ENV` environment variable to define the execution stage.
- **Pass a value to resolve_vars()**: You can also directly pass the execution stage value in the `values` dictionary when calling `resolve_vars()`.
- **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.

## Key Lookup with Fallback

When searching for a variable name (like `server_port`), TheOneConf uses a smart fallback mechanism:

1.  **Exact Key**: Checks for the exact key defined in `ConfigVarDef`.
2.  **Fallback List**: If `env_keys` or `file_keys` is defined as a list, it searches each candidate in order and uses the first one that gives a value.
    *   Example: `env_keys=["MYAPP_PORT", "PORT"]` checks `MYAPP_PORT` first, then `PORT`.
4.  **Variable Composition in keys**: Keys can be ConfigVar expressions whose operands are automatically converted to strings, for example `env_keys=["APP_" + AppConfig.exec_stage + "_PORT"]` or `file_keys=["host-" + AppConfig.exec_stage, "host"]`. Jinja templates remain available when filters or template logic are needed.

This example demonstrates how to use `exec_stage` and `os_type` (predefined variables) to implement context-aware defaults.

```python
import click
import the1conf
import tempfile
from pathlib import Path
from typing import Any
from click.testing import CliRunner

from the1conf import AppConfig, configvar

# 1. Create a dummy config file
tmp_dir = tempfile.TemporaryDirectory()
conf_file = Path(tmp_dir.name) / "myapp" / "conf.yaml"
conf_file.parent.mkdir(parents=True, exist_ok=True)
conf_file.write_text("""
browser-linux-prod: "/usr/bin/google-chrome-stable"
browser-linux-dev: "/usr/bin/google-chrome-beta"
browser-linux:  "google-chrome"
browser-windows-prod: "C:/Program Files/Google/Chrome/Application/chrome.exe"
browser-windows-dev: "C:/Program Files/Google/Chrome Beta/Application/chrome.exe"
browser-windows: "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
browser: "google-chrome"
""")

class MyConfig(AppConfig):
    # This variable will look for keys in this order:
    # 1. 'browser-{os_type}-{exec_stage}' (e.g. browser-linux-prod)
    # 2. 'browser-{os_type}'              (e.g. browser-linux)
    # 3. 'browser'                            (fallback)
    browser_cmd: str = configvar(
        file_keys=[
            "browser-" + AppConfig.os_type + "-" + AppConfig.exec_stage,
            "browser-" + AppConfig.os_type,
            "browser",
        ]
    )

try:
    @click.command()
    # Allow user to specify stage via CLI (e.g. --env prod)
    # exec_stage is a predefined variable in AppConfig
    @the1conf.click_option(AppConfig.exec_stage)
    def main(**kwargs: Any) -> None:
        # 'exec_stage' and 'os_type' are automatically available
        cfg = MyConfig()
        cfg.resolve_vars(conffile_path=conf_file, values=kwargs)

        print(f"Stage: {cfg.exec_stage}")
        print(f"OS: {cfg.os_type}")
        print(f"Browser: {cfg.browser_cmd}")

    # simulate CLI execution like: python app.py --env prod
    # Note: run on linux environment in this context
    
    runner = CliRunner()
    result = runner.invoke(main, ["--env", "prod"])
    if result.exception:
        raise result.exception
    assert result.exit_code == 0
finally:
    tmp_dir.cleanup()
```

## Config File Lookup with Fallback and variable composition

### Config File Lookup with Fallback

You can specify multiple potential locations for configuration files by passing to `resolve_vars()` a list of paths in parameter `conffile_path` (e.g. `['./config.json', '~/.myapp/config.json', '/etc/myapp/config.json']`).

Configuration files can be in **YAML**, **JSON**, or **TOML** format.

#### Example:

```python
import json
import tempfile
from pathlib import Path

from the1conf import AppConfig, configvar

tmp_dir = tempfile.TemporaryDirectory()
root_dir = Path(tmp_dir.name)
dev_conf = root_dir / "conf-dev.json"
dev_conf.unlink(missing_ok=True)

prod_conf = root_dir / "conf-prod.json"
prod_conf.write_text(
    json.dumps({"timeout": 120})
)
class MyConfig(AppConfig):
    timeout: int = configvar(default=30)

try:
    conf = MyConfig()

    conf.resolve_vars(conffile_path=[dev_conf, prod_conf])

    assert conf.timeout == 120
finally:
    tmp_dir.cleanup()
```
### Variable composition in configuration file paths

Configuration file paths can combine variables using either Jinja templates or ConfigVar expressions. Both forms are resolved before TheOneConf looks for the file.

**Important**: With either composition form, variables used in `conffile_path` must be resolved **before** TheOneConf attempts to load the file. This is typically achieved by:
1.  Using **Predefined Variables** (like `exec_stage` or `os_type`) which are available automatically.
2.  Using variables marked with `auto_prolog=True`.
3.  **Cascading Configuration Files**: Resolving the path-dependent variables first, then resolving the rest.

#### Example

```python
import json
import tempfile
from pathlib import Path

from the1conf import AppConfig, configvar

tmp_dir = tempfile.TemporaryDirectory()
root = Path(tmp_dir.name)
win_conf = root / "conf-win.json"
win_conf.unlink(missing_ok=True)

linux_conf = root / "conf-linux.json"
linux_conf.write_text(
    json.dumps({"timeout": 120})
)        

class MyConfig(AppConfig):
    timeout: int = configvar(default=30)

try:
    conf = MyConfig()

    conf.resolve_vars(
        conffile_path=root / ("conf-" + AppConfig.os_type + ".json")
    )

    assert conf.timeout == 120
finally:
    # Cleanup
    tmp_dir.cleanup()  
```

### 🏗️ Cascading Configuration Files

Complex applications often need to load configuration from multiple files depending on the context (e.g. system-wide, user-specific, project-specific, development stage, production stage, ...).

Since TheOneConf allows to call `resolve_vars()` multiple times, you can easily chain multiple calls to achieve **Cascading Configuration**.

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), it will only be resolved the first time a value is found, ignoring subsequent calls even if `allow_override` is set to `True` at the instance level or in the call to `resolve_vars()`

#### Example: Multi-layer loading

```python
import tempfile
from pathlib import Path

from the1conf import AppConfig, configvar

# Creating dummy files for the test
tmp_dir = tempfile.TemporaryDirectory()
root_dir = Path(tmp_dir.name)
app_config_file = root_dir.joinpath("superapp.json")

sys_conf_file = root_dir.joinpath("system_config.yaml")
sys_conf_file.write_text("system_val: system")

user_conf_file = root_dir.joinpath("user_config.yaml")
user_conf_file.write_text("user_val: user")

dev_conf_file = root_dir.joinpath("config-dev.yaml")
dev_conf_file.write_text("dev_val: dev")

class MyAppConfig(AppConfig):
    dev_val: str = configvar(default="init")
    user_val: str = configvar(default="init")
    system_val: str = configvar(default="init")

try:
    conf = MyAppConfig()

    # 1. Load system defaults (lowest priority)
    conf.resolve_vars(conffile_path=sys_conf_file)
    assert conf.system_val == "system"
    assert conf.user_val == "init"
    assert conf.dev_val == "init"
    
    # 2. Load user preferences (override system defaults)
    conf.resolve_vars(conffile_path=user_conf_file, allow_override=True)
    assert conf.system_val == "system"
    assert conf.user_val == "user"
    assert conf.dev_val == "init"
    # 3. Load stage specific config (highest priority)
    # Using substitution: loads 'config-dev.yaml' or 'config-prod.yaml'
    # Manually setting exec_stage for the test since we don't pass it in CLI
    conf.resolve_vars(
        conffile_path=dev_conf_file, allow_override=True, values={"exec_stage": "dev"}
    )

    assert conf.system_val == "system"
    assert conf.user_val == "user"
    assert conf.dev_val == "dev"
finally:
    tmp_dir.cleanup()
```

## 🧠 Dynamic Computed Values (Eval Forms)

An Eval Form is a callable used as a `default` or `transform`. It receives the variable name, the current configuration instance, and the current value. Use it when computing a value requires arbitrary Python logic, such as calling a library API or constructing an object.

```python
import sqlite3

from the1conf import AppConfig, configvar


class DBConfig(AppConfig):
    database_path: str = configvar(default=":memory:")
    connection: sqlite3.Connection = configvar(
        default=lambda _, config, __: sqlite3.connect(config.database_path),
        no_search=True,
    )


conf = DBConfig()
conf.resolve_vars()

try:
    conf.connection.execute("CREATE TABLE status (value TEXT)")
    conf.connection.execute("INSERT INTO status VALUES ('ready')")
    row = conf.connection.execute("SELECT value FROM status").fetchone()
    assert row == ("ready",)
finally:
    conf.connection.close()
```

Here, `database_path` is resolved before the Eval Form runs. The callable then uses it to create a live connection object. `no_search=True` ensures that the connection is always constructed by the callable instead of being loaded from a configuration source. Application code remains responsible for closing resources created this way.

## 🧮 ConfigVar Expressions

Configuration variables can be combined directly with Python operators. The operation is not evaluated while the class is defined: TheOneConf builds an expression tree and resolves it later, when all referenced variables have a value. For ordinary arithmetic, same-type string concatenation, and path composition, choose either ConfigVar expressions or Jinja according to your preferred syntax.

```python
from pathlib import Path

from the1conf import AppConfig, configvar


class ServiceConfig(AppConfig):
    workers: int = configvar(default=4)
    requests_per_worker: int = configvar(default=250)
    capacity: int = configvar(default=workers * requests_per_worker)

    scheme: str = configvar(default="https")
    host: str = configvar(default="api.example.com")
    endpoint: str = configvar(default=scheme + "://" + host)

    data_root: Path = configvar(default="/var/lib/myapp")
    cache_file: Path = configvar(default=data_root / "cache" / "state.db")


config = ServiceConfig()
config.resolve_vars()

assert config.capacity == 1000
assert config.endpoint == "https://api.example.com"
assert config.cache_file == Path("/var/lib/myapp/cache/state.db")
```

Expressions use the target ConfigVar type to perform unambiguous conversions before applying an operator: a `str` target makes `+` concatenate text, a numeric target makes arithmetic operands numeric, and a `Path` target makes `/` compose paths. Search-key expressions are resolved as strings and `conffile_path` expressions as paths. Without a known target, expressions preserve normal Python semantics. Use Jinja for filters, formatting, template conditions, or explicit conversion inside a subexpression.

See the [Usage Guide](USAGE.md#configvar-expressions) for supported operators, dependency order, paths, scopes, inheritance, and repeated resolution.

## 🔄 Data Transformation

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.

```python
from the1conf import AppConfig, configvar

class App(AppConfig):
    # transform: takes the found value (e.g. from env or CLI) and modifies it.
    # Here we ensure the API key is always uppercase and stripped of whitespace.
    api_key: str = configvar(
        default="  default_key  ",
        transform=lambda _, __, val: val.strip().upper() if val else val,
    )

conf = App()
# Pass a value that needs cleaning (whitespace, lowercase)
conf.resolve_vars(values={"api_key": "  my_custom_key  "})

assert conf.api_key == "MY_CUSTOM_KEY"  # Result has been stripped and uppercased

```

## 📂 Path Management

TheOneConf simplifies file-system paths with variable composition, OS-agnostic construction, and built-in directives for resolution and directory creation.

You can specify paths in several ways:

- **As a string**, which is automatically converted to `pathlib.Path`, for example `db_path: Path = configvar(default="data/cache/state.db")`.
- **As a `Path` object** when no configuration variable is involved, for example `db_path: Path = configvar(default=Path("data") / "cache" / "state.db")`.
- **As a ConfigVar expression using `/`** :
    ```python
    root_dir: str = configvar(default="/srv")
    app_name: str = configvar(default="myapp")
    app_dir: Path = configvar(default=root_dir / app_name / "cache")

    # works as well with Path objects
    path_root_dir: Path = configvar(default="/srv")
    from_path_dir: Path = configvar(default=path_root_dir / "myapp" / "cache")
    ```
- as a **Jinja template**, for example `db_path: Path = configvar(default="{{ root_dir }}/{{ app_name }}/cache")`.

You can choose the method that best fits your style or the complexity of your path construction.

See [Path values](USAGE.md#path-values) for ConfigVar expressions, Jinja templates, and dynamic configuration-file paths.

### Path Directives

When defining a configuration variable of type `Path`, you can use special directives to control how paths are resolved and managed:

- **`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).
- **`make_dirs`**: Automatically creates the directory hierarchy if it doesn't exist.

```python
import os
import shutil
from pathlib import Path

from the1conf import AppConfig, configvar
from the1conf.app_config import PathType

 # Set environment variable for the example
os.environ["APP_BASE_DIR"] = "/tmp/my_app_from_env"

class IOConfig(AppConfig):
    # Validates that 'base_dir' is a path
    # no_value_search=True means we ignore values passed in resolve_vars() dict
    base_dir: Path = configvar(
        default=Path("/tmp") / "default_data",
        env_keys="APP_BASE_DIR",
        no_value_search=True,
    )

    # If 'log_dir' is relative (e.g. "logs"), it becomes "{base_dir}/logs"
    # make_dirs=PathType.Dir ensures the directory is created on resolution
    log_dir: Path = configvar(
        default="logs", can_be_relative_to="base_dir", make_dirs=PathType.Dir
    )

    cache_file: Path = configvar(
        default=Path("cache") / "db.sqlite",
        can_be_relative_to="base_dir",
        make_dirs=PathType.File,
    )

conf = IOConfig()
# Pass a value for base_dir, but it will be IGNORED due to no_value_search=True
conf.resolve_vars(values={"base_dir": "/tmp/ignored_path"})

# Verification:
# 1. base_dir came from ENV: "APP_BASE_DIR" => "/tmp/my_app_from_env"
expected_base = Path("/tmp/my_app_from_env")
# 2. log_dir is resolved relative to base_dir
assert conf.log_dir == expected_base / "logs"
assert conf.log_dir.is_dir()  # Directory was automatically created

# Clean up for the example
if conf.base_dir.exists():
    shutil.rmtree(conf.base_dir)

```

## 📦 Nested Configurations (Namespaces)

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`).

Important: Namespaces are also used in searching configuration files (e.g. `db.host` looks for `{"db": {"host": ...}}` in YAML/JSON).

A scope can be applied to every variable in a namespace by declaring it on the
namespace class: `class db(NameSpace, scopes="db"):`. Nested namespaces inherit
this default scope.

To use a root variable or a variable from another namespace inside a namespace,
declare a local alias with `ref()`:

```python
from the1conf import AppConfig, NameSpace, configvar, ref


class MyApp(AppConfig):
    app_name: str = configvar(default="myapp")

    class db(NameSpace):
        app_name = ref("app_name")
        database_name: str = configvar(default=app_name + ".db")
```

See [USAGE.md](USAGE.md#referencing-variables-from-a-namespace) for qualified
references, path bases, and resolution-order rules.

```python
from pathlib import Path
from the1conf import AppConfig, NameSpace, configvar
import tempfile
import os

# config.yaml
# Creating dummy files for the test
tmp_dir = tempfile.TemporaryDirectory()
root_dir = Path(tmp_dir.name)
conf_file = root_dir / "config.yaml"
conf_file.write_text(
    """
env: "prod"
db:
    auth:
        username: "admin"
"""
)
os.environ["DB_PASSWORD"] = "secret"
os.environ["db.host"] = "app.dot.com"

class MyApp(AppConfig):
    env: str = configvar(default="dev")

    # Define a 'db' namespace
    class db(NameSpace):
        host: str = configvar(default="localhost")
        port: int = configvar(default=5432)

        # Nested namespaces can be infinitely deep
        class auth(NameSpace):
            username: str = configvar(default="admin")
            password: str = configvar(env_keys="DB_PASSWORD")
        class log(NameSpace):
            log_type: str = configvar(default="file")
try:
    conf = MyApp()
    # Resolve variables loading the config.yaml file defined above
    conf.resolve_vars(
        conffile_path=conf_file,
        values={"env": "prod", "db.log.log_type": "console"},
    )

    # Check that values are loaded from the file
    assert conf.env == "prod"  # from config.yaml
    assert conf.db.log.log_type == "console"  # from CLI args style
    assert conf.db.host == "app.dot.com"  # from environment variable
    assert conf.db.port == 5432  # from default (not defined in file or env)
    assert conf.db.auth.username == "admin"  # from config.yaml
    assert conf.db.auth.password == "secret"  # from environment variable
finally:
    tmp_dir.cleanup()
```

## 🌍 Scopes (Environment Awareness)

By tagging variables with `scope`, you can define which settings are relevant for a specific resolution. For instance if you have different modes for your application (e.g. "server", "client", "test") you can specify the active scope(s), and TheOneConf will ignore any variable not belonging to them. Scopes are also useful when configuration files are cascading (e.g. system config, user config, local config, db configuration file, ui configuration file) and you want to load only the relevant variables for each file.

**Why it matters**:

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.

```python
import the1conf

class ToolConfig(the1conf.AppConfig):
    # Common variable (available in all scope)
    verbose: bool = the1conf.configvar(default=False)
    """Enable verbose logging"""

    # Variable specific to 'server' scope
    port: int = the1conf.configvar(default=8080, scopes=["server"])
    """Port to listen on"""

    # Variable specific to 'client' scope
    timeout: int = the1conf.configvar(default=30, scopes=["client"])
    """Connection timeout"""

conf = ToolConfig()

# 1. Resolve for SERVER scope
# Only 'verbose' and 'port' are resolved. 'timeout' is ignored.
conf.resolve_vars(scopes=["server"])

assert conf.port == 8080  # Available in 'server' scope
assert conf.verbose is False  # Common variable
assert not hasattr(conf, "timeout")  # Ignored variable

# 2. Resolve for CLIENT scope (simulating a fresh run for clarity)
conf2 = ToolConfig()
conf2.resolve_vars(scopes=["client"])

assert conf2.timeout == 30  # Available in 'client' scope
assert conf2.verbose is False  # Common variable
assert not hasattr(conf2, "port")  # Ignored variable
```

## 🏗️ Inheritance and Extensibility (Decentralization)

One of the strengths of TheOneConf is its support for standard Python inheritance to achieve **decentralized configuration**. You can split your configuration definitions across multiple classes (e.g., one per module or component).

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 parents' content and the local content.

```python
import the1conf

# Base Component Configuration
class LogConfig(the1conf.AppConfig):
    verbose: bool = the1conf.configvar(default=False)
    log_file: str = the1conf.configvar(default="app.log")

# App Configuration extends the Component Config
class AppConfigExample(LogConfig):
    # We inherit 'verbose' and 'log_file'
    # And we add new specific variables
    port: int = the1conf.configvar(default=8080)

    # We can also override defaults
    log_file: str = the1conf.configvar(default="server.log")

conf = AppConfigExample()
conf.resolve_vars()

# variable from Base
assert conf.verbose is False
# overwritten variable
assert conf.log_file == "server.log"
# new variable

assert conf.port == 8080
```

**Modular & Independent Configuration**

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.

## 🛡️ Type Casting and Validation

TheOneConf uses standard Python type hints to convert configuration values and plain callables to enforce application constraints. 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.

```python
import pytest
from the1conf import AppConfig, configvar
from the1conf.app_config import AppConfigException
class ServerConfig(AppConfig):
    port: int = configvar(
        default=8080,
        validators=[lambda value: value > 1024, lambda value: value < 65536],
    )
    max_workers: int = configvar(
        default=4,
        validators=lambda value: value > 0,
    )
    

conf = ServerConfig()

# Simulate loading values from environment variables (strings)
conf.resolve_vars(
    values={
        "port": "9000",  # Cast: "9000" -> 9000 (int)
        "max_workers": "10",  # Cast & Validate: "10" -> 10 (int)
    }
)

assert conf.port == 9000
assert conf.max_workers == 10


# Validation ensures integrity
try:
    # Use a new instance to ensure we don't skip the variable because it's already set
    conf_invalid = ServerConfig()
    # Invalid: end_date before start_date
    conf_invalid.resolve_vars(
        values={"port": "80"}
    )
except AppConfigException:
    print("Validation correctly rejected an invalid configuration value")
else:
    pytest.fail("Validation should have failed")
```

**Powerful & Safe Configuration**

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.

## Saving Configuration

`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.

### `store_conf_infile`

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).

```python
from collections.abc import Sequence
from pathlib import Path
from typing import Union

def store_conf_infile(
    self,
    file: Union[Path, str],
    *,
    namespaces: Sequence[str] = [],
    scopes: Sequence[str] = [],
    type: str = "yaml",
) -> None:
    ...
```
## 🖱️ Click Integration

TheOneConf integrates seamlessly with [Click](https://click.palletsprojects.com/) to inject configuration variables into your CLI. The `@the1conf.click_option` decorator creates a click option from a `ConfigVarDef`.

**Benefits:**
1.  **Zero Boilerplate**: No need to manually define `click.option('--port', default=8080, help='...')`. TheOneConf infers everything from your class definition.
2.  **Single Source of Truth**: Change the default value or help text in your Config class, and the CLI updates automatically.
3.  **Complex Features Support**: It works seamlessly with TheOneConf's features like type casting, validation, and multi-source resolution.

```python
import click
import the1conf
from typing import Any
from click.testing import CliRunner

class MyConfig(the1conf.AppConfig):
    name: str = the1conf.configvar(default="World")
    """Name to greet. Help text is automatically exposed in --help"""

    port: int = the1conf.configvar(default=8080)
    """Server port"""

@click.command()
# Automatically generate --name and --port options
@the1conf.click_option(MyConfig.name)
@the1conf.click_option(MyConfig.port)
def main(**kwargs: Any) -> None:
    cfg = MyConfig()
    # Apply CLI args (highest priority) -> Env -> Files -> Defaults
    cfg.resolve_vars(values=kwargs)

    assert cfg.name == "Alice"
    assert cfg.port == 9000
    print(f"Server starting on {cfg.port} for {cfg.name}...")

# Simulate CLI execution: app.py --name Alice --port 9000
runner = CliRunner()
result = runner.invoke(main, ["--name", "Alice", "--port", "9000"])

if result.exit_code != 0:
    print(result.output)

assert result.exit_code == 0
assert "Server starting on 9000 for Alice..." in result.output
```


