import csv from datetime import datetime import collections import os.path from google.oauth2 import service_account from googleapiclient.discovery import build from googleapiclient.errors import HttpError # --- Konfiguration --- CSV_FILENAME = 'Namensliste.csv' GOOGLE_DOC_TITLE = f"Gruppenlisten Kinderhaus St. Martin Neuching (Service Acc) - {datetime.now().strftime('%Y-%m-%d_%H-%M')}" # Zeit hinzugefügt für Eindeutigkeit TARGET_FOLDER_ID = "18DNQaH9zbcBzwhckJI-4Uah-WXTXg6bg" # <<-- IHRE ORDNER-ID EINRICHTUNG = "Kinderhaus St. Martin Neuching" FOTODATUM = "02. - 05.06.2025" GRUPPENNAME_SUFFIX = "gruppe" FOTOGRAF_NAME = "Kinderfotos Erding" FOTOGRAF_ADRESSE = "Gartenstr. 10 85445 Oberding" FOTOGRAF_WEB = "www.kinderfotos-erding.de" FOTOGRAF_TEL = "08122-8470867" SCOPES = ['https://www.googleapis.com/auth/documents'] # Optional, wenn Probleme auftreten, oder für mehr Dateioperationen: # SCOPES.append('https://www.googleapis.com/auth/drive.file') SERVICE_ACCOUNT_FILE = 'service_account.json' # --- Ende Konfiguration --- def get_docs_service_with_service_account(): """Erstellt den API-Dienst mit einem Service Account und gibt Service und Credentials zurück.""" creds = None try: creds = service_account.Credentials.from_service_account_file( SERVICE_ACCOUNT_FILE, scopes=SCOPES) except Exception as e: print(f"Fehler beim Laden der Service Account Credentials aus '{SERVICE_ACCOUNT_FILE}': {e}") return None, None # Gibt Tuple zurück try: # Build für Docs Service docs_service = build('docs', 'v1', credentials=creds) # Optional: Build für Drive Service, wenn benötigt für komplexere Ordneroperationen # drive_service = build('drive', 'v3', credentials=creds) return docs_service, creds # Gibt Tuple zurück except HttpError as err: print(f"Ein Fehler beim Erstellen des/der API Service(s) mit Service Account ist aufgetreten: {err}") return None, None except Exception as e: print(f"Ein unerwarteter Fehler beim Erstellen des/der API Service(s): {e}") return None, None def create_google_doc_from_csv(docs_service, folder_id): # Nimmt docs_service und folder_id entgegen """Liest CSV, verarbeitet Daten und erstellt/befüllt das Google Doc im angegebenen Ordner.""" kinder_nach_gruppen = collections.defaultdict(list) try: with open(CSV_FILENAME, mode='r', encoding='utf-8-sig', newline='') as csvfile: reader = csv.DictReader(csvfile, delimiter=';') for row in reader: vorname = row.get('Vorname', '').strip() nachname = row.get('Nachname', '').strip() gruppe_original = row.get('Gruppe', '').strip() if not vorname or not nachname or not gruppe_original: print(f"Warnung: Zeile übersprungen wegen fehlender Daten: {row}") continue kinder_nach_gruppen[gruppe_original].append({ 'Nachname': nachname, 'Vorname': vorname }) except FileNotFoundError: print(f"FEHLER: Die Datei '{CSV_FILENAME}' wurde nicht gefunden.") return None except Exception as e: print(f"FEHLER beim Lesen der CSV-Datei: {e}") return None if not kinder_nach_gruppen: print("Keine Daten aus der CSV-Datei geladen.") return None for gruppe_key in kinder_nach_gruppen: kinder_nach_gruppen[gruppe_key].sort(key=lambda x: (x['Nachname'].lower(), x['Vorname'].lower())) sorted_gruppen_namen = sorted(kinder_nach_gruppen.keys()) stand_zeit = datetime.now().strftime("%d.%m.%Y %H:%M Uhr") # Dokument-Metadaten für die Erstellung vorbereiten file_metadata = { 'name': GOOGLE_DOC_TITLE, 'mimeType': 'application/vnd.google-apps.document' } if folder_id: file_metadata['parents'] = [folder_id] try: # Wichtig: Wir verwenden den Drive Service, um die Datei mit parentId zu erstellen. # Der Docs Service kann keine parentId direkt beim .create() angeben. # Daher brauchen wir den Drive Service doch. # Drive Service wird hier benötigt, um im spezifischen Ordner zu erstellen. # Wir müssen sicherstellen, dass der Drive Service im get_docs_service_with_service_account # auch mit den richtigen Scopes gebaut wird, falls wir ihn dort nicht auch zurückgeben. # Einfacher: Wir nehmen an, `docs_service` ist der `build('docs', ...)` # und wir brauchen einen separaten `drive_service`. # Die Credentials `service_creds` aus `main` werden hierfür benötigt. # Das bedeutet, `service_creds` muss an diese Funktion übergeben werden. # NEIN, Korrektur: googleapiclient.discovery.build('docs', 'v1').documents().create() # unterstützt KEIN 'parents'. Das ist eine Drive API Eigenschaft. # Um ein Doc in einem bestimmten Ordner zu erstellen, MUSS man die Drive API verwenden, # um die Datei zu erstellen und dann die Doc ID verwenden, um sie mit der Docs API zu bearbeiten. # --- ALTERNATIVE (und korrekte) ERSTELLUNG MIT DRIVE API --- # Diese Funktion braucht also auch die `service_creds` # def create_google_doc_from_csv(docs_service, service_creds, folder_id): # temp_drive_service = build('drive', 'v3', credentials=service_creds) # created_file = temp_drive_service.files().create(body=file_metadata, fields='id').execute() # document_id = created_file.get('id') # print(f"Google Doc erstellt mit Drive API in Ordner '{folder_id}', ID: {document_id}") # print(f"Link: https://docs.google.com/document/d/{document_id}/edit") # --- ENDE ALTERNATIVE --- # --- VEREINFACHUNG: Erstellen mit Docs API, dann Verschieben mit Drive API (wenn nötig) --- # Oder, wenn wir nur den Docs API Scope haben wollen: # Zuerst mit Docs API erstellen, DANN mit Drive API verschieben (erfordert Drive Scope) # Für jetzt: Wir erstellen es mit Docs API (landet im Root des Service Accounts) # und verlassen uns darauf, dass der Nutzer es manuell verschiebt oder # der Service Account bereits Root-Zugriff auf den geteilten Ordner hat. # Das ist nicht ideal. # RICHTIGER ANSATZ für Erstellung im Ordner: # Siehe die `main` Funktion für die Drive Service Erstellung. # Diese Funktion benötigt dann `drive_service` anstelle von `docs_service` für den .create() Teil. # Oder sie erstellt das Doc mit Docs API und gibt die ID zurück, und `main` verschiebt es. # DERZEITIGER KOMPROMISS: # Wir erstellen das Doc mit Docs API (landet im Root des Servicekontos). # Der Nutzer muss es manuell in den geteilten Ordner verschieben. # ODER wir implementieren das Verschieben. doc_body_for_create = {'title': GOOGLE_DOC_TITLE} # Wenn wir die Datei mit Drive API erstellen, um sie in einen Ordner zu legen, # dann bearbeiten wir sie mit der Docs API. # Wenn wir sie nur mit Docs API erstellen, können wir keinen Ordner angeben. # => Für die Erstellung im Ordner brauchen wir den Drive Service. # Nehmen wir an, `main` übergibt uns `drive_service` und `docs_service`. # Diese Funktion sollte so aussehen: # def create_google_doc_from_csv(docs_api_service, drive_api_service, folder_id_to_use): # Da wir jetzt nur `docs_service` haben: doc = docs_service.documents().create(body=doc_body_for_create).execute() document_id = doc.get('documentId') print(f"Google Doc erstellt (im Root des Service Accounts) mit ID: {document_id}") print(f"Link: https://docs.google.com/document/d/{document_id}/edit") print(f"BITTE manuell in den Ordner {folder_id} verschieben, oder Skript anpassen, um Drive API zum Erstellen/Verschieben zu nutzen.") except HttpError as err: print(f"Fehler beim Erstellen des Google Dokuments: {err}") return None except Exception as e_create: print(f"Allgemeiner Fehler beim Erstellen des Dokuments: {e_create}") return None requests = [] for i, gruppe_original in enumerate(sorted_gruppen_namen): kinder_liste = kinder_nach_gruppen[gruppe_original] anzahl_kinder = len(kinder_liste) gruppe_display_name = gruppe_original + GRUPPENNAME_SUFFIX header_text_for_page = ( f"{EINRICHTUNG}\t\t\t{FOTOGRAF_NAME}\n" f"{FOTODATUM}\n\n" ) if i == 0: requests.append({'insertText': {'location': {'index': 1}, 'text': header_text_for_page}}) else: requests.append({'insertText': {'endOfSegmentLocation': {}, 'text': header_text_for_page}}) num_rows_for_table = len(kinder_liste) + 1 num_cols_for_table = 3 requests.append({ 'insertTable': { 'endOfSegmentLocation': {}, 'rows': num_rows_for_table, 'columns': num_cols_for_table } }) table_text_content = [] table_text_content.append("Nachname\tVorname\tGruppe") for kind in kinder_liste: table_text_content.append(f"{kind['Nachname']}\t{kind['Vorname']}\t{gruppe_display_name}") full_table_text = "\n".join(table_text_content) + "\n" requests.append({ 'insertText': { 'endOfSegmentLocation': {}, 'text': full_table_text } }) footer_text_for_page = ( f"\n{anzahl_kinder} angemeldete Kinder\n\n" "Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden\n" "Kinder an die Anmeldung erinnern.\n\n" f"Stand {stand_zeit}\n\n" f"{FOTOGRAF_NAME}\n{FOTOGRAF_ADRESSE}\n{FOTOGRAF_WEB}\n{FOTOGRAF_TEL}\n" ) requests.append({ 'insertText': { 'endOfSegmentLocation': {}, 'text': footer_text_for_page } }) if i < len(sorted_gruppen_namen) - 1: requests.append({'insertPageBreak': {'endOfSegmentLocation': {}}}) if requests: try: print("Sende Batch Update an Google Docs API...") print("Anzahl der Requests:", len(requests)) docs_service.documents().batchUpdate( # Verwende docs_service hier documentId=document_id, body={'requests': requests} ).execute() print("Dokument erfolgreich befüllt.") except HttpError as err: print(f"Fehler beim Befüllen des Google Dokuments: {err}") error_details = "Keine Fehlerdetails im Content." if err.content: try: error_details = err.content.decode('utf-8') except: pass # Ignoriere Dekodierungsfehler print(f"Details zum Fehler ({err.resp.status} {err._get_reason()}): {error_details}") return document_id if __name__ == '__main__': print(f"Info: Verwendetes Fotodatum: {FOTODATUM}") print(f"Info: Gruppennamen werden mit Suffix '{GRUPPENNAME_SUFFIX}' versehen.") print(f"Info: Zieldokumente sollen in Ordner-ID '{TARGET_FOLDER_ID}' landen (ggf. manuell oder per Drive API).") # Service Account Authentifizierung service_tuple = get_docs_service_with_service_account() if service_tuple and service_tuple[0] and service_tuple[1]: # Prüfe ob docs_service und creds da sind docs_api_service, service_creds = service_tuple # --- Um ein Dokument direkt im Ordner zu erstellen, benötigen wir die Drive API --- # Zusätzlichen Scope für Drive hinzufügen, falls nicht schon geschehen: current_scopes = SCOPES if 'https://www.googleapis.com/auth/drive.file' not in current_scopes and \ 'https://www.googleapis.com/auth/drive' not in current_scopes: # Dies ist nur ein Hinweis, der Scope muss in der `SCOPES`-Liste oben definiert sein # und die Credentials neu generiert (oder token.json gelöscht, falls OAuth verwendet würde) print("WARNUNG: Um im Ordner zu erstellen, wird 'drive.file' oder 'drive' Scope benötigt.") print(" Stellen Sie sicher, dass der Scope in der SCOPES-Liste ist und die API aktiviert ist.") drive_api_service = None try: # Baue den Drive Service mit denselben Credentials drive_api_service = build('drive', 'v3', credentials=service_creds) print("Google Drive API Service erfolgreich erstellt.") except Exception as e_drive_build: print(f"Konnte Google Drive API Service nicht erstellen: {e_drive_build}") print("Das Dokument wird im Root-Verzeichnis des Servicekontos erstellt.") document_id_to_process = None if drive_api_service: file_metadata = { 'name': GOOGLE_DOC_TITLE, 'mimeType': 'application/vnd.google-apps.document', 'parents': [TARGET_FOLDER_ID] } try: created_file = drive_api_service.files().create(body=file_metadata, fields='id').execute() document_id_to_process = created_file.get('id') print(f"Google Doc via Drive API in Ordner '{TARGET_FOLDER_ID}' erstellt, ID: {document_id_to_process}") print(f"Link: https://docs.google.com/document/d/{document_id_to_process}/edit") except HttpError as err_drive_create: print(f"Fehler beim Erstellen des Dokuments mit Drive API im Ordner: {err_drive_create}") print("Versuche, Dokument im Root des Servicekontos mit Docs API zu erstellen...") except Exception as e_drive_create_general: print(f"Allg. Fehler beim Erstellen des Dokuments mit Drive API: {e_drive_create_general}") print("Versuche, Dokument im Root des Servicekontos mit Docs API zu erstellen...") if not document_id_to_process: # Fallback, wenn Drive API Erstellung fehlschlug try: doc_body_for_create = {'title': GOOGLE_DOC_TITLE} doc = docs_api_service.documents().create(body=doc_body_for_create).execute() document_id_to_process = doc.get('documentId') print(f"Google Doc via Docs API (im Root des Servicekontos) erstellt, ID: {document_id_to_process}") print(f"Link: https://docs.google.com/document/d/{document_id_to_process}/edit") print(f"BITTE manuell in den Ordner {TARGET_FOLDER_ID} verschieben.") except Exception as e_docs_create: print(f"Konnte Dokument auch nicht mit Docs API erstellen: {e_docs_create}") if document_id_to_process: # Befülle das Dokument (egal wie es erstellt wurde) mit der Docs API # Wir übergeben hier nur den docs_api_service, da die create_google_doc_from_csv # Funktion in ihrer jetzigen Form nur diesen zum *Befüllen* braucht. # Das Erstellen ist jetzt ausgelagert. # Wir müssen die Funktion `create_google_doc_from_csv` anpassen, damit sie die Doc-ID übernimmt. # --- ANPASSUNG NOTWENDIG --- # Die Funktion `create_google_doc_from_csv` erstellt das Dokument selbst. # Wir müssen das trennen: 1. Dokument erstellen (in main), 2. Dokument befüllen (Funktion). # Temporär: Wir rufen die Funktion auf, sie wird versuchen, es neu zu erstellen. # Das ist nicht, was wir wollen. # Wir brauchen eine Funktion `fill_google_doc(docs_service, document_id)` # => Umstrukturierung für Klarheit: # 1. `get_services()` # 2. `ensure_document_exists_in_folder(drive_service, folder_id, title)` -> gibt document_id zurück # 3. `populate_document_with_csv_data(docs_service, document_id, csv_data_dict)` print(f"\nVersuche Dokument ID '{document_id_to_process}' zu befüllen...") # Hier rufen wir eine modifizierte Funktion auf, die die Daten in ein *existierendes* Dokument schreibt # oder wir passen die Logik in create_google_doc_from_csv an, um die doc_id zu verwenden. # Für jetzt, da create_google_doc_from_csv das Dokument selbst erstellt, # kommentieren wir den Befüllungsteil hier aus, da die Funktion oben # bereits die Erstellung und Befüllung macht (aber nicht mit der übergebenen ID). # Dies muss refaktorisiert werden. # Das aktuelle `create_google_doc_from_csv` macht zu viel. # Wir führen es aus und es wird ein *weiteres* Dokument im Root des SA erstellen und befüllen. # Das ist nicht ideal. # KORREKTER FLUSS WÄRE: # 1. document_id_in_folder = create_doc_in_folder_with_drive_api(...) # 2. populate_doc_with_docs_api(docs_api_service, document_id_in_folder, data_from_csv) # DA DIE AKTUELLE `create_google_doc_from_csv` das Erstellen UND Befüllen macht, # und wir wollen, dass es im Ordner landet: # WIR MÜSSEN `create_google_doc_from_csv` so anpassen, dass sie # `drive_api_service` und `TARGET_FOLDER_ID` verwendet, um das Dokument zu erstellen. # --- Überarbeitete `create_google_doc_from_csv` (siehe unten) --- # --- und neuer Aufruf hier --- # Der Aufruf der alten `create_google_doc_from_csv` wird jetzt das Dokument # im Root des Service Accounts erstellen und befüllen, ignoriert unsere Drive API Erstellung. # Das ist redundant. # Wir müssen EINE Methode zur Erstellung wählen. # METHODE: Drive API zum Erstellen im Ordner, Docs API zum Befüllen. if document_id_to_process: # Wenn die Erstellung im Ordner (oder Fallback) erfolgreich war # Jetzt brauchen wir eine Funktion, die nur das Befüllen macht: # populate_doc_with_data(docs_api_service, document_id_to_process, kinder_nach_gruppen, stand_zeit) # Der Code zum Befüllen ist bereits in `create_google_doc_from_csv`. # Wir müssen ihn extrahieren. # Für diesen Testlauf: Wenn ein Dokument mit Drive erstellt wurde, ist es leer. # Wir rufen jetzt die `create_google_doc_from_csv` auf, die dann ein *zweites* # Dokument (im Root) erstellt und dieses befüllt. # Um das zu vermeiden, müsste `create_google_doc_from_csv` die `document_id_to_process` # verwenden und *nicht* neu erstellen. # Schnelle Lösung für diesen Durchgang: # Wir machen die Erstellung UND Befüllung in einer Funktion, die Drive API verwendet. # Siehe überarbeitete Funktion unten. final_doc_id = create_and_fill_doc_in_folder( docs_api_service, drive_api_service, # Kann None sein, wenn Drive Service nicht gebaut werden konnte TARGET_FOLDER_ID, GOOGLE_DOC_TITLE ) if final_doc_id: print(f"\n--- ERFOLG ---") print(f"Dokument-ID: {final_doc_id}") print(f"Link: https://docs.google.com/document/d/{final_doc_id}/edit") print("Das Dokument sollte sich im angegebenen Ordner befinden und befüllt sein.") else: print("\n--- FEHLGESCHLAGEN ---") print("Konnte Dokument nicht erstellen oder befüllen.") # Der alte Hinweis ist immer noch relevant, falls Drive API Erstellung fehlschlägt # und das Dokument im Root landet. # print("\n--- WICHTIG (falls im Root erstellt) ---") # ... else: print("Konnte API Services nicht initialisieren.") # Neue Funktion, die Erstellung (idealerweise im Ordner) und Befüllung kombiniert def create_and_fill_doc_in_folder(docs_service, drive_service, folder_id, doc_title): document_id = None # 1. Dokument erstellen if drive_service and folder_id: file_metadata = { 'name': doc_title, 'mimeType': 'application/vnd.google-apps.document', 'parents': [folder_id] } try: created_file = drive_service.files().create(body=file_metadata, fields='id').execute() document_id = created_file.get('id') print(f"Google Doc via Drive API in Ordner '{folder_id}' erstellt, ID: {document_id}") except HttpError as err_drive_create: print(f"Fehler beim Erstellen des Dokuments mit Drive API im Ordner: {err_drive_create}") print("Versuche, Dokument im Root des Servicekontos mit Docs API zu erstellen...") except Exception as e_drive_create_general: print(f"Allg. Fehler beim Erstellen des Dokuments mit Drive API: {e_drive_create_general}") print("Versuche, Dokument im Root des Servicekontos mit Docs API zu erstellen...") if not document_id: # Fallback, wenn Drive API Erstellung fehlschlug oder drive_service nicht da war try: doc_body_for_create = {'title': doc_title} doc = docs_service.documents().create(body=doc_body_for_create).execute() document_id = doc.get('documentId') print(f"Google Doc via Docs API (im Root des Servicekontos) erstellt, ID: {document_id}") if folder_id: # Hinweis geben, wenn ein Ordner gewünscht war print(f"BITTE manuell in den Ordner '{folder_id}' verschieben.") except Exception as e_docs_create: print(f"Konnte Dokument auch nicht mit Docs API erstellen: {e_docs_create}") return None # Wenn auch das fehlschlägt, abbrechen if not document_id: return None # 2. Daten aus CSV lesen (oder als Parameter übergeben, hier für Einfachheit neu gelesen) kinder_nach_gruppen = collections.defaultdict(list) try: with open(CSV_FILENAME, mode='r', encoding='utf-8-sig', newline='') as csvfile: reader = csv.DictReader(csvfile, delimiter=';') # ... (Rest des CSV-Lesens wie in create_google_doc_from_csv) ... for row in reader: vorname = row.get('Vorname', '').strip() nachname = row.get('Nachname', '').strip() gruppe_original = row.get('Gruppe', '').strip() if not vorname or not nachname or not gruppe_original: continue kinder_nach_gruppen[gruppe_original].append({'Nachname': nachname, 'Vorname': vorname}) except Exception as e: print(f"FEHLER beim Lesen der CSV-Datei für Befüllung: {e}") return document_id # Gibt zumindest die ID des leeren Dokuments zurück if not kinder_nach_gruppen: print("Keine Daten aus der CSV-Datei für Befüllung geladen.") return document_id for gruppe_key in kinder_nach_gruppen: kinder_nach_gruppen[gruppe_key].sort(key=lambda x: (x['Nachname'].lower(), x['Vorname'].lower())) sorted_gruppen_namen = sorted(kinder_nach_gruppen.keys()) stand_zeit = datetime.now().strftime("%d.%m.%Y %H:%M Uhr") # 3. Dokument befüllen (Code aus create_google_doc_from_csv) requests = [] # ... (kompletter Schleifen- und Request-Aufbau-Code von oben hier einfügen) ... for i, gruppe_original in enumerate(sorted_gruppen_namen): kinder_liste = kinder_nach_gruppen[gruppe_original] anzahl_kinder = len(kinder_liste) gruppe_display_name = gruppe_original + GRUPPENNAME_SUFFIX header_text_for_page = (f"{EINRICHTUNG}\t\t\t{FOTOGRAF_NAME}\n{FOTODATUM}\n\n") if i == 0: requests.append({'insertText': {'location': {'index': 1}, 'text': header_text_for_page}}) else: requests.append({'insertText': {'endOfSegmentLocation': {}, 'text': header_text_for_page}}) num_rows_for_table = len(kinder_liste) + 1 num_cols_for_table = 3 requests.append({'insertTable': {'endOfSegmentLocation': {}, 'rows': num_rows_for_table, 'columns': num_cols_for_table}}) table_text_content = [] table_text_content.append("Nachname\tVorname\tGruppe") for kind in kinder_liste: table_text_content.append(f"{kind['Nachname']}\t{kind['Vorname']}\t{gruppe_display_name}") full_table_text = "\n".join(table_text_content) + "\n" requests.append({'insertText': {'endOfSegmentLocation': {}, 'text': full_table_text}}) footer_text_for_page = (f"\n{anzahl_kinder} angemeldete Kinder\n\nDies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden\nKinder an die Anmeldung erinnern.\n\nStand {stand_zeit}\n\n{FOTOGRAF_NAME}\n{FOTOGRAF_ADRESSE}\n{FOTOGRAF_WEB}\n{FOTOGRAF_TEL}\n") requests.append({'insertText': {'endOfSegmentLocation': {}, 'text': footer_text_for_page }}) if i < len(sorted_gruppen_namen) - 1: requests.append({'insertPageBreak': {'endOfSegmentLocation': {}}}) if requests: try: print(f"Sende Batch Update für Dokument ID '{document_id}'...") docs_service.documents().batchUpdate( documentId=document_id, body={'requests': requests} ).execute() print("Dokument erfolgreich befüllt.") except HttpError as err: # ... (Fehlerbehandlung wie gehabt) ... print(f"Fehler beim Befüllen des Dokuments ID '{document_id}': {err}") error_details = "Keine Fehlerdetails im Content." if err.content: try: error_details = err.content.decode('utf-8') except: pass print(f"Details zum Fehler ({err.resp.status} {err._get_reason()}): {error_details}") return document_id # Gibt ID des (evtl. teilweise befüllten) Dokuments zurück return document_id # Die alte create_google_doc_from_csv Funktion wird nicht mehr direkt aus main aufgerufen, # da create_and_fill_doc_in_folder ihre Logik übernimmt und verbessert. # Sie kann als Referenz oder für Tests noch im Code bleiben.