Cello v1.2.0 — Bug Fixes & Rust-Native AsyncClient¶
Release Date: June 2, 2026 License: MIT Python: 3.12+
Overview¶
Cello v1.2.0 ships three bug fixes reported against v1.1.0 and two new features — including a Rust-native async HTTP client that eliminates Python entirely from the outbound HTTP hot path.
All changes are fully backwards-compatible. No API surface was removed or renamed.
Bug Fixes¶
Fix 1 — on_event("shutdown") coroutine never awaited¶
Symptom: async def shutdown handlers registered with @app.on_event("shutdown") produced a RuntimeWarning: coroutine '...' was never awaited and the handler body was never executed.
Root cause: Shutdown hooks were called but the returned coroutine was discarded instead of being driven to completion.
Fix: Shutdown (and startup) hooks now go through run_lifecycle_handler_async, which uses pyo3_asyncio::tokio::into_future to drive the coroutine to completion via Tokio — the same mechanism used for request handlers. The GIL is released during I/O waits inside the hook.
Fix 2 — KeyboardInterrupt propagating into shutdown handlers¶
Symptom: Pressing CTRL+C printed Error in shutdown handler: KeyboardInterrupt and could interrupt the shutdown sequence mid-way.
Fix: The shutdown handler loop now explicitly suppresses KeyboardInterrupt errors:
Err(e) if !e.to_string().contains("KeyboardInterrupt") => {
eprintln!("Error in shutdown handler: {e}");
}
_ => {}
KeyboardInterrupt is expected during a CTRL+C shutdown and is silently ignored; all other errors are still printed.
Fix 3 — request.redis raises AttributeError¶
Symptom: Accessing request.redis inside a handler raised AttributeError: 'Request' object has no attribute 'redis' even when app.enable_redis() was configured.
Fix:
- A
redisproperty and_inject_redismethod were added to the RustRequeststruct (src/request/mod.rs). - The Python dispatch wrapper (
__init__.py) now callsrequest._inject_redis(app._redis)before each handler invocation when Redis is configured.
app.enable_redis(RedisConfig(url="redis://localhost:6379", pool_size=10))
@app.get("/")
async def index(request):
r = request.redis # works — no longer raises AttributeError
await r.set("key", "val")
return {"ok": True}
New Features¶
Feature 1 — Redis Lua scripting: eval, evalsha, script_load¶
Three new methods on the Redis client enable atomic multi-command operations via Lua scripts, reducing round-trips for compound read-modify-write patterns.
| Method | Description |
|---|---|
await r.eval(script, numkeys, *keys_and_args) | Execute a Lua script inline |
await r.evalsha(sha, numkeys, *keys_and_args) | Execute a cached script by SHA1 |
await r.script_load(script) | Upload a script; returns its SHA1 |
Example — atomic check-and-enqueue:
sha = await r.script_load("""
local m = redis.call('SISMEMBER', KEYS[1], ARGV[1])
if m == 0 then return 0 end
redis.call('LPUSH', KEYS[2], ARGV[2])
return 1
""")
# 1 network round-trip instead of 2
result = await r.evalsha(sha, 2, "tokens", "queue", token, data)
Feature 2 — Rust-native AsyncClient (reqwest + Tokio)¶
The built-in AsyncClient has been rewritten from Python (urllib.request + asyncio.to_thread) to a pure Rust implementation backed by reqwest 0.11 and Tokio.
Before (v1.1.0): Python stdlib + thread-pool executor After (v1.2.0): Rust reqwest + Tokio — GIL never held during network I/O
The API is identical to v1.1.0. No user code changes are required.
from cello import AsyncClient
client = AsyncClient(timeout=10.0)
# GET
resp = await client.get("https://api.example.com/data")
print(resp.status) # int
print(resp.text) # str (UTF-8 decoded)
print(resp.json()) # dict (Python object)
print(resp.content) # bytes
# POST with JSON body
resp = await client.post("https://api.example.com/items", json={"name": "widget"})
# POST with raw bytes
resp = await client.post("https://api.example.com/upload", content=b"...", headers={"Content-Type": "application/octet-stream"})
# PUT / PATCH / DELETE
resp = await client.put("https://api.example.com/items/1", json={"name": "updated"})
resp = await client.patch("https://api.example.com/items/1", json={"name": "patched"})
resp = await client.delete("https://api.example.com/items/1")
# Async context manager
async with AsyncClient() as client:
resp = await client.get("https://example.com")
What changed internally:
| Aspect | v1.1.0 | v1.2.0 |
|---|---|---|
| Backend | urllib.request | reqwest (Rust) |
| Concurrency | asyncio.to_thread (thread pool) | Tokio native I/O |
| GIL during I/O | Held in thread | Never held |
| HTTP/2 | No | Yes (via reqwest) |
| TLS | Python stdlib | rustls (same as server) |
| Gzip decompression | No | Automatic |
| Connection pooling | OS per-thread | reqwest shared pool |
Implementation details:
src/http_client.rs— new module;PyHttpResponseandPyAsyncClientare#[pyclass]types- JSON bodies are serialized via Python's
json.dumpswhile the GIL is held (before the async boundary), then the raw bytes are passed into the Rust future — GIL is dropped for all I/O - Coroutines returned to Python via
pyo3_asyncio::tokio::future_into_py reqwest = { version = "0.11", features = ["rustls-tls", "gzip"], default-features = false }added to[dependencies]inCargo.toml
Architecture — Request Handler Hot Path (unchanged since v1.0.0)¶
For completeness, this section documents the end-to-end async execution path that was already in place and is unaffected by this release:
TCP accept → Rust (Tokio)
HTTP parse → Rust (hyper)
Route lookup → Rust (matchit radix tree)
Handler call → Python (GIL acquired, handler invoked)
Coroutine await → Rust (Tokio via pyo3-asyncio::tokio::into_future, GIL released)
Response build → Rust (SIMD JSON serialization)
Python async def handlers are driven by Tokio, not asyncio. The event loop seen inside a handler (asyncio.get_event_loop()) is the pyo3-asyncio Tokio-backed loop — uvloop is not required and has no effect.
Upgrade Guide¶
No code changes are required. Install the new wheel:
Or rebuild from source:
Version History¶
| Version | Date | Highlights |
|---|---|---|
| 1.2.0 | 2026-06-02 | Bug fixes (shutdown, redis), Lua scripting, Rust AsyncClient |
| 1.1.0 | 2026-04-11 | MiniJinja template engine |
| 1.0.1 | 2026-02-21 | Cross-platform fixes (Windows, ARM), async compatibility |
| 1.0.0 | — | Production-stable release |