diff --git a/brancheneinstufung.py b/brancheneinstufung.py index a61437a3..ab777c56 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -947,20 +947,23 @@ def token_count(text): return len(text.split()) # ==================== GOOGLE SHEET HANDLER ==================== +# Annahmen: +# - Globale Variablen/Konstanten: retry_on_failure, Config, CREDENTIALS_FILE, Config.SHEET_URL, debug_print, COLUMN_MAP +# - COLUMN_MAP enthält den Schlüssel "Website Scrape Timestamp" mit dem korrekten Index (45) + class GoogleSheetHandler: def __init__(self): self.sheet = None self.sheet_values = [] - self.headers = [] # Um Header-Zeilen zu speichern + self.headers = [] # Um Header-Zeilen zu speichern (Annahme: Zeile 1 sind die Namen) self._connect() - # _load_data wird jetzt nach erfolgreicher Verbindung aufgerufen if self.sheet: self._load_data() + # retry_on_failure Decorator sollte hier angewendet werden @retry_on_failure def _connect(self): """Stellt Verbindung zum Google Sheet her.""" - # Setze self.sheet initial auf None self.sheet = None debug_print("Verbinde mit Google Sheets...") try: @@ -968,13 +971,16 @@ class GoogleSheetHandler: creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope) gc = gspread.authorize(creds) sh = gc.open_by_url(Config.SHEET_URL) - self.sheet = sh.sheet1 # Zugriff auf das erste Blatt (Sheet1) + self.sheet = sh.sheet1 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 except Exception as e: - debug_print(f"FEHLER bei der Google Sheets Verbindung: {e}") - # Hier könnte man den Fehler weitergeben oder None lassen - raise # Fehler weitergeben, damit retry greift oder main abbricht + debug_print(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}") + raise # Damit retry greift + # retry_on_failure Decorator sollte hier angewendet werden @retry_on_failure def _load_data(self): """Lädt alle Daten aus dem Sheet.""" @@ -985,33 +991,46 @@ class GoogleSheetHandler: return debug_print("Lade Daten aus Google Sheet...") self.sheet_values = self.sheet.get_all_values() - if len(self.sheet_values) >= 1: # Mindestens Header sollte da sein - self.headers = self.sheet_values[:5] # Annahme: Zeilen 1-5 sind Header + 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.") def get_data(self): - """Gibt die geladenen Daten zurück (ohne Header).""" - header_rows = 5 # Annahme: Zeile 1-5 sind Header + """Gibt die geladenen 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.") 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.""" return self.sheet_values - # Angepasste Methode, um den Startindex basierend auf einer spezifischen Spalte zu finden - def get_start_row_index(self, check_column_index, min_sheet_row=7): # Entferne Standardwert hier + def _get_col_letter(self, col_idx_1_based): + """ Konvertiert 1-basierten Spaltenindex in Buchstaben. """ + string = "" + n = col_idx_1_based + while n > 0: + n, remainder = divmod(n - 1, 26) + string = chr(65 + remainder) + string + return string + + 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 - angegebenen Spalte fehlt. + Spalte, die durch den *Schlüssel* in COLUMN_MAP definiert ist, fehlt. Args: - check_column_index (int): Der 0-basierte Index der zu prüfenden Spalte. + check_column_key (str): Der Schlüssel in COLUMN_MAP für die zu prüfende Spalte. min_sheet_row (int): Die 1-basierte Zeilennummer im Sheet, ab der gesucht werden soll. Returns: @@ -1019,55 +1038,60 @@ class GoogleSheetHandler: oder der Index nach der letzten Zeile, wenn alle gefüllt sind. """ header_rows = 5 - data_rows = self.get_data() + data_rows = self.get_data() # Holt Daten OHNE die 5 Header if not data_rows: debug_print("Keine Datenzeilen vorhanden für get_start_row_index.") return 0 + # 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 + + actual_col_letter = self._get_col_letter(check_column_index + 1) # +1 für 1-basierte Konvertierung + + # Berechne den 0-basierten Startindex für die *Datenliste* data_rows search_start_index_in_data = max(0, min_sheet_row - header_rows - 1) - # DEBUG: Logge, welche Spalte tatsächlich geprüft wird - actual_col_letter = self._get_col_letter(check_column_index + 1) - debug_print(f"get_start_row_index: Suche ab Daten-Index {search_start_index_in_data} nach leerem Timestamp in Spalte {actual_col_letter} (Index {check_column_index}).") + debug_print(f"get_start_row_index: Suche ab Daten-Index {search_start_index_in_data} (Sheet-Zeile {search_start_index_in_data + header_rows + 1}) nach leerem Wert in Spalte '{check_column_key}' ({actual_col_letter}, Index {check_column_index}).") if search_start_index_in_data >= len(data_rows): - debug_print(f"Start-Suchindex ({search_start_index_in_data}) liegt nach oder auf letzter Datenzeile ({len(data_rows)-1}).") - # Prüfe die *letzte* Zeile, falls der Index genau darauf zeigt - if search_start_index_in_data == len(data_rows) -1: - last_row = data_rows[search_start_index_in_data] - if len(last_row) <= check_column_index or not last_row[check_column_index].strip(): - debug_print(f"Letzte Zeile (Daten-Index {search_start_index_in_data}) hat keinen Timestamp in Spalte {actual_col_letter}. Starte hier.") - return search_start_index_in_data - # Ansonsten sind alle vorherigen gefüllt - debug_print(f"Alle Zeilen ab Zeile {min_sheet_row} scheinen einen Zeitstempel in Spalte {actual_col_letter} zu haben. Nächster Daten-Index wäre {len(data_rows)}.") - return len(data_rows) + debug_print(f"Start-Suchindex ({search_start_index_in_data}) liegt nach oder auf letzter Datenzeile ({len(data_rows)-1}). Alle vorherigen Zeilen scheinen gefüllt.") + return len(data_rows) # Index nach der letzten Zeile + # Durchlaufe die Datenzeilen ab dem berechneten Startindex + for i in range(search_start_index_in_data, len(data_rows)): + row = data_rows[i] + current_sheet_row = i + header_rows + 1 - for i, row in enumerate(data_rows[search_start_index_in_data:], start=search_start_index_in_data): - # DEBUG: Logge den geprüften Wert für die ersten paar Iterationen - # if i < search_start_index_in_data + 5: - # checked_value = row[check_column_index].strip() if len(row) > check_column_index else "INDEX_FEHLER" - # debug_print(f" -> Prüfe Zeile {i + header_rows + 1}: Wert in Spalte {actual_col_letter} = '{checked_value}'") + # Prüfe den Wert in der Zielspalte + cell_value = None + 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 + is_empty = False - if len(row) <= check_column_index or not row[check_column_index].strip(): - actual_sheet_row = i + header_rows + 1 - debug_print(f"Erste Zeile ab Zeile {min_sheet_row} ohne Zeitstempel in Spalte {actual_col_letter} (Index {check_column_index}) gefunden: Zeile {actual_sheet_row} (Daten-Index {i})") - return i + # 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_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: + debug_print(f"Erste Zeile ab Zeile {min_sheet_row} ohne Wert in Spalte {actual_col_letter} gefunden: Zeile {current_sheet_row} (Daten-Index {i})") + return i # Gibt den 0-basierten Index *innerhalb der Datenliste* zurück + + # Wenn die Schleife durchläuft, sind alle Zeilen ab dem Start gefüllt last_index = len(data_rows) - debug_print(f"Alle Zeilen ab Zeile {min_sheet_row} haben einen Zeitstempel in Spalte {actual_col_letter}. Nächster Daten-Index wäre {last_index}.") + debug_print(f"Alle Zeilen ab Daten-Index {search_start_index_in_data} (Sheet Zeile {search_start_index_in_data + header_rows + 1}) haben einen Wert in Spalte {actual_col_letter}. Nächster Daten-Index wäre {last_index}.") return last_index - def _get_col_letter(self, col_idx): - """ Konvertiert 1-basierten Spaltenindex in Buchstaben. """ - string = "" - while col_idx > 0: - col_idx, remainder = divmod(col_idx - 1, 26) - string = chr(65 + remainder) + string - return string - # --- NEU HINZUGEFÜGTE METHODE --- + # retry_on_failure Decorator sollte hier angewendet werden @retry_on_failure def batch_update_cells(self, update_data): """ @@ -1082,25 +1106,19 @@ class GoogleSheetHandler: """ if not self.sheet: debug_print("FEHLER: Keine Sheet-Verbindung für Batch-Update.") - return False # Kein Erfolg, da keine Verbindung + return False if not update_data: - # debug_print("Keine Daten für Batch-Update vorhanden.") # Weniger Lärm im Log - return True # Kein Fehler, aber nichts zu tun + return True try: - # Der eigentliche API-Aufruf self.sheet.batch_update(update_data, value_input_option='USER_ENTERED') - # debug_print(f"Batch-Update erfolgreich ({len(update_data)} Zellen/Bereiche aktualisiert).") # Optional weniger Lärm return True except gspread.exceptions.APIError as e: - # Spezifische Fehlerbehandlung für gspread/Google API Fehler - debug_print(f"Google API Fehler beim Batch-Update: {e.response.status_code} - {e.response.text[:500]}") - # Der @retry_on_failure Decorator sollte diesen Fehler fangen und Wiederholungen versuchen + 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 except Exception as e: - # Allgemeine Fehlerbehandlung debug_print(f"Allgemeiner Fehler beim Batch-Update: {type(e).__name__} - {e}") - raise e # Fehler weitergeben + raise e @@ -2356,57 +2374,114 @@ def process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_ debug_print("Brancheneinschätzung (Batch) abgeschlossen.") +# Annahmen: +# - Funktionen debug_print, process_verification_only, process_website_batch, process_branch_batch sind definiert. +# - sheet_handler ist eine initialisierte Instanz von GoogleSheetHandler (mit der korrekten get_start_row_index Methode). +# - Globale Konstante header_rows (oder besser, hol sie vom sheet_handler?) + def run_dispatcher(mode, sheet_handler, row_limit=None): - """Wählt den passenden Batch-Prozess basierend auf dem Modus.""" + """ + Wählt den passenden Batch-Prozess basierend auf dem Modus. + Ermittelt die Startzeile dynamisch basierend auf dem Timestamp in der relevanten Spalte. + """ debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.") - - # Finde Startzeile (erste Zeile ab 7 ohne Zeitstempel in AO) - data = sheet_handler.get_all_data_with_headers() - header_rows = 5 - start_row_index_in_sheet = -1 - for i in range(header_rows + 1, len(data) + 1): - if i < 7: continue - row_index_in_list = i - 1 - row = data[row_index_in_list] - if len(row) <= COLUMN_MAP["Timestamp letzte Prüfung"] or not row[COLUMN_MAP["Timestamp letzte Prüfung"]].strip(): - start_row_index_in_sheet = i - break - - if start_row_index_in_sheet == -1: - debug_print("Keine Zeile ohne Zeitstempel in Spalte AO (ab Zeile 7) gefunden. Dispatcher beendet.") + header_rows = 5 # Definiere die Anzahl der Header-Zeilen + + # --- 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": + # Für combined nehmen wir den frühesten relevanten Timestamp, + # meist der letzte Gesamt-Timestamp (AO), da die Teilprozesse + # ihre eigenen Timestamps prüfen sollten. Oder AT als generellen Start? + # Nehmen wir AT als Startpunkt für den Bereich, den wir betrachten. + start_col_key = "Website Scrape Timestamp" # Spalte AT + # Füge ggf. andere Modi hinzu + + debug_print(f"Dispatcher: Ermittle Startzeile basierend auf Spalte '{start_col_key}'...") + 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) + 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 - # Bestimme Endzeile - if row_limit is not None and row_limit > 0: - end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, len(data)) - else: - end_row_index_in_sheet = len(data) + # Umrechnung des 0-basierten Daten-Index in die 1-basierte Sheet-Zeilennummer + start_row_index_in_sheet = start_data_index + header_rows + 1 - debug_print(f"Dispatcher: Verarbeitung startet ab Zeile {start_row_index_in_sheet}, bis Zeile {end_row_index_in_sheet}.") + # Hole Gesamtzahl der Zeilen für die Endberechnung + all_data = sheet_handler.get_all_data_with_headers() + total_sheet_rows = len(all_data) - if start_row_index_in_sheet > end_row_index_in_sheet: - debug_print("Startzeile liegt nach Endzeile. Keine Verarbeitung.") + # 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 + elif start_row_index_in_sheet > total_sheet_rows: + # Wenn das Sheet nur Header hat oder leer ist + debug_print(f"Sheet hat keine Datenzeilen oder Startzeile ({start_row_index_in_sheet}) ist ungültig. Dispatcher beendet.") return - # Modus auswählen - if mode == "wiki": - process_verification_only(sheet_handler, row_limit) # Nutzt jetzt row_limit intern anders - elif mode == "website": - process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) - elif mode == "branch": - process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) - elif mode == "combined": - debug_print("--- Start Combined Mode: Wiki ---") - process_verification_only(sheet_handler, row_limit) - debug_print("--- Start Combined Mode: Website ---") - # Website und Branch brauchen evtl. aktualisierte Daten nach Wiki -> neu laden? Oder mit alten Daten arbeiten? - # Annahme: Arbeite erstmal mit den Daten wie sie sind. Start/End Row bleiben gleich. - process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) - debug_print("--- Start Combined Mode: Branch ---") - process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) - debug_print("--- Combined Mode abgeschlossen ---") - else: - debug_print(f"Ungültiger Modus '{mode}' im Dispatcher.") + + # --- 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. + 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.") + return + else: # Kein Limit oder negatives Limit -> bis zum Ende des Sheets + end_row_index_in_sheet = total_sheet_rows + + 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 + debug_print("--- Start Combined Mode: Website ---") + process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AT + debug_print("--- Start Combined Mode: Branch ---") + process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AO + debug_print("--- Combined Mode abgeschlossen ---") + else: + debug_print(f"Ungültiger Modus '{mode}' wurde im Dispatcher übergeben.") + + 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 + +# --- Ende run_dispatcher Funktion --- # ==================== SERP API / LINKEDIN FUNCTIONS ====================