314dc03b0d
Moves the orchestrator into bot_bottle/orchestrator/ so one install gets everything. Entry point is now `python -m bot_bottle.orchestrator run`. - Add bot_bottle/orchestrator/ with all 14 modules (verbatim move; internal imports were already relative, so no changes inside orchestrator modules) - Rewrite bootstrap.py: remove the lazy bot_bottle import guard, use direct relative imports from ..contrib.* - Add bot_bottle/contrib/forge/base.py: ScopedForge (read-anywhere / write-scoped) - Add bot_bottle/contrib/gitea/client.py: GiteaClient + GiteaForge (urllib.request only) - Add bot_bottle/contrib/gitea/forge_state.py: ForgeState + SqliteForgeStateStore - Add tests/unit/orchestrator/ (82 tests: 63 migrated + 19 new for contrib modules) Closes #321
49 lines
1.5 KiB
Python
49 lines
1.5 KiB
Python
"""State store interface + an in-memory implementation.
|
|
|
|
The orchestrator persists one `RunRecord` per forge-targeted issue. At
|
|
runtime `bootstrap` supplies an adapter over bot-bottle's
|
|
`SqliteForgeStateStore`; the in-memory store here backs tests and a
|
|
`--no-bot-bottle` dry mode.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Protocol
|
|
|
|
from .model import RunRecord
|
|
|
|
|
|
class StateStore(Protocol):
|
|
"""Thin CRUD surface. Mirrors bot-bottle's `ForgeStateStore` so the
|
|
bootstrap adapter is a straight pass-through."""
|
|
|
|
def upsert(self, record: RunRecord) -> None: ...
|
|
|
|
def get(self, owner: str, repo: str, issue_number: int) -> RunRecord | None: ...
|
|
|
|
def delete(self, owner: str, repo: str, issue_number: int) -> None: ...
|
|
|
|
def all(self) -> list[RunRecord]: ...
|
|
|
|
|
|
class InMemoryStateStore:
|
|
"""Dict-backed `StateStore`, keyed by (owner, repo, issue_number)."""
|
|
|
|
def __init__(self) -> None:
|
|
self._by_key: dict[tuple[str, str, int], RunRecord] = {}
|
|
|
|
def upsert(self, record: RunRecord) -> None:
|
|
self._by_key[(record.owner, record.repo, record.issue_number)] = record
|
|
|
|
def get(self, owner: str, repo: str, issue_number: int) -> RunRecord | None:
|
|
return self._by_key.get((owner, repo, issue_number))
|
|
|
|
def delete(self, owner: str, repo: str, issue_number: int) -> None:
|
|
self._by_key.pop((owner, repo, issue_number), None)
|
|
|
|
def all(self) -> list[RunRecord]:
|
|
return sorted(
|
|
self._by_key.values(),
|
|
key=lambda r: (r.owner, r.repo, r.issue_number),
|
|
)
|