Design Document v1.1.md 32 KB

Technische Dokumentation

Notion → Apple Kalender / Exchange Sync

Version: 1.1
Stand: Mai 2026
Lösungsansatz: Python + AppleScript → Apple Kalender → Exchange Online

Änderungshistorie

Version Änderung
1.0 Erstversion
1.1 (1) Update-Modus für bestehende Termine ergänzt; (2) Termine pro Tag laufen sequenziell (back-to-back), nicht parallel; (3) Notion-Eigenschaft umbenannt: „Completion Date" → „Last Edit"

Inhaltsverzeichnis

  1. Projektübersicht
  2. Systemumgebung
  3. Beabsichtigtes Verhalten (Functional Specification)
  4. Architektur & Datenfluss
  5. Notion-Datenmodell
  6. Datenverarbeitung & Geschäftslogik
  7. Komponente 1 – Python-Skript
  8. Komponente 2 – AppleScript-Integration
  9. Konfiguration durch den Nutzer
  10. Projektstruktur
  11. Fehlerbehandlung & Logging
  12. Testszenarien
  13. Ausführung & Workflow

1. Projektübersicht

Ziel

Dieses Tool synchronisiert abgeschlossene Aufgaben aus einer Notion-Datenbank in den Microsoft Outlook-Kalender (via Exchange Online). Da auf dem System ausschließlich macOS eingesetzt wird und Apple Kalender bereits mit dem Exchange-Konto verbunden ist, erfolgt die Kalender-Integration über AppleScript. Apple Kalender übernimmt dann automatisch die Synchronisation zu Exchange/Outlook.

Kernprobleme

In Notion existieren pro Tag mehrere Einträge für verschiedene Projekte. Im Kalender soll pro Tag und Projekt genau ein gebündelter Termin entstehen. Mehrere Termine am selben Tag sollen nahtlos hintereinander liegen (back-to-back), nicht alle zur selben Zeit starten. Ändert sich nachträglich etwas in Notion (z. B. eine T-Shirt-Size wird korrigiert), soll der bestehende Kalendertermin aktualisiert werden.

Abgrenzung

  • Das Tool läuft nicht automatisch im Hintergrund, sondern wird manuell am Ende eines Kalendermonats (oder bei Bedarf zwischendurch) ausgeführt.
  • Bestehende Kalendereinträge können aktualisiert, aber nicht gelöscht werden.
  • Das Tool ist idempotent: Bei unveränderter Notion-Datenlage passiert beim zweiten Lauf nichts.

2. Systemumgebung

Hardware & Betriebssystem

Eigenschaft Wert
Betriebssystem macOS (aktuell)
Ausführungsumgebung Terminal / Shell
Sprache Python 3.x

Externe Dienste & Zugänge

Dienst Zugang / Schnittstelle Status
Notion Notion REST API v1 (notion.so) Integration vorhanden
Microsoft Exchange Exchange Online (Arbeitgeber) Über Apple Kalender verbunden
Apple Kalender AppleScript / osascript Lokale App, macOS-nativ

Voraussetzungen auf dem System

  • Python 3.8+ installiert (z. B. via Homebrew oder System-Python)
  • pip-Pakete (werden in Abschnitt 9 beschrieben)
  • Apple Kalender App ist geöffnet und mit Exchange-Konto verbunden
  • Notion API Integration mit Zugriff auf die Datenbank „Projekte" eingerichtet

3. Beabsichtigtes Verhalten

3.1 Trigger

Der Nutzer führt das Skript manuell aus. Per Parameter wird der Zielmonat angegeben (z. B. --monat 2026-04 für April 2026). Das Skript kann auch mehrfach für denselben Monat ausgeführt werden – neue Einträge werden erstellt, geänderte Einträge aktualisiert.

3.2 Datenquelle

Das Skript liest alle Seiten aus der Notion-Datenbank „Projekte", deren Eigenschaft Last Edit in den angegebenen Zielmonat fällt.

3.3 Aggregationslogik

Mehrere Notion-Einträge, die am gleichen Tag zum gleichen Projekt gehören, werden zu einem einzigen Kalendertermin zusammengeführt.

