From ad30f004923a49d682bc7b9aa4d1352c4b62ffc1 Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 19 Jul 2025 22:11:58 +0200 Subject: [PATCH] Add Excel-to-SQL conversion scripts for nodes and node predecessor chains Introduce Python scripts to convert node and node predecessor data from Excel files into SQL INSERT statements. Includes support for ISO code mappings, geo-position parsing, node type determination, and automated chain creation. Also added sample Excel files (`nodes.xlsx`, `pre_nodes.xlsx`) for testing and reference. --- .../excel_to_sql_converter_nodes.py | 211 ++++++++++++++++++ .../excel_to_sql_converter_pre_nodes.py | 116 ++++++++++ src/main/resources/master_data/nodes.xlsx | Bin 0 -> 14134 bytes src/main/resources/master_data/pre_nodes.xlsx | Bin 0 -> 9828 bytes 4 files changed, 327 insertions(+) create mode 100644 src/main/resources/master_data/excel_to_sql_converter_nodes.py create mode 100644 src/main/resources/master_data/excel_to_sql_converter_pre_nodes.py create mode 100644 src/main/resources/master_data/nodes.xlsx create mode 100644 src/main/resources/master_data/pre_nodes.xlsx diff --git a/src/main/resources/master_data/excel_to_sql_converter_nodes.py b/src/main/resources/master_data/excel_to_sql_converter_nodes.py new file mode 100644 index 0000000..38bb5c3 --- /dev/null +++ b/src/main/resources/master_data/excel_to_sql_converter_nodes.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +Excel zu SQL Konverter für Nodes +Konvertiert nodes.xlsx in SQL INSERT Statements unter Verwendung von countries.xlsx für ISO-Codes +""" + +import pandas as pd +import sys +import re +from pathlib import Path + +def load_countries_mapping(countries_file): + """ + Lädt die Länder-zu-ISO-Code Mapping aus der countries.xlsx Datei + """ + try: + # Lade die countries.xlsx Datei - verwende das erste Sheet + countries_df = pd.read_excel(countries_file, sheet_name=0) + + # Erstelle ein Mapping von Ländernamen zu ISO-Codes + country_mapping = {} + for _, row in countries_df.iterrows(): + country_name = str(row['Country']).strip() + iso_code = str(row['Country code']).strip() + country_mapping[country_name] = iso_code + + print(f"Erfolgreich {len(country_mapping)} Länder-Mappings geladen") + return country_mapping + + except Exception as e: + print(f"Fehler beim Laden der countries.xlsx: {e}") + sys.exit(1) + +def parse_geo_position(geo_str): + """ + Parst die Geo-Position aus dem Format "lat, lng" und rundet auf 4 Nachkommastellen + """ + if pd.isna(geo_str) or str(geo_str).strip() == '': + return None, None + + try: + # Entferne Leerzeichen und splitte bei Komma + coords = str(geo_str).strip().split(',') + if len(coords) == 2: + lat = round(float(coords[0].strip()), 4) + lng = round(float(coords[1].strip()), 4) + return lat, lng + else: + return None, None + except ValueError: + return None, None + +def determine_node_type(type_str): + """ + Bestimmt die Node-Typen basierend auf dem "Type" Feld + """ + if pd.isna(type_str): + return False, False, False # is_destination, is_source, is_intermediate + + type_lower = str(type_str).lower().strip() + + print(type_lower) + + # Standard: alle False, außer für spezifische Typen + is_destination = False + is_source = False + is_intermediate = False + + if 'sink' in type_lower: + is_destination = True + if 'source' in type_lower: + is_source = True + if 'intermediate' in type_lower: + is_intermediate = True + if is_intermediate == False and is_source == False and is_destination == False: + raise Exception(f"no node type in {type_lower} " ) + + return is_destination, is_source, is_intermediate + +def escape_sql_string(value): + """ + Escaped SQL-Strings für sichere Einfügung + """ + if pd.isna(value): + return 'NULL' + + # Konvertiere zu String und escape single quotes + str_value = str(value).replace("'", "''") + return f"'{str_value}'" + +def convert_nodes_to_sql(nodes_file, countries_file, output_file): + """ + Hauptfunktion: Konvertiert nodes.xlsx zu SQL INSERT Statements + """ + + # Lade Länder-Mapping + country_mapping = load_countries_mapping(countries_file) + + try: + # Lade nodes.xlsx + nodes_df = pd.read_excel(nodes_file, sheet_name='Tabelle1') + print(f"Erfolgreich {len(nodes_df)} Nodes geladen") + + except Exception as e: + print(f"Fehler beim Laden der nodes.xlsx: {e}") + sys.exit(1) + + # Öffne Output-Datei + try: + with open(output_file, 'w', encoding='utf-8') as f: + f.write("-- Generated SQL INSERT statements for nodes\n") + f.write("-- Generated from nodes.xlsx using countries.xlsx for ISO code mapping\n\n") + + # Iteriere über alle Zeilen + for index, row in nodes_df.iterrows(): + node_id = index + 1 # 1-basiert + + # Extrahiere Daten + external_mapping_id = str(row['external mapping id']).strip() if pd.notna(row['external mapping id']) else None + name = str(row['Name']).strip() if pd.notna(row['Name']) else None + address = str(row['Address']).strip() if pd.notna(row['Address']) else None + country_name = str(row['Country ']).strip() if pd.notna(row['Country ']) else None # Beachte das Leerzeichen im Spaltennamen + + # Prüfe, ob Country-Name im Mapping existiert + if not country_name or country_name not in country_mapping: + print(f"FEHLER: Land '{country_name}' in Zeile {node_id} nicht in countries.xlsx gefunden!") + print(f"Verfügbare Länder: {sorted(country_mapping.keys())}") + sys.exit(1) + + iso_code = country_mapping[country_name] + + # Parse Geo-Position + geo_lat, geo_lng = parse_geo_position(row['Geo position']) + + # Bestimme Node-Typen + is_destination, is_source, is_intermediate = determine_node_type(row['Type']) + + print(f"{node_id} {name}: {is_destination}, {is_source}, {is_intermediate}") + + # Predecessor required + predecessor_required = False + if pd.notna(row['predecessor_required']): + pred_str = str(row['predecessor_required']).lower().strip() + predecessor_required = pred_str in ['yes', 'true', '1', 'ja'] + + # Schreibe SQL INSERT Statement + f.write(f"-- Node {node_id}: {name or 'Unknown'}\n") + f.write("INSERT INTO node (\n") + f.write(" id,\n") + f.write(" country_id,\n") + f.write(" name,\n") + f.write(" address,\n") + f.write(" external_mapping_id,\n") + f.write(" predecessor_required,\n") + f.write(" is_destination,\n") + f.write(" is_source,\n") + f.write(" is_intermediate,\n") + f.write(" geo_lat,\n") + f.write(" geo_lng\n") + f.write(") VALUES (\n") + f.write(f" {node_id},\n") + f.write(f" (SELECT id FROM country WHERE iso_code = '{iso_code}'),\n") + f.write(f" {escape_sql_string(name)},\n") + f.write(f" {escape_sql_string(address)},\n") + f.write(f" {escape_sql_string(external_mapping_id)},\n") + f.write(f" {'true' if predecessor_required else 'false'},\n") + f.write(f" {'true' if is_destination else 'false'},\n") + f.write(f" {'true' if is_source else 'false'},\n") + f.write(f" {'true' if is_intermediate else 'false'},\n") + f.write(f" {geo_lat if geo_lat is not None else 'NULL'},\n") + f.write(f" {geo_lng if geo_lng is not None else 'NULL'}\n") + f.write(" );\n\n") + + print(f"SQL-Datei erfolgreich erstellt: {output_file}") + print(f"Insgesamt {len(nodes_df)} INSERT Statements generiert") + + except Exception as e: + print(f"Fehler beim Schreiben der SQL-Datei: {e}") + sys.exit(1) + +def main(): + """ + Hauptprogramm + """ + # Standarddateinamen + nodes_file = 'nodes.xlsx' + countries_file = 'countries.xlsx' + output_file = '03-nodes.sql' + + # Prüfe, ob Dateien existieren + if not Path(nodes_file).exists(): + print(f"Fehler: {nodes_file} nicht gefunden!") + sys.exit(1) + + if not Path(countries_file).exists(): + print(f"Fehler: {countries_file} nicht gefunden!") + sys.exit(1) + + print(f"Konvertiere {nodes_file} zu SQL...") + print(f"Verwende {countries_file} für ISO-Code Mapping...") + print(f"Output-Datei: {output_file}") + print("-" * 50) + + # Führe Konvertierung aus + convert_nodes_to_sql(nodes_file, countries_file, output_file) + + print("-" * 50) + print("Konvertierung erfolgreich abgeschlossen!") + +if __name__ == "__main__": + main() diff --git a/src/main/resources/master_data/excel_to_sql_converter_pre_nodes.py b/src/main/resources/master_data/excel_to_sql_converter_pre_nodes.py new file mode 100644 index 0000000..0d6f6bf --- /dev/null +++ b/src/main/resources/master_data/excel_to_sql_converter_pre_nodes.py @@ -0,0 +1,116 @@ +import pandas as pd +import sys +from pathlib import Path + +def convert_excel_to_sql(excel_file_path, output_file_path=None): + """ + Konvertiert eine Excel-Datei mit Node-Predecessor-Daten in SQL-Statements. + + Args: + excel_file_path (str): Pfad zur Excel-Datei + output_file_path (str, optional): Pfad zur Ausgabe-SQL-Datei. + Wenn None, wird automatisch generiert. + """ + try: + # Excel-Datei laden + df = pd.read_excel(excel_file_path) + + # Spalten-Namen bereinigen (falls nötig) + df.columns = df.columns.str.strip() + + # Erwartete Spalten prüfen + expected_columns = ['node', 'Pre-node 1', 'Pre-node 2', 'Pre-node 3'] + if not all(col in df.columns for col in expected_columns): + print(f"Fehler: Erwartete Spalten nicht gefunden. Gefundene Spalten: {list(df.columns)}") + return + + # Ausgabe-Datei festlegen + if output_file_path is None: + output_file_path = Path(excel_file_path).stem + '_converted.sql' + + # SQL-Statements generieren + sql_statements = [] + + # Header-Kommentar hinzufügen + sql_statements.append("-- Automatisch generierte SQL-Statements für Node Predecessor Chains") + sql_statements.append("-- Generiert aus: " + str(excel_file_path)) + sql_statements.append("") + + chain_counter = 1 + + for index, row in df.iterrows(): + node_id = row['node'] + + # Leere Zeilen überspringen + if pd.isna(node_id) or str(node_id).strip() == '': + continue + + # Kommentar für die Chain + sql_statements.append(f"-- Predecessor Chain {chain_counter}: {node_id}") + + # Node Predecessor Chain erstellen + sql_statements.append("INSERT INTO node_predecessor_chain (") + sql_statements.append(" node_id") + sql_statements.append(") VALUES (") + sql_statements.append(f" (SELECT id FROM node WHERE external_mapping_id = '{node_id}')") + sql_statements.append(" );") + sql_statements.append("") + + # Variable für Chain-ID setzen + sql_statements.append(f"SET @chain_id_{chain_counter} = LAST_INSERT_ID();") + sql_statements.append("") + + # Predecessor Entries erstellen (nur wenn nicht leer) + sequence_number = 1 + for pre_node_col in ['Pre-node 1', 'Pre-node 2', 'Pre-node 3']: + pre_node_value = row[pre_node_col] + + # Nur verarbeiten wenn Wert vorhanden ist + if not pd.isna(pre_node_value) and str(pre_node_value).strip() != '': + sql_statements.append("INSERT INTO node_predecessor_entry (") + sql_statements.append(" node_id,") + sql_statements.append(" node_predecessor_chain_id,") + sql_statements.append(" sequence_number") + sql_statements.append(") VALUES (") + sql_statements.append(f" (SELECT id FROM node WHERE external_mapping_id = '{pre_node_value}'),") + sql_statements.append(f" @chain_id_{chain_counter},") + sql_statements.append(f" {sequence_number}") + sql_statements.append(" );") + sql_statements.append("") + + sequence_number += 1 + + chain_counter += 1 + + # SQL-Datei schreiben + with open(output_file_path, 'w', encoding='utf-8') as f: + f.write('\n'.join(sql_statements)) + + print(f"Erfolgreich konvertiert! SQL-Datei erstellt: {output_file_path}") + print(f"Verarbeitete Zeilen: {len(df)}") + print(f"Generierte Chains: {chain_counter - 1}") + + except FileNotFoundError: + print(f"Fehler: Datei '{excel_file_path}' nicht gefunden.") + except Exception as e: + print(f"Fehler beim Verarbeiten der Datei: {str(e)}") + +def main(): + """ + Hauptfunktion für die Kommandozeilen-Nutzung. + """ + if len(sys.argv) < 2: + print("Verwendung: python excel_to_sql.py [ausgabe_datei]") + print("Beispiel: python excel_to_sql.py pre_nodes.xlsx output.sql") + return + + excel_file = sys.argv[1] + output_file = sys.argv[2] if len(sys.argv) > 2 else None + + convert_excel_to_sql(excel_file, output_file) + +if __name__ == "__main__": + main() + +# Beispiel für die direkte Verwendung im Script: +# convert_excel_to_sql('pre_nodes.xlsx', 'predecessor_chains.sql') \ No newline at end of file diff --git a/src/main/resources/master_data/nodes.xlsx b/src/main/resources/master_data/nodes.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1c01200aa1363282488eab4f011ffd7a2dfed7da GIT binary patch literal 14134 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 literal 0 HcmV?d00001