Skip to content

Commit 27ed848

Browse files
authored
Implement schedule (#34)
* Add initial models for schedule * Add the transformations * exclude slot count * Rename room -> rooms, and str -> list[str] for consistency * add slugs as well * Handle half-day workshops as well
1 parent bf08f41 commit 27ed848

File tree

8 files changed

+310
-14
lines changed

8 files changed

+310
-14
lines changed

src/download.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
}
1313

1414
base_url = f"https://pretalx.com/api/events/{Config.event}/"
15+
schedule_url = base_url + "schedules/latest/"
1516

1617
resources = [
1718
# Questions need to be passed to include answers in the same endpoint,
@@ -50,3 +51,17 @@
5051

5152
with open(filepath, "w") as fd:
5253
json.dump(res0, fd)
54+
55+
56+
# Download schedule
57+
response = requests.get(schedule_url, headers=headers)
58+
59+
if response.status_code != 200:
60+
raise Exception(f"Error {response.status_code}: {response.text}")
61+
62+
data = response.json()
63+
filename = "schedule_latest.json"
64+
filepath = Config.raw_path / filename
65+
66+
with open(filepath, "w") as fd:
67+
json.dump(data, fd)

src/misc.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,33 @@ class SubmissionState(Enum):
2424
rejected = "rejected"
2525
canceled = "canceled"
2626
submitted = "submitted"
27+
28+
29+
class Room(Enum):
30+
"""
31+
Rooms at the conference venue, this can change year to year
32+
"""
33+
34+
# Tutorial/workshop rooms
35+
club_a = "Club A"
36+
club_b = "Club B"
37+
club_c = "Club C"
38+
club_d = "Club D"
39+
club_e = "Club E"
40+
club_h = "Club H"
41+
42+
# Conference rooms
43+
forum_hall = "Forum Hall"
44+
terrace_2a = "Terrace 2A"
45+
terrace_2b = "Terrace 2B"
46+
north_hall = "North Hall"
47+
south_hall_2a = "South Hall 2A"
48+
south_hall_2b = "South Hall 2B"
49+
main_hall_a = "Main Hall A"
50+
main_hall_b = "Main Hall B"
51+
main_hall_c = "Main Hall C"
52+
53+
54+
class EventType(Enum):
55+
SESSION = "session"
56+
BREAK = "break"

src/models/europython.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
from datetime import datetime
1+
from __future__ import annotations
2+
3+
from datetime import date, datetime
24

35
from pydantic import BaseModel, Field, computed_field, model_validator
46

57
from src.config import Config
6-
from src.misc import SpeakerQuestion, SubmissionQuestion
8+
from src.misc import EventType, Room, SpeakerQuestion, SubmissionQuestion
79
from src.models.pretalx import PretalxAnswer
810

911

@@ -139,6 +141,7 @@ class EuroPythonSession(BaseModel):
139141
sessions_before: list[str] | None = None
140142
next_session: str | None = None
141143
prev_session: str | None = None
144+
slot_count: int = Field(..., exclude=True)
142145

143146
@computed_field
144147
def website_url(self) -> str:
@@ -166,3 +169,66 @@ def extract_answers(cls, values) -> dict:
166169
values["level"] = answer.answer_text.lower()
167170

168171
return values
172+
173+
174+
class EuroPythonScheduleSession(BaseModel):
175+
"""
176+
Model for EuroPython schedule session data
177+
"""
178+
179+
event_type: EventType = EventType.SESSION
180+
code: str
181+
slug: str
182+
title: str
183+
session_type: str
184+
speakers: list[dict[str, str]] # code, name, website_url
185+
tweet: str
186+
level: str
187+
total_duration: int = Field(..., exclude=True)
188+
rooms: list[Room]
189+
start: datetime
190+
slot_count: int = Field(..., exclude=True)
191+
website_url: str
192+
193+
@computed_field
194+
def duration(self) -> int:
195+
return self.total_duration // self.slot_count
196+
197+
198+
class EuroPythonScheduleBreak(BaseModel):
199+
"""
200+
Model for EuroPython schedule break data
201+
"""
202+
203+
event_type: EventType = EventType.BREAK
204+
title: str
205+
duration: int
206+
rooms: list[Room]
207+
start: datetime
208+
209+
210+
class DaySchedule(BaseModel):
211+
rooms: list[Room]
212+
events: list[EuroPythonScheduleSession | EuroPythonScheduleBreak]
213+
214+
215+
class Schedule(BaseModel):
216+
days: dict[date, DaySchedule]
217+
218+
@classmethod
219+
def from_events(
220+
cls, events: list[EuroPythonScheduleSession | EuroPythonScheduleBreak]
221+
) -> Schedule:
222+
day_dict = {}
223+
for event in events:
224+
event_date = event.start.date()
225+
if event_date not in day_dict:
226+
day_dict[event_date] = {"rooms": list(set(event.rooms)), "events": []}
227+
else:
228+
day_dict[event_date]["rooms"] = list(
229+
set(day_dict[event_date]["rooms"] + event.rooms)
230+
)
231+
day_dict[event_date]["events"].append(event)
232+
233+
day_schedule_dict = {k: DaySchedule(**v) for k, v in day_dict.items()}
234+
return cls(days=day_schedule_dict)

src/models/pretalx.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime
2+
from typing import Any
23

34
from pydantic import BaseModel, Field, field_validator, model_validator
45

@@ -65,6 +66,7 @@ class PretalxSubmission(BaseModel):
6566
resources: list[dict[str, str]] | None = None
6667
answers: list[PretalxAnswer]
6768
slot: PretalxSlot | None = Field(..., exclude=True)
69+
slot_count: int = Field(..., exclude=True)
6870

6971
# Extracted from slot data
7072
room: str | None = None
@@ -107,3 +109,40 @@ def process_values(cls, values) -> dict:
107109
@property
108110
def is_publishable(self) -> bool:
109111
return self.state in (SubmissionState.accepted, SubmissionState.confirmed)
112+
113+
114+
class PretalxScheduleBreak(BaseModel):
115+
"""
116+
Model for Pretalx schedule break data
117+
"""
118+
119+
room: str
120+
start: datetime
121+
end: datetime
122+
description: dict[str, str] | str
123+
124+
@field_validator("description", mode="before")
125+
@classmethod
126+
def handle_localized(cls, v) -> str | Any:
127+
if isinstance(v, dict):
128+
return v.get("en")
129+
return v
130+
131+
@model_validator(mode="before")
132+
@classmethod
133+
def set_slot_info(cls, values) -> dict:
134+
slot = PretalxSlot.model_validate(values)
135+
values["room"] = slot.room
136+
values["start"] = slot.start
137+
values["end"] = slot.end
138+
139+
return values
140+
141+
142+
class PretalxSchedule(BaseModel):
143+
"""
144+
Model for Pretalx schedule data
145+
"""
146+
147+
slots: list[PretalxSubmission]
148+
breaks: list[PretalxScheduleBreak]

src/transform.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
pretalx_speakers = Parse.publishable_speakers(
1515
Config.raw_path / "speakers_latest.json", pretalx_submissions.keys()
1616
)
17+
pretalx_schedule = Parse.schedule(Config.raw_path / "schedule_latest.json")
1718

1819
print("Computing timing relationships...")
1920
TimingRelationships.compute(pretalx_submissions.values())
@@ -23,6 +24,9 @@
2324
pretalx_submissions
2425
)
2526
ep_speakers = Transform.pretalx_speakers_to_europython_speakers(pretalx_speakers)
27+
ep_schedule = Transform.pretalx_schedule_to_europython_schedule(
28+
pretalx_schedule.breaks, ep_sessions, ep_speakers
29+
)
2630

