diff --git a/bot_bottle/cli/commands/__init__.py b/bot_bottle/cli/commands/__init__.py index cda230d9..813524f6 100644 --- a/bot_bottle/cli/commands/__init__.py +++ b/bot_bottle/cli/commands/__init__.py @@ -1,41 +1,52 @@ """CLI subcommand registry. One module per `bot-bottle `, each exposing a `cmd_(argv)` -handler. This package `__init__` assembles them into the COMMANDS table -the dispatcher (`bot_bottle.cli.__main__`) reads. Shared CLI helpers -(`constants`, `tui`) stay one level up in the `cli` package. +handler. This package `__init__` maps command names to their handlers +**lazily**: a short-lived CLI run dispatches exactly one command, so +importing all twelve handlers (and their transitive deps — backend, +manifest, orchestrator, …) up front is wasted work. Each COMMANDS value is +a thin wrapper that imports its handler's module on first call. Shared CLI +helpers (`constants`, `tui`) stay one level up in the `cli` package. """ from __future__ import annotations -from .backend import cmd_backend -from .cleanup import cmd_cleanup -from .commit import cmd_commit -from .edit import cmd_edit -from .help import cmd_help -from .info import cmd_info -from .init import cmd_init -from .list import cmd_list -from .login import cmd_login -from .resume import cmd_resume -from .start import cmd_start -from .supervise import cmd_supervise +from importlib import import_module +from typing import Callable -COMMANDS = { - "backend": cmd_backend, - "cleanup": cmd_cleanup, - "commit": cmd_commit, - "edit": cmd_edit, - "help": cmd_help, - "info": cmd_info, - "init": cmd_init, - "list": cmd_list, - "login": cmd_login, - "resume": cmd_resume, - "start": cmd_start, - "supervise": cmd_supervise, +# command name -> ":". Kept as strings so building +# the registry imports nothing; the module loads only when dispatched. +_HANDLERS: dict[str, str] = { + "backend": "backend:cmd_backend", + "cleanup": "cleanup:cmd_cleanup", + "commit": "commit:cmd_commit", + "edit": "edit:cmd_edit", + "help": "help:cmd_help", + "info": "info:cmd_info", + "init": "init:cmd_init", + "list": "list:cmd_list", + "login": "login:cmd_login", + "resume": "resume:cmd_resume", + "start": "start:cmd_start", + "supervise": "supervise:cmd_supervise", } + +def _lazy(spec: str) -> Callable[[list[str]], "int | None"]: + """Wrap a `:` handler so its module is imported only when + the command is actually dispatched, not when the registry is built.""" + module, attr = spec.split(":") + + def run(argv: list[str]) -> "int | None": + handler = getattr(import_module(f".{module}", __name__), attr) + return handler(argv) + + run.__name__ = attr + return run + + +COMMANDS = {name: _lazy(spec) for name, spec in _HANDLERS.items()} + # Commands that manage host prerequisites (or are otherwise store-free) and # must run before — or without — a migrated DB. `backend` provisions/probes # the host (TAP pool, /dev/kvm, firecracker) and never opens the store, so diff --git a/bot_bottle/orchestrator/__init__.py b/bot_bottle/orchestrator/__init__.py index 0ae241d3..0858e0e2 100644 --- a/bot_bottle/orchestrator/__init__.py +++ b/bot_bottle/orchestrator/__init__.py @@ -5,7 +5,7 @@ A single persistent per-host service that will run the gateway functions agent launches. This package is being built bottom-up, starting with the backend-neutral "consolidation core" that needs no VM packaging: - * `registry` — the SQLite runtime-state store + fail-closed + * `store.registry_store` — the SQLite runtime-state store + fail-closed attribution (source IP + per-bottle identity token). * `broker` — the signed, structured launch-request contract + a `LaunchBroker` (stub for the harness) that verifies @@ -28,36 +28,58 @@ orchestrator -> firecracker). from __future__ import annotations -from .store.registry_store import BottleRecord, RegistryStore, new_identity_token -from .broker import ( - BrokerAuthError, - LaunchBroker, - LaunchRequest, - StubBroker, - sign_request, - verify_request, -) -from .docker_broker import DockerBroker, DockerBrokerError -from ..gateway import Gateway, GatewayError -from .service import Orchestrator -from .server import OrchestratorServer, dispatch, make_server +from typing import TYPE_CHECKING, Any -__all__ = [ - "BottleRecord", - "RegistryStore", - "new_identity_token", - "BrokerAuthError", - "LaunchBroker", - "LaunchRequest", - "StubBroker", - "DockerBroker", - "DockerBrokerError", - "Gateway", - "GatewayError", - "sign_request", - "verify_request", - "Orchestrator", - "OrchestratorServer", - "dispatch", - "make_server", -] +if TYPE_CHECKING: + from .store.registry_store import BottleRecord, RegistryStore, new_identity_token + from .broker import ( + BrokerAuthError, + LaunchBroker, + LaunchRequest, + StubBroker, + sign_request, + verify_request, + ) + from .docker_broker import DockerBroker, DockerBrokerError + from ..gateway import Gateway, GatewayError + from .service import Orchestrator + from .server import OrchestratorServer, dispatch, make_server + + +# Facade name -> submodule that defines it. Lazy so importing a leaf (or the +# parent package, e.g. via `orchestrator.store.store_manager`) doesn't drag the +# whole orchestrator — server, docker_broker + the docker backend, http — while +# `from bot_bottle.orchestrator import RegistryStore` keeps working. +_LAZY: dict[str, str] = { + "BottleRecord": ".store.registry_store", + "RegistryStore": ".store.registry_store", + "new_identity_token": ".store.registry_store", + "BrokerAuthError": ".broker", + "LaunchBroker": ".broker", + "LaunchRequest": ".broker", + "StubBroker": ".broker", + "sign_request": ".broker", + "verify_request": ".broker", + "DockerBroker": ".docker_broker", + "DockerBrokerError": ".docker_broker", + "Gateway": "..gateway", + "GatewayError": "..gateway", + "Orchestrator": ".service", + "OrchestratorServer": ".server", + "dispatch": ".server", + "make_server": ".server", +} + + +def __getattr__(name: str) -> Any: + src = _LAZY.get(name) + if src is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + from importlib import import_module + + value = getattr(import_module(src, __name__), name) + globals()[name] = value + return value + + +__all__ = list(_LAZY)