Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ on:
branches:
- ep2024
- ep2025
- ep2026
schedule:
- cron: "*/10 * * * *" # every 10 minutes
- cron: "3-59/10 * * * *"
workflow_dispatch:

jobs:
tests:
name: Run tests
deploy:
name: Deploy to static server
runs-on: ubuntu-latest
timeout-minutes: 10

Expand All @@ -22,21 +23,21 @@ jobs:
- name: Set up Python 3
uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'

- name: Setup uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@v8.1.0

- name: Install dependencies from uv.lock
run: make deps/install
run: uv run make deps/install

- name: Download data
run: uv run make download > /dev/null 2>&1
run: uv run make download EXCLUDE="youtube" > /dev/null 2>&1
env:
PRETALX_TOKEN: ${{ secrets.PRETALX_TOKEN }}

- name: Transform data
run: uv run make transform > /dev/null 2>&1
run: uv run make transform EXCLUDE="youtube" > /dev/null 2>&1

- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.1
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14
rev: v0.15.16
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format

- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.12.1
rev: v2.23.0
hooks:
- id: pyproject-fmt

- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
rev: v0.25
hooks:
- id: validate-pyproject
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Variables for the project
# =========================
CONFERENCE ?= ep2025
CONFERENCE ?= ep2026
DATA_DIR ?= ./data/public/$(CONFERENCE)/

# Variables for remote host
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🎤 programapi

This project powers the **EuroPython 2025** website, Discord bot, and internal bot 🦜 by downloading, transforming, and serving clean, structured JSON files for sessions, speakers, and the schedule, all pulled from Pretalx.
This project powers the **EuroPython 2026** website, Discord bot, and internal bot 🦜 by downloading, transforming, and serving clean, structured JSON files for sessions, speakers, and the schedule, all pulled from Pretalx.

Built for transparency. Designed for reuse. Optimized for EuroPython.

Expand Down Expand Up @@ -87,14 +87,14 @@ PRETALX_TOKEN=your_api_token_here
Hosted at:

```
https://static.europython.eu/programme/ep2025/releases/current
https://static.europython.eu/programme/ep2026/releases/current
```

