Metadata-Version: 2.5
Name: cognisn-konfig
Version: 0.5.0
Summary: Settings management, pluggable secrets, and run-scoped logging for Python applications.
Author: Matthew Westwood-Hill
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cryptography>=42.0
Requires-Dist: keyring>=25.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: tomli>=2.0; python_version < '3.11'
Provides-Extra: aws
Requires-Dist: boto3>=1.34; extra == 'aws'
Provides-Extra: dev
Requires-Dist: black<25,>=24.0.0; extra == 'dev'
Requires-Dist: isort<6,>=5.13.0; extra == 'dev'
Requires-Dist: mypy<2,>=1.8.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: tomli>=2.0; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
Description-Content-Type: text/markdown

# Konfig

[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Cognisn_konfig&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Cognisn_konfig)
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Cognisn_konfig&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=Cognisn_konfig)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Cognisn_konfig&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=Cognisn_konfig)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Cognisn_konfig&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=Cognisn_konfig)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=Cognisn_konfig&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=Cognisn_konfig)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=Cognisn_konfig&metric=bugs)](https://sonarcloud.io/summary/new_code?id=Cognisn_konfig)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Cognisn_konfig&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Cognisn_konfig)
[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Cognisn_konfig&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Cognisn_konfig)

Settings management, pluggable secrets, and run-scoped logging for Python applications.

Konfig provides three foundational capabilities every Python application needs, with an optional lightweight app lifecycle context manager that ties them together. It is a clean-sheet replacement for `dtPyAppFramework`.

## Features

- **Layered settings** with system/user/env/runtime precedence and persistent writes
- **Pluggable secrets** with OS keyring, AES-encrypted file, and AWS Secrets Manager backends
- **Run-scoped logging** with historical retention, structured JSON mode, and stdio-safe output
- **Optional centralised log forwarding**: one nominated collector gathers every container's logs
  over pinned TLS, discovered automatically through the shared secrets store
- **Platform-aware defaults** for config, data, and log directories (macOS, Linux, Windows)
- **Optional app lifecycle** via a sync/async context manager — no inheritance required
- Python 3.10+

## Installation

```bash
pip install konfig
```

For AWS Secrets Manager support:

```bash
pip install konfig[aws]
```

## Secrets

### AWS Secrets Manager (designated secret)

Point konfig at a single AWS secret that holds a JSON bundle of your secrets (the secret must
already exist):

```bash
export KONFIG_AWS_SECRETS_MANAGER=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/secrets-AbCdEf
```

```python
from konfig import Secrets

secrets = Secrets()               # env var selects the AWS bundle backend
api_key = secrets.get("api_key")
secrets.set("api_key", "sk-new")  # read-modify-write back to the bundle
```

Requires `pip install konfig[aws]`.

### AWS Secrets Manager settings document (`KONFIG_AWS_SETTINGS`)

Deliver an application's entire configuration as one JSON document held in a
single Secrets Manager secret — no config file or per-setting environment
variables needed in the task definition:

```bash
export KONFIG_AWS_SETTINGS=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/settings-AbCdEf
```

The secret's `SecretString` must be a JSON object holding the same nested
settings tree a config file would. A plain secret name is also accepted; the
region then comes from the default AWS provider chain (with an ARN it is
taken from the ARN). An equivalent constructor argument exists for
programmatic use — `Settings(aws_settings=...)` — with the environment
variable winning when both are set. Requires `pip install konfig[aws]`.

Precedence (highest to lowest):
`runtime -> env vars -> AWS settings -> user file -> system file -> defaults`
— environment variables remain the operator's immediate per-container
override, while the AWS document overrides anything baked into the image.

Behaviour:

- **Read-only.** `settings.set(...)` keeps writing to the runtime layer or
  config files exactly as before.
- **Fail fast.** A missing/unreadable secret, non-JSON payload, or
  non-object top level raises at startup rather than silently degrading to
  defaults. Error messages name the secret, never its payload.
- **Fetched once.** The document is read at construction and only re-fetched
  by `settings.reload()`; there is no background polling.
- **Composes with secrets.** Values in the document may be `secret://` URIs,
  resolved as usual by the active secrets backend.

This is independent of `KONFIG_AWS_SECRETS_MANAGER`. Pointing both at the
same secret works, but keeping configuration and secret material in separate
Secrets Manager entries is recommended so IAM can distinguish "may read
config" from "may read credentials".

### First boot on AWS: seeded stores

When konfig starts against **empty** stores (an empty or whitespace-only
`SecretString`, or a bare `{}`), it writes a template for the operator instead
of leaving them to transcribe documentation:

- The `KONFIG_AWS_SETTINGS` settings secret is seeded with the application's
  defaults tree as pretty-printed JSON. Startup continues with reads resolving
  exactly as if the layer were absent.
- The `KONFIG_AWS_SECRETS_MANAGER` bundle secret is seeded with a `"CHANGEME"`
  placeholder for every `secret://<name>` reference in the effective settings.
  Resolving an unpopulated placeholder logs a warning naming the key.
  `AppContext` runs this automatically at startup; when composing `Settings`
  and `Secrets` yourself, call `secrets.seed_from(settings)`.

The intended first-boot flow: create the two (empty) secrets, grant
`secretsmanager:PutSecretValue` on them for first boot, start once, edit the
seeded documents, restart — and optionally drop the write grant again.

Seeding never touches a non-empty store (malformed payloads keep failing
fast, and partial documents are never merged into), never creates secrets,
and a failed seed write (for example `AccessDenied` under a read-only role)
only logs a warning. Concurrent replicas racing the first boot are benign:
every writer writes identical content. Operators who want no write attempt at
all can disable seeding outright with `KONFIG_AWS_SEED=false`.

### Running the LocalStack integration tests

The AWS integrations have opt-in integration tests that run against a local
[LocalStack](https://www.localstack.cloud/) container (no AWS account needed). They
auto-skip when LocalStack is not running, so they never affect the normal test run.

```bash
docker compose up -d         # start LocalStack
pip install -e ".[dev,aws]"  # boto3 + dev tools
pytest -m localstack         # run only the integration test
docker compose down          # stop LocalStack
```

The tests look for LocalStack at `http://127.0.0.1:4566`, the port `docker-compose.yml`
publishes. If it is published somewhere else, point the tests at it with
`KONFIG_TEST_LOCALSTACK_URL` (an http(s) URL with a host and a port):

```bash
KONFIG_TEST_LOCALSTACK_URL=http://127.0.0.1:24566 pytest -m localstack
```

## Quick Start

```python
from konfig import AppContext

with AppContext(
    name="My Application",
    version="1.0.0",
    config_file="config.yaml",
    env_prefix="MYAPP",
) as ctx:
    host = ctx.settings.get("database.host", "localhost")
    api_key = ctx.secrets.get("api_key")
    ctx.logger.info("Starting with host=%s", host)
```

For async applications:

```python
async with AppContext(name="My Server", version="2.0.0") as ctx:
    await run_server(ctx.settings)
```

### Config file format

The config file format is chosen by the `KONFIG_CONFIG_FORMAT` environment variable
(`yaml`, `json`, or `sqlite`). When unset, the format is detected from the file extension,
defaulting to YAML. Reading, updating, and creating settings work in every format.
(TOML config files are still supported by extension detection, but are read-only and
cannot be selected via `KONFIG_CONFIG_FORMAT`.)

```bash
export KONFIG_CONFIG_FORMAT=sqlite   # store settings in a SQLite database file
```

```python
from konfig import Settings

settings = Settings(config_file="config.db")
settings.set("database.host", "localhost", persist="user")  # written to SQLite
settings.set("debug", True)                                  # omit persist: in-memory only
host = settings.get("database.host")
```

### Centralised log forwarding

To collect every container's logs in one place, nominate one instance as the collector and turn
on forwarding everywhere else. Each instance keeps its own local logs as before:

```bash
# On the collector (needs a persistent volume at its log directory)
KONFIG_LOG_COLLECTOR=true
KONFIG_LOG_COLLECTOR_ADVERTISE=log-collector:9020

# On every other instance
KONFIG_LOG_CENTRAL=true
KONFIG_LOG_INSTANCE_LABEL=worker-3   # optional
```

`AppContext` does the rest. The collector generates a TLS certificate and an access token and
publishes them in the shared secrets store, senders discover them, and every record lands, tagged
with its instance, in `<log_dir>/central/latest/central.log` on the collector. See
[Centralised Log Forwarding](docs/logging.md#centralised-log-forwarding).

Each subsystem (Settings, Secrets, LogManager) can also be used independently. See the full documentation in the [`docs/`](docs/) directory:

- [Settings Guide](docs/settings.md)
- [Secrets Guide](docs/secrets.md)
- [Logging Guide](docs/logging.md)
- [AppContext Guide](docs/app-context.md)
- [Platform Paths](docs/platform-paths.md)
- [Configuration Reference](docs/configuration-reference.md)
- [API Reference](docs/api-reference.md)

## Samples

Working examples are provided in the [`samples/`](samples/) directory:

| File | Description |
|------|-------------|
| [`basic_settings.py`](samples/basic_settings.py) | Config files, defaults, env vars, overrides |
| [`secrets_usage.py`](samples/secrets_usage.py) | Store, retrieve, and delete secrets |
| [`logging_demo.py`](samples/logging_demo.py) | Run-scoped logging with retention |
| [`app_context.py`](samples/app_context.py) | Full lifecycle with all subsystems |
| [`async_app.py`](samples/async_app.py) | Async context manager usage |
| [`custom_backend.py`](samples/custom_backend.py) | Implementing a custom SecretBackend |
| [`central_logging.py`](samples/central_logging.py) | Centralised log forwarding: collector and sender roles |

## Development

CI installs the development tools from a hash-locked requirements file and then konfig
itself. Do the same locally to get exactly the versions CI tests with (the lock covers
Linux, macOS and Windows on Python 3.10 to 3.13, and includes the `dev` and `aws` extras):

```bash
python -m pip install --require-hashes --only-binary :all: -r requirements/dev.txt
python -m pip install --no-deps --no-build-isolation -e .
```

`pip install -e ".[dev,aws]"` still works when you do not need the locked versions.

The lock files are generated from the `.in` files beside them, together with
`pyproject.toml`, by [uv](https://docs.astral.sh/uv/) 0.13.6 (the version CI uses).
Regenerate them from the repository root after changing a dependency in `pyproject.toml` or
a `.in` file:

```bash
uv pip compile --universal --generate-hashes --python-version 3.10 requirements/build.in -o requirements/build.txt
uv pip compile --universal --generate-hashes --python-version 3.10 pyproject.toml requirements/dev.in --extra dev --extra aws -o requirements/dev.txt
```

uv keeps the versions already in the lock files; add `--upgrade` (or
`--upgrade-package <name>`) to move them on. CI regenerates both files and fails if the
result differs from what is committed.

The GitHub Actions in `.github/workflows/` are pinned to full commit SHAs with the tag as a
trailing comment; Dependabot (`.github/dependabot.yml`) proposes updates to them weekly, as
one grouped pull request, and never touches the lock files.

```bash
pytest
pytest --cov=konfig
mypy src/konfig
black src/ tests/
isort src/ tests/
```

## License

MIT License. See [LICENSE](LICENSE) for details.
