173 lines
5.1 KiB
Python
173 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .config import settings
|
|
|
|
|
|
def utc_now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def connect() -> sqlite3.Connection:
|
|
settings.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(settings.db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def init_db() -> None:
|
|
with connect() as conn:
|
|
conn.execute(
|
|
"""
|
|
create table if not exists jobs (
|
|
id text primary key,
|
|
type text not null,
|
|
status text not null,
|
|
params_json text not null,
|
|
command_json text not null,
|
|
cwd text not null,
|
|
description text not null default '',
|
|
pid integer,
|
|
exit_code integer,
|
|
created_at text not null,
|
|
started_at text,
|
|
finished_at text,
|
|
log_path text not null,
|
|
error text
|
|
)
|
|
"""
|
|
)
|
|
conn.execute(
|
|
"""
|
|
create table if not exists profiles (
|
|
id integer primary key autoincrement,
|
|
name text not null,
|
|
kind text not null,
|
|
data_json text not null,
|
|
updated_at text not null,
|
|
unique(name, kind)
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def _job_from_row(row: sqlite3.Row) -> dict[str, Any]:
|
|
data = dict(row)
|
|
data["params"] = json.loads(data.pop("params_json"))
|
|
data["command"] = json.loads(data.pop("command_json"))
|
|
return data
|
|
|
|
|
|
def insert_job(job: dict[str, Any]) -> None:
|
|
with connect() as conn:
|
|
conn.execute(
|
|
"""
|
|
insert into jobs (
|
|
id, type, status, params_json, command_json, cwd, description,
|
|
pid, exit_code, created_at, started_at, finished_at, log_path, error
|
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
job["id"],
|
|
job["type"],
|
|
job["status"],
|
|
json.dumps(job["params"], ensure_ascii=False),
|
|
json.dumps(job["command"], ensure_ascii=False),
|
|
job["cwd"],
|
|
job.get("description", ""),
|
|
job.get("pid"),
|
|
job.get("exit_code"),
|
|
job["created_at"],
|
|
job.get("started_at"),
|
|
job.get("finished_at"),
|
|
job["log_path"],
|
|
job.get("error"),
|
|
),
|
|
)
|
|
|
|
|
|
def update_job(job_id: str, **fields: Any) -> None:
|
|
if not fields:
|
|
return
|
|
allowed = {
|
|
"status",
|
|
"pid",
|
|
"exit_code",
|
|
"started_at",
|
|
"finished_at",
|
|
"error",
|
|
}
|
|
updates = {key: value for key, value in fields.items() if key in allowed}
|
|
if not updates:
|
|
return
|
|
assignments = ", ".join(f"{key}=?" for key in updates)
|
|
with connect() as conn:
|
|
conn.execute(f"update jobs set {assignments} where id=?", [*updates.values(), job_id])
|
|
|
|
|
|
def get_job(job_id: str) -> dict[str, Any] | None:
|
|
with connect() as conn:
|
|
row = conn.execute("select * from jobs where id=?", (job_id,)).fetchone()
|
|
return _job_from_row(row) if row else None
|
|
|
|
|
|
def list_jobs(limit: int = 100) -> list[dict[str, Any]]:
|
|
with connect() as conn:
|
|
rows = conn.execute(
|
|
"select * from jobs order by created_at desc limit ?", (limit,)
|
|
).fetchall()
|
|
return [_job_from_row(row) for row in rows]
|
|
|
|
|
|
def upsert_profile(name: str, kind: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
updated_at = utc_now()
|
|
with connect() as conn:
|
|
conn.execute(
|
|
"""
|
|
insert into profiles (name, kind, data_json, updated_at)
|
|
values (?, ?, ?, ?)
|
|
on conflict(name, kind) do update set
|
|
data_json=excluded.data_json,
|
|
updated_at=excluded.updated_at
|
|
""",
|
|
(name, kind, json.dumps(data, ensure_ascii=False), updated_at),
|
|
)
|
|
return {"name": name, "kind": kind, "data": data, "updated_at": updated_at}
|
|
|
|
|
|
def list_profiles(kind: str | None = None) -> list[dict[str, Any]]:
|
|
sql = "select name, kind, data_json, updated_at from profiles"
|
|
params: tuple = ()
|
|
if kind:
|
|
sql += " where kind=?"
|
|
params = (kind,)
|
|
sql += " order by kind, name"
|
|
with connect() as conn:
|
|
rows = conn.execute(sql, params).fetchall()
|
|
return [
|
|
{
|
|
"name": row["name"],
|
|
"kind": row["kind"],
|
|
"data": json.loads(row["data_json"]),
|
|
"updated_at": row["updated_at"],
|
|
}
|
|
for row in rows
|
|
]
|
|
|
|
|
|
def log_tail(log_path: str | Path, max_bytes: int = 8192) -> str:
|
|
path = Path(log_path)
|
|
if not path.exists():
|
|
return ""
|
|
size = path.stat().st_size
|
|
with path.open("rb") as handle:
|
|
if size > max_bytes:
|
|
handle.seek(size - max_bytes)
|
|
return handle.read().decode("utf-8", errors="replace")
|
|
|