FEAT-8d: docx-Importer fuer Wurfchronik-Detail (extract_docx.py + ImportDocxService)
tools/import/extract_docx.py (stdlib-Python, kein pip): Parst 'Wurfchronik der Kleinen Chaoten im Detail.docx' (Word/XML via zipfile). 93 Wuerfe + 227 benannte Tiere aus Tabellen extrahiert. Felder pro Tier: WS-Code, Wurfgeburtsdatum, Name, Farbschlag, Geschlecht (Stern- Suffix), Abnehmer, Abgabedatum, Tod-Datum + Ursache, Partnername + DOB. Sonderwerte (ZT/BLEIBT/FREI/VG:) werden herausgefiltert. Edge-Cases: Doppel-Datum (16./17.03.2021, 31.05/*01.06.2023), WS ohne Zaehler (/5), fehlende Leerzeichen vor WS:, mehrere Abnehmer (1.) ... 2.) ...). Output: output/docx_litters.json + output/docx_animals.json. tools/import/test_extract_docx.py: Unit-Tests fuer Regex-Logik + Live-Tests gegen die echte docx (skip wenn fehlt). 28/28 Tests gruen. GerbilManagerWebAPI/Import/ImportDocxService.cs: Idempotenter NACHZUG-Loader (fill-NULL-only, nie ueberschreiben): - WS-Code + Wurfgeburtsdatum -> PairingCode -> Gerbil.LitterId - Abnehmer -> Contact lookup-or-create -> Gerbil.ReceiverContactId - Abgabedatum -> Gerbil.GoHomeDate - Tod-Datum + Ursache -> Gerbil.DateOfDeath + CauseOfDeath Dry-Run zaehlt geplante Aenderungen, Execute schreibt. GerbilManagerWebAPI/Endpoints/ImportDocxEndpoints.cs: POST /import/docx/dry-run + /import/docx/execute (analog ImportEndpoints). GATE: 157/157 C#, 28/28 Python-docx-Tests, ef has-pending=No. NACHZUG: laueft NACH dem finalen WIPE+REIMPORT-3 (kein Impact auf aktuellen Pipeline).
This commit is contained in:
77
tools/import/test_extract_docx.py
Normal file
77
tools/import/test_extract_docx.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Tests for extract_docx.py — run: python test_extract_docx.py"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Require the docx to exist; skip if not present (CI won't have it)
|
||||
DOCX = os.path.join(r"C:\Users\gulum\dev",
|
||||
"Wurfchronik der Kleinen Chaoten im Detail.docx")
|
||||
SKIP = not os.path.isfile(DOCX)
|
||||
|
||||
import extract_docx as ed
|
||||
|
||||
failed = 0
|
||||
|
||||
def check(name, cond):
|
||||
global failed
|
||||
print(("ok: " if cond else "FAIL: ") + name)
|
||||
if not cond:
|
||||
failed += 1
|
||||
|
||||
# --- _norm_dob ---
|
||||
check("norm_dob 2-digit year", ed._norm_dob("12.09.21") == "12.09.2021")
|
||||
check("norm_dob 4-digit year", ed._norm_dob("07.04.2019") == "07.04.2019")
|
||||
check("norm_dob empty", ed._norm_dob("") == "")
|
||||
|
||||
# --- _LAST_DATE_RE ---
|
||||
check("last date: simple", ed._LAST_DATE_RE.findall("07.04.2019") == ["07.04.2019"])
|
||||
check("last date: dual-day", ed._LAST_DATE_RE.findall("16./17.03.2021") == ["17.03.2021"])
|
||||
check("last date: dual-month", ed._LAST_DATE_RE.findall("31.05/*01.06.2023") == ["01.06.2023"])
|
||||
|
||||
# --- _is_internal ---
|
||||
check("ZT is internal", ed._is_internal("ZT"))
|
||||
check("BLEIBT is internal", ed._is_internal("BLEIBT"))
|
||||
check("VG: is internal", ed._is_internal("VG: Partner"))
|
||||
check("real name not internal", not ed._is_internal("Marion Teichmann"))
|
||||
|
||||
# --- LITTER_RE ---
|
||||
cases = [
|
||||
("D19-Wurf *07.04.2019Von: Xhemile gen. Chanel v.d. Kleinen Chaoten & Omero v.d. Kleinen Chaoten WS: 2/4Notiz:", "2/4", "07.04.2019"),
|
||||
("-Wurf *16./17.03.2021Von: Victoria Welby v.d. Kleinen Chaoten & Patch v.d. Kleinen Chaoten WS: /5Notiz:", "/5", "16./17.03.2021"),
|
||||
("S22-Wurf *31.05/*01.06.2023Von: Velvet v.d. Kleinen Chaoten & Vance Sohn v.d. Kleinen ChaotenWS: 3/3", "3/3", "31.05/*01.06.2023"),
|
||||
("Q21-Wurf *21.03.2022Von: Belica gen. Emi v.d. Kleinen Chaoten & Zac gen. Action v.d. Kleinen ChaotenWS: 2/4Notiz:", "2/4", "21.03.2022"),
|
||||
]
|
||||
for para, expected_ws, _ in cases:
|
||||
m = ed.LITTER_RE.search(para)
|
||||
ws = m.group(5).replace(" ", "") if m else None
|
||||
check(f"LITTER_RE matches: {para[:50]}...", ws == expected_ws)
|
||||
|
||||
if SKIP:
|
||||
print("(Skipping live-docx tests: file not found)")
|
||||
else:
|
||||
litters, animals = ed.extract(DOCX)
|
||||
check("93 litters extracted", len(litters) == 93)
|
||||
check("All litters have wsCode", all(l["wsCode"] for l in litters))
|
||||
check("All litters have dob", all(l["dob"] for l in litters))
|
||||
check(">200 named animals", len(animals) >= 200)
|
||||
check(">150 animals with owner", sum(1 for a in animals if a["owner"]) >= 150)
|
||||
check(">20 animals with death date", sum(1 for a in animals if a["deathDate"]) >= 20)
|
||||
# Verify first litter
|
||||
d19 = next((l for l in litters if l["litterId"] == "D19-Wurf"), None)
|
||||
check("D19-Wurf found", d19 is not None)
|
||||
check("D19-Wurf dob correct", d19 and d19["dob"] == "07.04.2019")
|
||||
check("D19-Wurf wsCode = 2/4", d19 and d19["wsCode"] == "2/4")
|
||||
check("D19-Wurf mother contains Xhemile", d19 and "Xhemile" in d19["motherName"])
|
||||
# Verify Eddie in animals
|
||||
eddie = next((a for a in animals if a["name"] == "Eddie" and a["wsCode"] == "2/4"), None)
|
||||
check("Eddie found in D19-Wurf", eddie is not None)
|
||||
check("Eddie gender=male (Zobel* suffix)", eddie and eddie["gender"] == "male")
|
||||
check("Eddie abgabeDate", eddie and eddie["abgabeDate"] == "12.09.2021")
|
||||
# Flash death date
|
||||
flash = next((a for a in animals if a["name"] == "Flash" and a["wsCode"] == "3/3"), None)
|
||||
check("Flash death date extracted", flash and flash["deathDate"] == "16.09.2023")
|
||||
check("Flash death cause extracted", flash and "Tumor" in flash["deathCause"])
|
||||
|
||||
if failed:
|
||||
print(f"\n{failed} test(s) FAILED")
|
||||
sys.exit(1)
|
||||
print("\nALL PASS")
|
||||
Reference in New Issue
Block a user