# Python SDK

Use Enlitt from a Python backend such as FastAPI, Starlette, Django, or Flask.

The Python SDK, `enlitt` on PyPI, does what the Node.js SDK does on a Python
server:

* login handlers and a session middleware;
* standard calls through an OpenAI-shaped client;
* `with_premium_charge()` around any provider call;
* `usage()`;
* the same typed errors.

Every rule on the other pages applies unchanged. That includes
[login](/docs/authentication.md), [premium limits](/docs/premium-limits.md),
[idempotency](/docs/idempotency.md), and [errors](/docs/errors.md). This page shows
only the Python names.

## Install

The SDK needs Python 3.10 or later. It is async only, so every call is `await`ed.

```bash
pip install "enlitt[openai,starlette]"
# or
uv add "enlitt[openai,starlette]"
```

Each extra adds a dependency:

* `openai` adds the official `openai` package, for the OpenAI-shaped client.
* `starlette` adds the FastAPI and Starlette routes.

The core needs only `httpx`.

Then wire the project:

```bash
npx enlitt setup --name "My Python app"
```

The CLI needs Node.js. `setup` finds `pyproject.toml` or `requirements.txt`. It
then writes `ENLITT_CLIENT_ID` and `ENLITT_SECRET_KEY` to `.env` and records the
application in `enlitt.json`. Keep `.env` out of git; `npx enlitt doctor`
reports it when it is committed. Load `.env` with your usual tool, such as
`python-dotenv` or your process manager. The SDK reads `ENLITT_SECRET_KEY` from
the environment.

## Mount login and the session middleware (FastAPI)

```python
import os

from fastapi import FastAPI
from enlitt.asgi import EnlittSessionMiddleware
from enlitt.auth import CallbackConfig, StateRecord
from enlitt.starlette import enlitt_routes


class MemoryStateStore:
    """Development only. Use a shared store (Redis, your database) when you run more than one process."""

    def __init__(self) -> None:
        self._records: dict[str, StateRecord] = {}

    async def set(self, state: str, record: StateRecord, ttl_ms: int) -> None:
        self._records[state] = record

    async def get(self, state: str) -> StateRecord | None:
        return self._records.get(state)

    async def consume(self, state: str) -> StateRecord | None:
        return self._records.pop(state, None)


config = CallbackConfig(
    client_id=os.environ["ENLITT_CLIENT_ID"],
    app_origin="https://your-app.example",
    redirect_uri="https://your-app.example/enlitt/callback",
    default_return_to="/",
    state_store=MemoryStateStore(),
    # secret_key defaults to the ENLITT_SECRET_KEY environment variable.
)

app = FastAPI(routes=enlitt_routes(config=config))
app.add_middleware(EnlittSessionMiddleware)
```

`enlitt_routes()` mounts three routes:

* login start at `/enlitt/login`;
* the callback at `/enlitt/callback`, which must be the path of `redirect_uri`;
* logout at `/enlitt/logout`.

Each takes a keyword argument to change its path. The handlers behave exactly as
described on the [login page](/docs/authentication.md), and `StateRecord` and
`StateStore` have the same meaning there.

### Django and Flask

The handlers are plain async functions over a small `HandlerRequest` and
`HandlerResponse`, so any async framework can mount them. In Django, convert in
an async view:

```python
from django.http import HttpResponse
from enlitt.auth import HandlerRequest, login_start_handler

login = login_start_handler(config)


async def enlitt_login(request):
    response = await login(
        HandlerRequest(
            method=request.method,
            url=request.build_absolute_uri(),
            headers={k.lower(): v for k, v in request.headers.items()},
            body=request.body,
        )
    )
    out = HttpResponse(response.body, status=response.status)
    for name, value in response.headers:
        out.headers[name] = value  # use out.cookies for several set-cookie values
    return out
```

To make a model call from a sync view, wrap it in the session yourself:

```python
from enlitt import enlitt_session, run_with_enlitt_session

token = request.COOKIES.get("enlitt_session")
session = enlitt_session(token) if token else None
result = await run_with_enlitt_session(session, lambda: make_the_call())
```

Flask works the same way with `flask[async]`.

## Make a standard call

The client mirrors the OpenAI Responses API. Call it inside a request that has
gone through the middleware:

```python
from enlitt.openai import ENLITT_STANDARD_MODEL, enlitt

client = enlitt()


@app.post("/api/summary")
async def summary() -> dict[str, str]:
    response = await client.responses.create(
        model=ENLITT_STANDARD_MODEL,
        input="Summarize this support ticket.",
    )
    return {"text": response.output_text}
```

Pass `stream=True` to get an async iterator of OpenAI stream events. To cancel
a call, cancel the task or close the iterator. The standard model's limits
are on the [models page](/docs/models.md); unsupported options raise
`EnlittConfigurationError` before any request is sent.

## Make a premium call

Wrap your own `AsyncOpenAI` client. Your provider key stays on your server:

```python
from openai import AsyncOpenAI
from enlitt.openai import EnlittCallOptions, enlitt, per_token

premium = enlitt(AsyncOpenAI())

response = await premium.responses.create(
    model="gpt-4.1-mini",
    input="Write a long story.",
    enlitt=EnlittCallOptions(
        max_charge="20000000",  # NANOUSD, as a string
        price=per_token(input="400", output="1600"),
    ),
)
```

`enlitt=` is never sent to the provider. Amounts are always canonical integer
strings in NANOUSD.

For any other provider (images, voice, another model SDK), use
`with_premium_charge()`:

```python
from enlitt import PremiumProvider, WithPremiumChargeOptions, with_premium_charge


async def run(charge, operation):
    image = await my_image_provider.generate(prompt)
    charge("5000000")  # what this call costs the user, in NANOUSD
    return image


image = await with_premium_charge(
    WithPremiumChargeOptions(
        max_charge="5000000",
        provider=PremiumProvider(id="my-image-provider", model="image-v2"),
    ),
    run,
)
```

The call is reported exactly once. If `run` returns, it is reported as
completed; if it raises, as failed; if it is cancelled, as cancelled.

## Remaining capacity

```python
from enlitt import usage

capacity = await usage()
print(capacity.standard.remaining_percent, capacity.premium.limit_cap_nano_usd)
```

## Errors

The error classes have the same names as in Node.js: `EnlittError`,
`EnlittLoginRequiredError`, `EnlittInsufficientCreditsError`,
`EnlittLimitExceededError`, `EnlittSubscriptionInactiveError`,
`EnlittRateLimitError`, `EnlittAuthenticationError`,
`EnlittAuthorizationError`, `EnlittConfigurationError`, and
`EnlittModelError`. Each carries `code`, `provider_invoked`, `retryable`,
`retry_after_ms`, `request_id`, and `authorize_url`. The [errors page](/docs/errors.md)
says which ones are safe to show a user.

```python
from enlitt import EnlittLoginRequiredError

try:
    response = await client.responses.create(model=ENLITT_STANDARD_MODEL, input=prompt)
except EnlittLoginRequiredError:
    return RedirectResponse("/enlitt/login?return_to=/chat", status_code=303)
```

## Games and native apps

If your Python server backs a game or a desktop or mobile app, mount the
[client sign-in routes](/docs/client-sign-in.md) with
`enlitt_routes(config=config, client_sign_in=...)`. Then add the middleware
with `EnlittSessionMiddleware(app, header=True)`, so the game's
`Enlitt-Session` header is read.
