initial commit
This commit is contained in:
321
README.md
Normal file
321
README.md
Normal 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
|
||||
Reference in New Issue
Block a user