Initial Seg Data Server Net platform
This commit is contained in:
172
backend/app/db.py
Normal file
172
backend/app/db.py
Normal file
@@ -0,0 +1,172 @@
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user