Metadata-Version: 2.4
Name: amazing-marvin
Version: 0.2.0
Summary: Async Python client for the Amazing Marvin API
Project-URL: Homepage, https://github.com/jjsmackay/py-amazing-marvin
Project-URL: Source, https://github.com/jjsmackay/py-amazing-marvin
Author: jjsmackay
License: MIT
License-File: LICENSE
Keywords: amazing-marvin,async,home-assistant,productivity
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: aiohttp>=3.9
Provides-Extra: dev
Requires-Dist: aiohttp<3.14; extra == 'dev'
Requires-Dist: aioresponses>=0.7; extra == 'dev'
Requires-Dist: mypy>=1.9; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# amazing-marvin

[![Licence: MIT](https://img.shields.io/badge/Licence-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Async Python client library for the [Amazing Marvin](https://amazingmarvin.com) API.

Async-first, fully typed, no global state, no blocking I/O. Suitable for any Python application — CLIs, web services, MCP servers, Home Assistant integrations, sync bridges, scheduled scripts.

---

## Installation

```bash
pip install amazing-marvin
```

Requires Python 3.12+ and `aiohttp>=3.9`.

---

## Quick Start

```python
import asyncio
from amazing_marvin import MarvinClient

async def main():
    async with MarvinClient(api_token="your_token") as client:
        tasks = await client.get_today_items()
        for task in tasks:
            print(task.title, task.done)

asyncio.run(main())
```

When embedding inside a host that already manages an `aiohttp.ClientSession` (FastAPI, Home Assistant, an MCP server runtime), pass the existing session at construction so the client never owns or closes it:

```python
client = MarvinClient(
    api_token=token,
    full_access_token=full_token,   # optional
    tz_offset=600,                  # AEST
    throttle=True,
    session=existing_session,       # borrowed; not closed by client
)
tasks = await client.get_today_items()
task = await client.mark_done(item_id)
```

---

## Use Cases

The library is intentionally framework-agnostic. Common embeddings:

- **MCP servers** — wrap each `MarvinClient` method as a tool; the typed dataclasses serialise cleanly to JSON.
- **Home Assistant integrations** — use the borrowed-session pattern with `async_get_clientsession(hass)`.
- **CLIs** — pair with Typer or Click for terminal-driven task entry.
- **FastAPI / web dashboards** — share the app's `aiohttp` session via lifespan.
- **Sync bridges** — Notion, Todoist, Linear, calendar providers.
- **Scheduled scripts** — cron, GitHub Actions, weekly reviews, exports.

---

## Auth Setup

Amazing Marvin exposes two separate tokens, both found under **Profile > Integrations > API** in the Marvin app:

| Token | Constructor arg | Required for |
|---|---|---|
| API Token | `api_token` | Read and write operations (most methods) |
| Full Access Token | `full_access_token` | Raw document access, reminders, admin ops |

Methods that need a token you have not supplied will raise `MarvinAuthError` before making any HTTP request.

---

## CouchDB Queries (optional)

If you run a local CouchDB replica of Marvin's database (Marvin's `/pre?api`
page exposes your account's Cloudant credentials for pull replication), the
client can query it directly — fast, searchable reads with no Marvin API rate
limits:

```python
async with MarvinClient(
    couch_url="http://couchdb:5984",
    couch_db="marvin",
    couch_user="marvin_sync",
    couch_password="...",
) as client:
    await client.ensure_couch_indexes()          # idempotent, run once
    tasks = await client.find_tasks(title_contains="invoice", done=False)
    today = await client.tasks_by_day("2026-07-07")
    overdue = await client.tasks_due_by("2026-07-07")
```

Helpers: `find_tasks`, `tasks_by_day`, `tasks_due_by`, `planner_items`,
`recurring_generators`, `categories_from_couch`, plus raw `couch_find` for
custom Mango selectors. Couch methods raise `MarvinCouchError` before any HTTP
call when couch is unconfigured — HTTP-only users are unaffected.

`find_tasks(title_contains=...)` is a substring **scan**, not an indexed
lookup: CouchDB narrows on `db` via the index, then regex-filters the
remaining Task documents. It's bounded by `limit` (default 25) and fine for
interactive search, but don't rely on it for large unbounded queries.

**The replica is eventually consistent** (replication lag of seconds). Treat
it as read-only display state; never seed a write from it — fetch
authoritative state with `get_doc` instead.

---

## Rate Limits

The Amazing Marvin API enforces:

- **Burst**: 1 request per 3 seconds
- **Daily cap**: 1440 requests per calendar day (resets at midnight in your local timezone)

Pass `throttle=True` to have the client enforce these limits automatically using an internal asyncio lock. When the daily cap is hit, `MarvinRateLimitError` is raised with `daily_cap_exceeded=True`. When the server itself returns 429, `MarvinRateLimitError` is raised with the `retry_after` value from the response header (if present).

---

## Timezone Handling

Marvin uses **integer minutes east of UTC** for timezone offsets. For example:

- `tz_offset=600` — AEST (UTC+10)
- `tz_offset=0` — UTC
- `tz_offset=-300` — EST (UTC-5)

Pass `tz_offset` at client construction for a default, or override it per-call on methods that accept `tz_offset=`. The offset affects which calendar day is considered "today" for scheduling and daily cap resets.

---

## Licence

MIT — see [LICENSE](LICENSE).
