Skip to content

manyrows/manyrows-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

manyrows

Official Python SDK for ManyRows. Mirrors the surface of manyrows-go and @manyrows/manyrows-node.

The examples below assume a self-hosted deployment at https://manyrows.example.com. Swap in whatever host your install runs on (http://localhost:3000 for local development, your own domain in production).

Install

This SDK is not yet on PyPI. Install directly from GitHub:

pip install git+https://github.com/manyrows/manyrows-python.git

To pin to a specific commit or tag:

pip install git+https://github.com/manyrows/manyrows-python.git@<commit-or-tag>

If you need to decrypt secrets (see below), pull the optional extra:

pip install 'manyrows[secrets] @ git+https://github.com/manyrows/manyrows-python.git'

Requires Python 3.9+. Sync and async clients are both included; both use httpx under the hood.

Client

The client wraps the ManyRows Server API. Requires an API key.

from manyrows import Client

client = Client(
    base_url="https://manyrows.example.com",
    workspace_slug="your-workspace",
    app_id="your-app-id",
    api_key="mr_a1b2c3d4_yourSecretKey",
)

For async code:

from manyrows import AsyncClient

async with AsyncClient(
    base_url="https://manyrows.example.com",
    workspace_slug="your-workspace",
    app_id="your-app-id",
    api_key="mr_...",
) as client:
    user = await client.get_user("u_123")

Delivery (config + feature flags)

delivery = client.get_delivery()
# delivery.config.public, delivery.config.private, delivery.config.secrets
# delivery.flags.client, delivery.flags.server

Decrypt secrets

