Skip to main content

Cross-language static configuration loader — env + files + secret refs, schema-validated.

Project description

contriwork-config-core (Python)

Python adapter for the ContriWork config-core port. One API surface, three languages (Python / .NET / npm) — this package is the Python implementation.

Cross-language specification, contract, and release history live in the GitHub repository:

Sister packages: Contriwork.ConfigCore (NuGet), @contriwork/config-core (npm).

Install

pip install contriwork-config-core

Requires Python ≥ 3.13.

Quick start

import asyncio

from pydantic import BaseModel, SecretStr

from contriwork_config_core import (
    EnvSource,
    FileSource,
    PydanticAdapter,
    load_config,
)


class AppConfig(BaseModel):
    db_url: str
    debug: bool = False
    api_key: SecretStr | None = None


async def main() -> AppConfig:
    return await load_config(
        schema=PydanticAdapter(AppConfig),
        sources=[
            FileSource("./config.yaml", required=False),
            FileSource("./.env", required=False),  # v0.2.0: native dotenv
            EnvSource(prefix="APP_"),
        ],
    )


cfg = asyncio.run(main())

load_config is async. Calling it without await returns a coroutine object — not a config — and mypy / pyright will flag the call site. See CONTRACT.md for the full operation order (snapshot → merge → resolve → validate) and the error taxonomy.

Bootstrapping inside an async server

The naive bootstrap pattern — calling await load_config(...) from inside an async def lifespan handler with a sync wrapper at module-import time — fails non-obviously under hot-reload-capable web servers (uvicorn --reload, hypercorn --reload, etc.). The reload subprocess imports the user app from inside an already-running event loop, so any module-import-time code that wraps asyncio.run(load_config(...)) raises RuntimeError: asyncio.run() cannot be called from a running event loop. The error is silent in the sense that the bootstrap is skipped, the cached config singleton stays None, and the failure surfaces later as a confusing runtime error in unrelated code.

The naive try / except get_running_loop() workaround correctly detects the running loop but has no fallback path. The pattern below isolates the load to a fresh thread — its own event loop — so asyncio.run() succeeds whether or not the importing thread already has a loop:

import asyncio
import threading
from collections.abc import Callable, Coroutine
from typing import TypeVar

T = TypeVar("T")


def run_async_blocking(coro_factory: Callable[[], Coroutine[object, object, T]]) -> T:
    """Run an async loader synchronously, even when called from inside a
    running event loop.

    Why: ``uvicorn --reload`` imports the app from inside its own uvloop
    event loop. ``asyncio.run()`` refuses to nest, so we delegate to a
    fresh thread that owns a brand-new loop.
    """
    try:
        asyncio.get_running_loop()
        has_running_loop = True
    except RuntimeError:
        has_running_loop = False

    if not has_running_loop:
        return asyncio.run(coro_factory())  # normal path

    result: list[T] = []
    error: list[BaseException] = []

    def _runner() -> None:
        try:
            result.append(asyncio.run(coro_factory()))
        except BaseException as e:  # noqa: BLE001 — re-raised on the caller side
            error.append(e)

    t = threading.Thread(target=_runner, daemon=True)
    t.start()
    t.join()

    if error:
        raise error[0]
    return result[0]


# Module-level singleton — works under uvicorn --reload and in plain scripts.
_CONFIG = run_async_blocking(
    lambda: load_config(
        schema=PydanticAdapter(AppConfig),
        sources=[FileSource("./config.yaml"), EnvSource(prefix="APP_")],
    )
)

This helper lives in caller code on purpose — every app's bootstrap is slightly different (telemetry, fallback config, lazy-init policy) and shipping a one-size-fits-all utility inside contriwork-config-core would just push the same trap one layer deeper. The async API is the contract; the threading fallback is caller policy.

Local development

uv sync --all-extras
uv run pytest
uv run ruff check
uv run mypy src

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

contriwork_config_core-0.2.0.tar.gz (19.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

contriwork_config_core-0.2.0-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

File details

Details for the file contriwork_config_core-0.2.0.tar.gz.

File metadata

  • Download URL: contriwork_config_core-0.2.0.tar.gz
  • Upload date:
  • Size: 19.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for contriwork_config_core-0.2.0.tar.gz
Algorithm Hash digest
SHA256 3f819c92ecb224e6518f57e5787e7fae230a0760f41b8714dde931441283404e
MD5 b8273297d43aca74362f9e272793cf1f
BLAKE2b-256 fec604615950377520818c8dff1f90ff5ef670f8cabde61e9fc7e9ec82514cba

See more details on using hashes here.

Provenance

The following attestation bundles were made for contriwork_config_core-0.2.0.tar.gz:

Publisher: release-python.yml on contriwork/contriwork-config-core

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file contriwork_config_core-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for contriwork_config_core-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d14b5031f08f030f1faa0df5b024526d768acca15804cffc857fe6d37e38efe7
MD5 5a784cc0a2d357bf85e2e611fc286892
BLAKE2b-256 373cf079c3a84b95b13828568ff0e4a26dd49c8badda229510878ab0040b4fb4

See more details on using hashes here.

Provenance

The following attestation bundles were made for contriwork_config_core-0.2.0-py3-none-any.whl:

Publisher: release-python.yml on contriwork/contriwork-config-core

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page