reservations¶
The reservations package gates every live-lab subcommand on whether the
effective user actually holds the resources the selected lab needs.
It is pluggable: the check itself is fixed, but the “who has what
reserved?” query is answered by a ReservationBackend
implementation — shipped ones, or your own class selected by registered name in .otto/settings.toml.
For narrative setup, configuration, and writing a custom backend, see the user guide.
Package summary¶
Lab reservation / scheduler-check subsystem.
See otto.reservations.protocol for the backend contract and
docs/guide/reservations.md for the end-user and implementer docs.
The backend contract¶
Third-party backends implement the ReservationBackend
Protocol. The contract is deliberately small — three read-only
methods, no write methods of any kind. Otto never mutates scheduler
state.
- class otto.reservations.SupportsUsernameCompletion(*args, **kwargs)¶
Bases:
ProtocolOptional capability: enumerate usernames for
--as-usercompletion.A backend that can list its users implements
list_usernames; otto detects it structurally (isinstance(backend, SupportsUsernameCompletion)) and feeds the values into--as-usertab-completion (cached, seeotto.configmodule.completion_cache.collect_reservation_usernames). Backends that cannot enumerate users simply omit it.
Protocol contract for pluggable lab-reservation backends.
A reservation backend answers two questions for otto:
“What resources does user X currently have reserved?”
“Who, if anyone, currently holds resource Y?”
Otto is strictly a consumer — it never creates, modifies, or releases reservations. The scheduler (Jira, a booking tool, a shared JSON file, anything) remains authoritative.
Implementers¶
Third-party backends implement the ReservationBackend protocol, register under
a bare name via register_reservation_backend("my-team-jira", MyBackend) from an
init module, and are selected in the repo’s .otto/settings.toml:
[reservations]
backend = "my-team-jira"
url = "https://scheduler.example.com"
[reservations.my-team-jira]
api_key_env = "SCHEDULER_API_KEY"
The url key and any [reservations.<name>] sub-table are passed to
the backend’s __init__ as keyword arguments. url is optional on both
sides: implementers may accept and use it, or hardcode their own endpoint —
whichever fits the deployment.
All failure modes that prevent answering a query (network down, database
unreachable, credentials rejected, file corrupt) must be raised as
otto.reservations.check.ReservationBackendError so the CLI can
translate them into a fail-closed startup error with a clear hint about the
--skip-reservation-check escape hatch.
- class otto.reservations.protocol.ReservationBackend(*args, **kwargs)¶
Bases:
ProtocolRead-only view over a reservation scheduler.
- get_reserved_resources(username: str) set[str]¶
Return the set of resource identifiers currently reserved by
username.Parameters¶
- usernamestr
The reservation-system identity to query. Case sensitivity and any other normalization rules are the backend’s responsibility; otto passes the username through unchanged.
Returns¶
- set[str]
Resource identifiers the user currently holds. Empty set if the user has no active reservations. Resource strings must match byte-for-byte the values in
UnixHost.resourcesandLab.resources— any necessary normalization is the backend’s job.
Raises¶
- otto.reservations.check.ReservationBackendError
On any failure that prevents a definitive answer (network error, file I/O error, DB error, credential rejection, malformed data).
- who_reserved(resource: str) list[str]¶
Return the usernames currently holding
resource.Used for error messages when a reservation check fails (e.g.
"shared-lab is held by alice, bob") so the caller knows who to talk to.Parameters¶
- resourcestr
Resource identifier to look up.
Returns¶
- list[str]
The usernames holding the resource, in a deterministic order with duplicates removed. An empty list means no one currently holds it (there is no
Nonesentinel — a resource can have any number of concurrent holders).
Raises¶
- otto.reservations.check.ReservationBackendError
On any failure that prevents a definitive answer.
- class otto.reservations.protocol.SupportsUsernameCompletion(*args, **kwargs)¶
Bases:
ProtocolOptional capability: enumerate usernames for
--as-usercompletion.A backend that can list its users implements
list_usernames; otto detects it structurally (isinstance(backend, SupportsUsernameCompletion)) and feeds the values into--as-usertab-completion (cached, seeotto.configmodule.completion_cache.collect_reservation_usernames). Backends that cannot enumerate users simply omit it.
Exceptions¶
Two exceptions classify the two failure modes a caller cares about: the user doesn’t hold something versus we couldn’t ask. They are surfaced differently in the CLI — see Skip-flag hint policy below.
- exception otto.reservations.check.ReservationBackendError
Bases:
ExceptionRaised by backends when a query cannot be answered.
Network outages, DB errors, malformed data files, and authentication failures all surface as this exception so the CLI can translate them into a single fail-closed startup error.
- exception otto.reservations.check.MissingReservationError
Bases:
ExceptionRaised when the effective user does not hold every required resource.
The message lists the missing resources and their current holders. It does not mention
--skip-reservation-check— that suggestion belongs only in the backend-failure path, never on a legitimate contention failure (or the option gets abused).
The check¶
Lab reservation check logic and exceptions.
The check_reservations() function is the heart of the subsystem:
given a lab, a username, and a backend, it raises
MissingReservationError if the user does not hold every resource
the lab needs. The error message lists missing resources and their current
holders (via who_reserved()) but deliberately does
NOT advertise --skip-reservation-check — that flag is surfaced only when
the backend itself is unreachable, where proceeding requires it.
gate() is the subcommand-facing entry point that wires the check into
the CLI: it reads the per-invocation reservation state from Typer’s
ctx.meta["otto_reservation"], honors the top-level skip flag, emits the
bold-red skip warning when used, and otherwise runs the check.
-
class otto.reservations.check.ReservationState(backend: "'ReservationBackend | None'" =
None, identity: "'ResolvedIdentity | None'" =None, skip_check: 'bool' =False, backend_factory: "'Callable[[], ReservationBackend] | None'" =None)¶ Bases:
object-
backend : ReservationBackend | None =
None¶
-
identity : ResolvedIdentity | None =
None¶
-
backend_factory : Callable[[], ReservationBackend] | None =
None¶
-
backend : ReservationBackend | None =
- exception otto.reservations.check.ReservationBackendError¶
Bases:
ExceptionRaised by backends when a query cannot be answered.
Network outages, DB errors, malformed data files, and authentication failures all surface as this exception so the CLI can translate them into a single fail-closed startup error.
- exception otto.reservations.check.MissingReservationError¶
Bases:
ExceptionRaised when the effective user does not hold every required resource.
The message lists the missing resources and their current holders. It does not mention
--skip-reservation-check— that suggestion belongs only in the backend-failure path, never on a legitimate contention failure (or the option gets abused).
- otto.reservations.check.required_resources(lab: Lab) set[str]¶
Return every resource identifier the lab needs.
The union of the lab’s own
resourcesset and each host’sresourcesset. Any of these resources that are not held by the effective user will causecheck_reservations()to raise.
- otto.reservations.check.check_reservations(lab: Lab, username: str, backend: ReservationBackend) None¶
Raise
MissingReservationErrorifusernamedoes not coverlab.Parameters¶
- labLab
The lab about to be used.
- usernamestr
The reservation-system identity to check against.
- backendReservationBackend
The configured reservation backend.
Raises¶
- MissingReservationError
If any required resource is not held by
username.- ReservationBackendError
If the backend cannot answer the query (network, file, DB failure).
- otto.reservations.check.gate(ctx: Context) None¶
Run the reservation check for this invocation, reading state from ctx.meta.
When
-R(skip_check) is set, a loud warning is emitted regardless of whether a backend was configured. No-ops when no reservation state is present (e.g. unit tests invoking a subcommand app directly) or, after the skip-warning path, when no backend is configured. The active lab is fetched lazily so the no-op paths never require an OttoContext.
Skip-flag hint policy¶
Only ReservationBackendError surfaces
a suggestion to pass --skip-reservation-check / -R — because
with a broken backend the user has no other way to proceed.
MissingReservationError deliberately
does not mention the flag, since offering it on every contention
failure trains users to reach for the bypass instead of fixing the
underlying reservation.
Identity resolution¶
Effective-user resolution for the reservation check.
Precedence: --as-user USERNAME > getpass.getuser().
There is deliberately no persistent config or environment-variable source.
A CLI flag is always visible on the command line that uses it; a sticky
env var in shell rc would be invisible and could cause a user to operate
under someone else’s identity for weeks without realizing. When
--as-user IS used, the top-level Typer callback prints a bold-magenta
banner so the override is impossible to miss.
- class otto.reservations.identity.ResolvedIdentity(username: str, source: '--as-user' | '$USER')¶
Bases:
objectEffective reservation identity for this otto invocation.
Attributes¶
- usernamestr
The username passed to the backend.
- sourceLiteral[”–as-user”, “$USER”]
Where the username came from, used for diagnostic output.
- otto.reservations.identity.resolve_username(as_user: str | None) ResolvedIdentity¶
Resolve the effective reservation identity.
Parameters¶
- as_userstr | None
The value of the top-level
--as-userTyper option.Noneor an empty string means the option was not supplied.
Returns¶
- ResolvedIdentity
(username, source)—source="--as-user"when the flag was used, otherwisesource="$USER".
Bundled backends¶
JSON backend¶
Reference implementation and test double — also a perfectly usable production backend for small teams that don’t have a scheduler yet. See the user guide for the file format.
JSON-file reservation backend — reference implementation and test double.
Intended for two audiences:
Small teams with no scheduler who just want to hand-edit a JSON file checked into the repo (or kept on a shared volume) that lists who holds which resources.
Tests — unit and integration tests construct fixture JSON files and point a
JsonReservationBackendat them.
File format (version: 1):
{
"version": 1,
"reservations": [
{"user": "alice", "resources": ["rack3-psu"], "expires": "2026-05-01T00:00:00Z"},
{"user": "bob", "resources": ["rack4-psu"]}
]
}
reservationsis a list so one user may appear multiple times (useful when merging reservations from multiple sources).expiresis optional; past-dated entries are silently ignored.
-
class otto.reservations.json_backend.JsonReservationBackend(url: str | None =
None, *, path: Path)¶ Bases:
objectRead reservations from a JSON file on disk.
Parameters¶
- urlstr | None
Accepted and ignored. Kept in the signature so the factory (
otto.reservations.build_backend()) can passurl=urluniformly to any backend.- pathPath
Location of the reservation file on disk. Required.
Null backend¶
Default when no [reservations] section is configured, or when
backend = "none" is set.
check_reservations() recognizes this
type and becomes a no-op.
Null reservation backend used when no scheduler is configured.
Selected by setting backend = "none" in the repo’s [reservations]
TOML section. otto.reservations.check.check_reservations() recognizes
this type and becomes a no-op, so teams that haven’t set up a scheduler yet
aren’t blocked.
- class otto.reservations.null_backend.NullReservationBackend¶
Bases:
objectAlways returns “no reservations known” — the check is a no-op.
Backend factory¶
- otto.reservations.build_backend(settings: dict[str, Any], repo_dir: Path) ReservationBackend¶
Construct a reservation backend from a parsed
[reservations]section.Parameters¶
- settingsdict[str, Any]
The
[reservations]sub-dict parsed from.otto/settings.toml. Expected keys:backend—"json","none", or a name registered viaregister_reservation_backend()from an init module. Defaults to"none"when absent.url— optional string, forwarded asurl=...to the backend constructor when present.<backend-name>— optional nested table with backend-specific keyword arguments (e.g.[reservations.json] path = "...").
- repo_dirPath
The SUT repo root. Used only to expand the JSON backend’s
pathsetting when it is relative.
Returns¶
- ReservationBackend
A ready-to-query backend instance.
Raises¶
- ValueError
If
backendnames an unknown backend.- ReservationBackendError
If a third-party backend’s construction fails for backend reasons (network, bad credentials, etc.).
- otto.reservations.register_reservation_backend(name: str, cls: type) None¶
Make a custom reservation backend selectable as
backend = "<name>".Call from an
initmodule listed in.otto/settings.toml. The class must satisfy theReservationBackendprotocol.
- otto.reservations.build_reservation_state(repos: list[Any], *, as_user: str | None, skip_reservation_check: bool, cwd_fallback: Path) ReservationState¶
Resolve the per-invocation reservation state from the active repos.
The first repo with a
[reservations]section wins. Withskip_reservation_check(the-Rbreak-glass flag) the backend is not constructed at all — a scheduler that fails or hangs in its constructor can never block lab access. Abackend_factorythunk is always attached sootto reservationsubcommands can build it on demand.Raises¶
- ReservationBackendError
If construction fails and
skip_reservation_checkis False.
Name → class registry for reservation backends.
Mirrors 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 [reservations] backend =
"<name>" selects it. Built-ins none and json are pre-registered at
import so they resolve through the same path.
- otto.reservations.registry.register_reservation_backend(name: str, cls: type) None¶
Make a custom reservation backend selectable as
backend = "<name>".Call from an
initmodule listed in.otto/settings.toml. The class must satisfy theReservationBackendprotocol.
- otto.reservations.registry.get_reservation_backend_class(name: str) type¶
Return the backend class registered under name.
Raises¶
- ValueError
If name is not registered; the message lists the registered names.
Extension points for implementers¶
A custom backend needs three pieces:
A class that satisfies
ReservationBackend. Protocol satisfaction is structural — no explicit inheritance is required (and none is recommended).An init module that registers the class under a bare name:
from otto.reservations import register_reservation_backend register_reservation_backend("my-team-jira", MyBackend)The init module must be importable (add its containing directory to
libs = [...]in.otto/settings.toml, or install it into the same environment) and listed under[init]in.otto/settings.toml.A ``[reservations]`` entry selecting the registered name:
[reservations] backend = "my-team-jira"Optional per-backend kwargs go in a
[reservations.my-team-jira]sub-table and are passed to the constructor alongside the optionalurlsetting.
The factory calls the class as Class(url=url, **kwargs_from_settings) when url
is set in settings, otherwise Class(**kwargs_from_settings). Accept or omit url
as fits your deployment.
See the user guide for a worked example with request handling, credential loading, and package layout.