diff --git a/Manual/.~lock.manual_kap10-12.odt# b/Manual/.~lock.manual_kap10-12.odt# deleted file mode 100644 index 519a26c..0000000 --- a/Manual/.~lock.manual_kap10-12.odt# +++ /dev/null @@ -1 +0,0 @@ -,carsten,carsten-mint,04.04.2026 02:27,file:///home/carsten/.config/libreoffice/4; \ No newline at end of file diff --git a/Manual/.~lock.manual_kap13-16.odt# b/Manual/.~lock.manual_kap13-16.odt# deleted file mode 100644 index 6dc4e3b..0000000 --- a/Manual/.~lock.manual_kap13-16.odt# +++ /dev/null @@ -1 +0,0 @@ -,carsten,carsten-mint,04.04.2026 02:40,file:///home/carsten/.config/libreoffice/4; \ No newline at end of file diff --git a/Manual/.~lock.manual_kap13-16_v2.odt# b/Manual/.~lock.manual_kap13-16_v2.odt# deleted file mode 100644 index d68957e..0000000 --- a/Manual/.~lock.manual_kap13-16_v2.odt# +++ /dev/null @@ -1 +0,0 @@ -,carsten,carsten-mint,04.04.2026 02:53,file:///home/carsten/.config/libreoffice/4; \ No newline at end of file diff --git a/Manual/.~lock.manual_kap15_v3.odt# b/Manual/.~lock.manual_kap15_v3.odt# deleted file mode 100644 index 932ab2d..0000000 --- a/Manual/.~lock.manual_kap15_v3.odt# +++ /dev/null @@ -1 +0,0 @@ -,carsten,carsten-mint,04.04.2026 15:42,file:///home/carsten/.config/libreoffice/4; \ No newline at end of file diff --git a/output.txt b/output.txt new file mode 100644 index 0000000..1e6dea6 --- /dev/null +++ b/output.txt @@ -0,0 +1,34 @@ +Parser XSD: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/data/bankdata.xsd + +Fandt 10 komplekse typer: + ValutakurserType: 1 felter + ValutaType: 3 felter + KunderType: 1 felter + KundeType: 8 felter + AdresseType: 4 felter + KontaktType: 2 felter + KontiType: 1 felter + KontoType: 7 felter + PosteringerType: 1 felter + PosteringType: 4 felter + +Fandt 5 liste-elementer: + bank (bank) + valuta (bank.valutakurser.valuta) + kunde (bank.kunder.kunde) + konto (bank.kunder.kunde.konti.konto) + Overliggende: ['kunde'] + postering (bank.kunder.kunde.konti.konto.posteringer.postering) + Overliggende: ['kunde', 'konto'] + +Genererer YAML-filer i: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/ + Skrev: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/skabeloner.yaml + Skrev: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/nøgler.yaml + Skrev: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/udtræk_bank.yaml + Skrev: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/udtræk_valutakurser_valuta.yaml + Skrev: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/udtræk_kunder_kunde.yaml + Skrev: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/udtræk_kunder_kunde_konti_konto.yaml + Skrev: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/udtræk_kunder_kunde_konti_konto_posteringer_postering.yaml + Skrev: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/config_bankdata.yaml + +Færdig. diff --git a/schema_dataklasser.py b/schema_dataklasser.py new file mode 100644 index 0000000..cc2f2aa --- /dev/null +++ b/schema_dataklasser.py @@ -0,0 +1,111 @@ +from __future__ import annotations +from dataclasses import dataclass, field + +# ============================================================ +# Datastrukturer +# ============================================================ +@dataclass +class XsdChoiceElement: + element_navn: str + rod_sti: str + overliggende: list[str] + alternativer: list[tuple[str, str, str]] # (alt_navn, skabelon_navn, fuld_sti) + +@dataclass +class XsdFelt: + """ + Repræsenterer ét felt i en complexType – enten et element eller en attribut. + + navn: Feltets navn, fx 'FirstName' eller 'currCode' + er_attribut: True hvis det er en XML-attribut (@navn i YAML) + xsd_type: Den originale XSD-type, fx 'xsd:string', 'xsd:date' + påkrævet: True hvis feltet er obligatorisk + er_liste: True hvis feltet kan forekomme flere gange (maxOccurs > 1) + er_tekst: True hvis feltet er et simpleContent-element – tekstindholdet + er selve værdien og attributterne er metadata (fx MonAmnt_Type) + er_choice: True hvis feltet er ét alternativ i en choice + """ + navn: str + er_attribut: bool = False + xsd_type: str = "xsd:string" + påkrævet: bool = False + er_liste: bool = False + er_tekst: bool = False + er_choice: bool = False + xml_navn: str = "" # ← originalt XML-navn, sættes hvis navn omdøbes + + @property + def felt_ref(self) -> str: + # Brug xml_navn hvis det er sat (dvs. navn er omdøbt) + ref_navn = self.xml_navn if self.xml_navn else self.navn + return f"@{ref_navn}" if self.er_attribut else ref_navn + + @property + def yaml_navn(self) -> str: + return self.navn + + +@dataclass +class XsdSkabelonRef: + """ + Repræsenterer en reference til en anden navngiven complexType. + Bliver til '- skabelon: xxx' i YAML. + + element_navn: Elementnavnet i XML, fx 'Name', 'Address' + skabelon_navn: Det afledte skabelonnavn, fx 'nameperson', 'address' + er_liste: True hvis maxOccurs > 1 + påkrævet: True hvis minOccurs != 0 + er_choice: True hvis elementet er ét alternativ i en choice + """ + element_navn: str + skabelon_navn: str + er_liste: bool = False + påkrævet: bool = False + er_choice: bool = False + + @property + def prefix_felt(self) -> str: + return self.element_navn + + @property + def prefix_navn(self) -> str: + navn = self.element_navn.lower() + return f"{navn.replace('.', '_').replace('-', '_')}_" + + +@dataclass +class XsdKompleksType: + """ + Repræsenterer en navngiven complexType – bliver til en skabelon. + + navn: Typens navn, fx 'PersonParty_Type' + felter: Liste af XsdFelt og/eller XsdSkabelonRef + er_ekstern: True hvis typen kommer fra en importeret XSD-fil + har_tekst: True hvis typen bruger simpleContent (fx MonAmnt_Type) + """ + navn: str + felter: list[XsdFelt | XsdSkabelonRef] = field(default_factory=list) + er_ekstern: bool = False + har_tekst: bool = False + + +@dataclass +class XsdListeElement: + """ + Repræsenterer et maxOccurs > 1 element – bliver til en udtræk-fil. + + element_navn: Elementets navn, fx 'CryptoUsers' + type_navn: Navngiven complexType, fx 'CryptoUsers_Type' + rod_sti: Fuld dot-separeret sti, fx 'CARF_OECD.CARFBody.CryptoUsers' + overliggende: Navne på overliggende liste-elementer + felter: Felter fra anonym inline type (kun hvis type_navn er tom) + er_simpel: True hvis antal felter < min_felter – foldes ind som join + join_i_skabelon: Skabelonnavn der får join-kolonnen + """ + element_navn: str + type_navn: str = "" + rod_sti: str = "" + overliggende: list[str] = field(default_factory=list) + felter: list[XsdFelt | XsdSkabelonRef] = field(default_factory=list) + er_simpel: bool = False + join_i_skabelon: str = "" \ No newline at end of file diff --git a/xsd_til_yaml.py b/xsd_til_yaml.py index e69de29..de79656 100644 --- a/xsd_til_yaml.py +++ b/xsd_til_yaml.py @@ -0,0 +1,625 @@ +#!/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() \ No newline at end of file diff --git a/xsd_til_yaml_config.yaml b/xsd_til_yaml_config.yaml new file mode 100644 index 0000000..1c9af15 --- /dev/null +++ b/xsd_til_yaml_config.yaml @@ -0,0 +1,39 @@ +# xsd_til_yaml_config.yaml +# Konfiguration til xsd_til_yaml.py for CARF DAC8 + +xsd: /home/carsten/Hentet/xml-schema-carf/CARFXML_v1.4.xsd +output: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/ +prefix: udtræk +min_felter: 2 + +# Database schema – bruges til tabel-navne +db_schema: dbo + +# Tabel-navn prefix – fx 'DAC8_' giver 'dbo.DAC8_CARFBody_tmp' +tabel_prefix: DAC8_ + +# Output-type: fil, tabel, eller begge +output_type: fil # fil | tabel | begge + +# Historik-type for tabel-output +historik: t2 # t1 | t2 + + +forkortelser: + CryptoToCryptoIn: CTI + CryptoToCryptoOut: CTO + CryptoFiatIn: CFI + CryptoFiatOut: CFO + CryptoTransferIn: CTRI + CryptoTransferOut: CTRO + TransferWallet: TW + RelevantTransactions: RT + ControllingPerson: CP + NumberofTransactions: NoTrans + NumberofUnits: NumUnits + ExchangeType: ExchType + AltValuation: AltVal + Organisation: Org + Individual: Ind + ResCountryCode: ResCC + CryptoAsset: CAs diff --git a/xsd_til_yaml_config_bank.yaml b/xsd_til_yaml_config_bank.yaml new file mode 100644 index 0000000..57fd829 --- /dev/null +++ b/xsd_til_yaml_config_bank.yaml @@ -0,0 +1,38 @@ +# xsd_til_yaml_config.yaml +# Konfiguration til xsd_til_yaml.py for CARF DAC8 + +xsd: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/data/bankdata.xsd +output: /home/carsten/Dokumenter/GitClone/udpak_semistruktur/tests/bank_test/output/DAC8/ +prefix: udtræk +min_felter: 2 + +# Database schema – bruges til tabel-navne +db_schema: dbo + +# Tabel-navn prefix – fx 'DAC8_' giver 'dbo.DAC8_CARFBody_tmp' +tabel_prefix: bank_ + +# Output-type: fil, tabel, eller begge +output_type: fil # fil | tabel | begge + +# Historik-type for tabel-output +historik: t2 # t1 | t2 + + +#forkortelser: +# CryptoToCryptoIn: CTI +# CryptoToCryptoOut: CTO +# CryptoFiatIn: CFI +# CryptoFiatOut: CFO +# CryptoTransferIn: CTRI +# CryptoTransferOut: CTRO +# TransferWallet: TW +# RelevantTransactions: RT +# ControllingPerson: CP +# NumberofTransactions: NoTrans +# NumberofUnits: NumUnits +# ExchangeType: ExchType +# AltValuation: AltVal +# Organisation: Org +# Individual: Ind +# ResCountryCode: ResCC diff --git a/xsd_til_yaml_old.py b/xsd_til_yaml_old.py deleted file mode 100644 index 406fc37..0000000 --- a/xsd_til_yaml_old.py +++ /dev/null @@ -1,471 +0,0 @@ -#!/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() diff --git a/yaml_generator.py b/yaml_generator.py new file mode 100644 index 0000000..d8462e6 --- /dev/null +++ b/yaml_generator.py @@ -0,0 +1,660 @@ +""" +yaml_generator.py + +Fælles YAML-generering til brug med udpak_semistruktur. +Importeres af xsd_til_yaml.py, jsonschema_til_yaml.py og elastic_til_yaml.py. +""" + +from __future__ import annotations +import os +from schema_dataklasser import XsdFelt, XsdSkabelonRef, XsdKompleksType, XsdListeElement + + +# ============================================================ +# Hjælpefunktioner +# ============================================================ + +def _find_fælles_prefix(liste_elementer: list) -> int: + if not liste_elementer: + return 0 + # Udelad rod-elementet (første element med korteste sti) + alle_stier = [el.rod_sti.split(".") for el in liste_elementer if len(el.rod_sti.split(".")) > 1] + if not alle_stier: + return 0 + korteste = min(len(s) for s in alle_stier) + fælles = 0 + for i in range(korteste): + if len(set(s[i] for s in alle_stier)) == 1: + fælles += 1 + else: + break + return min(fælles, korteste - 1) + + +def _byg_choice_grupper(liste_elementer: list) -> dict[str, dict]: + """ + Grupperer liste-elementer der deler samme rod op til choice-niveauet. + Returnerer dict: rod_sti → { + 'fælles_rod': str, + 'valg': [str], # fx ['Entity', 'Individual'] + 'element_navn': str, # fx 'Address' + 'samlet_rod': str # fx 'CARF_OECD...UserID.[Entity, Individual].Address' + } + """ + from collections import defaultdict + grupper: dict[str, list] = defaultdict(list) + + for el in liste_elementer: + dele = el.rod_sti.split(".") + # Nøglen er: alt undtagen næstsidste led (choice-leddet) + if len(dele) >= 2: + nøgle = ".".join(dele[:-2]) + "." + dele[-1] # fjern choice-leddet + grupper[nøgle].append(el) + + resultat = {} + for nøgle, elementer in grupper.items(): + if len(elementer) > 1: + valg = [el.rod_sti.split(".")[-2] for el in elementer] + fælles_rod = ".".join(elementer[0].rod_sti.split(".")[:-2]) + element_navn = elementer[0].rod_sti.split(".")[-1] + samlet = f"{fælles_rod}.[{', '.join(valg)}].{element_navn}" + for el in elementer: + resultat[el.rod_sti] = { + 'fælles_rod': fælles_rod, + 'valg': valg, + 'element_navn': element_navn, + 'samlet_rod': samlet + } + return resultat + + +def _anvend_forkortelser(navn: str, forkortelser: dict[str, str]) -> str: + """Anvender forkortelser case-insensitivt.""" + resultat = navn + for langt, kort in forkortelser.items(): + resultat = resultat.replace(langt.lower(), kort.lower()) + resultat = resultat.replace(langt, kort) + return resultat + + +def _xsd_type_til_yaml(xsd_type: str) -> dict: + """Konverterer XSD-type til YAML-attributter.""" + t = xsd_type.lower() + if "decimal" in t: + return {"type": "decimal", "decimaler": 2} + if "integer" in t or ("int" in t and "string" not in t): + return {"type": "integer"} + if "boolean" in t: + return {"type": "boolean"} + if "datetime" in t: + return {"type": "date", + "dato_ind": '"%Y-%m-%dT%H:%M:%S"', + "dato_ud": '"SYBASE"'} + if "date" in t: + return {"type": "date", + "dato_ind": '"%Y-%m-%d"', + "dato_ud": '"%d-%m-%Y"'} + return {} + + +def _skriv_felt(felt: XsdFelt, indryk: str, forkortelser: dict, + liste_navne: set = set(), + fil_navne_index: dict = {}) -> list[str]: + # Tjek på felt_ref – ikke navn – da navn kan være forkortet + rod_del = felt.felt_ref.split(".")[0] if "." in felt.felt_ref else "" + er_i_liste_fil = rod_del and rod_del in liste_navne + + linjer = [] + navn = _anvend_forkortelser(felt.yaml_navn, forkortelser) + +# if rod_del: +# print(f" DEBUG: rod_del='{rod_del}' er_i_liste_fil={er_i_liste_fil} liste_navne_sample={list(liste_navne)[:5]}") + + if er_i_liste_fil: + fil_navn = fil_navne_index.get(rod_del, f"udtræk_{rod_del.lower()}.yaml") + linjer.append(f'') + linjer.append(f'{indryk}# OBS: {rod_del} har sin egen udtræk-fil: {fil_navn}') + linjer.append(f'{indryk}# - navn: {navn}') + linjer.append(f'{indryk}# felt: "{felt.felt_ref}"') + else: + linjer.append(f'') + navn_str = f'"{navn}"' if navn.startswith("#") else navn + linjer.append(f'{indryk}- navn: {navn_str}') + linjer.append(f'{indryk} felt: "{felt.felt_ref}"') + for k, v in _xsd_type_til_yaml(felt.xsd_type).items(): + linjer.append(f'{indryk} {k}: {v}') + if felt.påkrævet: + linjer.append(f'{indryk} påkrævet: true') + if felt.er_liste: + linjer.append(f'{indryk} join: true') + linjer.append(f'{indryk} join_separator: ", "') + linjer.append(f'{indryk} # OBS: Kan forekomme flere gange (maxOccurs > 1)') + + return linjer + +def _skriv_skabelon_ref(ref: XsdSkabelonRef, indryk: str, forkortelser: dict, + liste_navne: set = set(), tomme_skabeloner: set = set(), + fil_navne_index: dict = {}) -> list[str]: + linjer = [] + prefix_navn = _anvend_forkortelser(ref.prefix_navn, forkortelser) + + # Tjek om prefix_felt matcher et liste-element – med eller uden punktum + rod_del = ref.prefix_felt.split(".")[0] if "." in ref.prefix_felt else ref.prefix_felt + er_i_liste_fil = rod_del and rod_del in liste_navne + + # Tjek om skabelonen er tom (alle felter udkommenteret) + er_tom_skabelon = ref.skabelon_navn in tomme_skabeloner + + if er_i_liste_fil or er_tom_skabelon: + linjer.append(f'') + if er_i_liste_fil: + fil_navn = fil_navne_index.get(rod_del, f"udtræk_{rod_del.lower()}.yaml") + linjer.append(f'{indryk}# OBS: {rod_del} har sin egen udtræk-fil: {fil_navn}') + else: + linjer.append(f'{indryk}# OBS: {ref.skabelon_navn} skabelonen er tom – alle felter har egne udtræk-filer') + linjer.append(f'{indryk}# - skabelon: {ref.skabelon_navn}') + linjer.append(f'{indryk}# prefix_felt: {ref.prefix_felt}') + linjer.append(f'{indryk}# prefix_navn: "{prefix_navn}"') + return linjer + + if ref.er_choice: + linjer.append(f'') + linjer.append(f'{indryk}# Vælg ét alternativ (choice):') + else: + linjer.append(f'') + linjer.append(f'{indryk}- skabelon: {ref.skabelon_navn}') + linjer.append(f'{indryk} prefix_felt: {ref.prefix_felt}') + linjer.append(f'{indryk} prefix_navn: "{prefix_navn}"') + if ref.er_choice: + linjer.append(f'{indryk} hvis_findes: {ref.prefix_felt}') + if ref.er_liste: + element_basis = ref.element_navn.lower().split(".")[-1] + fil_navn_hint = fil_navne_index.get( + ref.element_navn.split(".")[-1], + f"udtræk_{element_basis}.yaml" + ) + linjer.append(f'{indryk} # OBS: Kan forekomme flere gange (maxOccurs > 1).') + linjer.append(f'{indryk} # Overvej om det skal udtrækkes i en separat fil.') + linjer.append(f'{indryk} # Se evt. {fil_navn_hint}') + return linjer + +def _har_aktive_felter(kt: XsdKompleksType, liste_navne: set[str]) -> bool: + """Returnerer True hvis skabelonen har mindst ét aktivt felt + der ikke er udkommenteret fordi det har sin egen udtræk-fil.""" + return any( + not (isinstance(f, XsdSkabelonRef) and + (f.prefix_felt.split(".")[0] if "." in f.prefix_felt else f.prefix_felt) in liste_navne) + and not (isinstance(f, XsdFelt) and + (f.felt_ref.split(".")[0] if "." in f.felt_ref else "") in liste_navne) + for f in kt.felter + ) + +# ============================================================ +# Generator: skabeloner.yaml +# ============================================================ + +def generer_skabeloner_yaml( + komplekse_typer: dict[str, XsdKompleksType], + output_mappe: str, + forkortelser: dict[str, str] = {}, + liste_navne: set[str] = set(), + liste_elementer: list = [] # ← tilføj +) -> None: + + fælles_prefix_længde = _find_fælles_prefix(liste_elementer) + fil_navne_index: dict[str, str] = {} + for el in liste_elementer: + rod_dele = el.rod_sti.split(".") + unikke_dele = rod_dele[fælles_prefix_længde:] if len(rod_dele) > fælles_prefix_længde else rod_dele + fil_del = _anvend_forkortelser("_".join(unikke_dele).lower(), forkortelser) + # Behold kun første registrering per element-navn + if el.element_navn not in fil_navne_index: + fil_navne_index[el.element_navn] = f"udtræk_{fil_del}.yaml" + + linjer = [] + linjer.append("# =============================================================================") + linjer.append("# skabeloner.yaml") + linjer.append("#") + linjer.append("# Auto-genereret feltskabeloner fra XSD.") + linjer.append("# Brug prefix_felt og prefix_navn når skabelonen inkluderes.") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("kolonne_skabeloner:") + linjer.append("") + +# Byg sæt af tomme skabeloner + tomme_skabeloner = { + kt.navn.replace("_Type", "").replace("Type", "").lower() + for kt in komplekse_typer.values() + if kt.felter and not kt.er_ekstern and not _har_aktive_felter(kt, liste_navne) + } + + relevante = {n: kt for n, kt in komplekse_typer.items() + if kt.felter and not kt.er_ekstern and _har_aktive_felter(kt, liste_navne)} + + for type_navn, kt in relevante.items(): + skabelon_navn = type_navn.replace("_Type", "").replace("Type", "").lower() + + linjer.append(f" # {type_navn}") + if not _har_aktive_felter(kt, liste_navne): + linjer.append(f" {skabelon_navn}: []") + linjer.append("") + continue + + linjer.append(f" {skabelon_navn}:") + for felt in kt.felter: + if isinstance(felt, XsdSkabelonRef): + linjer.extend(_skriv_skabelon_ref( + felt, indryk=" ", forkortelser=forkortelser, + liste_navne=liste_navne, tomme_skabeloner=tomme_skabeloner, + fil_navne_index=fil_navne_index + )) + elif isinstance(felt, XsdFelt): + linjer.extend(_skriv_felt( + felt, indryk=" ", forkortelser=forkortelser, + liste_navne=liste_navne, fil_navne_index=fil_navne_index + )) + 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}") + + +# ============================================================ +# Generator: choice udtræk-filer +# ============================================================ + +def generer_choice_udtræk_yaml( + choice_elementer: list, + komplekse_typer: dict, + liste_elementer: list, + output_mappe: str, + prefix: str = "udtræk", + forkortelser: dict = {}, + db_schema: str = "dbo", + tabel_prefix: str = "", + output_type: str = "begge", + historik: str = "t2" +) -> None: + from schema_dataklasser import XsdChoiceElement, XsdSkabelonRef, XsdFelt + + liste_navne = {el.element_navn for el in liste_elementer} + fælles_prefix_længde = _find_fælles_prefix(liste_elementer) + + for ce in choice_elementer: + # Find den overliggende komplekse type + overliggende_type = None + rod_element_navn = ce.rod_sti.split(".")[-1] + for el in liste_elementer: + if el.element_navn == rod_element_navn and el.type_navn: + type_navn_rent = el.type_navn.split(":")[-1] if ":" in el.type_navn else el.type_navn + overliggende_type = komplekse_typer.get(type_navn_rent) + break + + for alt_navn, alt_skabelon, alt_sti in ce.alternativer: + # Byg filnavn med samme prefix-logik som generer_udtræk_yaml + rod_dele = ce.rod_sti.split(".") + unikke_dele = rod_dele[fælles_prefix_længde:] if len(rod_dele) > fælles_prefix_længde else rod_dele + rod_del_str = _anvend_forkortelser("_".join(unikke_dele).lower(), forkortelser) + alt_forkortet = _anvend_forkortelser(alt_navn.lower(), forkortelser) + fil_del = f"{rod_del_str}_{alt_forkortet}" + fil_navn = f"{prefix}_{fil_del}.yaml" + + hvis_finder_sti = f"{ce.element_navn}.{alt_navn}" + prefix_navn = _anvend_forkortelser( + f"{ce.element_navn.lower()}_{alt_navn.lower()}_".replace(".", "_"), + forkortelser + ) + + linjer = [] + linjer.append("# =============================================================================") + linjer.append(f"# {fil_navn}") + linjer.append("#") + linjer.append(f"# Auto-genereret udtræk for {rod_element_navn} – {alt_navn} alternativ.") + linjer.append(f"# Rod-sti: {ce.rod_sti}") + linjer.append(f"# Kører kun hvis {hvis_finder_sti} eksisterer i XML.") + if ce.overliggende: + linjer.append("#") + linjer.append("# Udfyld nøgle-skabeloner i nøgler.yaml inden brug.") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("output_filer:") + linjer.append("") + linjer.append(f" - rod: {ce.rod_sti}") + linjer.append(f" hvis_findes: {hvis_finder_sti}") + linjer.append(f" kolonner:") + + # Nøgler + if ce.overliggende: + linjer.append(f" # Nøgler fra overliggende niveauer – udfyld i nøgler.yaml") + for ov_navn in ce.overliggende: + skabelon_navn = _anvend_forkortelser( + f"{ov_navn.lower()}_nøgle", forkortelser + ) + linjer.append(f" - skabelon: {skabelon_navn}") + linjer.append("") + + # Choice-alternativets skabelon + linjer.append(f" # {alt_navn} felter") + linjer.append(f" - skabelon: {alt_skabelon}") + linjer.append(f" prefix_felt: {ce.element_navn}.{alt_navn}") + linjer.append(f' prefix_navn: "{prefix_navn}"') + + # Øvrige felter fra den overliggende type + if overliggende_type and overliggende_type.felter: + ikke_choice_felter = [ + f for f in overliggende_type.felter + if not (isinstance(f, XsdSkabelonRef) and f.er_choice) + and not (isinstance(f, XsdFelt) and f.er_choice) + ] + if ikke_choice_felter: + linjer.append("") + linjer.append(f" # Øvrige felter fra {rod_element_navn}") + for felt in ikke_choice_felter: + if isinstance(felt, XsdSkabelonRef): + linjer.extend(_skriv_skabelon_ref( + felt, " ", forkortelser, liste_navne + )) + elif isinstance(felt, XsdFelt): + linjer.extend(_skriv_felt( + felt, " ", forkortelser, liste_navne + )) + + linjer.append("") + linjer.append(f" outputs:") + + if output_type in ("fil", "begge"): + linjer.append(f" - fil_navn: \"{fil_del}_{{yyyy}}{{mm}}{{dd}}.txt\"") + linjer.append(f" overskrifter: true") + linjer.append("") + + if output_type in ("tabel", "begge"): + tabel_navn = f"{tabel_prefix}{fil_del}" + linjer.append(f" - tabel:") + linjer.append(f" staging: {db_schema}.{tabel_navn}_tmp") + linjer.append(f" blivende: {db_schema}.{tabel_navn}") + linjer.append(f" historik: {historik}") + linjer.append(f" # TODO: Angiv forretningsnøgler") + linjer.append(f" forretnings_nøgler: [__NØGLE__]") + linjer.append(f" tekniske_nøgler: [løbenummer]") + linjer.append(f" ekstra_kolonner:") + linjer.append(f" - navn: løbenummer") + linjer.append(f' ddl_type: "int identity"') + linjer.append(f" placering: start") + linjer.append("") + + 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}") + +# ============================================================ +# Generator: nøgler.yaml +# ============================================================ + +def generer_nøgler_yaml( + liste_elementer: list[XsdListeElement], + output_mappe: str, + forkortelser: dict[str, str] = {} +) -> None: + # Find unikke overliggende niveauer + overliggende: dict[str, str] = {} + for el in liste_elementer: + rod_dele = el.rod_sti.split(".") + for ov_navn in el.overliggende: + if ov_navn not in overliggende: + try: + idx = rod_dele.index(ov_navn) + overliggende[ov_navn] = ".".join(rod_dele[:idx + 1]) + except ValueError: + overliggende[ov_navn] = f"__{ov_navn.upper()}__" + + if not overliggende: + return + + linjer = [] + linjer.append("# =============================================================================") + linjer.append("# nøgler.yaml") + linjer.append("#") + linjer.append("# Nøgle-skabeloner der binder udtræk-filer til overliggende niveauer.") + linjer.append("# UDFYLD: Erstat __FELT__ med den korrekte forretningsnøgle.") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("kolonne_skabeloner:") + linjer.append("") + + for ov_navn, ov_rod_sti in overliggende.items(): + skabelon_navn = _anvend_forkortelser(f"{ov_navn.lower()}_nøgle", forkortelser) + 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(f" 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( + liste_elementer: list[XsdListeElement], + komplekse_typer: dict[str, XsdKompleksType], + output_mappe: str, + prefix: str = "udtræk", + min_felter: int = 1, + forkortelser: dict[str, str] = {}, + db_schema: str = "dbo", + tabel_prefix: str = "", + output_type: str = "begge", + historik: str = "t2" +) -> None: + + # Beregn fælles prefix på tværs af alle rod-stier + fælles_prefix_længde = _find_fælles_prefix(liste_elementer) + + fil_navne_index: dict[str, str] = {} + for el in liste_elementer: + rod_dele = el.rod_sti.split(".") + unikke_dele = rod_dele[fælles_prefix_længde:] if len(rod_dele) > fælles_prefix_længde else rod_dele + fil_del = _anvend_forkortelser("_".join(unikke_dele).lower(), forkortelser) + if el.element_navn not in fil_navne_index: + fil_navne_index[el.element_navn] = f"udtræk_{fil_del}.yaml" + + # Byg choice-grupper til kommentar-generering (ikke filnavne) + choice_grupper = _byg_choice_grupper(liste_elementer) + + # Byg sæt af liste-element navne til udkommentering + liste_navne = {el.element_navn for el in liste_elementer} + + for el in liste_elementer: + + # Byg unikt filnavn fra rod-stien minus fælles prefix + rod_dele = el.rod_sti.split(".") + unikke_dele = rod_dele[fælles_prefix_længde:] if len(rod_dele) > fælles_prefix_længde else rod_dele + fil_del = _anvend_forkortelser( + "_".join(unikke_dele).lower(), forkortelser + ) + fil_navn = f"{prefix}_{fil_del}.yaml" + + gruppe = choice_grupper.get(el.rod_sti) + + linjer = [] + linjer.append("# =============================================================================") + linjer.append(f"# {fil_navn}") + linjer.append("#") + linjer.append(f"# Auto-genereret udtræk for {el.element_navn}.") + linjer.append(f"# Rod-sti: {el.rod_sti}") + if el.er_simpel: + linjer.append("#") + linjer.append(f"# OBS: Dette element er simpelt ({el.element_navn} har få felter).") + linjer.append(f"# Det er foldet ind som join-kolonne i skabelon: {el.join_i_skabelon}") + linjer.append(f"# Denne fil er udkommenteret i config – fjern udkommentering for fuldt udtræk.") + if el.overliggende: + linjer.append("#") + linjer.append("# Udfyld nøgle-skabeloner i nøgler.yaml inden brug.") + if gruppe: + linjer.append("#") + linjer.append("# OBS: Dette element forekommer i flere choice-grene:") + for valg in gruppe['valg']: + sti = f"{gruppe['fælles_rod']}.{valg}.{gruppe['element_navn']}" + linjer.append(f"# - {sti}") + linjer.append("# Disse kan samles i én fil ved at ændre rod til en rod med varianter:") + linjer.append(f"# rod: {gruppe['samlet_rod']}") + linjer.append("# Tilføj evt. en kolonne til at skelne grenene:") + linjer.append("# - navn: bruger_type") + linjer.append("# type: rod_variant") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("output_filer:") + linjer.append("") + linjer.append(f" - rod: {el.rod_sti}") + linjer.append(f" kolonner:") + + # Nøgler + if el.overliggende: + linjer.append(f" # Nøgler fra overliggende niveauer – udfyld i nøgler.yaml") + for ov_navn in el.overliggende: + skabelon_navn = _anvend_forkortelser( + f"{ov_navn.lower()}_nøgle", forkortelser + ) + linjer.append(f" - skabelon: {skabelon_navn}") + linjer.append("") + + # Felter fra elementets type eller inline felter + type_navn_rent = _strip_prefix(el.type_navn) if el.type_navn else "" + kt = komplekse_typer.get(type_navn_rent) + + if kt and kt.felter and not kt.er_ekstern: + skabelon_navn = type_navn_rent.replace("_Type", "").replace("Type", "").lower() + linjer.append(f" # Felter fra {el.element_navn} – se skabeloner.yaml") + linjer.append(f" - skabelon: {skabelon_navn}") + elif el.felter: + linjer.append(f" # Felter fra {el.element_navn}") + for felt in el.felter: + if isinstance(felt, XsdSkabelonRef): + linjer.extend(_skriv_skabelon_ref( + felt, " ", forkortelser, liste_navne, + fil_navne_index=fil_navne_index + )) + elif isinstance(felt, XsdFelt): + linjer.extend(_skriv_felt( + felt, " ", forkortelser, liste_navne # ← tilføj liste_navne + )) + elif el.er_simpel: + linjer.append(f" # Feltets egen værdi") + linjer.append(f" - navn: {el.element_navn}") + linjer.append(f" felt: \".\"") + + linjer.append("") + linjer.append(f" outputs:") + + if output_type in ("fil", "begge"): + linjer.append(f" - fil_navn: \"{fil_del}_{{yyyy}}{{mm}}{{dd}}.txt\"") + linjer.append(f" overskrifter: true") + linjer.append("") + + if output_type in ("tabel", "begge"): + tabel_navn = f"{tabel_prefix}{fil_del}" + linjer.append(f" - tabel:") + linjer.append(f" staging: {db_schema}.{tabel_navn}_tmp") + linjer.append(f" blivende: {db_schema}.{tabel_navn}") + linjer.append(f" historik: {historik}") + linjer.append(f" # TODO: Angiv forretningsnøgler") + linjer.append(f" forretnings_nøgler: [__NØGLE__]") + linjer.append(f" tekniske_nøgler: [løbenummer]") + linjer.append(f" ekstra_kolonner:") + linjer.append(f" - navn: løbenummer") + linjer.append(f' ddl_type: "int identity"') + linjer.append(f" placering: start") + linjer.append("") + linjer.append("") + + 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}") + +# ============================================================ +# Generator: config_xxx.yaml +# ============================================================ + +def generer_hoved_config( + liste_elementer: list[XsdListeElement], + xsd_sti: str, + output_mappe: str, + prefix: str = "udtræk", + forkortelser: dict[str, str] = {} +) -> None: + xsd_basis = os.path.splitext(os.path.basename(xsd_sti))[0] + + # Beregn fælles prefix – samme logik som generer_udtræk_yaml + fælles_prefix_længde = _find_fælles_prefix(liste_elementer) + + linjer = [] + linjer.append("# =============================================================================") + linjer.append(f"# config_{xsd_basis}.yaml") + linjer.append("#") + linjer.append("# Auto-genereret hoved-config.") + linjer.append("# Tilpas config-sektionen med korrekte stier og indstillinger.") + linjer.append("# =============================================================================") + linjer.append("") + linjer.append("include:") + linjer.append(" - skabeloner.yaml") + linjer.append(" - nøgler.yaml") + linjer.append("") + + normale = [] + simple = [] + + for el in liste_elementer: + rod_dele = el.rod_sti.split(".") + unikke_dele = rod_dele[fælles_prefix_længde:] if len(rod_dele) > fælles_prefix_længde else rod_dele + fil_del = _anvend_forkortelser( + "_".join(unikke_dele).lower(), forkortelser + ) + fil = f"{prefix}_{fil_del}.yaml" + if el.er_simpel: + simple.append((fil, el)) + else: + normale.append((fil, el)) + + for fil, _ in normale: + linjer.append(f" - {fil}") + + if simple: + linjer.append("") + linjer.append(" # Simple lister – foldet ind som join-kolonner i skabelonerne:") + linjer.append(" # Fjern udkommentering for fuldt udtræk i stedet for join.") + for fil, el in simple: + linjer.append(f" # - {fil} (join i skabelon: {el.join_i_skabelon})") + + 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"') + + sti = os.path.join(output_mappe, f"config_{xsd_basis}.yaml") + with open(sti, "w", encoding="utf-8") as f: + f.write("\n".join(linjer)) + print(f" Skrev: {sti}") + +def _strip_prefix(type_navn: str) -> str: + 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 \ No newline at end of file