Skip to content

Running BlackBull

BlackBull has three entry points, in increasing order of how "production-shaped" they are. All three resolve to the same underlying serve() function — pick whichever shape fits the deployment surface.

1. app.run(...) — in-Python, single command

# Single-worker, plain HTTP/1.1 on :8000
app.run(port=8000)

# HTTPS + HTTP/2 via ALPN on :8443
app.run(port=8443, certfile='cert.pem', keyfile='key.pem')

# Multi-worker (pre-forks N workers; master holds the listening sockets)
app.run(port=8443, certfile='cert.pem', keyfile='key.pem', workers=4)

# Development: hot-reload when *.py files change
# Requires the [reload] extra: pip install 'blackbull[reload]'
app.run(port=8000, reload=True)

app.run() is synchronous — it internalises asyncio.run so callers write app.run(port=8000) rather than asyncio.run(app.run(...)). reload=True and workers > 1 both trigger the master-process path (long-lived supervisor around the worker pool).

See BlackBull.run for its signature. Unset arguments resolve through BLACKBULL_* environment values, .env, AppConfig and built-in defaults; see Configuration. Declaring the settings once keeps __main__ to a single line:

from blackbull import BlackBull, AppConfig

app = BlackBull(config=AppConfig(port=8443, certfile='cert.pem',
                                 keyfile='key.pem', workers=4))

if __name__ == '__main__':
    app.run()

2. blackbull CLI — useful for systemd, Docker, PaaS

# Same flags as app.run kwargs
blackbull myapp:app \
    --bind 0.0.0.0:8443 \
    --certfile cert.pem --keyfile key.pem \
    --workers 4

# Auto-reload via the CLI
blackbull myapp:app --bind 127.0.0.1:8000 --reload

The CLI accepts any module:attribute import path; the attribute may be a BlackBull instance or a plain ASGI callable. See blackbull --help for the full flag list and Configuration for how flags compose with environment variables and TOML config files.

blackbull serve — zero-code static files

To serve a directory of static files without writing any application code — a drop-in upgrade over python -m http.server, with ETag, HTTP/2 (under TLS), and a directory index built in:

blackbull serve ./public --bind :8080
blackbull serve ./public --certfile cert.pem --keyfile key.pem  # HTTPS + HTTP/2

See Static files → blackbull serve.

3. Any external ASGI 3.0 server

BlackBull instances are valid ASGI 3.0 callables, so they work under any ASGI server:

uvicorn myapp:app --port 8000
hypercorn myapp:app --bind 0.0.0.0:8000
granian --interface asgi myapp:app

You lose BlackBull's own HTTP/2 frame layer and WebSocket codec (the surrounding server's protocol stack handles framing instead), but the application code is portable as-is.

Embedded use under an existing event loop

To run BlackBull inside an existing event loop, or to pre-bind a socket before forking a test subprocess, instantiate ASGIServer directly:

import asyncio
from blackbull.server import ASGIServer

server = ASGIServer(app, certfile='cert.pem', keyfile='key.pem')
server.open_socket(8443)
await server.run()

open_socket(port=0) binds to a kernel-assigned port and exposes it as server.port — the same pattern the testing fixture uses.

The direct API keeps ownership until run() has completely handed each socket to an event-loop server. Any exception or cancellation before that startup commit closes the listeners, event-loop servers, lifespan task, and connection tasks acquired so far. Cleanup shares one bounded deadline and does not replace the exception that caused startup to fail. Calling close_socket() again is safe.

Picking the deployment shape

Shape When
python myapp.py (calls app.run()) Development, one-shot scripts, tutorial-style apps
blackbull myapp:app … systemd, Docker, PaaS — anywhere the deployment surface is a CLI
External ASGI server When the surrounding deployment dictates uvicorn / granian / hypercorn for tooling reasons
ASGIServer direct Embedded inside another asyncio program, or when fine-grained socket / TLS control is needed (e.g. mTLS — see TLS)

Next