host.product

Product lifecycle strategy for hosts.

A Product is a unit of software-under-test deployed to a host — the lifecycle analog of BinaryLoader. It is a behavior contract (an ABC): projects subclass it and inject instances via products. The host orchestrates; the product knows how to stage/install/uninstall/check itself.

It is intentionally not a pydantic model — that would force every project product into pydantic and diverge from the sibling host strategies (CommandFrame, BinaryLoader, EmbeddedFileSystem). Concrete subclasses pick their own data representation (@dataclass or an OttoModel).

Products are behavior, so they are customized in code, not lab data: a product repo registers a register_product_provider() callback from a .otto init module, and otto applies it to each host as it is ingested (see apply_product_providers()). Lab data stays product-agnostic and evolves independently of product code; declaring products in lab data is deliberately not supported.

class otto.host.product.Product

Bases: ABC

A unit of software-under-test deployed to a host (behavior contract).

name : str

Logical identity — used for logging, is_installed lookups, and dedup. Not a file path: a product may be multi-file or installed from a repo.

abstract async stage(host: Host) tuple[Status, str]

Transfer/place this product’s artifacts onto host (no install).

abstract async install(host: Host) tuple[Status, str]

Install this product’s already-staged artifacts on host.

abstract async uninstall(host: Host) tuple[Status, str]

Remove this product from host.

abstract async is_installed(host: Host) bool

Return True when this product is currently installed on host.

class otto.host.product.FileProduct(artifact: ~pathlib.Path, name: str = '', dest_dir: ~pathlib.Path = <factory>)

Bases: Product

Convenience base for a product that is a single artifact file.

stage() transfers the artifact via put(). name defaults to the artifact’s basename. install/uninstall/is_installed remain abstract — they are inherently project-specific. Once the remote file-ops phase lands, the natural is_installed is await host.exists(self.dest_dir / self.artifact.name).

artifact : Path

Local path to the artifact file to stage onto the host.

name : str

Logical name; defaults to artifact.name when left empty.

dest_dir : Path

Destination directory on the host; resolved against the host’s default_dest_dir by put().

async stage(host: Host) tuple[Status, str]

Transfer/place this product’s artifacts onto host (no install).

otto.host.product.ProductProvider

A function that, given a host, returns the products it should carry.

Registered from a .otto init module via register_product_provider() and run once per lab-ingested host. All product knowledge stays in product-repo code; lab data never names a product.

otto.host.product.register_product_provider(provider: Callable[[Host], Iterable[Product] | None]) None

Register a function that decides which products a host carries.

Call from an init module listed in .otto/settings.toml — the same extension hook the other host strategies use. The provider runs once per lab-ingested host; inspect the host’s product-agnostic attributes (element, element_id, os_type, id, ip, resources) and return the products that host should carry (or None/[] for none). Behavior lives in code; lab data stays product-agnostic.

otto.host.product.apply_product_providers(host: Host) None

Run every registered provider against host, attaching their products.

Called at the single lab-ingest chokepoint (otto.storage.factory.create_host_from_dict()). Providers run in registration order and their results are concatenated onto host.products. A product whose Product.name already appears on the host is skipped (deduplication guards two overlapping providers). A provider that raises propagates — a misconfigured provider fails ingest loudly.