3.4 Sequenzielle Terminreihenfolge

An einem Tag mit mehreren Projekten liegen die Termine nahtlos hintereinander. Der erste Termin beginnt immer um 09:00 Uhr. Jeder weitere Termin desselben Tages startet genau dort, wo der vorherige endet.

Sortierkriterium: Die Reihenfolge der Termine pro Tag richtet sich nach dem Projektnamen in alphabetischer Reihenfolge (A → Z). Dadurch ist die Reihenfolge bei jedem Lauf identisch und reproduzierbar – auch bei nachträglichen Updates.

Beispiel:

Notion-Eintrag Last Edit Project T-Shirt Size
Aufgabe A 2026-04-03 EfA-RI NI S (30 min)
Aufgabe B 2026-04-03 EfA-RI NI M (60 min)
Aufgabe C 2026-04-03 DiPlanung XS (15 min)

Aggregation (Summen pro Projekt):

Projekt Gesamtdauer
DiPlanung 15 min
EfA-RI NI 90 min

Sortierung: DiPlanung (D) kommt vor EfA-RI NI (E).

Ergebnis im Kalender:

Kalendertermin Datum Projekt Startzeit Endzeit Berechnung
Termin 1 03.04.2026 DiPlanung 09:00 09:15 09:00 + 15 min
Termin 2 03.04.2026 EfA-RI NI 09:15 10:45 09:15 (Ende Termin 1) + 90 min

3.5 Update-Verhalten

Das Skript unterscheidet bei jedem ermittelten Kalendertermin drei Zustände:

Zustand Bedingung Aktion
Neu Kein Eintrag in der Protokolldatei für diese Datum+Projekt-Kombination Termin wird neu erstellt
Unverändert Protokolleintrag vorhanden, Inhalt identisch (Dauer, Notizen, Zeiten) Kein API-Call, wird übersprungen
Geändert Protokolleintrag vorhanden, aber Inhalt hat sich verändert Termin wird via UID in Apple Kalender gesucht und aktualisiert

Warum UID? Apple Kalender vergibt beim Erstellen eines Termins eine eindeutige uid. Diese wird in der Protokolldatei gespeichert. Bei einem Update-Lauf sucht das AppleScript den Termin anhand dieser UID und überschreibt dessen Eigenschaften – kein Löschen und Neuerstellen nötig.

Wichtiger Seiteneffekt der sequenziellen Reihenfolge: Ändert sich die Dauer eines Projekts an einem Tag, verschieben sich alle nachfolgenden Termine dieses Tages. Das Skript berechnet alle Startzeiten eines Tages neu und aktualisiert alle betroffenen Termine – auch solche, deren eigene Notion-Daten unverändert sind.

3.6 Kalendertermin-Eigenschaften

Feld Inhalt
Titel Name des Projekts (z. B. EfA-RI NI)
Datum Last Edit-Datum aus Notion
Startzeit Berechneter Beginn (09:00 für ersten Termin, Folgezeiten sequenziell)
Endzeit Startzeit + Summe aller T-Shirt-Size-Minuten
Notizen Aufzählung aller enthaltenen Notion-Aufgabentitel + Größen
Kalender Der Exchange-Kalender in Apple Kalender (konfigurierbar)

3.7 Nicht abgedeckte Fälle

  • T-Shirt-Size fehlt bei einem Eintrag → Eintrag wird übersprungen und geloggt
  • Last Edit fehlt → Eintrag wird übersprungen und geloggt
  • Projekt-Relation fehlt → Eintrag wird übersprungen und geloggt
  • Termin wurde manuell in Apple Kalender gelöscht, UID existiert nicht mehr → Termin wird neu erstellt, neue UID gespeichert

4. Architektur & Datenfluss

