BellD / DevelopersDocumentation · latest

Python SDK

The BellD Python SDK calls BellD capabilities (AI, email, storage and others) from a Python service deployed on BellD. It uses only the Python standard library and works on Python 3.10 or later; BellD builds and runs Python services with Python 3.14. The SDK has the same capabilities, errors and permission checks as the TypeScript SDK.

Install

The SDK is not published on PyPI. A package named belld on PyPI is only a placeholder; do not install it by name. Download belld-0.1.0-py3-none-any.whl from the Overview page in the Console and check it against SHA256SUMS on the same page.

Put the file in your repository, inside the service's directory (for example vendor/belld-0.1.0-py3-none-any.whl), and reference it from your lockfile by its relative path with its sha256. Commit the wheel together with the lockfile. Do not store the wheel in Git LFS: BellD would see the LFS pointer file instead of the wheel, and your revision would fail confirmation. Keep the file name as downloaded, and do not use a symbolic link. Each folder name in the path may use only letters, digits, ., _, + and -.

With uv, add the wheel as a path source in pyproject.toml, then run uv lock and commit uv.lock:

[project]
dependencies = ["belld"]

[tool.uv.sources]
belld = { path = "vendor/belld-0.1.0-py3-none-any.whl" }

With Poetry, add a path dependency, then run poetry lock and commit poetry.lock:

[tool.poetry.dependencies]
belld = { path = "vendor/belld-0.1.0-py3-none-any.whl" }

With requirements.txt, write the wheel on its own line: the path starting with ./, followed by exactly one --hash. Do not add extras, environment markers, other options or environment variables to that line.

./vendor/belld-0.1.0-py3-none-any.whl --hash=sha256:<sha256 from SHA256SUMS>

To update the SDK, replace the file, update the reference and the lockfile, and create a new revision.

Declare capabilities

Declare every capability your service calls in pyproject.toml under [tool.belld]. Each entry has the same keys as belld.capabilities in a Node project's package.json (see Getting started): id, required, actions and scope; provider is optional. For example:

[tool.belld]
capabilities = [
  { id = "ai.generate", required = true, actions = ["generate"], scope = { models = ["text"], max_tokens_per_call = 1000 }, provider = { allow = ["openai_compatible"], fallback = "none" } },
]

Before deploying, BellD scans your .py files. A call to a capability you did not declare blocks the deployment; a declared capability that is never called is a warning.

Make a call

Calls need the incoming request's BellD credentials. With FastAPI or another ASGI framework, wrap the app after you have added all routes:

import belld
from fastapi import FastAPI

app = FastAPI()

@app.post("/api/summary")
async def summary(body: dict):
    return await belld.aio.ai.generate(
        model_kind="text", max_tokens=256, input={"prompt": body["text"], "stream": False})

app = belld.asgi(app)

With Flask or another WSGI framework, wrap wsgi_app:

import belld
from flask import Flask, request

app = Flask(__name__)

@app.post("/api/summary")
def summary():
    return belld.ai.generate(
        model_kind="text", max_tokens=256, input={"prompt": request.json["text"], "stream": False})

app.wsgi_app = belld.wsgi(app.wsgi_app)

In other code, pass the incoming request headers to belld.request: with belld.request(headers): makes calls inside the block use that request's credentials. A thread you start yourself does not inherit them; run it with contextvars.copy_context().run(...), or open with belld.request(headers): inside the thread.

Synchronous calls are belld.<domain>.<name>(...), for example belld.ai.generate(...) or belld.data.kv.get(...). Asynchronous calls add aio: await belld.aio.data.kv.get(...). Pass the capability input as input= and request options as keyword arguments. Any other keyword argument is ignored without an error, so check the spelling. The input and options of each capability are listed in the capability catalog.

Write calls the scan can read

Always write calls as belld.<domain>.<name>( on the belld module itself. The pre-deploy scan does not recognize calls through another name: from belld import ai followed by ai.generate(...), b = belld, or belld imported from one of your own modules. It also does not see calls inside f-strings, and a quote used as a format fill character (as in f"{x:'>10}") can hide the code after it. Calls the scan cannot see still go through BellD's permission checks when they run, and an undeclared call is refused then. If such a call is the only use of a declared capability, the scan also warns that the capability is never called.

Errors

A refused or failed call raises belld.BellDCapabilityError. It has code, reason, status and correlation_id, and to_problem() returns the full error as a dictionary. A call without the incoming request's credentials (outside belld.asgi, belld.wsgi or with belld.request(headers):) fails with the code unauthenticated. The codes are the same as in the TypeScript SDK; see HTTP errors.

Differences from the TypeScript SDK

PythonTypeScript
belld.email.send(from_=...)from
An input containing NaN or Infinity is refused with bad_request.Converted to null.
belld.create(timeout=...) is in seconds (default 30).timeoutMs in milliseconds

Tests

belld.testing.FakeBellD answers calls from a dictionary, with no BellD endpoint or credentials. Keys are <capability id>/<action>:

from belld.testing import FakeBellD

fake = FakeBellD({"ai.generate/generate": {"response": {"text": "hello"}}})
assert fake.ai.generate(input={}) == {"text": "hello"}

A key you did not configure raises BellDCapabilityError with code not_found. To return an error, use {"error": belld.BellDCapabilityError({"code": "forbidden", "status": 403})} as the value.