storage

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

otto.storage.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.storage.register_lab_repository(name: str, cls: type) 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.

otto.storage.create_host_from_dict(host_data: dict[str, Any], preferences: dict[str, dict[str, Any]] | None = None) RemoteHost

Create the appropriate RemoteHost subclass from a host dict.

os_type selects the profile / class / spec. preferences is the unified {selector: {capability_list | option_table}} table; for each host the factory cascades it by id into capability selections (forwarded to to_host) and option-value defaults (merged per-key, product-wins). With preferences=None the result is identical to a bare host dict.

exception otto.storage.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”.

class otto.storage.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.storage.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).

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

Bases: object

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

The search paths are supplied once at construction — this is the built-in "json" backend, and otto.storage.build_lab_repository() feeds it the aggregated labs directories. Each hosts.json holds all known hosts; a host’s labs field lists the labs it belongs to, mirroring a row-with-membership database design.

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

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

Raises

LabNotFoundError

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

LabRepositoryError

If a hosts.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 hosts.json exists. A malformed hosts.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.storage.registry.register_lab_repository(name: str, cls: type) 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.

otto.storage.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.storage.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.storage.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”.

otto.storage.factory.create_host_from_dict(host_data: dict[str, Any], preferences: dict[str, dict[str, Any]] | None = None) RemoteHost

Create the appropriate RemoteHost subclass from a host dict.

os_type selects the profile / class / spec. preferences is the unified {selector: {capability_list | option_table}} table; for each host the factory cascades it by id into capability selections (forwarded to to_host) and option-value defaults (merged per-key, product-wins). With preferences=None the result is identical to a bare host dict.

otto.storage.factory.validate_host_dict(host_data: dict[str, Any]) None

Validate a host dict without constructing the host.

os_type must name a registered profile; the profile’s base spec validates the merged dict (extra='forbid', required fields, typed coercion, family-specific field validators for command_frame / filesystem / transfer / docker_capable).

Raises

ValueError

If os_type names no registered profile.

pydantic.ValidationError

On any structural problem (subclass of ValueError).