#!/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_.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()