Type-safe async Kafka SDK for Python microservices in the Series platform
Project description
Series Kafka SDK - Python
Type-safe async event streaming for Python microservices in the Series platform.
Features
- ✅ Async/Await - Built on aiokafka for non-blocking I/O
- ✅ Type Safety - Pydantic models with full validation
- ✅ Idempotent - Exactly-once delivery by default
- ✅ Auto DLQ - Per-topic dead letter queues with metadata tracking
- ✅ Observability - OpenTelemetry tracing and metrics (optional)
- ✅ Plugin System - Extensible topic architecture
- ✅ Schema Validation - JSON Schema with Pydantic
- ✅ Subset Fields - Extract only needed fields for performance
- ✅ Retry Logic - Exponential backoff for transient errors
- ✅ Graceful Shutdown - Proper offset commits with timeout
Installation
pip install series-kafka
Or with Poetry:
poetry add series-kafka
Quick Start
Producer
import asyncio
from datetime import datetime, timezone
from series_kafka import AsyncProducer, ProducerConfig
from series_kafka_topics.users import UsersTopic, UserCreatedPayload
async def main():
# Configure producer
config = ProducerConfig(
bootstrap_servers="localhost:9092",
service_name="my-service",
security_protocol="PLAINTEXT" # Use SASL_SSL for production
)
# Create and start producer
producer = AsyncProducer(config)
await producer.start()
try:
# Create payload
payload = UserCreatedPayload(
user_id="usr_123",
email="user@example.com",
username="johndoe",
created_at=datetime.now(timezone.utc)
)
# Produce event
event_id = await producer.produce(
topic=UsersTopic(),
payload=payload,
event_type="user.created"
)
print(f"Event produced: {event_id}")
finally:
await producer.stop()
asyncio.run(main())
Consumer
import asyncio
from series_kafka import AsyncConsumer, ConsumerConfig, BaseMessage
from series_kafka_topics.users import UsersTopic
async def handle_message(message: BaseMessage):
"""Process incoming message."""
print(f"Received: {message.event_type}")
print(f"User ID: {message.payload.user_id}")
print(f"Email: {message.payload.email}")
async def main():
# Configure consumer
config = ConsumerConfig(
bootstrap_servers="localhost:9092",
group_id="my-consumer-group",
service_name="my-service",
security_protocol="PLAINTEXT",
enable_dlq=True, # Enable dead letter queue
max_retries=3
)
# Create and start consumer
consumer = AsyncConsumer(
config=config,
topics=[UsersTopic()],
handler=handle_message
)
await consumer.start()
await consumer.consume() # Runs until stopped
asyncio.run(main())
Advanced Features
Subset Field Extraction
Extract only specific fields for performance and security:
config = ConsumerConfig(
bootstrap_servers="localhost:9092",
group_id="my-consumer",
service_name="my-service",
security_protocol="PLAINTEXT",
# Extract only needed fields
subset_fields=[
"event_id",
"payload.user_id",
"payload.username"
]
)
async def handle_fields(fields: dict):
"""Handler receives only extracted fields."""
print(f"User ID: {fields['user_id']}")
print(f"Username: {fields['username']}")
consumer = AsyncConsumer(config, [UsersTopic()], handle_fields)
Error Handling with DLQ
from series_kafka.exceptions import RetryableError, ValidationError, FatalError
async def handle_message(message: BaseMessage):
try:
# Process message
result = await process_data(message.payload)
except NetworkError as e:
# Transient errors will be retried with exponential backoff
raise RetryableError(f"Network issue: {e}")
except InvalidDataError as e:
# Validation errors go directly to DLQ (no retry)
raise ValidationError(f"Bad data: {e}")
except CriticalError as e:
# Fatal errors go directly to DLQ
raise FatalError(f"Critical failure: {e}")
Event Type Filtering
config = ConsumerConfig(
bootstrap_servers="localhost:9092",
group_id="my-consumer",
service_name="my-service",
security_protocol="PLAINTEXT",
# Only process specific event types
event_type_filter={"user.created", "user.updated"}
)
Idempotent Producer
Idempotency is enabled by default for exactly-once delivery:
config = ProducerConfig(
bootstrap_servers="localhost:9092",
service_name="my-service",
security_protocol="PLAINTEXT",
enable_idempotence=True # Default: True
)
Configuration
Producer Configuration
from series_kafka import ProducerConfig
config = ProducerConfig(
# Connection (required)
bootstrap_servers="localhost:9092",
service_name="my-service",
# Authentication (optional)
sasl_username="your-api-key",
sasl_password="your-api-secret",
sasl_mechanism="PLAIN", # PLAIN, SCRAM-SHA-256, SCRAM-SHA-512
security_protocol="SASL_SSL", # PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL
# Idempotency (default: True)
enable_idempotence=True,
# Performance tuning
compression_type="gzip", # none, gzip, snappy, lz4, zstd
batch_size=16384,
linger_ms=10,
# Observability (default: False)
enable_metrics=False,
enable_tracing=False,
)
Consumer Configuration
from series_kafka import ConsumerConfig
config = ConsumerConfig(
# Connection (required)
bootstrap_servers="localhost:9092",
group_id="my-consumer-group",
service_name="my-service",
# Authentication (optional)
sasl_username="your-api-key",
sasl_password="your-api-secret",
# DLQ settings
enable_dlq=True,
max_retries=3,
retry_backoff_ms=1000,
# Event filtering
event_type_filter={"user.created", "user.updated"},
# Subset field extraction
subset_fields=["payload.user_id", "payload.username"],
# Consumer settings
auto_offset_reset="earliest", # earliest or latest
shutdown_timeout_seconds=30,
)
Topic Plugins
The SDK includes built-in topic plugins for Series platform:
- MessagesTopic - Communication events (SendBlue, Linq, Agent responses)
- UsersTopic - User lifecycle events (created, updated, deleted)
- PostsTopic - Content lifecycle events (created, updated, deleted)
Creating Custom Topics
from series_kafka import Topic, BasePayload
from series_kafka.contracts.base import TopicContract
from pydantic import Field
# Define payload
class ProductCreatedPayload(BasePayload):
product_id: str = Field(..., pattern=r"^prod_")
name: str
price: float
# Implement topic plugin
class ProductsTopic(Topic):
@property
def name(self) -> str:
return "products"
@property
def schema_registry(self) -> dict[str, type[BasePayload]]:
return {
"product.created": ProductCreatedPayload,
}
def get_partition_key(self, message, event_type) -> str | None:
return message.payload.product_id
def validate_message(self, message) -> bool:
return message.payload.price > 0
def get_contract(self) -> TopicContract:
return TopicContract(
name=self.name,
version="1.0.0",
event_types={"product.created": {"type": "object"}}
)
Testing
Local Development
Start local Kafka with Docker Compose:
docker-compose up -d
Run tests:
# Unit tests
pytest tests/unit/ -v
# Integration tests (requires local Kafka)
pytest tests/integration/ -v
# All tests with coverage
pytest --cov=series_kafka --cov=series_kafka_topics
Test Results
- ✅ 154 tests passing
- ✅ 81.15% code coverage
- ✅ Zero bugs
- ✅ All E2E tests validated against real Kafka
Development
Setup
git clone https://github.com/series/kafka-sdk-python
cd kafka-sdk-python
poetry install
Code Quality
# Format code
black series_kafka series_kafka_topics tests
# Lint code
ruff check series_kafka series_kafka_topics tests
# Type check
mypy series_kafka
Architecture
series_kafka/
├── core/ # Core SDK components
│ ├── producer.py # AsyncProducer
│ ├── consumer.py # AsyncConsumer
│ ├── message.py # BaseMessage, BasePayload
│ ├── topic.py # Topic ABC
│ ├── config.py # Configuration models
│ ├── dlq.py # DLQ handler
│ └── field_extractor.py # Subset field extraction
├── schema/ # Validation & serialization
│ ├── validator.py # Schema validation
│ └── serializer.py # Message serialization
├── contracts/ # Data contracts
│ └── base.py # TopicContract
└── exceptions.py # Exception hierarchy
series_kafka_topics/
├── messages/ # Messages topic plugin
├── users/ # Users topic plugin
└── posts/ # Posts topic plugin
Error Handling
The SDK provides three error types for proper DLQ routing:
RetryableError
Transient errors that should be retried with exponential backoff:
from series_kafka.exceptions import RetryableError
# Consumer will retry up to max_retries times
raise RetryableError("Temporary network issue")
ValidationError
Data validation errors that go directly to DLQ (no retry):
from series_kafka.exceptions import ValidationError
# Goes to DLQ immediately
raise ValidationError("Invalid email format")
FatalError
Unrecoverable errors that go directly to DLQ:
from series_kafka.exceptions import FatalError
# Goes to DLQ immediately
raise FatalError("Authentication failed")
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests:
pytest - Format code:
black . - Lint:
ruff check . - Submit pull request
License
MIT License - See LICENSE file for details.
Support
For issues and questions:
- GitHub Issues: https://github.com/series/kafka-sdk-python/issues
- Documentation: https://docs.series.io/kafka-sdk-python
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 series_kafka-1.0.1.tar.gz.
File metadata
- Download URL: series_kafka-1.0.1.tar.gz
- Upload date:
- Size: 25.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6fff725f8beee470920e3ead314fa569d5c0b23f7864bfb4f1511b24f03c78b
|
|
| MD5 |
ae216cf38aab7228f117c24c4185ed23
|
|
| BLAKE2b-256 |
d0455aafb0345e84118ec8f5266ce78133b16cd0ea96f568aeeda7e2e0aa3998
|
File details
Details for the file series_kafka-1.0.1-py3-none-any.whl.
File metadata
- Download URL: series_kafka-1.0.1-py3-none-any.whl
- Upload date:
- Size: 31.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19275da4dc7a384d9cd1291b18fc90c7674f1e2b5e4a28f33e064ebd06b2f9de
|
|
| MD5 |
cc5c90b7a2319b7451fb1f2671074894
|
|
| BLAKE2b-256 |
dad6d843a707cc2a0f85ffb82a7c6daac426ecbd7ca8ad0cf2da72d6c647a2c9
|