Pretalx Integration¶
This document covers the architecture of the pretalx-client package, how to
regenerate it from the upstream OpenAPI schema, the compatibility guarantees it
provides, and the rollout plan for publishing it as a standalone library.
Architecture Overview¶
The Pretalx integration is organized into three layers, each with a distinct responsibility:
packages/pretalx-client/src/pretalx_client/
+-- generated/ # Layer 1: Codegen output (DO NOT EDIT)
| +-- models.py # datamodel-code-generator output
+-- adapters/ # Layer 2: Runtime quirk handling
| +-- normalization.py # Multilingual field resolution, ID-to-name mapping
+-- models.py # Layer 2: Typed dataclasses (PretalxSpeaker, PretalxTalk, PretalxSlot)
+-- client.py # Layer 3: Stable public API (PretalxClient)
+-- __init__.py # Public re-exports
Layer 1: Generated Models¶
Output of datamodel-code-generator run against the Pretalx OpenAPI 3.0.3
schema. Located in packages/pretalx-client/src/pretalx_client/generated/.
These files are machine-generated and must never be edited by hand – they are
overwritten on every schema sync.
Layer 2: Adapters and Typed Models¶
Handwritten code that compensates for runtime behavior not captured in the OpenAPI schema:
models.py– Frozen dataclasses (PretalxSpeaker,PretalxTalk,PretalxSlot) withfrom_api()class methods that normalize raw API dicts into well-typed Python objects. Handles multilingual field resolution, ID-to-name mapping, and datetime parsing.adapters/normalization.py– Standalone helpers (localized(),resolve_id_or_localized()) for resolving Pretalx’s multilingual and integer-ID field patterns.
These files are maintained manually and are the primary place to handle new API quirks as they are discovered.
Layer 3: Client Facade¶
PretalxClient in client.py is the stable public API. It provides methods
like fetch_speakers(), fetch_talks(), and fetch_schedule() that return
typed dataclasses. Consumers (including the Django sync service) import from
here and should not depend on generated code or adapter internals directly.
Django Integration¶
src/django_program/pretalx/sync.py contains PretalxSyncService, which
consumes PretalxClient and maps the typed dataclasses into Django ORM models
(Speaker, Talk, ScheduleSlot, Room). The re-export shim at
src/django_program/pretalx/client.py bridges the workspace package into the
Django app’s import namespace.
Overrides¶
Pretalx is the source of truth for your schedule, but the real world does not care about your source of truth. Speakers cancel at the last minute. Rooms get renamed. A talk moves from Hall A to Hall B because of a fire alarm. The override system lets organizers patch any synced field locally without touching the upstream Pretalx record, and those patches survive re-syncs.
How Overrides Work¶
Each synced model (Talk, Speaker, Room) has a matching one-to-one override
model (TalkOverride, SpeakerOverride, RoomOverride). Override fields
default to blank/null. When a field is populated, it takes priority over the
synced value. When blank, the original synced value shows through.
The synced models expose effective_* properties that handle this resolution:
talk = Talk.objects.get(pretalx_code="ABCDEF")
# Returns the synced title unless a TalkOverride sets override_title
talk.effective_title
# Returns "cancelled" if is_cancelled=True on the override,
# otherwise override_state if set, otherwise the synced state
talk.effective_state
Use effective_* properties in templates and views. Use the bare fields
(talk.title, talk.state) only when you need the raw synced value.
Override Models¶
TalkOverride¶
One-to-one with Talk. Overridable fields:
Field |
Type |
Effect |
|---|---|---|
|
|
Replaces |
|
|
Replaces |
|
|
Replaces |
|
|
Replaces |
|
|
Replaces |
|
|
Replaces |
|
|
Forces |
|
|
Internal-only note (not displayed to attendees) |
The is_cancelled flag takes priority over override_state. If both are set,
the talk shows as cancelled.
SpeakerOverride¶
One-to-one with Speaker. Overridable fields:
Field |
Type |
Effect |
|---|---|---|
|
|
Replaces |
|
|
Replaces |
|
|
Replaces |
|
|
Replaces |
|
|
Internal-only note |
RoomOverride¶
One-to-one with Room. Overridable fields:
Field |
Type |
Effect |
|---|---|---|
|
|
Replaces |
|
|
Replaces |
|
|
Replaces |
|
|
Internal-only note |
SubmissionTypeDefault¶
SubmissionTypeDefault is not an override in the same sense. It provides
fallback room and time-slot values for talks of a given Pretalx submission type
(e.g. “Poster”, “Tutorial”) that arrive from Pretalx without scheduling data.
Field |
Type |
Purpose |
|---|---|---|
|
|
The Pretalx type name to match (case-sensitive) |
|
|
Room assigned to unscheduled talks of this type |
|
|
Date for synthesized slot times |
|
|
Start time combined with |
|
|
End time combined with |
Type defaults are applied automatically at the end of sync_all() via
apply_type_defaults(). They only affect talks where room is None, so
talks already assigned a room by Pretalx are left untouched.
Creating Overrides via Django Admin¶
All override models are registered in the Django admin. Navigate to Pretalx > Talk Overrides (or Speaker Overrides, Room Overrides) to create or edit overrides.
Cancel a talk¶
Go to Pretalx > Talk Overrides > Add.
Select the talk from the
talkfield.Check
is_cancelled.Add a note explaining why (e.g. “Speaker flight cancelled”).
Save. The talk’s
effective_statenow returns"cancelled".
Move a talk to a different room¶
Go to Pretalx > Talk Overrides > Add.
Select the talk.
Set
override_roomto the new room.Optionally adjust
override_slot_startandoverride_slot_endif the time also changed.Save.
Rename a room on-site¶
Go to Pretalx > Room Overrides > Add.
Select the room.
Set
override_nameto the correct name (e.g. “Hall B” instead of “Ballroom 2”).Save. All schedule displays using
room.effective_namenow show the new name.
Set up defaults for poster sessions¶
Go to Pretalx > Submission Type Defaults > Add.
Set
submission_typeto"Poster"(must match the Pretalx type exactly).Set
default_roomto the poster hall.Set
default_date,default_start_time, anddefault_end_timefor the poster session window.Save. On the next sync, any poster talks arriving without scheduling data get assigned to this room and time slot.
Overrides and Sync¶
Overrides are stored in separate database tables from the synced data. When
sync_all() runs, it updates the Talk, Speaker, and Room tables with
fresh data from Pretalx. The override tables are not touched. This means:
Overrides persist across syncs. A
TalkOverridecreated before a sync is still there after the sync completes.The synced (base) fields get updated to match Pretalx. If Pretalx changes a talk’s title, the
Talk.titlefield updates, butTalkOverride.override_titlestays as-is. Theeffective_titleproperty still returns the override value.Override resolution happens at read time via the
effective_*properties, not during sync. There is no merge step.Deleting an override restores the synced values instantly. The base data was never modified.
The conference field on each override is validated against the parent entity’s
conference. The admin and model validation both reject overrides that reference
entities from a different conference.
The is_empty Property¶
Each override model has an is_empty property that returns True when no
override fields carry a value (only the note field is populated, or everything
is blank). Use this to identify overrides that were created but never filled in:
stale = TalkOverride.objects.all()
for override in stale:
if override.is_empty:
override.delete()
Schema Regeneration¶
Prerequisites¶
uvinstalled and project dependencies synced (uv sync --all-groups)Internet access (fetches from
https://docs.pretalx.org/schema.yml)
Running the Pipeline¶
make pretalx-sync-schema
This is an alias for make pretalx-codegen, which chains three steps:
Validate – Runs
scripts/pretalx/validate_schema.pyto verify the OpenAPI schema integrity (required top-level keys, structure checks).Generate models – Runs
scripts/pretalx/generate_client.pyagainst the schema and writes Python dataclass models topackages/pretalx-client/src/pretalx_client/generated/models.py.Generate HTTP client (
make pretalx-generate-http-client) – Runsscripts/pretalx/generate_http_client.pyto produce the typed HTTP client.
Automated Schema Drift Detection¶
A GitHub Actions workflow (.github/workflows/pretalx-schema-sync.yml) runs
weekly on Monday at 06:00 UTC. It executes the full make pretalx-sync-schema
pipeline and, if any files changed, opens a pull request on the
chore/pretalx-schema-sync branch. The workflow can also be triggered manually
via workflow_dispatch.
After Regeneration¶
If the generated models changed:
Run
make cito verify lint, formatting, type-checks, and tests pass.Review the diff in
generated/models.pyfor any breaking changes to field names or types that would affect the adapter layer.Update
models.pyandclient.pyif new fields need to be surfaced through the typed dataclasses.Update
sync.pyon the Django side if the domain model mapping needs to change.
Compatibility Guarantees¶
What is Stable¶
The client facade (PretalxClient and the typed dataclasses exported from
pretalx_client) provides a stable API. Consumers can depend on:
Method signatures:
fetch_speakers(),fetch_talks(),fetch_schedule(),fetch_rooms(),fetch_submissions(),fetch_events()Return types:
list[PretalxSpeaker],list[PretalxTalk],list[PretalxSlot]Dataclass field names and types on
PretalxSpeaker,PretalxTalk,PretalxSlot
What May Change¶
Generated code (
generated/) is regenerated from upstream and may change at any time. Do not import frompretalx_client.generateddirectly.Adapter internals (
adapters/) are implementation details. The public helperslocalized()andresolve_id_or_localized()are available but not part of the semver contract.
What the Adapters Handle¶
The adapters exist because the real Pretalx API behaves differently from what the OpenAPI schema documents. These runtime quirks are not bugs to report upstream – they are intentional behavior that varies by Pretalx instance configuration and event setup.
Known Non-Schema Runtime Quirks¶
The following behaviors are observed in production Pretalx instances but are not documented in the OpenAPI schema. The adapter layer handles all of them transparently.
Multilingual Fields Return Variable Types¶
Pretalx localized fields (name, title, abstract, etc.) return either a
plain str or a dict[str, str] keyed by language code, depending on the
instance’s Accept-Language header handling and localization configuration.
# When the instance has a single language:
{"title": "My Talk"}
# When the instance has multiple languages:
{"title": {"en": "My Talk", "de": "Mein Vortrag"}}
The _localized() helper resolves both forms, preferring the en key when
available.
ID Fields vs. Inline Objects¶
The submission_type, track, and room fields on submissions may be returned
as integer IDs or as localized inline objects, depending on whether the request
is authenticated and which API version the instance runs.
# Integer ID form (common with API tokens):
{"submission_type": 42, "track": 7, "room": 3}
# Inline object form (common with public/unauthenticated access):
{"submission_type": {"en": "Talk"}, "track": {"en": "Web"}, "room": {"en": "Main Hall"}}
The _resolve_id_or_localized() helper handles both. When IDs are returned, the
client pre-fetches /submission-types/, /tracks/, and /rooms/ endpoints to
build lookup tables.
/talks/ Endpoint Returns 404¶
Some Pretalx events (notably PyCon US) do not expose the /talks/ endpoint at
all, returning HTTP 404. The client falls back to
/submissions/?state=confirmed combined with /submissions/?state=accepted to
capture all scheduled content including tutorials and sponsor workshops.
# The client handles this automatically:
talks = client.fetch_talks() # tries /talks/, falls back to /submissions/
/slots/ Uses Different Field Names¶
The paginated /slots/ endpoint returns slot objects with different keys than
the legacy /schedules/latest/ response:
Uses
submissioninstead ofcodeto reference the linked talkDoes not include a
titlefield (the title must be looked up from the talk)Returns
roomas an integer ID rather than a name string
The PretalxSlot.from_api() method handles both formats.
Speaker Avatar Field Name Varies¶
Different Pretalx instances use either avatar_url or avatar for the speaker
profile image URL. PretalxSpeaker.from_api() checks both keys.
Rollout Checklist¶
Versioning¶
The pretalx-client package follows semver:
Change Type |
Version Bump |
Example |
|---|---|---|
Generated code regeneration (no adapter changes) |
Patch |
|
New adapter/facade features (backward-compatible) |
Minor |
|
Breaking changes to facade API |
Major |
|
Generated code changes alone are patch bumps because the facade API remains stable. If a schema change requires adapter updates that alter the public dataclass fields, that is a minor or major bump depending on backward compatibility.
Release Process¶
The package will be published to PyPI from its own repository at
github.com/JacobCoffee/pretalx-client. The release flow:
Bump the version in
packages/pretalx-client/pyproject.toml:cd packages/pretalx-client uv version --bump patch # or minor/major
Run
make cito verify everything passes.Commit and tag:
git add packages/pretalx-client/pyproject.toml git commit -m "chore: bump pretalx-client to X.Y.Z" git tag pretalx-client-vX.Y.Z git push origin main --tags
The tag triggers a publish workflow that builds and uploads to PyPI.
Migration from In-Tree Client¶
Currently, src/django_program/pretalx/client.py is a re-export shim that
imports everything from the pretalx_client workspace package and re-exports it
under the django_program.pretalx.client namespace:
# src/django_program/pretalx/client.py (current shim)
from pretalx_client.client import PretalxClient
from pretalx_client.models import PretalxSpeaker, PretalxTalk, PretalxSlot, ...
This allows existing code in sync.py and elsewhere to continue importing from
django_program.pretalx.client without changes.
Migration path:
Now (workspace phase): The shim exists. All Django-side code imports from
django_program.pretalx.client. Thepretalx-clientpackage is resolved via[tool.uv.sources]as a workspace member.When published to PyPI: Remove the
[tool.uv.sources]workspace override souvresolvespretalx-clientfrom PyPI instead. The shim continues to work – no code changes needed in consumers.Eventually: Update imports in
sync.pyand other Django code to import directly frompretalx_client:# Before: from django_program.pretalx.client import PretalxClient # After: from pretalx_client import PretalxClient
Then remove the shim file entirely.
uv Sources Strategy¶
The pyproject.toml at the project root uses [tool.uv.sources] to bridge
development and production:
# During development: resolve from workspace
[tool.uv.sources]
pretalx-client = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
# In production (after PyPI publish): remove the sources override
# uv will resolve pretalx-client from PyPI using the version in [project.dependencies]
[project]
dependencies = [
"pretalx-client>=0.1.0",
]
The workspace member at packages/pretalx-client/ has its own pyproject.toml
with independent versioning. During development, uv sync links it as an
editable install. In CI and production, once the sources override is removed,
uv fetches the published wheel from PyPI.