Releases: dynamic-config-rs/dynamic-config-python
Release list
v0.3.3
Added
-
AbsentError. A store that answers "that path holds nothing" raises
its own class rather than the base one. Distinct fromRemoteError, which
is an outage waiting cures, and fromMissingError, which is a key absent
from a document that was read — the three deserve different answers, and
a program that cannot tell them apart serves a deleted secret forever. -
fingerprint()— a stable digest of the configuration installed, or
Nonebefore the first load.sha256:…, over the resolved tree rather
than any rendering of it, so two processes agree whether their files were
written as TOML or as YAML.Safe to log: every field named to
secrets=[..]is masked by position
before hashing, so it moves when a secret appears or disappears and stays
put when one merely rotates — a digest that moved on rotation would be an
oracle for the value that moved it. It answers what
status().generationcannot: two processes on generation 4 have nothing
in particular in common.
Changed
- The engine and the nine store crates move to 0.10.
v0.3.2
Changed
- Built on engine 0.9. The engine's breaking release — resolution is
its own now, with the fold behind a swappableEngineandfigment
optional — is a build-time fact here: this package embeds the engine
rather than depending on a published version of it, so the surface a
caller sees is unchanged. What travels with it is the engine's own
0.9 work: a load with one layer no longer folds it (a document, a
reload and anexplainare all cheaper than 0.8's), a value nested
past sixty-four levels reads as a string rather than overflowing the
stack, and a variable whose name is not ASCII no longer aborts a load
that reads the environment.
v0.3.1
Fixed
- A wake could be lost when an install raced the notifier thread's
first breath. The thread read its baseline generation on its own
first instruction — so an install landing after a waiter's
check-register-check but before the thread was scheduled folded into
the baseline and was never reported; under load,changed_async
timed out on an install that had already happened. The baseline is
now read on the caller's thread, under the registration lock, before
waitreturns — the same discipline forevents()' notifier, which
carried the identical pattern from birth.
Changed
- Built on engine 0.8. The engine's breaking release (a
LoadSpec
field, MSRV 1.88) is a build-time fact here — the wheel embeds it
statically and the Python surface is unchanged, which is why this
release stays a patch. The development-only[patch.crates-io]
block is gone: wheels build from exactly what crates.io serves.
Changed
- A refused reload wakes
events()natively. The engine's 0.7.1
failure hook signals the same parked notifier thread an install does,
soReloadFailedarrives when the refusal happens rather than at the
next poll — and the stream starts no timer at all. Delivery is
latest-wins, likechanges(): coalesced refusals arrive as one event
carrying the currentconsecutivecount, and a refusal followed by an
install arrives as both events, refusal first. The limitations page's
"a refused reload cannot wake anything" section retires with this.
Deprecated
events(failure_poll=...)is accepted, ignored, and warns once:
the interval refusals were polled at, now that they wake the stream
themselves. Remove the argument; the parameter goes away in 0.4.
v0.3.0
Fixed
- A free-threaded interpreter could segfault on exit mid-reload. A
watcher thread attaching to Python while finalization tears the
runtime down crashed free-threaded 3.14 (the GIL build used to park
such a thread instead). Theatexitsweep now closes a finalization
gate first: background attaches from that point on are refused —
reloads skipped, not delivered to a dying interpreter — and the ones
in flight are waited out while the interpreter is still whole. This
also makesWatch.detach()'s "built to survive exit" promise true on
free-threaded builds, which is where it was not.
Changed (book)
- The book opens with a Quick Start, and the 477-line introduction
became three pages: the pitch, Core Concepts, and The Decorator &
Typing. The 991-line Rust-stores page split the same way: narrative,
a credentials/TLS/runtime cookbook, and a per-store reference.
Changed
-
The engine's diagnostics arrive through
loggingnow.⚠️ From the
first import, the lines the compiled engine used to write straight to
file descriptor 2 —[dynamic-config] db#1: reloaded in 0ms, the
failed-reload warnings, the last-known-good recovery notice — are
ordinary records onlogging.getLogger("dynamic_config.engine").
Handlers, formatters, filters,caplogand structured logging all see
them; nothing is written to raw stderr any more unless asked.The bridge never takes the GIL on an engine thread: lines cross a
bounded channel to one forwarder thread, overflow is counted and
reported in the next delivered record, and the whole thing stands down
at interpreter exit. A script that configures no logging still sees
warnings vialogging's last-resort handler; the INFO reload lines
become opt-in, which is the one visible change.configure_logging(level=...)sets the engine-side volume in
logging's units, andconfigure_logging(raw_stderr=True)restores
the 0.2 behaviour wholesale for the deployment that greps stderr.
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.
v0.1.3
Changed
-
The wheels moved to their own repository,
dynamic-config-rs/dynamic-config-python,
and release on their own schedule from there. The package name, the
import name and the API are unchanged; what moved is where issues are
filed and where the book lives —
dynamic-config-rs.github.io/python/. -
The project links on PyPI say more.
Homepageis the book rather
than a Rust workspace's front page,Documentationpoints at the API
reference, andIssueswas added — a reader arriving from PyPI now
lands on Python prose in one click. -
More keywords and classifiers, because both are how PyPI's search
and filters find a package at all:Framework :: Pydantic,
Framework :: AsyncIO, the systems-administration and distributed
topics, and the words a person actually types (dotenv,toml,
twelve-factor,live-reload). -
The engine and the store crates are named with a caret in the
manifests these wheels are built from, so a patch release of either
reaches a source build without a release here.