# -*- coding: utf-8 -*- """Parst ALKIS-Objektartenkataloge (AAA-Anwendungsschema, AC_FeatureCatalogue-XML von adv-online) und erzeugt atomare Markdown-Wissensdateien je Objektart/Datentyp plus eine Nachschlage-Datei (INDEX.md). Das Skript kennt zwei Versionen nebeneinander (aktuell im Projekt relevant: GID6 = operativer WFS-Produktivstand, GID7 = Zielstand). Rohdaten liegen NICHT hier im Ordner, sondern unversioniert unter referenz/alkis/ (siehe .gitignore) -- dieser Ordner enthaelt nur die generierten .md-Dateien. Aufrufe: Generieren (schreibt nach docs/wissen/alkis//): python parse_alkis.py generate gid7 "AAA-Anwendungsschema 7.1.2 (Zielstand)" python parse_alkis.py generate gid6 "AAA-Anwendungsschema 6.0.1 (Produktivstand)" Aufgeloeste Attribute einer Klasse ueber die Vererbungskette ausgeben (kein Datei-Hopping noetig): python parse_alkis.py resolve AX_Grenzpunkt Zwei Versionen fuer eine Klasse vergleichen (nur Unterschiede, berechnet -- nicht gepflegt): python parse_alkis.py diff AX_Grenzpunkt Bestehende Dateien im jeweiligen Versionsordner werden beim Generieren ueberschrieben. """ import xml.etree.ElementTree as ET import re import os import sys NS = { 'gml': 'http://www.opengis.net/gml/3.2', 'nas': 'http://www.adv-online.de/namespaces/adv/gid/7.1', 'fc': 'http://www.adv-online.de/namespaces/adv/gid/fc/7.1', } GML_ID = '{http://www.opengis.net/gml/3.2}id' XLINK_HREF = '{http://www.w3.org/1999/xlink}href' ALKIS_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) def tag(el): return el.tag.split('}', 1)[-1] def gml_desc(el): d = el.find('gml:description', NS) return (d.text or '').strip() if d is not None else '' def gml_name(el): d = el.find('gml:identifier[@codeSpace="urn:adv:name"]', NS) return (d.text or '').strip() if d is not None else '' def gml_kennung(el): d = el.find('gml:name[@codeSpace="urn:adv:kennung"]', NS) return (d.text or '').strip() if d is not None else '' def child(el, local_name): """Findet ein Kindelement nach lokalem Namen, unabhaengig vom (Default-)Namespace.""" for c in el: if tag(c) == local_name: return c return None def child_text(el, local_name): c = child(el, local_name) return (c.text or '').strip() if c is not None and c.text else '' def rule_texts(cls_el, wrapper_local, holder_local, inner_local): """Liest wiederholbare Regel-/Bedingungselemente wie .... """ texts = [] for c in cls_el: if tag(c) == wrapper_local: holder = child(c, holder_local) if holder is not None: t = child_text(holder, inner_local) if t: texts.append(t) return texts def retired_info(el): retired = child_text(el, 'isRetired').lower() == 'true' since = child_text(el, 'retiredSinceVersion') return retired, since def slugify(name): return re.sub(r'[^A-Za-z0-9_]+', '_', name).strip('_') def md_escape(text): return text.replace('\r\n', '\n').strip() # --------------------------------------------------------------------------- # Parsen einer Katalog-XML in eine neutrale Datenstruktur (einmal pro Quelle) # --------------------------------------------------------------------------- def parse_catalog(src_path): print(f"Parse {src_path} ...") tree = ET.parse(src_path) root = tree.getroot() registry = {} # gml:id -> dict(kind, name, kennung) def register(el, kind): gid = el.get(GML_ID) if gid: registry[gid] = {'kind': kind, 'name': gml_name(el), 'kennung': gml_kennung(el)} for el in root.iter(): t = tag(el) if t in ('AC_FeatureType', 'AC_DataType', 'AC_AssociationRole', 'FC_FeatureAssociation', 'AC_FeatureAttribute'): register(el, t) def resolve_href(xlink_href): if not xlink_href: return None return registry.get(xlink_href.lstrip('#')) def href_of(el): return el.get(XLINK_HREF) # -- Objektartenbereich / -gruppe Kontext -- classes = [] # list of dicts: kind, el, bereich, bereich_kennung, gruppe, gruppe_kennung def walk_bereich(bereich_el): bereich_name = gml_name(bereich_el) bereich_kennung = gml_kennung(bereich_el) for entry in bereich_el.findall('gml:dictionaryEntry', NS): for child_el in entry: t = tag(child_el) if t == 'AC_Objektartengruppe': gruppe_name = gml_name(child_el) gruppe_kennung = gml_kennung(child_el) for gentry in child_el.findall('gml:dictionaryEntry', NS): for cls_el in gentry: ct = tag(cls_el) if ct in ('AC_FeatureType', 'AC_DataType'): classes.append({ 'kind': ct, 'el': cls_el, 'bereich': bereich_name, 'bereich_kennung': bereich_kennung, 'gruppe': gruppe_name, 'gruppe_kennung': gruppe_kennung, }) elif t in ('AC_FeatureType', 'AC_DataType'): classes.append({ 'kind': t, 'el': child_el, 'bereich': bereich_name, 'bereich_kennung': bereich_kennung, 'gruppe': '(bereichsweit, keiner Objektartengruppe zugeordnet)', 'gruppe_kennung': '-', }) for entry in root.findall('gml:dictionaryEntry', NS): for bereich_el in entry: if tag(bereich_el) == 'AC_Objektartenbereich': walk_bereich(bereich_el) print(f" Klassen gefunden: {len(classes)}") # -- FC_FeatureAssociation -> beteiligte AC_AssociationRole-ids (Gegenrollen-Aufloesung) -- fc_role_ids = {} for el in root.iter(): if tag(el) == 'FC_FeatureAssociation': gid = el.get(GML_ID) role_ids = [e.get(XLINK_HREF).lstrip('#') for e in el if tag(e) == 'dictionaryEntry' and e.get(XLINK_HREF)] if gid: fc_role_ids[gid] = role_ids # -- AC_AssociationRole -> Traegerklasse (fuer Gegenrollen-Aufloesung) -- role_owner = {} for entry_ in classes: if entry_['kind'] != 'AC_FeatureType': continue cls_el = entry_['el'] owner_name = gml_name(cls_el) owner_kennung = gml_kennung(cls_el) for dentry in cls_el.findall('gml:dictionaryEntry', NS): for c in dentry: if tag(c) == 'AC_AssociationRole': rid = c.get(GML_ID) if rid: role_owner[rid] = { 'name': gml_name(c), 'kennung': gml_kennung(c), 'cardinality': child_text(c, 'cardinality'), 'navigable': child_text(c, 'isNavigable'), 'owner_name': owner_name, 'owner_kennung': owner_kennung, } def type_ref_text(el): vt = child(el, 'valueType') if vt is not None: info = resolve_href(href_of(vt)) if info: suffix = 'Datentyp' if info['kind'] == 'AC_DataType' else ('Objektart' if info['kind'] == 'AC_FeatureType' else info['kind']) return f"{info['name']} ({suffix}, {info['kennung']})" if info['kennung'] else f"{info['name']} ({suffix})" return f"(unaufgeloeste Referenz {href_of(vt)})" vtn = child(el, 'valueTypeName') return (vtn.text or '').strip() if vtn is not None else '' def listed_values(attr_el): values = [] for entry in attr_el.findall('gml:dictionaryEntry', NS): for lv in entry: if tag(lv) == 'AC_ListedValue': retired, retired_since = retired_info(lv) values.append({ 'name': gml_name(lv), 'kennung': gml_kennung(lv), 'desc': gml_desc(lv), 'nutzungsartkennung': child_text(lv, 'nutzungsartkennung'), 'retired': retired, 'retired_since': retired_since, }) return values def extract_attributes(cls_el): attrs = [] for entry in cls_el.findall('gml:dictionaryEntry', NS): for c in entry: if tag(c) == 'AC_FeatureAttribute': retired, retired_since = retired_info(c) attrs.append({ 'name': gml_name(c), 'kennung': gml_kennung(c), 'cardinality': child_text(c, 'cardinality'), 'type': type_ref_text(c), 'desc': gml_desc(c), 'objektbildend': child_text(c, 'objektbildend'), 'listed_values': listed_values(c), 'bildungsregeln': rule_texts(c, 'bildungsregel', 'AC_Bildungsregel', 'regel'), 'retired': retired, 'retired_since': retired_since, 'landnutzung': child_text(c, 'landnutzung'), }) return attrs def extract_associations(cls_el): assocs = [] for entry in cls_el.findall('gml:dictionaryEntry', NS): for c in entry: if tag(c) == 'AC_AssociationRole': own_id = c.get(GML_ID) rel_el = child(c, 'relation') rel_href = href_of(rel_el) if rel_el is not None else None fc_id = rel_href.lstrip('#') if rel_href else None gegenrolle = None if fc_id and fc_id in fc_role_ids: for oid in [rid for rid in fc_role_ids[fc_id] if rid != own_id]: if oid in role_owner: gegenrolle = role_owner[oid] break assocs.append({ 'name': gml_name(c), 'kennung': gml_kennung(c), 'cardinality': child_text(c, 'cardinality'), 'target': type_ref_text(c), 'desc': gml_desc(c), 'navigable': child_text(c, 'isNavigable'), 'gegenrolle': gegenrolle, }) return assocs def extract_supertypes(cls_el): sups = [] for entry in cls_el.findall('gml:dictionaryEntry', NS): for c in entry: if tag(c) == 'FC_InheritanceRelation': stn_text = child_text(c, 'supertypeName') st = child(c, 'supertype') if stn_text: sups.append(stn_text) elif st is not None: info = resolve_href(href_of(st)) if info: sups.append(info['name']) return sups def is_abstract(cls_el): return child_text(cls_el, 'isAbstract').lower() == 'true' # -- Alle Klassen einmal vollstaendig auswerten (fuer Markdown UND fuer resolve/diff) -- parsed_classes = {} # name -> dict mit allen Feldern for entry_ in classes: cls_el = entry_['el'] name = gml_name(cls_el) class_retired, class_retired_since = retired_info(cls_el) parsed_classes[name] = { 'kind': entry_['kind'], 'name': name, 'kennung': gml_kennung(cls_el), 'desc': md_escape(gml_desc(cls_el)), 'abstract': is_abstract(cls_el), 'bereich': entry_['bereich'], 'bereich_kennung': entry_['bereich_kennung'], 'gruppe': entry_['gruppe'], 'gruppe_kennung': entry_['gruppe_kennung'], 'sups': extract_supertypes(cls_el), 'attrs': extract_attributes(cls_el), 'assocs': extract_associations(cls_el) if entry_['kind'] == 'AC_FeatureType' else [], 'retired': class_retired, 'retired_since': class_retired_since, 'typisiert_durch': child_text(cls_el, 'wirdTypisiertDurch'), 'kategorie': child_text(cls_el, 'kategorie'), 'landnutzung': child_text(cls_el, 'landnutzung'), 'nutzungsart': child_text(cls_el, 'nutzungsart'), 'nutzungsartkennung': child_text(cls_el, 'nutzungsartkennung'), 'bildungsregeln': rule_texts(cls_el, 'bildungsregel', 'AC_Bildungsregel', 'regel'), 'konsistenzbedingungen': rule_texts(cls_el, 'konsistenzbedingung', 'AC_Konsistenzbedingung', 'bedingung'), 'erfassungskriterien': rule_texts(cls_el, 'erfassungskriterium', 'AC_Erfassungskriterium', 'kriterium'), } return parsed_classes # --------------------------------------------------------------------------- # Vererbung aufloesen (fuer resolve-Modus und fuer den Inline-Hinweis in den .md-Dateien) # --------------------------------------------------------------------------- def resolve_effective(parsed_classes, name, _seen=None): """Liefert (attrs, assocs) inkl. aller von AX_*-Oberklassen geerbten Eintraege. Eigene Attribute/Relationen zuerst, dann je Oberklasse in der Vererbungskette (nur soweit diese selbst im Katalog als Klasse vorkommt -- UML-Basistypen wie TA_PointComponent/AA_ZUSO tragen keine ALKIS-Attribute und werden ignoriert). """ _seen = _seen or set() if name in _seen or name not in parsed_classes: return [], [] _seen.add(name) cls = parsed_classes[name] attrs = [dict(a, herkunft=name) for a in cls['attrs']] assocs = [dict(a, herkunft=name) for a in cls['assocs']] for sup_name in cls['sups']: sup_attrs, sup_assocs = resolve_effective(parsed_classes, sup_name, _seen) attrs.extend(sup_attrs) assocs.extend(sup_assocs) return attrs, assocs def inherited_names_hint(parsed_classes, sup_name): """Kurzer Hinweistext, welche Attribut-/Relationsnamen eine Oberklasse beitraegt -- ohne die vollen Beschreibungen/Codelisten zu duplizieren.""" if sup_name not in parsed_classes: return '' cls = parsed_classes[sup_name] names = [a['name'] for a in cls['attrs']] + [a['name'] for a in cls['assocs']] if not names: return '' return " [erbt: " + ", ".join(names) + "]" # --------------------------------------------------------------------------- # Markdown-Erzeugung # --------------------------------------------------------------------------- def write_class_file(cls, out_dir, parsed_classes, version_label): kind = cls['kind'] name = cls['name'] art = 'Objektart' if kind == 'AC_FeatureType' else 'Datentyp' prefix = 'objektart' if kind == 'AC_FeatureType' else 'datentyp' fname = f"{prefix}_{slugify(name)}.md" fpath = os.path.join(out_dir, fname) attrs = cls['attrs'] assocs = cls['assocs'] lines = [] lines.append(f"# {name} ({cls['kennung']})") lines.append("") lines.append(f"- **Version:** {version_label}") lines.append(f"- **Art:** {art}{' (abstrakt)' if cls['abstract'] else ''}") lines.append(f"- **Objektartenbereich:** {cls['bereich']} ({cls['bereich_kennung']})") lines.append(f"- **Objektartengruppe:** {cls['gruppe']} ({cls['gruppe_kennung']})") if cls['sups']: sup_parts = [] for s in cls['sups']: sup_info = parsed_classes.get(s) label = f"{s} ({sup_info['kennung']})" if sup_info else s sup_parts.append(label + inherited_names_hint(parsed_classes, s)) lines.append(f"- **Oberklassen:** {'; '.join(sup_parts)}") if cls['typisiert_durch']: lines.append(f"- **Wird typisiert durch:** {cls['typisiert_durch']} (Objekttyp-Code, z. B. REO=Realobjekt, ZUSO=zusammengesetztes Objekt)") if cls['kategorie']: lines.append(f"- **Kategorie (Datentyp):** {cls['kategorie']}" + (" — Union: es ist jeweils nur eines der Attribute belegt" if cls['kategorie'] == 'Union' else "")) if cls['landnutzung']: na = f", Nutzungsartkennung {cls['nutzungsartkennung']}" if cls['nutzungsartkennung'] else "" lines.append(f"- **Tatsächliche Nutzung:** {cls['nutzungsart']}{na} (landnutzung={cls['landnutzung']})") if cls['retired']: lines.append(f"- **Zurückgezogen (retired) seit Version:** {cls['retired_since'] or 'unbekannt'} — nicht mehr aktiv verwenden, ggf. nur für Altdaten relevant.") lines.append("") lines.append("## Beschreibung") lines.append("") lines.append(cls['desc'] if cls['desc'] else "_(keine Beschreibung im Katalog)_") lines.append("") for section_title, rules in ( ("Bildungsregeln", cls['bildungsregeln']), ("Konsistenzbedingungen", cls['konsistenzbedingungen']), ("Erfassungskriterien", cls['erfassungskriterien']), ): if rules: lines.append(f"## {section_title}") lines.append("") for r in rules: lines.append(f"- {md_escape(r).replace(chr(10), ' ')}") lines.append("") if attrs: lines.append("## Attribute") lines.append("") lines.append("| Attribut | Kennung | Kardinalität | Typ | Objektbildend | Retired |") lines.append("|---|---|---|---|---|---|") for a in attrs: retired_mark = f"seit {a['retired_since']}" if a['retired'] else "" lines.append(f"| {a['name']} | {a['kennung']} | {a['cardinality']} | {a['type']} | {a['objektbildend']} | {retired_mark} |") lines.append("") for a in attrs: if a['desc'] or a['listed_values'] or a['bildungsregeln']: lines.append(f"### Attribut `{a['name']}` ({a['kennung']})") lines.append("") if a['retired']: lines.append(f"_Zurückgezogen (retired) seit Version {a['retired_since'] or 'unbekannt'}._") lines.append("") if a['desc']: lines.append(md_escape(a['desc'])) lines.append("") if a['bildungsregeln']: lines.append("Bildungsregel(n):") lines.append("") for r in a['bildungsregeln']: lines.append(f"- {md_escape(r).replace(chr(10), ' ')}") lines.append("") if a['listed_values']: has_nak = any(lv['nutzungsartkennung'] for lv in a['listed_values']) has_retired = any(lv['retired'] for lv in a['listed_values']) lines.append("Wertebereich (Codeliste):") lines.append("") header = ["Code", "Bedeutung", "Beschreibung"] if has_nak: header.append("Nutzungsartkennung") if has_retired: header.append("Retired") lines.append("| " + " | ".join(header) + " |") lines.append("|" + "---|" * len(header)) for lv in a['listed_values']: d = md_escape(lv['desc']).replace('\n', ' ') row = [lv['kennung'], lv['name'], d] if has_nak: row.append(lv['nutzungsartkennung']) if has_retired: row.append(f"seit {lv['retired_since']}" if lv['retired'] else "") lines.append("| " + " | ".join(row) + " |") lines.append("") else: lines.append("## Attribute") lines.append("") lines.append("_(keine eigenen Attribute im Katalog verzeichnet)_") lines.append("") if assocs: lines.append("## Relationen / Assoziationen") lines.append("") lines.append("| Rolle | Kennung | Kardinalität | Ziel | Navigierbar | Gegenrolle |") lines.append("|---|---|---|---|---|---|") for a in assocs: g = a['gegenrolle'] gtext = f"`{g['name']}` auf {g['owner_name']} ({g['owner_kennung']})" if g else "" lines.append(f"| {a['name']} | {a['kennung']} | {a['cardinality']} | {a['target']} | {a['navigable']} | {gtext} |") lines.append("") for a in assocs: if a['desc'] or a['gegenrolle']: lines.append(f"### Relation `{a['name']}` ({a['kennung']})") lines.append("") if a['desc']: lines.append(md_escape(a['desc'])) lines.append("") g = a['gegenrolle'] if g: lines.append( f"Gegenstück: `{g['name']}` ({g['kennung']}) auf `{g['owner_name']}` " f"({g['owner_kennung']}), Kardinalität {g['cardinality']}, navigierbar: {g['navigable']}." ) lines.append("") if cls['sups']: real_sups = [s for s in cls['sups'] if s in parsed_classes and (parsed_classes[s]['attrs'] or parsed_classes[s]['assocs'])] if real_sups: lines.append("## Geerbte Attribute/Relationen (nicht in dieser Datei aufgeführt)") lines.append("") lines.append("Für die vollständige, über die Vererbungskette aufgelöste Liste (Name, Kardinalität, Typ,") lines.append("Herkunftsklasse) ohne Datei-Hopping:") lines.append(f"`python _werkzeuge/parse_alkis.py resolve {name}`") lines.append("") for s in real_sups: sup = parsed_classes[s] sup_fname = f"{'objektart' if sup['kind'] == 'AC_FeatureType' else 'datentyp'}_{slugify(s)}.md" lines.append(f"- [{s}]({sup_fname}) ({sup['kennung']}): {', '.join(a['name'] for a in sup['attrs'] + sup['assocs'])}") lines.append("") lines.append("## Hinweise für Tests") lines.append("") hints = [] pflicht = [a for a in attrs if a['cardinality'] in ('1', '1..*')] optional = [a for a in attrs if a['cardinality'] in ('0..1', '0..*')] if pflicht: hints.append(f"- Pflichtattribute (Kardinalität 1 oder 1..\\*): {', '.join(a['name'] for a in pflicht)} — Testfälle für Validierung bei Fehlen sinnvoll.") if optional: hints.append(f"- Optionale Attribute: {', '.join(a['name'] for a in optional)} — Testfälle mit und ohne Belegung.") enum_attrs = [a for a in attrs if a['listed_values']] if enum_attrs: hints.append(f"- Attribute mit Codeliste: {', '.join(a['name'] for a in enum_attrs)} — Testfälle für gültige und ungültige Codes.") multi_attrs = [a for a in attrs if a['cardinality'] in ('0..*', '1..*')] if multi_attrs: hints.append(f"- Mehrfach belegbare Attribute: {', '.join(a['name'] for a in multi_attrs)} — Testfälle mit 0, 1 und mehreren Werten.") retired_attrs = [a for a in attrs if a['retired']] if retired_attrs: hints.append(f"- Zurückgezogene (retired) Attribute: {', '.join(a['name'] for a in retired_attrs)} — sollten in neuen Daten nicht mehr vorkommen; Testfälle für Umgang mit Altdaten/Migration sinnvoll.") retired_codes = [(a['name'], lv['kennung'], lv['name']) for a in attrs for lv in a['listed_values'] if lv['retired']] if retired_codes: joined = ', '.join(f"{n}={k} ({b})" for n, k, b in retired_codes) hints.append(f"- Zurückgezogene Codelisten-Werte: {joined} — Testfälle, dass diese Werte in neuen Daten abgelehnt bzw. beim Import toleriert werden.") if cls['konsistenzbedingungen']: hints.append("- Konsistenzbedingungen oben je als eigener Testfall (Positiv- und Verletzungsfall) sinnvoll.") if cls['bildungsregeln']: hints.append("- Bildungsregeln oben pruefen: definieren i. d. R. das objektbildende Attribut/die Objektidentität — Testfall für Duplikaterkennung/Schlüsselaufbau sinnvoll.") if cls['kategorie'] == 'Union': hints.append("- Union-Typ: Es darf jeweils **genau eines** der Attribute belegt sein, nicht mehrere gleichzeitig und nicht keines — Testfälle: genau ein Attribut gesetzt (gültig je Variante), keines gesetzt (ungültig), mehrere gleichzeitig gesetzt (ungültig).") if not hints: hints.append("- Keine besonderen Auffälligkeiten aus dem Katalog ableitbar.") lines.extend(hints) lines.append("") with open(fpath, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) return fname def generate(version_label, version_desc, src_path): out_dir = os.path.join(ALKIS_DIR, version_label) os.makedirs(out_dir, exist_ok=True) parsed_classes = parse_catalog(src_path) index_rows = [] for cls in parsed_classes.values(): fname = write_class_file(cls, out_dir, parsed_classes, version_desc) index_rows.append((fname, cls)) print(f"Geschrieben: {len(index_rows)} Dateien in {out_dir}") by_bereich = {} for fname, cls in index_rows: key = (cls['bereich_kennung'], cls['bereich']) by_bereich.setdefault(key, {}).setdefault((cls['gruppe_kennung'], cls['gruppe']), []).append((fname, cls)) art_of = lambda cls: 'Objektart' if cls['kind'] == 'AC_FeatureType' else 'Datentyp' idx = [] idx.append(f"# Nachschlage-Index: ALKIS-Objektartenkatalog — {version_desc}") idx.append("") idx.append(f"Quelle: `{os.path.basename(src_path)}`. Version-Kennung in diesem Baum: `{version_label}`.") idx.append(f"Enthält {sum(1 for _, c in index_rows if art_of(c)=='Objektart')} Objektarten und " f"{sum(1 for _, c in index_rows if art_of(c)=='Datentyp')} Datentypen, je als eigene Wissensdatei.") idx.append("") idx.append("Jede Datei enthält alle Katalog-Informationen zur Klasse: Beschreibung, Oberklassen (mit Namen der") idx.append("geerbten Attribute/Relationen als Kurzhinweis), Objekttyp-Code (REO/ZUSO/…), Bildungsregeln,") idx.append("Konsistenzbedingungen, Erfassungskriterien, eigene Attribute (Kardinalität, Typ, Objektbildend-Flag,") idx.append("ggf. Codeliste inkl. zurückgezogener Werte), bei Objektarten zusätzlich Relationen/Assoziationen") idx.append("inkl. Gegenrolle auf der Zielklasse, sowie daraus abgeleitete Hinweise für mögliche Testfälle.") idx.append("") idx.append("Für die über die Vererbungskette **aufgelöste** Attributliste einer Klasse (ohne Datei-Hopping):") idx.append(f"`python _werkzeuge/parse_alkis.py resolve `.") idx.append("") idx.append("Für die schlanke Praxis-Übersicht dessen, was `sn_verfahrensgebiet` tatsächlich aus den WFS-Diensten") idx.append("nutzt (inkl. abweichender Feldnamen im Dienst), siehe [`../../alkis_objektarten.md`](../../alkis_objektarten.md).") idx.append("Für den Versionsüberblick (GID6 vs. GID7) siehe [`../INDEX.md`](../INDEX.md).") idx.append("") idx.append("Dateien sind maschinell erzeugt (Skript: `_werkzeuge/parse_alkis.py`) und werden beim erneuten") idx.append("Ausführen vollständig überschrieben -- nicht von Hand bearbeiten.") idx.append("") idx.append("## Objektartenbereiche") idx.append("") for (bk, bname) in sorted(by_bereich.keys(), key=lambda x: x[0]): idx.append(f"### {bname} ({bk})") idx.append("") groups = by_bereich[(bk, bname)] for (gk, gname) in sorted(groups.keys(), key=lambda x: x[0]): idx.append(f"**{gname} ({gk})**") idx.append("") for fname, cls in sorted(groups[(gk, gname)], key=lambda r: r[1]['kennung']): flag = ' _(abstrakt)_' if cls['abstract'] else '' nattr, nassoc = len(cls['attrs']), len(cls['assocs']) idx.append(f"- [{cls['name']}]({fname}) — {cls['kennung']}, {art_of(cls)}{flag}, {nattr} Attribute" + (f", {nassoc} Relationen" if art_of(cls) == 'Objektart' else "")) idx.append("") with open(os.path.join(out_dir, "INDEX.md"), 'w', encoding='utf-8') as f: f.write('\n'.join(idx)) print(f"Index geschrieben: {out_dir}\\INDEX.md") return parsed_classes # --------------------------------------------------------------------------- # CLI: resolve (aufgeloeste Attribute) und diff (Versionsvergleich einer Klasse) # --------------------------------------------------------------------------- def cmd_resolve(src_path, class_name): parsed = parse_catalog(src_path) if class_name not in parsed: print(f"'{class_name}' nicht gefunden in {src_path}") return attrs, assocs = resolve_effective(parsed, class_name) print(f"\n=== Aufgelöste Attribute von {class_name} (inkl. Vererbung) ===") print(f"{'Attribut':30} {'Kennung':8} {'Kard.':8} {'Herkunft':30} Typ") for a in attrs: print(f"{a['name']:30} {a['kennung']:8} {a['cardinality']:8} {a['herkunft']:30} {a['type']}") if assocs: print(f"\n=== Aufgelöste Relationen von {class_name} (inkl. Vererbung) ===") print(f"{'Rolle':30} {'Kennung':15} {'Kard.':8} {'Herkunft':30} Ziel") for a in assocs: print(f"{a['name']:30} {a['kennung']:15} {a['cardinality']:8} {a['herkunft']:30} {a['target']}") def cmd_diff(src_a, src_b, class_name): parsed_a = parse_catalog(src_a) parsed_b = parse_catalog(src_b) if class_name not in parsed_a or class_name not in parsed_b: print(f"Vorhanden in A ({os.path.basename(src_a)}): {class_name in parsed_a}") print(f"Vorhanden in B ({os.path.basename(src_b)}): {class_name in parsed_b}") return attrs_a, _ = resolve_effective(parsed_a, class_name) attrs_b, _ = resolve_effective(parsed_b, class_name) by_name_a = {a['name']: a for a in attrs_a} by_name_b = {a['name']: a for a in attrs_b} only_a = sorted(set(by_name_a) - set(by_name_b)) only_b = sorted(set(by_name_b) - set(by_name_a)) common = sorted(set(by_name_a) & set(by_name_b)) print(f"\n=== Diff {class_name}: {os.path.basename(src_a)} (A) vs. {os.path.basename(src_b)} (B) ===") if only_a: print(f"\nNur in A: {', '.join(only_a)}") if only_b: print(f"\nNur in B: {', '.join(only_b)}") changed = [] for n in common: a, b = by_name_a[n], by_name_b[n] diffs = [] if a['cardinality'] != b['cardinality']: diffs.append(f"Kardinalität {a['cardinality']} -> {b['cardinality']}") if a['type'] != b['type']: diffs.append(f"Typ '{a['type']}' -> '{b['type']}'") if diffs: changed.append(f"- {n}: {'; '.join(diffs)}") if changed: print("\nGeändert:") print('\n'.join(changed)) if not only_a and not only_b and not changed: print("\nKeine Unterschiede in den aufgelösten Attributen gefunden.") if __name__ == '__main__': if len(sys.argv) < 2: print(__doc__) sys.exit(1) mode = sys.argv[1] if mode == 'generate' and len(sys.argv) == 5: generate(sys.argv[2], sys.argv[3], sys.argv[4]) elif mode == 'resolve' and len(sys.argv) == 4: cmd_resolve(sys.argv[2], sys.argv[3]) elif mode == 'diff' and len(sys.argv) == 5: cmd_diff(sys.argv[2], sys.argv[3], sys.argv[4]) else: print(__doc__) sys.exit(1)