File size: 6,785 Bytes
ff453a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""SQLite persistence for projects, records, jobs, logs, and artifacts."""

from __future__ import annotations

import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from threading import Lock
from typing import Any
from uuid import uuid4

DB_PATH = Path(__file__).resolve().parent.parent / "data" / "platform.db"
_LOCK = Lock()


def utcnow() -> str:
    return datetime.now(timezone.utc).isoformat()


def init_db(path: Path | None = None) -> None:
    path = path or DB_PATH
    path.parent.mkdir(parents=True, exist_ok=True)
    with sqlite3.connect(path) as db:
        db.executescript(
            """
            CREATE TABLE IF NOT EXISTS projects (
                project_id TEXT PRIMARY KEY, name TEXT NOT NULL, cancer_type TEXT NOT NULL,
                metadata TEXT NOT NULL, created_at TEXT NOT NULL
            );
            CREATE TABLE IF NOT EXISTS records (
                record_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, kind TEXT NOT NULL,
                payload TEXT NOT NULL, created_at TEXT NOT NULL,
                FOREIGN KEY(project_id) REFERENCES projects(project_id)
            );
            CREATE INDEX IF NOT EXISTS idx_records_project_kind
                ON records(project_id, kind);
            CREATE TABLE IF NOT EXISTS jobs (
                job_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, operation TEXT NOT NULL,
                status TEXT NOT NULL, input TEXT NOT NULL, output TEXT,
                error TEXT, logs TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
            );
            CREATE TABLE IF NOT EXISTS artifacts (
                artifact_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, job_id TEXT,
                kind TEXT NOT NULL, name TEXT NOT NULL, media_type TEXT NOT NULL,
                payload TEXT NOT NULL, created_at TEXT NOT NULL
            );
            """
        )


def _execute(sql: str, args: tuple = (), *, fetchone=False, fetchall=False):
    with _LOCK, sqlite3.connect(DB_PATH) as db:
        db.row_factory = sqlite3.Row
        cur = db.execute(sql, args)
        db.commit()
        if fetchone:
            row = cur.fetchone()
            return dict(row) if row else None
        if fetchall:
            return [dict(row) for row in cur.fetchall()]
        return cur.lastrowid


def create_project(name: str, cancer_type: str, metadata: dict) -> dict:
    project_id = f"PRJ-{uuid4().hex[:12]}"
    now = utcnow()
    _execute(
        "INSERT INTO projects VALUES (?,?,?,?,?)",
        (project_id, name, cancer_type, json.dumps(metadata), now),
    )
    return get_project(project_id)


def get_project(project_id: str) -> dict | None:
    row = _execute("SELECT * FROM projects WHERE project_id=?", (project_id,), fetchone=True)
    if row:
        row["metadata"] = json.loads(row["metadata"])
    return row


def add_records(project_id: str, kind: str, payloads: list[dict]) -> list[str]:
    ids = []
    id_fields = {
        "variant": "variant_id", "structure": "structure_id", "pocket": "pocket_id",
        "compound": "compound_id", "docking": "docking_id", "md": "md_id",
        "ranking": "ranking_id",
    }
    with _LOCK, sqlite3.connect(DB_PATH) as db:
        for payload in payloads:
            rid = payload.get(id_fields.get(kind, "record_id"))
            rid = rid or f"{kind.upper()}-{uuid4().hex[:12]}"
            db.execute(
                "INSERT OR REPLACE INTO records VALUES (?,?,?,?,?)",
                (rid, project_id, kind, json.dumps(payload, allow_nan=False), utcnow()),
            )
            ids.append(rid)
        db.commit()
    return ids


def get_record(project_id: str, kind: str, record_id: str) -> dict | None:
    row = _execute(
        "SELECT payload FROM records WHERE project_id=? AND kind=? AND record_id=?",
        (project_id, kind, record_id),
        fetchone=True,
    )
    return json.loads(row["payload"]) if row else None


def list_records(project_id: str, kind: str) -> list[dict]:
    rows = _execute(
        "SELECT payload FROM records WHERE project_id=? AND kind=? ORDER BY created_at",
        (project_id, kind),
        fetchall=True,
    )
    return [json.loads(row["payload"]) for row in rows]


def create_job(project_id: str, operation: str, input_data: dict) -> dict:
    job_id = f"JOB-{uuid4().hex[:12]}"
    now = utcnow()
    _execute(
        "INSERT INTO jobs VALUES (?,?,?,?,?,?,?,?,?,?)",
        (job_id, project_id, operation, "queued", json.dumps(input_data), None, None, "[]", now, now),
    )
    return get_job(job_id)


def update_job(job_id: str, status: str, *, output: Any = None, error: str | None = None, log: str | None = None):
    job = get_job(job_id)
    logs = job["logs"] if job else []
    if log:
        logs.append({"at": utcnow(), "message": log})
    _execute(
        "UPDATE jobs SET status=?, output=?, error=?, logs=?, updated_at=? WHERE job_id=?",
        (
            status,
            json.dumps(output, allow_nan=False) if output is not None else None,
            error,
            json.dumps(logs),
            utcnow(),
            job_id,
        ),
    )


def get_job(job_id: str) -> dict | None:
    row = _execute("SELECT * FROM jobs WHERE job_id=?", (job_id,), fetchone=True)
    if not row:
        return None
    row["input"] = json.loads(row["input"])
    row["output"] = json.loads(row["output"]) if row["output"] else None
    row["logs"] = json.loads(row["logs"])
    return row


def list_jobs(project_id: str) -> list[dict]:
    rows = _execute(
        "SELECT * FROM jobs WHERE project_id=? ORDER BY created_at DESC",
        (project_id,),
        fetchall=True,
    )
    for row in rows:
        row["input"] = json.loads(row["input"])
        row["output"] = json.loads(row["output"]) if row["output"] else None
        row["logs"] = json.loads(row["logs"])
    return rows


def add_artifact(project_id: str, kind: str, name: str, payload: Any, media_type: str, job_id: str | None = None) -> dict:
    artifact_id = f"ART-{uuid4().hex[:12]}"
    _execute(
        "INSERT INTO artifacts VALUES (?,?,?,?,?,?,?,?)",
        (artifact_id, project_id, job_id, kind, name, media_type, json.dumps(payload, allow_nan=False), utcnow()),
    )
    return get_artifact(artifact_id)


def get_artifact(artifact_id: str) -> dict | None:
    row = _execute("SELECT * FROM artifacts WHERE artifact_id=?", (artifact_id,), fetchone=True)
    if row:
        row["payload"] = json.loads(row["payload"])
    return row


def list_artifacts(project_id: str) -> list[dict]:
    rows = _execute(
        "SELECT artifact_id,project_id,job_id,kind,name,media_type,created_at FROM artifacts WHERE project_id=?",
        (project_id,),
        fetchall=True,
    )
    return rows