exlab_wizard.controller#

Controller package. Backend Spec §4.4.1, §4.7, §4.7.1, §4.8.

Re-exports the public surface of the creation controller – the state machine enums + transition helpers, the in-memory session store, and the CreationController class itself – so callers can write from exlab_wizard.controller import CreationController, SessionState without reaching into the submodules.

class exlab_wizard.controller.CreationController(*, config, validator, template_engine, plugin_host, cache_creation, cache_equipment, cache_log=None, readme_generator=None, nas_sync=None, session_store=None)[source]#

Bases: object

Drives the §4.7 state machine end-to-end.

Composes the validator (Phase 4), template engine (Phase 5), plugin host (Phase 6), cache writers (Phase 3), README generator (Phase 8 – NoOp until then), and NAS sync client (Phase 10 – NoOp until then).

Parameters:
async cancel(session_id, *, discard_files=False)[source]#

Abort an in-flight session.

Pushes a None onto the resume queue (so an INPUT_REQUIRED session wakes immediately), cancels the pipeline task, and runs the cleanup hook. discard_files deletes the partially-created directory; otherwise the directory is left in place as an orphan (Backend Spec §4.7 / §4.8).

Parameters:
  • session_id (str)

  • discard_files (bool)

Return type:

None

async create_project(req)[source]#

Open a project-creation session and start the pipeline.

The session is registered with the store immediately and the pipeline runs as a background asyncio task; the returned SessionHandle reflects the post-VALIDATING state. Failures from the validation gate transition the session to FAILED synchronously before returning – so the caller can detect them on the very first response.

Parameters:

req (ProjectCreateRequest)

Return type:

SessionHandle

async create_run(req)[source]#

Open a run-creation session and start the pipeline.

Parameters:

req (RunCreateRequest)

Return type:

SessionHandle

async resume(session_id, extra_inputs)[source]#

Supply extra_inputs after a PluginInputRequired prompt.

Pushes the payload onto the session’s resume queue; the suspended pipeline wakes, re-spawns the trigger plugin’s worker with the new inputs, and continues. Backend Spec §4.7 / §6.4.

Parameters:
Return type:

SessionHandle

property session_store: SessionStore#

Expose the in-memory session store for the API surface.

async status(session_id)[source]#

Return a snapshot SessionHandle for session_id.

Parameters:

session_id (str)

Return type:

SessionHandle

async subscribe(session_id)[source]#

Yield WebSocket-event dicts for the named session.

Wraps the session’s event_queue. The iterator terminates when the session reaches a terminal state and the queue drains.

Parameters:

session_id (str)

Return type:

AsyncIterator[dict[str, Any]]

class exlab_wizard.controller.NASSyncProtocol(*args, **kwargs)[source]#

Bases: Protocol

The NAS sync surface the controller depends on. Phase 10.

async enqueue(run_path)[source]#
Parameters:

run_path (Path)

Return type:

None

class exlab_wizard.controller.NoOpNASSync[source]#

Bases: object

No-op stand-in for NASSyncClient until Phase 10 lands.

async enqueue(run_path)[source]#
Parameters:

run_path (Path)

Return type:

None

class exlab_wizard.controller.NoOpReadmeGenerator[source]#

Bases: object

Minimal README generator used until Phase 8 lands the real one.

Writes a tiny README.md containing only the core fields so the post-validate pass has something to scan.

async generate(dst, ctx)[source]#
Parameters:
Return type:

Path

class exlab_wizard.controller.Phase(*values)[source]#

Bases: StrEnum

Externally-emitted phase event. Backend Spec §4.6.2.

Sent over the WS /api/v1/sessions/{id}/events channel on every state transition that has a corresponding phase event. The enum values match the spec’s wire-format strings verbatim.

DONE = 'done'#
INPUT_REQUIRED = 'input_required'#
QUEUEING_NAS_SYNC = 'queueing_nas_sync'#
RENDERING_TEMPLATE = 'rendering_template'#
RUNNING_PLUGINS = 'running_plugins'#
VALIDATING_INPUTS = 'validating_inputs'#
VALIDATING_POST_CREATION = 'validating_post_creation'#
WRITING_CACHE = 'writing_cache'#
class exlab_wizard.controller.ProjectCreateRequest(equipment_id, template_path, lims_project, variables, label, operator, objective, readme_extra=<factory>)[source]#