┌─────────────────────────────────────────────────────────┐
│                    PYTHON-SKRIPT                        │
│                                                         │
│  1. Konfiguration laden (config.json)                   │
│  2. Protokolldatei laden (sync_state.json)              │
│  3. Notion API abfragen (Datenbank „Projekte")          │
│  4. Rohdaten filtern (nach Monat)                       │
│  5. Daten aggregieren (pro Tag + Projekt)               │
│  6. Sequenzielle Startzeiten pro Tag berechnen          │
│  7. Für jeden Termin: Zustand bestimmen                 │
│     (neu / unverändert / geändert)                      │
│  8a. Neu      → AppleScript CREATE ausführen            │
│  8b. Geändert → AppleScript UPDATE (per UID) ausführen  │
│  8c. Unverändert → überspringen                         │
│  9. Protokolldatei aktualisieren (UIDs speichern)       │
└────────────────────┬────────────────────────────────────┘
                     │  osascript (subprocess)
                     ▼
┌─────────────────────────────────────────────────────────┐
│                  APPLE KALENDER APP                     │
│                                                         │
│  - CREATE: Neuen Termin anlegen, UID zurückgeben        │
│  - UPDATE: Termin per UID suchen, Felder überschreiben  │
│  - Synchronisiert automatisch mit Exchange Online       │
└─────────────────────────────────────────────────────────┘
                     │  Exchange ActiveSync / EWS
                     ▼
┌─────────────────────────────────────────────────────────┐
│            MICROSOFT EXCHANGE ONLINE                    │
│            → sichtbar in Outlook / OWA                  │
└─────────────────────────────────────────────────────────┘

5. Notion-Datenmodell

Datenbank: „Projekte"

Eigenschaft Notion-Typ Beschreibung
Name Title Bezeichnung der Aufgabe (frei)
T-Shirt Size Select Dauer der Aufgabe (XS / S / M / L / XL)
Last Edit Date Datum, an dem die Aufgabe abgeschlossen wurde
Project Relation Verknüpfung zu einer Projekt-Datenbank; liefert ID + Name

T-Shirt-Size-Mapping

Select-Wert Minuten
XS 15
S 30
M 60
L 120
XL 240

Dieses Mapping wird in der Konfigurationsdatei (config.json) hinterlegt und kann dort einfach angepasst werden.

Notion API – Relevante Endpoints

  • Datenbank abfragen: POST https://api.notion.com/v1/databases/{database_id}/query
  • Filter: Last Edit between Monatsanfang und Monatsende
  • API-Version: 2022-06-28

Beispiel-API-Filter (JSON-Body)

{
  "filter": {
    "and": [
      {
        "property": "Last Edit",
        "date": {
          "on_or_after": "2026-04-01"
        }
      },
      {
        "property": "Last Edit",
        "date": {
          "on_or_before": "2026-04-30"
        }
      }
    ]
  }
}

Pagination

Die Notion API gibt maximal 100 Ergebnisse pro Anfrage zurück. Das Skript muss über den next_cursor-Mechanismus alle Seiten vollständig einlesen (Pagination-Loop).

Relation-Feld auslesen

Das Feld Project liefert im API-Response eine Liste von IDs. Um den Projektnamen zu erhalten, muss das Skript für jede eindeutige Projekt-ID einmalig GET https://api.notion.com/v1/pages/{page_id} aufrufen und den title-Wert extrahieren. Die Ergebnisse werden innerhalb eines Laufs gecacht, um API-Calls zu minimieren.


6. Datenverarbeitung & Geschäftslogik

6.1 Datenabruf

notion_pages = query_notion_database(
    database_id = config["notion_database_id"],
    month_start = "YYYY-MM-01",
    month_end   = "YYYY-MM-{letzter_tag}"
)

6.2 Normalisierung je Notion-Seite

Für jede geladene Notion-Seite werden folgende Felder extrahiert:

{
    "title":        str,   # Name der Notion-Seite
    "t_shirt_size": str,   # z. B. "M"
    "minutes":      int,   # aus Mapping: 60
    "last_edit":    date,  # Python date-Objekt
    "project_id":   str,   # Notion-Page-ID des Projekts
    "project_name": str    # aufgelöst via separatem API-Call + Cache
}

6.3 Aggregation

Schritt 1 – Gruppierung nach Schlüssel (last_edit, project_name):

Für jede Gruppe wird ermittelt:

  • total_minutes = Summe aller minutes-Werte
  • notes = Aufzählung aller Aufgabentitel + T-Shirt-Größe, z. B.:

    - Konzept erstellt (S)
    - Review-Meeting vorbereitet (M)
    

Schritt 2 – Tagesweise Sortierung und Startzeitberechnung:

Alle Gruppen eines Tages werden alphabetisch nach project_name sortiert. Anschließend werden die Startzeiten sequenziell berechnet:

day_start_hour = config["day_start_hour"]   # Standard: 9
cursor = datetime(year, month, day, day_start_hour, 0)

for event in sorted_events_for_day:
    event["start_time"] = cursor
    event["end_time"]   = cursor + timedelta(minutes=event["total_minutes"])
    cursor              = event["end_time"]   # nächster Termin beginnt hier

Ergebnis je Kalender-Event:

{
    "project_name":  str,
    "date":          date,
    "start_time":    time,   # berechnete Startzeit
    "end_time":      time,   # berechnete Endzeit
    "total_minutes": int,
    "notes":         str
}

6.4 Zustandsbestimmung (neu / unverändert / geändert)

Die Protokolldatei sync_state.json enthält für jede bereits verarbeitete Datum+Projekt-Kombination einen Eintrag:

{
  "2026-04-03|DiPlanung": {
    "uid":           "ABC123-...",
    "start_time":    "09:00",
    "end_time":      "09:15",
    "total_minutes": 15,
    "notes":         "- Aufgabe C (XS)"
  },
  "2026-04-03|EfA-RI NI": {
    "uid":           "DEF456-...",
    "start_time":    "09:15",
    "end_time":      "10:45",
    "total_minutes": 90,
    "notes":         "- Aufgabe A (S)\n- Aufgabe B (M)"
  }
}

Vergleichslogik pro ermitteltem Event:

key = "YYYY-MM-DD|Projektname"

if key not in protocol:
    → Zustand: NEU
elif protocol[key].total_minutes != event.total_minutes
  or protocol[key].notes         != event.notes
  or protocol[key].start_time    != event.start_time:
    → Zustand: GEÄNDERT  (uid aus Protokoll übernehmen)
else:
    → Zustand: UNVERÄNDERT

Hinweis zu Startzeitenänderungen: Da Startzeiten von anderen Projekten desselben Tages abhängen, kann ein Event als GEÄNDERT gelten, auch wenn seine eigenen Notion-Daten unverändert sind (weil ein Vorgänger-Termin eine andere Dauer bekommen hat). Dies ist korrekt – der Termin muss dann verschoben werden.

6.5 UID-Rückgabe bei CREATE

Das AppleScript gibt nach dem Erstellen eines neuen Termins dessen uid als String zurück. Python liest diesen Wert aus dem stdout des osascript-Prozesses und speichert ihn in der Protokolldatei.


7. Komponente 1 – Python-Skript

Dateiname: notion_to_calendar.py

Abhängigkeiten (pip)

requests>=2.31.0
python-dateutil>=2.8.2

Installation: pip install requests python-dateutil

Kommandozeilenparameter

Parameter Pflicht Beschreibung Beispiel
--monat Ja Zielmonat im Format YYYY-MM --monat 2026-04
--dry-run Nein Zeigt Termine an, ohne sie anzulegen oder zu ändern --dry-run
--kalender Nein Name des Zielkalenders (überschreibt config.json) --kalender "Work"

Programmablauf (Pseudocode)

1.  Konfiguration laden aus config.json
2.  Kommandozeilenparameter parsen
3.  Monatsanfang und Monatsende berechnen
4.  Protokolldatei laden (sync_state.json), falls vorhanden
5.  Notion-Datenbank abfragen (mit Pagination)
6.  Für jede Notion-Seite:
    a. Pflichtfelder prüfen (Last Edit, T-Shirt Size, Project)
    b. Falls fehlend: loggen, überspringen
    c. Daten normalisieren
    d. Projekt-ID → Projektname auflösen (mit Cache)
7.  Daten aggregieren (gruppiert nach Last Edit + Projektname)
8.  Für jeden Tag: Projekte alphabetisch sortieren, Startzeiten sequenziell berechnen
9.  Für jedes Event: Zustand bestimmen (neu / unverändert / geändert)
10. Im --dry-run-Modus: alle Events tabellarisch mit Zustand ausgeben, dann Abbruch
11. Für jedes Event (nicht dry-run):
    a. UNVERÄNDERT → überspringen, loggen
    b. NEU         → AppleScript CREATE ausführen
                   → UID aus stdout lesen
                   → Protokolldatei aktualisieren
    c. GEÄNDERT    → AppleScript UPDATE (mit gespeicherter UID) ausführen
                   → Bei "not found"-Fehler: Fallback auf CREATE, neue UID speichern
                   → Protokolldatei aktualisieren
12. Zusammenfassung ausgeben (X erstellt, Y aktualisiert, Z unverändert, W Fehler)

Wichtige Python-Funktionen (zu implementieren)

def load_config(path: str) -> dict
    # Lädt config.json, validiert Pflichtfelder

def get_month_bounds(month_str: str) -> tuple[date, date]
    # "2026-04" → (date(2026,4,1), date(2026,4,30))

def query_notion_database(database_id: str, token: str,
                          start: date, end: date) -> list[dict]
    # Notion API mit Pagination; gibt rohe page-Objekte zurück

def resolve_project_name(page_id: str, token: str,
                         cache: dict) -> str | None
    # Löst Projekt-ID → Name auf; nutzt Cache

def normalize_page(page: dict, token: str,
                   cache: dict, size_map: dict) -> dict | None
    # Extrahiert und normalisiert Felder; None bei fehlendem Pflichtfeld

def aggregate_events(pages: list[dict]) -> dict[date, list[dict]]
    # Gruppiert nach (date, project) und summiert Minuten
    # Gibt dict: date → Liste von Event-Dicts (noch ohne Startzeiten)

def assign_sequential_times(events_by_day: dict,
                             day_start_hour: int) -> list[dict]
    # Sortiert pro Tag alphabetisch, berechnet Startzeiten sequenziell
    # Gibt flache Liste aller Events mit start_time und end_time zurück

def determine_state(event: dict, protocol: dict) -> str
    # Gibt "new", "unchanged" oder "changed" zurück

def build_create_script(event: dict, calendar_name: str) -> str
    # Generiert AppleScript für neuen Termin; Script gibt UID auf stdout zurück

def build_update_script(event: dict, uid: str,
                        calendar_name: str) -> str
    # Generiert AppleScript für Update eines bestehenden Termins per UID

def run_applescript(script: str) -> tuple[bool, str]
    # Führt AppleScript via subprocess aus
    # Gibt (Erfolg: bool, stdout: str) zurück

def load_protocol(path: str) -> dict
    # Lädt sync_state.json; gibt leeres dict zurück, wenn Datei fehlt

def save_protocol(path: str, protocol: dict) -> None
    # Schreibt aktualisiertes dict als JSON zurück

def escape_applescript_string(s: str) -> str
    # Escaped Sonderzeichen für AppleScript-Strings

8. Komponente 2 – AppleScript-Integration

Prinzip

Das Python-Skript generiert AppleScript-Strings und führt diese via subprocess.run(["osascript", "-e", script]) aus. Es gibt zwei Script-Templates: eines für CREATE (gibt UID zurück), eines für UPDATE (sucht Event per UID und überschreibt Felder).

AppleScript-Template: CREATE

Das Script erstellt den Termin und gibt dessen uid auf stdout aus. Python liest diesen Wert aus.

tell application "Calendar"
    tell calendar "{CALENDAR_NAME}"
        set startDate to current date
        set year of startDate to {YEAR}
        set month of startDate to {MONTH}
        set day of startDate to {DAY}
        set hours of startDate to {START_HOUR}
        set minutes of startDate to {START_MINUTE}
        set seconds of startDate to 0

        set endDate to current date
        set year of endDate to {YEAR}
        set month of endDate to {MONTH}
        set day of endDate to {DAY}
        set hours of endDate to {END_HOUR}
        set minutes of endDate to {END_MINUTE}
        set seconds of endDate to 0

        set newEvent to make new event with properties {¬
            summary:"{PROJECT_NAME}", ¬
            start date:startDate, ¬
            end date:endDate, ¬
            description:"{NOTES}"}

        return uid of newEvent
    end tell
end tell

Python liest die UID aus stdout:

result = subprocess.run(["osascript", "-e", script],
                        capture_output=True, text=True)
uid = result.stdout.strip()

AppleScript-Template: UPDATE

Das Script sucht den Termin anhand der gespeicherten UID und überschreibt alle relevanten Felder.

tell application "Calendar"
    tell calendar "{CALENDAR_NAME}"
        set matchingEvents to (every event whose uid is "{UID}")
        if (count of matchingEvents) = 0 then
            return "ERROR: not found"
        end if

        set theEvent to item 1 of matchingEvents

        set startDate to current date
        set year of startDate to {YEAR}
        set month of startDate to {MONTH}
        set day of startDate to {DAY}
        set hours of startDate to {START_HOUR}
        set minutes of startDate to {START_MINUTE}
        set seconds of startDate to 0

        set endDate to current date
        set year of endDate to {YEAR}
        set month of endDate to {MONTH}
        set day of endDate to {DAY}
        set hours of endDate to {END_HOUR}
        set minutes of endDate to {END_MINUTE}
        set seconds of endDate to 0

        set start date of theEvent to startDate
        set end date of theEvent to endDate
        set description of theEvent to "{NOTES}"
        return "OK"
    end tell
end tell

Rückgabewert-Auswertung (UPDATE)

success, stdout = run_applescript(script)
if not success or stdout.strip().startswith("ERROR"):
    # Termin existiert nicht mehr → Fallback auf CREATE
    log_warning(f"Update fehlgeschlagen ({stdout.strip()}), lege Termin neu an.")
    # → build_create_script aufrufen, neue UID speichern

Sonderzeichen escapen

def escape_applescript_string(s: str) -> str:
    s = s.replace("\\", "\\\\")  # Backslashes zuerst
    s = s.replace('"', '\\"')    # Anführungszeichen
    s = s.replace("\n", "\\n")   # Zeilenumbrüche in description
    return s

Kalender-Name-Validierung

Existiert der konfigurierte Kalender nicht, gibt AppleScript einen Fehler zurück. Das Skript fängt diesen ab und bricht mit einer klaren Meldung ab: „Kalender '{name}' nicht gefunden – bitte calendar_name in config.json prüfen."


9. Konfiguration durch den Nutzer

Die gesamte nutzerspezifische Konfiguration liegt in config.json. Diese Datei darf nicht in ein öffentliches Repository eingecheckt werden.

Datei: config.json

{
  "notion_token": "secret_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "notion_database_id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "calendar_name": "NAME_DES_EXCHANGE_KALENDERS",
  "day_start_hour": 9,
  "t_shirt_size_map": {
    "XS": 15,
    "S":  30,
    "M":  60,
    "L":  120,
    "XL": 240
  },
  "protocol_file": "sync_state.json",
  "log_file": "notion_sync.log"
}

Feld day_start_hour: Startzeit des ersten Termins eines Tages in Stunden (Standard: 9 für 09:00 Uhr). Kann hier ohne Code-Änderung angepasst werden.

Schritt-für-Schritt-Anleitung zur Konfiguration

Schritt 1 – Notion API Token ermitteln

  1. Öffne https://www.notion.so/my-integrations
  2. Wähle deine bestehende Integration aus
  3. Kopiere den „Internal Integration Token" (beginnt mit secret_...)
  4. Trage den Wert für "notion_token" in config.json ein

Schritt 2 – Notion Datenbank-ID ermitteln

  1. Öffne die Notion-Datenbank „Projekte" im Browser
  2. Die URL hat das Format: https://www.notion.so/{workspace}/{DATABASE_ID}?v=...
  3. Kopiere die 32-stellige ID vor dem ?v=
  4. Falls keine Bindestriche vorhanden: manuell einfügen nach Schema 8-4-4-4-12
    Beispiel: 1234567890123456789012345678901212345678-9012-3456-7890-123456789012
  5. Trage die ID als "notion_database_id" ein

Wichtig: Die Integration muss Datenbankzugriff haben.
In Notion: Datenbank öffnen → ... Menü → „Connections" → Integration auswählen.

Schritt 3 – Kalender-Name ermitteln

  1. Öffne die Apple Kalender App
  2. In der linken Seitenleiste alle verfügbaren Kalender prüfen
  3. Den Kalender finden, der mit dem Exchange-Konto verbunden ist
    (erkennbar am Exchange-Icon oder der Arbeits-E-Mail-Adresse)
  4. Den exakten Namen notieren (Groß-/Kleinschreibung und Leerzeichen beachten)
  5. Als "calendar_name" eintragen

Schritt 4 – Python-Abhängigkeiten installieren

pip install requests python-dateutil
# oder bei Homebrew-Python:
pip3 install requests python-dateutil

10. Projektstruktur

notion-to-calendar/
│
├── notion_to_calendar.py   # Haupt-Skript
├── config.json             # Nutzerkonfiguration (nicht einchecken!)
├── sync_state.json         # Protokolldatei mit UIDs (automatisch erstellt)
├── notion_sync.log         # Log-Datei (automatisch erstellt)
├── .gitignore
└── README.md

.gitignore (Mindestinhalt)

config.json
sync_state.json
notion_sync.log
__pycache__/
*.pyc

11. Fehlerbehandlung & Logging

Fehlerklassen

Fehlertyp Verhalten
Notion API nicht erreichbar Abbruch mit Fehlermeldung, Exit Code 1
Ungültiger Notion-Token Abbruch mit Hinweis, Token in config.json zu prüfen
Datenbank-ID nicht gefunden Abbruch mit Hinweis auf falsche ID
Pflichtfeld fehlt (Notion-Seite) Seite überspringen, in Log schreiben, weitermachen
Unbekannter T-Shirt-Size-Wert Seite überspringen, in Log schreiben
Apple Kalender nicht geöffnet osascript-Fehler abfangen, Meldung ausgeben, Termin überspringen
Kalender-Name nicht gefunden Abbruch mit Hinweis auf falschen Kalender-Namen
UID bei UPDATE nicht gefunden Fallback auf CREATE, neue UID speichern, Warnung loggen
AppleScript-Fehler (allgemein) Termin überspringen, Fehler in Log schreiben
Protokolldatei nicht schreibbar Warnung ausgeben, Skript läuft weiter

Log-Format

[2026-04-30 17:42:01] INFO  Starte Sync für Monat: 2026-04
[2026-04-30 17:42:02] INFO  21 Notion-Seiten geladen
[2026-04-30 17:42:02] WARN  Seite "Aufgabe X": T-Shirt Size fehlt – übersprungen
[2026-04-30 17:42:03] INFO  NEU:          DiPlanung  | 2026-04-03 | 09:00–09:15
[2026-04-30 17:42:03] INFO  NEU:          EfA-RI NI  | 2026-04-03 | 09:15–10:45
[2026-04-30 17:42:05] INFO  GEÄNDERT:     EfA-RI NI  | 2026-04-07 | 09:00–11:00 (vorher 09:00–10:30)
[2026-04-30 17:42:06] INFO  UNVERÄNDERT:  DiPlanung  | 2026-04-07 – übersprungen
[2026-04-30 17:42:10] INFO  Sync abgeschlossen: 10 erstellt, 3 aktualisiert, 8 unverändert, 1 Fehler

12. Testszenarien

Szenario 1 – Normalbetrieb, erster Lauf

  • Input: 2 Projekte, 3 Tage, mehrere Einträge pro Tag
  • Erwartung: Korrekte Anzahl Termine, sequenzielle Uhrzeiten, alphabetische Reihenfolge, UIDs in sync_state.json gespeichert

Szenario 2 – Zweiter Lauf ohne Änderungen

  • Input: Identische Notion-Daten, sync_state.json bereits vorhanden
  • Erwartung: 0 Termine erstellt oder geändert; alle als „unverändert" geloggt

Szenario 3 – Notion-Daten wurden nachträglich geändert

  • Input: T-Shirt-Size eines Eintrags für EfA-RI NI von S auf L geändert
  • Erwartung: EfA-RI NI-Termin wird aktualisiert; falls DiPlanung am gleichen Tag danach kommt, wird dessen Startzeit ebenfalls neu gesetzt (Update)

Szenario 4 – Termin manuell in Apple Kalender gelöscht

  • Input: UID in sync_state.json vorhanden, aber Termin im Kalender nicht mehr existent
  • Erwartung: Fallback auf CREATE, Termin neu angelegt, neue UID gespeichert, Warnung im Log

Szenario 5 – --dry-run

  • Input: --monat 2026-04 --dry-run
  • Erwartung: Tabellarische Ausgabe aller Events mit Zustand (NEU / GEÄNDERT / UNVERÄNDERT), kein AppleScript ausgeführt, sync_state.json unverändert. Oberhalb der Tabelle erscheint eine Legende. Farbkodierung: Spalte „Dauer" grün bei ≥ 480 min (8 h), rot bei > 600 min (10 h); Spalte „Datum" orange für alle Tage einer ISO-Woche, deren Gesamtminuten > 2880 (48 h).

Szenario 6 – Fehlende Pflichtfelder

  • Input: Notion-Seite ohne Last Edit
  • Erwartung: Seite wird übersprungen, Warnung im Log, keine Exception

Szenario 7 – Ungültiger Kalender-Name

  • Input: Falscher calendar_name in config.json
  • Erwartung: Klare Fehlermeldung beim ersten Script-Aufruf, sauberer Abbruch

Szenario 8 – Einzelner Eintrag pro Tag/Projekt

  • Input: Genau ein Notion-Eintrag (XS = 15 min), kein zweites Projekt an diesem Tag
  • Erwartung: Termin 09:00–09:15

Szenario 9 – Drei Projekte an einem Tag

  • Input: Projekte „Alpha" (M=60 min), „Beta" (S=30 min), „Gamma" (XS=15 min)
  • Erwartung:
    • Alpha: 09:00–10:00
    • Beta: 10:00–10:30
    • Gamma: 10:30–10:45

Szenario 10 – Große Gesamtdauer

  • Input: XL + L + L an einem Tag für ein Projekt (480 min = 8h)
  • Erwartung: Termin 09:00–17:00; kein Crash auch wenn nachfolgende Termine über 17:00 hinausgehen

13. Ausführung & Workflow

Erster Start (Einrichtung)

# 1. Projektverzeichnis anlegen
mkdir notion-to-calendar && cd notion-to-calendar

# 2. notion_to_calendar.py ablegen

# 3. config.json anlegen und befüllen (siehe Abschnitt 9)

# 4. Abhängigkeiten installieren
pip install requests python-dateutil

# 5. Probelauf ohne echte Kalendereinträge
python notion_to_calendar.py --monat 2026-04 --dry-run

Reguläre Monatsausführung

# Apple Kalender sicherstellen (muss geöffnet und mit Exchange verbunden sein)
open -a Calendar

# Sync ausführen
python notion_to_calendar.py --monat 2026-04

Nachträgliche Korrekturen in Notion

Falls nach dem Monatsabschluss Daten in Notion korrigiert wurden:

# Skript erneut ausführen – geänderte Termine werden automatisch aktualisiert
python notion_to_calendar.py --monat 2026-04

Nach der Ausführung

  1. Apple Kalender auf erstellte/aktualisierte Termine prüfen
  2. Kurz warten (~1–5 Minuten) bis Exchange-Sync abgeschlossen
  3. Termine in Outlook / OWA verifizieren
  4. Log-Datei bei Bedarf prüfen: cat notion_sync.log

Ende der Dokumentation – Version 1.1