exlab_wizard.cache.creation_writer#

Atomic reader/writer for creation.json. Backend Spec §4.4.5.

All creation.json mutations in the codebase MUST go through CreationWriter. The class enforces the four invariants of §4.4.5:

  1. Typed encode/decode via msgspec.json against the CreationJson Struct hierarchy (no stdlib json, no separate Pydantic round-trip).

  2. Tempfile + ``os.replace`` for every write (atomic on POSIX, atomic-on-same-volume on Windows).

  3. Per-file advisory file lock. Writes use filelock.FileLock (exclusive); reads use filelock.ReadWriteLock in read mode (shared) so concurrent readers do not block each other.

  4. Lock-for-full-cycle on mutations: the read, mutator-apply, and write all happen inside a single exclusive lock acquisition so two writers cannot lost-update each other.

Forward-compat: unknown fields encountered on read are preserved on write per §11.9.3 writer-policy rule 2. The writer keeps the raw dict[str, Any] decoded form alongside the typed struct, and merges any keys that were not consumed by the Struct decoder back into the encoded output.

Backward-compat: when the file’s schema_version is older than the writer’s current version, the documented defaults from §11.3’s history table are applied during decode and the next mutation rewrites the file at the writer’s current version (§11.9.3 rule 3). Major-version mismatches raise SchemaMajorMismatchError per §11.9.2 rule 3.

Functions

select_active_overrides(validation_overrides, *)

Return the subset of override entries that are currently active.

Classes

CreationWriter([lock_timeout_seconds])

Atomic reader/writer for creation.json.

class exlab_wizard.cache.creation_writer.CreationWriter(lock_timeout_seconds=30.0)[source]#

Bases: object

Atomic reader/writer for creation.json. Backend Spec §4.4.5.

Parameters:

lock_timeout_seconds (float)

async read_creation_snapshot(path)[source]#

Read a snapshot under LOCK_SH (shared/read lock).

Concurrent readers do not block each other; an LOCK_EX writer waits for active readers to release. Use this when you need a typed view of the file but do not intend to mutate.

Parameters:

path (Path)

Return type:

CreationJson

async update_creation_atomic(path, mutator)[source]#

Read, mutate, and write creation.json under one LOCK_EX.

The full read-mutator-write cycle happens inside a single filelock.FileLock acquisition. Two concurrent update_creation_atomic calls on the same path serialize: the second waits for the first to release before it reads, so neither lost-updates the other.

The mutator is allowed to either mutate the struct in place and return it, or return a fresh struct.

Parameters:
Return type:

CreationJson

async write_creation(path, payload)[source]#

Write a fresh creation.json. Reserved for initial creation.

Acquires the per-file exclusive lock defensively even though no prior file is expected; that way two simultaneous “first writers” serialize correctly and the second one observes a now-existing file (which the controller treats as a conflict).

Parameters:
Return type:

None

exlab_wizard.cache.creation_writer.select_active_overrides(validation_overrides, *, now=None)[source]#

Return the subset of override entries that are currently active.

Implements the matching algorithm in spec §11.3:

  1. Build a set revoked_ids of every entry’s revokes pointer where entry.revoked == True.

  2. An override entry is active iff entry.revoked == False, its id is not in revoked_ids, and its expires_at is either absent/None or strictly greater than now.

now defaults to the current UTC time. Pass an explicit value for deterministic tests. Tombstones whose revokes target is missing from the array are logged at WARN and otherwise have no effect.

Parameters:
Return type:

list[dict[str, Any]]