Andrey Epifancev c9688c036b Поля проекта от модулей, доски — в модуль jira (#91)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:24:33 +04:00

142 lines
6.7 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.

"""Проекты-метки в модуле jira: базовая JQL из меток, доски, миграция v3 → v4, проект по умолчанию."""
import logging
import re
from ...core import db
from ...core import projects as core_projects
from ...core import settings
from ...core.projects import ProjectError, label_error, labels
from . import project_fields
log = logging.getLogger(__name__)
LEGACY_LABEL = "AWPCA"
LEGACY_NAME = "АРМ КА"
BOARD_TYPES = ("scrum", "kanban")
def base_jql(conn) -> str | None:
"""Общая JQL обновления: все метки проектов; без проектов — None."""
found = labels(conn)
if not found:
return None
key = settings.get(conn, "project_key") or "AURORA"
return f"project = {key} AND labels in ({', '.join(f'"{x}"' for x in found)})"
def create(conn, label: str, name: str, board_id: int | None = None) -> dict:
"""Проект с доской: проект ядра и строка jira_project (прямой путь без POST /projects: init, миграция, тесты).
Неверная доска — ProjectError("board_id", …) до записи."""
try:
board_id = project_fields.validate_board(board_id)
except ValueError as e:
raise ProjectError("board_id", str(e))
with db.transaction(conn):
project = core_projects.create(conn, label, name)
conn.execute("INSERT INTO jira_project(label, board_id) VALUES (?, ?)", (project["label"], board_id))
return project
def board_of(conn, label: str) -> int | None:
row = conn.execute("SELECT board_id FROM jira_project WHERE label = ?", (label,)).fetchone()
return None if row is None else row["board_id"]
def light_list(conn) -> list[dict]:
"""Проекты с доской (label, board_id, board_type) в порядке списка проектов."""
return [dict(r) for r in conn.execute(
"SELECT p.label, jp.board_id, jp.board_type FROM project p LEFT JOIN jira_project jp ON jp.label = p.label "
"ORDER BY p.created_at, p.label")]
def migrate_v6(conn) -> None:
"""Шаг миграции 6 → 7: board_id и board_type уходят из project в jira_project. База, собранная из схемы v7
(колонок уже нет), не трогается."""
cols = {r["name"] for r in conn.execute("PRAGMA table_info(project)")}
if "board_id" not in cols:
return
conn.execute("INSERT OR IGNORE INTO jira_project(label, board_id, board_type) "
"SELECT label, board_id, board_type FROM project")
conn.execute("ALTER TABLE project DROP COLUMN board_id")
conn.execute("ALTER TABLE project DROP COLUMN board_type")
def boards(conn) -> list[dict]:
"""Доски проектов без повторов (порядок первого проекта). type — None, если тип неизвестен хотя бы у одного
проекта доски (агент тогда запросит его, record_board_types проставит всем); иначе первый известный."""
out: dict[int, str | None] = {}
unknown: set[int] = set()
for r in conn.execute("SELECT jp.board_id, jp.board_type FROM jira_project jp JOIN project p ON p.label = jp.label "
"WHERE jp.board_id IS NOT NULL ORDER BY p.created_at, p.label"):
out.setdefault(r["board_id"], r["board_type"])
if r["board_type"] is None:
unknown.add(r["board_id"])
return [{"id": i, "type": None if i in unknown else t} for i, t in out.items()]
def record_board_types(conn, found) -> int:
"""Записать типы досок {"<id>": "scrum" | "kanban"} всем проектам с этой доской; прочее пропустить."""
if not isinstance(found, dict):
return 0
updated = 0
for raw, kind in found.items():
if kind not in BOARD_TYPES:
continue
try:
board = int(raw)
except (TypeError, ValueError):
continue
updated += conn.execute("UPDATE jira_project SET board_type = ? WHERE board_id = ?", (kind, board)).rowcount
return updated
_LEGACY = re.compile(r"\blabels\s*(?:=\s*(?P<one>\S+)|in\s*\((?P<many>[^()]*)\))\s*$", re.IGNORECASE)
def parse_legacy_labels(jql) -> list[str] | None:
"""Метки из прежней base_jql (`labels = X` / `labels in (X, Y)` в конце); не разобрана — None."""
m = _LEGACY.search(jql) if isinstance(jql, str) else None
if not m:
return None
out: list[str] = []
for raw in ([m["one"]] if m["one"] is not None else m["many"].split(",")):
label = raw.strip().strip('"')
if label_error(label):
return None
if label.casefold() not in {x.casefold() for x in out}:
out.append(label)
return out or None
def ensure_legacy(conn, board_id: int | None) -> bool:
"""Завести AWPCA «АРМ КА» с доской, если проекта с меткой awpca (без учёта регистра) нет; True — заведён.
Неверная доска из конфига — проект без доски."""
if LEGACY_LABEL.casefold() in {x.casefold() for x in labels(conn)}:
return False
try:
project_fields.validate_board(board_id)
except ValueError:
board_id = None
create(conn, LEGACY_LABEL, LEGACY_NAME, board_id)
return True
def migrate_v3(conn) -> None:
"""Миграция v3 → v4: проекты из прежней base_jql, доска — единственному проекту, очередь и журнал — на новую JQL."""
old = settings.get(conn, "base_jql")
found = parse_legacy_labels(old)
if found is None:
log.warning("миграция v4: base_jql не разобрана (%r) — проектов нет, заведите их POST /projects", old)
return
board = settings.get(conn, "board_id")
board = board if isinstance(board, int) and not isinstance(board, bool) and board > 0 else None
if board is not None and len(found) > 1:
log.warning("миграция v4: доска %s не отдана ни одному проекту — меток несколько: %s", board, ", ".join(found))
board = None
for label in found:
create(conn, label, LEGACY_NAME if label == LEGACY_LABEL else label, board)
new = base_jql(conn)
conn.execute("UPDATE sync_run SET query = ? WHERE query = ?", (new, old))
conn.execute("UPDATE command SET params = json_set(params, '$.jql', ?) WHERE type = 'refresh' "
"AND status IN ('queued', 'claimed') AND json_extract(params, '$.jql') = ?", (new, old))