Files
udpak_semistruktur/xsd_til_yaml.py
2026-04-07 03:56:43 +02:00

625 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
xsd_til_yaml.py
Genererer YAML-skelet fra en XSD-fil til brug med udpak_semistruktur.
Producerer:
- skabeloner.yaml feltskabeloner for alle navngivne complexTypes
- nøgler.yaml nøgle-placeholders for overliggende liste-niveauer
- udtræk_<navn>.yaml én fil per maxOccurs > 1 element
- config_<navn>.yaml hoved-config der inkluderer alle filer
Kør med:
python3 xsd_til_yaml.py --config xsd_til_yaml_config.yaml
"""
from email import parser
import os
import argparse
from xml.etree import ElementTree as ET
from generators.schema_dataklasser import XsdFelt, XsdSkabelonRef, XsdKompleksType, XsdListeElement
XS_NS = "http://www.w3.org/2001/XMLSchema"
# ============================================================
# Fase 1: Hjælpefunktioner
# ============================================================
def _byg_choice_søskende_index(liste_elementer: list) -> dict[str, list[str]]:
"""
Returnerer dict: element_navn → liste af rod_stier
for elementer der forekommer i flere choice-grene.
"""
fra_navn: dict[str, list[str]] = {}
for el in liste_elementer:
fra_navn.setdefault(el.element_navn, []).append(el.rod_sti)
# Behold kun dem der har mere end én sti
return {navn: stier for navn, stier in fra_navn.items() if len(stier) > 1}
def _find_namespaces(xsd_sti: str) -> dict[str, str]:
"""Læser alle namespace-deklarationer fra XSD-filen."""
namespaces = {}
for event, elem in ET.iterparse(xsd_sti, events=["start-ns"]):
prefix, uri = elem
namespaces[prefix] = uri
return namespaces
def _byg_xs_tag(lokalt_navn: str) -> str:
"""Bygger fuldt kvalificeret XS-tag."""
return f"{{{XS_NS}}}{lokalt_navn}"
def _strip_prefix(type_navn: str) -> str:
"""
Fjerner namespace-præfiks fra et typenavn.
'carf:PersonParty_Type''PersonParty_Type'
'xsd:string''xsd:string' (bevares)
"""
if not type_navn or ":" not in type_navn:
return type_navn
prefix, lokalt = type_navn.split(":", 1)
if prefix in ("xs", "xsd"):
return type_navn
return lokalt
def _er_xs_type(type_navn: str) -> bool:
"""True hvis typen er en XSD-standardtype."""
if not type_navn:
return False
return type_navn.startswith("xs:") or type_navn.startswith("xsd:")
def _er_liste(max_occ: str) -> bool:
"""True hvis maxOccurs > 1."""
if max_occ == "unbounded":
return True
try:
return int(max_occ) > 1
except (ValueError, TypeError):
return False
def _type_til_skabelon_navn(type_navn: str) -> str:
"""
'PersonParty_Type''personparty'
'carf:Address_Type''address'
"""
lokalt = _strip_prefix(type_navn)
return lokalt.replace("_Type", "").replace("Type", "").lower()
# ============================================================
# Fase 2: Parser
# ============================================================
class XsdParser:
def __init__(self, xsd_sti: str):
self.xsd_sti = xsd_sti
self.tree = ET.parse(xsd_sti)
self.root = self.tree.getroot()
self.namespaces = _find_namespaces(xsd_sti)
self.ns_url_til_prefix = {v: k for k, v in self.namespaces.items()}
self.target_ns = self.root.get("targetNamespace", "")
self.komplekse_typer: dict[str, XsdKompleksType] = {}
self.liste_elementer: list[XsdListeElement] = []
self.choice_elementer: list = [] # ← tilføj denne linje
def _xs(self, navn: str) -> str:
return _byg_xs_tag(navn)
def _er_intern_type(self, type_navn: str) -> bool:
if not type_navn or _er_xs_type(type_navn):
return False
if ":" not in type_navn:
return True
prefix = type_navn.split(":")[0]
return self.namespaces.get(prefix, "") == self.target_ns
def parse(self, min_felter: int = 1) -> None:
self._registrer_eksterne_typer()
self._parse_alle_komplekse_typer()
self._find_liste_elementer(min_felter)
# Kør skabelon-parsing igen nu vi ved hvilke elementer der har egne filer
self._parse_alle_komplekse_typer()
self._registrer_choice_elementer()
def _registrer_eksterne_typer(self) -> None:
for import_node in self.root.findall(self._xs("import")):
ns_url = import_node.get("namespace", "")
schema_sti = import_node.get("schemaLocation", "")
if schema_sti:
import_sti = os.path.join(
os.path.dirname(self.xsd_sti), schema_sti
)
if os.path.exists(import_sti):
self._parse_importeret_fil(import_sti)
continue
prefix = self.ns_url_til_prefix.get(ns_url, "")
print(f" Info: Ekstern namespace '{prefix}' ({ns_url}) "
f"kan ikke læse felter")
def _parse_importeret_fil(self, sti: str) -> None:
try:
root = ET.parse(sti).getroot()
except Exception as e:
print(f" Advarsel: Kunne ikke parse '{sti}': {e}")
return
# Pas 1: registrer alle typenavne
for ct in root.findall(self._xs("complexType")):
navn = ct.get("name")
if navn and navn not in self.komplekse_typer:
self.komplekse_typer[navn] = XsdKompleksType(navn=navn, er_ekstern=False)
# Pas 2: udpak felter to gange så forward-references løses
for _ in range(2):
for ct in root.findall(self._xs("complexType")):
navn = ct.get("name")
if not navn:
continue
kt = self.komplekse_typer[navn]
kt.felter = self._udpak_felter(ct)
if ct.find(self._xs("simpleContent")) is not None:
kt.har_tekst = True
# Omdøb #text til type-navnet
type_basis = navn.replace("_Type", "").replace("Type", "").lower()
for felt in kt.felter:
if isinstance(felt, XsdFelt) and felt.navn == "#text":
print(f" DEBUG omdøb: {navn} #text → {type_basis}, xml_navn={felt.xml_navn}")
felt.xml_navn = "#text"
felt.navn = "value" # ← generisk navn
print(f" DEBUG efter: navn={felt.navn} xml_navn={felt.xml_navn} felt_ref={felt.felt_ref}")
# Rekursive imports
for import_node in root.findall(self._xs("import")):
schema_sti = import_node.get("schemaLocation", "")
if schema_sti:
import_sti = os.path.join(os.path.dirname(sti), schema_sti)
if os.path.exists(import_sti):
self._parse_importeret_fil(import_sti)
def _parse_alle_komplekse_typer(self) -> None:
"""To-pas parsing så forward-references løses."""
# Pas 1: registrer alle typenavne
for ct in self.root.findall(self._xs("complexType")):
navn = ct.get("name")
if navn and navn not in self.komplekse_typer:
self.komplekse_typer[navn] = XsdKompleksType(navn=navn)
# Pas 2: udpak felter to gange så forward-references løses
for _ in range(2):
for ct in self.root.findall(self._xs("complexType")):
navn = ct.get("name")
if not navn:
continue
kt = self.komplekse_typer[navn]
kt.felter = self._udpak_felter(ct)
if ct.find(self._xs("simpleContent")) is not None:
kt.har_tekst = True
# Omdøb #text til type-navnet
type_basis = navn.replace("_Type", "").replace("Type", "").lower()
for felt in kt.felter:
if isinstance(felt, XsdFelt) and felt.navn == "#text":
print(f" DEBUG omdøb: {navn} #text → {type_basis}, xml_navn={felt.xml_navn}")
felt.xml_navn = "#text"
felt.navn = "value" # ← generisk navn
print(f" DEBUG efter: navn={felt.navn} xml_navn={felt.xml_navn} felt_ref={felt.felt_ref}")
def _udpak_felter(self, ct_node) -> list:
"""Koordinator detekterer XSD-mønster og kalder rette hjælpefunktion."""
simple = ct_node.find(self._xs("simpleContent"))
if simple is not None:
return self._udpak_simple_content(simple)
complex_content = ct_node.find(self._xs("complexContent"))
if complex_content is not None:
return self._udpak_complex_content(complex_content)
felter = self._udpak_sequence_eller_choice(ct_node)
felter.extend(self._udpak_attributter(ct_node))
return felter
def _udpak_attributter(self, node) -> list:
felter = []
for attr in node.findall(self._xs("attribute")):
navn = attr.get("name")
if not navn:
continue
felter.append(XsdFelt(
navn=navn,
er_attribut=True,
xsd_type=attr.get("type", "xsd:string"),
påkrævet=attr.get("use") == "required"
))
return felter
def _udpak_simple_content(self, simple_node, element_navn: str = "#text") -> list:
felter = []
ext = simple_node.find(self._xs("extension"))
if ext is None:
ext = simple_node.find(self._xs("restriction"))
if ext is not None:
felter.append(XsdFelt(
navn=element_navn, # ← brug element_navn i stedet for #text
xsd_type=ext.get("base", "xsd:string"),
påkrævet=True,
er_tekst=True
))
felter.extend(self._udpak_attributter(ext))
return felter
def _udpak_complex_content(self, complex_node) -> list:
"""Arv via extension/restriction."""
felter = []
node = complex_node.find(self._xs("extension"))
if node is None:
node = complex_node.find(self._xs("restriction"))
if node is None:
return felter
base_navn = _strip_prefix(node.get("base", ""))
if base_navn and base_navn in self.komplekse_typer:
felter.extend(self.komplekse_typer[base_navn].felter)
felter.extend(self._udpak_sequence_eller_choice(node))
felter.extend(self._udpak_attributter(node))
return felter
def _udpak_sequence_eller_choice(self, node, prefix: str = "") -> list:
felter = []
for seq in node.findall(self._xs("sequence")):
felter.extend(self._udpak_container(seq, prefix, er_choice=False))
for choice in node.findall(self._xs("choice")):
felter.extend(self._udpak_container(choice, prefix, er_choice=True))
return felter
def _udpak_container(self, container, prefix: str = "", er_choice: bool = False) -> list:
"""Udpakker elementer fra én sequence eller choice."""
felter = []
for el in container.findall(self._xs("element")):
el_navn = el.get("name")
el_type = el.get("type", "")
max_occ = el.get("maxOccurs", "1")
min_occ = el.get("minOccurs", "1")
er_liste = _er_liste(max_occ)
påkrævet = min_occ != "0"
if not el_navn:
continue
fuldt_navn = f"{prefix}.{el_navn}" if prefix else el_navn
# Navngiven kompleks type → XsdSkabelonRef
if el_type and not _er_xs_type(el_type):
type_navn_rent = _strip_prefix(el_type)
kt = self.komplekse_typer.get(type_navn_rent)
if kt and not kt.er_ekstern:
felter.append(XsdSkabelonRef(
element_navn=fuldt_navn,
skabelon_navn=_type_til_skabelon_navn(el_type),
er_liste=er_liste,
påkrævet=påkrævet,
er_choice=er_choice
))
else:
# Ekstern eller ukendt → simpelt felt
felter.append(XsdFelt(
navn=fuldt_navn,
xsd_type=el_type,
påkrævet=påkrævet,
er_liste=er_liste,
er_choice=er_choice
))
continue
# Anonym inline complexType
inline_ct = el.find(self._xs("complexType"))
if inline_ct is not None:
sc = inline_ct.find(self._xs("simpleContent"))
if sc is not None:
# simpleContent elementnavnet som prefix
for felt in self._udpak_simple_content(sc):
if felt.navn == "#text":
felt.navn = fuldt_navn # ← brug element-navnet
felt.er_tekst = False
else:
felt.navn = f"{fuldt_navn}_{felt.navn}"
felt.er_tekst = False
felter.append(felt)
else:
felter.extend(self._udpak_sequence_eller_choice(inline_ct, prefix=fuldt_navn))
felter.extend(self._udpak_attributter(inline_ct))
continue
# Simpelt xs:-element
felter.append(XsdFelt(
navn=fuldt_navn,
xsd_type=el_type or "xsd:string",
påkrævet=påkrævet,
er_liste=er_liste,
er_choice=er_choice
))
# Nested containers
for nested_seq in container.findall(self._xs("sequence")):
felter.extend(self._udpak_container(nested_seq, prefix, er_choice))
for nested_choice in container.findall(self._xs("choice")):
felter.extend(self._udpak_container(nested_choice, prefix, er_choice=True))
return felter
def _find_liste_elementer(self, min_felter: int = 1) -> None:
rod_el = self.root.find(self._xs("element"))
if rod_el is None:
return
rod_navn = rod_el.get("name", "rod")
rod_type = rod_el.get("type", "")
# Registrer rod-elementet selv som udtræk-punkt
rod_inline_ct = rod_el.find(self._xs("complexType"))
if rod_type:
inline_felter = []
elif rod_inline_ct is not None:
inline_felter = self._udpak_felter(rod_inline_ct)
else:
inline_felter = []
self.liste_elementer.append(XsdListeElement(
element_navn=rod_navn,
type_navn=rod_type,
rod_sti=rod_navn,
overliggende=[],
felter=inline_felter,
er_simpel=False,
join_i_skabelon=""
))
# Traverser resten af XSD-træet
self._traverser(
node=rod_el,
sti=[rod_navn],
overliggende=[],
type_navn=rod_type,
min_felter=min_felter
)
def _traverser(self, node, sti, overliggende, type_navn="", min_felter=1) -> None:
ct_node = self._hent_ct_node(node, type_navn)
if ct_node is None:
return
for tag in [self._xs("sequence"), self._xs("choice")]:
for container in ct_node.findall(tag):
self._traverser_container(container, sti, overliggende, min_felter)
def _hent_ct_node(self, node, type_navn: str):
if type_navn:
navn_rent = _strip_prefix(type_navn)
for ct in self.root.findall(self._xs("complexType")):
if ct.get("name") == navn_rent:
return ct
return None
return node.find(self._xs("complexType"))
def _traverser_container(self, container, sti, overliggende, min_felter) -> None:
for el in container.findall(self._xs("element")):
el_navn = el.get("name")
el_type = el.get("type", "")
max_occ = el.get("maxOccurs", "1")
if not el_navn:
continue
ny_sti = sti + [el_navn]
if _er_liste(max_occ):
rod_sti = ".".join(ny_sti)
inline_felter = []
inline_ct = el.find(self._xs("complexType"))
if inline_ct is not None and not el_type:
inline_felter = self._udpak_felter(inline_ct)
antal = self._tæl_felter(el_type, inline_felter)
er_simpel = antal < min_felter
join_i = self._find_join_skabelon(overliggende) if er_simpel else ""
self.liste_elementer.append(XsdListeElement(
element_navn=el_navn,
type_navn=el_type,
rod_sti=rod_sti,
overliggende=list(overliggende),
felter=inline_felter,
er_simpel=er_simpel,
join_i_skabelon=join_i
))
self._traverser(el, ny_sti, overliggende + [el_navn], el_type, min_felter)
else:
inline_ct = el.find(self._xs("complexType"))
if el_type and not _er_xs_type(el_type):
self._traverser(el, ny_sti, overliggende, el_type, min_felter)
elif inline_ct is not None:
self._traverser(el, ny_sti, overliggende, "", min_felter)
for tag in [self._xs("sequence"), self._xs("choice")]:
for nested in container.findall(tag):
self._traverser_container(nested, sti, overliggende, min_felter)
def _tæl_felter(self, type_navn: str, inline_felter: list) -> int:
if inline_felter:
return len(inline_felter)
if not type_navn or _er_xs_type(type_navn):
return 1
kt = self.komplekse_typer.get(_strip_prefix(type_navn))
if kt:
if kt.er_ekstern:
navn = _strip_prefix(type_navn)
if any(s in navn for s in ("String", "Code", "Enum")) and \
not any(s in navn for s in ("Party", "Person", "Organisation", "Address")):
return 1
return 99
return len(kt.felter)
return 1
def _find_join_skabelon(self, overliggende: list[str]) -> str:
if not overliggende:
return ""
nærmeste = overliggende[-1]
for el in self.liste_elementer:
if el.element_navn == nærmeste and el.type_navn:
return _type_til_skabelon_navn(el.type_navn)
return nærmeste.lower()
def _registrer_choice_elementer(self) -> None:
"""Finder alle choice-elementer i liste-elementerne."""
from generators.schema_dataklasser import XsdChoiceElement
for liste_el in self.liste_elementer:
type_navn_rent = _strip_prefix(liste_el.type_navn) if liste_el.type_navn else ""
kt = self.komplekse_typer.get(type_navn_rent)
if not kt or not kt.felter:
continue
# Find alle choice-grupper i skabelonen
choice_refs = [f for f in kt.felter
if isinstance(f, XsdSkabelonRef) and f.er_choice]
if not choice_refs:
continue
# Grupper efter element_navn's rod-del (fx 'UserID')
grupper: dict[str, list] = {}
for ref in choice_refs:
rod = ref.element_navn.split(".")[0] if "." in ref.element_navn else ref.element_navn
grupper.setdefault(rod, []).append(ref)
for rod_navn, refs in grupper.items():
alternativer = []
for ref in refs:
alt_navn = ref.element_navn.split(".")[-1] if "." in ref.element_navn else ref.element_navn
alt_sti = f"{liste_el.rod_sti}.{ref.element_navn}"
alternativer.append((alt_navn, ref.skabelon_navn, alt_sti))
self.choice_elementer.append(XsdChoiceElement(
element_navn=rod_navn,
rod_sti=liste_el.rod_sti,
overliggende=liste_el.overliggende + [liste_el.element_navn]
if liste_el.overliggende is not None else [liste_el.element_navn],
alternativer=alternativer
))
# ============================================================
# CLI og main
# ============================================================
def læs_config_fil(config_sti: str) -> dict:
import yaml
if not os.path.exists(config_sti):
raise FileNotFoundError(f"Config-filen '{config_sti}' findes ikke.")
with open(config_sti, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def byg_argument_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Genererer YAML-skelet fra XSD til udpak_semistruktur."
)
parser.add_argument("--config", help="Sti til xsd_til_yaml_config.yaml")
parser.add_argument("--xsd", help="Sti til XSD-filen")
parser.add_argument("--output", help="Output-mappe til YAML-filer")
parser.add_argument("--prefix", help="Prefix til udtræk-filnavne (standard: udtræk)")
parser.add_argument("--min_felter", type=int,
help="Elementer med færre felter foldes ind som join (standard: 1)")
parser.add_argument("--db_schema", help="Database schema (standard: dbo)")
parser.add_argument("--tabel_prefix",help="Prefix til tabelnavne")
parser.add_argument("--output_type", help="Output-type: fil, tabel eller begge (standard: begge)")
parser.add_argument("--historik", help="Historik-type: t1 eller t2 (standard: t1)")
return parser
def main():
parser = byg_argument_parser()
args = parser.parse_args()
cfg = {}
if args.config:
cfg = læs_config_fil(args.config)
xsd_sti = args.xsd or cfg.get("xsd")
output = args.output or cfg.get("output")
prefix = args.prefix or cfg.get("prefix", "udtræk")
min_felter = args.min_felter or cfg.get("min_felter", 1)
forkortelser = cfg.get("forkortelser", {})
db_schema = args.db_schema or cfg.get("db_schema", "dbo")
tabel_prefix = args.tabel_prefix or cfg.get("tabel_prefix", "")
output_type = args.output_type or cfg.get("output_type", "begge")
historik = args.historik or cfg.get("historik", "t1")
if not xsd_sti:
print("FEJL: 'xsd' skal angives enten i config-fil eller med --xsd")
return
if not output:
print("FEJL: 'output' skal angives enten i config-fil eller med --output")
return
if not os.path.exists(xsd_sti):
print(f"FEJL: XSD-filen '{xsd_sti}' findes ikke.")
return
os.makedirs(output, exist_ok=True)
print(f"Parser XSD: {xsd_sti}")
if forkortelser:
print(f"Forkortelser: {len(forkortelser)} defineret")
xsd = XsdParser(xsd_sti)
xsd.parse(min_felter=min_felter)
print(f"\nFandt {len(xsd.komplekse_typer)} komplekse typer:")
for navn, kt in xsd.komplekse_typer.items():
ekstern = " (ekstern)" if kt.er_ekstern else ""
print(f" {navn}{ekstern}: {len(kt.felter)} felter")
print(f"\nFandt {len(xsd.liste_elementer)} liste-elementer:")
for el in xsd.liste_elementer:
simpel = f" [join → {el.join_i_skabelon}]" if el.er_simpel else ""
print(f" {el.element_navn}{simpel} ({el.rod_sti})")
if el.overliggende:
print(f" Overliggende: {el.overliggende}")
print(f"\nGenererer YAML-filer i: {output}")
from generators.yaml_generator import (
generer_skabeloner_yaml,
generer_nøgler_yaml,
generer_udtræk_yaml,
generer_choice_udtræk_yaml,
generer_hoved_config
)
liste_navne = {el.element_navn for el in xsd.liste_elementer}
generer_skabeloner_yaml(
xsd.komplekse_typer, output, forkortelser, liste_navne, xsd.liste_elementer
)
generer_nøgler_yaml(xsd.liste_elementer, output, forkortelser)
generer_udtræk_yaml(
xsd.liste_elementer, xsd.komplekse_typer,
output, prefix, min_felter, forkortelser,
db_schema, tabel_prefix, output_type, historik
)
generer_choice_udtræk_yaml(
xsd.choice_elementer,
xsd.komplekse_typer,
xsd.liste_elementer,
output, prefix, forkortelser,
db_schema, tabel_prefix,
output_type, historik
)
generer_hoved_config(
xsd.liste_elementer, xsd_sti, output, prefix, forkortelser
)
print("\nFærdig.")
if __name__ == "__main__":
main()