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:
- Root README — ecosystem overview
CONTRACT.md— language-agnostic port specCHANGELOG.md
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.