This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class ConsoleHistoryStore:
|
||||
def __init__(
|
||||
self,
|
||||
db_path: str | Path = "tasks/host_console_history.sqlite3",
|
||||
*,
|
||||
limit: int = 200,
|
||||
now: Callable[[], datetime] | None = None,
|
||||
) -> None:
|
||||
self.db_path = Path(db_path)
|
||||
self.limit = limit
|
||||
self._now = now or (lambda: datetime.now(UTC))
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._ensure_schema()
|
||||
|
||||
def record_assignment(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
attempt: int,
|
||||
status: str,
|
||||
failure_reason: str | None,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
summary = f"{task_id} attempt {attempt} on {device_id}: {status}"
|
||||
if failure_reason:
|
||||
summary = f"{summary} ({failure_reason})"
|
||||
detail = {
|
||||
"task_id": task_id,
|
||||
"attempt": attempt,
|
||||
"status": status,
|
||||
"failure_reason": failure_reason,
|
||||
"device_id": device_id,
|
||||
}
|
||||
self._insert("assignment", summary, detail)
|
||||
|
||||
def record_heartbeat(self, *, device_count: int) -> None:
|
||||
summary = f"heartbeat: {device_count} devices"
|
||||
detail = {"device_count": device_count}
|
||||
self._insert("heartbeat", summary, detail)
|
||||
|
||||
def list_recent(self, limit: int | None = None) -> list[dict[str, Any]]:
|
||||
effective_limit = limit if limit is not None else self.limit
|
||||
with self._connect() as connection:
|
||||
rows = connection.execute(
|
||||
"select kind, occurred_at, summary, detail_json"
|
||||
" from history_entries order by id desc limit ?",
|
||||
(effective_limit,),
|
||||
).fetchall()
|
||||
return [
|
||||
{
|
||||
"kind": row["kind"],
|
||||
"occurred_at": row["occurred_at"],
|
||||
"summary": row["summary"],
|
||||
"detail": json.loads(row["detail_json"]),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def _insert(self, kind: str, summary: str, detail: dict[str, Any]) -> None:
|
||||
occurred_at = self._now().isoformat()
|
||||
with self._connect() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
insert into history_entries (kind, occurred_at, summary, detail_json)
|
||||
values (?, ?, ?, ?)
|
||||
""",
|
||||
(kind, occurred_at, summary, json.dumps(detail, ensure_ascii=False)),
|
||||
)
|
||||
connection.execute(
|
||||
"""
|
||||
delete from history_entries where id not in (
|
||||
select id from history_entries order by id desc limit ?
|
||||
)
|
||||
""",
|
||||
(self.limit,),
|
||||
)
|
||||
|
||||
def _ensure_schema(self) -> None:
|
||||
with self._connect() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
create table if not exists history_entries (
|
||||
id integer primary key autoincrement,
|
||||
kind text not null,
|
||||
occurred_at text not null,
|
||||
summary text not null,
|
||||
detail_json text not null
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
connection = sqlite3.connect(self.db_path)
|
||||
connection.row_factory = sqlite3.Row
|
||||
return connection
|
||||
Reference in New Issue
Block a user