PRD 0003: Bottle Backend abstraction #5

Merged
didericis merged 44 commits from add-bottle-factory-abstraction into main 2026-05-11 14:49:43 -04:00
4 changed files with 18 additions and 10 deletions
Showing only changes of commit aaed390953 - Show all commits
+6 -2
View File
@@ -31,7 +31,6 @@ from abc import ABC, abstractmethod
from contextlib import AbstractContextManager from contextlib import AbstractContextManager
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Protocol
from ..log import die from ..log import die
from ..manifest import Manifest from ..manifest import Manifest
@@ -82,7 +81,7 @@ class BottleCleanupPlan(ABC):
short-circuit before showing the y/N.""" short-circuit before showing the y/N."""
class Bottle(Protocol): class Bottle(ABC):
"""Handle to a running bottle. Yielded by a platform's launch step. """Handle to a running bottle. Yielded by a platform's launch step.
`exec_claude` runs `claude` inside the bottle and blocks until the `exec_claude` runs `claude` inside the bottle and blocks until the
@@ -92,8 +91,13 @@ class Bottle(Protocol):
name: str name: str
@abstractmethod
def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: ... def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: ...
@abstractmethod
def cp_in(self, host_path: str, container_path: str) -> None: ... def cp_in(self, host_path: str, container_path: str) -> None: ...
@abstractmethod
def close(self) -> None: ... def close(self) -> None: ...
+4 -2
View File
@@ -5,21 +5,23 @@ The bulk of the implementation lives in sibling modules:
- util: thin Docker subprocess wrappers - util: thin Docker subprocess wrappers
- bottle_plan: DockerBottlePlan - bottle_plan: DockerBottlePlan
- bottle_cleanup_plan: DockerBottleCleanupPlan - bottle_cleanup_plan: DockerBottleCleanupPlan
- bottle: _DockerBottle handle - bottle: DockerBottle handle
- platform: DockerBottlePlatform - platform: DockerBottlePlatform
This file only re-exports the platform class so This file only re-exports the public names so
`from claude_bottle.bottles.docker import DockerBottlePlatform` keeps `from claude_bottle.bottles.docker import DockerBottlePlatform` keeps
working. working.
""" """
from __future__ import annotations from __future__ import annotations
from .bottle import DockerBottle
from .bottle_cleanup_plan import DockerBottleCleanupPlan from .bottle_cleanup_plan import DockerBottleCleanupPlan
from .bottle_plan import DockerBottlePlan from .bottle_plan import DockerBottlePlan
from .platform import DockerBottlePlatform from .platform import DockerBottlePlatform
__all__ = [ __all__ = [
"DockerBottle",
"DockerBottleCleanupPlan", "DockerBottleCleanupPlan",
"DockerBottlePlan", "DockerBottlePlan",
"DockerBottlePlatform", "DockerBottlePlatform",
+4 -2
View File
@@ -1,4 +1,4 @@
"""_DockerBottle — concrete Bottle handle yielded by """DockerBottle — concrete Bottle handle yielded by
DockerBottlePlatform.launch. DockerBottlePlatform.launch.
Holds the container name plus the in-container prompt path so Holds the container name plus the in-container prompt path so
@@ -10,8 +10,10 @@ from __future__ import annotations
import subprocess import subprocess
from .. import Bottle
class _DockerBottle:
class DockerBottle(Bottle):
"""Concrete Bottle for Docker.""" """Concrete Bottle for Docker."""
def __init__(self, container: str, teardown, prompt_path_in_container: str | None): def __init__(self, container: str, teardown, prompt_path_in_container: str | None):
+4 -4
View File
@@ -2,7 +2,7 @@
Methods: Methods:
.prepare(spec, stage_dir=...) -> DockerBottlePlan .prepare(spec, stage_dir=...) -> DockerBottlePlan
.launch(plan) -> ContextManager[_DockerBottle] .launch(plan) -> ContextManager[DockerBottle]
.prepare_cleanup() -> DockerBottleCleanupPlan .prepare_cleanup() -> DockerBottleCleanupPlan
.cleanup(plan) -> None .cleanup(plan) -> None
.list_active() -> None .list_active() -> None
@@ -25,7 +25,7 @@ from ...env_resolve import env_resolve
from ...log import die, info from ...log import die, info
from .. import BottleCleanupPlan, BottlePlan, BottlePlatform, BottleSpec from .. import BottleCleanupPlan, BottlePlan, BottlePlatform, BottleSpec
from . import util as docker_mod from . import util as docker_mod
from .bottle import _DockerBottle from .bottle import DockerBottle
from .bottle_cleanup_plan import DockerBottleCleanupPlan from .bottle_cleanup_plan import DockerBottleCleanupPlan
from .bottle_plan import DockerBottlePlan from .bottle_plan import DockerBottlePlan
@@ -128,7 +128,7 @@ class DockerBottlePlatform(BottlePlatform):
) )
@contextmanager @contextmanager
def launch(self, plan: BottlePlan) -> Iterator[_DockerBottle]: def launch(self, plan: BottlePlan) -> Iterator[DockerBottle]:
"""Build, launch, and provision a Docker bottle. Teardown on exit.""" """Build, launch, and provision a Docker bottle. Teardown on exit."""
assert isinstance(plan, DockerBottlePlan), ( assert isinstance(plan, DockerBottlePlan), (
f"DockerBottlePlatform.launch expects DockerBottlePlan, " f"DockerBottlePlatform.launch expects DockerBottlePlan, "
@@ -187,7 +187,7 @@ class DockerBottlePlatform(BottlePlatform):
prompt_path = self._provision_container(plan, container) prompt_path = self._provision_container(plan, container)
bottle = _DockerBottle(container, teardown, prompt_path) bottle = DockerBottle(container, teardown, prompt_path)
yield bottle yield bottle
finally: finally:
teardown() teardown()