Files
Tatami/CLAUDE.md
T
lars 91dc7729f9 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.
2026-06-19 15:17:37 +02:00

7.3 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

Tatami ("Tool for Arranging Tasty Appointments, Meetings & Invitations") generates a "running dinner" masterplan: given a list of participants (with home addresses), it groups them into hosting groups for starter/main/dessert courses, assigns which groups visit which hosts for each course, and orders things to minimize travel time (by bike, via the Google Maps Routes API), finishing at a shared after-party location.

The actively developed package is src/tatami/. running_dinner/running_dinner.py is a legacy, pre-package standalone script (German-language field names, brute-force search over team pairings) kept for reference — it is not wired into the tatami package and uses a different/older Google Maps API (the legacy Distance Matrix API vs. the new Routes API used in traveltimes.py). Don't assume code or conventions from running_dinner.py apply to src/tatami/.

Commands

This project uses uv for dependency management (Python >=3.13).

  • Install deps: uv sync
  • Run the masterplan script: uv run python -m tatami.tatami_masterplan (requires test-config.csv in the working directory and GOOGLE_MAPS_API_KEY set; optionally set GOOGLE_SHEETS_CREDENTIALS_FILE + GOOGLE_SHEETS_SPREADSHEET_ID to also export to a shared Google Sheet — see .env.example)
  • Lint: uv run ruff check
  • Format: uv run ruff format
  • Type check: uv run mypy --allow-redefinition src/ (mypy is configured to treat untyped imports as errors except where ignored)
  • Pre-commit runs ruff check, ruff format, and mypy automatically (see .pre-commit-config.yaml); install hooks with uv run pre-commit install if working interactively.

There is currently no test suite in the repo.

Architecture

The pipeline (see src/tatami/tatami_masterplan.py __main__ block) is:

  1. Load participantsload_csv_to_participants reads a tab-separated CSV (name, address, phone, kitchen_size, allergies) into Participant objects (src/tatami/classes.py).
  2. Fetch travel timestraveltimes.get_participant_distance_matrix calls the Google Routes API (GOOGLE_MAPS_API_KEY env var required) to build a full pairwise duration matrix between all participant addresses plus the after-party address, indexed by participant UUID.
  3. Build masterplancompute_masterplan_groups (the live-object core; get_masterplan is a thin wrapper around it that returns plain dicts instead):
    • Splits participants into hosts (one per group, len(participants)//6 groups of 3 courses each) and semi_hosts (non-hosting members assigned round-robin into existing groups), ranked by each participant's get_after_party_time (kitchen size penalty + distance to after-party).
    • Reduces the full distance matrix to just host-to-host distances (reduce_distance_matrix), adding each host's kitchen-size penalty into their row.
    • Runs run_simulated_annealing / simulated_annealing (Boltzmann-style annealing over itertools.permutations of group order — note this is brute-force over all permutations per iteration, so it only scales to a small number of groups) to find a low-travel-time ordering of groups.
    • assign_courses assigns each group a course (starter/main/dessert cycling) and, via get_courses, determines which other groups host it for each course (offsets of +1 and -4 mod total groups — this fixed relationship is what defines the dinner-rotation topology).
  4. get_masterplan returns two lists of plain dicts (group.dict(), participant.dict()) suitable for serialization; compute_masterplan_groups returns the live Group/Participant objects, which is what the sheet export step needs (.hosts, .get_guests(...)).
  5. Export to Google Sheets (optional) — if GOOGLE_SHEETS_SPREADSHEET_ID is set, __main__ calls sheets_export.export_masterplan_to_sheet to populate a pre-existing, pre-shared spreadsheet with an Overview tab and one tab per group. This is opt-in and never sends anything directly to participants — the organizer still shares the sheet link manually.

Core domain model (src/tatami/classes.py)

  • Participant: a person with an address, phone, kitchen size (010, used as a "willingness/suitability to host" proxy via get_penalty, which adds travel-time-equivalent minutes for smaller kitchens), and allergies.
  • Group: a hosting unit with a main_member (used as the group's representative location for all distance lookups — other members' addresses are not used for travel calculations), a course, and a hosts list (the groups that host this group across the evening, kept sorted starter→main→dessert via sort_hosts). get_total_time sums travel + penalty across this group's full route (its hosts, then the after-party).
  • Distance/time lookups throughout the codebase are keyed by Participant.uuid (host groups are addressed via main_member.uuid), not by name — when adding new matrix operations, index by uuid for consistency with traveltimes.py and classes.py.

Travel times (src/tatami/traveltimes.py)

  • Wraps the Google Routes API computeRouteMatrix endpoint. Raises at import time if GOOGLE_MAPS_API_KEY is unset.
  • get_distance_matrix returns a square DataFrame of pd.Timedelta (or float meters) indexed/columned by integer position; get_participant_distance_matrix relabels both axes to participant UUIDs.
  • reduce_distance_matrix is the bridge between the full participant-level matrix and the group-level matrix used by the annealing step.

Sheet export (src/tatami/sheets_export.py)

  • Wraps gspread (Google Sheets API). Unlike traveltimes.py, validation of GOOGLE_SHEETS_CREDENTIALS_FILE happens lazily inside load_sheets_client, not at import time — this feature is optional/opt-in, so importing the module must not require the env var.
  • export_masterplan_to_sheet expects an existing gspread.Spreadsheet (organizer pre-creates it and shares Editor access with the service account's client_email once) and rewrites it idempotently: an "Overview" tab listing every participant, plus one tab per group named after all of that group's members joined with & (deduplicated; see _group_titles — deliberately not a single member's name, since a group can have several members) showing that group's course, route (their hosts, sorted starter→main→dessert, plus the after-party), and their guests for the course they host (Group.get_guests). Course start times are a fixed dict passed in by the caller (COURSE_TIMES in tatami_masterplan.py) — the dinner runs on a synchronized schedule, not on travel-time-derived timing.
  • The Overview tab also gets three caller-supplied sections below the group assignments, each rendered as-is (no computation) and each skippable: a "Meal Times" table built straight from course_times, a "Support Contacts" table from the optional organizer_contacts: list[tuple[str, str]] argument, and a free-text "Info" block from the optional info_text: str argument (split into one row per line). All three are configured in tatami_masterplan.py (COURSE_TIMES, ORGANIZER_CONTACTS, INFO_TEXT) and just passed through — sheets_export.py has no event-specific content baked in.
  • Tabs left over from a previous run with a different group count are deleted so reruns don't accumulate stale tabs.