Skip to content

Releases: dynamic-config-rs/dynamic-config-python

v0.3.3

Choose a tag to compare

@github-actions github-actions released this 23 Aug 13:23
63b8373

Added

  • AbsentError. A store that answers "that path holds nothing" raises
    its own class rather than the base one. Distinct from RemoteError, which
    is an outage waiting cures, and from MissingError, 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
    None before 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().generation cannot: 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

Choose a tag to compare

@github-actions github-actions released this 21 Aug 15:18
a77aa2c

Changed

  • Built on engine 0.9. The engine's breaking release — resolution is
    its own now, with the fold behind a swappable Engine and figment
    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 an explain are 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

Choose a tag to compare

@github-actions github-actions released this 20 Aug 05:42
cfc5367

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
    wait returns — the same discipline for events()' 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,
    so ReloadFailed arrives when the refusal happens rather than at the
    next poll — and the stream starts no timer at all. Delivery is
    latest-wins, like changes(): coalesced refusals arrive as one event
    carrying the current consecutive count, 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

Choose a tag to compare

@github-actions github-actions released this 18 Aug 16:06
9e77958

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). The atexit sweep 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 makes Watch.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 logging now. ⚠️ 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 on logging.getLogger("dynamic_config.engine").
    Handlers, formatters, filters, caplog and 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 via logging'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, and configure_logging(raw_stderr=True) restores
    the 0.2 behaviour wholesale for the deployment that greps stderr.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 17 Aug 22:11
d29c189

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 and concurrency= to
    bound how many load at once. group.status() and group.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 of Reloaded(generation, at, changed, reason) and
    ReloadFailed(generation, at, kind, path, consecutive) — frozen
    dataclasses a match reads as prose. No event carries a value, the
    same rule explain() and check() 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=...), plus on_reload_async and
    on_change_async for coroutine functions. Dispatch is inline
    (the default, unchanged), executor or asyncio; Backpressure is
    every, latest (the default off the installing thread), serial or
    cancel_previous. Both are str enums, so dispatch="executor" works
    and a typo is a ValueError at registration rather than a callback
    that silently never runs. A coroutine function registered with no
    dispatch now 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 called refresh_remote_async(),
    so an httpx.AsyncClient runs on the loop it was built on; cancelling
    the refresh cancels the fetch, and a raising fetch() reaches the
    caller as its own exception rather than as RemoteError. The
    synchronous refresh_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 _async twin of each do the same for the
    pieces. The shape that cannot leak a watcher by forgetting to stop it.

  • configure_executor(workers) and executor(...).
    configure_executor builds the blocking pool, names its threads
    dynamic-config-blocking-N and shuts it down at exit; executor() is
    the same choice as a block, restored on the way out. set_executor is
    unchanged, and the pool passed to it is still never shut down here.

  • Model.config keeps the model's type. A configuration reached
    through the decorator was DynamicConfig[Any], so everything reached
    through itModel.config.current(), changes(),
    changed_async() — came back as Any under mypy --strict while
    Model.current() was correctly typed. Configured now declares it as
    a descriptor generic over the class it is read from, which is how
    classmethod itself is typed, so Database.config is
    DynamicConfig[Database]. Runtime behaviour is unchanged, and the
    ClassVar Pydantic needs is still what Pydantic sees.

Changed

  • The book has partsGuide, 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()
    and events() 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_executor still 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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 10:51
2b254f1

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. Homepage is the book rather
    than a Rust workspace's front page, Documentation points at the API
    reference, and Issues was 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.