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:
- 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f819c92ecb224e6518f57e5787e7fae230a0760f41b8714dde931441283404e
|
|
| MD5 |
b8273297d43aca74362f9e272793cf1f
|
|
| BLAKE2b-256 |
fec604615950377520818c8dff1f90ff5ef670f8cabde61e9fc7e9ec82514cba
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
contriwork_config_core-0.2.0.tar.gz -
Subject digest:
3f819c92ecb224e6518f57e5787e7fae230a0760f41b8714dde931441283404e - Sigstore transparency entry: 1394480325
- Sigstore integration time:
-
Permalink:
contriwork/contriwork-config-core@e0f4f5d971e803ee7fda7f6ac059842211269ec7 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/contriwork
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@e0f4f5d971e803ee7fda7f6ac059842211269ec7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file contriwork_config_core-0.2.0-py3-none-any.whl.
File metadata
- Download URL: contriwork_config_core-0.2.0-py3-none-any.whl
- Upload date:
- Size: 15.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d14b5031f08f030f1faa0df5b024526d768acca15804cffc857fe6d37e38efe7
|
|
| MD5 |
5a784cc0a2d357bf85e2e611fc286892
|
|
| BLAKE2b-256 |
373cf079c3a84b95b13828568ff0e4a26dd49c8badda229510878ab0040b4fb4
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
contriwork_config_core-0.2.0-py3-none-any.whl -
Subject digest:
d14b5031f08f030f1faa0df5b024526d768acca15804cffc857fe6d37e38efe7 - Sigstore transparency entry: 1394480394
- Sigstore integration time:
-
Permalink:
contriwork/contriwork-config-core@e0f4f5d971e803ee7fda7f6ac059842211269ec7 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/contriwork
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@e0f4f5d971e803ee7fda7f6ac059842211269ec7 -
Trigger Event:
push
-
Statement type: