Source code for shinobi.backends
"""Backend abstraction: a backend takes a cab and a resolved argv and runs
it somewhere -- natively, in a container, on Slurm, on Kubernetes, ...
A backend knows nothing about recipes or output schemas beyond the argv
it's handed and the cab's ``image``/``command`` metadata; it only knows
how to execute and how to capture output. Wrangling that output into
structured results is the dispatch layer's job, so a backend returns a
raw ``BackendRun`` (returncode/stdout/stderr), nothing schema-aware.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any
from shinobi.results import BackendRun
from shinobi.steps.schema import Cab
[docs]
class Backend(ABC):
"""Abstract base class for execution backends.
Attributes:
name: Registry key used to look up this backend (e.g. ``"native"``,
``"container"``, ``"slurm"``).
"""
name: str
[docs]
@abstractmethod
def run(
self,
cab: Cab,
argv: list[str],
inputs: dict[str, Any],
*,
label: str = "",
stream: bool = True,
) -> BackendRun:
"""Execute argv (as built by shinobi.policies.build_argv) and
return a BackendRun. Must not raise on a non-zero exit -- that's
reported via BackendRun.returncode / BackendRun.success.
``inputs`` is the *prepared* inputs dict argv was built from (the
one `_prepare_inputs` produces, so MUTABLE fields are the caller's
own objects by reference). Most backends ignore it, but container
backends need it to know which File/MS-valued params to bind-mount.
``label``/``stream`` control live stdout/stderr echo (see
`shinobi.backends._stream.run_streaming`) -- only `native` and
`container` act on them today; `slurm`/`kubernetes` accept and
ignore both (neither has any log-tailing infrastructure yet, so
they keep reading output once after the job/pod finishes).
"""
_REGISTRY: dict[str, type[Backend]] = {}
[docs]
def register(backend_cls: type[Backend]) -> type[Backend]:
"""Register a backend class under its ``name`` attribute.
Intended for use as a class decorator on `Backend` subclasses.
Args:
backend_cls: The backend class to register.
Returns:
The same class, unmodified, so it can be used as a decorator.
"""
_REGISTRY[backend_cls.name] = backend_cls
return backend_cls
[docs]
def get_backend(name: str, **opts) -> Backend:
"""Instantiate a registered backend by name.
Args:
name: Registry key of the backend (e.g. ``"native"``, ``"slurm"``).
**opts: Keyword arguments forwarded to the backend's constructor.
Returns:
A new instance of the requested backend.
Raises:
ValueError: If no backend is registered under ``name``.
"""
try:
backend_cls = _REGISTRY[name]
except KeyError:
raise ValueError(
f"unknown backend '{name}' (available: {sorted(_REGISTRY)})"
) from None
return backend_cls(**opts)
[docs]
def registered_backend_classes() -> list[type[Backend]]:
"""Return all backend classes currently registered.
Returns:
A list of registered `Backend` subclasses.
"""
return list(_REGISTRY.values())
# Import submodules for their @register side effects.
from shinobi.backends import container as _container # noqa: E402,F401
from shinobi.backends import kubernetes as _kubernetes # noqa: E402,F401
from shinobi.backends import native as _native # noqa: E402,F401
from shinobi.backends import recording as _recording # noqa: E402,F401
from shinobi.backends import slurm as _slurm # noqa: E402,F401