# google_sheet_handler.py __version__ = "v2.0.1" import os import logging import gspread import pandas as pd from oauth2client.service_account import ServiceAccountCredentials from config import Config, COLUMN_MAP, CREDENTIALS_FILE from helpers import retry_on_failure, _get_col_letter class GoogleSheetHandler: """ Kapselt alle Interaktionen mit dem Google Sheet. Finale, robuste Version v2.1.2 """ def __init__(self, sheet_url=None): 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: '{self.sheet_url}'") self.client = None self.sheet = None self._all_data_with_headers = [] self._header_rows = 5 @retry_on_failure def _connect(self): if self.client: return True self.logger.info("Stelle neue Verbindung mit Google Sheets her...") try: if not os.path.exists(CREDENTIALS_FILE): raise FileNotFoundError(f"Credential-Datei nicht gefunden: {CREDENTIALS_FILE}") creds = ServiceAccountCredentials.from_json_keyfile_name(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 erfolgreich.") return True except Exception as e: self.logger.error(f"FEHLER bei Google Sheets Verbindung: {e}") self.client = None return False @retry_on_failure def load_data(self): if not self.client and not self._connect(): return False self.logger.info("Lade Daten aus dem Haupt-Sheet ('Tabelle1')...") try: self._all_data_with_headers = self.sheet.get_all_values() self.logger.info(f"Daten geladen: {len(self._all_data_with_headers)} Zeilen.") for i, row in enumerate(self._all_data_with_headers): if "CRM Name" in row: self._header_rows = i + 1 break return True except Exception as e: self.logger.critical(f"Fehler beim Laden der Sheet Daten: {e}") return False def get_all_data_with_headers(self): 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. NEU: Funktioniert auch, wenn die Header-Zeile doppelte Spaltennamen enthält. """ 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) # Lese alle Werte als Liste von Listen, das ist robuster all_values = worksheet.get_all_values() if not all_values: self.logger.warning(f"Tabellenblatt '{sheet_name}' ist leer. Erstelle leeren DataFrame.") return pd.DataFrame() # Nimm die erste Zeile als Header und die restlichen als Daten header = all_values[0] data = all_values[1:] df = pd.DataFrame(data, columns=header) 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): try: if not self.client and not self._connect(): return False worksheet = self.client.open_by_url(self.sheet_url).worksheet(sheet_name) worksheet.append_rows(values, value_input_option='USER_ENTERED') self.logger.info(f"{len(values)} 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): try: if not self.client and not self._connect(): return False 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 Exception as e: self.logger.error(f"Fehler bei clear_and_write_data für '{sheet_name}': {e}") return False def batch_update_cells(self, update_data): 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 def get_main_sheet_name(self): """ Stellt eine Verbindung sicher und gibt den Namen des Haupt-Tabellenblatts zurück. """ if not self.sheet and not self._connect(): self.logger.error("FEHLER: Kann Sheet-Namen nicht abrufen, da keine Verbindung besteht.") return None return self.sheet.title