Metadata-Version: 2.4
Name: arr-py-client
Version: 0.9.0
Summary: Typed sync + async Python client for Radarr v3 and Sonarr v3 APIs
Project-URL: Homepage, https://github.com/allada-homelab/arr-py-client
Project-URL: Source, https://github.com/allada-homelab/arr-py-client
Project-URL: Issues, https://github.com/allada-homelab/arr-py-client/issues
Author-email: David Allada <davidanilallada@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: <3.15,>=3.11
Requires-Dist: anyio>=4.13.0
Requires-Dist: httpx<1.0,>=0.28
Requires-Dist: pydantic-settings<3.0,>=2.2
Requires-Dist: pydantic<3.0,>=2.13.3
Provides-Extra: api
Provides-Extra: config
Requires-Dist: pyyaml<7.0,>=6.0; extra == 'config'
Provides-Extra: mcp
Requires-Dist: mcp<2.0,>=1.27; extra == 'mcp'
Provides-Extra: webhooks
Requires-Dist: fastapi<1.0,>=0.110; extra == 'webhooks'
Description-Content-Type: text/markdown

# arr-py-client

Typed Python client + MCP server + declarative config sync + workflow
primitives for [Radarr](https://radarr.video), [Sonarr](https://sonarr.tv),
and [Prowlarr](https://prowlarr.com).

[![CI](https://github.com/allada-homelab/arr-py-client/actions/workflows/ci.yml/badge.svg)](https://github.com/allada-homelab/arr-py-client/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/arr-py-client.svg)](https://pypi.org/project/arr-py-client/)
[![Python](https://img.shields.io/pypi/pyversions/arr-py-client.svg)](https://pypi.org/project/arr-py-client/)
[![Coverage](https://img.shields.io/badge/coverage-82%25-brightgreen.svg)](https://github.com/allada-homelab/arr-py-client/actions/workflows/ci.yml)
[![License](https://img.shields.io/pypi/l/arr-py-client.svg)](LICENSE)

## What's in the box

- **Typed sync + async client** — pydantic v2 models, `httpx` transport,
  full Radarr v3 + Sonarr v3 + Prowlarr v1 endpoint coverage, ships
  `py.typed`.
- **MCP server** (`arr-py-mcp`) — 60+ tools exposing the client as
  LLM-callable operations, with uniform `_meta.action_hints` so chains
  self-suggest. Single-tenant via env vars or **multi-tenant** via a
  `ClientProvider` + OAuth 2.1 — see
  [`docs/guides/mcp-multi-tenant.md`](docs/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

```bash
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)

```python
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`):

```python
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)

```bash
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:

```bash
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`:

```python
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`](docs/guides/mcp-multi-tenant.md).

## Quickstart (config sync)

Put this in `config.yaml` (see [docs/examples/config-sync/](docs/examples/config-sync/)
for the full schema):

```yaml
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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](docs/architecture.md)
- Roadmap: [docs/roadmap.md](docs/roadmap.md)
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)

## Development

```bash
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):

```bash
just test-int
```

Regenerate clients from upstream specs:

```bash
just gen-radarr <radarr-tag>
just gen-sonarr <sonarr-tag>
```

## License

MIT. See [LICENSE](LICENSE).
