sdlc-hub/src/jira_hub/importer.py
Andrey Epifancev acbc19c7eb fix(hub): импорт не затирает правки хаба пустыми ячейками CSV, весь импорт в транзакции
Пустая ячейка CSV не меняет существующее поле заметки; заметка без непустых полей не пишется,
но учитывается в счётчике как раньше. Импорт обёрнут в db.transaction (set_note — SAVEPOINT).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
2026-10-11 22:29:08 +04:00

89 lines
3.8 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.

"""Разовый импорт CSV-пакета admin/jira-sync: служебные колонки → issue_note; links.csv — сверочный набор
(ожидаемые связи против зеркала), не команды. Повтор обновляет заметки, не дублирует.
Пустая ячейка CSV не меняет существующее поле заметки (правки, сделанные в хабе, сохраняются)."""
import csv
from pathlib import Path
from . import db, mirror, settings
def _rows(path: Path) -> list[dict]:
if not path.exists():
return []
with open(path, encoding="utf-8-sig", newline="") as f:
return list(csv.DictReader(f))
def _clean(value) -> str | None:
value = (value or "").strip()
return value or None
def _note(conn, key, fields: dict) -> None:
"""Записать только непустые поля; если их нет — заметку не трогаем."""
fields = {k: v for k, v in fields.items() if v is not None}
if fields:
mirror.set_note(conn, key, fields, "import")
def import_package(conn, folder) -> dict:
folder = Path(folder)
with db.transaction(conn):
return _import(conn, folder)
def _import(conn, folder: Path) -> dict:
notes = 0
csv_keys = []
for r in _rows(folder / "tasks.csv"):
key = _clean(r.get("Код"))
if not key:
continue
_note(conn, key, {
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
"done_text": _clean(r.get("Что сделано по факту (внутр.)")), "rest_text": _clean(r.get("Что осталось (внутр.)")),
"owner_note": _clean(r.get("Комментарий актуализации"))})
notes += 1
csv_keys.append(key)
new_keys = {} # new:<строка> → ключ Jira (как в jira_sync.py)
for i, r in enumerate(_rows(folder / "new_tasks.csv"), 2):
real = _clean(r.get("Ключ"))
key = real or f"new:{i}"
if real:
new_keys[f"new:{i}"] = real
csv_keys.append(real)
_note(conn, key, {
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
"rest_text": _clean(r.get("Основание (внутр.)")),
"owner_note": None if real else _clean(r.get("Тема"))})
notes += 1
for r in _rows(folder / "epics.csv"):
key = _clean(r.get("Ключ"))
if not key:
continue
_note(conn, key, {"work_kind": "эпик", "owner_note": _clean(r.get("Комментарий"))})
notes += 1
csv_keys.append(key)
link_types = settings.get(conn, "link_types") or {}
expected, missing_links = [], []
for r in _rows(folder / "links.csv"):
src = new_keys.get(_clean(r.get("Откуда")) or "", _clean(r.get("Откуда")))
dst = new_keys.get(_clean(r.get("Куда")) or "", _clean(r.get("Куда")))
kind = (_clean(r.get("Тип")) or "").lower()
if not src or not dst or src.startswith("new:") or dst.startswith("new:"):
continue
link = {"source": src, "target": dst, "link_type": link_types.get(kind, kind)}
expected.append(link)
issue = mirror.get_issue(conn, src)
present = issue and any(l["target_key"] == dst and l["link_type"] == link["link_type"] for l in issue["links_out"])
if not present:
missing_links.append(link)
known = set(mirror.known_keys(conn))
missing_keys = sorted({k for k in csv_keys if k not in known})
return {"notes": notes, "expected_links": len(expected), "missing_keys": missing_keys, "missing_links": missing_links}