diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 306162ed..4a457678 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -953,38 +953,48 @@ class GoogleSheetHandler: self.sheet_values = [] self.headers = [] # Um Header-Zeilen zu speichern self._connect() - self._load_data() + # _load_data wird jetzt nach erfolgreicher Verbindung aufgerufen + if self.sheet: + self._load_data() @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...") - scope = ["https://www.googleapis.com/auth/spreadsheets"] - 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 - debug_print("Verbindung zu Google Sheets erfolgreich.") + try: + scope = ["https://www.googleapis.com/auth/spreadsheets"] + 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) + debug_print("Verbindung zu Google Sheets erfolgreich.") + 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 @retry_on_failure def _load_data(self): """Lädt alle Daten aus dem Sheet.""" if not self.sheet: debug_print("Fehler: Keine Sheet-Verbindung zum Laden der Daten.") + self.sheet_values = [] + self.headers = [] return debug_print("Lade Daten aus Google Sheet...") self.sheet_values = self.sheet.get_all_values() - if len(self.sheet_values) >= 5: - self.headers = self.sheet_values[:5] # Zeilen 1-5 als Header speichern + if len(self.sheet_values) >= 1: # Mindestens Header sollte da sein + self.headers = self.sheet_values[:5] # Annahme: Zeilen 1-5 sind Header else: - self.headers = self.sheet_values[:] # Alle Zeilen als Header, falls weniger als 5 + self.headers = [] + debug_print("Warnung: Google Sheet scheint leer zu sein.") debug_print(f"Daten geladen: {len(self.sheet_values)} Zeilen insgesamt.") - # Hier könnte die COLUMN_MAP dynamisch erstellt werden, falls gewünscht def get_data(self): """Gibt die geladenen Daten zurück (ohne Header).""" - # Annahme: Die ersten 5 Zeilen sind Header - header_rows = 5 + header_rows = 5 # Annahme: Zeile 1-5 sind Header if len(self.sheet_values) <= header_rows: return [] return self.sheet_values[header_rows:] @@ -993,131 +1003,86 @@ class GoogleSheetHandler: """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=COLUMN_MAP["Website Scrape Timestamp"], 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. - - Args: - check_column_index (int): Der 0-basierte Index der zu prüfenden Spalte. - min_sheet_row (int): Die 1-basierte Zeilennummer im Sheet, ab der gesucht werden soll. - - Returns: - int: Der 0-basierte Index in der Datenliste (ohne Header), - oder der Index nach der letzten Zeile, wenn alle gefüllt sind. """ - header_rows = 5 # Annahme: Zeilen 1-5 sind Header - data_rows = self.get_data() # Holt Daten ohne Header + header_rows = 5 + data_rows = self.get_data() if not data_rows: - debug_print("Keine Datenzeilen vorhanden.") + debug_print("Keine Datenzeilen vorhanden für get_start_row_index.") return 0 - # Berechne den 0-basierten Startindex für die *Datenliste*, - # der der min_sheet_row entspricht. search_start_index_in_data = max(0, min_sheet_row - header_rows - 1) + # Stelle sicher, dass der Startindex nicht außerhalb der Liste liegt + if search_start_index_in_data >= len(data_rows): + col_letter = self._get_col_letter(check_column_index + 1) + debug_print(f"Start-Suchindex ({search_start_index_in_data}) liegt nach der letzten Datenzeile ({len(data_rows)-1}). Suche ab letzter Zeile für Spalte {col_letter}.") + search_start_index_in_data = len(data_rows) -1 # Beginne bei der letzten Zeile falls Start zu weit + if search_start_index_in_data < 0: return 0 # Falls gar keine Datenzeilen + + for i, row in enumerate(data_rows[search_start_index_in_data:], start=search_start_index_in_data): + # Sicherheitscheck für Zeilenlänge if len(row) <= check_column_index or not row[check_column_index].strip(): - actual_sheet_row = i + header_rows + 1 # 1-basierte Zeilennummer im Sheet - # Finde den Spaltenbuchstaben für die Log-Ausgabe + actual_sheet_row = i + header_rows + 1 col_letter = self._get_col_letter(check_column_index + 1) debug_print(f"Erste Zeile ab Zeile {min_sheet_row} ohne Zeitstempel in Spalte {col_letter} (Index {check_column_index}) gefunden: Zeile {actual_sheet_row} (Daten-Index {i})") - return i # Gibt den 0-basierten Index *innerhalb der Datenliste* zurück + return i - # Wenn alle Zeilen ab min_sheet_row einen Zeitstempel haben last_index = len(data_rows) col_letter = self._get_col_letter(check_column_index + 1) debug_print(f"Alle Zeilen ab Zeile {min_sheet_row} haben einen Zeitstempel in Spalte {col_letter}. Nächster Daten-Index wäre {last_index}.") - return last_index # Gibt den Index nach der letzten Datenzeile zurück + return last_index - # Hilfsfunktion zur Umwandlung von Spaltenindex in Buchstaben (für Logs) def _get_col_letter(self, col_idx): - """ Konvertiert 1-basierten Spaltenindex in Buchstaben (A, B, ..., Z, AA, ...). """ + """ 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 -# Anpassung in run_dispatcher: Verwende die neue Methode mit Spalte AT -def run_dispatcher(mode, sheet_handler, row_limit=None): - """Wählt den passenden Batch-Prozess basierend auf dem Modus.""" - debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.") - - # Finde Startzeile basierend auf Timestamp in Spalte AT (Index 45) - # Verwende die neue Methode des Handlers - start_data_index = sheet_handler.get_start_row_index(check_column_index=COLUMN_MAP["Website Scrape Timestamp"], min_sheet_row=7) - header_rows = 5 - start_row_index_in_sheet = start_data_index + header_rows + 1 - - all_data = sheet_handler.get_all_data_with_headers() # Hole alle Daten - total_sheet_rows = len(all_data) - - if start_row_index_in_sheet > total_sheet_rows: - debug_print(f"Startzeile ({start_row_index_in_sheet}) liegt hinter der letzten Sheet-Zeile ({total_sheet_rows}). Dispatcher beendet.") - return - - # Bestimme Endzeile - if row_limit is not None and row_limit > 0: - # Berechne Endzeile basierend auf Startzeile und Limit - end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows) - else: - end_row_index_in_sheet = total_sheet_rows # Bis zum Ende des Sheets - - debug_print(f"Dispatcher: Verarbeitung startet ab Zeile {start_row_index_in_sheet}, bis Zeile {end_row_index_in_sheet}.") - - if start_row_index_in_sheet > end_row_index_in_sheet: - debug_print("Startzeile liegt nach Endzeile. Keine Verarbeitung.") - return - - # --- Modusausführung (bleibt gleich, ABER die aufgerufenen Funktionen müssen den Timestamp prüfen!) --- - if mode == "wiki": - # process_verification_only muss AN Timestamp prüfen - process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) - elif mode == "website": - # process_website_batch muss AT Timestamp prüfen - process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) - elif mode == "branch": - # process_branch_batch muss AO Timestamp prüfen - 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, 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}' im Dispatcher.") - + # --- NEU HINZUGEFÜGTE METHODE --- @retry_on_failure def batch_update_cells(self, update_data): """ - Führt ein Batch-Update im Google Sheet durch. + Führt ein Batch-Update im Google Sheet durch. Beinhaltet Fehlerbehandlung. + Args: update_data (list): Eine Liste von Dictionaries, jedes mit 'range' und 'values'. z.B. [{'range': 'A1', 'values': [['Wert']]}, ...] + + Returns: + bool: True bei Erfolg, False bei Fehler nach Retries. """ if not self.sheet: - debug_print("Fehler: Keine Sheet-Verbindung für Batch-Update.") - return False + debug_print("FEHLER: Keine Sheet-Verbindung für Batch-Update.") + return False # Kein Erfolg, da keine Verbindung if not update_data: - debug_print("Keine Daten für Batch-Update vorhanden.") + # debug_print("Keine Daten für Batch-Update vorhanden.") # Weniger Lärm im Log return True # Kein Fehler, aber nichts zu tun + try: - self.sheet.batch_update(update_data) - debug_print(f"Batch-Update erfolgreich ({len(update_data)} Zellen/Bereiche aktualisiert).") + # 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: - debug_print(f"Google API Fehler beim Batch-Update: {e}") - # Hier könnte spezifische Fehlerbehandlung erfolgen (z.B. RateLimit) - raise # Fehler weitergeben, damit retry greifen kann + # 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 + raise e # Fehler weitergeben, damit der Decorator ihn fängt except Exception as e: - debug_print(f"Allgemeiner Fehler beim Batch-Update: {e}") - raise # Fehler weitergeben + # Allgemeine Fehlerbehandlung + debug_print(f"Allgemeiner Fehler beim Batch-Update: {type(e).__name__} - {e}") + raise e # Fehler weitergeben + # ==================== WIKIPEDIA SCRAPER ====================