Async Python client library for the Amazing Marvin 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.
pip install amazing-marvinRequires Python 3.12+ and aiohttp>=3.9.
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:
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)The library is intentionally framework-agnostic. Common embeddings:
- MCP servers — wrap each
MarvinClientmethod 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
aiohttpsession via lifespan. - Sync bridges — Notion, Todoist, Linear, calendar providers.
- Scheduled scripts — cron, GitHub Actions, weekly reviews, exports.
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.
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:
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.
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).
Marvin uses integer minutes east of UTC for timezone offsets. For example:
tz_offset=600— AEST (UTC+10)tz_offset=0— UTCtz_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.
MIT — see LICENSE.