ALKIS-Objektartenkatalog als Wissensbasis fuer GID6 und GID7 angelegt

Atomare Markdown-Wissensdateien je Objektart/Datentyp (Beschreibung, Attribute,
Codelisten, Bildungsregeln, Konsistenzbedingungen, Relationen inkl. Gegenrolle),
maschinell erzeugt aus den ADV-Katalog-XML unter referenz/alkis/ (unversioniert).

Zwei getrennte Baeume, weil operativ noch GID6 laeuft und GID7 erst der Zielstand
ist: docs/wissen/alkis/gid6/ (189 Klassen, WFS-Produktivstand, fuer Plugin-Code
massgeblich) und docs/wissen/alkis/gid7/ (209 Klassen, kuenftige Umstellung).

Vererbung wird nicht in jede Datei hineinkopiert (Drift-/Tokenrisiko), sondern per
Inline-Hinweis auf die Oberklasse referenziert plus on-demand ueber
_werkzeuge/parse_alkis.py resolve aufgeloest; ein diff-Modus vergleicht eine
Klasse zwischen beiden Versionen. alkis_objektarten.md verweist jetzt auf beide
Versionsbaeume.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 09:55:27 +02:00
co-authored by Claude Sonnet 5
parent ce795cdc6f
commit f41666b3d6
403 changed files with 31066 additions and 0 deletions
+663
View File
@@ -0,0 +1,663 @@
# -*- 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/<version>/):
python parse_alkis.py generate gid7 "AAA-Anwendungsschema 7.1.2 (Zielstand)" <pfad-zur-xml>
python parse_alkis.py generate gid6 "AAA-Anwendungsschema 6.0.1 (Produktivstand)" <pfad-zur-xml>
Aufgeloeste Attribute einer Klasse ueber die Vererbungskette ausgeben (kein Datei-Hopping noetig):
python parse_alkis.py resolve <pfad-zur-xml> AX_Grenzpunkt
Zwei Versionen fuer eine Klasse vergleichen (nur Unterschiede, berechnet -- nicht gepflegt):
python parse_alkis.py diff <pfad-zur-xml-a> <pfad-zur-xml-b> 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
<bildungsregel><AC_Bildungsregel><regel>...</regel></AC_Bildungsregel></bildungsregel>.
"""
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 <xml> {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 <pfad-zur-xml> <Klassenname>`.")
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)