Metadata-Version: 2.5
Name: docket-client
Version: 0.3.2
Summary: Typed Python client for the docket scheduling API
Project-URL: Homepage, https://github.com/AllenNeuralDynamics/docket
Project-URL: Repository, https://github.com/AllenNeuralDynamics/docket
Project-URL: Bug Tracker, https://github.com/AllenNeuralDynamics/docket/issues
Project-URL: Changelog, https://github.com/AllenNeuralDynamics/docket/releases
Author: Allen Institute for Neural Dynamics
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<0.29.0,>=0.23.1
Provides-Extra: auth
Requires-Dist: msal<2,>=1.31; extra == 'auth'
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.30; extra == 'mcp'
Requires-Dist: msal<2,>=1.31; extra == 'mcp'
Requires-Dist: tzdata>=2024.1; (sys_platform == 'win32') and extra == 'mcp'
Requires-Dist: tzlocal<6,>=5; extra == 'mcp'
Description-Content-Type: text/markdown

# docket

A typed Python client for the **docket** scheduling API.

## Installation

```bash
pip install docket-client
# the unreleased version, from the repository:
pip install "docket-client @ git+https://github.com/AllenNeuralDynamics/docket#subdirectory=client"
# or, from the repo root during development:
pip install ./client
```

## Usage

```python
from docket import Client

client = Client(base_url="http://localhost:8001")
```

For authenticated endpoints use `AuthenticatedClient`:

```python
from docket import AuthenticatedClient

client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
```

Call any endpoint:

```python
from docket.api.scheduled_acquisitions import list_active_acquisitions

sessions = list_active_acquisitions.sync(
    client=client,
    instrument_id="BEH.B-Box1",
    before_minutes=15,
    effective_status=["scheduled", "ongoing"],
)
```

Every endpoint module exposes four variants:

| Function | Behaviour |
| --- | --- |
| `sync` | Blocking — returns the parsed response body or `None` |
| `sync_detailed` | Blocking — always returns a `Response` object with status code, headers, and `parsed` |
| `asyncio` | Async equivalent of `sync` |
| `asyncio_detailed` | Async equivalent of `sync_detailed` |

See [examples/downstream_usage.py](examples/downstream_usage.py) for a
full end-to-end walkthrough.

## MCP server (`docket-mcp`)

`docket-mcp` lets an AI assistant (Claude Code, Claude Desktop, any MCP client) look at the
schedule and change it, through the same API as everyone else. It runs **locally over stdio**:
the client starts it on your machine, so it reaches Docket over your own network or VPN and
nothing new is hosted. It is the `docket_mcp` package here, next to `docket_auth`.

### Install

```bash
pip install "docket-client[mcp]"
# or as an isolated tool (puts `docket-mcp` on your PATH):
uv tool install "docket-client[mcp]"
# from a checkout:
uv tool install "./client[mcp]"
```

### Add it to your assistant

Claude Code:

```bash
claude mcp add docket -e DOCKET_API_URL=http://localhost:8001 -e DOCKET_TIME_ZONE=America/Los_Angeles -- docket-mcp
# with an API token (see "Sign-in"):
claude mcp add docket -e DOCKET_API_URL=https://docket.example.org -e DOCKET_API_TOKEN=dkt_... -- docket-mcp
# straight from a checkout, without installing:
claude mcp add docket -e DOCKET_API_URL=http://localhost:8001 -- uv run --project ./client --extra mcp docket-mcp
```

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "docket": {
      "command": "docket-mcp",
      "env": {
        "DOCKET_API_URL": "http://localhost:8001",
        "DOCKET_TIME_ZONE": "America/Los_Angeles",
        "DOCKET_API_TOKEN": "dkt_..."
      }
    }
  }
}
```

If `docket-mcp` is not on the app's `PATH`, give its full path as `command`.

### Configuration

All through environment variables:

| Variable | Meaning |
| --- | --- |
| `DOCKET_API_URL` | The API's address. Default `http://localhost:8001`. A frontend origin works too (`https://docket.example.org` or `…/api`), with its URL prefix if it has one (`https://host/docket`). |
| `DOCKET_TIME_ZONE` | IANA zone for the local times you give and get back. Default: this machine's zone. A tool's `time_zone` argument overrides it. |
| `DOCKET_API_TOKEN` | A Docket API token (`dkt_…`). Checked **first**; works in Tokens and Entra mode. |
| `DOCKET_AUTH_CLIENT_ID`, `DOCKET_AUTH_SCOPE`, `DOCKET_AUTH_TENANT_ID` (or `DOCKET_AUTH_AUTHORITY`) | Entra sign-in when there is no API token: the SPA's public client id, `api://…/access_as_user`, and the tenant (or another OIDC authority, such as the mock provider). |
| `DOCKET_AUTH_CLIENT_SECRET` | With the three above: sign in as an application (client credentials) instead of as a person. |
| `DOCKET_AUTH_CACHE_PATH` | Where the person's token cache lives. Default `~/.docket/mcp-token-cache.json` (holds refresh tokens: protect it). |

