exlab_wizard.lims#

LIMS integration package. Backend Spec §7.2 + §7.4.

Public surface:

class exlab_wizard.lims.HealthStatus(ok, latency_ms, reason=None)[source]#

Bases: object

Result of LIMSClient.health_check(). Backend Spec §7.2.3.

ok is True iff the LIMS responded to GET /me with 2xx. latency_ms is the wall-clock duration in milliseconds (rounded to the nearest int). reason is None on success and carries a short human-readable failure summary on failure.

Parameters:
latency_ms: int#
ok: bool#
reason: str | None = None#
class exlab_wizard.lims.KeyringStore(*, state_dir, passphrase_provider=None)[source]#

Bases: object

OS keyring with encrypted-at-rest fallback.

Backend Spec §7.4. Tries the OS keyring first via the keyring module. When that backend raises keyring.errors.KeyringError (or one of its subclasses for “no backend installed”), falls back to <state_dir>/exlab-wizard/secrets.enc encrypted with Fernet (AES-128-CBC + HMAC-SHA256), keyed by an Argon2id KDF over a user-supplied master passphrase.

The fallback path requires a passphrase_provider callable; if the keyring is available this argument may be None – it is only invoked when the fallback engages.

Parameters:
delete_password(*, username)[source]#

Remove the secret stored under (KEYRING_SERVICE, username).

Missing entries are silent in both backends. Keyring errors defer to the fallback.

Parameters:

username (str)

Return type:

None

get_password(*, username)[source]#

Look up the secret for (KEYRING_SERVICE, username).

Returns None when the entry is absent in both backends. Errors from the OS keyring are caught and the fallback is consulted; errors from the fallback propagate as exlab_wizard.errors.KeyringUnavailableError.

Parameters:

username (str)

Return type:

str | None

is_keyring_available()[source]#

Best-effort probe of the OS keyring backend.

Implemented as a round-trip set + delete of a sentinel value. A backend that raises on either side is considered unavailable. Per §7.4.4 the keyring package’s “fail” backends raise keyring.errors.KeyringError from these calls.

Return type:

bool

set_password(*, username, password)[source]#

Persist password under (KEYRING_SERVICE, username).

On a keyring backend error the fallback file is written; on fallback failure this raises exlab_wizard.errors.KeyringUnavailableError.

Parameters:
  • username (str)

  • password (str)

Return type:

None

class exlab_wizard.lims.LIMSCache(db_path, *, ttl_hours=24)[source]#

Bases: object

SQLite TTL cache for LIMS project rows. Backend Spec §7.2.4.

The cache is async-native via aiosqlite so that LIMSClient can interleave cache reads with httpx network calls without blocking the event loop.

Parameters:
async close()[source]#

Close the underlying aiosqlite connection.

Return type:

None

async get_project(endpoint, uid_or_short_id)[source]#

Return one project by uid or short_id, or None if absent.

Parameters:
  • endpoint (str)

  • uid_or_short_id (str)

Return type:

LIMSProject | None

async init()[source]#

Create the table and index if absent. Idempotent.

Return type:

None

async is_fresh(endpoint)[source]#

True iff the most recent last_refreshed is within ttl_hours of the wizard’s current UTC time. False when the cache has no rows for endpoint.

Parameters:

endpoint (str)

Return type:

bool

async list_projects(endpoint, *, status_filter=None)[source]#

Return every cached project for endpoint, optionally filtered by status_filter (an OR of allowed status values).

Parameters:
Return type:

list[LIMSProject]

async upsert_many(endpoint, projects)[source]#

Insert or update every row. last_refreshed is taken from each LIMSProject.fetched_at; the caller stamps that value before invoking this method so a single refresh batch shares one timestamp.

Parameters:
Return type:

None

class exlab_wizard.lims.LIMSClient(*, endpoint, email, keyring_password_provider)[source]#

Bases: object

Read-only LIMS client. Backend Spec §7.2 Mapping B.

Cookie-session auth: login() establishes the session; subsequent list/get methods reuse the cookie. On 401, the client refreshes the cookie via login() once before failing.

The keyring_password_provider callable is invoked from login() when no explicit password is passed. Callers wire this to exlab_wizard.lims.keyring_store.KeyringStore.get_password in production; tests can pass a lambda that returns a static value.

Parameters:
async close()[source]#

Close the underlying httpx.AsyncClient. Idempotent.

Return type:

None

property email: str#

Configured operator email; used as the login username.

property endpoint: str#

Configured base URL (trailing slashes stripped).

async get_me()[source]#

GET /api/v1/me; returns the current operator’s row.

Return type:

LIMSUser

async get_project(uid_or_short_id)[source]#

GET /api/v1/projects/<id>; returns None on 404.

uid_or_short_id may be either a UUID (uid column) or a PROJ-NNNN string (short_id column). The LIMS resolves both at the same endpoint.

Parameters:

uid_or_short_id (str)

Return type:

LIMSProject | None

async health_check()[source]#

Return a HealthStatus snapshot. Backend Spec §7.2.3.

Calls GET /api/v1/me and times the response. On any error (network, 4xx, 5xx) returns ok=False with a short reason rather than raising – the Settings “Test connection” UX needs a value to render.

Return type:

HealthStatus

async list_projects(*, status_filter=None)[source]#

GET /api/v1/projects; returns one LIMSProject per row.

status_filter is an optional list of allowed status values; rows whose status is not in the set are dropped on the client side. Filtering happens after deserialization so the wire format stays uniform.

Wire envelope: upstream returns {"data": [...], "count": N}; a missing data key is treated as an empty list rather than propagating a KeyError to the caller.

Parameters:

status_filter (list[str] | None)

Return type:

list[LIMSProject]

async login(*, password=None)[source]#

POST /api/v1/login with email + password.

On success the underlying httpx.AsyncClient retains the session cookie automatically; subsequent reads reuse it.

Raises exlab_wizard.errors.ConfigError when the keyring provider returns no password and none was supplied – that is a configuration condition, not a transient network failure.

Parameters:

password (str | None)

Return type:

None

class exlab_wizard.lims.LIMSProject(*, uid: str, short_id: str, name: str, status: LIMSProjectStatus, owner: str, fetched_at: str, description: str | None = None, contact_name: str | None = None, metadata: dict = <factory>)[source]#

Bases: Struct

One LIMS project row. Backend Spec §7.2.3.

The metadata field is a JSONB blob the LIMS owns; ExLab-Wizard does not mutate it. fetched_at is a UTC ISO 8601 timestamp set when the wizard pulled the row – the local cache uses it for freshness bookkeeping (§7.2.4).

contact_name: str | None#
description: str | None#
fetched_at: str#
metadata: dict#
name: str#
owner: str#
short_id: str#
status: LIMSProjectStatus#
uid: str#
class exlab_wizard.lims.LIMSUser(*, uid: str, email: str, role: str)[source]#

Bases: Struct

One LIMS user row. Backend Spec §7.2.3.

Mirrors the upstream safe_user contract returned by GET /api/v1/me. Only the fields ExLab-Wizard surfaces are typed; everything else is dropped by msgspec.

email: str#
role: str#
uid: str#
class exlab_wizard.lims.OfflineCatalogue(schema_version, produced_by, produced_at, lims_endpoint, projects)[source]#

Bases: object

Decoded offline catalogue. Backend Spec §7.2.9.1.

schema_version is pinned to the constant declared in exlab_wizard.constants.schema_versions; mismatches surface as exlab_wizard.errors.ConfigError.

lims_endpoint is verified by read_catalogue() against the consumer’s configured LIMS endpoint; mismatches are rejected per §7.2.9.3 to defend against accidentally pointing at a different lab’s LIMS.

Parameters:
lims_endpoint: str#
produced_at: str#
produced_by: str#
projects: list[LIMSProject]#
schema_version: str#
exlab_wizard.lims.read_catalogue(path, *, expected_endpoint)[source]#

Read and validate the catalogue file.

Raises exlab_wizard.errors.ConfigError on any of:

  • file missing / unreadable

  • JSON parse error

  • schema_version is not the constant exlab_wizard.constants.OFFLINE_CATALOGUE_VERSION

  • lims_endpoint differs from expected_endpoint (per §7.2.9.3 the producer’s LIMS must match the consumer’s configuration; cross-lab leakage is rejected, not warned).

Parameters:
  • path (Path)

  • expected_endpoint (str)

Return type:

OfflineCatalogue

exlab_wizard.lims.write_catalogue(path, catalogue)[source]#

Atomically write catalogue to path. Backend Spec §7.2.9.2.

Protocol: serialize, write to <path>.tmp.<pid>, fsync, then os.replace to the final path. Concurrent producers do not corrupt the file – each rename is atomic; the last writer wins.

Parameters:
Return type:

None

Modules

cache

aiosqlite-backed cache for LIMS project rows.

catalogue

Offline LIMS project catalogue read/write.

client

Read-only LIMS client (Mapping B).

keyring_store

OS keyring storage with encrypted-at-rest fallback.

schemas

msgspec.Struct types for the LIMS read-only client.