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.
7.3 KiB
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(requirestest-config.csvin the working directory andGOOGLE_MAPS_API_KEYset; optionally setGOOGLE_SHEETS_CREDENTIALS_FILE+GOOGLE_SHEETS_SPREADSHEET_IDto 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 withuv run pre-commit installif working interactively.
There is currently no test suite in the repo.
Architecture
The pipeline (see src/tatami/tatami_masterplan.py __main__ block) is:
- Load participants —
load_csv_to_participantsreads a tab-separated CSV (name,address,phone,kitchen_size,allergies) intoParticipantobjects (src/tatami/classes.py). - Fetch travel times —
traveltimes.get_participant_distance_matrixcalls the Google Routes API (GOOGLE_MAPS_API_KEYenv var required) to build a full pairwise duration matrix between all participant addresses plus the after-party address, indexed by participant UUID. - Build masterplan —
compute_masterplan_groups(the live-object core;get_masterplanis a thin wrapper around it that returns plain dicts instead):- Splits participants into
hosts(one per group,len(participants)//6groups of 3 courses each) andsemi_hosts(non-hosting members assigned round-robin into existing groups), ranked by each participant'sget_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 overitertools.permutationsof 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_coursesassigns each group a course (starter/main/dessert cycling) and, viaget_courses, determines which other groups host it for each course (offsets of+1and-4mod total groups — this fixed relationship is what defines the dinner-rotation topology).
- Splits participants into
get_masterplanreturns two lists of plain dicts (group.dict(),participant.dict()) suitable for serialization;compute_masterplan_groupsreturns the liveGroup/Participantobjects, which is what the sheet export step needs (.hosts,.get_guests(...)).- Export to Google Sheets (optional) — if
GOOGLE_SHEETS_SPREADSHEET_IDis set,__main__callssheets_export.export_masterplan_to_sheetto 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 (0–10, used as a "willingness/suitability to host" proxy viaget_penalty, which adds travel-time-equivalent minutes for smaller kitchens), and allergies.Group: a hosting unit with amain_member(used as the group's representative location for all distance lookups — other members' addresses are not used for travel calculations), acourse, and ahostslist (the groups that host this group across the evening, kept sorted starter→main→dessert viasort_hosts).get_total_timesums 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 viamain_member.uuid), not by name — when adding new matrix operations, index by uuid for consistency withtraveltimes.pyandclasses.py.
Travel times (src/tatami/traveltimes.py)
- Wraps the Google Routes API
computeRouteMatrixendpoint. Raises at import time ifGOOGLE_MAPS_API_KEYis unset. get_distance_matrixreturns a squareDataFrameofpd.Timedelta(or float meters) indexed/columned by integer position;get_participant_distance_matrixrelabels both axes to participant UUIDs.reduce_distance_matrixis 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). Unliketraveltimes.py, validation ofGOOGLE_SHEETS_CREDENTIALS_FILEhappens lazily insideload_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_sheetexpects an existinggspread.Spreadsheet(organizer pre-creates it and shares Editor access with the service account'sclient_emailonce) 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 (theirhosts, 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_TIMESintatami_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 optionalorganizer_contacts: list[tuple[str, str]]argument, and a free-text "Info" block from the optionalinfo_text: strargument (split into one row per line). All three are configured intatami_masterplan.py(COURSE_TIMES,ORGANIZER_CONTACTS,INFO_TEXT) and just passed through —sheets_export.pyhas 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.