kafka-component 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kafka_component-0.1.0/.github/workflows/ci.yml +23 -0
- kafka_component-0.1.0/.github/workflows/publish-pypi.yml +31 -0
- kafka_component-0.1.0/.gitignore +6 -0
- kafka_component-0.1.0/.python-version +1 -0
- kafka_component-0.1.0/CHANGELOG.md +5 -0
- kafka_component-0.1.0/CONTEXT.md +25 -0
- kafka_component-0.1.0/LICENSE +21 -0
- kafka_component-0.1.0/PKG-INFO +97 -0
- kafka_component-0.1.0/README.md +73 -0
- kafka_component-0.1.0/docker-compose.yml +14 -0
- kafka_component-0.1.0/docs/adr/0001-aiokafka-client-and-delivery-semantics.md +16 -0
- kafka_component-0.1.0/docs/superpowers/plans/2026-09-02-kafka-component.md +1250 -0
- kafka_component-0.1.0/kafka_component/__init__.py +19 -0
- kafka_component-0.1.0/kafka_component/_routes.py +15 -0
- kafka_component-0.1.0/kafka_component/consumer.py +99 -0
- kafka_component-0.1.0/kafka_component/errors.py +45 -0
- kafka_component-0.1.0/kafka_component/producer.py +55 -0
- kafka_component-0.1.0/kafka_component/py.typed +0 -0
- kafka_component-0.1.0/kafka_component/tests/__init__.py +0 -0
- kafka_component-0.1.0/kafka_component/tests/conftest.py +17 -0
- kafka_component-0.1.0/kafka_component/tests/helpers.py +40 -0
- kafka_component-0.1.0/kafka_component/tests/test_consumer.py +204 -0
- kafka_component-0.1.0/kafka_component/tests/test_errors.py +68 -0
- kafka_component-0.1.0/kafka_component/tests/test_producer.py +55 -0
- kafka_component-0.1.0/kafka_component/tests/test_routes.py +21 -0
- kafka_component-0.1.0/kafka_component/tests/test_system_integration.py +54 -0
- kafka_component-0.1.0/pyproject.toml +45 -0
- kafka_component-0.1.0/pytest.ini +7 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v3
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: uv sync --all-groups
|
|
21
|
+
- run: uv run pytest
|
|
22
|
+
- run: uv run ruff format --check .
|
|
23
|
+
- run: uv run ruff check .
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
target:
|
|
9
|
+
description: "PyPI target"
|
|
10
|
+
required: true
|
|
11
|
+
default: "pypi"
|
|
12
|
+
type: choice
|
|
13
|
+
options: ["pypi", "testpypi"]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
ci:
|
|
17
|
+
uses: ./.github/workflows/ci.yml
|
|
18
|
+
|
|
19
|
+
publish:
|
|
20
|
+
needs: ci
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: ${{ github.event.inputs.target || 'pypi' }}
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: astral-sh/setup-uv@v3
|
|
28
|
+
- run: uv build
|
|
29
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
30
|
+
with:
|
|
31
|
+
repository-url: ${{ (github.event.inputs.target == 'testpypi') && 'https://test.pypi.org/legacy/' || '' }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# kafka-component
|
|
2
|
+
|
|
3
|
+
Component library for Kafka producer/consumer lifecycle, built on python-components' `Component` contract. Provides async producer and consumer components with explicit, no-silent-failure delivery semantics.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
**KafkaProducerComponent**:
|
|
8
|
+
A `Component` that publishes JSON-encoded message values to Kafka, waiting for broker acknowledgment before returning.
|
|
9
|
+
_Avoid_: Producer, Kafka publisher
|
|
10
|
+
|
|
11
|
+
**KafkaConsumerComponent**:
|
|
12
|
+
A `Component` that consumes JSON-encoded message values from Kafka, committing offsets only after the message handler succeeds.
|
|
13
|
+
_Avoid_: Consumer, Kafka subscriber
|
|
14
|
+
|
|
15
|
+
**ErrorPolicy**:
|
|
16
|
+
The strategy a `KafkaConsumerComponent` invokes when a message handler raises, deciding what happens to the failed message.
|
|
17
|
+
_Avoid_: failure handler, retry handler
|
|
18
|
+
|
|
19
|
+
**SkipAndLogPolicy**:
|
|
20
|
+
The default `ErrorPolicy` — logs the failure and advances past the message without retrying.
|
|
21
|
+
_Avoid_: default policy
|
|
22
|
+
|
|
23
|
+
**DeadLetterPolicy**:
|
|
24
|
+
An `ErrorPolicy` that republishes the failed message and its error metadata to a dead-letter topic, defaulting to `{topic}.DLQ` when the caller doesn't supply one explicitly.
|
|
25
|
+
_Avoid_: DLQ handler
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabricio Lima
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kafka-component
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Kafka producer/consumer components for python-components
|
|
5
|
+
Project-URL: Homepage, https://github.com/fabriciooml/kafka-component
|
|
6
|
+
Project-URL: Repository, https://github.com/fabriciooml/kafka-component
|
|
7
|
+
Project-URL: Issues, https://github.com/fabriciooml/kafka-component/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/fabriciooml/kafka-component/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Fabricio Lima
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: aiokafka,asyncio,kafka,python-components
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: aiokafka<1,>=0.14
|
|
21
|
+
Requires-Dist: fastapi>=0.115
|
|
22
|
+
Requires-Dist: python-components<0.5,>=0.4.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# kafka-component
|
|
26
|
+
|
|
27
|
+
Async Kafka producer/consumer components for [python-components](https://github.com/lucassant95/python-components).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add kafka-component
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Python >= 3.11
|
|
38
|
+
- A running Kafka broker (KRaft or ZooKeeper mode)
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
|
|
45
|
+
from python_components import System
|
|
46
|
+
from kafka_component import KafkaProducerComponent, KafkaConsumerComponent, DeadLetterPolicy
|
|
47
|
+
|
|
48
|
+
producer = KafkaProducerComponent(bootstrap_servers="localhost:9092")
|
|
49
|
+
|
|
50
|
+
async def handle_order(value):
|
|
51
|
+
print("received order", value)
|
|
52
|
+
|
|
53
|
+
consumer = KafkaConsumerComponent(
|
|
54
|
+
bootstrap_servers="localhost:9092",
|
|
55
|
+
group_id="orders-service",
|
|
56
|
+
topics=["orders"],
|
|
57
|
+
handler=handle_order,
|
|
58
|
+
error_policy=DeadLetterPolicy(producer, dlq_topic="orders.DLQ"),
|
|
59
|
+
).using(["producer"])
|
|
60
|
+
|
|
61
|
+
system = System({"producer": producer, "consumer": consumer})
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
async with system:
|
|
65
|
+
await producer.send("orders", {"order_id": 1})
|
|
66
|
+
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The consumer's `.using(["producer"])` declares a start-order dependency on the
|
|
71
|
+
producer so `System`'s topological sort always starts the producer first —
|
|
72
|
+
it is not read as an injected `self.producer` attribute by
|
|
73
|
+
`KafkaConsumerComponent`; the `DeadLetterPolicy` above already holds a
|
|
74
|
+
direct reference to the `producer` instance.
|
|
75
|
+
|
|
76
|
+
## Semantics and caveats
|
|
77
|
+
|
|
78
|
+
- **At-least-once delivery.** The consumer commits offsets only after the handler succeeds. A crash between a successful handler call and the commit can redeliver a message — handlers should be idempotent if that matters.
|
|
79
|
+
- **Sequential processing.** One message is handled at a time, in partition order. There is no built-in concurrency; scale via more consumer instances/partitions, not intra-instance parallelism.
|
|
80
|
+
- **Not a supervisor.** On handler failure, the configured `ErrorPolicy` runs (default `SkipAndLogPolicy`, or `DeadLetterPolicy`) and the loop continues — the component itself does not retry, crash-loop, or restart. Health is exposed via `routes()`; process supervision is the caller's job.
|
|
81
|
+
- **JSON value-only.** Message values are `json.dumps`/`json.loads`. Keys and headers pass through untouched.
|
|
82
|
+
- **Graceful shutdown drains the in-flight message.** `shutdown()` waits for the currently-processing message's handler (and its `ErrorPolicy`, if it fails) to finish before stopping — it does not abandon work mid-message.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv sync --all-groups
|
|
88
|
+
uv run pytest
|
|
89
|
+
uv run ruff format --check .
|
|
90
|
+
uv run ruff check .
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Integration tests spin up a real Kafka broker via [testcontainers](https://testcontainers.com/modules/kafka/). For local development against a persistent broker instead, run `docker compose up -d`.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# kafka-component
|
|
2
|
+
|
|
3
|
+
Async Kafka producer/consumer components for [python-components](https://github.com/lucassant95/python-components).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add kafka-component
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python >= 3.11
|
|
14
|
+
- A running Kafka broker (KRaft or ZooKeeper mode)
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
|
|
21
|
+
from python_components import System
|
|
22
|
+
from kafka_component import KafkaProducerComponent, KafkaConsumerComponent, DeadLetterPolicy
|
|
23
|
+
|
|
24
|
+
producer = KafkaProducerComponent(bootstrap_servers="localhost:9092")
|
|
25
|
+
|
|
26
|
+
async def handle_order(value):
|
|
27
|
+
print("received order", value)
|
|
28
|
+
|
|
29
|
+
consumer = KafkaConsumerComponent(
|
|
30
|
+
bootstrap_servers="localhost:9092",
|
|
31
|
+
group_id="orders-service",
|
|
32
|
+
topics=["orders"],
|
|
33
|
+
handler=handle_order,
|
|
34
|
+
error_policy=DeadLetterPolicy(producer, dlq_topic="orders.DLQ"),
|
|
35
|
+
).using(["producer"])
|
|
36
|
+
|
|
37
|
+
system = System({"producer": producer, "consumer": consumer})
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
async with system:
|
|
41
|
+
await producer.send("orders", {"order_id": 1})
|
|
42
|
+
|
|
43
|
+
asyncio.run(main())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The consumer's `.using(["producer"])` declares a start-order dependency on the
|
|
47
|
+
producer so `System`'s topological sort always starts the producer first —
|
|
48
|
+
it is not read as an injected `self.producer` attribute by
|
|
49
|
+
`KafkaConsumerComponent`; the `DeadLetterPolicy` above already holds a
|
|
50
|
+
direct reference to the `producer` instance.
|
|
51
|
+
|
|
52
|
+
## Semantics and caveats
|
|
53
|
+
|
|
54
|
+
- **At-least-once delivery.** The consumer commits offsets only after the handler succeeds. A crash between a successful handler call and the commit can redeliver a message — handlers should be idempotent if that matters.
|
|
55
|
+
- **Sequential processing.** One message is handled at a time, in partition order. There is no built-in concurrency; scale via more consumer instances/partitions, not intra-instance parallelism.
|
|
56
|
+
- **Not a supervisor.** On handler failure, the configured `ErrorPolicy` runs (default `SkipAndLogPolicy`, or `DeadLetterPolicy`) and the loop continues — the component itself does not retry, crash-loop, or restart. Health is exposed via `routes()`; process supervision is the caller's job.
|
|
57
|
+
- **JSON value-only.** Message values are `json.dumps`/`json.loads`. Keys and headers pass through untouched.
|
|
58
|
+
- **Graceful shutdown drains the in-flight message.** `shutdown()` waits for the currently-processing message's handler (and its `ErrorPolicy`, if it fails) to finish before stopping — it does not abandon work mid-message.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv sync --all-groups
|
|
64
|
+
uv run pytest
|
|
65
|
+
uv run ruff format --check .
|
|
66
|
+
uv run ruff check .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Integration tests spin up a real Kafka broker via [testcontainers](https://testcontainers.com/modules/kafka/). For local development against a persistent broker instead, run `docker compose up -d`.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
services:
|
|
2
|
+
kafka:
|
|
3
|
+
image: apache/kafka:latest
|
|
4
|
+
ports:
|
|
5
|
+
- "9092:9092"
|
|
6
|
+
environment:
|
|
7
|
+
KAFKA_NODE_ID: 1
|
|
8
|
+
KAFKA_PROCESS_ROLES: broker,controller
|
|
9
|
+
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
|
|
10
|
+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
|
|
11
|
+
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
|
12
|
+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
|
|
13
|
+
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
|
|
14
|
+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# aiokafka client with explicit delivery semantics
|
|
6
|
+
|
|
7
|
+
kafka-component needs a Kafka client that fits the family's async-first, no-supervisor stance (components expose health, they don't self-heal). We chose **aiokafka** over confluent-kafka because confluent's asyncio support wraps a threaded producer/consumer rather than being asyncio-native, which fights the async-first design; we accept aiokafka's lower raw throughput and community (not enterprise-SLA) maintenance pace as the trade-off.
|
|
8
|
+
|
|
9
|
+
On top of that, `KafkaConsumerComponent` commits offsets manually — only after the handler succeeds (at-least-once, no auto-commit) — and `KafkaProducerComponent` defaults to wait-for-ack (`acks=all`) rather than fire-and-forget. Per-message failure handling is pluggable via `ErrorPolicy` (default `SkipAndLogPolicy`, optional `DeadLetterPolicy`) instead of a built-in retry loop, keeping the component from silently losing or endlessly retrying messages.
|
|
10
|
+
|
|
11
|
+
## Considered Options
|
|
12
|
+
|
|
13
|
+
- confluent-kafka (rejected: threaded async wrapper, not asyncio-native)
|
|
14
|
+
- auto-commit consumer (rejected: risks silent data loss on crash mid-processing)
|
|
15
|
+
- fire-and-forget producer, `acks=0`/`1` (rejected as default: conflicts with family's explicit-failure stance; could be exposed later as opt-in)
|
|
16
|
+
- built-in retry-N policy (rejected for v1: real design surface — backoff, ordering guarantees — punted; pluggable `ErrorPolicy` leaves room to add without breaking the API)
|