refactor: extract QueueStore and AuditStore to their own modules
lint / lint (push) Successful in 2m2s
test / unit (pull_request) Successful in 56s
test / integration (pull_request) Successful in 20s
test / coverage (pull_request) Failing after 59s

Moves _QueueStore → bot_bottle/queue_store.py (public QueueStore) and
_AuditStore → bot_bottle/audit_store.py (public AuditStore). Removes
the public queue_db_path() function; QueueStore resolves the DB path
via host_db_path() on the host, or via the SUPERVISE_DB_PATH env var
in the sidecar container (internal mechanism, not public API).

Adds queue_store.py and audit_store.py to Dockerfile.sidecars so the
sidecar bundle picks them up. Updates __all__ in supervise.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 21:45:08 +00:00
parent 29904609da
commit 244ad6a914
6 changed files with 394 additions and 312 deletions
+2
View File
@@ -66,6 +66,8 @@ COPY bot_bottle/egress_dlp_config.py /app/egress_dlp_config.py
COPY bot_bottle/egress_addon.py /app/egress_addon.py COPY bot_bottle/egress_addon.py /app/egress_addon.py
COPY bot_bottle/dlp_detectors.py /app/dlp_detectors.py COPY bot_bottle/dlp_detectors.py /app/dlp_detectors.py
COPY bot_bottle/yaml_subset.py /app/yaml_subset.py COPY bot_bottle/yaml_subset.py /app/yaml_subset.py
COPY bot_bottle/queue_store.py /app/queue_store.py
COPY bot_bottle/audit_store.py /app/audit_store.py
COPY bot_bottle/supervise.py /app/supervise.py COPY bot_bottle/supervise.py /app/supervise.py
COPY bot_bottle/supervise_server.py /app/supervise_server.py COPY bot_bottle/supervise_server.py /app/supervise_server.py
COPY bot_bottle/sidecar_init.py /app/sidecar_init.py COPY bot_bottle/sidecar_init.py /app/sidecar_init.py
+120
View File
@@ -0,0 +1,120 @@
"""SQLite-backed audit store for supervise (PRD 0013)."""
from __future__ import annotations
import sqlite3
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .supervise import AuditEntry
def _sv() -> object:
"""Lazy import of supervise to avoid a circular-import at module init time.
Mirrors our own module identity so patches on supervise.bot_bottle_root
propagate correctly in both flat (sidecar / sys.path-injection tests) and
package contexts."""
import sys
sv_name = "supervise" if __name__ == "audit_store" else "bot_bottle.supervise"
if sv_name in sys.modules:
return sys.modules[sv_name]
try:
import bot_bottle.supervise as _m
except ImportError:
import supervise as _m # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
return _m
def _audit_entry_from_row(row: sqlite3.Row) -> AuditEntry:
m = _sv()
return m.AuditEntry( # type: ignore[attr-defined]
timestamp=row["timestamp"],
bottle_slug=row["bottle_slug"],
component=row["component"],
operator_action=row["operator_action"],
operator_notes=row["operator_notes"],
justification=row["justification"],
diff=row["diff"],
)
def _host_db_path() -> Path:
return _sv().host_db_path() # type: ignore[attr-defined,no-any-return]
class AuditStore:
"""SQLite-backed persistent store for supervise audit entries."""
def __init__(self, db_path: Path | None = None) -> None:
self.db_path = db_path or _host_db_path()
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init()
def write_audit_entry(self, entry: AuditEntry) -> Path:
with self._connect() as conn:
conn.execute(
"""
INSERT INTO supervise_audit_entries (
timestamp, bottle_slug, component, operator_action,
operator_notes, justification, diff
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
entry.timestamp,
entry.bottle_slug,
entry.component,
entry.operator_action,
entry.operator_notes,
entry.justification,
entry.diff,
),
)
self._chmod()
return self.db_path
def read_audit_entries(self, component: str, slug: str) -> list[AuditEntry]:
if not self.db_path.is_file():
return []
with self._connect() as conn:
rows = conn.execute(
"""
SELECT * FROM supervise_audit_entries
WHERE component = ? AND bottle_slug = ?
ORDER BY id
""",
(component, slug),
).fetchall()
return [_audit_entry_from_row(row) for row in rows]
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _init(self) -> None:
with self._connect() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS supervise_audit_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
bottle_slug TEXT NOT NULL,
component TEXT NOT NULL,
operator_action TEXT NOT NULL,
operator_notes TEXT NOT NULL,
justification TEXT NOT NULL,
diff TEXT NOT NULL
)
"""
)
self._chmod()
def _chmod(self) -> None:
try:
self.db_path.chmod(0o600)
except OSError:
pass
__all__ = ["AuditStore"]
+248
View File
@@ -0,0 +1,248 @@
"""SQLite-backed queue store for supervise proposals and responses (PRD 0013)."""
from __future__ import annotations
import os
import sqlite3
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .supervise import Proposal, Response
def _sv() -> object:
"""Lazy import of supervise to avoid a circular-import at module init time.
By the time any QueueStore method is called, both modules are fully loaded.
Mirrors our own module identity: when we are 'queue_store' (sidecar flat
context or tests that inject bot_bottle/ into sys.path) we use the flat
'supervise' module so that patches on supervise.bot_bottle_root propagate
correctly. When we are 'bot_bottle.queue_store' we use 'bot_bottle.supervise'."""
import sys
sv_name = "supervise" if __name__ == "queue_store" else "bot_bottle.supervise"
if sv_name in sys.modules:
return sys.modules[sv_name]
try:
import bot_bottle.supervise as _m
except ImportError:
import supervise as _m # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
return _m
def _proposal_from_row(row: sqlite3.Row) -> Proposal:
m = _sv()
return m.Proposal( # type: ignore[attr-defined]
id=row["id"],
bottle_slug=row["bottle_slug"],
tool=row["tool"],
proposed_file=row["proposed_file"],
justification=row["justification"],
arrival_timestamp=row["arrival_timestamp"],
current_file_hash=row["current_file_hash"],
)
def _response_from_row(row: sqlite3.Row) -> Response:
m = _sv()
return m.Response( # type: ignore[attr-defined]
proposal_id=row["proposal_id"],
status=row["status"],
notes=row["notes"],
final_file=row["final_file"],
)
def _host_db_path() -> Path:
return _sv().host_db_path() # type: ignore[attr-defined,no-any-return]
class QueueStore:
"""SQLite-backed persistent store for supervise proposals and responses."""
def __init__(self, queue_key: str, db_path: Path | None = None) -> None:
self.queue_key = queue_key
if db_path is not None:
self.db_path = db_path
else:
# In the sidecar container SUPERVISE_DB_PATH points at the
# bind-mounted host DB. On the host this env var is never set,
# so we always fall through to host_db_path().
env_path = os.environ.get("SUPERVISE_DB_PATH", "").strip()
self.db_path = Path(env_path) if env_path else _host_db_path()
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init()
def write_proposal(self, proposal: Proposal) -> Path:
with self._connect() as conn:
conn.execute(
"""
INSERT OR REPLACE INTO supervise_proposals (
queue_key, id, bottle_slug, tool, proposed_file, justification,
arrival_timestamp, current_file_hash, archived
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
""",
(
self.queue_key,
proposal.id,
proposal.bottle_slug,
proposal.tool,
proposal.proposed_file,
proposal.justification,
proposal.arrival_timestamp,
proposal.current_file_hash,
),
)
self._chmod()
return self.db_path
def read_proposal(self, proposal_id: str) -> Proposal:
with self._connect() as conn:
row = conn.execute(
"""
SELECT * FROM supervise_proposals
WHERE queue_key = ? AND id = ? AND archived = 0
""",
(self.queue_key, proposal_id),
).fetchone()
if row is None:
raise FileNotFoundError(proposal_id)
return _proposal_from_row(row)
def list_pending_proposals(self) -> list[Proposal]:
if not self.db_path.is_file():
return []
with self._connect() as conn:
rows = conn.execute(
"""
SELECT p.* FROM supervise_proposals p
WHERE p.archived = 0
AND p.queue_key = ?
AND NOT EXISTS (
SELECT 1 FROM supervise_responses r
WHERE r.queue_key = p.queue_key
AND r.proposal_id = p.id
AND r.archived = 0
)
ORDER BY p.arrival_timestamp, p.id
""",
(self.queue_key,),
).fetchall()
return [_proposal_from_row(row) for row in rows]
def list_all_pending_proposals(self) -> list[Proposal]:
if not self.db_path.is_file():
return []
with self._connect() as conn:
rows = conn.execute(
"""
SELECT p.* FROM supervise_proposals p
WHERE p.archived = 0
AND NOT EXISTS (
SELECT 1 FROM supervise_responses r
WHERE r.queue_key = p.queue_key
AND r.proposal_id = p.id
AND r.archived = 0
)
ORDER BY p.arrival_timestamp, p.id
"""
).fetchall()
return [_proposal_from_row(row) for row in rows]
def write_response(self, response: Response) -> Path:
with self._connect() as conn:
conn.execute(
"""
INSERT OR REPLACE INTO supervise_responses (
queue_key, proposal_id, status, notes, final_file, archived
) VALUES (?, ?, ?, ?, ?, 0)
""",
(
self.queue_key,
response.proposal_id,
response.status,
response.notes,
response.final_file,
),
)
self._chmod()
return self.db_path
def read_response(self, proposal_id: str) -> Response:
with self._connect() as conn:
row = conn.execute(
"""
SELECT * FROM supervise_responses
WHERE queue_key = ? AND proposal_id = ? AND archived = 0
""",
(self.queue_key, proposal_id),
).fetchone()
if row is None:
raise FileNotFoundError(proposal_id)
return _response_from_row(row)
def archive_proposal(self, proposal_id: str) -> None:
if not self.db_path.is_file():
return
with self._connect() as conn:
conn.execute(
"""
UPDATE supervise_proposals SET archived = 1
WHERE queue_key = ? AND id = ?
""",
(self.queue_key, proposal_id),
)
conn.execute(
"""
UPDATE supervise_responses SET archived = 1
WHERE queue_key = ? AND proposal_id = ?
""",
(self.queue_key, proposal_id),
)
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _init(self) -> None:
with self._connect() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS supervise_proposals (
queue_key TEXT NOT NULL,
id TEXT NOT NULL,
bottle_slug TEXT NOT NULL,
tool TEXT NOT NULL,
proposed_file TEXT NOT NULL,
justification TEXT NOT NULL,
arrival_timestamp TEXT NOT NULL,
current_file_hash TEXT NOT NULL,
archived INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (queue_key, id)
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS supervise_responses (
queue_key TEXT NOT NULL,
proposal_id TEXT NOT NULL,
status TEXT NOT NULL,
notes TEXT NOT NULL,
final_file TEXT,
archived INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (queue_key, proposal_id)
)
"""
)
self._chmod()
def _chmod(self) -> None:
try:
self.db_path.chmod(0o600)
except OSError:
pass
__all__ = ["QueueStore"]
+23 -307
View File
@@ -33,8 +33,6 @@ from __future__ import annotations
import dataclasses import dataclasses
import difflib import difflib
import hashlib import hashlib
import os
import sqlite3
import time import time
import uuid import uuid
from abc import ABC from abc import ABC
@@ -109,11 +107,6 @@ def host_db_path() -> Path:
return bot_bottle_root() / HOST_DB_FILENAME return bot_bottle_root() / HOST_DB_FILENAME
def queue_db_path() -> Path:
env_path = os.environ.get("SUPERVISE_DB_PATH", "").strip()
return Path(env_path) if env_path else host_db_path()
# --- Dataclasses ----------------------------------------------------------- # --- Dataclasses -----------------------------------------------------------
@@ -226,37 +219,46 @@ class AuditEntry:
return dataclasses.asdict(self) return dataclasses.asdict(self)
try:
from .queue_store import QueueStore
from .audit_store import AuditStore
except ImportError:
# Sidecar bundle: files are flat-copied under /app, not a package.
from queue_store import QueueStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from audit_store import AuditStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
# --- Queue I/O ------------------------------------------------------------- # --- Queue I/O -------------------------------------------------------------
def write_proposal(proposal: Proposal) -> Path: def write_proposal(proposal: Proposal) -> Path:
"""Persist `proposal` in the queue database, mode 0o600. """Persist `proposal` in the queue database, mode 0o600.
Directory is created if missing.""" Directory is created if missing."""
return _QueueStore(proposal.bottle_slug).write_proposal(proposal) return QueueStore(proposal.bottle_slug).write_proposal(proposal)
def read_proposal(bottle_slug: str, proposal_id: str) -> Proposal: def read_proposal(bottle_slug: str, proposal_id: str) -> Proposal:
return _QueueStore(bottle_slug).read_proposal(proposal_id) return QueueStore(bottle_slug).read_proposal(proposal_id)
def list_pending_proposals(bottle_slug: str) -> list[Proposal]: def list_pending_proposals(bottle_slug: str) -> list[Proposal]:
"""All proposals for `bottle_slug` that do not yet have a matching """All proposals for `bottle_slug` that do not yet have a matching
response. Sorted by `arrival_timestamp` so the operator response. Sorted by `arrival_timestamp` so the operator
sees the queue FIFO.""" sees the queue FIFO."""
return _QueueStore(bottle_slug).list_pending_proposals() return QueueStore(bottle_slug).list_pending_proposals()
def list_all_pending_proposals() -> list[Proposal]: def list_all_pending_proposals() -> list[Proposal]:
"""All pending proposals across bottles, sorted FIFO.""" """All pending proposals across bottles, sorted FIFO."""
return _QueueStore("").list_all_pending_proposals() return QueueStore("").list_all_pending_proposals()
def write_response(bottle_slug: str, response: Response) -> Path: def write_response(bottle_slug: str, response: Response) -> Path:
return _QueueStore(bottle_slug).write_response(response) return QueueStore(bottle_slug).write_response(response)
def read_response(bottle_slug: str, proposal_id: str) -> Response: def read_response(bottle_slug: str, proposal_id: str) -> Response:
return _QueueStore(bottle_slug).read_response(proposal_id) return QueueStore(bottle_slug).read_response(proposal_id)
def wait_for_response( def wait_for_response(
@@ -272,7 +274,7 @@ def wait_for_response(
natural shape, since the operator's response time is unbounded. natural shape, since the operator's response time is unbounded.
Polls SQLite so the implementation stays portable and stdlib-only.""" Polls SQLite so the implementation stays portable and stdlib-only."""
store = _QueueStore(bottle_slug) store = QueueStore(bottle_slug)
while True: while True:
try: try:
return store.read_response(proposal_id) return store.read_response(proposal_id)
@@ -286,7 +288,7 @@ def wait_for_response(
def archive_proposal(bottle_slug: str, proposal_id: str) -> None: def archive_proposal(bottle_slug: str, proposal_id: str) -> None:
"""Mark both proposal and response rows processed. """Mark both proposal and response rows processed.
Idempotent — missing rows are silently skipped.""" Idempotent — missing rows are silently skipped."""
_QueueStore(bottle_slug).archive_proposal(proposal_id) QueueStore(bottle_slug).archive_proposal(proposal_id)
# --- Audit log ------------------------------------------------------------- # --- Audit log -------------------------------------------------------------
@@ -294,12 +296,12 @@ def archive_proposal(bottle_slug: str, proposal_id: str) -> None:
def write_audit_entry(entry: AuditEntry) -> Path: def write_audit_entry(entry: AuditEntry) -> Path:
"""Append `entry` to the host supervise audit table.""" """Append `entry` to the host supervise audit table."""
return _AuditStore().write_audit_entry(entry) return AuditStore().write_audit_entry(entry)
def read_audit_entries(component: str, slug: str) -> list[AuditEntry]: def read_audit_entries(component: str, slug: str) -> list[AuditEntry]:
"""Load all audit entries for the given component+slug.""" """Load all audit entries for the given component+slug."""
return _AuditStore().read_audit_entries(component, slug) return AuditStore().read_audit_entries(component, slug)
# --- Diff rendering -------------------------------------------------------- # --- Diff rendering --------------------------------------------------------
@@ -325,293 +327,6 @@ def sha256_hex(content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest() return hashlib.sha256(content.encode("utf-8")).hexdigest()
# --- SQLite storage --------------------------------------------------------
class _QueueStore:
def __init__(self, queue_key: str) -> None:
self.queue_key = queue_key
self.db_path = queue_db_path()
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init()
def write_proposal(self, proposal: Proposal) -> Path:
with self._connect() as conn:
conn.execute(
"""
INSERT OR REPLACE INTO supervise_proposals (
queue_key, id, bottle_slug, tool, proposed_file, justification,
arrival_timestamp, current_file_hash, archived
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
""",
(
self.queue_key,
proposal.id,
proposal.bottle_slug,
proposal.tool,
proposal.proposed_file,
proposal.justification,
proposal.arrival_timestamp,
proposal.current_file_hash,
),
)
self._chmod()
return self.db_path
def read_proposal(self, proposal_id: str) -> Proposal:
with self._connect() as conn:
row = conn.execute(
"""
SELECT * FROM supervise_proposals
WHERE queue_key = ? AND id = ? AND archived = 0
""",
(self.queue_key, proposal_id),
).fetchone()
if row is None:
raise FileNotFoundError(proposal_id)
return _proposal_from_row(row)
def list_pending_proposals(self) -> list[Proposal]:
if not self.db_path.is_file():
return []
with self._connect() as conn:
rows = conn.execute(
"""
SELECT p.* FROM supervise_proposals p
WHERE p.archived = 0
AND p.queue_key = ?
AND NOT EXISTS (
SELECT 1 FROM supervise_responses r
WHERE r.queue_key = p.queue_key
AND r.proposal_id = p.id
AND r.archived = 0
)
ORDER BY p.arrival_timestamp, p.id
""",
(self.queue_key,),
).fetchall()
return [_proposal_from_row(row) for row in rows]
def list_all_pending_proposals(self) -> list[Proposal]:
if not self.db_path.is_file():
return []
with self._connect() as conn:
rows = conn.execute(
"""
SELECT p.* FROM supervise_proposals p
WHERE p.archived = 0
AND NOT EXISTS (
SELECT 1 FROM supervise_responses r
WHERE r.queue_key = p.queue_key
AND r.proposal_id = p.id
AND r.archived = 0
)
ORDER BY p.arrival_timestamp, p.id
"""
).fetchall()
return [_proposal_from_row(row) for row in rows]
def write_response(self, response: Response) -> Path:
with self._connect() as conn:
conn.execute(
"""
INSERT OR REPLACE INTO supervise_responses (
queue_key, proposal_id, status, notes, final_file, archived
) VALUES (?, ?, ?, ?, ?, 0)
""",
(
self.queue_key,
response.proposal_id,
response.status,
response.notes,
response.final_file,
),
)
self._chmod()
return self.db_path
def read_response(self, proposal_id: str) -> Response:
with self._connect() as conn:
row = conn.execute(
"""
SELECT * FROM supervise_responses
WHERE queue_key = ? AND proposal_id = ? AND archived = 0
""",
(self.queue_key, proposal_id),
).fetchone()
if row is None:
raise FileNotFoundError(proposal_id)
return _response_from_row(row)
def archive_proposal(self, proposal_id: str) -> None:
if not self.db_path.is_file():
return
with self._connect() as conn:
conn.execute(
"""
UPDATE supervise_proposals SET archived = 1
WHERE queue_key = ? AND id = ?
""",
(self.queue_key, proposal_id),
)
conn.execute(
"""
UPDATE supervise_responses SET archived = 1
WHERE queue_key = ? AND proposal_id = ?
""",
(self.queue_key, proposal_id),
)
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _init(self) -> None:
with self._connect() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS supervise_proposals (
queue_key TEXT NOT NULL,
id TEXT NOT NULL,
bottle_slug TEXT NOT NULL,
tool TEXT NOT NULL,
proposed_file TEXT NOT NULL,
justification TEXT NOT NULL,
arrival_timestamp TEXT NOT NULL,
current_file_hash TEXT NOT NULL,
archived INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (queue_key, id)
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS supervise_responses (
queue_key TEXT NOT NULL,
proposal_id TEXT NOT NULL,
status TEXT NOT NULL,
notes TEXT NOT NULL,
final_file TEXT,
archived INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (queue_key, proposal_id)
)
"""
)
self._chmod()
def _chmod(self) -> None:
try:
self.db_path.chmod(0o600)
except OSError:
pass
class _AuditStore:
def __init__(self, db_path: Path | None = None) -> None:
self.db_path = db_path or host_db_path()
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init()
def write_audit_entry(self, entry: AuditEntry) -> Path:
with self._connect() as conn:
conn.execute(
"""
INSERT INTO supervise_audit_entries (
timestamp, bottle_slug, component, operator_action,
operator_notes, justification, diff
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
entry.timestamp,
entry.bottle_slug,
entry.component,
entry.operator_action,
entry.operator_notes,
entry.justification,
entry.diff,
),
)
self._chmod()
return self.db_path
def read_audit_entries(self, component: str, slug: str) -> list[AuditEntry]:
if not self.db_path.is_file():
return []
with self._connect() as conn:
rows = conn.execute(
"""
SELECT * FROM supervise_audit_entries
WHERE component = ? AND bottle_slug = ?
ORDER BY id
""",
(component, slug),
).fetchall()
return [_audit_entry_from_row(row) for row in rows]
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _init(self) -> None:
with self._connect() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS supervise_audit_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
bottle_slug TEXT NOT NULL,
component TEXT NOT NULL,
operator_action TEXT NOT NULL,
operator_notes TEXT NOT NULL,
justification TEXT NOT NULL,
diff TEXT NOT NULL
)
"""
)
self._chmod()
def _chmod(self) -> None:
try:
self.db_path.chmod(0o600)
except OSError:
pass
def _proposal_from_row(row: sqlite3.Row) -> Proposal:
return Proposal(
id=row["id"],
bottle_slug=row["bottle_slug"],
tool=row["tool"],
proposed_file=row["proposed_file"],
justification=row["justification"],
arrival_timestamp=row["arrival_timestamp"],
current_file_hash=row["current_file_hash"],
)
def _response_from_row(row: sqlite3.Row) -> Response:
return Response(
proposal_id=row["proposal_id"],
status=row["status"],
notes=row["notes"],
final_file=row["final_file"],
)
def _audit_entry_from_row(row: sqlite3.Row) -> AuditEntry:
return AuditEntry(
timestamp=row["timestamp"],
bottle_slug=row["bottle_slug"],
component=row["component"],
operator_action=row["operator_action"],
operator_notes=row["operator_notes"],
justification=row["justification"],
diff=row["diff"],
)
# --- Sidecar plan + abstract lifecycle ------------------------------------- # --- Sidecar plan + abstract lifecycle -------------------------------------
@@ -642,8 +357,8 @@ class Supervise(ABC):
must be set by the launch step before .start runs.""" must be set by the launch step before .start runs."""
del stage_dir del stage_dir
db_path = host_db_path() db_path = host_db_path()
_QueueStore(slug) QueueStore(slug)
_AuditStore(db_path) AuditStore(db_path)
return SupervisePlan( return SupervisePlan(
slug=slug, slug=slug,
db_path=db_path, db_path=db_path,
@@ -662,10 +377,12 @@ def _require_str(raw: dict[str, object], key: str) -> str:
__all__ = [ __all__ = [
"ACTION_OPERATOR_EDIT", "ACTION_OPERATOR_EDIT",
"AuditEntry", "AuditEntry",
"AuditStore",
"COMPONENT_FOR_TOOL", "COMPONENT_FOR_TOOL",
"DEFAULT_POLL_INTERVAL_SEC", "DEFAULT_POLL_INTERVAL_SEC",
"DB_PATH_IN_CONTAINER", "DB_PATH_IN_CONTAINER",
"Proposal", "Proposal",
"QueueStore",
"Response", "Response",
"STATUSES", "STATUSES",
"STATUS_APPROVED", "STATUS_APPROVED",
@@ -690,7 +407,6 @@ __all__ = [
"host_db_path", "host_db_path",
"list_pending_proposals", "list_pending_proposals",
"list_all_pending_proposals", "list_all_pending_proposals",
"queue_db_path",
"read_audit_entries", "read_audit_entries",
"read_proposal", "read_proposal",
"read_response", "read_response",
+1 -2
View File
@@ -20,7 +20,6 @@ from bot_bottle.supervise import (
archive_proposal, archive_proposal,
host_db_path, host_db_path,
list_pending_proposals, list_pending_proposals,
queue_db_path,
read_audit_entries, read_audit_entries,
read_proposal, read_proposal,
read_response, read_response,
@@ -132,7 +131,7 @@ class TestQueueIO(unittest.TestCase):
p = _proposal() p = _proposal()
path = write_proposal(p) path = write_proposal(p)
self.assertTrue(path.exists()) self.assertTrue(path.exists())
self.assertEqual(queue_db_path(), path) self.assertEqual(host_db_path(), path)
self.assertEqual(0o600, path.stat().st_mode & 0o777) self.assertEqual(0o600, path.stat().st_mode & 0o777)
loaded = read_proposal(self.slug, p.id) loaded = read_proposal(self.slug, p.id)
self.assertEqual(p, loaded) self.assertEqual(p, loaded)
-3
View File
@@ -38,9 +38,6 @@ class TestPathHelpers(unittest.TestCase):
def test_bot_bottle_root(self) -> None: def test_bot_bottle_root(self) -> None:
self.assertTrue(str(supervise.bot_bottle_root()).endswith(".bot-bottle")) self.assertTrue(str(supervise.bot_bottle_root()).endswith(".bot-bottle"))
def test_queue_db_path_is_host_db_path(self) -> None:
self.assertEqual(supervise.host_db_path(), supervise.queue_db_path())
class TestReadMalformed(unittest.TestCase): class TestReadMalformed(unittest.TestCase):
def test_read_proposal_missing_row(self) -> None: def test_read_proposal_missing_row(self) -> None: