Metadata-Version: 2.4
Name: jmaple
Version: 0.1.0
Summary: A pluggable JMAP server framework built on FastAPI.
Project-URL: Homepage, https://github.com/doughepi/jmaple
Project-URL: Repository, https://github.com/doughepi/jmaple
Project-URL: Issues, https://github.com/doughepi/jmaple/issues
Project-URL: Changelog, https://github.com/doughepi/jmaple/releases
Author-email: doughepi <me@pipd.io>
License-Expression: MIT
License-File: LICENSE
Keywords: fastapi,jmap,rfc8620,rfc8621,rfc9610,sync
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Framework :: FastAPI
Classifier: Framework :: Pydantic :: 2
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Communications
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Typing :: Typed
Requires-Python: >=3.14
Requires-Dist: aiosqlite>=0.20
Requires-Dist: alembic>=1.13
Requires-Dist: argon2-cffi>=23.1
Requires-Dist: asyncpg>=0.29
Requires-Dist: fastapi>=0.115
Requires-Dist: greenlet>=3.1
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic-settings>=2.5
Requires-Dist: pydantic>=2.9
Requires-Dist: pyjwt[crypto]>=2.9
Requires-Dist: python-ulid>=2.7
Requires-Dist: sqlalchemy[asyncio]>=2.0.30
Requires-Dist: sse-starlette>=2.1
Requires-Dist: typer>=0.12
Requires-Dist: uvicorn[standard]>=0.32
Requires-Dist: websockets>=13.1
Description-Content-Type: text/markdown

# Jmaple

