landvex: IOL v3 + Identify API + Neo4j-graf + byggpipeline
- FastAPI Identify API (port 8081) - Neo4j propertygraf (64 noder, 80 kanter) - Ingest-pipeline (Python) - Byggscript + integrationstester - Docker Compose setup
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
# Landvex IOL — våg 5: konsolidering v2 (Tier 1 + Tier 2), validering, grafexport
|
||||
import json, re
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path("/mnt/user-data/outputs")
|
||||
PREDIKAT = {"monteras_pa","star_pa","ansluts_med","matas_fran","ingar_i","del_av",
|
||||
"ersatter","ersatts_av","kompatibel_med","forvaxlas_med","tillverkas_av","foljer_standard","variant_av"}
|
||||
VERIF = {"obekraftad","kallbelagd","faltverifierad","tillverkarbekraftad"}
|
||||
DOMANER = {"TRP","ELN","VAT","TEL","BYG","PRK","JVG","HMN","FLG"}
|
||||
ID_RE = re.compile(r"^LVX-(TRP|ELN|VAT|TEL|BYG|PRK|JVG|HMN|FLG)-[0-9]{4}$|^LVX-P-[0-9]{7}$")
|
||||
|
||||
def load(name):
|
||||
with open(OUT / name, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
poster = {}
|
||||
for fn in ["lvx-klassposter-master-v1.json","lvx-klassposter-tier2-trp.json","lvx-klassposter-tier2-ovriga.json"]:
|
||||
for p in load(fn)["poster"]:
|
||||
poster[p["lvx_id"]] = p
|
||||
|
||||
def multi_lvx(mal):
|
||||
return " / " in mal and all(ID_RE.match(x.strip()) for x in mal.split(" / "))
|
||||
|
||||
olosta = {}
|
||||
for p in poster.values():
|
||||
for rel in p.get("relationer", []):
|
||||
mal = rel.get("mal","")
|
||||
if rel.get("predikat") == "foljer_standard" or multi_lvx(mal) or ID_RE.match(mal):
|
||||
continue
|
||||
olosta[mal] = olosta.get(mal, 0) + 1
|
||||
|
||||
modeller = {}
|
||||
for fn in ["lvx-produktmodeller-vag2.json","lvx-produktmodeller-vag3.json"]:
|
||||
for m in load(fn)["poster"]:
|
||||
modeller[m["lvx_id"]] = m
|
||||
|
||||
fel = []
|
||||
def kolla(rec, typ):
|
||||
rid = rec.get("lvx_id","?")
|
||||
if not ID_RE.match(rid): fel.append(f"{rid}: ogiltigt ID-format")
|
||||
if rec.get("doman") not in DOMANER: fel.append(f"{rid}: ogiltig domän")
|
||||
namn = rec.get("namn",{})
|
||||
if not (namn.get("sv") and namn.get("en")): fel.append(f"{rid}: namn.sv/en saknas")
|
||||
if rec.get("posttyp") not in {"objektklass","produktmodell"}: fel.append(f"{rid}: posttyp")
|
||||
prov = rec.get("proveniens",{})
|
||||
if prov.get("verifieringsniva") not in VERIF: fel.append(f"{rid}: verifieringsnivå")
|
||||
if not prov.get("skapad"): fel.append(f"{rid}: proveniens.skapad saknas")
|
||||
for pr in rec.get("problem",[]):
|
||||
if not pr.get("kalla"): fel.append(f"{rid}: problem utan källa")
|
||||
for rel in rec.get("relationer",[]):
|
||||
if rel.get("predikat") not in PREDIKAT: fel.append(f"{rid}: okänt predikat {rel.get('predikat')}")
|
||||
if typ=="modell" and not rec.get("parent"): fel.append(f"{rid}: parent saknas")
|
||||
if prov.get("verifieringsniva")=="kallbelagd" and not prov.get("kallor"):
|
||||
fel.append(f"{rid}: källbelagd utan kallor")
|
||||
|
||||
for p in poster.values(): kolla(p,"klass")
|
||||
for m in modeller.values(): kolla(m,"modell")
|
||||
|
||||
kanter = []
|
||||
def kant(s, pred, obj, niva, beskr=""):
|
||||
typ = "lvx" if ID_RE.match(obj or "") else ("standard" if pred=="foljer_standard" else "text")
|
||||
kanter.append({"subjekt": s, "predikat": pred, "objekt": obj, "objekt_typ": typ,
|
||||
"niva": niva, **({"beskrivning": beskr} if beskr else {})})
|
||||
|
||||
def emit(s, pred, mal, niva, beskr=""):
|
||||
if multi_lvx(mal):
|
||||
for x in mal.split(" / "):
|
||||
kant(s, pred, x.strip(), niva, beskr)
|
||||
else:
|
||||
kant(s, pred, mal, niva, beskr)
|
||||
|
||||
for p in poster.values():
|
||||
for rel in p.get("relationer", []):
|
||||
emit(p["lvx_id"], rel["predikat"], rel["mal"], "klass", rel.get("beskrivning",""))
|
||||
for st in p.get("standarder", []):
|
||||
if st.get("beteckning"):
|
||||
kant(p["lvx_id"], "foljer_standard", st["beteckning"], "klass", st.get("roll",""))
|
||||
|
||||
for m in modeller.values():
|
||||
kant(m["lvx_id"], "tillhor_klass", m["parent"], "modell")
|
||||
hist = m.get("historik", {})
|
||||
for e in hist.get("ersatter", []): kant(m["lvx_id"], "ersatter", e, "modell")
|
||||
for e in hist.get("ersatts_av", []): kant(m["lvx_id"], "ersatts_av", e, "modell")
|
||||
for rel in m.get("relationer", []):
|
||||
emit(m["lvx_id"], rel["predikat"], rel["mal"], "modell", rel.get("beskrivning",""))
|
||||
|
||||
lvx_k = sum(1 for k in kanter if k["objekt_typ"]=="lvx")
|
||||
std_k = sum(1 for k in kanter if k["objekt_typ"]=="standard")
|
||||
verif_count = {}
|
||||
for p in poster.values():
|
||||
v = p["proveniens"]["verifieringsniva"]
|
||||
verif_count[v] = verif_count.get(v,0)+1
|
||||
|
||||
master = {
|
||||
"beskrivning": "Landvex IOL — konsoliderade klassposter v2: Tier 1 + Tier 2 (37 klasser). Ersätter master-v1 och de två tier2-filerna som arbetskopia.",
|
||||
"genererad": "2026-07-04",
|
||||
"antal_poster": len(poster),
|
||||
"verifieringsfordelning": verif_count,
|
||||
"poster": [poster[k] for k in sorted(poster)]
|
||||
}
|
||||
with open(OUT/"lvx-klassposter-master-v2.json","w",encoding="utf-8") as f:
|
||||
json.dump(master,f,ensure_ascii=False,indent=2)
|
||||
|
||||
graf = {
|
||||
"beskrivning": "Landvex IOL — grafexport v2. Tier 1 + Tier 2-klasser samt produktmodeller. objekt_typ: lvx=upplöst nod, standard=standardbeteckning, text=olöst platshållare (framtida klass).",
|
||||
"genererad": "2026-07-04",
|
||||
"noder": {"objektklasser": len(poster), "produktmodeller": len(modeller)},
|
||||
"statistik": {"kanter_totalt": len(kanter), "upplosta_lvx_kanter": lvx_k, "standardkanter": std_k, "textnoder_olosta": len(olosta)},
|
||||
"olosta_platshallare": [{"text": k, "forekomster": v} for k,v in sorted(olosta.items())],
|
||||
"kanter": kanter
|
||||
}
|
||||
with open(OUT/"lvx-graf-kanter-v2.json","w",encoding="utf-8") as f:
|
||||
json.dump(graf,f,ensure_ascii=False,indent=2)
|
||||
|
||||
print(f"Klassposter: {len(poster)} | fördelning: {verif_count}")
|
||||
print(f"Produktmodeller: {len(modeller)}")
|
||||
print(f"Grafkanter: {len(kanter)} totalt | {lvx_k} LVX-upplösta | {std_k} standardkanter")
|
||||
print(f"Olösta textnoder: {len(olosta)}")
|
||||
for k,v in sorted(olosta.items()): print(f" - {k} ({v})")
|
||||
print(f"Valideringsfel: {len(fel)}")
|
||||
for e in fel: print(f" ! {e}")
|
||||
Reference in New Issue
Block a user