243 lines
9.8 KiB
Python
243 lines
9.8 KiB
Python
# --- START OF FILE google_sheet_handler.py ---
|
|
|
|
#!/usr/bin/env python3
|
|
"""
|
|
google_sheet_handler.py
|
|
|
|
Klasse zur Kapselung der Interaktionen mit dem Google Sheet.
|
|
Stellt Verbindung her, lädt Daten und führt Batch-Updates durch.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import logging
|
|
import traceback
|
|
from datetime import datetime
|
|
|
|
import gspread
|
|
from oauth2client.service_account import ServiceAccountCredentials
|
|
|
|
# Import der abhängigen Module
|
|
from config import Config, CREDENTIALS_FILE, COLUMN_MAP
|
|
from helpers import retry_on_failure
|
|
|
|
class GoogleSheetHandler:
|
|
"""
|
|
Kapselt die Interaktionen mit dem Google Sheet, inklusive Verbindung,
|
|
Daten laden und Batch-Updates. Nutzt den retry_on_failure Decorator.
|
|
"""
|
|
def __init__(self):
|
|
"""
|
|
Initialisiert den Handler, stellt die Verbindung her und laedt die Daten.
|
|
"""
|
|
self.logger = logging.getLogger(__name__ + ".GoogleSheetHandler")
|
|
self.sheet = None
|
|
self.sheet_values = []
|
|
self._header_rows = 5 # Annahme: Die ersten 5 Zeilen sind Header
|
|
|
|
self.logger.info("Initialisiere GoogleSheetHandler...")
|
|
try:
|
|
self._connect()
|
|
if self.sheet:
|
|
self.load_data()
|
|
else:
|
|
self.logger.critical(
|
|
"GoogleSheetHandler Init FEHLER: Verbindung konnte nicht hergestellt werden (sheet ist None)."
|
|
)
|
|
raise ConnectionError("Google Sheet Handler Init failed: Verbindung konnte nicht hergestellt werden.")
|
|
except Exception as e:
|
|
self.logger.critical(f"FATAL: Fehler bei Initialisierung von GoogleSheetHandler: {type(e).__name__} - {e}")
|
|
self.logger.debug(traceback.format_exc())
|
|
raise ConnectionError(f"Google Sheet Handler Init failed: {e}")
|
|
|
|
@retry_on_failure
|
|
def _connect(self):
|
|
"""Stellt Verbindung zum Google Sheet her."""
|
|
self.sheet = None
|
|
self.logger.info("Versuche Verbindung mit Google Sheets herstellen...")
|
|
try:
|
|
if not os.path.exists(CREDENTIALS_FILE):
|
|
raise FileNotFoundError(f"Credential-Datei nicht gefunden: {CREDENTIALS_FILE}")
|
|
|
|
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
|
|
self.logger.info("Verbindung zu Google Sheets erfolgreich.")
|
|
except Exception as e:
|
|
self.logger.error(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}")
|
|
self.logger.debug(traceback.format_exc())
|
|
raise e
|
|
|
|
@retry_on_failure
|
|
def load_data(self):
|
|
"""
|
|
Laedt alle Daten aus dem Sheet und aktualisiert self.sheet_values.
|
|
"""
|
|
if not self.sheet:
|
|
self.logger.error("Fehler: Keine Sheet-Verbindung zum Laden der Daten.")
|
|
self.sheet_values = []
|
|
return False
|
|
|
|
self.logger.info("Lade Daten aus Google Sheet...")
|
|
try:
|
|
self.sheet_values = self.sheet.get_all_values()
|
|
|
|
if not self.sheet_values:
|
|
self.logger.warning("Google Sheet scheint leer zu sein oder get_all_values() lieferte keine Daten.")
|
|
self.headers = []
|
|
return True
|
|
|
|
num_rows = len(self.sheet_values)
|
|
num_cols = len(self.sheet_values[0]) if num_rows > 0 else 0
|
|
self.logger.info(f"Daten neu geladen: {num_rows} Zeilen, {num_cols} Spalten.")
|
|
|
|
try:
|
|
max_col_idx_in_map = max(COLUMN_MAP.values())
|
|
if num_cols <= max_col_idx_in_map:
|
|
self.logger.warning(
|
|
f"Geladenes Sheet hat {num_cols} Spalten, erwartet werden aber mindestens "
|
|
f"{max_col_idx_in_map + 1} basierend auf COLUMN_MAP."
|
|
)
|
|
except ValueError:
|
|
self.logger.warning("COLUMN_MAP scheint leer zu sein. Kann Spaltenanzahl nicht pruefen.")
|
|
except Exception as e:
|
|
self.logger.error(f"Fehler bei der Pruefung der Spaltenanzahl gegen COLUMN_MAP: {e}")
|
|
|
|
if num_rows > 0:
|
|
self.headers = self.sheet_values[0]
|
|
else:
|
|
self.headers = []
|
|
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(f"Allgemeiner Fehler beim Laden der Google Sheet Daten: {type(e).__name__} - {e}")
|
|
self.logger.debug(traceback.format_exc())
|
|
raise e
|
|
|
|
def get_data(self):
|
|
"""
|
|
Gibt die aktuell im Handler gespeicherten Datenzeilen zurueck
|
|
(ohne die ersten N Header-Zeilen).
|
|
"""
|
|
if not self.sheet_values or len(self.sheet_values) <= self._header_rows:
|
|
self.logger.debug(
|
|
f"get_data: Keine Datenzeilen verfuegbar "
|
|
f"(geladen: {len(self.sheet_values) if self.sheet_values else 0} Zeilen, "
|
|
f"{self._header_rows} Header)."
|
|
)
|
|
return []
|
|
return self.sheet_values[self._header_rows:].copy()
|
|
|
|
def get_all_data_with_headers(self):
|
|
"""Gibt alle aktuell im Handler gespeicherten Daten inklusive Header zurueck."""
|
|
if not self.sheet_values:
|
|
self.logger.debug("get_all_data_with_headers: Keine Daten im Handler gespeichert.")
|
|
return []
|
|
return self.sheet_values.copy()
|
|
|
|
def _get_col_letter(self, col_idx_1_based):
|
|
"""
|
|
Konvertiert einen 1-basierten Spaltenindex in den entsprechenden
|
|
Google Sheets Spaltenbuchstaben (A, B, ..., Z, AA, ...).
|
|
"""
|
|
if not isinstance(col_idx_1_based, int) or col_idx_1_based < 1:
|
|
self.logger.error(f"Ungueltiger Spaltenindex ({col_idx_1_based}) fuer _get_col_letter erhalten.")
|
|
return None
|
|
|
|
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 0-basierten Index in der DATENliste (ohne Header),
|
|
ab einer Mindestzeilennummer im Sheet, in der der Wert in der
|
|
Spalte (definiert durch check_column_key) EXAKT LEER ("") ist.
|
|
"""
|
|
if not self.load_data():
|
|
self.logger.error("Fehler beim Laden der Daten fuer get_start_row_index.")
|
|
return -1
|
|
|
|
data_rows = self.get_data()
|
|
if not data_rows:
|
|
self.logger.info("Keine Datenzeilen im Sheet gefunden. Startindex fuer leere Zelle ist 0.")
|
|
return 0
|
|
|
|
check_column_index = COLUMN_MAP.get(check_column_key)
|
|
if check_column_index is None:
|
|
self.logger.critical(f"FEHLER: Schluessel '{check_column_key}' nicht in COLUMN_MAP gefunden!")
|
|
return -1
|
|
|
|
actual_col_letter = self._get_col_letter(check_column_index + 1)
|
|
if actual_col_letter is None:
|
|
actual_col_letter = f"Index_{check_column_index + 1}"
|
|
|
|
search_start_index_in_data = max(0, (min_sheet_row - 1) - self._header_rows)
|
|
self.logger.info(
|
|
f"get_start_row_index: Suche ab Daten-Index {search_start_index_in_data} "
|
|
f"(Sheet-Zeile {search_start_index_in_data + self._header_rows + 1}) "
|
|
f"nach EXAKT LEEREM Wert (=='') in Spalte '{check_column_key}' ({actual_col_letter})..."
|
|
)
|
|
|
|
if search_start_index_in_data >= len(data_rows):
|
|
self.logger.warning(
|
|
f"Start-Suchindex in Daten ({search_start_index_in_data}) liegt hinter der letzten Datenzeile ({len(data_rows)})."
|
|
)
|
|
return len(data_rows)
|
|
|
|
for i in range(search_start_index_in_data, len(data_rows)):
|
|
row = data_rows[i]
|
|
current_sheet_row = i + self._header_rows + 1
|
|
cell_value = ""
|
|
is_exactly_empty = True
|
|
if len(row) > check_column_index:
|
|
cell_value = row[check_column_index]
|
|
if cell_value != "":
|
|
is_exactly_empty = False
|
|
|
|
if is_exactly_empty:
|
|
self.logger.info(
|
|
f"Erste Zeile ab Sheet-Zeile {min_sheet_row} mit EXAKT LEEREM Wert in Spalte "
|
|
f"{actual_col_letter} gefunden: Zeile {current_sheet_row} (Daten-Index {i})"
|
|
)
|
|
return i
|
|
|
|
last_data_index = len(data_rows)
|
|
self.logger.info(
|
|
f"Alle Zeilen ab Daten-Index {search_start_index_in_data} haben einen "
|
|
f"nicht-leeren Wert in Spalte {actual_col_letter}. Naechster Daten-Index waere {last_data_index}."
|
|
)
|
|
return last_data_index
|
|
|
|
@retry_on_failure
|
|
def batch_update_cells(self, update_data):
|
|
"""
|
|
Fuehrt ein Batch-Update im Google Sheet durch.
|
|
|
|
Args:
|
|
update_data (list): Eine Liste von Dictionaries, jedes mit 'range' (str)
|
|
und 'values' (list of lists).
|
|
Returns:
|
|
bool: True bei Erfolg, False bei endgueltigem Fehler.
|
|
"""
|
|
if not self.sheet:
|
|
self.logger.error("FEHLER: Keine Sheet-Verbindung fuer Batch-Update.")
|
|
return False
|
|
if not update_data:
|
|
return True
|
|
|
|
try:
|
|
total_cells_to_update = sum(len(row) for item in update_data for row in item.get('values', []))
|
|
self.logger.debug(f" -> Versuche sheet.batch_update mit {len(update_data)} Anfragen ({total_cells_to_update} Zellen)...")
|
|
self.sheet.batch_update(update_data, value_input_option='USER_ENTERED')
|
|
return True
|
|
except Exception:
|
|
self.logger.error(f"Endgueltiger Fehler beim Batch-Update nach Retries. Kann {len(update_data)} Operationen nicht durchfuehren.")
|
|
return False
|
|
|
|
# --- END OF FILE google_sheet_handler.py --- |