feat(hub): импорт CSV-пакета admin/jira-sync в заметки, сверка связей
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
parent
a67ce091e1
commit
47495fa5db
75
src/jira_hub/importer.py
Normal file
75
src/jira_hub/importer.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""Разовый импорт CSV-пакета admin/jira-sync: служебные колонки → issue_note; links.csv — сверочный набор
|
||||
(ожидаемые связи против зеркала), не команды. Повтор обновляет заметки, не дублирует."""
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
from . import 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 import_package(conn, folder) -> dict:
|
||||
folder = Path(folder)
|
||||
notes = 0
|
||||
csv_keys = []
|
||||
|
||||
for r in _rows(folder / "tasks.csv"):
|
||||
key = _clean(r.get("Код"))
|
||||
if not key:
|
||||
continue
|
||||
mirror.set_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("Комментарий актуализации"))}, "import")
|
||||
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)
|
||||
mirror.set_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("Тема"))}, "import")
|
||||
notes += 1
|
||||
|
||||
for r in _rows(folder / "epics.csv"):
|
||||
key = _clean(r.get("Ключ"))
|
||||
if not key:
|
||||
continue
|
||||
mirror.set_note(conn, key, {"work_kind": "эпик", "owner_note": _clean(r.get("Комментарий"))}, "import")
|
||||
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}
|
||||
52
tests/test_importer.py
Normal file
52
tests/test_importer.py
Normal file
@ -0,0 +1,52 @@
|
||||
import csv
|
||||
|
||||
from jira_hub import db, importer, mirror, settings
|
||||
|
||||
|
||||
def write_csv(path, header, rows):
|
||||
with open(path, "w", encoding="utf-8-sig", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(header)
|
||||
w.writerows(rows)
|
||||
|
||||
|
||||
def make_package(folder):
|
||||
write_csv(folder / "tasks.csv",
|
||||
["Код", "Тема", "Вид работы", "Статус по факту", "Комментарий актуализации",
|
||||
"Что сделано по факту (внутр.)", "Что осталось (внутр.)", "Спринт"],
|
||||
[["AURORA-1533", "Тест ОСВ", "тестирование", "Ожидание разработки", "Актуализация 10.10.2026. Порядок",
|
||||
"", "Прогнать на стенде", "Спринт 2"],
|
||||
["AURORA-1416", "Карточка", "UI", "В работе", "", "Сделан список", "Модалка", ""]])
|
||||
write_csv(folder / "new_tasks.csv",
|
||||
["Ключ", "Тема", "Вид работы", "Статус по факту", "Описание", "Основание (внутр.)", "Спринт"],
|
||||
[["AURORA-1586", "Справочники: тестирование", "тестирование", "Ожидание разработки", "h3. Контекст", "решение 11.10", "Спринт 2"],
|
||||
["", "Ещё не создана", "разработка", "Ожидание разработки", "h3. Контекст", "идея", ""]])
|
||||
write_csv(folder / "epics.csv", ["Код", "Ключ", "Эпик", "Комментарий"],
|
||||
[["AURORA-1350", "AURORA-1350", "Загрузка", "ядро"], ["NEW-1", "AURORA-1529", "Роли", "новый"]])
|
||||
write_csv(folder / "links.csv", ["Откуда", "Тип", "Куда", "Пояснение (внутр.)"],
|
||||
[["AURORA-1533", "блокирует начало", "AURORA-1416", "x"],
|
||||
["new:2", "связанные", "AURORA-1533", "y"],
|
||||
["new:3", "связанные", "AURORA-1533", "z"]])
|
||||
|
||||
|
||||
def test_import_notes_and_report(tmp_path):
|
||||
make_package(tmp_path)
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
settings.set_(conn, "link_types", {"блокирует начало": "ASUPR_Блокер_Начала", "связанные": "ASUPR_Связанные"})
|
||||
mirror.apply_state(conn, [{"key": "AURORA-1533", "summary": "т", "labels": []}, {"key": "AURORA-1586", "summary": "т", "labels": []}],
|
||||
[{"source_key": "AURORA-1586", "target_key": "AURORA-1533", "link_type": "ASUPR_Связанные"}])
|
||||
rep = importer.import_package(conn, tmp_path)
|
||||
assert rep["notes"] == 6
|
||||
n = mirror.get_note(conn, "AURORA-1533")
|
||||
assert n["fact_status"] == "Ожидание разработки" and n["work_kind"] == "тестирование"
|
||||
assert n["rest_text"] == "Прогнать на стенде" and n["owner_note"] == "Актуализация 10.10.2026. Порядок" and n["updated_by"] == "import"
|
||||
assert mirror.get_note(conn, "AURORA-1416")["done_text"] == "Сделан список"
|
||||
assert mirror.get_note(conn, "AURORA-1586")["rest_text"] == "решение 11.10"
|
||||
assert mirror.get_note(conn, "new:3")["owner_note"] == "Ещё не создана"
|
||||
assert mirror.get_note(conn, "AURORA-1529")["work_kind"] == "эпик"
|
||||
assert rep["missing_keys"] == ["AURORA-1350", "AURORA-1416", "AURORA-1529"]
|
||||
assert rep["expected_links"] == 2 # new:3 без ключа — не сверяется
|
||||
assert rep["missing_links"] == [{"source": "AURORA-1533", "target": "AURORA-1416", "link_type": "ASUPR_Блокер_Начала"}]
|
||||
assert importer.import_package(conn, tmp_path)["notes"] == 6
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM issue_note").fetchone()["n"] == 6
|
||||
Loading…
x
Reference in New Issue
Block a user