exlab_wizard.cache.equipment#
Writer for equipment.json (registry) and test_runs.json (marker).
Backend Spec §11.4.1 (equipment.json), §11.4.2 (test_runs.json), §4.4.5 (CacheWriter contract), §11.9 (schema-version policy).
Both files in this module are written rarely:
equipment.json– written on the first project creation under an equipment, plus on Settings-driven re-syncs wheneverconfig.yamlis edited. Treated as a registry record (the equipment is a configured workstation peripheral, not an instance of a creation flow).test_runs.json– a marker file written ONCE on the first test run within<equipment>/<project>/TestRuns/. Subsequent test-run creations under the same project do NOT rewrite this file (§11.4.2).
Both writes use the canonical CacheWriter recipe (§4.4.5):
msgspec.json.encodefor the typed encode (replaces stdlibjson; schema validation lives in themsgspec.Structtypes).Atomic write via tempfile +
fsync+os.replace.Per-file advisory lock via
filelock.FileLockso concurrent writers serialize without corrupting the file.Async wrappers around the synchronous lock+write logic via
asyncio.to_threadso the asyncio event loop is not blocked.
The reader path (EquipmentCacheWriter.read_equipment()) does NOT
acquire the lock: per §4.4.5 a snapshot read is uncontended, and shared
locks in filelock would force the writer to wait on stale readers.
A reader that races with a writer will see either the pre-write content
(via the original inode) or the post-write content (via the
os.replace atomic rename); both are valid points-in-time.
The require_schema_major() helper at module bottom is the shared
schema-major gate used by every cache writer in the package – it
implements §11.9.2 reader rule 3 (a reader at major R MUST refuse any
file at major M != R).
Classes
Writer for |
- class exlab_wizard.cache.equipment.EquipmentCacheWriter[source]#
Bases:
objectWriter for
equipment.jsonandtest_runs.json.Instances are stateless; the class serves only to group the two related writers together (per §4.3 package layout). Both write methods are async wrappers; the actual lock + I/O work runs on a worker thread via
asyncio.to_threadso the asyncio event loop is not blocked on disk syscalls.- async read_equipment(path)[source]#
Read and decode an existing
equipment.jsonfile.Uses
msgspec.json.decode(..., type=EquipmentJson)so schema validation happens in one pass with the decode. No lock is acquired: the writer’sos.replaceis atomic, so a racing reader sees either the pre- or post-write inode but never a torn file.Raises
FileNotFoundError(from the underlyingPath.read_bytes) if the file does not exist; the caller is responsible for handling the absent-file case.- Parameters:
path (
Path)- Return type:
- async read_test_runs_marker(path)[source]#
Read and decode a
test_runs.jsonmarker file.Raises
SchemaMajorMismatchErrorif the file’s schema major version exceeds the reader’s supported major (1) per §11.9.2.- Parameters:
path (
Path)- Return type:
- async write_equipment(path, payload)[source]#
Write or rewrite the per-equipment
equipment.jsonfile.Stamping rules (§11.4.1):
last_modified_atis set to UTC now on every write.first_seen_atis set to UTC now ONLY when the file does not yet exist. On a re-write of an existing file thefirst_seen_atfrom the on-disk version is preserved – the spec is explicit that this field is never updated after the first write.
The mutation runs entirely under the per-file advisory lock so a concurrent writer never sees the stale tempfile or a partially-renamed file.
- Parameters:
path (
Path)payload (
EquipmentJson)
- Return type:
- async write_test_runs_marker(path, payload)[source]#
Write the
test_runs.jsonmarker file.Idempotent per §11.4.2: if the file already exists, this is a no-op (the on-disk content is left untouched even if
payloaddiffers). The first write captures the project’s TestRuns subtree as test-only; subsequent test-run creations under the same project leave the marker alone.The on-disk-existence check is performed under the per-file advisory lock so a concurrent first-time writer does not end up with two competing writes both passing the existence guard and racing on
os.replace.- Parameters:
path (
Path)payload (
TestRunsJson)
- Return type:
- exlab_wizard.cache.equipment.require_schema_major(version, *, expected_major)[source]#
Raise
SchemaMajorMismatchErrorwhenversionis a different major.Backend Spec §11.9.2 rule 3: a reader at major
RMUST refuse any file at majorM != Rwith a structured error. The error carriesexpected_majorandfoundso the caller can report it via the §4.6.3 error envelope.A
None, empty, or non-MAJOR.MINORversion string is treated as a major mismatch (the reader cannot tell what version the file claims to be). Callers that want to be lenient about a missing version (e.g. a partially-written or corrupt registry file that should be recovered by rewriting) check that condition themselves before invoking this helper.