---
sidebar_position: 13
---

# Python reference: `wordplus-realtime`

The package needs Python 3.9 or later, with PyNaCl. It takes the same options as the Node and PHP packages, and answers the browser the same way.

## `Realtime( site, secret, local_key, **options )`

`Realtime.from_environment( **options )` is the same, with `site`, `secret` and `local_key` taken from `WORDPLUS_SITE`, `WORDPLUS_SECRET` and `WORDPLUS_LOCAL_KEY`.

| Option | |
|---|---|
| `site`, `secret` | your app's key and secret |
| `local_key` | 64 hex characters of your own ([encryption](encryption.md#your-local-key)) |
| `profile` | `lambda user: {"name": …, "avatar": …, "url": …}` or `None`: the user's directory entry, and by default their presence info and call info |
| `has_keys` | `lambda user: False` cuts a user off every key |
| `generations` | a `GenerationStore`, for `rotate_channel()` and `rotate_user()` |
| `seal` | `False` sends everything plain |
| `base` | where your routes are, `/api/realtime` by default, for `config()` |
| `headers` | headers the browser sends to your routes, such as a CSRF token, for `config()` |
| `worker`, `rooms` | where your app serves the two files, for `config()` |
| `token_ttl`, `call_token_ttl` | seconds: 6 hours by default; a channels token lasts at most a day, a calls token at most 6 hours |
| `server`, `api` | WordPlus Cloud by default |
| `http` | `(method, url, headers, body) -> (status, body)`, your own client for publishing |

A user is the id your app gives, an integer or a string, or `None` for nobody. In this version, calls and the directory take integer ids only.

## Owners

| Method | |
|---|---|
| `channel(prefix, lambda user, channel: answer)` | private and presence channels, by the start of their names; answers `False`, `True`, or `{"id": …, "info": …}` ([channels](channels.md), [presence](presence.md)) |
| `calls(prefix, lambda user, call, with_user: answer)` | calls and rooms; answers `False`, `True`, or `{"info", "publish", "subscribe", "admin", "hidden"}` ([calls](calls.md)) |
| `kind(kind, lambda rest, user, label: bool, newest=None)` | keys of your own kind ([encryption](encryption.md#your-own-sealed-values)) |

The longest prefix that fits decides. The callbacks are plain functions, not `async`. Each method returns the instance, so calls chain.

## Routes

| Method | |
|---|---|
| `answer_request(method, content_type, action, body, user)` | → `(status, body)`: one route, `auth`, `calls` or `keys`, for a request's raw body |
| `handle(action, body, user)` | → `(status, body)`, for a body already decoded |
| `wordplus_realtime.django.urls(realtime, user=…)` | the routes in Django ([Django](django.md)) |
| `wordplus_realtime.fastapi.router(realtime, user)` | the routes in FastAPI and Starlette ([FastAPI](fastapi.md)) |

The routes take JSON POSTs only, and answer anything else with `415`. A refusal is part of the answer (`denied`), not an HTTP error.

| Method | What it answers |
|---|---|
| `authorize(channels, user)` | `{"token", "exp", "keys", "denied"}` |
| `authorize_call(request, user)` | `{"token", "exp", "key"}` or `{"denied": …}` |
| `keys_answer(labels, user)` | `{"keys", "identity"?}` |
| `identity(user)` | `{"token", "exp"}` or `None` |
| `token(channels, user=None, info=None, ttl=None)` | a channels token, signed as the auth route signs |
| `config(user=None, headers=None)` | the browser's settings for `connect()`, with a signed-in user's keys and identity |
| `may_join(channel, user)`, `may_have(label, user)` | what the routes decide, for your own checks |

## Publishing

| Method | |
|---|---|
| `publish(channels, event, data=None, *, except_tab=None, seal=True)` | one channel, or up to 100; data up to 10 KB of JSON ([publishing](publishing.md)) |
| `await apublish(…)` | the same from async code, on a worker thread |
| `publish_batch([{"channels", "event", "data", "except", "seal"}, …])` | 10 events a request |
| `channel_info(channel)` | `{"subscriptions", "members"?}` |
| `rotate_channel(channel)`, `rotate_user(user)` | moves a key on; needs `generations` |

They raise `RealtimeError` when WordPlus Cloud refuses: `e.code` holds the [code](errors.md#publishing), and `e.status` the HTTP status.

## Keys

`realtime.keys` is the app's `Keys`: `seal(text, label)`, `seal_json(data, label)`, `open(value)`, `entry(label)`, `day_label()`, `channel_label(channel)`, `user_label(user)`.

## Generations

```python
class GenerationStore(Protocol):
    def get(self, name: str) -> Optional[int]: ...
    def set(self, name: str, generation: int) -> None: ...
```

Keep them where all your servers see them, and for good: a database row, or a cache that doesn't evict.

## Helpers

`channel_kind(channel)` gives `public`, `private` or `presence`. `valid_channel(channel)` says whether WordPlus Cloud takes the name. `label_of(value)` names the key a sealed value needs. All three come from `wordplus_realtime`.