Secret values are returned as encrypted envelopes. Decrypt them with your workspace private key (downloaded once when you generated the workspace key in your install's admin UI):

import json, os
from manyrows import decrypt_secret

private_key_jwk = json.loads(os.environ["MANYROWS_WORKSPACE_PRIVATE_KEY"])
delivery = client.get_delivery()

for sec in delivery.config.secrets:
    if not sec.is_set or not sec.envelope:
        continue
    plaintext = decrypt_secret(sec.envelope, private_key_jwk)
    # plaintext is bytes of the JSON-encoded value. For a string secret
    # you'll get b'"hello"' (with quotes) — json.loads to recover.
    value = json.loads(plaintext.decode("utf-8"))

The private key never leaves your server — secrets are decrypted in process. Requires the optional cryptography dep — pull the [secrets] extra when installing (see the Install section above).

See src/manyrows/secrets.py for the full algorithm (ECDH P-256 + HKDF-SHA256 + AES-256-GCM).

Check permission

allowed = client.has_permission(user_id, "posts:edit")

# Or get the full result:
result = client.check_permission(user_id, "posts:edit")
# result.allowed, result.permission, result.account_id

User lookup

# By ID
user = client.get_user(user_id)
# user.user.email, user.roles, user.permissions, user.fields

# By email
user = client.get_user_by_email("user@example.com")

Members

result = client.list_members(page=0, page_size=50)
# result.members, result.total, result.page, result.page_size

# Filter by email substring:
result = client.list_members(page=0, page_size=50, email="alice")

# Or the convenience alias:
result = client.list_members_by_email("alice")

User fields

fields = client.list_user_fields()
# fields[0].key, fields[0].value_type, fields[0].label

Error handling

Non-2xx responses raise ManyRowsError:

from manyrows import ManyRowsError

try:
    client.get_user("bogus")
except ManyRowsError as err:
    print(err.status, err.body)

Auth helpers

Verify the user's JWT locally against your install's JWKS. Fetches ${base_url}/.well-known/jwks.json once on first verify, caches the parsed keys in-process, refetches on a kid mismatch. No per-request round trip to ManyRows. Use bearer_token to pull the JWT from the Authorization header and mr_at_cookie to fall back to the cookie that AppKit sets in cookie mode.

Built on PyJWT[crypto] — the de-facto Python JWT library.

verify_token

Returns the user ID (sub claim) on success, None for any verification failure (expired, malformed, wrong signature, missing sub, JWKS unreachable). Doesn't raise on auth-decision-equivalent conditions — fail-closed is the caller's job; None is the "not authenticated" signal.

from manyrows import bearer_token, mr_at_cookie, verify_token

# Try Authorization header first, then mr_at cookie (cookie-mode AppKit).
token = (
    bearer_token(request.headers.get("Authorization"))
    or mr_at_cookie(request.headers.get("Cookie"))
)
if not token:
    return Response("Unauthorized", status=401)

user_id = verify_token(
    token,
    base_url="https://manyrows.example.com",
    workspace_slug="your-workspace",
    app_id="your-app-id",
)
if user_id is None:
    return Response("Unauthorized", status=401)

Async — verify_token_async

from manyrows import verify_token_async

user_id = await verify_token_async(
    token,
    base_url="https://manyrows.example.com",
    workspace_slug="your-workspace",
    app_id="your-app-id",
)

FastAPI

from typing import Annotated
from fastapi import Depends, FastAPI, Header, HTTPException

from manyrows import bearer_token, mr_at_cookie, verify_token_async

app = FastAPI()

async def manyrows_user_id(
    authorization: Annotated[str | None, Header()] = None,
    cookie: Annotated[str | None, Header()] = None,
) -> str:
    token = bearer_token(authorization) or mr_at_cookie(cookie)
    if not token:
        raise HTTPException(401)
    user_id = await verify_token_async(
        token,
        base_url="https://manyrows.example.com",
        workspace_slug="your-workspace",
        app_id="your-app-id",
    )
    if user_id is None:
        raise HTTPException(401)
    return user_id

@app.get("/api/profile")
async def profile(user_id: Annotated[str, Depends(manyrows_user_id)]):
    return {"user_id": user_id}

Flask

from functools import wraps
from flask import Flask, request, abort, g

from manyrows import bearer_token, mr_at_cookie, verify_token

app = Flask(__name__)

def manyrows_auth(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        token = (
            bearer_token(request.headers.get("Authorization"))
            or mr_at_cookie(request.headers.get("Cookie"))
        )
        if not token:
            abort(401)
        user_id = verify_token(
            token,
            base_url="https://manyrows.example.com",
            workspace_slug="your-workspace",
            app_id="your-app-id",
        )
        if user_id is None:
            abort(401)
        g.manyrows_user_id = user_id
        return f(*args, **kwargs)
    return wrapper

@app.route("/api/profile")
@manyrows_auth
def profile():
    return {"user_id": g.manyrows_user_id}

Custom HTTP client

Inject your own httpx.Client / httpx.AsyncClient for testing, request tracing, or custom timeout/transport configuration:

import httpx
from manyrows import Client

http = httpx.Client(timeout=30.0, headers={"X-Trace-Id": "abc"})
client = Client(
    base_url="https://manyrows.example.com",
    workspace_slug="your-workspace",
    app_id="your-app-id",
    api_key="mr_...",
    http_client=http,
)

When you pass your own client, you own its lifecycle — call http.close() (or use it as a context manager) yourself.

Webhook verification

ManyRows signs every outbound webhook delivery. Use verify_webhook on your receiver:

from manyrows import verify_webhook, WebhookError
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/webhooks/manyrows")
async def webhook(request: Request):
    body = await request.body()  # raw bytes — not request.json()
    try:
        verify_webhook(secret=secret, headers=request.headers, body=body)
    except WebhookError as err:
        raise HTTPException(401, detail=err.code)
    # body is verified — json.loads(body) and process
    return {"ok": True}

verify_webhook checks both the HMAC-SHA256 signature (over <timestamp>.<body>) and that X-Webhook-Timestamp is within ±5 minutes of now. Pass tolerance=timedelta(...) to widen or tighten.

Read the body as raw bytes before verifying — re-serializing parsed JSON changes whitespace and breaks the check.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages