Add optional Google Sheets export for the masterplan

Lets organizers automatically populate a shared Google Sheet (the same kind
they previously built by hand) with an Overview tab and one tab per group,
instead of printing dicts. Group tabs are named after all their members
(not a single host) and show each group's route, course times, and guests
with allergies. The Overview tab also gets configurable Meal Times, Support
Contacts, and Info sections, passed through as plain data from
tatami_masterplan.py. Export is fully opt-in via GOOGLE_SHEETS_CREDENTIALS_FILE
and GOOGLE_SHEETS_SPREADSHEET_ID; without them, behavior is unchanged.
This commit is contained in:
2026-06-19 15:17:37 +02:00
parent 21eb9539d4
commit 91dc7729f9
10 changed files with 1013 additions and 8 deletions
+56 -2
View File
@@ -79,6 +79,10 @@ gitignored, so your secret never gets committed. An alreadyexported
> The package raises at import time if no key is found, so `GOOGLE_MAPS_API_KEY`
> must be set (even to a dummy value) just to import `tatami.traveltimes`.
Optionally, also set `GOOGLE_SHEETS_CREDENTIALS_FILE` and
`GOOGLE_SHEETS_SPREADSHEET_ID` in `.env` to export the plan to a shared
Google Sheet — see [Sharing the plan with participants](#sharing-the-plan-with-participants).
## Input: the participant CSV
A **tabseparated** file (the default working file is `test-config.csv` in the
@@ -153,14 +157,63 @@ keyed by UUID, so resolve names/addresses by looking participants up by `uuid`.
> Note: only a group's `main_member` address is used for all travel calculations;
> other members are assumed to join at the main member's home.
## Sharing the plan with participants
`get_masterplan`'s dicts are great for code, but participants need something
readable. `compute_masterplan_groups` (the same computation, returning live
`Group`/`Participant` objects instead of dicts) feeds an optional Google
Sheets export — the same kind of shared spreadsheet organizers have used in
previous years, just generated automatically instead of by hand.
Set up once:
1. Create a Google Cloud service account and enable the **Google Sheets API**
for its project.
2. Download the service account's JSON key and point
`GOOGLE_SHEETS_CREDENTIALS_FILE` at it (in `.env`).
3. Create a blank Google Sheet, share it with the service account's
`client_email` (from the JSON key) as **Editor**, and set
`GOOGLE_SHEETS_SPREADSHEET_ID` to that sheet's ID (in `.env`).
With both set, running `uv run python -m tatami.tatami_masterplan` populates
that spreadsheet with an **Overview** tab (every participant, their group,
course, address, phone, allergies — followed by a Meal Times table, a Support
Contacts table, and a free-text Info block, see below) and one tab per group
(their own course, route with addresses and fixed course times, and the
guest list — with allergies — for the course they host). Reruns are
idempotent: tabs are cleared and rewritten, and stale tabs from a previous
run are deleted.
Tatami never contacts participants directly — sharing the sheet's link is
still up to the organizer, exactly as before.
The three extra Overview sections are plain configuration, passed straight
through to the sheet with no logic in between — edit these in
`tatami_masterplan.py`:
```python
COURSE_TIMES = {
"starter": "18:30", "main": "20:00", "dessert": "22:00", "after_party": "23:30",
} # -> "Meal Times" table
ORGANIZER_CONTACTS = [("Lars (Organizer)", "0151-23456789")] # -> "Support Contacts" table
INFO_TEXT = "Welcome to the running dinner! ..." # -> "Info" block (one row per line)
```
`organizer_contacts` and `info_text` are optional (`None`/empty skips that
section); `course_times` is also reused for each group's own route table.
If neither variable is set, this step is skipped entirely and Tatami just
prints the plan, as before.
## What you can tweak
All knobs currently live in the source. The most useful ones:
| What | Where | Default | Effect |
|------|-------|---------|--------|
| **Afterparty address** | `tatami_masterplan.py:230` (`__main__`) | a Karlsruhe address | Where everyone ends the night; also influences host ranking. |
| **Travel mode** | `tatami_masterplan.py:233` (`mode="BICYCLE"`) | `BICYCLE` | Any Routes API `travelMode`: `BICYCLE`, `DRIVE`, `WALK`, `TWO_WHEELER`, `TRANSIT`. |
| **Afterparty address** | `tatami_masterplan.py:251` (`__main__`) | a Karlsruhe address | Where everyone ends the night; also influences host ranking. |
| **Travel mode** | `tatami_masterplan.py:254` (`mode="BICYCLE"`) | `BICYCLE` | Any Routes API `travelMode`: `BICYCLE`, `DRIVE`, `WALK`, `TWO_WHEELER`, `TRANSIT`. |
| **Course start times** | `tatami_masterplan.py` (`COURSE_TIMES`) | `18:30` / `20:00` / `22:00` / `23:30` | Fixed slot times written into the Google Sheet export; the dinner runs on a synchronized schedule, not travel-derived timing. |
| **Group sizing** | `tatami_masterplan.py:24` (`len(participants) / 6`) | 1 group per ~6 people | The divisor sets how many participants form one "coursetriple". Larger → fewer, bigger groups. |
| **Kitchensize penalty** | `classes.py:19` (`minutes=3 * (10 - kitchen_size)`) | 3 min per point | Traveltimeequivalent penalty for small kitchens. Raise the `3` to push hosting toward big kitchens. |
| **Annealing schedule** | `tatami_masterplan.py:4850` | `T=1000`, `cooling=0.99`, `iters=10000` | Optimization quality vs. runtime. More iterations / slower cooling → better routes, slower. |
@@ -240,6 +293,7 @@ uv run pytest -m e2e # opt-in live test that calls the real Routes API
src/tatami/
classes.py # Participant and Group domain model
traveltimes.py # Google Routes API wrapper + matrix helpers
sheets_export.py # optional Google Sheets export for participants
tatami_masterplan.py # pipeline: load → fetch → group → optimize → assign
tests/ # pytest suite (offline + opt-in live e2e)
running_dinner/ # legacy standalone prototype — NOT used by the package