exlab_wizard.sync#
Sync package. Backend Spec §7.
Public re-exports for the NAS sync subsystem. Callers should depend on
this package’s surface rather than reach into the sub-modules; the
NASSyncClient is the only stateful object outside callers
typically interact with.
- exlab_wizard.sync.HandleState#
alias of
SyncHandleState
- class exlab_wizard.sync.NASSyncClient(*, config, queue_db, validator, cache_creation, sync_state_writer=None, keyring_store=None, worker_poll_interval_s=0.05, push_callable_factory=None, check_callable_factory=None, lsjson_callable_factory=None)[source]#
Bases:
objectDurable, per-equipment NAS sync queue with Pre-Sync Gate.
Backend Spec §7.1, §7.3.
Lifecycle:
init()opens the queue DB, replays any in-flight jobs, and starts a single background worker task.enqueue()runs the Pre-Sync Gate, gates the run if needed, and otherwise inserts aQUEUEDrow.close()cancels the worker and closes the DB.
The worker loop is a simple “pick the oldest QUEUED whose
next_attempt_athas passed” scheduler with at-most-one inflight job at a time. This keeps determinism for tests; production deployments can extend to per-equipment parallelism without changing the public API.- Parameters:
config (
Config)queue_db (
Path)validator (
Validator)cache_creation (
CreationWriter)sync_state_writer (
SyncStateWriter|None)keyring_store (
Any)worker_poll_interval_s (
float)push_callable_factory (
Callable[[EquipmentConfig],Callable[...,Any]] |None)check_callable_factory (
Callable[[EquipmentConfig],Callable[...,Any]] |None)lsjson_callable_factory (
Callable[[EquipmentConfig],Callable[...,Any]] |None)
- apply_config(config)[source]#
Swap the cached config + equipment map in place (no relaunch).
The
equipment_id -> EquipmentConfiglookup is rebuilt into a local before assignment so the worker loop – which runs in the same event loop – never observes a half-built map. In-flight queued jobs carry their own captured paths; a removed equipment id simply errors that one job exactly as it would after a relaunch.
- async enqueue(run_path, files=None)[source]#
Pre-Sync Gate -> if hard-tier finding without override, mark
sync_status='blocked_by_validation'. Otherwise insert aQUEUEDrow.files(operator-free per-file NAS sync, 2026-05-21) is the per-file subset of run-relative POSIX paths eligible at enqueue time; an empty / omitted list means “the whole run”.The queue holds one row per
run_path(UNIQUE). Re-enqueue behaviour:existing job in a terminal state (
VERIFIED/CLEANUP_ELIGIBLE/CLEANED/FAILED) and a non-emptyfileslist -> reset toQUEUEDcarrying the new subset. This is how a file modified after a prior verify gets re-synced.existing job in a terminal
VERIFIED/CLEANUP_ELIGIBLE/CLEANEDstate with an emptyfileslist -> falls through to a no-op: there is no subset to re-sync and a successfully-verified run is not blindly re-queued. (Only a terminalFAILEDrow with emptyfilesis re-armed – the manual-retry branch below.)existing job active (
QUEUED/RUNNING/AWAITING_VERIFY) -> no-op (newly settled files ride the next sweep).existing terminal
FAILEDjob with nofiles-> re-armed viareset_to_queuedso the manual-retry contract holds.no existing job -> insert a
QUEUEDrow withfiles.
Returns a
SyncJobHandle. The handle’sstateis eitherSyncHandleState.BLOCKEDorSyncHandleState.QUEUED.- Parameters:
- Return type:
- async force_verify(run_path)[source]#
Re-run
rclone check --downloadagainst the configured remote.Used by the Settings “verify integrity” action. Reports only – does NOT advance the queue state and does NOT update
verified_sha256insync_state.json(the rclone-only migration deliberately keeps Slot A SHA capture scoped to the sync-time path that has access to a freshly-read local copy).Resolves the equipment from
run_path’s first component, gathers every tracked file insync_state.jsonas the--files-fromsubset, and asks the driver to compare. Returns a populatedVerifyResult; the caller rendersmismatched,missing, anderrorsto the operator. A run with no tracked files yieldsok=True(nothing to verify).- Parameters:
run_path (
Path)- Return type:
- async status(run_path)[source]#
Return the queue state of the job for
run_path."none"when no job exists; otherwise the underlyingSyncJobStatevalue.
- class exlab_wizard.sync.SyncHandleState(*values)[source]#
Bases:
StrEnumIn-process state of a NAS sync job handle as observed by callers.
Distinct from
SyncStatus(which is persisted in creation.json) and from the queue’s internalSyncJobState(which tracks the row in sync_queue.db). Backend Spec §7.1.- BLOCKED = 'blocked'#
- QUEUED = 'queued'#
- class exlab_wizard.sync.SyncJobHandle(job_id, state, run_path, blocking_findings=())[source]#
Bases:
objectLightweight handle returned by
NASSyncClient.enqueue().job_idis empty when the gate blocked enqueue (the on-disksync_statuswill reflect the block).blocking_findingsis present iffstate == BLOCKED.- Parameters:
- state: SyncHandleState#
- class exlab_wizard.sync.SyncJobRow(id, run_path, equipment_id, state, attempts=0, last_attempt_at=None, next_attempt_at=None, last_error=None, verify_passes=0, verified_at=None, enqueued_at='', nas_path=None, files=())[source]#
Bases:
objectOne row in the
jobstable. Backend Spec §7.1.1.- Parameters:
- state: SyncJobState#
- class exlab_wizard.sync.SyncJobState(*values)[source]#
Bases:
StrEnumState machine for a sync job. Backend Spec §7.1.2.
- AWAITING_VERIFY = 'awaiting_verify'#
- CLEANED = 'cleaned'#
- CLEANUP_ELIGIBLE = 'cleanup_eligible'#
- FAILED = 'failed'#
- QUEUED = 'queued'#
- RUNNING = 'running'#
- VERIFIED = 'verified'#
- class exlab_wizard.sync.SyncQueue(db_path)[source]#
Bases:
objectAsync SQLite-backed durable sync queue. Backend Spec §7.1.1.
Use
init()once at application startup; the database file is created on demand. After init, all CRUD methods are coroutines.- Parameters:
db_path (
Path)
- async get_by_id(job_id)[source]#
Return the row with the given
job_idorNone.- Parameters:
job_id (
str)- Return type:
- async get_by_run_path(run_path)[source]#
Return the row whose
run_pathmatches, orNone.- Parameters:
run_path (
Path)- Return type:
- async init()[source]#
Open the connection, ensure the schema, and replay in-flight rows.
Replay semantics (§7.1.2): any
RUNNINGrow at startup gets downgraded toQUEUED(the worker died mid-transfer); anyAWAITING_VERIFYrow is left as-is so the verifier picks it up.- Return type:
- async insert(*, run_path, equipment_id, nas_path=None, job_id=None, files=None)[source]#
Insert a new
QUEUEDrow forrun_path.filesis the per-file subset (run-relative POSIX paths) eligible at enqueue time; an empty / omitted list means “the whole run”.Raises
aiosqlite.IntegrityError(via the UNIQUE constraint onrun_path) if a row already exists for the same path.
- static is_terminal(state)[source]#
Return True if the state is a terminal state (no further work).
- Parameters:
state (
SyncJobState)- Return type:
- async list_in_state(state)[source]#
Return every row currently in
stateordered byenqueued_at.- Parameters:
state (
SyncJobState)- Return type:
- async record_failure(job_id, error, *, terminal=False, now=None)[source]#
Record a transport failure on
job_id.If
terminalis True (auth failure, local file vanished) the job goes straight toFAILEDwith no backoff. Otherwise:increment
attemptsif
attempts >= MAX_ATTEMPTS: terminalFAILED.else: stay in
QUEUEDwithnext_attempt_atper backoff.
- async requeue_with_files(job_id, files)[source]#
Reset a terminal job back to
QUEUEDcarrying a newfileslist.Operator-free per-file NAS sync design (2026-05-21): a file modified after a prior verify becomes re-eligible. When the poller observes such a file it re-arms the run’s terminal job (
VERIFIED/CLEANUP_ELIGIBLE/CLEANED/FAILED) with the freshly eligible subset.attempts/last_error/verify_passesare cleared so the backoff schedule and verify counter start fresh.
- async reset_to_queued(job_id)[source]#
Reset a
FAILEDjob back toQUEUEDfor a manual retry.Per §7.1.5 the Problems-tab Retry action re-enqueues a failed job.
attemptsandlast_errorare cleared so the backoff schedule starts fresh.- Parameters:
job_id (
str)- Return type:
- async transition(job_id, new_state, *, last_error=None, increment_attempts=False, increment_verify_passes=False, verified_at=None, next_attempt_at=None, last_attempt_at=None, nas_path=None)[source]#
Transition a job to
new_stateand patch the auxiliary columns.The patch is one
UPDATEstatement so either every column moves or none do. RaisesValueErrorif the job is missing.
- exception exlab_wizard.sync.TransportError(message, *, error_kind=None)[source]#
Bases:
ExceptionRaised when a transport probe cannot complete.
Distinct from
TransportResult: this surfaces conditions where the transport’s hashsum-style probe could not produce a manifest at all – either because the upstream binary is missing (error_kind=Nonefor the historical “binary not on PATH” case) or because the probe ran and failed in a classifiable way (AUTH, NETWORK, UNKNOWN). The queue worker useserror_kindto route the failure through the §7.1.5 retry policy:AUTH– terminal FAILED, no retry (configuration problem).NETWORK– non-terminal failure, exponential backoff retry.UNKNOWN– treated asNETWORKfor retry purposes.None– legacy “binary missing” callers. Routed through the §7.1.5 HASH_MISMATCH branch: one immediate retry, then terminal FAILED on the second occurrence. The operator surfaces the binary-missing reason vialast_error.
- Parameters:
message (
str)error_kind (
TransportErrorKind|None)
- class exlab_wizard.sync.TransportErrorKind(*values)[source]#
Bases:
StrEnumKind of failure that the queue worker uses to drive the retry policy.
Backend Spec §7.1.5.
NETWORK: timeout, ECONNRESET, transient SSH failure – retried with exponential backoff up toMAX_ATTEMPTS.AUTH: authentication failure – terminal FAILED, no retry.HASH_MISMATCH: post-transport hash check failed – single retry of the transport phase, then terminal.LOCAL_FILE_VANISHED: the local file disappeared between transport and verify – terminal FAILED withlocal_file_vanishedreason.UNKNOWN: catch-all for transports returning a non-zero code we don’t recognize – treated asNETWORKfor retry purposes.
- AUTH = 'auth'#
- HASH_MISMATCH = 'hash_mismatch'#
- LOCAL_FILE_VANISHED = 'local_file_vanished'#
- NETWORK = 'network'#
- UNKNOWN = 'unknown'#
- class exlab_wizard.sync.TransportResult(ok, error_kind=None, stderr='', stdout='', returncode=0)[source]#
Bases:
objectOutcome of a transport push.
okis True iff the transport reported success. On failure,error_kindselects the retry path;stderris the raw stderr text for log surfacing;returncodeis the subprocess exit code.- Parameters:
- error_kind: TransportErrorKind | None#
- class exlab_wizard.sync.Verifier(driver=None)[source]#
Bases:
objectVerifier:
rclone checkwrapper, no Python SHA pipeline.Constructed with an
RcloneDriver; production callers pass the same driver instance the push path uses so subprocess settings (binary path, etc.) stay consistent. Tests can pass a stub driver whosecheckreturns a cannedCheckResult.- Parameters:
driver (
RcloneDriver|None)
- async verify(run_path, remote, *, files_from)[source]#
Run
rclone check --downloadoverfiles_fromand translate.Raises
TransportErrorfrom the driver only on a spawn failure (the rclone binary is missing); every other failure mode – auth / network / hash-mismatch – is folded into the returnedVerifyResult.- Parameters:
- Return type:
- class exlab_wizard.sync.VerifyResult(ok, mismatched=(), missing=(), extra=(), errors=(), error_kind=None, verified=())[source]#
Bases:
objectOutcome of one
rclone check --downloadpass against a remote.okis True iff no files differ, none are missing on the destination, and the rclone subprocess reported no per-file errors. Files inmismatched/missing/errorsare run-relative POSIX paths drawn from the--combinedoutput.extralists files present on the destination but not in the source – it does not flipok(it is informational, mirroring the pre-migration contract).error_kindis set when the rclone subprocess itself failed (auth / network / unknown) before producing usable combined output. The queue worker keys off this field to route through the spec §7.1.5 retry policy.- Parameters:
- error_kind: TransportErrorKind | None#
- classmethod from_check_result(check_result)[source]#
Translate a successful
rclone checkinto aVerifyResult.okis True iff nothing differs, nothing is missing on the destination, and rclone reported no per-file errors.extra(present on the destination only) is carried for reporting but does not flipok. The single source of truth for theCheckResult -> VerifyResultmapping shared by the queue worker, theforce_verifypath, andVerifier.verify().- Parameters:
check_result (
CheckResult)- Return type:
- exlab_wizard.sync.cleanup_interlocks_satisfied(*, job, run_path, now_utc, config, overrides_active)[source]#
Evaluate every §7.1.6 interlock; return True iff all pass.
Logs a debug entry naming the failing interlock when one fails so the operator can see why a job stayed in
CLEANUP_ELIGIBLE.The
run_pathparameter is accepted (but currently unused) so callers can pass the run directory through unchanged; future interlocks (e.g., size-on-disk threshold) may consult it.- Parameters:
job (
SyncJobRow)run_path (
Any)now_utc (
datetime)config (
NASCleanupConfig)
- Return type:
- exlab_wizard.sync.effective_bandwidth_limit_kibps(cfg, *, now_local)[source]#
Return the effective
--bwlimitin KiB/s fornow_local.Decision tree per §7.1.7:
If
cfg.upload_mbpsisNone-> unlimited (None).Else if
cfg.scheduleis empty -> the cap applies always.Else if
now_localfalls inside any schedule window -> the cap applies for this transfer.Else -> unlimited (
None).
- Parameters:
cfg (
BandwidthConfig)now_local (
datetime)
- Return type:
- exlab_wizard.sync.is_eligible(*, validator, creation_json_path, creation)[source]#
Evaluate the §7.3 eligibility rule for a run.
Returns
(True, [])iff there is no hard-tier finding without an active override. Otherwise returns(False, blocking_findings)whereblocking_findingsis the list of unmasked hard-tier findings (so the caller can surface them in logs).The
creation_json_pathis the path to.exlab-wizard/creation.json; the run directory is its parent’s parent. The validator runs in creation-time mode (no walk; just rules over path segments + file names + the cached creation payload).
Modules
Bandwidth schedule evaluator. |
|
Cleanup safety interlocks. |
|
Pre-rclone file-stability guard: confirm files have stopped growing. |
|
Parser for |
|
NAS sync client. |
|
Pre-Sync Gate. |
|
Durable SQLite-backed sync-job queue. |
|
Keep-local-aware, symlink-safe deletion of a run's staging copy. |
|
Sync transports package. |
|
SHA-256 verifier wrapper around |