Truly async Python stream processing with real Kafka transactions for exactly-once delivery, and an MQTT→Kafka bridge that ACKs only after Kafka has the data.
Project description
Flechtwerk
Truly async Python stream processing with real Kafka transactions for exactly-once delivery, and an MQTT→Kafka bridge that ACKs only after Kafka has the data.
📖 Documentation: bsure-analytics.github.io/flechtwerk — guides, concepts, and the full API reference.
What It Is
Flechtwerk (German: interlacing, wickerwork) is a small async stream processing framework for Kafka. It takes the operational design that Kafka Streams nailed a decade ago — consumer groups for partition assignment, compacted changelog topics as the durable state of record, Kafka transactions tying state writes, output messages, and offset commits into a single atomic unit — and ports it to modern async Python.
If you've run Kafka Streams in production, the model is immediately familiar: stateful operators backed by RocksDB, recovery via changelog replay, exactly-once delivery via real Kafka transactions, ephemeral compute that can be killed and rescheduled freely because all durable state lives in Kafka.
It exists because the existing Python options each miss a constraint that matters for I/O-bound, transactional, multi-instance stream processing — Faust's multi-instance recovery is fragile and its "exactly-once" isn't real transactions; Quix Streams' core loop is synchronous; Bytewax is awkward for async I/O; Beam-on-Flink spans two runtimes. See Why Flechtwerk exists for the full comparison.
Installation
pip install flechtwerk # or: uv add flechtwerk
pip install "flechtwerk[mqtt]" # with the MQTT→Kafka bridge (paho-mqtt)
Python 3.12+. Runtime dependencies: aiokafka[zstd], prometheus-client, reactor-di, and rocksdict. Run it on uvloop for best throughput — the framework works on stock asyncio (and therefore on Windows), but the event loop is the application's choice.
Quickstart
The whole contract is two yield statements inside an async generator:
yield Message(...)to emit an output record.yield State(...)to persist state for the current key — oryielda falsyStateto tombstone it.
That's it. There are no agents, no tables, no DSL, no global app to register against, no fluent builders — a stage is a plain async generator, and a single decorator names its topics and turns it into a runnable stage. Every Python developer already knows how to read one.
from collections.abc import AsyncIterator
from flechtwerk import Event, IncomingMessage, Message, State, transformer
from flechtwerk.attribute import Attribute, DATETIME, INT
SEEN = Attribute("seen", INT)
"""How many events this key has produced so far."""
TIMESTAMP = Attribute("timestamp", DATETIME)
"""When the event happened at the source."""
@transformer(input_topics=["my-input"])
async def stage(msg: IncomingMessage, state: State) -> AsyncIterator[Message | State]:
seen = (state.get(SEEN) or 0) + 1
yield Message(key=msg.key, topic="my-output", value=Event({**msg.value, SEEN: seen}))
yield State({SEEN: seen, TIMESTAMP: msg.value[TIMESTAMP]})
Running a stage is one call — all configuration is injected, nothing is read from the environment:
import asyncio
from flechtwerk import Flechtwerk
async def main() -> None:
await Flechtwerk.of(
application_id="my-transformer",
bootstrap_servers="localhost:9092",
client_id="my-transformer-0", # process identity: unique per instance, stable across restarts
stage=stage, # from above
).run()
if __name__ == "__main__":
asyncio.run(main())
That plus one stage definition is the whole program — point it at any Kafka broker. The same two-yield contract drives an Extractor from the other end (polling an external source, with State as its resume cursor), and an MqttExtractor pushes into the poll loop.
Learn More
The documentation has the full story.
Guides
- Getting Started — install, a minimal transformer and extractor, and running a stage.
- Extractors — poll an external source into Kafka, with
Stateas the resume cursor. - MQTT Extractors — a push-driven MQTT source that ACKs to the broker only once a batch is durable in Kafka.
- Transformers — stream-to-stream processing with partitioned, exactly-once tasks.
- Best Practices — co-partitioning, the let-it-crash error strategy, and the operational rules that keep a multi-instance deployment correct.
- Observability — the Prometheus metrics the runners emit.
Concepts
- Typed Attributes & Records — the middle ground between dicts and dataclasses: the
flechtwerk.attributelibrary enforces the JSON boundary at the write site, once per field declaration, with no dataclass per message shape. - Config Topics — shared, eventually-consistent lookup tables (Kafka Streams' GlobalKTable, specialized to configuration).
- Exactly-Once Delivery — the task model tying output messages, state writes, and offset commits into one Kafka transaction per batch.
- Architecture — the hexagonal (ports and adapters) design and the operational model.
API Reference — generated from the source docstrings.
Development
uv sync # venv + all dependencies
uv run pytest # unit tier — Docker-free
uv run pytest -m integration # integration tier — ephemeral Kafka/Mosquitto via testcontainers
uv run coverage run -m pytest -m "integration or not integration" && uv run coverage report
These commands are also exposed as poe tasks — uv run poe test | cov | build | docs | docs-build. The documentation site is built with MkDocs Material; uv run poe docs serves it locally with live reload, and it deploys to GitHub Pages on every push to main.
Releases are cut by tagging: pushing a vX.Y.Z tag runs the test suite, builds the package with the tag-derived version (hatch-vcs), and publishes it to PyPI via trusted publishing.
Status
Flechtwerk was extracted from the data integration platform it was developed in, where it runs every stage in production. The API is small and settled in shape, but pre-1.0 — minor releases may still move things around. One design rule carries over from its origin as an embedded framework: framework code references no application paths, modules, or environment variables; all configuration is injected by the caller.
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 flechtwerk-0.6.0.tar.gz.
File metadata
- Download URL: flechtwerk-0.6.0.tar.gz
- Upload date:
- Size: 509.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9517921c6f17fe9b1f53bb88f58e4485df49de1cc0a1e820c4bdab4fc714158
|
|
| MD5 |
bdaf4406eddaea2e8300d0a3997ba6b7
|
|
| BLAKE2b-256 |
28d75f3f35de24203b5f8461c5f4f06f49d212d3fffcf4c0cf55355ac864cee5
|
File details
Details for the file flechtwerk-0.6.0-py3-none-any.whl.
File metadata
- Download URL: flechtwerk-0.6.0-py3-none-any.whl
- Upload date:
- Size: 74.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94f98649f15cbba9711ec8c25b19029aa06ab948ed496b73bfef03a86711ff50
|
|
| MD5 |
57cbdcfd73758a535a857e012c87a5bc
|
|
| BLAKE2b-256 |
00f90148a0b2ab038b35e9ae929b1c9309dc36d778cdd4a4a8c2e16a3bda5477
|