#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Triage-Helfer: Beantwortet die Gegenfragen der Züchterin zu RPRO3-Namensdubletten. Liefert pro Variante (A/B/C … exakt wie im Rückfrage-Ticket) die Daten, die die Züchterin typischerweise sehen will: Geburtsdatum, Farbe, **Gencode (Fcode)**, Herkunft, Eltern (Vater/Mutter) sowie Nachzucht inkl. Co-Elternteil (Partner). Die Variantenbildung/Label-Vergabe spiegelt rpro3_tickets.py + compare_rpro3.dedup exakt (Union-Find über gleiche Namen; Filter „informativ"; Sortierung (not is_own, -count) → A,B,C…). Im Gegensatz zum Ticket wird hier über den GESAMTEN Cluster aggregiert (rpro3_tickets kappt rids auf 4 – Nachzucht wäre sonst unvollständig). Aufruf: python rpro3_lookup.py <_rpro3.db> "" [LETTERS] LETTERS optional, z. B. "B,C" → nur diese Varianten. Default: alle. python rpro3_lookup.py <_rpro3.db> "" --json → strukturiert (für Agenten) """ from __future__ import annotations import sys, json from collections import defaultdict import compare_rpro3 as C LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" def informative(recs): return any(r["dob"] or r["farbe"] or r["origin"] for r in recs) def build_clusters(animals): """Spiegelt compare_rpro3.dedup: Union-Find je Name, gibt {name: [cluster_recs,...]}.""" def compat(x, y): return (not x) or (not y) or (x == y) for a in animals: a["namek"] = C.norm_name(a["name"]) a["dobk"] = a["dob"] a["farbek"] = C.norm_farbe(a["farbe"]) a["origink"] = C.norm_origin(a["origin"]) by_name = defaultdict(list) for a in animals: if a["namek"] in C.PLACEHOLDER_NAMES: continue by_name[a["namek"]].append(a) parent = {} def find(x): while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x def union(x, y): parent.setdefault(x, x); parent.setdefault(y, y) parent[find(x)] = find(y) def positive(a, b): agree = 0 if a["dobk"] and b["dobk"] and a["dobk"] == b["dobk"]: agree += 1 if a["farbek"] and b["farbek"] and a["farbek"] == b["farbek"]: agree += 1 if a["origink"] and b["origink"] and a["origink"] == b["origink"]: agree += 1 return agree def conflict(a, b): c = 0 if a["dobk"] and b["dobk"] and a["dobk"] != b["dobk"]: c += 1 if a["farbek"] and b["farbek"] and a["farbek"] != b["farbek"]: c += 1 if a["origink"] and b["origink"] and a["origink"] != b["origink"]: c += 1 return c for name, group in by_name.items(): for a in group: parent.setdefault(a["rid"], a["rid"]) n = len(group) for i in range(n): for j in range(i + 1, n): a, b = group[i], group[j] comp = (compat(a["dobk"], b["dobk"]) and compat(a["farbek"], b["farbek"]) and compat(a["origink"], b["origink"])) if comp and positive(a, b) >= 1 and conflict(a, b) == 0: union(a["rid"], b["rid"]) # Cluster je Name sammeln name_clusters = {} by_rid = {a["rid"]: a for a in animals} for name, group in by_name.items(): roots = defaultdict(list) for a in group: roots[find(a["rid"])].append(a) name_clusters[name] = list(roots.values()) return name_clusters def variants_for(animals, display_name): """Liefert [(letter, recs)] für einen Namen – Reihenfolge wie im Ticket.""" namek = C.norm_name(display_name) clusters = build_clusters(animals).get(namek, []) info = [recs for recs in clusters if informative(recs)] # Sortierung exakt wie rpro3_tickets.py: eigene Tiere zuerst, dann größere Cluster info.sort(key=lambda recs: (not any(r["src"] == "stamm" for r in recs), -len(recs))) return [(LETTERS[i], recs) for i, recs in enumerate(info)] def aggregate(recs, animals): """Aggregiert Daten + Nachzucht über alle recs eines Clusters.""" names = {r["name"] for r in recs} dob = sorted({C.iso(r["dob"]) for r in recs if r["dob"]}) farbe = sorted({r["farbe"] for r in recs if r["farbe"]}) fcode = sorted({r["fcode"] for r in recs if r["fcode"]}) origin = sorted({r["origin"] for r in recs if r["origin"]}) fathers = sorted({r["father"] for r in recs if r["father"]}) mothers = sorted({r["mother"] for r in recs if r["mother"]}) is_own = any(r["src"] == "stamm" for r in recs) rids = [r["rid"] for r in recs] # Nachzucht: alle Tiere, deren Eltern-rid auf eine rid dieses Clusters zeigt rid_set = set(rids) kids = [] seen = set() for a in animals: if str(a.get("mid_raw")) in rid_set or str(a.get("pid_raw")) in rid_set: co = a["father"] if a["mother"] in names else a["mother"] key = (C.norm_name(a["name"]), C.iso(a["dob"]) if a["dob"] else "") if key in seen: continue seen.add(key) kids.append({"name": a["name"], "dob": C.iso(a["dob"]) if a["dob"] else None, "farbe": a["farbe"], "fcode": a["fcode"], "partner": co}) return {"names": sorted(names), "rids": rids, "count": len(recs), "is_own": is_own, "dob": dob, "farbe": farbe, "fcode": fcode, "origin": origin, "fathers": fathers, "mothers": mothers, "kids": kids} def fmt_block(letter, agg): src = "eigenes Tier" if agg["is_own"] else "externer Ahn" L = [] L.append(f"**{letter}** ({src}, {agg['count']}× in RennmausPro):") L.append(f"• Geburtsdatum: {', '.join(agg['dob']) or 'unbekannt'}") L.append(f"• Farbe: {', '.join(agg['farbe']) or 'unbekannt'}") L.append(f"• Gencode: {', '.join(agg['fcode']) or 'unbekannt'}") L.append(f"• Herkunft: {', '.join(agg['origin']) or 'unbekannt'}") vat = ', '.join(agg['fathers']) or 'unbekannt' mut = ', '.join(agg['mothers']) or 'unbekannt' L.append(f"• Eltern: Vater {vat} · Mutter {mut}") if agg["kids"]: L.append("• Nachzucht:") for k in agg["kids"]: d = k["dob"] or "?" f = k["farbe"] or "?" gc = f" [{k['fcode']}]" if k["fcode"] else "" p = f" — Partner: {k['partner']}" if k["partner"] else "" L.append(f" – {k['name']} (geb. {d}, {f}{gc}){p}") else: L.append("• Nachzucht: keine in RennmausPro hinterlegt") return "\n".join(L) def variants_by_rids(animals, rid_groups): """rid_groups: Liste von rid-Listen (eine je Variante/Buchstabe). Aggregiert pro Gruppe über den GANZEN Auto-Cluster (eine rid zieht ihren Cluster mit). Für Platzhalter-Namen („...") und Freitext-Tickets, wo der Name nicht greift, aber rids bekannt sind.""" from collections import defaultdict # Auto-Cluster wie build_clusters, aber global über rids indizieren clusters = build_clusters(animals) rid_to_recs = {} for recs_list in clusters.values(): for recs in recs_list: for r in recs: rid_to_recs[r["rid"]] = recs out = [] for i, grp in enumerate(rid_groups): merged, seen = [], set() for rid in grp: for r in rid_to_recs.get(rid, []): if r["rid"] not in seen: seen.add(r["rid"]); merged.append(r) if merged: out.append((LETTERS[i], merged)) return out def main(): db = sys.argv[1] name = sys.argv[2] # --rids "u84,u83;u1" → Variante A=u84,u83 Variante B=u1 (per Semikolon getrennt) if name == "--rids": rid_spec = sys.argv[3] R = C.load_rpro3(db) groups = [[x.strip() for x in g.split(",") if x.strip()] for g in rid_spec.split(";") if g.strip()] vs = variants_by_rids(R["animals"], groups) print("=== (per rids) ===") for letter, recs in vs: print(fmt_block(letter, aggregate(recs, R["animals"]))) print() return as_json = "--json" in sys.argv[3:] letters = None for a in sys.argv[3:]: if a != "--json": letters = {x.strip().upper() for x in a.split(",") if x.strip()} R = C.load_rpro3(db) animals = R["animals"] vs = variants_for(animals, name) if not vs: print(f"(keine Varianten für „{name}“ gefunden)") return out = [] for letter, recs in vs: if letters and letter not in letters: continue agg = aggregate(recs, animals) out.append((letter, agg)) if as_json: print(json.dumps({"name": name, "variants": {l: a for l, a in out}}, ensure_ascii=False, indent=1)) else: print(f"=== {name} ===") for letter, agg in out: print(fmt_block(letter, agg)) print() if __name__ == "__main__": main()