exlab_wizard.controller.creation#

Creation controller. Backend Spec §4.4.1, §4.7, §4.8.

Composes the validator, template engine, plugin host, cache writers, README generator, and NAS sync client into the §4.7 state machine, driving each session from SessionState.PENDING through to SessionState.DONE (or SessionState.FAILED / SessionState.ABORTED on failure / cancel).

Validation gate (UI-spec §2). Before transitioning out of SessionState.VALIDATING the controller enforces the mandatory core-field set:

  • label non-empty after trim, ≤ 100 chars.

  • operator non-empty after trim; allowlisted when config.operators.allowlist is non-empty.

  • objective non-empty after trim, ≤ 2000 chars.

  • equipment_id is in config.equipment.

  • The project-folder name (the verbatim <project>/ segment, §3.2) is a safe single filesystem path segment per exlab_wizard.paths.validate_project_name().

  • Template-required field ids and config-required field ids are all present in readme_extra.

ReadmeGenerator and NASSyncClient are injected as Protocols so this phase ships before Phase 8 / Phase 10 land their concrete implementations. NoOpReadmeGenerator writes a minimal README.md that the post-validate pass can scan; NoOpNASSync is a true no-op (the sync queue is built in Phase 10).

Classes

CreationController(*, config, validator, ...)

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

NASSyncProtocol(*args, **kwargs)

The NAS sync surface the controller depends on.

NoOpNASSync()

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

NoOpReadmeGenerator()

Lightweight README generator for tests / headless fixtures.

ProjectCreateRequest(equipment_id, ...[, ...])

Inputs for CreationController.create_project().

ReadmeGeneratorProtocol(*args, **kwargs)

The README generator surface the controller depends on.

RunCreateRequest(equipment_id, project_name, ...)

Inputs for CreationController.create_run().

SessionHandle(session_id, state, ...)

Snapshot of session state.

class exlab_wizard.controller.creation.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:
apply_config(config, *, plugin_host=None)[source]#

Swap the controller’s config (and optionally plugin host) in place.

Every config read happens at create-time, so reassigning self._config makes a live settings save take effect on the next create_* call without a tray relaunch. plugin_host is re-injected only when the coordinator rebuilt it (a paths.plugin_dir change); otherwise the existing host is kept.

Parameters:
Return type:

None

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.creation.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.creation.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.creation.NoOpReadmeGenerator[source]#

Bases: object

Lightweight README generator for tests / headless fixtures.

Writes a tiny README.md containing only the core fields – enough for the post-validate pass to scan – and reports the cache path without performing the full §10 field merge or validation. Production injects the real ReadmeGenerator.

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

tuple[Path, Path]

class exlab_wizard.controller.creation.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.creation.ReadmeContext(level, core, template_fields, config_fields, custom_fields, system, template_field_decls=<factory>, config_field_decls=<factory>)[source]#

Bases: object

Inputs to ReadmeGenerator. Composed by the controller.

The controller pre-merges the four layers into the dicts below so the generator does not need to know about the merge order; the generator’s job is to validate, render, and persist.

Parameters:
config_field_decls: list[TemplateFieldDecl]#
config_fields: dict[str, Any]#
core: CoreFields#
custom_fields: list[CustomField]#
level: CreationLevel#
system: SystemFields#
template_field_decls: list[TemplateFieldDecl]#
template_fields: dict[str, Any]#
class exlab_wizard.controller.creation.ReadmeGeneratorProtocol(*args, **kwargs)[source]#

Bases: Protocol

The README generator surface the controller depends on. Backend §10.

Mirrors exlab_wizard.readme.ReadmeGenerator.generate(): validate the composed ReadmeContext, write both README.md and readme_fields.json, and return (readme, cache).

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

tuple[Path, Path]

class exlab_wizard.controller.creation.RunCreateRequest(equipment_id, project_name, 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. project_name is the human-readable LIMS name of the parent project – the verbatim <project>/ folder segment (§3.2). The run inherits the parent’s full LIMS identity (uid / short_id) from that project’s creation.json at pipeline time.

Parameters:
equipment_id: str#
label: str#
lims_project: dict[str, Any]#
objective: str#
operator: str#
project_name: 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.creation.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#