diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 54a13aae..fba43f09 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -955,10 +955,16 @@ class GoogleSheetHandler: def __init__(self): self.sheet = None self.sheet_values = [] - self.headers = [] # Um Header-Zeilen zu speichern (Annahme: Zeile 1 sind die Namen) - self._connect() - if self.sheet: - self._load_data() + self.headers = [] + try: + self._connect() + if self.sheet: + self.load_data() # Erste Datenladung bei Initialisierung + except Exception as e: + # Fehler bei Initialisierung bereits loggen und None zurückgeben? + debug_print(f"FATAL: Fehler bei Initialisierung von GoogleSheetHandler: {e}") + # Hier sollte das Hauptprogramm den Fehler erkennen und abbrechen. + # Man könnte auch eine Exception werfen: raise ConnectionError(...) # retry_on_failure Decorator sollte hier angewendet werden @retry_on_failure @@ -966,6 +972,7 @@ class GoogleSheetHandler: """Stellt Verbindung zum Google Sheet her.""" self.sheet = None debug_print("Verbinde mit Google Sheets...") + # Fehlerbehandlung innerhalb ist gut, aber raise am Ende, damit retry greift try: scope = ["https://www.googleapis.com/auth/spreadsheets"] creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope) @@ -975,49 +982,76 @@ class GoogleSheetHandler: debug_print("Verbindung zu Google Sheets erfolgreich.") except gspread.exceptions.APIError as e: debug_print(f"FEHLER bei Google API Verbindung: Status {e.response.status_code} - {e.response.text[:200]}") - raise # Damit retry greift + raise e except Exception as e: debug_print(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}") - raise # Damit retry greift + raise e # retry_on_failure Decorator sollte hier angewendet werden @retry_on_failure - def _load_data(self): - """Lädt alle Daten aus dem Sheet.""" + def load_data(self): + """Lädt alle Daten aus dem Sheet und aktualisiert self.sheet_values.""" if not self.sheet: debug_print("Fehler: Keine Sheet-Verbindung zum Laden der Daten.") self.sheet_values = [] self.headers = [] - return + return False # Signalisiert Fehler debug_print("Lade Daten aus Google Sheet...") - self.sheet_values = self.sheet.get_all_values() - if len(self.sheet_values) >= 1: - # Speichere die *echten* Header-Namen aus der ersten Zeile - self.headers = self.sheet_values[0] - # Die Alignment-Demo Header (Zeilen 1-5) werden hier nicht separat gespeichert, - # sheet_values enthält alles. - else: - self.headers = [] - debug_print("Warnung: Google Sheet scheint leer zu sein.") - debug_print(f"Daten geladen: {len(self.sheet_values)} Zeilen insgesamt.") + try: + # Hol die rohen Daten + raw_values = self.sheet.get_all_values() + + # Prüfe, ob überhaupt Daten zurückkamen + if not raw_values: + debug_print("Warnung: Google Sheet scheint leer zu sein oder keine Daten zurückgegeben.") + self.sheet_values = [] + self.headers = [] + return True # Kein Fehler beim Laden, aber keine Daten + + self.sheet_values = raw_values # Speichere die kompletten Daten + + # Setze Header basierend auf der ersten Zeile + if len(self.sheet_values) >= 1: + self.headers = self.sheet_values[0] + else: + self.headers = [] # Sollte nicht passieren, wenn raw_values nicht leer war + + debug_print(f"Daten neu geladen: {len(self.sheet_values)} Zeilen insgesamt.") + return True # Signalisiert Erfolg + except gspread.exceptions.APIError as e: + debug_print(f"Google API Fehler beim Laden der Sheet Daten: Status {e.response.status_code} - {e.response.text[:200]}") + # self.sheet_values = [] # Im Fehlerfall alte Daten behalten oder leeren? Besser behalten. + # self.headers = [] + raise e # Damit retry greift + except Exception as e: + debug_print(f"Allgemeiner Fehler beim Laden der Google Sheet Daten: {e}") + # self.sheet_values = [] + # self.headers = [] + raise e # Damit retry greift + # return False # Wird nur bei Exception erreicht, die nicht weitergeworfen wird def get_data(self): - """Gibt die geladenen Daten zurück (ohne die ersten 5 Header-Zeilen).""" + """Gibt die aktuell im Handler gespeicherten Daten zurück (ohne die ersten 5 Header-Zeilen).""" header_rows = 5 # Definiert die Anzahl der zu überspringenden Header-Zeilen - if len(self.sheet_values) <= header_rows: - debug_print("Warnung in get_data: Weniger Zeilen als Header-Zeilen vorhanden.") + if not self.sheet_values or len(self.sheet_values) <= header_rows: + # Logge nur, wenn sheet_values existiert aber zu kurz ist + if self.sheet_values: + debug_print(f"Warnung in get_data: Nur {len(self.sheet_values)} Zeilen vorhanden, weniger als {header_rows} Header-Zeilen erwartet.") return [] # Gibt eine Slice der Liste zurück, die die Datenzeilen enthält return self.sheet_values[header_rows:] def get_all_data_with_headers(self): - """Gibt alle Daten inklusive Header zurück.""" + """Gibt alle aktuell im Handler gespeicherten Daten inklusive Header zurück.""" + if not self.sheet_values: + debug_print("Warnung in get_all_data_with_headers: Keine Daten im Handler gespeichert.") return self.sheet_values def _get_col_letter(self, col_idx_1_based): - """ Konvertiert 1-basierten Spaltenindex in Buchstaben. """ + """ Konvertiert 1-basierten Spaltenindex in Buchstaben (A, B, ..., Z, AA, ...). """ string = "" n = col_idx_1_based + if n < 1: return None # Ungültiger Index while n > 0: n, remainder = divmod(n - 1, 26) string = chr(65 + remainder) + string @@ -1026,8 +1060,9 @@ class GoogleSheetHandler: def get_start_row_index(self, check_column_key, min_sheet_row=7): """ Findet den Index der ersten Zeile (0-basiert für Daten nach Header), - ab einer Mindestzeilennummer im Sheet, in der der Timestamp in der - Spalte, die durch den *Schlüssel* in COLUMN_MAP definiert ist, fehlt. + ab einer Mindestzeilennummer im Sheet, in der der Wert in der + Spalte (definiert durch check_column_key in COLUMN_MAP) fehlt oder leer ist. + Lädt die Daten vor der Prüfung neu. Args: check_column_key (str): Der Schlüssel in COLUMN_MAP für die zu prüfende Spalte. @@ -1035,25 +1070,28 @@ class GoogleSheetHandler: Returns: int: Der 0-basierte Index in der Datenliste (ohne Header), + oder -1 bei Fehler (z.B. Schlüssel nicht gefunden), oder der Index nach der letzten Zeile, wenn alle gefüllt sind. """ + # Lade Daten *vor* der Prüfung neu, um Aktualität sicherzustellen + if not self.load_data(): + debug_print("FEHLER beim Laden der Daten in get_start_row_index. Breche ab.") + return -1 # Fehlerindikator + header_rows = 5 - data_rows = self.get_data() # Holt Daten OHNE die 5 Header + data_rows = self.get_data() # Greift auf die neu geladenen Daten zu if not data_rows: - debug_print("Keine Datenzeilen vorhanden für get_start_row_index.") - return 0 + debug_print("Keine Datenzeilen vorhanden für get_start_row_index nach Neuladen.") + return 0 # Index 0 signalisiert Start am Anfang (oder keine Daten) # Hole den Spaltenindex aus COLUMN_MAP check_column_index = COLUMN_MAP.get(check_column_key) if check_column_index is None: debug_print(f"FEHLER: Schlüssel '{check_column_key}' nicht in COLUMN_MAP gefunden!") - # Fallback oder Fehler werfen? Vorerst auf eine bekannte Spalte (AO) zurückfallen? Schlecht. - # Besser: None zurückgeben oder Fehler werfen, damit aufrufende Funktion es merkt. - # Hier: Gib -1 zurück als Fehlerindikator - return -1 + return -1 # Fehlerindikator - actual_col_letter = self._get_col_letter(check_column_index + 1) # +1 für 1-basierte Konvertierung + actual_col_letter = self._get_col_letter(check_column_index + 1) # Berechne den 0-basierten Startindex für die *Datenliste* data_rows search_start_index_in_data = max(0, min_sheet_row - header_rows - 1) @@ -1074,11 +1112,14 @@ class GoogleSheetHandler: is_empty = True if len(row) > check_column_index: cell_value = row[check_column_index] - if cell_value and str(cell_value).strip(): # Prüft auf nicht leer und nicht nur Whitespace + # Prüft explizit auf None und leeren String nach strip() + if cell_value is not None and str(cell_value).strip(): is_empty = False + # else: is_empty bleibt True, da Spalte nicht existiert - # DEBUG Log für jede 1000ste Zeile oder wenn ein relevanter Übergang erwartet wird - if i == search_start_index_in_data or i % 1000 == 0 or current_sheet_row in [2121, 2122, 8926, 8927, 8928]: + # Debug Log für jede 1000ste Zeile oder relevante Übergänge + log_debug = (i == search_start_index_in_data or i % 1000 == 0 or current_sheet_row in [2121, 2122, 8926, 8927, 8928]) + if log_debug: debug_print(f" -> Prüfe Daten-Index {i} (Sheet Zeile {current_sheet_row}): Wert in Spalte {actual_col_letter}='{cell_value}' -> Leer? {is_empty}") if is_empty: @@ -1108,6 +1149,7 @@ class GoogleSheetHandler: debug_print("FEHLER: Keine Sheet-Verbindung für Batch-Update.") return False if not update_data: + # debug_print("Keine Daten für Batch-Update vorhanden.") # Weniger Lärm return True try: @@ -1115,11 +1157,13 @@ class GoogleSheetHandler: return True except gspread.exceptions.APIError as e: debug_print(f"Google API Fehler beim Batch-Update: Status {e.response.status_code} - {e.response.text[:500]}") - raise e # Fehler weitergeben, damit der Decorator ihn fängt + raise e except Exception as e: debug_print(f"Allgemeiner Fehler beim Batch-Update: {type(e).__name__} - {e}") raise e +# --- Ende GoogleSheetHandler Klasse --- + # ==================== WIKIPEDIA SCRAPER ==================== @@ -2464,31 +2508,24 @@ def run_dispatcher(mode, sheet_handler, row_limit=None): """ Wählt den passenden Batch-Prozess basierend auf dem Modus. Ermittelt die Startzeile dynamisch basierend auf dem Timestamp in der relevanten Spalte. + Die aufgerufenen Prozessfunktionen laden ihre Daten selbst oder verwenden den Handler. """ debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.") - header_rows = 5 # Definiere die Anzahl der Header-Zeilen + header_rows = 5 # Feste Annahme für Header # --- Startzeilen-Ermittlung basierend auf Modus --- - # Definiere, welche Spalte für welchen Modus den Startpunkt bestimmt - # Standardmäßig AO für Gesamtprüfung, AT für Website, AN für Wiki, AO für Branch start_col_key = "Timestamp letzte Prüfung" # Standard (Spalte AO) - min_start_row = 7 # Mindestens ab Zeile 7 suchen - - if mode == "website": - start_col_key = "Website Scrape Timestamp" # Spalte AT - elif mode == "wiki": - start_col_key = "Wikipedia Timestamp" # Spalte AN - elif mode == "branch": - start_col_key = "Timestamp letzte Prüfung" # Spalte AO - elif mode == "combined": - # HIER KORRIGIERT: Combined startet basierend auf AO (letzter Schritt) - start_col_key = "Timestamp letzte Prüfung" # Spalte AO - # Füge ggf. andere Modi hinzu + min_start_row = 7 + if mode == "website": start_col_key = "Website Scrape Timestamp" # AT + elif mode == "wiki": start_col_key = "Wikipedia Timestamp" # AN + elif mode == "branch": start_col_key = "Timestamp letzte Prüfung" # AO + elif mode == "combined": start_col_key = "Timestamp letzte Prüfung" # AO (Combined startet, wo der letzte Schritt fehlt) debug_print(f"Dispatcher: Ermittle Startzeile basierend auf Spalte '{start_col_key}'...") + # get_start_row_index wird aufgerufen, lädt die Daten neu start_data_index = sheet_handler.get_start_row_index(check_column_key=start_col_key, min_sheet_row=min_start_row) - # Fehlerprüfung: Wenn get_start_row_index -1 zurückgibt (Schlüssel nicht gefunden) + # Fehlerprüfung für get_start_row_index if start_data_index == -1: debug_print(f"FEHLER: Konnte Startzeile nicht ermitteln (Spaltenschlüssel '{start_col_key}' in COLUMN_MAP prüfen?). Dispatcher beendet.") return @@ -2496,25 +2533,21 @@ def run_dispatcher(mode, sheet_handler, row_limit=None): # Umrechnung des 0-basierten Daten-Index in die 1-basierte Sheet-Zeilennummer start_row_index_in_sheet = start_data_index + header_rows + 1 - # Hole Gesamtzahl der Zeilen für die Endberechnung - all_data = sheet_handler.get_all_data_with_headers() - total_sheet_rows = len(all_data) + # Hole Gesamtzahl der Zeilen (nach potentiellem Neuladen in get_start_row_index) + total_sheet_rows = len(sheet_handler.sheet_values) - # Prüfe, ob der Startpunkt überhaupt im Sheet liegt - if start_row_index_in_sheet > total_sheet_rows and total_sheet_rows > header_rows: - # Wenn der Startpunkt HINTER der letzten Zeile liegt, gibt es nichts zu tun - debug_print(f"Startzeile ({start_row_index_in_sheet}) liegt hinter der letzten Sheet-Zeile ({total_sheet_rows}). Keine neuen Zeilen zu verarbeiten. Dispatcher beendet.") - return + # Prüfe, ob der Startpunkt überhaupt im Sheet liegt oder ob das Sheet leer ist + if start_data_index >= len(sheet_handler.get_data()): # Prüfe gegen Daten ohne Header + debug_print(f"Ermittelter Start-Daten-Index ({start_data_index}) liegt nach der letzten Datenzeile ({len(sheet_handler.get_data())-1}). Keine neuen Zeilen zu verarbeiten. Dispatcher beendet.") + return elif start_row_index_in_sheet > total_sheet_rows: - # Wenn das Sheet nur Header hat oder leer ist + # Dieser Fall sollte durch die Prüfung oben abgedeckt sein, aber zur Sicherheit debug_print(f"Sheet hat keine Datenzeilen oder Startzeile ({start_row_index_in_sheet}) ist ungültig. Dispatcher beendet.") return - # --- Endzeilen-Ermittlung --- if row_limit is not None and row_limit > 0: - # Berechne Endzeile basierend auf Startzeile und Limit, - # aber nicht über die letzte Zeile hinaus. + # Berechne Endzeile basierend auf Startzeile und Limit end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows) elif row_limit == 0: debug_print("Zeilenlimit ist 0. Keine Verarbeitung.") @@ -2524,32 +2557,27 @@ def run_dispatcher(mode, sheet_handler, row_limit=None): debug_print(f"Dispatcher: Verarbeitung geplant für Sheet-Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}.") - # Zusätzliche Prüfung: Liegt Start nach Ende? (Sollte nicht passieren, aber sicher ist sicher) if start_row_index_in_sheet > end_row_index_in_sheet: debug_print("Berechnete Startzeile liegt nach der Endzeile. Keine Verarbeitung.") return # --- Modusauswahl und Aufruf der Verarbeitungsfunktionen --- - # Die aufgerufenen Funktionen müssen jetzt selbst prüfen, ob die jeweilige Zeile - # wegen eines bereits vorhandenen Timestamps übersprungen werden soll. try: if mode == "wiki": - # process_verification_only prüft Timestamp AN intern process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) elif mode == "website": - # process_website_batch prüft Timestamp AT intern process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) elif mode == "branch": - # process_branch_batch prüft Timestamp AO intern process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) elif mode == "combined": - # Führt die Teile nacheinander aus, jeder Teil prüft seinen eigenen Timestamp debug_print("--- Start Combined Mode: Wiki ---") - process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AN + process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Lädt Daten, prüft AN + time.sleep(1) # Kurze Pause debug_print("--- Start Combined Mode: Website ---") - process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AT + process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Lädt Daten, prüft AT + time.sleep(1) # Kurze Pause debug_print("--- Start Combined Mode: Branch ---") - process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AO + process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Lädt Daten, prüft AO debug_print("--- Combined Mode abgeschlossen ---") else: debug_print(f"Ungültiger Modus '{mode}' wurde im Dispatcher übergeben.") @@ -2557,7 +2585,7 @@ def run_dispatcher(mode, sheet_handler, row_limit=None): except Exception as e: debug_print(f"FEHLER im Dispatcher während der Ausführung von Modus '{mode}': {e}") import traceback - debug_print(traceback.format_exc()) # Gib den Traceback aus für detaillierte Fehlersuche + debug_print(traceback.format_exc()) # --- Ende run_dispatcher Funktion ---