A pluggable [JMAP](https://jmap.io) server framework, built on FastAPI.

📖 **[Documentation](https://doughepi.github.io/jmaple/)** — tutorial, topic
guides, and API reference.

JMAP (RFC 8620) was designed as a generic JSON-over-HTTP sync protocol with a
clean separation between **core machinery** (sessions, request/response envelope,
method dispatch, references, state strings, push) and **data-type capabilities**
(Mail = RFC 8621, Contacts = RFC 9610, …). Jmaple takes that separation seriously
and treats every data type — including Mail — as a *plugin*.

- **Python:** 3.14+
- **Package manager:** [uv](https://docs.astral.sh/uv/)
- **Web framework:** FastAPI
- **ORM:** SQLAlchemy (async)
- **Migrations:** Alembic (dynamically discovers plugin migrations via Python entry points)
- **Operator interface:** a single `jmaple` CLI — there is no separate admin web UI.

## What's in the box

- Full JMAP Core (RFC 8620): session resource, method dispatch, result references,
  state strings, error handling, upload/download.
- A clean **plugin contract** so a third party can add a new capability
  (`urn:vendor:thing`) by registering schemas and method handlers, without
  touching the core.
- A **pluggable auth subsystem** (JWT, OIDC, opaque bearer) wired into a unified
  accounts/grants authorization layer stored in the database.
- A **pluggable persistence layer** built on async SQLAlchemy 2.0 + Alembic, with
  PostgreSQL as the default and SQLite for development.
- All three JMAP push transports: SSE (`/eventsource`), WebSocket (RFC 8887),
  and PushSubscription webhooks (RFC 8620 §7.2).
- A reference plugin (`urn:jmaple:notes`) demonstrating end-to-end use of every
  abstraction.

## Quickstart

You need [uv](https://docs.astral.sh/uv/) and [just](https://just.systems) on
your `$PATH`.

```bash
just install              # uv sync
cp .env.example .env      # tweak JMAPLE_DATABASE_URL, JMAPLE_SECRET_KEY, …
just migrate              # apply Alembic migrations
just serve                # uvicorn at http://localhost:8000

# In another terminal: issue the first admin token.
uv run jmaple token issue --user alice --admin
```

Then point a JMAP client at `http://localhost:8000/.well-known/jmap` with the
issued token in the `Authorization: Bearer <token>` header.

## Top-level recipes

| Recipe | Description |
| --- | --- |
| `just install` | `uv sync`. |
| `just serve` | Run uvicorn with `--reload`. |
| `just migrate` | `jmaple migrate upgrade head`. |
| `just revision "msg"` | `jmaple migrate revision --autogenerate -m "msg"`. |
| `just fmt` / `just lint` / `just type` / `just test` | Ruff format / Ruff lint / `ty check` / pytest. |
| `just check` | All four of the above. |
| `just package` | `uv build` — produces wheel + sdist in `dist/`. |

## The `jmaple` CLI

The CLI is the only operator interface. Run `uv run jmaple --help` for the full
tree. Top-level groups:

- `jmaple serve` — start the FastAPI server.
- `jmaple migrate {upgrade, downgrade, revision, history, current}` — Alembic.
- `jmaple user {list, show, create, promote}` — manage users.
- `jmaple account {list, create, rename, delete}` — manage accounts.
- `jmaple grant {list, add, remove}` — manage account-sharing grants.
- `jmaple token {issue, list, revoke}` — manage opaque bearer tokens.
- `jmaple subscription {list, revoke}` — inspect / revoke push subscriptions.
- `jmaple capability {list, settings get, settings set}` — inspect / configure
  registered capabilities.
- `jmaple version` — print the installed version.

## Public API

When writing a capability, import from these modules. Everything under
`jmaple.core.*`, `jmaple.auth.*`, and `jmaple.api.*` is internal and may change
without notice.

| Module | What's in it |
| --- | --- |
| `jmaple.capabilities` | `Capability`, `CrudCapability`, `BaseCapability`, `MethodHandler`, `DataType`, `make_capability`, `register_capability` |
| `jmaple.db` | `Base`, `JMAPObject`, `DataChange`, `UTCDateTime`, `new_id`, `utcnow`, `id_column`, `created_column` |
| `jmaple.schemas` | `JMAPGetArgs`, `JMAPGetResponse`, `JMAPSetArgs`, `JMAPSetResponse`, `JMAPQueryArgs`, `JMAPQueryResponse`, `JMAPChangesArgs`, `JMAPChangesResponse`, `JMAPCopyArgs`, `JMAPCopyResponse`, `JMAPQueryChangesArgs`, `JMAPSort` |
| `jmaple.types` | `Id`, `UTCDate`, `ResultReference`, `PatchObject` |
| `jmaple.context` | `MethodContext` |
| `jmaple.errors` | `MethodError`, `SetError`, all `ERR_*` constants |

A minimal capability looks like:

```python
# my_plugin/models.py
from jmaple.db import Base, JMAPObject
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

class Todo(Base, JMAPObject):
    __tablename__ = "todos"
    text: Mapped[str] = mapped_column(String(512))

# my_plugin/capability.py
from jmaple.capabilities import CrudCapability
from my_plugin.models import Todo
from my_plugin.schema import TodoSchema, TodoCreate

class TodosCapability(CrudCapability):
    urn = "urn:example:todos"
    data_type_name = "Todo"
    model = Todo
    schema_class = TodoSchema
    create_schema_class = TodoCreate

capability = TodosCapability.as_capability()
```

Register it programmatically in your ASGI app module:

```python
# my_plugin/app.py
from jmaple.app import create_app
from jmaple.capabilities import register_capability
from my_plugin import models as _models  # noqa: F401  # land Todo on Base.metadata
from my_plugin.capability import capability

register_capability(capability)
app = create_app()
```

Run with `uvicorn my_plugin.app:app`. For CLI ops, write a parallel
`my_plugin/cli.py` that registers the capability then re-exposes
`jmaple.cli:app`, and point `pyproject.toml`'s `jmaple` script at it.

## Environment

The server reads its config from `.env` in the working directory (via
`pydantic-settings`). Copy `.env.example` and adjust as needed:

- `JMAPLE_DATABASE_URL` — SQLAlchemy async URL (defaults to a local SQLite file).
- `JMAPLE_SECRET_KEY` — random string for state-vector / session signing.
- `JMAPLE_DEBUG` — `true` for verbose logs.

Operators of the published PyPI wheel set the same `JMAPLE_*` env at deploy time.
