Typed sync + async Python client for Radarr v3 and Sonarr v3 APIs
Project description
arr-py-client
Typed Python client + MCP server + declarative config sync + workflow primitives for Radarr, Sonarr, and Prowlarr.
What's in the box
- Typed sync + async client — pydantic v2 models,
httpxtransport, full Radarr v3 + Sonarr v3 + Prowlarr v1 endpoint coverage, shipspy.typed. - MCP server (
arr-py-mcp) — 60+ tools exposing the client as LLM-callable operations, with uniform_meta.action_hintsso chains self-suggest. Single-tenant via env vars or multi-tenant via aClientProvider+ OAuth 2.1 — seedocs/guides/mcp-multi-tenant.md. - Composed workflows —
config_sync(plan/apply against a YAML desired-state),queue.janitor(...)(policy-based cleanup with named bundles),library.backfill(...)(rate-limited missing-content search with.estimate()),releases.explain(...)(grading + plain-English.advice()). - Webhook receivers — parse typed events and dispatch via WSGI or FastAPI handlers.
- Testing utilities —
make_fake_radarr()/make_fake_sonarr()fakes +@replay(...)fixture decorator for record-on-miss / replay-on-hit. - Zero-dep CLI (
arr-py) — status + basic add only; workflows live in Python and MCP on purpose. - Python 3.11–3.14.
Install
pip install arr-py-client # core SDK
pip install arr-py-client[mcp] # + MCP server
pip install arr-py-client[config] # + YAML loader for config_sync
pip install arr-py-client[webhooks] # + FastAPI receiver helper
Quickstart (SDK)
from arr_py_client import RadarrClient
with RadarrClient(base_url="http://radarr:7878", api_key="YOUR_KEY") as client:
movies = client.movies.list()
for m in movies[:5]:
print(m.id, m.title, m.year)
Or via env / .env (RADARR_BASE_URL, RADARR_API_KEY):
from arr_py_client import RadarrClient
with RadarrClient() as client:
print(len(client.movies.list()))
Async mirrors the sync API via AsyncRadarrClient / AsyncSonarrClient.
Quickstart (MCP)
pip install arr-py-client[mcp]
export RADARR_BASE_URL=http://radarr:7878 RADARR_API_KEY=...
export SONARR_BASE_URL=http://sonarr:8989 SONARR_API_KEY=...
arr-py-mcp # stdio MCP server; register with Claude, Cursor, etc.
arr-py-mcp --transport http # streamable-http (MCP 2025-11 preferred remote transport)
Every list/get tool returns a projected envelope with
_meta.action_hints — the LLM client can read the suggested next tool
calls directly from the response.
Run via Docker
A prebuilt image is published to GHCR on every release:
docker run --rm -p 3000:3000 \
-e RADARR_BASE_URL=http://radarr:7878 -e RADARR_API_KEY=... \
-e SONARR_BASE_URL=http://sonarr:8989 -e SONARR_API_KEY=... \
ghcr.io/allada-homelab/arr-py-client:latest
Defaults to streamable-HTTP on 0.0.0.0:3000. Override the command for
other transports — e.g., docker run --rm -i ghcr.io/allada-homelab/arr-py-client:latest --transport stdio
for an stdio wrapper. Tags: latest, {version}, {major}.{minor},
{major}, and sha-<short> for immutable pinning.
Multi-tenant deployments
For embedding the MCP server in a larger app where each user has their
own Radarr / Sonarr / Prowlarr — including users with multiple
instances of the same brand — implement a ClientProvider:
from fastapi import FastAPI
from arr_py_client.mcp import (
build_server, CallbackTokenVerifier, InMemoryCachedProvider,
)
class MyProvider(InMemoryCachedProvider):
async def identity(self, ctx):
return ctx.principal.id
async def build_client(self, ctx, brand, instance_id, identity):
# look up encrypted creds in your DB and return AsyncRadarrClient(...)
...
mcp = build_server(
provider=MyProvider(),
token_verifier=CallbackTokenVerifier(verify=app_auth.to_principal),
auth=AuthSettings(issuer_url=..., resource_server_url=...),
)
app = FastAPI()
app.mount("/mcp", mcp.streamable_http_app())
Per-tool scope enforcement (mcp:arr:read vs mcp:arr:mutate),
RFC 6750 §3.1 403 + WWW-Authenticate on scope failure, and an
audit= callback come for free. Full guide:
docs/guides/mcp-multi-tenant.md.
Quickstart (config sync)
Put this in config.yaml (see docs/examples/config-sync/
for the full schema):
tags: [4k, anime, kids]
custom_formats:
- name: x265
specifications:
- name: x265
implementation: ReleaseTitleSpecification
required: true
fields: [{ name: value, value: "(h|x).?265" }]
quality_profiles:
- name: HD-Bluray
upgradeAllowed: true
cutoff: 7
formatItems:
- { name: x265, score: -10000 }
Apply it:
from arr_py_client import RadarrClient
from arr_py_client.config_sync import load, plan, apply
with RadarrClient() as client:
desired = load("config.yaml")
plan_ = plan(client, desired)
print(plan_.summary())
report = apply(client, plan_, dry_run=False)
Quickstart (queue janitor)
Named policy bundles for common opinions:
from arr_py_client import RadarrClient, POLICIES
with RadarrClient() as client:
report = client.queue.janitor(
policies=POLICIES.default, # or .conservative / .aggressive / .ratio_preserving
protected_trackers=("private-tracker.example",),
dry_run=False,
)
print(report.total_matches)
Quickstart (webhook receiver)
Parse-only:
from arr_py_client.webhooks import parse_event, OnGrab
event = parse_event(request.json())
if isinstance(event, OnGrab):
notify(f"Grabbed {event.movie.title if event.movie else '?'}")
With FastAPI:
from fastapi import FastAPI
from arr_py_client.webhooks import fastapi_router
app = FastAPI()
app.include_router(fastapi_router(on_event), prefix="/webhooks/arr")
Or zero-dep WSGI:
from wsgiref.simple_server import make_server
from arr_py_client.webhooks import wsgi_app
make_server("0.0.0.0", 9000, wsgi_app(on_event)).serve_forever() # noqa: S104
Comparison
| arr-py-client | pyarr | Recyclarr | |
|---|---|---|---|
| Pydantic v2 models | yes | no (dicts) | n/a |
| Async | yes | no | n/a |
| Radarr / Sonarr v3 coverage | yes | yes | partial (config only) |
| Prowlarr v1 coverage | yes | yes | yes |
| Lidarr / Readarr | planned | yes | yes |
| MCP server | yes (56 tools) | no | no |
| Declarative config sync | yes (YAML/JSON/TOML) | no | yes |
| Queue janitor / backfill / release explain | yes | no | no |
| Webhook receiver helper | yes | no | no |
Ships py.typed |
yes | no | n/a |
Documentation
- API reference: https://allada-homelab.github.io/arr-py-client/
- Architecture: docs/architecture.md
- Roadmap: docs/roadmap.md
- Contributing: CONTRIBUTING.md
Development
git clone https://github.com/allada-homelab/arr-py-client
cd arr-py-client
uv sync --all-extras --all-groups
just test
Integration tests (require Docker):
just test-int
Regenerate clients from upstream specs:
just gen-radarr <radarr-tag>
just gen-sonarr <sonarr-tag>
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arr_py_client-0.9.1.tar.gz.
File metadata
- Download URL: arr_py_client-0.9.1.tar.gz
- Upload date:
- Size: 300.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
404483c74345b065ca33c43710d09a1d301d4b683f6a3f5a31f0d2e8f0aa7eaa
|
|
| MD5 |
8d204fca4fb9967fe277b1f0f2d46ada
|
|
| BLAKE2b-256 |
53dfb2f68f67f1d478265c94e5c7f82899950351993a63245537cc7df469e0d2
|
Provenance
The following attestation bundles were made for arr_py_client-0.9.1.tar.gz:
Publisher:
release.yml on allada-homelab/arr-py-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arr_py_client-0.9.1.tar.gz -
Subject digest:
404483c74345b065ca33c43710d09a1d301d4b683f6a3f5a31f0d2e8f0aa7eaa - Sigstore transparency entry: 1358407997
- Sigstore integration time:
-
Permalink:
allada-homelab/arr-py-client@531b13cbee1ae0c2b8260313f78ad17f79c59e69 -
Branch / Tag:
refs/tags/v0.9.1 - Owner: https://github.com/allada-homelab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
release.yml@531b13cbee1ae0c2b8260313f78ad17f79c59e69 -
Trigger Event:
release
-
Statement type:
File details
Details for the file arr_py_client-0.9.1-py3-none-any.whl.
File metadata
- Download URL: arr_py_client-0.9.1-py3-none-any.whl
- Upload date:
- Size: 431.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0b8cd375872af92952a9f44bd541d8cb76cc69e11adecad908f6f7f009b9e3d
|
|
| MD5 |
cdcecab115400ef8b6c9f3b2570af307
|
|
| BLAKE2b-256 |
028fe5fd360c7cee9089fa124af9ebf1b818c977e0f341e64722da1ec1cb27bd
|
Provenance
The following attestation bundles were made for arr_py_client-0.9.1-py3-none-any.whl:
Publisher:
release.yml on allada-homelab/arr-py-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arr_py_client-0.9.1-py3-none-any.whl -
Subject digest:
c0b8cd375872af92952a9f44bd541d8cb76cc69e11adecad908f6f7f009b9e3d - Sigstore transparency entry: 1358408011
- Sigstore integration time:
-
Permalink:
allada-homelab/arr-py-client@531b13cbee1ae0c2b8260313f78ad17f79c59e69 -
Branch / Tag:
refs/tags/v0.9.1 - Owner: https://github.com/allada-homelab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
release.yml@531b13cbee1ae0c2b8260313f78ad17f79c59e69 -
Trigger Event:
release
-
Statement type: