Metadata-Version: 2.5
Name: typedconf
Version: 0.9.5
Summary: A type-safe configuration management library for Python
Project-URL: Repository, https://codeberg.org/the_pi/typedconf
Project-URL: Issues, https://codeberg.org/the_pi/typedconfissues
Author: the_pi
License-Expression: MIT
Keywords: cli,configuration,env,json,pydantic,settings,toml,type-safe,yaml
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pydantic>=2.11.7
Provides-Extra: all
Requires-Dist: python-dotenv>=0.9.9; extra == 'all'
Requires-Dist: pyyaml>=6.0.3; extra == 'all'
Requires-Dist: tomli-w>=1.2.0; extra == 'all'
Requires-Dist: tomli>=1.2.0; (python_version < '3.11') and extra == 'all'
Provides-Extra: dev
Requires-Dist: pytest>=9.0.3; extra == 'dev'
Requires-Dist: python-dotenv>=0.9.9; extra == 'dev'
Requires-Dist: pyyaml>=6.0.3; extra == 'dev'
Requires-Dist: tomli-w>=1.2.0; extra == 'dev'
Requires-Dist: tomli>=1.2.0; (python_version < '3.11') and extra == 'dev'
Provides-Extra: dotenv
Requires-Dist: python-dotenv>=0.9.9; extra == 'dotenv'
Provides-Extra: toml-export
Requires-Dist: tomli-w>=1.2.0; extra == 'toml-export'
Provides-Extra: toml-read
Requires-Dist: tomli>=1.2.0; (python_version < '3.11') and extra == 'toml-read'
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0.3; extra == 'yaml'
Description-Content-Type: text/markdown

# TypedConf

