Async ASGI 3.0 framework with a from-scratch HTTP/1.1 parser, HTTP/2 frame layer, and WebSocket codec. HPACK delegates to the hpack library.
Project description
BlackBull
BlackBull is a Python ASGI 3.0 web framework for developers who
want one pip install, zero C compilers, and the ability to
programmatically test their HTTP clients and servers against
deliberate protocol misbehaviour.
Hello, world
from blackbull import BlackBull
app = BlackBull()
@app.route(path='/')
async def hello():
return "Hello, world!"
if __name__ == '__main__':
app.run(port=8000)
$ python app.py &
$ curl -i http://localhost:8000/
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 13
Hello, world!
Why BlackBull
- Zero ceremony.
app.run()is the entire deploy story — orblackbull serve ./publicfor a static site with ETag + HTTP/2 and no code at all. No separate ASGI runner, no YAML config, nogunicornclass path. - Readable stack. Every byte on the wire passes through Python
you can step through with
pdb. No C extensions to debug. - Break things on purpose. The same protocol code that serves real traffic can drive a programmable misbehaving client or server. Test your own HTTP/2 client against half-closed streams, exhausted windows, and illegal SETTINGS — in CI.
- RFC-grade correctness. Passes the same external conformance
suites used to validate nginx and Envoy (
h2spec, Autobahn). - Typed throughout. Your editor and
mypy/pyrightsee every parameter; PEP 561 typed distribution.
Install
pip install blackbull
pip install 'blackbull[compression]' # add brotli + zstandard codecs
pip install 'blackbull[speed]' # add uvloop event loop
pip install 'blackbull[reload]' # add watchfiles for --reload
pip install 'blackbull[fault-injection]' # add cryptography + httpx for the toolkit
Simplified handlers
Route handlers may return a str, bytes, dict, or Response;
path parameters are coerced to the annotation type:
@app.route(path='/tasks/{task_id:int}')
async def get_task(task_id: int):
return {"id": task_id, "title": "..."}
Drop down to full ASGI (scope, receive, send) whenever you need it
— routes accept either shape.
TLS + HTTP/2
app.run(port=8443, certfile='cert.pem', keyfile='key.pem')
ALPN negotiates h2 automatically; HTTP/1.1 clients fall back via
the same socket.
WebSocket
from http import HTTPMethod
from blackbull.utils import Scheme
@app.route(path='/ws', methods=[HTTPMethod.GET], scheme=Scheme.websocket)
async def ws_echo(scope, receive, send):
await receive() # websocket.connect
await send({'type': 'websocket.accept'})
while True:
msg = await receive()
if msg['type'] == 'websocket.disconnect':
break
if msg['type'] == 'websocket.receive':
await send({'type': 'websocket.send',
'text': msg.get('text') or ''})
Event API
Two decorators cover both lifecycle hooks and per-request behaviour
— observation (@app.on, fire-and-forget) and interception
(@app.intercept, synchronous, may short-circuit):
@app.on_startup
async def warm_caches():
...
@app.intercept('request_received')
async def auth(scope, receive, send, call_next):
# raise to abort, or skip call_next to short-circuit
await call_next(scope, receive, send)
@app.on('request_completed')
async def emit_metrics(event):
metrics.increment('requests', status=event['status'])
Events: app_startup, app_shutdown, request_received,
before_handler, request_completed, websocket_message. @app.on
isolates exceptions per observer; @app.intercept is part of the
request path and can deny / rewrite / pass through. See
docs/guide/events.md
for the full event catalogue and detail payloads.
Built-in middleware
Compose via app.use(...) or per-route middlewares=[...]:
| Middleware | What it does |
|---|---|
Compression |
Negotiates br / zstd / gzip from Accept-Encoding |
StaticFiles |
Serves files from a directory under a URL prefix |
Cache |
Per-worker LRU + ETag / Cache-Control honouring |
Session |
Signed-cookie sessions (HMAC-SHA256) |
CORS |
Preflight + actual-request header injection |
TrustedProxy |
Rewrites scope['client'] / scope['scheme'] from proxy headers |
websocket |
Auto-accepts the WebSocket handshake and emits websocket.close after the handler returns |
OpenAPI / Swagger UI
app.enable_openapi() # publishes /openapi.json and /docs
Auto-generates an OpenAPI 3.1 spec from route signatures, path-param
converters, docstrings, and @dataclass annotations on body
parameters. Dataclass-typed bodies are also deserialized at runtime
— async def h(body: CreateTask): ... receives a constructed
instance, no manual json.loads.
Fault injection
BlackBull's single most distinctive feature: a programmable deliberate-misbehaviour toolkit you can point at your own HTTP/2 client (or proxy, or middleware) directly from a pytest suite.
import pytest
from blackbull.fault_injection import H2FaultServer, make_self_signed_h2_context
from blackbull.fault_injection.catalogue import half_closed_stream_no_data
@pytest.mark.asyncio
async def test_my_client_handles_half_closed_streams():
ssl_ctx = make_self_signed_h2_context()
async with H2FaultServer(
scenario=half_closed_stream_no_data(), ssl_context=ssl_ctx,
) as srv:
# Your client must time out or RST_STREAM rather than block
# forever when the server sends HEADERS without END_STREAM.
with pytest.raises(TimeoutError):
await my_h2_client.get(srv.url, timeout=1.0)
The catalogue ships four spec-grade categories (half-closed streams,
exhausted flow-control windows, illegal SETTINGS, weird frame
sequences); the symmetric HTTP/1.1 client side drives a real server
through trickled headers, partial requests, and abrupt RST. See
docs/guide/fault_injection.md
for the full tutorial.
Early Alpha
⚠ Early Alpha — The API may change between MINOR versions. See Conformance for protocol-level test coverage and Known Limitations for the explicit list of behaviours to expect before adopting.
Examples
| Example | Demonstrates |
|---|---|
examples/SimpleTaskManager/ |
REST API + HTML UI, middleware pipeline, route groups, SQLite, Bearer token auth |
examples/ChatServer/ |
WebSocket, SSE, long polling side by side; Session + Compression + custom auth |
examples/typed_routes_ok.py |
{param:converter} syntax, url_path_for |
examples/scenario_h1_fault_injection.py |
HTTP/1.1 fault scenarios driven against stdlib http.server |
examples/scenario_h2_fault_injection.py |
HTTP/2 fault scenarios served against httpx |
Documentation
- Guide:
docs/guide/index.md - Architecture:
docs/about/internals.md - Changelog:
CHANGELOG.md
Versioning
BlackBull uses ZeroVer prior to a 1.0 commitment.
MINOR advances at each sprint close; PATCH is for bug fixes and
harness work between sprints. See CHANGELOG.md for
the full release history.
License
Apache License 2.0 — © TOKUJI.
Next steps
→ Read the Guide — routing, middleware, WebSockets, HTTP/2, and more.
→ Browse the examples — copy-pasteable starting points for REST APIs, chat servers, and SSE streams.
→ Star on GitHub — every star helps the project grow.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file blackbull-0.43.1.tar.gz.
File metadata
- Download URL: blackbull-0.43.1.tar.gz
- Upload date:
- Size: 244.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d94dddc08e984449953e22c32bba93f96289f017ed2e53b6e7da329c9657a5ca
|
|
| MD5 |
68e74534c457c79159dfa00adf916c07
|
|
| BLAKE2b-256 |
65db43bd56a2107a45f535a2d21064a9f6b4ab7cea1611d5a1e64597304703cc
|
Provenance
The following attestation bundles were made for blackbull-0.43.1.tar.gz:
Publisher:
publish.yml on TOKUJI/BlackBull
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
blackbull-0.43.1.tar.gz -
Subject digest:
d94dddc08e984449953e22c32bba93f96289f017ed2e53b6e7da329c9657a5ca - Sigstore transparency entry: 1898615344
- Sigstore integration time:
-
Permalink:
TOKUJI/BlackBull@aa8ed8ea493965997136034c01227423803ac75e -
Branch / Tag:
refs/tags/v0.43.1 - Owner: https://github.com/TOKUJI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@aa8ed8ea493965997136034c01227423803ac75e -
Trigger Event:
push
-
Statement type:
File details
Details for the file blackbull-0.43.1-py3-none-any.whl.
File metadata
- Download URL: blackbull-0.43.1-py3-none-any.whl
- Upload date:
- Size: 278.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8617e85f398ec5f25165363e6b714582fa2cbff79e8633ffcf98c30c1d6c5b1
|
|
| MD5 |
681da111ec09ff1654bf30ead479b369
|
|
| BLAKE2b-256 |
75373b877cbf7b1d39b22135681e2d903c78d8c52da88285ea889aedd5ef7270
|
Provenance
The following attestation bundles were made for blackbull-0.43.1-py3-none-any.whl:
Publisher:
publish.yml on TOKUJI/BlackBull
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
blackbull-0.43.1-py3-none-any.whl -
Subject digest:
a8617e85f398ec5f25165363e6b714582fa2cbff79e8633ffcf98c30c1d6c5b1 - Sigstore transparency entry: 1898615605
- Sigstore integration time:
-
Permalink:
TOKUJI/BlackBull@aa8ed8ea493965997136034c01227423803ac75e -
Branch / Tag:
refs/tags/v0.43.1 - Owner: https://github.com/TOKUJI
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@aa8ed8ea493965997136034c01227423803ac75e -
Trigger Event:
push
-
Statement type: