labs

The labs package provides a DB-agnostic host-source (LabRepository) backend, selected by name and constructed via otto.labs.build_lab_repository(). The built-in json backend reads lab.json files; custom backends register a name via otto.labs.register_lab_repository() from an init module.

otto.labs.build_lab_repository(settings: dict[str, Any], repo_dir: Path, *, search_paths: list[Path] | None = None) LabRepository

Construct a host-source backend from a parsed [lab] section.

Parameters

settingsdict[str, Any]

The [lab] sub-dict parsed from .otto/settings.toml. backend selects a registered name (defaults to "json"); [lab.<name>] holds the backend’s keyword arguments.

repo_dirPath

The SUT repo root, forwarded as repo_dir= to a custom backend’s constructor. The built-in json backend ignores it and uses search_paths instead.

search_pathslist[Path] | None

The aggregated labs directories. Passed to the built-in json backend (preserving today’s multi-repo path merge); custom backends carry their own config and do not receive it.

Returns

LabRepository

A ready-to-query backend instance.

Raises

ValueError

If the [lab] envelope is malformed.

LabRepositoryError

If backend names an unknown (unregistered) backend.

otto.labs.register_lab_repository(name: str, cls: type, *, overwrite: bool = False) None

Make a custom host-source backend selectable as backend = "<name>".

Call from an init module listed in .otto/settings.toml. The class must satisfy the LabRepository protocol.

overwrite replaces an existing registration under name deliberately (e.g. a built-in); by default a duplicate name raises.

exception otto.labs.LabNotFoundError

Bases: LabRepositoryError

load_lab was asked for a lab name the backend does not know.

A missing lab must raise this — not return None or raise a bare KeyError / FileNotFoundError — so callers can distinguish “unknown lab” from “backend is broken”.

LabRepository protocol — the DB-agnostic interface all lab-repository backends satisfy.

class otto.labs.protocol.LabRepository(*args, **kwargs)

Bases: Protocol

DB-agnostic interface for loading labs.

A backend is configured at construction time (the built-in JSON backend takes its search_paths in __init__), then queried through the two methods below. Selection and construction happen in otto.labs.build_lab_repository().

load_lab(name: str, preferences: dict[str, dict[str, Any]] | None = None) Lab

Load a lab by name.

Parameters

namestr

Name of the lab to load.

preferencesdict[str, dict[str, Any]] | None

The unified {selector: {capability: [...] | option_table: {key: val}}} product-preference table forwarded to the factory, which matches each host’s id and applies the result. None reproduces today’s behavior.

Returns

Lab

Fully constructed lab.

Raises

LabNotFoundError

If no lab named name exists.

LabRepositoryError

If the backend fails to satisfy the query (I/O, parse, network).

list_labs() list[str]

List all lab names this backend can provide.

Returns

list[str]

Lab names (every element a str).

Built-in "json" LabRepository backend: loads labs from lab.json files.

otto.labs.json_repository.parse_lab_sections(data: object, source: str) dict[str, list[Any]]

Validate a parsed lab.json object’s section shape; return its sections.

The single source of truth for the lab.json object contract — shared by the runtime loader (JsonFileLabRepository._load_lab_file) and the otto init doctor (otto.cli.init._validate_lab) so the doctor cannot drift from what otto actually accepts (there is no second validator to drift). data is the already-parsed JSON value; source names its origin (a file path) for error messages.

Top-level _-prefixed keys are comment space (same idiom as host entries); unknown sections fail loud. Returns a dict carrying every known section as a (possibly empty) list.

Raises

LabRepositoryError

If data is not a JSON object, carries an unknown top-level section, or a section’s value is not a JSON array.

class otto.labs.json_repository.JsonFileLabRepository(search_paths: list[Path] | None = None)

Bases: object

Load labs from lab.json files under a fixed set of search paths.

Each lab.json is a JSON object with array sections — {"hosts": [...], "links": [...]}. The search paths are supplied once at construction — this is the built-in "json" backend, and otto.labs.build_lab_repository() feeds it the aggregated labs directories. The hosts section holds all known hosts; a host’s labs field lists the labs it belongs to, mirroring a row-with-membership database design. Top-level _-prefixed keys are comment space; unknown sections fail loud.

load_lab(name: str, preferences: dict[str, dict[str, Any]] | None = None) Lab

Load a lab by filtering hosts from the configured lab.json files.

Raises

LabNotFoundError

If no lab.json exists in any search path, or no host belongs to the requested lab.

LabRepositoryError

If a lab.json is malformed or a host’s data is invalid.

list_labs() list[str]

List all lab names referenced by hosts across the configured paths.

Returns an empty list when no lab.json exists. A malformed lab.json is skipped rather than raised, so listing stays best-effort.

Name → class registry for host-source (LabRepository) backends.

Mirrors otto.reservations.registry and otto’s other extension registries (register_term_backend / register_transfer_backend / register_host_class): a custom backend registers a bare name from an init module, and [lab] backend = "<name>" selects it. The built-in json backend is pre-registered at import so it resolves through the same path.

otto.labs.registry.register_lab_repository(name: str, cls: type, *, overwrite: bool = False) None

Make a custom host-source backend selectable as backend = "<name>".

Call from an init module listed in .otto/settings.toml. The class must satisfy the LabRepository protocol.

overwrite replaces an existing registration under name deliberately (e.g. a built-in); by default a duplicate name raises.

otto.labs.registry.get_lab_repository_class(name: str) type

Return the backend class registered under name.

Raises

LabRepositoryError

If name is not registered; the message lists the registered names.

Error contract for the host-source (LabRepository) backend interface.

Mirrors the reservation backend’s error contract (ReservationBackendError): a backend signals trouble through these types so callers and the conformance suite can rely on a stable surface instead of backend-specific exceptions.

exception otto.labs.errors.LabRepositoryError

Bases: Exception

A host-source backend failed to satisfy a query.

Raised for I/O, network, parse, or credential failures while loading or listing labs — anything other than “the named lab does not exist”, which raises the more specific LabNotFoundError.

exception otto.labs.errors.LabNotFoundError

Bases: LabRepositoryError

load_lab was asked for a lab name the backend does not know.

A missing lab must raise this — not return None or raise a bare KeyError / FileNotFoundError — so callers can distinguish “unknown lab” from “backend is broken”.