| Endpoint | Description |
|-----------------------------------------------------------------------------------------------------|-------------------------------|
| [`/speakers.json`](https://static.europython.eu/programme/ep2025/releases/current/speakers.json) | List of confirmed speakers |
| [`/sessions.json`](https://static.europython.eu/programme/ep2025/releases/current/sessions.json) | List of confirmed sessions |
| [`/schedule.json`](https://static.europython.eu/programme/ep2025/releases/current/schedule.json) | Latest conference schedule |
| [`/speakers.json`](https://static.europython.eu/programme/ep2026/releases/current/speakers.json) | List of confirmed speakers |
| [`/sessions.json`](https://static.europython.eu/programme/ep2026/releases/current/sessions.json) | List of confirmed sessions |
| [`/schedule.json`](https://static.europython.eu/programme/ep2026/releases/current/schedule.json) | Latest conference schedule |

---

Expand All @@ -111,4 +111,4 @@ Feel free to open an issue or reach us at [infra@europython.eu](mailto:infra@eur

---

📅 Last updated for: **EuroPython 2025**
📅 Last updated for: **EuroPython 2026**
4 changes: 2 additions & 2 deletions data/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"description": "Slides for the session"
}
],
"room": "South Hall 2A",
"room": "Auditorium Hall (S1)",
"start": "2099-07-10T14:00:00+02:00",
"end": "2099-07-10T15:00:00+02:00",
"website_url": "https://ep2099.europython.eu/session/example-talk",
Expand Down Expand Up @@ -211,6 +211,6 @@

### 🛠 Notes & Logic

- `room` normalization maps `"Main Hall"` sessions to `"Exhibit Hall"` — Poster sessions rejoice!
- `room` normalization maps `"Poster Hall"` sessions to `"Exhibit Hall"` — Poster sessions rejoice!
- All `"Registration & Welcome"` events automatically include **all active rooms**.
- Various `social_*_url` fields handle malformed inputs like `@name`, full URLs, or just `username`.
4 changes: 2 additions & 2 deletions data/examples/europython/sessions.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"sessions_before": null,
"next_session": null,
"prev_session": null,
"website_url": "https://ep2025.europython.eu/session/this-is-a-test-talk-from-a-test-speaker-about-a-test-topic",
"website_url": "https://ep2026.europython.eu/session/this-is-a-test-talk-from-a-test-speaker-about-a-test-topic",
"youtube_url": "https://youtube.com/watch?v=01234567890"
},
"B8CD4F": {
Expand All @@ -57,7 +57,7 @@
"sessions_before": null,
"next_session": null,
"prev_session": null,
"website_url": "https://ep2025.europython.eu/session/a-talk-with-shorter-title",
"website_url": "https://ep2026.europython.eu/session/a-talk-with-shorter-title",
"youtube_url": "https://youtube.com/watch?v=12345679012"
}
}
2 changes: 1 addition & 1 deletion data/examples/europython/speakers.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"bluesky_url": "https://bsky.app/profile/username.bsky.social",
"mastodon_url": null,
"twitter_url": null,
"website_url": "https://ep2025.europython.eu/speaker/a-speaker"
"website_url": "https://ep2026.europython.eu/speaker/a-speaker"
}
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "programapi"
version = "2025.4.5"
version = "2026.4.17"
description = "Programme API for EuroPython"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
4 changes: 2 additions & 2 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@


class Config:
event = "europython-2025"
event_dir_name = "ep2025"
event = "europython-2026"
event_dir_name = "ep2026"
api_version = "v1"

project_root = Path(__file__).resolve().parents[1]
Expand Down
25 changes: 10 additions & 15 deletions src/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,16 @@ class Room(Enum):
Rooms at the conference venue, this can change year to year
"""

# Tutorial/workshop rooms
club_a = "Club A"
club_b = "Club B"
club_c = "Club C"
club_d = "Club D"
club_e = "Club E"
club_h = "Club H"

# Conference rooms
forum_hall = "Forum Hall"
terrace_2a = "Terrace 2A"
terrace_2b = "Terrace 2B"
north_hall = "North Hall"
south_hall_2a = "South Hall 2A"
south_hall_2b = "South Hall 2B"
auditorium_s1 = "Auditorium Hall (S1)"
theatre_s2 = "Theatre Hall (S2)"
conference_s4 = "Conference Hall Complex (S4)"
chamber_s3a = "Chamber Hall A (S3A)"
chamber_s3b = "Chamber Hall B (S3B)"
conference_s4a = "Conference Hall Complex A (S4A)"
conference_s4b = "Conference Hall Complex B (S4B)"
glass_room_f0 = "Conference room F0 (Glass room)"
multifunctional_1 = "Multifunctional room 1 (2.015/2.016)"
fishbowl_f2 = "Reception Room F2 (Fishbowl)"
exhibit_hall = "Exhibit Hall"


Expand Down
3 changes: 2 additions & 1 deletion src/models/europython.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,13 @@ class EuroPythonSession(BaseModel):
next_session: str | None = None
prev_session: str | None = None
slot_count: int = Field(..., exclude=True)
scheduled_slot_starts: list[datetime] = Field(default_factory=list, exclude=True)
youtube_url: str | None = None

@field_validator("room", mode="before")
@classmethod
def handle_poster_room(cls, value) -> str | None:
if value and "Main Hall" in value:
if value and "Poster Hall" in value:
return "Exhibit Hall"
return value

Expand Down
15 changes: 9 additions & 6 deletions src/models/pretalx.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,17 @@ def process_values(cls, values) -> dict:
]
values["resources"] = resources

# Set slot information
# Set slot information from scheduled slots. Pretalx may include empty
# placeholder slots for multi-slot submissions.
if values.get("slots"):
first_slot = PretalxSlot.model_validate(values["slots"][0])
values["room"] = first_slot.room
values["start"] = first_slot.start
slots = [PretalxSlot.model_validate(slot) for slot in values["slots"]]

last_slot = PretalxSlot.model_validate(values["slots"][-1])
values["end"] = last_slot.end
if start_slot := next((slot for slot in slots if slot.start), None):
values["room"] = start_slot.room
values["start"] = start_slot.start

if end_slot := next((slot for slot in reversed(slots) if slot.end), None):
values["end"] = end_slot.end

return values

Expand Down
24 changes: 18 additions & 6 deletions src/utils/timing_relationships.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,38 @@ class TimingRelationships:
def compute(
cls, all_sessions: ValuesView[PretalxSubmission] | list[PretalxSubmission]
) -> None:
for session in all_sessions:
cls.all_sessions_in_parallel = {}
cls.all_sessions_after = {}
cls.all_sessions_before = {}
cls.all_next_session = {}
cls.all_prev_session = {}

timed_sessions = [
session for session in all_sessions if session.start and session.end
]

for session in timed_sessions:
if not session.start or not session.end:
continue

sessions_in_parallel = cls.compute_sessions_in_parallel(
session, all_sessions
session, timed_sessions
)
sessions_after = cls.compute_sessions_after(
session, all_sessions, sessions_in_parallel
session, timed_sessions, sessions_in_parallel
)
sessions_before = cls.compute_sessions_before(
session, all_sessions, sessions_in_parallel
session, timed_sessions, sessions_in_parallel
)

cls.all_sessions_in_parallel[session.code] = sessions_in_parallel
cls.all_sessions_after[session.code] = sessions_after
cls.all_sessions_before[session.code] = sessions_before
cls.all_next_session[session.code] = cls.compute_prev_or_next_session(
session, sessions_after, all_sessions
session, sessions_after, timed_sessions
)
cls.all_prev_session[session.code] = cls.compute_prev_or_next_session(
session, sessions_before, all_sessions
session, sessions_before, timed_sessions
)

@classmethod
Expand Down Expand Up @@ -70,7 +80,9 @@ def compute_sessions_in_parallel(
if (
other_session.code == session.code
or other_session.start is None
or other_session.end is None
or session.start is None
or session.end is None
):
continue

Expand Down
5 changes: 5 additions & 0 deletions src/utils/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ def pretalx_submissions_to_europython_sessions(
next_session=TimingRelationships.get_next_session(submission.code),
prev_session=TimingRelationships.get_prev_session(submission.code),
slot_count=submission.slot_count,
scheduled_slot_starts=[
slot.start
for slot in submission.slots
if slot.start and slot.end and slot.room
],
youtube_url=youtube_data.get(submission.code),
)
ep_sessions[code] = ep_session
Expand Down
3 changes: 3 additions & 0 deletions src/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ def start_times(session: EuroPythonSession) -> list[datetime]:

TODO: We assume a lot of things here, IMHO we should make things more flexible :)
"""
if session.scheduled_slot_starts:
return session.scheduled_slot_starts

if session.slot_count == 2:
# Half day sessions have 2 slots, 90 minutes each, with a 15-minute break in between
return [session.start, session.start + timedelta(minutes=90 + 15)]
Expand Down
Loading