#!/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_.yaml – én fil per maxOccurs > 1 element - config_.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 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 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 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()