# API reference

This page documents the key functions and classes available when building uproot experiments. All of these are available automatically in app modules via `from uproot.smithereens import *` and `from uproot.fields import *`.

## Page types

### `Page`

The standard page type. Displays a template and optionally collects form data.

```python
class MyPage(Page):
    fields = dict(participant_name=StringField(label="Your name"))

    @classmethod
    def templatevars(page, player):
        return dict(greeting="Hello")
```

See [Page methods](https://uproot.science/reference/page-methods.md) for all available methods.

### `NoshowPage`

A page that runs logic without displaying anything to the participant.

```python
class Calculate(NoshowPage):
    @classmethod
    def after_always_once(page, player):
        player.payoff = player.correct * 10
```

### `GroupCreatingWait`

A wait page that forms groups of participants.

```python
class GroupPlease(GroupCreatingWait):
    group_size = 2

    @classmethod
    def after_grouping(page, group):
        for player, role in zip(group.players, ["proposer", "responder"]):
            player.role = role
```

### `SynchronizingWait`

A wait page that synchronizes group or session members.

```python
class Sync(SynchronizingWait):
    @classmethod
    def all_here(page, group):
        for player in group.players:
            set_payoff(player)
```

Set `synchronize = "session"` to synchronize across the entire session.

## SmoothOperators

### `RandomOrder`

Shuffles pages into a random order per participant.

```python
page_order = [RandomOrder(TaskA, TaskB, TaskC)]
```

### `Bracket`

Groups pages as an atomic unit within `RandomOrder` or `Between`. SmoothOperators take only pages and other SmoothOperators, so use `Bracket` instead of a list.

```python
page_order = [RandomOrder(Bracket(Intro1, Task1), Bracket(Intro2, Task2))]
```

### `Rounds`

Repeats pages a fixed number of times. Sets `player.round` (1-indexed), `player.block`, and `player.round_nested`.

```python
page_order = [Rounds(Decision, Feedback, n=5)]
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `n` | `int` | — | Number of rounds (0 or more) |
| `block` | `str` or `None` | `None` | [Block](https://uproot.science/building/operators.md#blocks) name (a valid Python identifier); required if an app can run several loops |

### `Repeat`

Repeats pages indefinitely until `player.add_round = False`. Sets the same fields as `Rounds`.

```python
page_order = [Repeat(Trial, Check), Done]
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `block` | `str` or `None` | `None` | [Block](https://uproot.science/building/operators.md#blocks) name (a valid Python identifier); required if an app can run several loops |

### `Between`

Randomly selects one option per participant. Appends the pages of the selected option to `player.between_showed`, one list per `Between` (see [Tracking which condition was shown](https://uproot.science/building/operators.md#tracking-which-condition-was-shown)).

```python
page_order = [Between(Treatment, Control)]
```

## `PlayerContext`

Base class for reusable computed properties. During page execution, properties on the current app’s `Context` class are available in Python and templates as `player.context.*`.

```python
class Context(PlayerContext):
    @property
    def earnings(self):
        return self.player.payoff * C.EXCHANGE_RATE
```

| Access | How the class is selected | Use it when |
|--------|---------------------------|-------------|
| `player.context` | uproot reads `player.app` and uses that app’s `Context` class | A page method or template is running inside the app |
| `Context(player)`{ .text-nowrap } | Python resolves the `Context` name directly in the app module | Code is outside page execution, such as a callable `page_order(player=)` or `digest(session)` |

`player.context` is `None` when the participant has no active app or the active app defines no `Context` class. See [The PlayerContext class](https://uproot.science/building/pages.md#the-playercontext-class) for the lifecycle details and examples.

## Real-time functions

### `notify`

Send data from one player to one or more recipients.

```python
notify(sender, recipients, data, event="EventName", where=...)
```

| Parameter | Description |
|-----------|-------------|
| `sender` | The sending player (determines page context) |
| `recipients` | Player, `StorageBunch`, or list of players |
| `data` | Any JSON-serializable data |
| `event` | Custom event name (default: `"_uproot_Received"`) |
| `where` | Recipient page index; `...` delivers regardless of page |

### `send_to`

Send data to one or more players without a sender context.

```python
send_to(recipients, data, event="EventName", where=...)
```

### `send_to_one`

Send data to a single player.

```python
send_to_one(player, data, event="EventName", where=...)
```

### `reload`

Force a player’s browser to reload the current page.

```python
reload(player)
```

### `move_to_page`

Move a player forward to the next occurrence of a page in their page order, skipping the pages in between.

```python
move_to_page(player, TargetPage, reload_=True)
```

- If the target lies inside [SmoothOperators](https://uproot.science/building/operators.md), uproot sets them up on the way, as if the player had arrived normally: the target’s round starts, a `RandomOrder` is shuffled (pages that end up before the target are skipped), and a `Between` makes its selection. Moving into a later app starts that app.
- SmoothOperators that the move skips entirely are left out. For example, moving from round 2 to a page after the `Rounds` keeps `player.round == 2`.
- It raises a `ValueError` if the target is not ahead of the player, or if a `Between` on the way does not select the target. The player then stays where they are, and nothing about them changes, not even in the data.
- The player arrives at the target as navigation does: its `early` and `before_always_once` run, and if its `show` is `False`, it is skipped. If you call `move_to_page` from a page method of the player’s own request, for example in `before_once`, the other methods of the same step still run first (see [the lifecycle](https://uproot.science/reference/page-methods.md#lifecycle-overview)), and then the player enters the target.

### `move_to_end`

Move a player past all remaining pages.

```python
move_to_end(player, reload_=True)
```

### `spawn`

Run an async function as a supervised background task. The task keeps running after the calling method returns. Exceptions are logged, and all spawned tasks are cancelled at server shutdown. Tasks do not survive server restarts.

```python
spawn(my_coroutine(player))
```

Because spawned tasks run outside a page method, wrap data mutations in a context manager (`with player as p:`). See [Background tasks](https://uproot.science/multiplayer/real-time.md#background-tasks-with-spawn) for examples.

## Dropout functions

### `watch_for_dropout`

Monitor a player for disconnection and call a handler when they go offline.

```python
watch_for_dropout(player, handler, tolerance=30.0)
```

### `stop_watching_for_dropout`

End all of a player’s dropout watchers, for example once they have finished. See [Stopping the watcher](https://uproot.science/advanced/dropouts.md#stopping-the-watcher).

```python
stop_watching_for_dropout(player)
```

### `mark_dropout`

Add a player to the manual dropout set used by the dropout watcher.

```python
mark_dropout(player_pid)
```

## Group functions

### `create_group`

Create a group from a list of players.

```python
gid = create_group(session, [player1, player2], gname="custom_name", overwrite=False)
```

### `create_groups`

Create multiple groups at once.

```python
gids = create_groups(session, [[p1, p2], [p3, p4]])
```

### `add_to_group`

Add players to an existing group.

```python
add_to_group(group, player)
add_to_group(group, [player1, player2])
add_to_group(group, player, overwrite=True)  # reassign players already in a group
```

## The `@live` decorator

Makes a page method callable from JavaScript via WebSocket.

```python
class MyPage(Page):
    @live
    async def my_method(page, player, arg: str):
        return {"result": arg.upper()}
```

Called from JavaScript:

```javascript
uproot.invoke("my_method", "hello").then(data => console.log(data.result));
```

## App HTTP APIs

Apps may expose HTTP endpoints by defining `api(request, session)` or `api2(request, session)`.

### Operator APIs

`api` is served at `/api/{appname}/{sname}/` and requires an `Authorization: Bearer ...` header matching one of the server API keys in `upd.API_KEYS`. Use it for programmatic access that should be limited to trusted operators or scripts.

### Participant APIs

`api2` is served at `/api2/{appname}/{sname}/`. It is for participant-browser assets and callbacks, such as loading a captcha image. Direct requests remain public: anyone who knows the app name and session name can call the endpoint without participant credentials.

!!! warning "Trust boundary"
    Treat an `api2` request as public unless the callback receives an authenticated `player`. Participant authentication identifies one participant; it does not grant operator privileges. Do not expose admin-only data or actions through `api2`. Use `api` for those.

#### Identifying the calling participant

This is an advanced feature. Use it when browser code must call an HTTP endpoint as the participant who is viewing the current page—for example, to upload data with `fetch()` while saving the result on that participant.

Define `player` as an optional callback argument and reject public calls explicitly:

```python
from fastapi import HTTPException

async def api2(request, session, player=None):
    if player is None:
        raise HTTPException(status_code=403, detail="Participant required")

    data = await request.json()
    player.last_action = data["action"]
    return {"saved": True}
```

Call the endpoint from a participant template with `uproot.api2()`:

```html
<script>
async function saveAction(action) {
    const response = await uproot.api2("my_app", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({action}),
    });

    if (!response.ok) {
        throw new Error(`Request failed with status ${response.status}`);
    }

    return response.json();
}
</script>
```

`uproot.api2(appname, options)` accepts the usual [`fetch()` options](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#parameters) and returns its `Promise<Response>`. It builds the URL for the current session and adds the current participant’s name and CSRF proof as request headers. It also defaults `credentials` to `"same-origin"`. Do not construct or copy these authentication headers yourself.

The server handles the three credential states differently:

| Credential state | Result |
|------------------|--------|
| Both headers absent | The public callback runs with `request` and `session`; the `player` keyword is omitted |
| Both headers present and valid | The callback also receives the authenticated `player` |
| One header missing, either header invalid, or the CSRF proof non-ASCII | The server returns 403 without running the callback |

For an authenticated call, `request.state.uproot_player` refers to the same object as `player`. For a public call, it is `None`. The `session` and authenticated `player` remain inside their storage contexts while the callback runs, so assignments such as `player.last_action = ...` are saved normally.

!!! warning "Keep the optional default"
    The server omits the `player` keyword entirely for a public call. Write `player=None` if a callback may receive either kind of request. Merely adding a `player` parameter does not protect the endpoint—check for `None` and return an error as shown above.

!!! warning "Use only the authenticated player"
    Do not identify a participant from a username in the query string or request body. Those values are controlled by the caller. Read and modify only the authenticated `player` supplied by uproot.

## Utility functions

### `rng`

Create an independent `random.Random` generator with an OS-random seed. Store it on a player if you need to resume the same stream later.

```python
player.treatment = rng().choice(["A", "B"])
player.rng = rng()
player.bomb = player.rng.randint(1, 25)
```

See [Random numbers](https://uproot.science/building/data.md#random-numbers).

### `uuid`

Return a UUID (uuid7 if the Python version provides it, otherwise uuid4).

```python
player.token = str(uuid())
```

### `cu`

Create a `Decimal` from a string. Shorthand for `Decimal(value)`.

```python
endowment = cu("10.00")
```

### `safe`

Mark a string as HTML-safe (will not be escaped in templates).

```python
label=StringField(label=safe("Enter a value <b>in euros</b>"))
```

### `data_uri`

Convert binary data to a data URI string for embedding in HTML.

```python
player.image_uri = data_uri(image_bytes)
```

The MIME type is inferred from common file signatures such as JPEG, PNG, GIF, PDF, ZIP, and MP4. Unknown data is emitted as `application/octet-stream`.

### `append_to_csv`

Add one row to a CSV file outside the database. A relative path is relative to your project folder. The file is created if it does not exist, and new keys become new columns.

```python
append_to_csv("payments.csv", {"player": player.name, "iban": data["iban"]})
```

The whole file is rewritten for each row and then swapped in at once, so a crash never leaves a half-written file. This makes it slow for very large files. Use it for data that must stay out of the database, such as payment details from [stealth fields](https://uproot.science/building/forms.md#sensitive-data).

!!! warning
    The file lives on your server’s disk, not in the database. It is not part of data exports or `uproot dump`, and hosting platforms with temporary disks delete it when the app restarts. Back it up yourself.

## Identifier types

| Type | Description |
|------|-------------|
| `PlayerIdentifier` | Uniquely identifies a player |
| `SessionIdentifier` | Uniquely identifies a session |
| `GroupIdentifier` | Uniquely identifies a group |
| `ModelIdentifier` | Uniquely identifies a model |

These are used internally and in [custom data models](https://uproot.science/advanced/models.md).

## StorageBunch

A collection of storage objects (players, groups) with query methods.

| Method | Description |
|--------|-------------|
| `len(bunch)` | Number of items |
| `bunch[i]` | Get by index |
| `item in bunch` | Membership test |
| `bunch.filter(*.comparisons)`{ .text-nowrap } | Filter by field values using `_` |
| `bunch.find_one(**kwargs)` | Find exactly one match |
| `bunch.assign(key, values)` | Set a field on all items |
| `bunch.each(*keys)` | Extract fields from all items |
| `bunch.apply(fn)` | Apply a function to all items |

### The `_` field referent

`_` is a `FieldReferent`: a placeholder that stands for each item in the collection. It builds lazy comparison objects that are evaluated per item during `filter()`.

```python
# _ is auto-imported via `from uproot.smithereens import *`
cooperators = group.players.filter(_.cooperate == True)
high_earners = session.players.filter(_.payoff > 10)

# Multiple conditions (all must match)
eligible = session.players.filter(_.present == True, _.age >= 18)

# Chained attribute access
same_round = session.players.filter(_.group.round == 3)
```

Supported operators: `==`, `!=`, `>`, `>=`, `<`, `<=`. Bare `_.field` (without an operator) tests for truthiness.

See [Filtering with `_`](https://uproot.science/multiplayer/groups.md#filtering-with-_) for more examples.
