v0.2.0
Added
-
Nine extras that resolve to the web adapters.
dynamic-config-py[fastapi],[litestar],[flask],[quart],
[django],[drf],[ninja],[robyn]and[django-bolt]— plus
[web]for the shared core with no framework — each resolving to
dynamic-config-py-web,
which is where the wiring, the request scope, the health surface and the
test doors now live. Not in[all], exactly as[remote]is not:
[all]means the schema libraries.The book's Web Frameworks page keeps the two rules and the hand-written
version — the adapters are what those rules look like when they are
checked rather than recommended — and points at that package's book for
the installed one. -
ConfigGroup: several configurations under one lifecycle.
ConfigGroup(db, cache, queue)initialises, watches, reports and stops
its members together, with async twins throughout andconcurrency=to
bound how many load at once.group.status()andgroup.generations()
answer per key, for a health endpoint. The group owns lifecycle, not
storage:db.current()is still the read path. -
group.reload_atomic(): every member validates, or none installs.
The engine's prepare-then-commit —ReloadGroup, which Rust callers
have had since 0.4 — driven from Python. A refusal leaves every
snapshot exactly as it was, generation included, instead of leaving a
deployment half-applied across two configurations. -
events(): installs and refusals as typed events. An async
iterator ofReloaded(generation, at, changed, reason)and
ReloadFailed(generation, at, kind, path, consecutive)— frozen
dataclasses amatchreads as prose. No event carries a value, the
same ruleexplain()andcheck()follow.failure_poll=opts into
checking for refusals, which nothing can wake a stream for: the engine
bumps no generation for a load that installed nothing. -
Reload hooks can say where they run.
on_reload(hook, dispatch=..., backpressure=...), pluson_reload_asyncand
on_change_asyncfor coroutine functions.Dispatchisinline
(the default, unchanged),executororasyncio;Backpressureis
every,latest(the default off the installing thread),serialor
cancel_previous. Both arestrenums, sodispatch="executor"works
and a typo is aValueErrorat registration rather than a callback
that silently never runs. A coroutine function registered with no
dispatchnow runs as a task instead of being called inline and
returning a coroutine nobody awaits. -
AsyncRemoteSource: a remote store whose client is async. Its
fetch()is awaited on the loop that calledrefresh_remote_async(),
so anhttpx.AsyncClientruns on the loop it was built on; cancelling
the refresh cancels the fetch, and a raisingfetch()reaches the
caller as its own exception rather than asRemoteError. The
synchronousrefresh_remote()raises on such a store rather than
driving it from a private loop. -
The lifetime as a block.
with config.running():is init, then
watch, then stop;config.watching(),group.watching(),
group.running()and the_asynctwin of each do the same for the
pieces. The shape that cannot leak a watcher by forgetting to stop it. -
configure_executor(workers)andexecutor(...).
configure_executorbuilds the blocking pool, names its threads
dynamic-config-blocking-Nand shuts it down at exit;executor()is
the same choice as a block, restored on the way out.set_executoris
unchanged, and the pool passed to it is still never shut down here. -
Model.configkeeps the model's type. A configuration reached
through the decorator wasDynamicConfig[Any], so everything reached
through it —Model.config.current(),changes(),
changed_async()— came back asAnyundermypy --strictwhile
Model.current()was correctly typed.Configurednow declares it as
a descriptor generic over the class it is read from, which is how
classmethoditself is typed, soDatabase.configis
DynamicConfig[Database]. Runtime behaviour is unchanged, and the
ClassVarPydantic needs is still what Pydantic sees.
Changed
-
The book has parts — Guide, Use Cases, Advanced and Reference
— and pre-forking servers move out of the bottom of Web Frameworks into
a chapter of their own. -
Awaiting a reload no longer polls.
changed_async(),changes()
andevents()are answered by one notifier thread per configuration —
shared by every awaiting task on it, parked in the engine with the GIL
released, and woken only by an install or by release. Before this, each
waiter re-submitted a quarter-second wait to an executor for as long as
it waited.What changes for callers: cancellation is immediate rather than
within 250 ms; an idle service does no work at all for the
configuration it is watching; and the executor is free for loads, so a
hundred awaiting tasks no longer contend with the reload they are
waiting for.set_executorstill answers the same question it did. -
Watcher-side threads are named. A notifier thread is
dynamic-config-notify-<key>and a dispatched hook's thread is
dynamic-config-hook, so a thread dump says which configuration it is
looking at.