Bases: object

Inputs for CreationController.create_project().

Backend Spec §4.6.1 / UI-spec §3.1. lims_project mirrors the §11.3 lims_project block (uid, short_id, name_at_creation, source).

Parameters:
equipment_id: str#
label: str#
lims_project: dict[str, Any]#
objective: str#
operator: str#
readme_extra: dict[str, Any]#
template_path: Path#
variables: dict[str, Any]#
class exlab_wizard.controller.ReadmeContext(label, operator, objective, equipment_id, project_short_id, run_kind, variables, template, extra_fields=<factory>)[source]#

Bases: object

Inputs handed to the README generator. Phase 8 owns the canonical type; this lightweight stand-in lets Phase 7 ship before Phase 8 lands.

Parameters:
equipment_id: str#
extra_fields: dict[str, Any]#
label: str#
objective: str#
operator: str#
project_short_id: str#
run_kind: str#
template: ResolvedTemplate#
variables: dict[str, Any]#
class exlab_wizard.controller.ReadmeGeneratorProtocol(*args, **kwargs)[source]#

Bases: Protocol

The README generator surface the controller depends on. Phase 8.

async generate(dst, ctx)[source]#
Parameters:
Return type:

Path

class exlab_wizard.controller.RunCreateRequest(equipment_id, project_short_id, template_path, run_kind, variables, label, operator, objective, readme_extra=<factory>, run_date=None, lims_project=<factory>)[source]#

Bases: object

Inputs for CreationController.create_run().

Backend Spec §4.6.1 / UI-spec §3.2 / §3.3. run_kind is the core mode flag and is immutable mid-session per UI-spec §3.3.

Parameters:
equipment_id: str#
label: str#
lims_project: dict[str, Any]#
objective: str#
operator: str#
project_short_id: str#
readme_extra: dict[str, Any]#
run_date: datetime | None = None#
run_kind: RunKind#
template_path: Path#
variables: dict[str, Any]#
class exlab_wizard.controller.Session(session_id, kind, state, request, created_at, last_heartbeat, current_phase=None, next_action=NextAction.NONE, event_queue=None, pending_input=None, error=None, result=None)[source]#

Bases: object

One creation session. Backend Spec §4.4.7.

session_id#

UUID4 string assigned by the store on open().

kind#

"project" or "run" – mirrors the controller’s create_* entry point.

state#

Current SessionState. Mutated only via SessionStore.transition().

request#

The original create request bundle (ProjectCreateRequest or RunCreateRequest).

created_at#

UTC timestamp at SessionStore.open().

last_heartbeat#

Most recent client-driven heartbeat. Refreshed by SessionStore.heartbeat(); consulted by the GC.

current_phase#

Mirrors state_to_phase() of state. Maintained by SessionStore.transition().

next_action#

"awaiting_input" while the session is in SessionState.INPUT_REQUIRED; "none" otherwise.

event_queue#

WebSocket fan-out queue. Set by SessionStore.attach_event_queue().

pending_input#

Latest InputRequiredPayload dict surfaced by the plugin host; cleared on resume.

error#

Structured error envelope ({code, message, ...}) on failure. None while the session is in flight.

result#

Structured done payload at session close. None while in flight or on failure.

Parameters:
created_at: datetime#
current_phase: Phase | None = None#
error: dict[str, Any] | None = None#
event_queue: Queue[dict[str, Any]] | None = None#
is_terminal()[source]#

Return True if the session reached a terminal state.

Return type:

bool

kind: SessionKind#
last_heartbeat: datetime#
next_action: NextAction = 'none'#
pending_input: dict[str, Any] | None = None#
request: Any#
result: dict[str, Any] | None = None#
session_id: str#
state: SessionState#
class exlab_wizard.controller.SessionHandle(session_id, state, current_phase, next_action)[source]#

Bases: object

Snapshot of session state. Backend Spec §4.4.1.

Parameters:
current_phase: Phase | None#
next_action: str#
session_id: str#
state: SessionState#
class exlab_wizard.controller.SessionState(*values)[source]#

Bases: StrEnum

Internal creation-session state. Backend Spec §4.7.

