agent
This commit is contained in:
2
agent/app/storage/__init__.py
Normal file
2
agent/app/storage/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""SQLite storage layer."""
|
||||
|
||||
102
agent/app/storage/database.py
Normal file
102
agent/app/storage/database.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
SCHEMA = """
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
app_id TEXT,
|
||||
app_name TEXT,
|
||||
status TEXT NOT NULL,
|
||||
progress INTEGER DEFAULT 0,
|
||||
current_step TEXT,
|
||||
current_component_id TEXT,
|
||||
error_message TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
finished_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS installed_apps (
|
||||
app_id TEXT PRIMARY KEY,
|
||||
app_name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
manifest_hash TEXT,
|
||||
status TEXT NOT NULL,
|
||||
installed_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS installed_components (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
app_id TEXT NOT NULL,
|
||||
component_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
install_order INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
package_name TEXT,
|
||||
package_version TEXT,
|
||||
service_name TEXT,
|
||||
docker_image TEXT,
|
||||
docker_digest TEXT,
|
||||
container_name TEXT,
|
||||
compose_project_name TEXT,
|
||||
installed_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(app_id, component_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_components (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
app_id TEXT NOT NULL,
|
||||
component_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
install_order INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
progress INTEGER DEFAULT 0,
|
||||
current_step TEXT,
|
||||
error_message TEXT,
|
||||
started_at TEXT,
|
||||
finished_at TEXT,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def get_connection() -> sqlite3.Connection:
|
||||
connection = sqlite3.connect(settings.db_path, timeout=30)
|
||||
connection.row_factory = sqlite3.Row
|
||||
return connection
|
||||
|
||||
|
||||
def initialize_database() -> None:
|
||||
db_path = Path(settings.db_path)
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
settings.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
settings.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
with get_connection() as connection:
|
||||
connection.executescript(SCHEMA)
|
||||
|
||||
255
agent/app/storage/repository.py
Normal file
255
agent/app/storage/repository.py
Normal file
@@ -0,0 +1,255 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from app.storage.database import get_connection
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
def row_to_dict(row: Any) -> dict[str, Any] | None:
|
||||
if row is None:
|
||||
return None
|
||||
return dict(row)
|
||||
|
||||
|
||||
class Repository:
|
||||
def create_task(self, task_id: str, task_type: str, app_id: str, app_name: str | None) -> None:
|
||||
now = utc_now()
|
||||
with get_connection() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO tasks (id, type, app_id, app_name, status, progress, current_step, created_at)
|
||||
VALUES (?, ?, ?, ?, 'queued', 0, 'queued', ?)
|
||||
""",
|
||||
(task_id, task_type, app_id, app_name, now),
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT INTO task_logs (task_id, timestamp, level, message) VALUES (?, ?, 'info', ?)",
|
||||
(task_id, now, f"Task {task_id} queued"),
|
||||
)
|
||||
|
||||
def update_task(
|
||||
self,
|
||||
task_id: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
progress: int | None = None,
|
||||
current_step: str | None = None,
|
||||
current_component_id: str | None = None,
|
||||
error_message: str | None = None,
|
||||
started_at: str | None = None,
|
||||
finished_at: str | None = None,
|
||||
) -> None:
|
||||
fields: list[str] = []
|
||||
values: list[Any] = []
|
||||
for key, value in {
|
||||
"status": status,
|
||||
"progress": progress,
|
||||
"current_step": current_step,
|
||||
"current_component_id": current_component_id,
|
||||
"error_message": error_message,
|
||||
"started_at": started_at,
|
||||
"finished_at": finished_at,
|
||||
}.items():
|
||||
if value is not None:
|
||||
fields.append(f"{key} = ?")
|
||||
values.append(value)
|
||||
if not fields:
|
||||
return
|
||||
values.append(task_id)
|
||||
with get_connection() as connection:
|
||||
connection.execute(f"UPDATE tasks SET {', '.join(fields)} WHERE id = ?", values)
|
||||
|
||||
def add_log(self, task_id: str, level: str, message: str) -> None:
|
||||
with get_connection() as connection:
|
||||
connection.execute(
|
||||
"INSERT INTO task_logs (task_id, timestamp, level, message) VALUES (?, ?, ?, ?)",
|
||||
(task_id, utc_now(), level, message),
|
||||
)
|
||||
|
||||
def get_task(self, task_id: str) -> dict[str, Any] | None:
|
||||
with get_connection() as connection:
|
||||
row = connection.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
|
||||
return row_to_dict(row)
|
||||
|
||||
def get_task_logs(self, task_id: str) -> list[dict[str, Any]]:
|
||||
with get_connection() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT timestamp, level, message
|
||||
FROM task_logs
|
||||
WHERE task_id = ?
|
||||
ORDER BY id ASC
|
||||
""",
|
||||
(task_id,),
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def create_task_component(
|
||||
self,
|
||||
task_id: str,
|
||||
app_id: str,
|
||||
component_id: str,
|
||||
component_type: str,
|
||||
install_order: int,
|
||||
) -> None:
|
||||
with get_connection() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO task_components (
|
||||
task_id, app_id, component_id, type, install_order, status, progress, current_step
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, 'queued', 0, 'queued')
|
||||
""",
|
||||
(task_id, app_id, component_id, component_type, install_order),
|
||||
)
|
||||
|
||||
def update_task_component(
|
||||
self,
|
||||
task_id: str,
|
||||
component_id: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
progress: int | None = None,
|
||||
current_step: str | None = None,
|
||||
error_message: str | None = None,
|
||||
started_at: str | None = None,
|
||||
finished_at: str | None = None,
|
||||
) -> None:
|
||||
fields: list[str] = []
|
||||
values: list[Any] = []
|
||||
for key, value in {
|
||||
"status": status,
|
||||
"progress": progress,
|
||||
"current_step": current_step,
|
||||
"error_message": error_message,
|
||||
"started_at": started_at,
|
||||
"finished_at": finished_at,
|
||||
}.items():
|
||||
if value is not None:
|
||||
fields.append(f"{key} = ?")
|
||||
values.append(value)
|
||||
if not fields:
|
||||
return
|
||||
values.extend([task_id, component_id])
|
||||
with get_connection() as connection:
|
||||
connection.execute(
|
||||
f"UPDATE task_components SET {', '.join(fields)} WHERE task_id = ? AND component_id = ?",
|
||||
values,
|
||||
)
|
||||
|
||||
def get_task_components(self, task_id: str) -> list[dict[str, Any]]:
|
||||
with get_connection() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT component_id, type, status, progress, current_step, error_message, started_at, finished_at
|
||||
FROM task_components
|
||||
WHERE task_id = ?
|
||||
ORDER BY install_order ASC, id ASC
|
||||
""",
|
||||
(task_id,),
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def list_installed_apps(self) -> list[dict[str, Any]]:
|
||||
with get_connection() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT app_id, app_name, version, manifest_hash, status, installed_at, updated_at
|
||||
FROM installed_apps
|
||||
ORDER BY app_name ASC
|
||||
"""
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def upsert_installed_app(
|
||||
self,
|
||||
app_id: str,
|
||||
app_name: str,
|
||||
version: str,
|
||||
manifest_hash: str | None,
|
||||
status: str = "installed",
|
||||
) -> None:
|
||||
now = utc_now()
|
||||
with get_connection() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO installed_apps (app_id, app_name, version, manifest_hash, status, installed_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(app_id) DO UPDATE SET
|
||||
app_name = excluded.app_name,
|
||||
version = excluded.version,
|
||||
manifest_hash = excluded.manifest_hash,
|
||||
status = excluded.status,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(app_id, app_name, version, manifest_hash, status, now, now),
|
||||
)
|
||||
|
||||
def delete_installed_app(self, app_id: str) -> None:
|
||||
with get_connection() as connection:
|
||||
connection.execute("DELETE FROM installed_components WHERE app_id = ?", (app_id,))
|
||||
connection.execute("DELETE FROM installed_apps WHERE app_id = ?", (app_id,))
|
||||
|
||||
def upsert_installed_component(self, app_id: str, component: dict[str, Any]) -> None:
|
||||
now = utc_now()
|
||||
with get_connection() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO installed_components (
|
||||
app_id, component_id, type, install_order, status, package_name, package_version,
|
||||
service_name, docker_image, docker_digest, container_name, compose_project_name,
|
||||
installed_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 'installed', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(app_id, component_id) DO UPDATE SET
|
||||
type = excluded.type,
|
||||
install_order = excluded.install_order,
|
||||
status = excluded.status,
|
||||
package_name = excluded.package_name,
|
||||
package_version = excluded.package_version,
|
||||
service_name = excluded.service_name,
|
||||
docker_image = excluded.docker_image,
|
||||
docker_digest = excluded.docker_digest,
|
||||
container_name = excluded.container_name,
|
||||
compose_project_name = excluded.compose_project_name,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
app_id,
|
||||
component["componentId"],
|
||||
component["type"],
|
||||
component.get("installOrder", 10),
|
||||
component.get("packageName"),
|
||||
component.get("version"),
|
||||
component.get("serviceName"),
|
||||
component.get("image"),
|
||||
component.get("digest"),
|
||||
component.get("containerName"),
|
||||
component.get("projectName"),
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
|
||||
def list_installed_components(self, app_id: str) -> list[dict[str, Any]]:
|
||||
with get_connection() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM installed_components
|
||||
WHERE app_id = ?
|
||||
ORDER BY install_order ASC, id ASC
|
||||
""",
|
||||
(app_id,),
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def export_manifest_hash(self, manifest: dict[str, Any]) -> str:
|
||||
return json.dumps(manifest, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
Reference in New Issue
Block a user