Metadata-Version: 2.4
Name: arr-py-client
Version: 0.6.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`) — 56 tools exposing the client as
  LLM-callable operations, with uniform `_meta.action_hints` so chains
  self-suggest.
- **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.
```

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.

## 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).