2731
# Warn about duplicates if the flag is set
2832
if len(sys.argv) > 1 and sys.argv[1] == "--warn-dupes":
@@ -36,3 +40,6 @@
3640
print(f"Writing the data to {Config.public_path}...")
3741
Utils.write_to_file(Config.public_path / "sessions.json", ep_sessions)
3842
Utils.write_to_file(Config.public_path / "speakers.json", ep_speakers)
43+
Utils.write_to_file(
44+
Config.public_path / "schedule.json", ep_schedule, direct_dump=True
45+
)

src/utils/parse.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from collections.abc import KeysView
33
from pathlib import Path
44

5-
from src.models.pretalx import PretalxSpeaker, PretalxSubmission
5+
from src.models.pretalx import PretalxSchedule, PretalxSpeaker, PretalxSubmission
66
from src.utils.utils import Utils
77

88

@@ -47,3 +47,17 @@ def publishable_speakers(
4747
}
4848

4949
return publishable_speakers_by_code
50+
51+
@staticmethod
52+
def schedule(input_file: Path | str) -> PretalxSchedule:
53+
"""
54+
Returns the schedule:
55+
56+
PretalxSchedule.slots: list[PretalxSubmission]
57+
PretalxSchedule.breaks: list[PretalxScheduleBreak]
58+
"""
59+
with open(input_file) as fd:
60+
js = json.load(fd)
61+
schedule = PretalxSchedule.model_validate(js)
62+
63+
return schedule

