"""Разовый импорт 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, reversed_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) if issue and any(l["target_key"] == dst and l["link_type"] == link["link_type"] for l in issue["links_out"]): continue if issue and any(l["source_key"] == dst and l["link_type"] == link["link_type"] for l in issue["links_in"]): reversed_links.append(link) # связь есть, но в Jira стоит в обратную сторону (O-3) else: 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, "reversed_links": reversed_links}