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>
664 lines
31 KiB
Python
664 lines
31 KiB
Python
# -*- 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)
|