Files
udpak_semistruktur/xsd_til_yaml_old.py
2026-04-04 20:45:35 +02:00

472 lines
17 KiB
Python
Raw 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 complexTypes
- nøgler.yaml nøgle-placeholders for overliggende unbounded-niveauer
- udtræk_<navn>.yaml én fil per maxOccurs="unbounded" element
Kør med:
python3 xsd_til_yaml.py --xsd bankdata.xsd --output ./yaml_output/
"""
from __future__ import annotations
import os
import argparse
from xml.etree import ElementTree as ET
from dataclasses import dataclass, field
from typing import Optional
# XML Schema namespace
XS = "http://www.w3.org/2001/XMLSchema"
# ============================================================
# Datastrukturer
# ============================================================
@dataclass
class XsdFelt:
"""Repræsenterer ét felt (attribut eller element) i en complexType."""
navn: str
er_attribut: bool
xsd_type: str = "xs:string"
påkrævet: bool = False
@property
def felt_ref(self) -> str:
"""Returnerer felt-referencen som den skrives i YAML."""
return f"@{self.navn}" if self.er_attribut else self.navn
@dataclass
class XsdKompleksType:
"""Repræsenterer en complexType fra XSD."""
navn: str
felter: list[XsdFelt] = field(default_factory=list)
@dataclass
class XsdUdboundElement:
"""Repræsenterer et maxOccurs=unbounded element bliver til en output-fil."""
element_navn: str # fx "postering"
type_navn: str # fx "PosteringType"
rod_sti: str # fx "bank.kunder.kunde.konti.konto.posteringer.postering"
overliggende: list[str] # navne på overliggende unbounded elementer
# fx ["kunde", "konto"]
# ============================================================
# XSD Parser
# ============================================================
class XsdParser:
"""Parser en XSD-fil og udtrækker complexTypes og unbounded elementer."""
def __init__(self, xsd_sti: str):
self.xsd_sti = xsd_sti
self.tree = ET.parse(xsd_sti)
self.root = self.tree.getroot()
# Navngivne complexTypes fra XSD
self.komplekse_typer: dict[str, XsdKompleksType] = {}
# Unbounded elementer med rod-sti og overliggende niveauer
self.unbounded_elementer: list[XsdUdboundElement] = []
def parse(self) -> None:
"""Kør den komplette parsing."""
self._parse_navngivne_typer()
self._find_unbounded_elementer()
def _xs(self, tag: str) -> str:
"""Returnerer fuldt kvalificeret XS-tag."""
return f"{{{XS}}}{tag}"
def _parse_navngivne_typer(self) -> None:
"""Find alle navngivne complexTypes på topniveau."""
for ct in self.root.findall(self._xs("complexType")):
navn = ct.get("name")
if not navn:
continue
felter = self._udpak_felter(ct)
self.komplekse_typer[navn] = XsdKompleksType(navn=navn, felter=felter)
def _udpak_felter(self, ct_node) -> list[XsdFelt]:
"""Udpakker alle felter (attributter + simple elementer) fra en complexType."""
felter = []
# Attributter bliver til @navn i YAML
for attr in ct_node.findall(self._xs("attribute")):
navn = attr.get("name")
xsd_type = attr.get("type", "xs:string")
påkrævet = attr.get("use") == "required"
felter.append(XsdFelt(
navn=navn,
er_attribut=True,
xsd_type=xsd_type,
påkrævet=påkrævet
))
# Sequence-elementer
for seq in ct_node.findall(self._xs("sequence")):
for el in seq.findall(self._xs("element")):
el_type = el.get("type", "xs:string")
el_navn = el.get("name")
max_occ = el.get("maxOccurs", "1")
# Spring komplekse typer og unbounded over de håndteres separat
if max_occ == "unbounded":
continue
if el_type and not el_type.startswith("xs:"):
continue
felter.append(XsdFelt(
navn=el_navn,
er_attribut=False,
xsd_type=el_type or "xs:string",
påkrævet=True
))
return felter
def _find_unbounded_elementer(self) -> None:
"""
Gennemgår XSD rekursivt og finder alle maxOccurs=unbounded elementer.
Bygger rod-stier og registrerer overliggende unbounded niveauer.
"""
# Find rod-elementet
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")
self._traverse(
node=rod_el,
sti=[rod_navn],
overliggende_unbounded=[],
rod_type=rod_type
)
def _hent_type_node(self, type_navn: str):
"""Henter en navngiven complexType-node fra XSD."""
for ct in self.root.findall(self._xs("complexType")):
if ct.get("name") == type_navn:
return ct
return None
def _traverse(
self,
node,
sti: list[str],
overliggende_unbounded: list[str],
rod_type: Optional[str] = None
) -> None:
"""
Rekursiv gennemgang af XSD-strukturen.
Finder unbounded elementer og bygger rod-stier.
"""
# Find complexType-noden at arbejde med
if rod_type:
ct_node = self._hent_type_node(rod_type)
else:
ct_node = node.find(self._xs("complexType"))
if ct_node is None:
return
# Gennemgå sequence-elementer
for seq in ct_node.findall(self._xs("sequence")):
for el in seq.findall(self._xs("element")):
el_navn = el.get("name")
el_type = el.get("type")
max_occ = el.get("maxOccurs", "1")
ny_sti = sti + [el_navn]
if max_occ == "unbounded":
# Dette element bliver en output-fil
rod_sti = ".".join(ny_sti)
self.unbounded_elementer.append(XsdUdboundElement(
element_navn=el_navn,
type_navn=el_type or "",
rod_sti=rod_sti,
overliggende=list(overliggende_unbounded)
))
# Fortsæt rekursivt med dette som nyt overliggende niveau
self._traverse(
node=el,
sti=ny_sti,
overliggende_unbounded=overliggende_unbounded + [el_navn],
rod_type=el_type
)
else:
# Ikke unbounded fortsæt ned hvis det er en kompleks type
if el_type and not el_type.startswith("xs:"):
self._traverse(
node=el,
sti=ny_sti,
overliggende_unbounded=overliggende_unbounded,
rod_type=el_type
)
# ============================================================
# Hjælpefunktion: XSD type → YAML type
# ============================================================
def xsd_type_til_yaml(xsd_type: str) -> dict:
"""Konverterer en XSD-type til YAML kolonne-attributter."""
mapping = {
"xs:string": {},
"xs:decimal": {"type": "decimal", "decimaler": 2},
"xs:integer": {"type": "integer"},
"xs:int": {"type": "integer"},
"xs:boolean": {"type": "boolean"},
"xs:date": {"type": "date", "dato_ind": "%Y-%m-%d", "dato_ud": "%d-%m-%Y"},
"xs:dateTime": {"type": "date", "dato_ind": "%Y-%m-%dT%H:%M:%S", "dato_ud": "SYBASE"},
}
return mapping.get(xsd_type, {})
# ============================================================
# CLI
# ============================================================
def byg_argument_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Genererer YAML-skelet fra XSD til udpak_semistruktur."
)
parser.add_argument("--xsd", required=True, help="Sti til XSD-filen")
parser.add_argument("--output", required=True, help="Output-mappe til YAML-filer")
parser.add_argument("--prefix", default="udtræk",
help="Prefix til udtræk-filnavne (standard: udtræk)")
return parser
# ============================================================
# YAML Generatorer
# ============================================================
def _yaml_type_linjer(xsd_type: str, indryk: str) -> list[str]:
"""Returnerer YAML-linjer for typekonvertering baseret på XSD-type."""
attrs = xsd_type_til_yaml(xsd_type)
linjer = []
for k, v in attrs.items():
if isinstance(v, str):
linjer.append(f'{indryk}{k}: "{v}"')
else:
linjer.append(f'{indryk}{k}: {v}')
return linjer
def generer_skabeloner_yaml(xsd: XsdParser, output_mappe: str) -> None:
"""Genererer skabeloner.yaml med feltskabeloner for alle relevante complexTypes."""
linjer = []
linjer.append("# =============================================================================")
linjer.append("# skabeloner.yaml")
linjer.append("#")
linjer.append("# Auto-genereret feltskabeloner fra XSD.")
linjer.append("# Relative feltnavne brug prefix_felt naar skabelonen anvendes.")
linjer.append("# =============================================================================")
linjer.append("")
linjer.append("kolonne_skabeloner:")
linjer.append("")
relevante = {navn: kt for navn, kt in xsd.komplekse_typer.items() if kt.felter}
for type_navn, kt in relevante.items():
skabelon_navn = type_navn.replace("Type", "").lower()
linjer.append(f" # Felter fra {type_navn}")
linjer.append(f" {skabelon_navn}:")
for felt in kt.felter:
linjer.append(f" - navn: {felt.navn}")
linjer.append(f' felt: "{felt.felt_ref}"')
for type_linje in _yaml_type_linjer(felt.xsd_type, " "):
linjer.append(type_linje)
if felt.påkrævet:
linjer.append(f" påkrævet: true")
linjer.append("")
sti = os.path.join(output_mappe, "skabeloner.yaml")
with open(sti, "w", encoding="utf-8") as f:
f.write("\n".join(linjer))
print(f" Skrev: {sti}")
def generer_nøgler_yaml(xsd: XsdParser, output_mappe: str) -> None:
"""Genererer nøgler.yaml med placeholders for forretningsnøgler."""
overliggende_niveauer = set()
for el in xsd.unbounded_elementer:
for ov in el.overliggende:
overliggende_niveauer.add(ov)
if not overliggende_niveauer:
print(" Ingen overliggende niveauer nøgler.yaml ikke genereret.")
return
linjer = []
linjer.append("# =============================================================================")
linjer.append("# nøgler.yaml")
linjer.append("#")
linjer.append("# Nøgle-skabeloner der binder output-filer til overliggende niveauer.")
linjer.append("# UDFYLD: Erstat __FELT__ med den korrekte forretningsnøgle.")
linjer.append("# =============================================================================")
linjer.append("")
linjer.append("kolonne_skabeloner:")
linjer.append("")
skrevne = set()
for el in xsd.unbounded_elementer:
if not el.overliggende:
continue
rod_dele = el.rod_sti.split(".")
for ov_navn in el.overliggende:
skabelon_navn = f"{ov_navn}_nøgle"
if skabelon_navn in skrevne:
continue
skrevne.add(skabelon_navn)
try:
idx = rod_dele.index(ov_navn)
ov_rod_sti = ".".join(rod_dele[:idx + 1])
except ValueError:
ov_rod_sti = f"__ROD_STI_TIL_{ov_navn.upper()}__"
linjer.append(f" # Nøgle fra {ov_navn}-niveau")
linjer.append(f" # TODO: Angiv den korrekte forretningsnøgle for {ov_navn}")
linjer.append(f" {skabelon_navn}:")
linjer.append(f" - navn: __FELT__")
linjer.append(" felt: __FELT__ # fx '@id' eller 'nr'")
linjer.append(f" rod: {ov_rod_sti}")
linjer.append("")
sti = os.path.join(output_mappe, "nøgler.yaml")
with open(sti, "w", encoding="utf-8") as f:
f.write("\n".join(linjer))
print(f" Skrev: {sti}")
def generer_udtræk_yaml(xsd: XsdParser, output_mappe: str, prefix: str) -> None:
"""Genererer én udtræk-yaml per unbounded element."""
for el in xsd.unbounded_elementer:
linjer = []
linjer.append("# =============================================================================")
linjer.append(f"# {prefix}_{el.element_navn}.yaml")
linjer.append("#")
linjer.append(f"# Auto-genereret udtræk for {el.element_navn}.")
if el.overliggende:
linjer.append(f"# Udfyld nøgle-skabeloner i nøgler.yaml inden brug.")
linjer.append("# =============================================================================")
linjer.append("")
linjer.append("output_filer:")
linjer.append("")
linjer.append(f" - rod: {el.rod_sti}")
linjer.append(f" kolonner:")
if el.overliggende:
linjer.append(f" # Nøgler fra overliggende niveauer udfyld i nøgler.yaml")
for ov_navn in el.overliggende:
linjer.append(f" - skabelon: {ov_navn}_nøgle")
linjer.append("")
if el.type_navn and el.type_navn in xsd.komplekse_typer:
kt = xsd.komplekse_typer[el.type_navn]
if kt.felter:
skabelon_navn = el.type_navn.replace("Type", "").lower()
linjer.append(f" # Felter fra {el.element_navn} se skabeloner.yaml")
linjer.append(f" - skabelon: {skabelon_navn}")
linjer.append("")
linjer.append(f" outputs:")
linjer.append(f" - fil_navn: \"{el.element_navn}_{{yyyy}}{{mm}}{{dd}}.txt\"")
linjer.append(f" overskrifter: true")
linjer.append("")
fil_navn = f"{prefix}_{el.element_navn}.yaml"
sti = os.path.join(output_mappe, fil_navn)
with open(sti, "w", encoding="utf-8") as f:
f.write("\n".join(linjer))
print(f" Skrev: {sti}")
def generer_hoved_config(xsd: XsdParser, output_mappe: str, prefix: str) -> None:
"""Genererer en hoved-config der inkluderer alle genererede filer."""
xsd_basis = os.path.splitext(os.path.basename(xsd.xsd_sti))[0]
linjer = []
linjer.append("# =============================================================================")
linjer.append(f"# config_{xsd_basis}.yaml")
linjer.append("#")
linjer.append("# Auto-genereret hoved-config. Tilpas config-sektionen.")
linjer.append("# =============================================================================")
linjer.append("")
linjer.append("include:")
linjer.append(" - skabeloner.yaml")
linjer.append(" - nøgler.yaml")
for el in xsd.unbounded_elementer:
linjer.append(f" - {prefix}_{el.element_navn}.yaml")
linjer.append("")
linjer.append("config:")
linjer.append(f" input_fil: {xsd_basis}.xml")
linjer.append(" output_path: __OUTPUT_PATH__")
linjer.append(" logfil: log_{yyyy}{mm}{dd}.txt")
linjer.append(" log_niveau: info")
linjer.append(" log_output: begge")
linjer.append(" encoding: utf-8")
linjer.append(" separator: \"\\t\"")
linjer.append(" skrivetilstand: w")
linjer.append(" dato_ind: \"%Y-%m-%d\"")
linjer.append(" dato_ud: \"%d-%m-%Y\"")
fil_navn = f"config_{xsd_basis}.yaml"
sti = os.path.join(output_mappe, fil_navn)
with open(sti, "w", encoding="utf-8") as f:
f.write("\n".join(linjer))
print(f" Skrev: {sti}")
def main():
parser = byg_argument_parser()
args = parser.parse_args()
if not os.path.exists(args.xsd):
print(f"FEJL: XSD-filen '{args.xsd}' findes ikke.")
return
os.makedirs(args.output, exist_ok=True)
print(f"Parser XSD: {args.xsd}")
xsd = XsdParser(args.xsd)
xsd.parse()
print(f"\nFandt {len(xsd.komplekse_typer)} komplekse typer:")
for navn in xsd.komplekse_typer:
kt = xsd.komplekse_typer[navn]
print(f" {navn}: {len(kt.felter)} felter")
print(f"\nFandt {len(xsd.unbounded_elementer)} unbounded elementer:")
for el in xsd.unbounded_elementer:
print(f" {el.element_navn} ({el.rod_sti})")
if el.overliggende:
print(f" Overliggende: {el.overliggende}")
print(f"\nGenererer YAML-filer i: {args.output}")
generer_skabeloner_yaml(xsd, args.output)
generer_nøgler_yaml(xsd, args.output)
generer_udtræk_yaml(xsd, args.output, args.prefix)
generer_hoved_config(xsd, args.output, args.prefix)
print("\nFærdig! Husk at:")
print(" 1. Udfyld nøgler.yaml med de korrekte forretningsnøgler")
print(" 2. Ret output_path i hoved-config")
print(" 3. Tjek at rod-stier passer til din XML-struktur")
if __name__ == "__main__":
main()