Values mirror the §4.7 state-machine diagram. Lower-case strings so JSON encoding is direct (StrEnum makes SessionState.PENDING render as "pending").

ABORTED = 'aborted'#
CACHE_WRITE = 'cache_write'#
DONE = 'done'#
FAILED = 'failed'#
INPUT_REQUIRED = 'input_required'#
PENDING = 'pending'#
PLUGIN_PASS = 'plugin_pass'#
POST_VALIDATE = 'post_validate'#
RENDERING = 'rendering'#
SYNC_QUEUED = 'sync_queued'#
VALIDATING = 'validating'#
class exlab_wizard.controller.SessionStore[source]#

Bases: object

In-memory session store. Backend Spec §4.4.7.

Sessions are keyed by UUID4 string; the dict is in-memory for v1 (no persistence across server restarts – Backend Spec §4.8).

abandoned_older_than(age)[source]#

Return ids of SessionState.INPUT_REQUIRED sessions whose last_heartbeat is older than age.

Used by gc_loop() to identify sessions abandoned by their operator (no client heartbeat for the configured window). Only INPUT_REQUIRED sessions are eligible – transient states are owned by the controller and finish on their own.

Parameters:

age (timedelta)

Return type:

list[str]

attach_event_queue(session_id, queue)[source]#

Attach a WebSocket fan-out queue to the session.

The controller pushes WebSocket frames onto the queue; the WS /api/v1/sessions/{id}/events channel reads from it. One queue per session; re-attaching replaces the prior queue.

Parameters:
Return type:

None

close(session_id, outcome)[source]#

Stamp a terminal-state session with the outcome envelope.

outcome is the structured payload that the WebSocket done / failed frame carried. DONE outcomes go into result; FAILED outcomes go into error; ABORTED sessions store the outcome under result so the operator can recover the partial-creation summary if the cancel was a deliberate abort.

Parameters:
Return type:

None

async gc_loop(interval_seconds=300.0)[source]#

Run the abandoned-session GC forever. Backend Spec §4.4.7.

Sleeps interval_seconds between passes (default 5 min); on each wake closes every INPUT_REQUIRED session whose heartbeat is older than SESSION_GC_AFTER_SECONDS (default 1 hour). Cancellation is honored cleanly: the loop catches asyncio.CancelledError and re-raises so the caller’s cancellation propagates.

Parameters:

interval_seconds (float)

Return type:

None

get(session_id)[source]#

Return the session keyed by session_id, or None.

Parameters:

session_id (str)

Return type:

Session | None

heartbeat(session_id)[source]#

Refresh last_heartbeat so the GC will not close this session.

No-op when the session is unknown so a stale client does not crash the server.

Parameters:

session_id (str)

Return type:

None

open(kind, req)[source]#

Create a fresh session in SessionState.PENDING state.

Parameters:
  • kind (SessionKind)

  • req (Any)

Return type:

Session

transition(session_id, new_state)[source]#

Move session_id to new_state, updating current_phase.

Validates the transition against exlab_wizard.controller.state_machine.VALID_TRANSITIONS and raises ValueError on illegal edges. next_action is updated alongside state: INPUT_REQUIRED -> "awaiting_input", every other state -> "none".

Parameters:
Return type:

None

exlab_wizard.controller.assert_transition(current, new_state)[source]#

Raise ValueError if current -> new_state is illegal.

Defensive guard used by SessionStore.transition and the controller’s pipeline. Backend Spec §4.7 / §4.7.1 are the source of truth for the legal edges; this function consults VALID_TRANSITIONS via the shared exlab_wizard.utils.state.assert_forward_transition() helper.

Parameters:
Return type:

None

exlab_wizard.controller.state_to_phase(state)[source]#

Return the Phase event corresponding to state.

Backend Spec §4.7.1 mapping table. PENDING, FAILED, and ABORTED return None (no phase event). INPUT_REQUIRED returns Phase.INPUT_REQUIRED; the API surface encodes that as a kind: "input_required" envelope rather than a phase frame, but the mapping is preserved so a single dispatch knows the relationship.

Parameters:

state (SessionState)

Return type:

Phase | None

Modules

creation

Creation controller.

session_store

In-memory session store + GC for creation sessions.

state_machine

Creation-session state machine.