#!/usr/bin/env python3 """ brancheneinstufung.py - Hauptskript v1.8.0 Dieses Skript dient als Haupteinstiegspunkt für das Projekt zur automatisierten Anreicherung, Validierung und Standardisierung von Unternehmensdaten. Es parst Kommandozeilen-Argumente, initialisiert die notwendigen Handler und den DataProcessor und startet den ausgewählten Verarbeitungsmodus. Autor: Christian Godelmann Version: v1.8.0 """ print("--- START ---") import logging print("--- logging importiert ---") import os print("--- os importiert ---") import argparse print("--- argparse importiert ---") import time print("--- time importiert ---") from datetime import datetime print("--- datetime importiert ---") from config import Config print("--- config importiert ---") from helpers import create_log_filename, initialize_target_schema, alignment_demo, log_module_versions print("--- helpers importiert ---") from google_sheet_handler import GoogleSheetHandler print("--- google_sheet_handler importiert ---") from wikipedia_scraper import WikipediaScraper print("--- wikipedia_scraper importiert ---") from data_processor import DataProcessor print("--- data_processor importiert ---") from sync_manager import SyncManager print("--- sync_manager importiert ---") import helpers import google_sheet_handler import wikipedia_scraper import data_processor # ============================================================================== # 1. INITIALE KONFIGURATION (wird vor allem anderen ausgeführt) # ============================================================================== # Logging sofort konfigurieren, damit es für alle importierten Module greift. LOG_LEVEL = logging.DEBUG if Config.DEBUG else logging.INFO LOG_FORMAT = '%(asctime)s - %(levelname)-8s - %(name)-25s - %(message)s' logging.basicConfig(level=LOG_LEVEL, format=LOG_FORMAT, force=True, handlers=[logging.StreamHandler()]) # Haupt-Logger für dieses Skript logger = logging.getLogger(__name__) # ============================================================================== # 2. HAUPTFUNKTION # ============================================================================== def main(): """ Haupteinstiegspunkt des Skripts. Verarbeitet Kommandozeilen-Argumente, richtet Logging ein, initialisiert Komponenten und dispatchet zu den passenden Modi. """ # --- Importe innerhalb der Funktion, um Abhängigkeiten klar zu halten --- import argparse import time import logging import os # KORREKTUR: Die Funktionen kommen aus 'helpers', nicht aus 'config' from config import Config from helpers import log_module_versions, create_log_filename from google_sheet_handler import GoogleSheetHandler from wikipedia_scraper import WikipediaScraper from data_processor import DataProcessor from sync_manager import SyncManager import helpers import google_sheet_handler # --- Argument Parser --- parser = argparse.ArgumentParser( description=f"Firmen-Datenanreicherungs-Skript {Config.VERSION}.", formatter_class=argparse.RawTextHelpFormatter ) mode_categories = { "Daten-Synchronisation": ["sync", "simulate_sync"], "Batch-Verarbeitung": ["wiki_verify", "website_scraping", "summarize_website", "branch_eval", "suggest_parents", "fsm_pitch"], "Sequentielle Verarbeitung": ["full_run"], "Re-Evaluation": ["reeval"], "Dienstprogramme": ["find_wiki_serp", "check_urls", "contacts", "update_wiki_suggestions", "wiki_reextract_missing_an", "website_details", "train_technician_model", "predict_technicians", "alignment", "reparatur_sitz", "plausi_check_data"], "Kombinierte Läufe": ["combined_all"], "Spezial-Modi": ["reclassify_branches"], } valid_modes = [mode for modes in mode_categories.values() for mode in modes] mode_help_text = "Betriebsmodus. Waehlen Sie einen der folgenden:\n" for category, modes in mode_categories.items(): mode_help_text += f"\n{category}:\n" + "".join([f" - {mode}\n" for mode in modes]) parser.add_argument("--mode", type=str, help=mode_help_text) parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen.", default=None) parser.add_argument("--start_sheet_row", type=int, help="Startzeile im Sheet (1-basiert).", default=None) parser.add_argument("--end_sheet_row", type=int, help="Endzeile im Sheet (1-basiert).", default=None) valid_steps = ['wiki', 'chat', 'web', 'ml_predict'] parser.add_argument("--steps", type=str, help=f"Schritte für 'reeval'/'full_run' (z.B. 'wiki,chat'). Optionen: {', '.join(valid_steps)}.", default=','.join(valid_steps)) parser.add_argument("--min_umsatz", type=float, help="Mindestumsatz in MIO € für 'find_wiki_serp'.", default=200.0) parser.add_argument("--min_employees", type=int, help="Mindest-MA für 'find_wiki_serp'.", default=500) parser.add_argument("--debug_id", type=str, help="Eine spezifische CRM ID für eine Tiefenanalyse im 'debug_sync'-Modus.", default=None) parser.add_argument("--sync_file", type=str, help="Pfad zur D365 Excel-Exportdatei für den 'sync'-Modus.", default="d365_export.xlsx") args = parser.parse_args() # --- Modusauswahl (interaktiv, wenn nicht über CLI) --- selected_mode = args.mode.lower() if args.mode else None if not selected_mode: print("\nBitte waehlen Sie den Betriebsmodus:") mode_map = {} counter = 1 for category, modes in mode_categories.items(): print(f"\n{category}:") for mode in modes: print(f" {counter}: {mode}") mode_map[str(counter)] = mode mode_map[mode] = mode counter += 1 print("\n 0: Abbrechen") mode_map['0'] = 'exit' while selected_mode is None: try: choice = input("Geben Sie den Modusnamen oder die Zahl ein: ").strip().lower() if choice in mode_map: selected_mode = mode_map[choice] if selected_mode == 'exit': print("Abgebrochen.") return else: print("Ungueltige Eingabe.") except (EOFError, KeyboardInterrupt): print("\nAbgebrochen.") return # --- Logging Konfiguration --- LOG_LEVEL = logging.DEBUG if Config.DEBUG else logging.INFO LOG_FORMAT = '%(asctime)s - %(levelname)-8s - %(name)-25s - %(message)s' logging.basicConfig(level=LOG_LEVEL, format=LOG_FORMAT) logger = logging.getLogger(__name__) log_file_path = create_log_filename(selected_mode) if log_file_path: file_handler = logging.FileHandler(log_file_path, mode='a', encoding='utf-8') file_handler.setLevel(LOG_LEVEL) file_handler.setFormatter(logging.Formatter(LOG_FORMAT)) logging.getLogger('').addHandler(file_handler) logger.info(f"===== Skript gestartet: Modus '{selected_mode}' =====") logger.info(f"Projekt-Version (Config): {Config.VERSION}") logger.info(f"Logdatei: {log_file_path or 'FEHLER - Keine Logdatei'}") logger.info(f"CLI Argumente: {args}") # --- Hauptlogik --- try: Config.load_api_keys() sheet_handler = GoogleSheetHandler() # --- Modus-Dispatching --- start_time = time.time() if selected_mode == "simulate_sync": logger.info("Führe Initialisierung für Sync-Simulations-Modus durch...") if not sheet_handler.load_data(): logger.critical("Konnte initiale Daten aus dem Google Sheet nicht laden. Simulation wird abgebrochen.") return d365_file_path = args.sync_file if not os.path.exists(d365_file_path): logger.critical(f"Export-Datei nicht gefunden: {d365_file_path}") else: sync_manager = SyncManager(sheet_handler, d365_file_path) sync_manager.simulate_sync() # Aufruf der neuen Simulations-Funktion # Der elif-Block für den regulären Sync elif selected_mode == "sync": logger.info("Führe Initialisierung für Sync-Modus durch...") if not sheet_handler.load_data(): logger.critical("Konnte initiale Daten aus dem Google Sheet nicht laden. Sync-Prozess wird abgebrochen.") return d365_file_path = args.sync_file if not os.path.exists(d365_file_path): logger.critical(f"Export-Datei nicht gefunden: {d365_file_path}") else: sync_manager = SyncManager(sheet_handler, d365_file_path) sync_manager.run_sync() # Ab hier beginnt die bisherige Logik für alle anderen Modi else: wiki_scraper = WikipediaScraper() data_processor = DataProcessor(sheet_handler=sheet_handler, wiki_scraper=wiki_scraper) # --- Modul-Versionen loggen (NACH der Initialisierung) --- modules_to_log = { "DataProcessor": data_processor, "GoogleSheetHandler": google_sheet_handler, "WikipediaScraper": wikipedia_scraper, "Helpers": helpers } log_module_versions(modules_to_log) # --- Ende Version-Logging --- # Expliziter Setup-Aufruf, nachdem alle Konfigurationen geladen sind. if not data_processor.setup(): logger.critical("Setup des DataProcessors fehlgeschlagen. Das Skript wird beendet.") return # --- Modus-Dispatching für die restlichen Modi --- steps_to_run_set = set(step.strip().lower() for step in args.steps.split(',') if step.strip() in valid_steps) if args.steps else set(valid_steps) if selected_mode == "full_run": start_row = args.start_sheet_row or sheet_handler.get_start_row_index("Timestamp letzte Pruefung") + sheet_handler._header_rows + 1 num_to_process = args.limit or (len(sheet_handler.get_all_data_with_headers()) - start_row + 1) data_processor.process_rows_sequentially( start_sheet_row=start_row, num_to_process=num_to_process, process_wiki_steps='wiki' in steps_to_run_set, process_chatgpt_steps='chat' in steps_to_run_set, process_website_steps='web' in steps_to_run_set, process_ml_steps='ml_predict' in steps_to_run_set ) elif selected_mode == "reeval": data_processor.process_reevaluation_rows( row_limit=args.limit, clear_flag=True, process_wiki_steps='wiki' in steps_to_run_set, process_chatgpt_steps='chat' in steps_to_run_set, process_website_steps='web' in steps_to_run_set, process_ml_steps='ml_predict' in steps_to_run_set ) elif selected_mode == "reclassify_branches": data_processor.reclassify_all_branches(start_sheet_row=args.start_sheet_row, limit=args.limit) elif selected_mode == "alignment": alignment_demo(sheet_handler) elif selected_mode == "train_technician_model": data_processor.train_technician_model() elif selected_mode == "predict_technicians": data_processor.process_predict_technicians(start_sheet_row=args.start_sheet_row, limit=args.limit) elif hasattr(data_processor, f"process_{selected_mode}"): method_to_call = getattr(data_processor, f"process_{selected_mode}") method_args = {} if "limit" in method_to_call.__code__.co_varnames: method_args["limit"] = args.limit if "start_sheet_row" in method_to_call.__code__.co_varnames: method_args["start_sheet_row"] = args.start_sheet_row if "end_sheet_row" in method_to_call.__code__.co_varnames: method_args["end_sheet_row"] = args.end_sheet_row if "min_umsatz" in method_to_call.__code__.co_varnames: method_args["min_umsatz"] = args.min_umsatz if "min_employees" in method_to_call.__code__.co_varnames: method_args["min_employees"] = args.min_employees method_to_call(**method_args) elif hasattr(data_processor, f"run_{selected_mode}"): method_to_call = getattr(data_processor, f"run_{selected_mode}") method_to_call(start_sheet_row=args.start_sheet_row, end_sheet_row=args.end_sheet_row, limit=args.limit) else: logger.error(f"Unbekannter Modus '{selected_mode}' im Dispatcher.") duration = time.time() - start_time logger.info(f"Verarbeitung im Modus '{selected_mode}' abgeschlossen. Dauer: {duration:.2f} Sekunden.") except (KeyboardInterrupt, EOFError): logger.warning("Skript durch Benutzer unterbrochen.") print("\n! Skript wurde manuell beendet.") except Exception as e: logger.critical(f"FATAL: Unerwarteter Fehler im Hauptprozess: {e}", exc_info=True) print(f"\n! Ein kritischer Fehler ist aufgetreten: {e}") if 'log_file_path' in locals() and log_file_path: print(f"Bitte pruefen Sie die Logdatei fuer Details: {log_file_path}") finally: logger.info(f"===== Skript beendet =====") logging.shutdown() if 'selected_mode' in locals() and selected_mode != 'exit' and 'log_file_path' in locals() and log_file_path: print(f"\nVerarbeitung abgeschlossen. Logfile: {log_file_path}") # ============================================================================== # 3. SKRIPT-AUSFÜHRUNG # ============================================================================== if __name__ == "__main__": main()