Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
strategy:
fail-fast: false
matrix:
router: [ts, go]
router: [ts, go, python]
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -84,6 +84,11 @@ jobs:
uv venv --python 3.10 .venv
uv pip install --python .venv/bin/python schemathesis pytest httpx jsonschema

- name: Install the Python router into the harness
if: matrix.router == 'python'
working-directory: conformance
run: uv pip install --python .venv/bin/python fastapi uvicorn -e ../routers/python

- name: Run conformance (${{ matrix.router }})
working-directory: conformance
env:
Expand Down
38 changes: 38 additions & 0 deletions conformance/servers/py_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Boot the Python router for conformance testing.

Default (mock) mode points at the in-memory mock via ``MONAD_API_BASE``; live
mode sets the same knobs to real Monad staging values. Every value falls back to
the mock fixture default, so nothing changes for the existing hermetic run.
"""

import os

import uvicorn
from fastapi import FastAPI

from monad_embed import EmbedConfig, Provision, embed_router

# A provisioned id may be intentionally empty (a live tenant with no store) →
# treat "" as "not provisioned" so the router falls back to a dev/null sink.
_store = os.environ.get("MONAD_STORE_ID", "out_store") or None
_source = os.environ.get("MONAD_SOURCE_ID", "in_source") or None
# Empty allow-list → expose the whole catalog (None), never the empty set.
_allow = [s.strip() for s in os.environ.get("MONAD_CATALOG_ALLOW", "aws-cloudtrail,okta-systemlog").split(",") if s.strip()] or None

app = FastAPI()
app.include_router(
embed_router(
EmbedConfig(
api_key=os.environ.get("MONAD_API_KEY", "conf-key"),
api_base=os.environ["MONAD_API_BASE"],
frame_origin=os.environ.get("MONAD_FRAME_ORIGIN", "https://app.monad.com/embed"),
get_customer_org_id=lambda req: os.environ.get("MONAD_ORG_ID", "org_conf"),
get_provisioned_components=lambda org: Provision(destination_output_id=_store, source_input_id=_source),
catalog_allow=_allow,
)
),
prefix="/embed",
)

if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=int(os.environ.get("PORT", "8791")), log_level="warning")
7 changes: 7 additions & 0 deletions routers/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv/
__pycache__/
*.pyc
.pytest_cache/
*.egg-info/
dist/
build/
104 changes: 104 additions & 0 deletions routers/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Monad Embed — Python router

A mountable `/embed` backend router for a Monad embed integration, as a FastAPI
`APIRouter`. Implements the [`/embed` route contract](../../packages/embed/openapi/embed.openapi.yaml).

```python
from fastapi import FastAPI
from monad_embed import EmbedConfig, Provision, embed_router

app = FastAPI()
app.include_router(
embed_router(EmbedConfig(
api_key=os.environ["MONAD_API_KEY"],
# your auth → the tenant's Monad team id (sync or async)
get_customer_org_id=lambda req: req.session["org_id"],
# server-side lookup of a tenant's pre-provisioned components
get_provisioned_components=lambda org: Provision(destination_output_id=STORES[org]),
)),
prefix="/embed",
)
```

Mounted with `prefix="/embed"`, the router serves `/embed/session`,
`/embed/pipelines/ingress`, etc. The browser holds only a session token; this
router holds the API key and is the seam to the Monad API.

## Mounting on common frameworks

`embed_router(cfg)` is a FastAPI `APIRouter` (ASGI), so it slots into any FastAPI
or Starlette app. Assume `router = embed_router(cfg)` below.

**FastAPI** — include it in your app

```python
app.include_router(router, prefix="/embed")
```

**FastAPI** — as an isolated sub-application (its own docs, middleware, lifespan)

```python
embed_app = FastAPI()
embed_app.include_router(router)
app.mount("/embed", embed_app)
```

**Starlette** — [starlette](https://www.starlette.io)

```python
from starlette.applications import Starlette
from starlette.routing import Mount

embed_app = FastAPI()
embed_app.include_router(router)

app = Starlette(routes=[
Mount("/embed", app=embed_app),
# ... your other routes
])
```

Serve it with any ASGI server:

```sh
uvicorn app:app
# or: gunicorn -k uvicorn.workers.UvicornWorker app:app
# or: hypercorn app:app
```

**Flask / Django (WSGI)** — the router is ASGI, so either run it as its own
service, or host it inside your WSGI app with an ASGI bridge such as
[`a2wsgi`](https://github.com/abersheeran/a2wsgi):

```python
from a2wsgi import ASGIMiddleware
from werkzeug.middleware.dispatcher import DispatcherMiddleware

embed_app = FastAPI()
embed_app.include_router(router)

# /embed/* → the ASGI embed app; everything else → Flask
flask_app.wsgi_app = DispatcherMiddleware(
flask_app.wsgi_app,
{"/embed": ASGIMiddleware(embed_app)},
)
```

## Config

| Field | Purpose |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `api_key` | Long-lived Monad API key (server-side only). |
| `api_base` | Monad API base. Optional — defaults to `https://app.monad.com/api` (production). |
| `frame_origin` | Iframe origin returned by `GET /embed/config`. Optional — defaults to production. |
| `get_customer_org_id` | `Request → org id`, sync or async. Return `""` / raise to reject (→ 401). |
| `get_provisioned_components` | `org → Provision(destination_output_id, source_input_id)`. Omit → ingress uses dev/null, egress unavailable. |
| `catalog_allow` | Restrict the catalog to these connector type ids. Omit → all. |

## Develop

```sh
uv venv --python 3.10 .venv
uv pip install --python .venv/bin/python "fastapi>=0.110" "httpx>=0.27" "pytest>=8"
.venv/bin/python -m pytest -q
```
34 changes: 34 additions & 0 deletions routers/python/monad_embed/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""monad-embed — a mountable ``/embed`` backend router (FastAPI).

Implements the ``/embed`` route contract (``packages/embed/openapi/embed.openapi.yaml``)
for a Monad embed integration.

from fastapi import FastAPI
from monad_embed import EmbedConfig, Provision, embed_router

app = FastAPI()
app.include_router(
embed_router(EmbedConfig(
api_key=os.environ["MONAD_API_KEY"],
api_base="https://app.monad.com/api",
frame_origin="https://app.monad.com/embed",
get_customer_org_id=lambda req: req.session["org_id"], # your auth → Monad team
get_provisioned_components=lambda org: Provision(destination_output_id=STORES[org]),
)),
prefix="/embed",
)
"""

from .client import MonadClient
from .config import EmbedConfig, Provision
from .errors import EmbedError, MonadError
from .router import embed_router

__all__ = [
"EmbedConfig",
"Provision",
"MonadClient",
"EmbedError",
"MonadError",
"embed_router",
]
Loading
Loading