Skip to main content

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.

Metadata

Release files for contriwork-config-core 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for contriwork-config-core 0.2.0
File Size Uploaded Provenance Actions
contriwork_config_core-0.2.0.tar.gz 19.5 kB With provenance Details for contriwork_config_core-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for contriwork-config-core 0.2.0
File Interpreter ABI Platform Actions
contriwork_config_core-0.2.0-py3-none-any.whl Python 3 none any Details for contriwork_config_core-0.2.0-py3-none-any.whl

Total release size: 35.4 kB

Release files / contriwork_config_core-0.2.0.tar.gz

Download URL contriwork_config_core-0.2.0.tar.gz
Size 19.5 kB
Tags Source
SHA-256 checksum
How to use checksums
3f819c92ecb224e6518f57e5787e7fae230a0760f41b8714dde931441283404e
BLAKE2b-256 checksum
How to use checksums
fec604615950377520818c8dff1f90ff5ef670f8cabde61e9fc7e9ec82514cba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 27, 2026.

Transparency log

Release files / contriwork_config_core-0.2.0-py3-none-any.whl

Download URL contriwork_config_core-0.2.0-py3-none-any.whl
Size 15.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d14b5031f08f030f1faa0df5b024526d768acca15804cffc857fe6d37e38efe7
BLAKE2b-256 checksum
How to use checksums
373cf079c3a84b95b13828568ff0e4a26dd49c8badda229510878ab0040b4fb4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 27, 2026.

Transparency log

Security for contriwork-config-core 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for contriwork_config_core-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page