Metadata-Version: 2.4
Name: pypretix_device
Version: 0.0.2
Summary: A minimal API client for pretix device authentication and ticket scanning
Author-email: Toothwitch <witch@toothwit.ch>
License: MIT
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Requires-Dist: certifi
Dynamic: license-file

# 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