### Sign-in

Reads are anonymous. The server asks `/api/v1/me` once for `auth_mode`, and only **writes** carry a token.

- `auth_mode: disabled` (the default Docket): nothing to set up and you are never asked to sign in.
- `auth_mode: tokens`: set `DOCKET_API_TOKEN`. Ask an admin for a token from **Settings > Access**. Without one, write tools say so.
- `auth_mode: entra`: set `DOCKET_API_TOKEN` too (simplest), or sign in as yourself. The first write prints a URL and a code to stderr **and** returns them as the tool's error text (some clients hide stderr); open the URL, enter the code, then run the tool again. The token is cached afterwards. Set `DOCKET_AUTH_CLIENT_SECRET` for an unattended application.

Tokens are never logged or returned by a tool. `whoami` shows your auth mode, caller, roles (and where each came from) and grants: as yourself when a credential is available without interaction (an API token, client credentials or a cached sign-in), otherwise as anonymous (`signed_in: false`). It never starts a sign-in.

The device-code token cache holds refresh tokens. It is created owner-only (`chmod 600`) on Linux and macOS; on Windows `chmod` is a no-op and the file relies on your user profile's ACL, so keep it there (the default is `~/.docket/`).

### Tools

| Read | |
| --- | --- |
| `list_rigs`, `list_projects` | what you can book, with optional `search` |
| `lookup_subject`, `lookup_person` | check an id or username exists |
| `get_schedule` | sessions between two dates (rig, subject, person, archived filters) |
| `get_acquisition` | one session in full: people, notes, resolved settings, runs |
| `find_free_slots` | gaps on a rig long enough for a session, within daily bounds |
| `list_series`, `get_series` | recurring series and their rules |
| `list_templates`, `list_settings`, `list_runs`, `whoami` | |

| Write (all take `dry_run`, default **true**) | |
| --- | --- |
| `schedule_acquisition` | book one session (optionally from a template) |
| `move_acquisition` | change start, length or rig |
| `archive_acquisition` | archive a session (nothing is ever hard-deleted) |
| `create_series` | a finite daily/weekly series |
| `edit_series_from` | change a series from a date on (the old one ends, a new one starts) |
| `archive_series` | archive a series, keeping or archiving its upcoming sessions |
| `mark_outcome` | record succeeded / failed / cancelled by hand |

Not in v1: pushing settings, syncing vocabulary, managing templates and roles.

### Dry runs

Every write tool previews by default and changes nothing: it returns what would happen,
including overlap warnings, skipped (daylight-saving) dates and which sessions a series edit
would replace or keep. The tool descriptions tell the assistant to show you the preview and
repeat the call with `dry_run=false` only once you agree. Series and moves use the server's own
preview endpoints; for a single session the API has none, so the preview checks the schedule
itself with the server's overlap rule (same rig only; the same subject on another rig is listed
as `advisory`) and the server checks again when it creates it.

Each preview returns what the confirmation needs, under `confirm_with`: the `expected_version`
of what it looked at (moves, archives, outcomes, series edits and archives) and an
`idempotency_key` (new sessions, series and series edits). Repeat the call with
`dry_run=false` and those values. If the session or series changed in between, the call is
refused with "changed since your preview; preview again" and nothing is written. A retry
after a timeout with the same key cannot book or create twice.

### Times

Give local wall-clock times such as `2026-03-08T09:30`, never offsets. Results show the local
time (with its offset) and UTC. A time that does not exist (a spring-forward gap) is refused;
a repeated one (fall back) takes the first occurrence and says so.

### Example prompts

- "What is on rig `BEH.A` next week, and which afternoons are free for a 90-minute session?"
- "Book subject 812345 on `BEH.A` under project `Vision` tomorrow at 10:00 for an hour, with me as experimenter." (it previews first)
- "Move session 412 to Thursday at 14:00."
- "Set up a weekly series, Mondays and Thursdays at 09:00 for eight sessions, for subject 812345."
- "From 2026-11-02, shift that series to 10:30." / "Cancel the rest of series 17 but keep what already ran."
- "Did anything fail on the FIP rigs this week?"

### Developing

```bash
uv sync --group dev
uv run pytest client/tests                       # unit tests + the stdio smoke test
uv run pytest client/tests -m integration        # schedule -> move -> archive on a real server
```

The integration test builds the server image and starts its own Compose project
(`docket-mcp-it`, API on port 8020, static vocabulary, nothing from the institute) and removes
it afterwards; set `DOCKET_IT_URL` to use a server that is already running instead.

## Building / publishing

```bash
# from the repo root
uv build --package docket-client   # → dist/docket_client-*.whl
```
