initial commit

This commit is contained in:
2026-06-30 19:52:07 +02:00
commit 9321d1fad6
8 changed files with 1899 additions and 0 deletions

321
README.md Normal file
View File

@@ -0,0 +1,321 @@
# pypretix-device
A minimal Python API client for [pretix](https://pretix.eu) device authentication and ticket scanning.
Designed for kiosk/scan-station devices that need to check volunteers or attendees in and out via QR/barcode scanning.
## Installation
```bash
pip install pypretix-device
```
Editable install for development:
```bash
pip install -e pypretix-device
```
## Quick Start
```python
from pypretix_device import auth, PretixDeviceClient
from pypretix_device.models import CheckinResult
# --- On first use: register device ---------------------------------
# Go to Organizer Settings → Devices in the pretix web UI to get a
# 24-character initialization token, then:
device = auth.get_or_register_device(
base_url="http://localhost",
init_token="xxxxxxxxxxxxxxxxxxxxxxxx", # from pretix web UI
device_name="example-scanner",
)
# Config is saved to ~/.config/pypretix/config.json automatically
# On subsequent runs it loads the stored token:
device = auth.get_or_register_device(
base_url="http://localhost",
init_token=None, # no token needed anymore
)
# --- Create the client ---------------------------------------------
client = PretixDeviceClient(
base_url="http://localhost",
api_token=device.api_token,
organizer_slug="FL",
event_slug="PB26",
checkin_list_id=1,
)
# --- Check a ticket in (entry) -------------------------------------
result = client.checkin("qrCodeSecretFromBarcode")
if result.status == "ok":
print(f"Welcome: {result.position['attendee_name']}")
else:
print(f"Error: {result.reason}") # e.g. "invalid", "already_redeemed"
# --- Check a ticket out (exit) -------------------------------------
result = client.checkout("qrCodeSecretFromBarcode")
if result.status == "ok":
print(f"Checked out: {result.position['attendee_name']}")
# --- Look up ticket status (Infodesk) ------------------------------
result = client.search("qrCodeSecretFromBarcode")
if result.position:
print(f"Found: {result.position['attendee_name']}")
print(f"Checkins: {len(result.position.get('checkins', []))}")
for ci in result.position['checkins']:
print(f" {ci['datetime'][:19]} - {ci['type']}")
else:
print("Token not found")
# --- List events ---------------------------------------------------
events = client.list_events()
for e in events:
print(f"{e.slug}{e.name}")
# --- List check-in lists for the current event ---------------------
lists = client.list_checkin_lists()
for cl in lists:
print(f"id={cl.id}: {cl.name} ({cl.checkin_count}/{cl.position_count})")
# --- Device info ---------------------------------------------------
info = client.device_info()
print(f"Device ID: {info.device_id}")
print(f"Server: {info.server_version}")
```
## API Reference
### `pypretix_device.auth` — Device Registration & Token Management
#### `get_or_register_device(base_url, init_token=None, device_name="example-scanner")`
Load stored device config, or register a new device if no config exists.
| Parameter | Description |
|-----------|-------------|
| `base_url` | pretix instance URL, e.g. `"http://localhost"` or `"https://events.example.com"` |
| `init_token` | 24-char initialization token from pretix web UI (Organizer Settings → Devices). Required only on first run. |
| `device_name` | Human-readable name for the device. |
**Returns:** `DeviceInfo` object with `api_token`, `device_id`, `organizer`, etc.
**Raises:** `RegistrationFailedError` if no config exists and no `init_token` provided.
#### `register_device(base_url, init_token, device_name="helferscanner")`
Register a new device. Same parameters as above. The device must already exist in the pretix web UI (with an initialization token created). The `init_token` is consumed and cannot be reused.
**Returns:** `DeviceInfo` with the new device API token.
#### `revoke_device()`
Permanently invalidate the device's API token and remove the config file. The device must be deleted from the pretix web UI as well.
### `PretixDeviceClient` — Ticket Scanning Operations
Create with:
```python
client = PretixDeviceClient(
base_url: str, # pretix instance URL
api_token: str, # Device API token (from registration)
organizer_slug: str, # Organizer slug, e.g. "FL"
event_slug: str, # Event slug, e.g. "PB26"
checkin_list_id: int | None, # Check-in list ID to use (e.g. 1)
)
```
#### `checkin(barcode, lists=None) -> CheckinResult`
Check a ticket in (entry). Sets `type="entry"` on the checkinrpc/redeem endpoint. Uses `force=true` to handle pending/unpaid orders gracefully.
| Parameter | Description |
|-----------|-------------|
| `barcode` | The QR code secret string scanned from the ticket |
| `lists` | Optional override for check-in list IDs. Falls back to client default. |
**Returns:** `CheckinResult`
- `status`: `"ok"` on success, `"error"` on failure, `"incomplete"` if questions need answering
- `reason`: Error code string (e.g. `"invalid"`, `"already_redeemed"`, `"unpaid"`, `"blocked"`)
- `position`: Order position data (attendee name, item info, checkin history)
- `require_attention`: `True` if the item/order has the checkin_attention flag set
- `checkin_texts`: Additional display strings for the user
- `questions`: Required question list if `status == "incomplete"`
#### `checkout(barcode, lists=None) -> CheckinResult`
Check a ticket out (exit). Sets `type="exit"` on the checkinrpc/redeem endpoint. Same parameters and return type as `checkin()`.
#### `search(barcode, lists=None) -> SearchResult`
Read-only ticket lookup (Infodesk mode). Queries checkinrpc/search for the ticket secret.
| Parameter | Description |
|-----------|-------------|
| `barcode` | QR code secret or attendee name to search |
| `lists` | Optional check-in list IDs to search within |
**Returns:** `SearchResult`
- `position`: Full order position dict with `checkins` history (list of `{datetime, type, gate}`)
- `require_attention`: `True` if the item/order has checkin_attention flag set
#### `list_events() -> list[Event]`
List all events for the configured organizer.
**Returns:** `list[Event]` with fields: `slug`, `name`, `testmode`, `date_from`, `date_to`.
#### `list_checkin_lists(event_slug=None) -> list[CheckinList]`
List check-in lists for the current (or specified) event.
**Returns:** `list[CheckinList]` with fields: `id`, `name`, `all_products`, `checkin_count`, `position_count`, `allow_entry_after_exit`.
#### `select_event(event_slug) -> bool`
Switch the client to a different event. Returns `True` on success.
#### `device_info() -> DeviceInfo \| None`
Fetch device information from the pretix server (device ID, server version, gate assignment).
#### `device_update(software_version) -> DeviceInfo \| None`
Notify the server about a software version update.
#### Connection status properties
| Property | Description |
|----------|-------------|
| `client.is_connected` | `True` if the last API request succeeded |
| `client.last_error` | Error message string, or `None` |
| `client.last_response_time` | Timestamp (float) of last successful response |
### Data Models
See `pypretix_device.models`:
| Class | Description |
|-------|-------------|
| `CheckinResult` | Result of checkin/checkout operations |
| `SearchResult` | Result of search (read-only) operations |
| `Event` | Event data (slug, name, dates) |
| `CheckinList` | Check-in list data (id, name, counts) |
| `DeviceInfo` | Device metadata (id, organizer, server version) |
| `AttendeeInfo` | Personalized ticket attendee data |
## Configuration
Config is stored in `~/.config/pypretix/config.json`:
```json
{
"base_url": "http://localhost",
"api_token": "a1b2c3d4e5f6...",
"device_id": 5,
"organizer": "TEST",
"device_name": "example-scanner",
"software_version": "0.0.1"
}
```
To use a custom config directory:
```python
import pypretix_device.models
pypretix_device.models.CONFIG_DIR = "/path/to/custom/config"
```
## Environment Variables (for running the app)
```bash
export PRETIX_BASE_URL="http://localhost" # pretix instance URL
export PRETIX_ORGANIZER="TEST" # organizer slug
export PRETIX_EVENT="PB26" # event slug
export PRETIX_CHECKIN_LIST="1" # check-in list ID
export PRETIX_DEVICE_INIT_TOKEN="xxx" # only needed on first run
```
## Error Codes
The `reason` field in `CheckinResult` contains one of these codes:
| Code | Meaning |
|------|---------|
| `invalid` | Ticket barcode not known |
| `already_redeemed` | Ticket already checked in |
| `unpaid` | Order not paid for |
| `blocked` | Ticket has been blocked |
| `invalid_time` | Ticket outside valid time window |
| `canceled` | Ticket has been canceled |
| `ambiguous` | Multiple tickets match — can't resolve |
| `revoked` | Ticket secret has been revoked |
| `unapproved` | Order not yet approved by organizer |
| `product` | Ticket product not allowed on this check-in list |
| `rules` | Check-in prevented by organizer-defined rules |
| `incomplete` | Required questions need answers (only with `questions_supported: True`) |
| `error` | Internal server error |
## Lifecycle Example
### 1. Initial device setup (one-time)
1. Log into pretix web UI as organizer admin
2. Go to **Organizer Settings → Devices → New Device**
3. Name the device (e.g. "Example-Scanner")
4. Note the **Initialization Token** (24 characters)
5. Store it as env var: `PRETIX_DEVICE_INIT_TOKEN=xxxxx`
6. On first run, `get_or_register_device()` uses this token and saves the device API token to config
### 2. Normal operation (daily)
On every subsequent run, config is loaded automatically — no token needed:
```python
device = get_or_register_device(base_url, init_token=None)
client = PretixDeviceClient(..., api_token=device.api_token)
result = client.checkin(barcode)
```
### 3. Ticket check-in flow
```
Scanner reads QR code → barcode string → client.checkin(barcode)
→ status="ok" → show OK + name
→ status="error" → show error code (e.g. "Bereits gescannt")
```
### 4. Ticket check-out flow
```
Scanner reads QR code → barcode string → client.checkout(barcode)
→ status="ok" → show OK + name
→ status="error" → show error code
```
### 5. Info desk lookup
```
Scanner reads QR code → barcode string → client.search(barcode)
→ position exists → show attendee name + checkin history
→ position None → show "Token nicht gefunden"
```
### 6. Deprovisioning
When the device is no longer needed:
```python
from pypretix_device.auth import revoke_device
revoke_device() # invalidates token + removes config
```
Also delete the device from the pretix web UI (Organizer Settings → Devices).
## License
GNUGPLv3