A lightweight, type-safe configuration management library powered by Pydantic. Following the [12-factor application guide](https://12factor.net/config), it centralizes your application configuration/settings by merging data from multiple sources with a defined priority: 
`env > cli > files > payload > defaults`.

TypedConf combines the strengths of Pydantic and Dynaconf: While it requires developers to define classes to 
reflect the configuration structure (unlike [Dynaconf](https://www.dynaconf.com/)), this approach heavily 
reduces headaches 🤕🤕😠 caused by mistyped property names, wrong types, or formatting errors.
TypedConf offers comprehensive IDE support, type safety, and runtime data validation, 
powered by [Pydantic](https://pydantic.dev/docs/validation/latest/get-started/why/).

Thanks to Pydantic, after parsing and validation, TypedConf guarantees that the fields of the resultant model instance will conform to the field types defined on the subclass of ConfigModel. TypedConf offers comprehensive IDE support, type safety, and runtime data validation.

## Key Features

- **Type-Safe**: Built on Pydantic, ensuring configuration values are validated at runtime.
- **IDE and REPL Support**: Full type-hinting and IntelliSense/code-completion support for seamless development.
- **Nested Support**: Easily handle complex configuration structures.
- **TOML, YAML and JSON** Interface: Load configuration from various file formats.
- **CLI and ENV**ironment Interface: Load configuration data from CLI arguments (`--cfg_myint=1`) and ENV variables (`export CFG_MYINT=1`).
- **Layered Configuration**: Merges configuration data with a clear priority: `env > cli > files  > payload > defaults` (env has highest priority).
- **Immutability**: Configuration data is read-only (frozen) by default after loading.
- **Self-Documenting**: Generate help text directly from your configuration schema definition.

---

## Quick Start

Instead of defining scattered constants at the beginning of a script, use `ConfigModel` to manage your application 
configuration centrally, type-safely, and with built-in validation.

```python
from typedconf import ConfigModel

# Define configuration schema
class AppConfig(ConfigModel):
    app_name: str       # Required field, no default
    port: int = 8080    # Default value

# Load configuration from payload
conf = AppConfig.load(payload={'app_name': 'app'})
print(f"Running {conf.app_name} on port {conf.port}")   # Running app on port 8080
```

![IDE & REPL auto-completion demo](doc/out_show_repl_autocomplete.gif)


## Load Configuration from TOML, YAML or JSON

Most of the time you will load data from a configuration file. Let's say from this TOML file (`config.toml`):

```toml
# config.toml
app_name = "myapp"
port = 2000
```

```python
from typedconf import ConfigModel

class AppConfig(ConfigModel):
    app_name: str
    port: int = 8080

# Load configuration from defaults and toml-file
conf = AppConfig.load(file='config.toml')
print(f"Running {conf.app_name} on port {conf.port}")   # Running myapp on port 2000
```

Some uses cases need more than just a single configuration file. So there are few ways to load data from files:
- `file` - A single configuration file to load (toml, json, yaml - based on the file extension)
- `toml` - List of TOML files
- `yaml` - List of YAML files 
- `json` - List of JSON files

The files will be loaded and deep-merged in this order: file > toml > yaml > json  (file has highest priority). 
Lists will be loaded from left (idx=0) to right. 

Default behaviour for loadiing data from files is "Raise a `ConfigError` on any file error (i.e. file not found, parser error)". Set `skip_errors=True` to override this behaviour, like saying "YES, ignore all file and parsing errors and continue loading and validating data".

*Note: 
loading yaml files requires optional python package `pyyaml`. 
loading toml-files on python < 3.11 requires optional python package `tomli`.*

## Data Validation and Documentation

`ConfigModel` is a Pydantic [`BaseModel`](https://pydantic.dev/docs/validation/dev/get-started/why/). 
You can use [`Field`](https://pydantic.dev/docs/validation/dev/concepts/fields/) to add descriptions, constraints, or validation rules.

Loading a configuration means:
1. load and parse data from all sources.
2. deep-merge all data together, where data from higher priority source overwrites data from lower priority sources.
3. create and validate a new ConfigModel instance with the merged data.

Loading the configuration may raise a `ConfigError` (on step 1 or 3). ConfigError is just a wrapper around pydantic's [ValidationError](https://pydantic.dev/docs/validation/dev/errors/errors/). 

```python
from pydantic import Field
from typedconf import ConfigModel, ConfigError

class AppConfig(ConfigModel):
    app_name: str = Field(..., description="Application name. required field.")
    port: int = Field(8080, ge=1000, le=9999, description="Port to listen on (1000-9999). optional field. default 8080")

try:
    conf1 = AppConfig.load(file='config.toml')                      # ok!
    print(f"Running {conf.app_name} on port {conf.port}")           # Running myapp on port 2000

    conf2 = AppConfig.load(payload={'app_name': 'app', 'port': 20}) # fail! port is invalid
except ConfigError as e:
    print("Validation failed! Can't load configuration:", e)
```


## Nested Configuration

Nested configurations can be applied by nesting `ConfigModel` classes.

```python
from pydantic import Field
from typedconf import ConfigModel, ConfigError

class DatabaseConfig(ConfigModel):
    con: str = Field(..., description="DB connection string")
    user: str = Field(..., description="DB username")
    pwd: str = Field(..., description="DB password")

class AppConfig(ConfigModel):
    app_name: str = Field(..., description="Application name")
    port: int = Field(8080, ge=1000, le=9999, description="Port")
    db: DatabaseConfig = Field(default_factory=DatabaseConfig, description="Database configuration")

try:
    conf = AppConfig.load(file='config.toml')
    print(f"Running {conf.app_name} on port {conf.port}. DB user: {conf.db.user}")
except ConfigError as e:
    print(e)

```

If your TOML file doesn't match the new nested structure (e.g., missing `[db]` section), TypedConf/Pydantic will raise a `ConfigError`:

```text
3 validation errors for DatabaseConfig 
con Field required [type=missing, ...]
user Field required [type=missing, ...]
pwd Field required [type=missing, ...]

```

Let's fix our TOML file. *Remember: It's best practice not to store sensitive data like passwords inside configuration files!*

```toml
# config.toml
app_name = "toml-app"
port = 9090

[db]
con = "postgresql://localhost:5432/mydb"
user = "db_user_readonly"

```

## ENV & CLI Interface

You can inject missing or secret data (like database passwords) through the CLI or environment variables. Both interfaces are enabled by default.

```sh
# CLI interface
$ python app.py --cfg_db__pwd="secret"
Running toml-app on port 9090.

# Environment variables interface
$ export CFG_DB__PWD="secret"
$ python app.py

# Combining them
$ export CFG_DB__USER="db_user_admin"
$ export CFG_DB__PWD="secret"
$ CFG_PORT=2525 python app.py --cfg_app_name="cli-app"

```

**Conventions:**

* Case sensitivity: CLI is *lowercase*, ENV is *UPPERCASE*.
* CLI uses long format arguments like `--key=val`.
* Prefix: Both use `cfg_` by default (customizable via `cli_prefix`).
* Separator: Nested fields are separated by `__` (customizable via `cli_separator`).

## Priority Chain

TypedConf merges all data sources in a specific order. Higher-priority sources overwrite lower-priority ones:

1. **Environment Variables (Highest):** e.g., `export CFG_DB__PWD="abc"`
2. **CLI Arguments:** e.g., `--cfg_db__pwd='abc'`
3. **Single File**
4. **TOML Files:** Merged from left to right
5. **YAML Files:** Merged from left to right
3. **JSON Files:** Merged from left to right
5. **Payload:** Dictionary passed directly via `.load(payload=...)`
6. **Defaults (Lowest):** Defined inside the `ConfigModel` class

*Note: The system performs a deep merge, preserving nested structures when partial overrides are provided.*


## CLI Help included

TypedConf can include a `--help` argument to your application and generates a nice helptext for all field-names. Let's step back to our *nested configuration example* and add help for the user by using `user_needs_help()` 

```python
from pydantic import Field
from typedconf import ConfigModel, ConfigError

RFC3986_URI_REGEX = r'^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})(?::\d+)?([\/\w \.-]*)*\/?$'

# define configuration schema
class DatabaseConfig(ConfigModel):
    con: str = Field(..., pattern=RFC3986_URI_REGEX, description="DB connection-string, required field.")
    user: str = Field(..., description="DB username, required field.")
    pwd: str = Field(..., description="DB password, required field.")

class AppConfig(ConfigModel):
    app_name: str = Field(..., description="application name, required field.")
    port: int = Field(8080, gt=1000, lt=9999, description="application listen on port. Between 1000 and 9999, defaullt=8080")
    db: DatabaseConfig = Field(default_factory=DatabaseConfig, description="database configuration")

# need some help?
if AppConfig.user_needs_help():
    print(f"MYAPP Help\n\nAvailable CLI Parameter\n{AppConfig.get_cli_helptext()}")
    exit(0)

# Load configuration
try:
    conf = AppConfig.load(file='config.toml')
    print(f"Running {conf.app_name} on port {conf.port}. DB connected {conf.db.user} @ {conf.db.con}")
except ConfigError as e:
    print(e)
```

Same effect creates `cli_help_enabled=True` while loading. This raises a `UserNeedsHelp` Exception, containing a propper version of the  help-text, with respect to `cli_prefix` and `cli_separator`.

```python
try:
    conf = AppConfig.load(cli_help_enabled=True, file='config.toml')
    print(f"Running {conf.app_name} on port {conf.port}")
except UserNeedsHelp as e:
    print(f"MYAPP Help\n\nAvailable CLI Parameter\n{e}")
    exit(0)
except ConfigError as e:
    print(e)
```

However, TypedConf adds in both versions an additional `--help` flag to the CLI interface:
```text
$ python main.py --help
MYAPP Help

Available CLI Parameter
--cfg_app_name (AppConfig.app_name)
   type=str, default=None
   application name, required field.

--cfg_db__con (DatabaseConfig.con)
   type=str, default=None
   DB connection-string, required field.

--cfg_db__pwd (DatabaseConfig.pwd)
   type=str, default=None
   DB password, required field.

--cfg_db__user (DatabaseConfig.user)
   type=str, default=None
   DB username, required field.

--cfg_port (AppConfig.port)
   type=int, default=8080
   application listen on port. Between 1000 and 9999, defaullt=8080
```


## Mutable Configuration

ConfigModel sets the pydantic model_config to:

```python
model_config = {
    "frozen": True,
    "extra": "forbid",
    "validate_default": True,
    "validate_assignment": True,
}
```

- Set ConfigModel to readonly
- Raise error, when loading unknown extra data
- Validate default values when loading
- validate when assigning a new value to a writeable ConfigModel

If you explicitly need a mutable/writable configuration at runtime, set (all nested) Pydantic's `frozen` Model_config to `False`:

```python
class AppConfig(ConfigModel):
    model_config = {'frozen': False}  # Makes pydantic basemodel mutable
    
    app_name: str
    port: int = 8080

conf = AppConfig.load(payload={'app_name': 'test'})
conf.port = 9999  # is now valid

```

## Exporting Configurations

Export your current configuration instance to TOML, YAML or JSON format.

```python
print(conf.dumps_toml())
print(conf.dumps_yaml())
print(conf.dumps_json())
```

*Note: Exporting to TOML requires python package `tomli-w`, loading and exporting yaml requires package `pyyaml`*



## Install 

```bash
pip install typedconf                          # minimal — nur pydantic
pip install "typedconf[dotenv]"                # .env support
pip install "typedconf[toml_export]"           # TOML export (dumps_toml)
pip install "typedconf[toml_read]"             # TOML import (python <3.11)
pip install "typedconf[yaml]"                  # YAML support (load and dumps)
pip install "typedconf[all]"                   # TOML write, YAML, dotenv
pip install "typedconf[dev]"                   # development: all + pytest
pip install "typedconf[dotenv,toml_export]"
```

---

## TODOs
- ⚙️ codeberg ci-pipeline: pytest on every push, publish to PyPi on new "v*" tag
- ✔️ YAML support
- not-ok: ✖️  unknown: ❔


## Changelog

- v0.9.4
  - YAML Support
  - load a single-File (toml or json or yaml))
- v0.9.3
  - published to PyPi
- v0.9.0 
  - cleanup code for 1st release/tag

## License

This project is licensed under the GPL-3.0 License - see the [LICENSE](https://www.google.com/search?q=LICENSE) file for details.


## Demos

### Demo-A

```python
from typedconf import ConfigModel

class MyConfig(ConfigModel):
    app: str = 'myapp'
    log_enabled: bool = False
    tags: list[str] = ['a', 'b']

conf = MyConfig.load()
print(conf)     # app='myapp' log_enabled=False tags=['a', 'b']
```


### Demo-B

```python
from pydantic import Field
from typedconf import ConfigError, ConfigModel, UserNeedsHelp

import logging
#logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')

class DatabaseConfig(ConfigModel):
    con: str = Field(..., description="DB connection string")
    user: str = Field(..., description="DB username")
    pwd: str = Field(..., description="DB password")

class AppConfig(ConfigModel):
    app_name: str = Field(..., description="Application name")
    port: int = Field(8080, ge=1000, le=9999, description="Port")
    db: DatabaseConfig = Field(default_factory=DatabaseConfig, description="Database configuration")

try:
    conf = AppConfig.load(file='config_b.toml', cli_help_enabled=True)
    print(f"Running app {conf.app_name} on port {conf.port}, DB {conf.db.user}@{conf.db.con} pwd={''.join('*' for _ in conf.db.pwd)}")
except UserNeedsHelp as hlp:
    print(hlp)
except ConfigError as e:
    print(e)

```

```toml
# config_b.toml
app_name="TOML-app3"
port=7777

[db]
con="https://example.com/db"
user="db_readonly"
```

```text
bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_b.py
<typedconf> Loading field 'pwd' failed! 1 validation error for AppConfig db.pwd Field required [type=missing, input_value={'con': 'https://example...., 'user': 'db_readonly'}, input_type=dict] For further information visit https://errors.pydantic.dev/2.11/v/missing
1 validation error for AppConfig db.pwd Field required [type=missing, input_value={'con': 'https://example...., 'user': 'db_readonly'}, input_type=dict] For further information visit https://errors.pydantic.dev/2.11/v/missing

bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_b.py --help
--cfg_app_name (AppConfig.app_name)
  type=str, default=None
  Application name

--cfg_db__con (DatabaseConfig.con)
  type=str, default=None
  DB connection string

--cfg_db__pwd (DatabaseConfig.pwd)
  type=str, default=None
  DB password

--cfg_db__user (DatabaseConfig.user)
  type=str, default=None
  DB username

--cfg_port (AppConfig.port)
  type=int, default=8080
  Port

bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_b.py --cfg_db__pwd=asdfsd
Running app TOML-app3 on port 7777, DB db_readonly@https://example.com/db pwd=******
```

Animated GIF

### Demo-C

```python
"""TypedConf Demo-C"""
import logging
from typedconf import ConfigModel, ConfigError, UserNeedsHelp
from pydantic import Field

logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')
RFC3986_URI_REGEX = r'^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})(?::\d+)?([\/\w \.-]*)*\/?$'


## CONFIGURATION
class MyConfig(ConfigModel):
    """Configuration for this script. In it's core ConfigModel is a pydantic Basemodel"""
    app: str
    log_enabled: bool = False
    loglevel: int = Field(0, ge=0, le=2, description="database log-level: 0=no logging, 1=errors, 2=info. Default 0")
    url: str = Field("http://myserver.com:1234/mydatabase", pattern=RFC3986_URI_REGEX, description="load data from this URL. validates url against RFC3986")
    tags: list[str] = Field(['a', 'b'], description="A list of strings. Hint: json list-style input is available in CLI, but not in ENV")


## MAIN
script_fn = sys.argv[0]
try:
    conf = MyConfig.load(
        payload={'app':'payload-app'},  # source payload
        file='config_c.yaml',      # load data from a single file (toml,json,yaml)
        toml=['config_c.toml'],    # load data from TOML file-list
        yaml=['config_c.yaml'],
        json=['config_c.json'],
        cli_help_enabled=True,  # enable --help in CLI, raises UserNeedsHelp
        skip_errors = True,      # ignore errors while loading and parsing files. Default = False. NB: skip_errors does not effect pydantic's validation
    )
    print(f"== JSON {'=' * 42}\n{conf.dumps_json()}\n{'=' * 50}") # conf.dumps_toml() available, if python package 'tomli_w' is installed

except UserNeedsHelp as e:
    help_config_fields = e
    help_message = f"""\
TypedConfig Demo-01.
Loads config from default, payload, toml-file. Available CLI arguments / ENV variables
{help_config_fields}

Examples:
uv run {script_fn}  # loads config from defaults and payload (and files)
uv run {script_fn} --cfg_app=cli-app  # overwrite config from CLI
uv run {script_fn} --cfg_app=cli-app --cfg_loglevel=2 --cfg_log_enabled=1 --cfg_tags='["x","y"]'
CFG_APP='env-app' uv run {script_fn} --cfg_app=cli-app  # ENV overrides CLI
uv run {script_fn} --cfg_loglevel=9  # validation failed
"""
    print(help_message)

    exit(1)
except ConfigError as e:
    print(f"⚠️ ConfigError raised! {e}")
    exit(2)

print(f"Try `uv run {script_fn} --help`")
```

config_c.toml
```toml
app="TOML-app"
tags=["XXX", "YYY", "ZZZ"]
```

config_c.json
```json

{
  "app": "JSON-app",
  "log_enabled": true
}
```

config_c.yaml
```yaml
app: "YAML-app"
tags:
  - "XXX"
  - "YYY"
  - "ZZZ"
  - "AAA"

```

```text
bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_c.py
DEBUG - <typedconf> LibWrapper using python 3.12.3, package to read toml [tomllib=True, tomli=False], optional package to write toml [tomli_w=True], optional package [dotenv=True, pyyaml=True]
DEBUG - <typedconf> json read from config_c.json
DEBUG - <typedconf> yaml data read from: config_c.yaml
DEBUG - <typedconf> toml data read from: config_c.toml
DEBUG - <typedconf> yaml data read from: config_c.yaml
DEBUG - <typedconf> cli nothing found. prefix: '--cfg_' seperator: '__'
DEBUG - <typedconf> env nothing found. prefix: 'CFG_' seperator: '__'
INFO - <typedconf> ConfigModel 'MyConfig' loaded (readonly)
== JSON ==========================================
{
  "app": "YAML-app",
  "log_enabled": true,
  "loglevel": 0,
  "url": "http://myserver.com:1234/mydatabase",
  "tags": [
    "XXX",
    "YYY",
    "ZZZ",
    "AAA"
  ]
}
==================================================
Try `uv run main_demo_c.py --help`
bernd@Venus:~/projects/typedconf/demo$
```


### Demo-D

```python
import logging
from enum import Enum
from pydantic import Field
from typedconf import ConfigError, ConfigModel, UserNeedsHelp

logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')
script_fn = sys.argv[0]


class StatusEnum(str,Enum):
    none  = "NONE"
    ok    = "OK"
    nok   = "NOT-OK"
    warn  = "WARNING"
    err   = "ERROR"

## configuration-schema
class L2(ConfigModel):
    name:str = 'L2'
    d2: int = Field(2, description="data L2-D2")
    d2b: list[str] = Field(['a','b','c'], description="list-data L2-D2b")
    d2c: int = Field(1111, ge=1000, le=9999, description="int-data L2-D2c, validate between 1000 and 9999.")
    d2d: StatusEnum = Field(StatusEnum.none, description="Last known status. Default none")

class L1(ConfigModel):
    name:str = 'L1'
    d1: int = Field(1, description="data L1-D1")
    l2: L2 = Field(default_factory=L2, description="link to a L2")
    
class L0(ConfigModel):
    name:str = 'L0'
    d0: int = Field(0, description="data L0-D0")
    l1: L1 = Field(default_factory=L1, description="link to a L1")

## main
try:
    conf = L0.load(
        payload={'name':'payload_L0', 'l1':{'name':'payload_L1', 'l2':{'d2c':2222, 'd2d':StatusEnum.ok}}},
        cli_help_enabled=True,          # --help enabled
        cli_prefix='',                  # no prefix for cli, env is disabled
        env=False,
        cli=True,
    )
    if conf.dumps_toml():
        print(f"== TOML {'=' * 42}\n{conf.dumps_toml()}\n{'=' * 50}")   # optional tomli_w is installed :)
    else:
        print(f"== JSON {'=' * 42}\n{conf.dumps_json()}\n{'=' * 50}")   # json fallback

except UserNeedsHelp as e:
    cli_args_helptxt = e
    print(f"""\
TypedConfig Demo-D. Available cli/env arguments:

{cli_args_helptxt}

Examples:
# 1) loads config from defaults and payload
  uv run {script_fn}
# 2) cli interface (no prefix, env is disabled)
  uv run {script_fn}  --name='cli_L0'
  uv run {script_fn} --l1__l2__d2d=ERROR         # using enum-values
  uv run {script_fn} --l1__l2__d2b='["x","y"]'   # using json-style array
# 3) data validation
  uv run {script_fn} --l1__l2__d2c=7777          # ok
  uv run {script_fn} --l1__l2__d2c=77            # fail
  uv run {script_fn} --l1__l2__d2d='WARNING      # ok
  uv run {script_fn} --l1__l2__d2d='UNKNOWN'     # fail
""")
    exit(1)

except ConfigError as e:
    print(f"Configuration ERROR: {e}")
    exit(2)

print(f"Try `uv run {script_fn} --help`")
```

```text
bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_d.py
DEBUG - <typedconf> LibWrapper using python 3.12.3, package to read toml [tomllib=True, tomli=False], optional package to write toml [tomli_w=True], optional package [dotenv=True, pyyaml=True]
DEBUG - <typedconf> cli nothing found. prefix: '--' seperator: '__'
INFO - <typedconf> ConfigModel 'L0' loaded (readonly)
== TOML ==========================================
name = "payload_L0"
d0 = 0

[l1]
name = "payload_L1"
d1 = 1

[l1.l2]
name = "L2"
d2 = 2
d2b = [
    "a",
    "b",
    "c",
]
d2c = 2222
d2d = "OK"

==================================================
Try `uv run main_demo_d.py --help`
bernd@Venus:~/projects/typedconf/demo$
```
