Metadata-Version: 2.4
Name: faceplant
Version: 0.9.6
Summary: Build a faceplant platform service in ~30 lines: identity, heartbeat, dashboards, runtime plugins, and MCP — all under the hood.
Project-URL: Homepage, https://github.com/faceplant-ai/faceplant-sdk
Project-URL: Documentation, https://github.com/faceplant-ai/faceplant-sdk/tree/main/docs
Project-URL: Changelog, https://github.com/faceplant-ai/faceplant-sdk/blob/main/CHANGELOG.md
Author: faceplant-ai
License-Expression: MIT
License-File: LICENSE
Keywords: faceplant,fastapi,mcp,microservice,plugins
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.27
Requires-Dist: pyjwt<3,>=2.8
Requires-Dist: python-multipart<1,>=0.0.9
Provides-Extra: mcp
Requires-Dist: mcp>=1.26; extra == 'mcp'
Provides-Extra: serve
Requires-Dist: uvicorn[standard]>=0.30; extra == 'serve'
Requires-Dist: websockets>=13; extra == 'serve'
Description-Content-Type: text/markdown

# faceplant

Build a [faceplant platform](https://github.com/faceplant-ai) service in ~30 lines. Identity, the
30-second broker heartbeat, dashboards, the runtime plugin host, the daily
garbage collector, and MCP tool serving are all under the hood — you write
domain code.

```bash
pip install faceplant
```

## Quickstart — a whole service

```python
from faceplant import Service, Table, Column

svc = Service(
    "billing",
    description="Track usage bills.",
    icon="receipt",
    category="tools",
)

@svc.get("/data")
async def bills(user):                       # user.email, user.slug, user.dir
    rows = load_bills(user.dir)              # per-user private storage, created for you
    return Table(
        title="Bills",
        columns=[Column(key="month", header="Month"),
                 Column(key="amount", header="Amount")],
        rows=rows,
    )

@svc.responder("billing.total")              # a broker API other services can call
async def total(data):
    return {"total": sum_bills()}

app = svc.app                                # uvicorn src.main:app
```

That's the entire service. `Service(...)` already gave you:

- a **gateway card** heartbeated every 30s (stop the pod → the card disappears);
- `/health`, `/manifest`, `/_plugins` and CORS;
- **identity**: `user` is injected from the platform JWT (401 when absent), with
  `user.dir` for per-user storage and `user.tags` / `user.services`;
- the **runtime plugin host**: other services can extend this one with signed
  wheels, with zero code here (`svc.registry` if you want to add hook points);
- the **daily GC** that reclaims a user's data ~10 days after they're gone.

## More surfaces (each one decorator)

```python
@svc.consumer("CHAT", filter_keys=["chat.>"])   # subscribe to broker events
async def on_chat(data): ...

@svc.mcp_tool()                                  # a tool for coding agents  (pip install faceplant[mcp])
async def get_bill(month: str) -> str: ...

svc.announce_wheel(target="faceplant-users",     # ship an extension wheel AT another service
                   wheel="plugin", entrypoint="billing_gate.plugin:register")

@svc.manifest                                    # compute the dashboard per request
def dash(user): return Stack(children=[...])
```

Escape hatches, always available: `svc.app` is a real FastAPI app (add
WebSockets, SSE, middleware); `svc.broker.request(key, data)` / `.publish(...)`;
`await svc.provision(fn)` for blocking work off the DNS-critical event loop;
`@svc.background` for startup loops; `@svc.on_purge` / `@svc.gc_verdict` to
customize GC.

## Testing is safe by default

`Service` only starts its heartbeat when a broker is actually configured
(`BROKER_URL` set). A bare `pytest` or `with TestClient(app)` runs **offline** —
no accidental registration against a live gateway. To test online behavior, use
the in-process stub:

```python
from fastapi.testclient import TestClient
from faceplant.testing import StubBroker, dev_jwt

def test_bills():
    with TestClient(svc.app) as client:                    # offline: no heartbeat
        r = client.get("/data", headers={"authorization": f"Bearer {dev_jwt('a@b.co')}"})
        assert r.status_code == 200

async def test_registration():
    stub = StubBroker(); stub.install(svc)
    await stub.beat(svc)
    assert "billing.total" in stub.responders
```

## Deployment is boilerplate-free too

Your repo needs no `Dockerfile` and no `k8s/values-prod.yaml` — the SDK
generates them. A deployable service is just `src/main.py`, `pyproject.toml`,
and a ~10-line deploy workflow that calls the SDK's reusable one:

```yaml
jobs:
  deploy:
    uses: faceplant-ai/faceplant-sdk/.github/workflows/deploy-service.yml@v1
    with: { service: billing, kind: stateless }
    secrets: inherit
```

Or generate the files to check in: `faceplant init --name billing`. See
[docs/deployment.md](docs/deployment.md).

## Docs

One page per concept in [`docs/`](docs/): [identity](docs/identity.md),
[widgets](docs/widgets.md), [plugin-host](docs/plugin-host.md),
[providers](docs/providers.md),
[consumers & responders](docs/consumers-and-responders.md), [mcp](docs/mcp.md),
[gc](docs/gc.md), [escape hatches](docs/escape-hatches.md),
[testing](docs/testing.md), and
[migrating from the template](docs/migration-from-template.md).

## License

MIT.