src/utils/transform.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
from src.models.europython import EuroPythonSession, EuroPythonSpeaker
2-
from src.models.pretalx import PretalxSpeaker, PretalxSubmission
1+
from src.models.europython import (
2+
EuroPythonScheduleBreak,
3+
EuroPythonScheduleSession,
4+
EuroPythonSession,
5+
EuroPythonSpeaker,
6+
Schedule,
7+
)
8+
from src.models.pretalx import PretalxScheduleBreak, PretalxSpeaker, PretalxSubmission
39
from src.utils.timing_relationships import TimingRelationships
410
from src.utils.utils import Utils
511

@@ -50,6 +56,7 @@ def pretalx_submissions_to_europython_sessions(
5056
),
5157
next_session=TimingRelationships.get_next_session(submission.code),
5258
prev_session=TimingRelationships.get_prev_session(submission.code),
59+
slot_count=submission.slot_count,
5360
)
5461
ep_sessions[code] = ep_session
5562

@@ -81,3 +88,65 @@ def pretalx_speakers_to_europython_speakers(
8188
ep_speakers[code] = ep_speaker
8289

8390
return ep_speakers
91+
92+
@staticmethod
93+
def pretalx_schedule_to_europython_schedule(
94+
breaks: list[PretalxScheduleBreak],
95+
ep_sessions: dict[str, EuroPythonSession],
96+
ep_speakers: dict[str, EuroPythonSpeaker],
97+
) -> Schedule:
98+
"""
99+
Transforms the given Pretalx schedule to EuroPython schedule
100+
"""
101+
102+
speakers_values = ep_speakers.values()
103+
speaker_code_to_name = {s.code: s.name for s in speakers_values}
104+
speaker_code_to_slug = {s.code: s.slug for s in speakers_values}
105+
speaker_code_to_website_url = {s.code: s.website_url for s in speakers_values}
106+
107+
# Merge breaks with the same start and end times
108+
breaks = Utils.merge_breaks(breaks)
109+
ep_breaks = []
110+
for title, duration, rooms, start in breaks:
111+
ep_break = EuroPythonScheduleBreak(
112+
title=title,
113+
duration=duration,
114+
rooms=rooms,
115+
start=start,
116+
)
117+
ep_breaks.append(ep_break)
118+
119+
# Split the sessions that covers multiple slots
120+
ep_schedule_sessions_split = []
121+
for session in ep_sessions.values():
122+
# Skip the sessions that are not assigned in the schedule
123+
if not session.start or not session.room:
124+
continue
125+
start_times = Utils.start_times(session)
126+
for start_time in start_times:
127+
ep_schedule_session = EuroPythonScheduleSession(
128+
code=session.code,
129+
slug=session.slug,
130+
title=session.title,
131+
session_type=session.session_type,
132+
speakers=[
133+
{
134+
"code": speaker_code,
135+
"name": speaker_code_to_name[speaker_code],
136+
"slug": speaker_code_to_slug[speaker_code],
137+
"website_url": speaker_code_to_website_url[speaker_code],
138+
}
139+
for speaker_code in session.speakers
140+
],
141+
tweet=session.tweet,
142+
level=session.level,
143+
total_duration=int(session.duration),
144+
rooms=[session.room],
145+
start=start_time,
146+
website_url=session.website_url,
147+
slot_count=session.slot_count,
148+
)
149+
150+
ep_schedule_sessions_split.append(ep_schedule_session)
151+
152+
return Schedule.from_events(ep_breaks + ep_schedule_sessions_split)

0 commit comments

Comments
 (0)