Skip to content

Commit b5dd81a

Browse files
committed
download pretix data
1 parent 0f2483f commit b5dd81a

File tree

6 files changed

+381
-3
lines changed

6 files changed

+381
-3
lines changed

intbot/core/integrations/pretix.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import logging
2+
from typing import Any
3+
4+
import httpx
5+
from core.models import PretixData
6+
from django.conf import settings
7+
8+
logger = logging.getLogger(__name__)
9+
10+
PRETALX_EVENTS = [
11+
"2022",
12+
"ep2023",
13+
"ep2024",
14+
"ep2025",
15+
]
16+
17+
ENDPOINTS = {
18+
# Questions need to be passed to include answers in the same endpoint,
19+
# saving us later time with joining the answers.
20+
PretixData.PretixResources.orders: "orders/",
21+
PretixData.PretixResources.products: "items/",
22+
PretixData.PretixResources.vouchers: "vouchers/",
23+
}
24+
25+
26+
JsonType = dict[str, Any]
27+
28+
29+
def get_event_url(event):
30+
assert event in PRETALX_EVENTS
31+
32+
pretix_url = f"https://tickets.europython.eu"
33+
url = f"{pretix_url}/api/v1/organizers/europython/events/{event}/"
34+
return url
35+
36+
37+
def fetch_pretix_data(
38+
event: str, resource: PretixData.PretixResources
39+
) -> list[JsonType]:
40+
headers = {
41+
"Authorization": f"Token {settings.PRETIX_API_TOKEN}",
42+
"Content-Type": "application/json",
43+
}
44+
45+
base_url = get_event_url(event)
46+
endpoint = ENDPOINTS[resource]
47+
url = f"{base_url}{endpoint}"
48+
49+
# Pretalx paginates the output, so we will need to do multiple requests and
50+
# then merge multiple pages to one big dictionary
51+
results = []
52+
page = 0
53+
54+
# This takes advantage of the fact that url will contain a url to the
55+
# next page, until there is more data to fetch. If this is the last page,
56+
# then the url will be None (falsy), and thus stop the while loop.
57+
while url:
58+
page += 1
59+
response = httpx.get(url, headers=headers)
60+
61+
if response.status_code != 200:
62+
raise Exception(f"Error {response.status_code}: {response.text}")
63+
64+
logger.info("Fetching data from %s, page %s", url, page)
65+
66+
data = response.json()
67+
results += data["results"]
68+
url = data["next"]
69+
70+
return results
71+
72+
73+
def download_latest_orders(event: str) -> PretixData:
74+
data = fetch_pretix_data(event, PretixData.PretixResources.orders)
75+
76+
pretalx_data = PretixData.objects.create(
77+
resource=PretixData.PretixResources.orders,
78+
content=data,
79+
)
80+
81+
return pretalx_data

intbot/core/models.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,7 @@ class PretalxData(models.Model):
8787
"""
8888
Table to store raw data download from pretalx for later parsing.
8989
90-
We first download data from pretalx to this table, and then fire a separate
91-
background task that pulls data from this table and stores in separate
92-
"business" tables, like "Proposal" or "Speaker".
90+
We first download data from pretix, then we later parse the latest jsons.
9391
"""
9492

9593
class PretalxResources(models.TextChoices):
@@ -110,3 +108,30 @@ class PretalxResources(models.TextChoices):
110108

111109
def __str__(self):
112110
return f"{self.uuid}"
111+
112+
113+
class PretixData(models.Model):
114+
"""
115+
Table to store raw data download from pretix for later parsing.
116+
117+
We first download data from pretix, then we later parse the latest jsons.
118+
"""
119+
120+
class PretixResources(models.TextChoices):
121+
orders = "orders", "Orders"
122+
products = "products", "Products"
123+
vouchers = "vouchers", "Vouchers"
124+
125+
uuid = models.UUIDField(default=uuid.uuid4)
126+
resource = models.CharField(
127+
max_length=255,
128+
choices=PretixResources.choices,
129+
)
130+
content = models.JSONField()
131+
132+
created_at = models.DateTimeField(auto_now_add=True)
133+
modified_at = models.DateTimeField(auto_now=True)
134+
processed_at = models.DateTimeField(blank=True, null=True)
135+
136+
def __str__(self):
137+
return f"{self.uuid}"

intbot/intbot/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ def get(name) -> str:
200200
# Pretalx
201201
PRETALX_API_TOKEN = get("PRETALX_API_TOKEN")
202202

203+
# Pretix
204+
PRETIX_API_TOKEN = get("PRETIX_API_TOKEN")
205+
203206

204207
if DJANGO_ENV == "dev":
205208
DEBUG = True
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import pytest
2+
import respx
3+
from core.integrations import pretix
4+
from core.models import PretixData
5+
from httpx import Response
6+
7+
8+
def orders_pages_generator(url):
9+
"""
10+
Generator to simulate pagination.
11+
12+
Extracted to a generator because we use it in multiple places
13+
"""
14+
yield Response(
15+
200,
16+
json={
17+
"results": [
18+
{"hello": "world"},
19+
],
20+
"next": f"{url}&page=2",
21+
},
22+
)
23+
24+
yield Response(
25+
200,
26+
json={
27+
"results": [
28+
{"foo": "bar"},
29+
],
30+
# It's important to make it last page in tests.
31+
# Otherwise it will be infinite loop :)
32+
"next": None,
33+
},
34+
)
35+
36+
37+
def speaker_pages_generator(url):
38+
"""
39+
Generator to simulate pagination.
40+
41+
Extracted to a generator because we use it in multiple places
42+
"""
43+
yield Response(
44+
200,
45+
json={
46+
"results": [
47+
{"hello": "world"},
48+
],
49+
"next": f"{url}&page=2",
50+
},
51+
)
52+
53+
yield Response(
54+
200,
55+
json={
56+
"results": [
57+
{"foo": "bar"},
58+
],
59+
# It's important to make it last page in tests.
60+
# Otherwise it will be infinite loop :)
61+
"next": None,
62+
},
63+
)
64+
65+
66+
@respx.mock
67+
def test_fetch_orders_from_pretix():
68+
url = "https://tickets.europython.eu/api/v1/organizers/europython/ep2025/orders/"
69+
data = orders_pages_generator(url)
70+
respx.get(url).mock(return_value=next(data))
71+
respx.get(url + "&page=2").mock(return_value=next(data))
72+
73+
orders = pretix.fetch_pretix_data(
74+
"ep2025",
75+
PretixData.PretixResources.orders,
76+
)
77+
78+
assert orders == [
79+
{"hello": "world"},
80+
{"foo": "bar"},
81+
]
82+
83+
84+
@respx.mock
85+
@pytest.mark.django_db
86+
def test_download_latest_orders():
87+
url = "https://tickets.europython.eu/api/v1/organizers/europython/ep2025/orders/"
88+
data = orders_pages_generator(url)
89+
respx.get(url).mock(return_value=next(data))
90+
respx.get(url + "&page=2").mock(return_value=next(data))
91+
92+
pretix.download_latest_orders("ep2025")
93+
94+
pd = PretixData.objects.get(resource=PretixData.PretixResources.orders)
95+
assert pd.resource == "orders"
96+
assert pd.content == [
97+
{"hello": "world"},
98+
{"foo": "bar"},
99+
]

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies = [
2525
"respx>=0.22.0",
2626
"pydantic>=2.10.6",
2727
"freezegun>=1.5.1",
28+
"ipython>=9.1.0",
2829
]
2930

3031
[tool.pytest.ini_options]

0 commit comments

Comments
 (0)