Andrey Epifancev 81850783d4 feat(hub): скелет пакета, схема SQLite, транзакции
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
2026-10-11 21:41:00 +04:00

140 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Соединение с SQLite и схема хаба. Схема создаётся при первом подключении; версия — в setting.schema_version."""
import json
import sqlite3
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
SCHEMA_VERSION = 1
SCHEMA = """
CREATE TABLE IF NOT EXISTS setting (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS issue (
key TEXT PRIMARY KEY,
id TEXT,
issue_type TEXT,
summary TEXT,
description TEXT,
status TEXT,
priority TEXT,
assignee TEXT,
reporter TEXT,
duedate TEXT,
planned_start TEXT,
planned_end TEXT,
epic_key TEXT,
labels TEXT NOT NULL DEFAULT '[]',
sprint TEXT,
created TEXT,
jira_updated TEXT,
fetched_at TEXT NOT NULL,
neighbor INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS issue_link (
source_key TEXT NOT NULL,
target_key TEXT NOT NULL,
link_type TEXT NOT NULL,
PRIMARY KEY (source_key, target_key, link_type)
);
CREATE INDEX IF NOT EXISTS ix_issue_link_target ON issue_link(target_key);
CREATE TABLE IF NOT EXISTS issue_note (
key TEXT PRIMARY KEY,
fact_status TEXT,
work_kind TEXT,
done_text TEXT,
rest_text TEXT,
owner_note TEXT,
updated_at TEXT,
updated_by TEXT
);
CREATE TABLE IF NOT EXISTS batch (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
created_at TEXT NOT NULL,
cancelled_at TEXT,
estimate_seconds INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS command (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_id INTEGER NOT NULL REFERENCES batch(id),
seq INTEGER NOT NULL,
type TEXT NOT NULL,
params TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'queued',
claimed_at TEXT,
finished_at TEXT,
result TEXT,
error TEXT,
jira_calls INTEGER NOT NULL DEFAULT 0,
rate_limited INTEGER NOT NULL DEFAULT 0,
pause_seconds INTEGER,
depends_on INTEGER REFERENCES command(id)
);
CREATE INDEX IF NOT EXISTS ix_command_status ON command(status, batch_id, seq);
CREATE TABLE IF NOT EXISTS sync_run (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TEXT NOT NULL,
finished_at TEXT,
kind TEXT NOT NULL,
query TEXT,
issues_count INTEGER NOT NULL DEFAULT 0,
links_count INTEGER NOT NULL DEFAULT 0,
jira_calls INTEGER NOT NULL DEFAULT 0,
rate_limited INTEGER NOT NULL DEFAULT 0,
command_id INTEGER
);
CREATE TABLE IF NOT EXISTS agent_status (
id INTEGER PRIMARY KEY CHECK (id = 1),
last_seen_at TEXT,
agent_version TEXT,
settings_version INTEGER,
current_command_id INTEGER,
last_jira_call_at TEXT
);
"""
def now() -> str:
"""Текущее время UTC в ISO-8601 с секундами — единый формат дат в базе."""
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def connect(path) -> sqlite3.Connection:
"""Открыть базу (создать при отсутствии), применить схему. Соединение в автокоммите; для атомарных
операций — transaction()."""
is_memory = str(path) == ":memory:"
if not is_memory:
Path(path).parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(path), timeout=10, isolation_level=None, check_same_thread=False)
conn.row_factory = sqlite3.Row
if not is_memory:
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA foreign_keys = ON")
migrate(conn)
return conn
def migrate(conn: sqlite3.Connection) -> None:
conn.executescript(SCHEMA)
row = conn.execute("SELECT value FROM setting WHERE key = 'schema_version'").fetchone()
if row is None:
conn.execute("INSERT INTO setting(key, value) VALUES ('schema_version', ?)", (json.dumps(SCHEMA_VERSION),))
conn.execute("INSERT OR IGNORE INTO agent_status(id) VALUES (1)")
@contextmanager
def transaction(conn: sqlite3.Connection):
"""BEGIN IMMEDIATE … COMMIT; при исключении — ROLLBACK. Нужна там, где читаем и пишем вместе (claim, отчёт)."""
conn.execute("BEGIN IMMEDIATE")
try:
yield conn
except BaseException:
conn.execute("ROLLBACK")
raise
else:
conn.execute("COMMIT")