Why this shape
Crossing into Kafka means more than importing a client. The important contract is what happens when Python calls back into Sifr from another thread. Five rules drive the Kafka path:- Callbacks need an attached policy.
@python.callback(...)names the parameter that becomes the Python callable and states lifetime, dispatch, and concurrency. - Foreign threads are explicit.
dispatch=foreignmeans Python-created threads may enter the handler. Captures must be sendable and thread-safe. - Lifetime drains accepted work.
lifetime=callkeeps the callable only until the declaration returns, then drains it. Longer lifetimes need an opaque owner with deterministic cleanup. - Bridge the broker workflow. Producer/consumer setup, polling, and thread handoff belong in
src/python_bridges/. The Sifr declaration stays a typed boundary. - Trust the import root. Authorize
kafkaunder[trust].python. Add[trust].python-nativeonly if the chosen client loads native extensions in-process.
Package setup
Installkafka-python in the root uv project:
sifr.toml
Bridge the poll-and-callback workflow
src/python_bridges/kafka_consumer.py
dispatch=foreign.
Declare the callback boundary
Handler failures cross Python as
SifrCallbackError and still return through the declared Result channel. If Python also fails, the Python error stays primary.
Retention and cleanup
This example uses call-scoped callbacks. If the consumer must retain a handler across many polls, switch tolifetime=result or lifetime=Self on an opaque owner with close, async_close, context, or async_context cleanup. Owner shutdown must unregister first, reject new entries, drain accepted invocations, and release captures exactly once.
Next steps
- Full callback and resource contracts: Embedded Python Interop
- Typed package objects without callbacks: schwifty
- Native numerical work: numpy
