Metadata-Version: 2.4
Name: kaava
Version: 2026.6.20
Summary: Load typed Python configuration from YAML, TOML, or JSON into dataclasses, with validation, source tracing, environment-variable overlays, and a diagnostic CLI.
Author-email: Stefan Hagen <stefan@hagen.link>
Maintainer-email: Stefan Hagen <stefan@hagen.link>
License-Expression: MIT
Project-URL: Documentation, https://codes.dilettant.life/docs/kaava
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# kaava

Load typed Python configuration from YAML, TOML, or JSON into dataclasses,
with validation, source tracing, environment-variable overlays, and a diagnostic CLI.

Requires Python 3.11 or later.
YAML support uses PyYAML; TOML and JSON use the standard library.

## Install

```
pip install kaava
```

## Quickstart

Define your configuration shape as a plain dataclass and call `load()`.

```python
import dataclasses
from kaava import conf_field, load


@dataclasses.dataclass
class DBConfig:
    host: str = conf_field(description='database host')
    port: int = conf_field(default=5432)


@dataclasses.dataclass
class AppConfig:
    name: str = conf_field(description='application name')
    db: DBConfig = conf_field(description='database connection')
    debug: bool = conf_field(default=False)


cfg = load(AppConfig, 'config.yaml')
print(cfg.name, cfg.db.host, cfg.db.port)
```

`config.yaml`:

```yaml
name: myapp
db:
  host: localhost
```

Fields with no `default` or `default_factory` are required.
Nested dataclasses map to YAML mappings and are built recursively.
`load()` raises on the first error encountered; use `validate()` or `load_valid()` to collect all errors.

For a quick feature-by-feature tour, see `quickstart/README.md`.
For a step-by-step tutorial that builds a real command-line tool from scratch, see `tutorial/README.md`.

## Field metadata

`conf_field()` extends `dataclasses.field()` with configuration-specific metadata.

- `default` / `default_factory` — the field's default value or factory
- `description` — shown in `explain()` output and in YAML templates produced by `eject()`
- `env` — explicit environment-variable name used by `env_overlay()`
- `secret` — when `True`, `explain()` masks the value; useful for passwords and API keys
- `validator` — a callable `(value) -> bool | str`; a falsy return or raised `ValueError` becomes a `ValidationError`

## Sources

`load()` accepts one or more file paths (`str` or `pathlib.Path`) or `https://` URLs.
The format is inferred from the file extension (`.yaml` / `.yml`, `.toml`, `.json`); YAML is the default for unrecognised extensions.

```python
cfg = load(AppConfig, 'base.yaml', 'prod.yaml')
```

Sources are applied in order — later sources deep-merge over earlier ones.
A nested key in a later source only replaces its own subtree,
leaving sibling keys from earlier sources intact.

### Auto-discovery

When called with no sources, `load()` searches for configuration in standard locations, in this order:

- `~/.config/kaava.toml` — user-level defaults
- `pyproject.toml`, `[tool.kaava]` section — project-level, if the section exists
- `kaava.toml`, `kaava.yaml`, or `kaava.json` in the working directory

Later entries override earlier ones via the same deep-merge.
Passing at least one explicit source skips discovery entirely.

## Environment-variable overlays

Overlays are applied on top of all sources, last-wins.

```python
from kaava import env_overlay, load

overlay = env_overlay(AppConfig, prefix='APP_')
cfg = load(AppConfig, 'config.yaml', overlays=[overlay])
```

With `prefix='APP_'`, the field `db.host` maps to `APP_DB_HOST`.
An explicit `env=` annotation on a field always takes precedence over the derived name.
`env_overlay()` reads from `os.environ` by default; pass `environ=` to supply a custom mapping.

### Dotenv files

```python
from kaava import dotenv_environ, env_overlay, load

environ = dotenv_environ('.env')
overlay = env_overlay(AppConfig, environ=environ, prefix='APP_')
cfg = load(AppConfig, 'config.yaml', overlays=[overlay])
```

`dotenv_environ()` parses `KEY=VALUE` pairs.
Blank lines and `#` comments are skipped.
An `export KEY=VALUE` prefix and surrounding quotes are stripped.

### CLI arguments

```python
from kaava import cli_from_namespace, load

overlay = cli_from_namespace(vars(args))
cfg = load(AppConfig, 'config.yaml', overlays=[overlay])
```

`cli_from_namespace()` accepts an `argparse.Namespace` or a plain `dict`.
Dotted keys (`db.host`) are unflattened into the nested config structure automatically.

## Collecting all errors

`validate()` returns every error as a list without raising, which is useful for CLI tools that want to report all problems at once.

```python
from kaava import validate

errors = validate(AppConfig, 'config.yaml')
for e in errors:
    print(e)
```

`load_valid()` does the same collection but raises `ConfigErrors` at the end if any errors were found.

## Source tracing and explain

`load_traced()` returns the config kaava together with a `SourceMap` recording where each field value came from.

```python
from kaava import explain, load_traced

cfg, source_map = load_traced(AppConfig, 'base.yaml', 'prod.yaml')
print(explain(cfg, source_map))
```

`explain()` renders a table of field paths, current values, sources, and descriptions.
Pass `show='non-default'` to show only fields whose value came from a source, not a dataclass default.

## Diagnostics

`doctor()` probes every source, captures all load and validation errors without raising,
and reports which environment variables declared in the dataclass are set or unset.

```python
from kaava import doctor

report = doctor(AppConfig, 'config.yaml', prefix='APP_')
print(report)
print('ok:', report.ok)
```

The returned `DoctorReport` has fields `sources_ok`, `sources_failed`, `errors`,
`set_env`, `unset_env`, `kaava`, and `source_map`.
`report.ok` is `True` only when no sources failed and no errors were found.

## Ejecting a template

`eject()` prints a YAML scaffold derived from the dataclass schema.
Required fields are annotated with `# required`; defaults, descriptions, and env-var names appear as inline comments.

```python
from kaava import eject

print(eject(AppConfig))
```

## Companion CLI

The `kaava` command exposes the four diagnostic functions as subcommands.
The target dataclass is identified by a dotted import path of the form `module.path:ClassName`.

```
kaava doctor   myapp.config:AppConfig config.yaml
kaava explain  myapp.config:AppConfig config.yaml
kaava validate myapp.config:AppConfig config.yaml
kaava eject    myapp.config:AppConfig
```

See `man kaava` for the full reference, including `--env-prefix`, `--dotenv`, and exit codes.

## Changes

See `docs/changes.md` for the release history.

## Coverage

The test suite maintains 100% branch coverage.
The HTML report is in `site/coverage/`.

## SBOM

Runtime dependency information is published in `docs/sbom/` in SPDX 3.0 (JSON-LD) and CycloneDX 1.6 (JSON) formats.
See `docs/sbom/README.md` for the component inventory and validation guide.
