Skip to content

Commit 20fbd5e

Browse files
committed
decomposition: 1 class 1 file
update: task_with_topic interface update: tests
1 parent 54886ab commit 20fbd5e

10 files changed

Lines changed: 234 additions & 75 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ broker = AioKafkaBroker(
5555
)
5656

5757

58-
@broker.task(topic="emails")
58+
@broker.task_with_topic("emails")
5959
async def send_email(user_id: int) -> None:
6060
print(f"Send email to {user_id}")
6161

6262

63-
@broker.task(topic="reports")
63+
@broker.task_with_topic("reports")
6464
async def build_report(report_id: int) -> None:
6565
print(f"Build report {report_id}")
6666
```
@@ -76,6 +76,7 @@ await send_email.kicker().with_topic("reports").kiq(user_id=1)
7676
```
7777

7878
Tasks without a custom topic keep the old behavior and are sent to `kafka_topic`.
79+
The regular `@broker.task` decorator keeps the standard taskiq labels behavior.
7980

8081
```python
8182
@broker.task

taskiq_aio_kafka/broker.py

Lines changed: 75 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,32 @@
33
import asyncio
44
from collections.abc import AsyncGenerator, Callable, Iterable
55
from logging import getLogger
6-
from typing import Any, TypeAlias, TypeVar
6+
from typing import Any, TypeVar, overload
77

88
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
99
from kafka.admin import KafkaAdminClient, NewTopic
1010
from kafka.coordinator.assignors.roundrobin import RoundRobinPartitionAssignor
1111
from kafka.partitioner.default import DefaultPartitioner
1212
from taskiq import AsyncResultBackend, BrokerMessage
1313
from taskiq.abc.broker import AsyncBroker
14-
from taskiq.decor import AsyncTaskiqDecoratedTask
15-
from taskiq.kicker import AsyncKicker
1614
from typing_extensions import ParamSpec
1715

18-
from taskiq_aio_kafka.exceptions import WrongAioKafkaBrokerParametersError
19-
from taskiq_aio_kafka.models import KafkaConsumerParameters, KafkaProducerParameters
20-
from taskiq_aio_kafka.topic import Topic
16+
from .constants import TASK_TOPIC_LABEL
17+
from .decorated_task import AioKafkaDecoratedTask
18+
from .exceptions import WrongAioKafkaBrokerParametersError
19+
from .models import KafkaConsumerParameters, KafkaProducerParameters
20+
from .topic import Topic
21+
from .types import TopicType
22+
from .utils import get_topic_name
2123

2224
_T = TypeVar("_T")
2325
_FuncParams = ParamSpec("_FuncParams")
2426
_ReturnType = TypeVar("_ReturnType")
25-
TopicType: TypeAlias = str | NewTopic | Topic
26-
TASK_TOPIC_LABEL = "taskiq_aio_kafka_topic"
2727

2828

2929
logger = getLogger("taskiq.kafka_broker")
3030

3131

32-
def _get_topic_name(topic: TopicType) -> str:
33-
if isinstance(topic, str):
34-
return topic
35-
return topic.name
36-
37-
38-
class AioKafkaKicker(AsyncKicker[_FuncParams, _ReturnType]):
39-
"""Kicker that can override kafka topic for a task call."""
40-
41-
def with_topic(
42-
self,
43-
topic: TopicType,
44-
) -> "AioKafkaKicker[_FuncParams, _ReturnType]":
45-
"""Set kafka topic for current kick."""
46-
self.labels = {
47-
**self.labels,
48-
TASK_TOPIC_LABEL: _get_topic_name(topic),
49-
}
50-
return self
51-
52-
53-
class AioKafkaDecoratedTask(AsyncTaskiqDecoratedTask[_FuncParams, _ReturnType]):
54-
"""Taskiq decorated task with kafka-specific kicker."""
55-
56-
def kicker(self) -> AioKafkaKicker[_FuncParams, _ReturnType]:
57-
"""Return kafka-aware kicker."""
58-
return AioKafkaKicker(
59-
task_name=self.task_name,
60-
broker=self.broker,
61-
labels=self.labels,
62-
return_type=self.return_type,
63-
)
64-
65-
6632
class AioKafkaBroker(AsyncBroker):
6733
"""Broker that works with Kafka."""
6834

@@ -110,7 +76,7 @@ def __init__( # noqa: PLR0913
11076
if kafka_topics is not None:
11177
for topic in kafka_topics:
11278
self._kafka_topics.setdefault(
113-
self._get_topic_name(topic),
79+
get_topic_name(topic),
11480
topic,
11581
)
11682

@@ -137,10 +103,6 @@ def __init__( # noqa: PLR0913
137103
self._is_producer_started = False
138104
self._is_consumer_started = False
139105

140-
@staticmethod
141-
def _get_topic_name(topic: TopicType) -> str:
142-
return _get_topic_name(topic)
143-
144106
@classmethod
145107
def _normalize_default_topic(
146108
cls,
@@ -203,21 +165,72 @@ def configure_consumer(self, **consumer_parameters: Any) -> None:
203165
**consumer_parameters,
204166
)
205167

206-
def task( # type: ignore[override]
168+
@overload
169+
def task(
170+
self,
171+
task_name: Callable[_FuncParams, _ReturnType],
172+
**labels: Any,
173+
) -> AioKafkaDecoratedTask[_FuncParams, _ReturnType]: ...
174+
175+
@overload
176+
def task(
177+
self,
178+
task_name: str | None = None,
179+
**labels: Any,
180+
) -> Callable[
181+
[Callable[_FuncParams, _ReturnType]],
182+
AioKafkaDecoratedTask[_FuncParams, _ReturnType],
183+
]: ...
184+
185+
def task(
186+
self,
187+
task_name: str | Callable[..., Any] | None = None,
188+
**labels: Any,
189+
) -> Any:
190+
"""Decorate function."""
191+
if callable(task_name):
192+
return super().task(task_name, **labels)
193+
194+
return super().task(
195+
task_name=task_name,
196+
**labels,
197+
)
198+
199+
@overload
200+
def task_with_topic(
201+
self,
202+
topic: TopicType,
203+
task_name: Callable[_FuncParams, _ReturnType],
204+
**labels: Any,
205+
) -> AioKafkaDecoratedTask[_FuncParams, _ReturnType]: ...
206+
207+
@overload
208+
def task_with_topic(
209+
self,
210+
topic: TopicType,
211+
task_name: str | None = None,
212+
**labels: Any,
213+
) -> Callable[
214+
[Callable[_FuncParams, _ReturnType]],
215+
AioKafkaDecoratedTask[_FuncParams, _ReturnType],
216+
]: ...
217+
218+
def task_with_topic(
207219
self,
220+
topic: TopicType,
208221
task_name: str | Callable[..., Any] | None = None,
209-
*,
210-
topic: TopicType | None = None,
211222
**labels: Any,
212223
) -> Any:
213224
"""Decorate function and bind it to a kafka topic by default."""
214-
if topic is not None:
215-
topic_name = self._get_topic_name(topic)
216-
self._kafka_topics.setdefault(topic_name, topic)
217-
labels[self.task_topic_label] = topic_name
225+
topic_name = get_topic_name(topic)
226+
self._kafka_topics.setdefault(topic_name, topic)
227+
labels[self.task_topic_label] = topic_name
228+
229+
if callable(task_name):
230+
return super().task(task_name, **labels)
218231

219232
return super().task(
220-
task_name=task_name, # type: ignore[arg-type]
233+
task_name=task_name,
221234
**labels,
222235
)
223236

@@ -229,12 +242,13 @@ async def startup(self) -> None:
229242
"""
230243
await super().startup()
231244
existed_topic_names = set(self._kafka_admin_client.list_topics())
232-
new_topics = [
233-
new_topic
234-
for topic in self._kafka_topics.values()
235-
if (new_topic := self._get_declaration_topic(topic)) is not None
236-
and new_topic.name not in existed_topic_names
237-
]
245+
246+
new_topics = []
247+
for topic in self._kafka_topics.values():
248+
new_topic = self._get_declaration_topic(topic)
249+
if new_topic is not None and new_topic.name not in existed_topic_names:
250+
new_topics.append(new_topic)
251+
238252
if new_topics:
239253
self._kafka_admin_client.create_topics(
240254
new_topics=new_topics,

taskiq_aio_kafka/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__all__ = ("TASK_TOPIC_LABEL",)
2+
3+
TASK_TOPIC_LABEL = "taskiq_aio_kafka_topic"

taskiq_aio_kafka/decorated_task.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
__all__ = ("AioKafkaDecoratedTask",)
2+
3+
from typing import TypeVar
4+
5+
from taskiq.decor import AsyncTaskiqDecoratedTask
6+
from typing_extensions import ParamSpec
7+
8+
from .kicker import AioKafkaKicker
9+
10+
_FuncParams = ParamSpec("_FuncParams")
11+
_ReturnType = TypeVar("_ReturnType")
12+
13+
14+
class AioKafkaDecoratedTask(AsyncTaskiqDecoratedTask[_FuncParams, _ReturnType]):
15+
"""Taskiq decorated task with kafka-specific kicker."""
16+
17+
def kicker(self) -> AioKafkaKicker[_FuncParams, _ReturnType]:
18+
"""Return kafka-aware kicker."""
19+
return AioKafkaKicker(
20+
task_name=self.task_name,
21+
broker=self.broker,
22+
labels=self.labels,
23+
return_type=self.return_type,
24+
)

taskiq_aio_kafka/kicker.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
__all__ = ("AioKafkaKicker",)
2+
3+
from typing import TypeVar
4+
5+
from taskiq.kicker import AsyncKicker
6+
from typing_extensions import ParamSpec
7+
8+
from .constants import TASK_TOPIC_LABEL
9+
from .types import TopicType
10+
from .utils import get_topic_name
11+
12+
_FuncParams = ParamSpec("_FuncParams")
13+
_ReturnType = TypeVar("_ReturnType")
14+
15+
16+
class AioKafkaKicker(AsyncKicker[_FuncParams, _ReturnType]):
17+
"""Kicker that can override kafka topic for a task call."""
18+
19+
def with_topic(
20+
self,
21+
topic: TopicType,
22+
) -> "AioKafkaKicker[_FuncParams, _ReturnType]":
23+
"""Set kafka topic for current kick."""
24+
self.labels = {
25+
**self.labels,
26+
TASK_TOPIC_LABEL: get_topic_name(topic),
27+
}
28+
return self

taskiq_aio_kafka/topic.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,7 @@
77

88
from kafka.admin import NewTopic
99

10-
11-
@dataclasses.dataclass
12-
class TopicConfig:
13-
"""Kafka topic declaration settings."""
14-
15-
declare: bool = False
16-
num_partitions: int = 1
17-
replication_factor: int = 1
18-
replica_assignments: dict[int, list[int]] = dataclasses.field(default_factory=dict)
19-
topic_configs: dict[str, str] = dataclasses.field(default_factory=dict)
10+
from .topic_config import TopicConfig
2011

2112

2213
@dataclasses.dataclass

taskiq_aio_kafka/topic_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
__all__ = ("TopicConfig",)
2+
3+
import dataclasses
4+
5+
6+
@dataclasses.dataclass
7+
class TopicConfig:
8+
"""Kafka topic declaration settings."""
9+
10+
declare: bool = False
11+
num_partitions: int = 1
12+
replication_factor: int = 1
13+
replica_assignments: dict[int, list[int]] = dataclasses.field(default_factory=dict)
14+
topic_configs: dict[str, str] = dataclasses.field(default_factory=dict)

taskiq_aio_kafka/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
__all__ = ("TopicType",)
2+
3+
from typing import TypeAlias
4+
5+
from kafka.admin import NewTopic
6+
7+
from .topic import Topic
8+
9+
TopicType: TypeAlias = str | NewTopic | Topic

taskiq_aio_kafka/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
__all__ = ("get_topic_name",)
2+
3+
from .types import TopicType
4+
5+
6+
def get_topic_name(topic: TopicType) -> str:
7+
"""Get kafka topic name."""
8+
if isinstance(topic, str):
9+
return topic
10+
return topic.name

0 commit comments

Comments
 (0)