Move Participant/Group to pydantic and add a saveable/editable Plan model

Participant and Group are now pydantic BaseModels. Group.hosts can form
cycles between groups, so it's kept as a private, non-persisted live list
(set via set_hosts()/add_host()) backed by a serializable host_uuids field,
re-linked via resolve_hosts() after a reload.

A new Plan model (src/tatami/plan.py) bundles groups, the after-party group,
and the event config (course_times, organizer_contacts, info_text,
spreadsheet_id) and supports save()/load() to/from JSON. tatami_masterplan's
__main__ now saves to masterplan.json (PLAN_FILE env var to override) on
first run and loads it on later runs instead of recomputing, so the plan can
be hand-edited (move a member between groups, change a course, fill in
spreadsheet_id) and picked up on rerun without hitting the Routes API again.
spreadsheet_id moves out of .env (GOOGLE_SHEETS_SPREADSHEET_ID) onto the plan
itself, since it's part of the plan rather than a secret.
This commit is contained in:
2026-06-19 16:07:47 +02:00
parent 91dc7729f9
commit 6b0fca0100
12 changed files with 538 additions and 157 deletions
+16 -4
View File
@@ -11,8 +11,12 @@ from conftest import make_participants, make_timedelta_matrix
class TestParticipant:
def test_uuid_is_unique(self):
a = Participant("A", "addr", "", 5, "")
b = Participant("A", "addr", "", 5, "")
a = Participant(
name="A", address="addr", phone="", kitchen_size=5, allergies=""
)
b = Participant(
name="A", address="addr", phone="", kitchen_size=5, allergies=""
)
assert a.uuid != b.uuid
@pytest.mark.parametrize(
@@ -20,7 +24,9 @@ class TestParticipant:
[(10, 0), (7, 9), (0, 30), (5, 15)],
)
def test_penalty_scales_with_kitchen_size(self, kitchen_size, minutes):
p = Participant("A", "addr", "", kitchen_size, "")
p = Participant(
name="A", address="addr", phone="", kitchen_size=kitchen_size, allergies=""
)
assert p.get_penalty() == dt.timedelta(minutes=minutes)
def test_after_party_time_combines_penalty_and_travel(self):
@@ -32,7 +38,13 @@ class TestParticipant:
assert result == dt.timedelta(minutes=9) + dt.timedelta(seconds=600)
def test_dict_roundtrip_fields(self):
p = Participant("Alice", "addr", "555", 8, "peanuts")
p = Participant(
name="Alice",
address="addr",
phone="555",
kitchen_size=8,
allergies="peanuts",
)
d = p.dict()
assert d == {
"uuid": p.uuid,