# google_sheet_handler.py import os import logging import gspread import pandas as pd from oauth2client.service_account import ServiceAccountCredentials from config import Config, COLUMN_MAP from helpers import retry_on_failure, _get_col_letter class GoogleSheetHandler: """ Kapselt alle Interaktionen mit dem Google Sheet. Finale, robuste Version v2.1.0 """ def __init__(self, sheet_url=None): """ Initialisiert den Handler. Die Verbindung wird bei Bedarf hergestellt ("lazy connect"). """ self.logger = logging.getLogger(__name__ + ".GoogleSheetHandler") self.logger.info("Initialisiere GoogleSheetHandler...") self.sheet_url = sheet_url or Config.SHEET_URL if "docs.google.com" not in self.sheet_url: raise ValueError(f"Ungültige Google Sheet URL in config.py: '{self.sheet_url}'") # Attribute werden immer initialisiert, um AttributeErrors zu vermeiden. self.client = None self.sheet = None # Haupt-Tabellenblatt ('Tabelle1') self._all_data_with_headers = [] self._header_rows = 5 @retry_on_failure def _connect(self): """ Stellt die Verbindung zu Google Sheets her, falls sie noch nicht besteht. """ if self.client: self.logger.debug("Verbindung zu Google Sheets besteht bereits.") return True self.logger.info("Stelle neue Verbindung mit Google Sheets her...") try: creds = ServiceAccountCredentials.from_json_keyfile_name(Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"]) self.client = gspread.authorize(creds) spreadsheet = self.client.open_by_url(self.sheet_url) self.sheet = spreadsheet.sheet1 self.logger.info("Verbindung zu Google Sheets erfolgreich hergestellt.") return True except Exception as e: self.logger.error(f"FEHLER bei der Google Sheets Verbindung: {e}") self.client = None # Verbindung im Fehlerfall zurücksetzen return False @retry_on_failure def load_data(self): """Lädt alle Daten aus dem Haupt-Sheet ('Tabelle1').""" if not self.client and not self._connect(): return False self.logger.info("Lade Daten aus dem Haupt-Google-Sheet...") try: self._all_data_with_headers = self.sheet.get_all_values() self.logger.info(f"Daten erfolgreich geladen: {len(self._all_data_with_headers)} Zeilen.") # Dynamisch die Anzahl der Header-Zeilen finden for i, row in enumerate(self._all_data_with_headers): if "CRM Name" in row: # Annahme: "CRM Name" ist in der Haupt-Header-Zeile self._header_rows = i + 1 break return True except Exception as e: self.logger.critical(f"Allgemeiner Fehler beim Laden der Google Sheet Daten: {e}") return False def get_all_data_with_headers(self): """Gibt alle aktuell im Handler gespeicherten Daten inklusive Header zurück.""" return self._all_data_with_headers.copy() def get_sheet_as_dataframe(self, sheet_name): """Liest ein komplettes Tabellenblatt und gibt es als Pandas DataFrame zurück.""" try: if not self.client and not self._connect(): return None self.logger.debug(f"Lese Tabellenblatt '{sheet_name}' als DataFrame...") worksheet = self.client.open_by_url(self.sheet_url).worksheet(sheet_name) data = worksheet.get_all_records() df = pd.DataFrame(data) self.logger.info(f"{len(df)} Zeilen aus '{sheet_name}' als DataFrame geladen.") return df except gspread.exceptions.WorksheetNotFound: self.logger.warning(f"Tabellenblatt '{sheet_name}' nicht gefunden. Erstelle leeren DataFrame.") return pd.DataFrame() except Exception as e: self.logger.error(f"Fehler beim Lesen des Sheets '{sheet_name}' als DataFrame: {e}") return None def append_rows(self, sheet_name, values_to_append): """Hängt eine Liste von Zeilen an ein Tabellenblatt an.""" try: if not self.client and not self._connect(): return False self.logger.debug(f"Hänge {len(values_to_append)} Zeilen an das Tabellenblatt '{sheet_name}' an...") worksheet = self.client.open_by_url(self.sheet_url).worksheet(sheet_name) worksheet.append_rows(values_to_append, value_input_option='USER_ENTERED') self.logger.info(f"{len(values_to_append)} Zeilen erfolgreich an '{sheet_name}' angehängt.") return True except Exception as e: self.logger.error(f"Fehler beim Anhängen von Zeilen an das Sheet '{sheet_name}': {e}") return False def clear_and_write_data(self, sheet_name, data): """Leert das angegebene Tabellenblatt vollständig und schreibt neue Daten hinein.""" try: if not self.client and not self._connect(): return False self.logger.info(f"Greife auf Tabellenblatt '{sheet_name}' zu, um es zu leeren und neu zu beschreiben...") worksheet = self.client.open_by_url(self.sheet_url).worksheet(sheet_name) worksheet.clear() if not data: self.logger.warning("Keine Daten zum Schreiben in '{sheet_name}' vorhanden.") return True end_col_letter = _get_col_letter(len(data[0])) range_to_update = f'A1:{end_col_letter}{len(data)}' worksheet.update(range_name=range_to_update, values=data) self.logger.info(f"Schreiben von {len(data)} Zeilen in '{sheet_name}' erfolgreich.") return True except gspread.exceptions.WorksheetNotFound: self.logger.error(f"FATAL: Das Tabellenblatt '{sheet_name}' wurde nicht gefunden.") return False except Exception as e: self.logger.error(f"FATAL: Unerwarteter Fehler bei clear_and_write_data für '{sheet_name}': {e}") return False @retry_on_failure def batch_update_cells(self, update_data): """Führt ein Batch-Update im Haupt-Sheet durch.""" if not self.sheet and not self._connect(): self.logger.error("FEHLER: Keine Sheet-Verbindung fuer Batch-Update.") return False if not update_data: return True sanitized_update_data = [] for item in update_data: if 'range' in item and 'values' in item and isinstance(item['values'], list): sanitized_values = [[str(cell) if cell is not None else "" for cell in row] for row in item['values']] sanitized_update_data.append({'range': item['range'], 'values': sanitized_values}) if not sanitized_update_data: return True total_cells = sum(len(row) for item in sanitized_update_data for row in item.get('values', [])) self.logger.debug(f"Sende Batch-Update mit {len(sanitized_update_data)} Anfragen ({total_cells} Zellen)...") self.sheet.batch_update(sanitized_update_data, value_input_option='USER_ENTERED') self.logger.info(f"Batch-Update mit {total_cells} Zellen erfolgreich gesendet.") return True