1+ __all__ = ("AioKafkaBroker" ,)
2+
13import asyncio
2- from collections .abc import AsyncGenerator , Callable
4+ from collections .abc import AsyncGenerator , Callable , Iterable
35from logging import getLogger
4- from typing import Any , TypeVar
6+ from typing import Any , TypeVar , overload
57
68from aiokafka import AIOKafkaConsumer , AIOKafkaProducer
79from kafka .admin import KafkaAdminClient , NewTopic
810from kafka .coordinator .assignors .roundrobin import RoundRobinPartitionAssignor
911from kafka .partitioner .default import DefaultPartitioner
1012from taskiq import AsyncResultBackend , BrokerMessage
1113from taskiq .abc .broker import AsyncBroker
14+ from typing_extensions import ParamSpec
1215
13- from taskiq_aio_kafka .exceptions import WrongAioKafkaBrokerParametersError
14- from taskiq_aio_kafka .models import KafkaConsumerParameters , KafkaProducerParameters
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
1523
1624_T = TypeVar ("_T" )
25+ _FuncParams = ParamSpec ("_FuncParams" )
26+ _ReturnType = TypeVar ("_ReturnType" )
1727
1828
1929logger = getLogger ("taskiq.kafka_broker" )
2232class AioKafkaBroker (AsyncBroker ):
2333 """Broker that works with Kafka."""
2434
35+ task_topic_label = TASK_TOPIC_LABEL
36+
2537 def __init__ ( # noqa: PLR0913
2638 self ,
2739 bootstrap_servers : str | list [str ] | None ,
28- kafka_topic : NewTopic | None = None ,
40+ kafka_topic : TopicType | None = None ,
41+ kafka_topics : Iterable [TopicType ] | None = None ,
2942 result_backend : AsyncResultBackend [_T ] | None = None ,
3043 task_id_generator : Callable [[], str ] | None = None ,
3144 kafka_admin_client : KafkaAdminClient | None = None ,
@@ -35,7 +48,8 @@ def __init__( # noqa: PLR0913
3548 """Construct a new broker.
3649
3750 :param bootstrap_servers: string with url to kafka or list with urls.
38- :param kafka_topic: kafka topic.
51+ :param kafka_topic: default kafka topic.
52+ :param kafka_topics: all kafka topics to listen.
3953 :param result_backend: custom result backend.
4054 :param task_id_generator: custom task_id generator.
4155 :param kafka_admin_client: configured KafkaAdminClient.
@@ -46,6 +60,7 @@ def __init__( # noqa: PLR0913
4660 aiokafka_consumer were specified but bootstrap_servers wasn't specified.
4761 """
4862 super ().__init__ (result_backend , task_id_generator )
63+ self .decorator_class = AioKafkaDecoratedTask
4964
5065 if kafka_admin_client and not bootstrap_servers :
5166 raise WrongAioKafkaBrokerParametersError
@@ -54,11 +69,16 @@ def __init__( # noqa: PLR0913
5469
5570 self ._loop : asyncio .AbstractEventLoop | None = loop
5671
57- self ._kafka_topic : NewTopic = kafka_topic or NewTopic (
58- name = "taskiq_topic" ,
59- num_partitions = 1 ,
60- replication_factor = 1 ,
61- )
72+ self ._kafka_topic : NewTopic = self ._normalize_default_topic (kafka_topic )
73+ self ._kafka_topics : dict [str , TopicType ] = {
74+ self ._kafka_topic .name : self ._kafka_topic ,
75+ }
76+ if kafka_topics is not None :
77+ for topic in kafka_topics :
78+ self ._kafka_topics .setdefault (
79+ get_topic_name (topic ),
80+ topic ,
81+ )
6282
6383 self ._aiokafka_producer_params : KafkaProducerParameters = (
6484 KafkaProducerParameters ()
@@ -83,6 +103,44 @@ def __init__( # noqa: PLR0913
83103 self ._is_producer_started = False
84104 self ._is_consumer_started = False
85105
106+ @classmethod
107+ def _normalize_default_topic (
108+ cls ,
109+ kafka_topic : TopicType | None ,
110+ ) -> NewTopic :
111+ if kafka_topic is None :
112+ return NewTopic (
113+ name = "taskiq_topic" ,
114+ num_partitions = 1 ,
115+ replication_factor = 1 ,
116+ )
117+ if isinstance (kafka_topic , str ):
118+ return NewTopic (
119+ name = kafka_topic ,
120+ num_partitions = 1 ,
121+ replication_factor = 1 ,
122+ )
123+ if isinstance (kafka_topic , Topic ):
124+ if kafka_topic .topic_config .declare :
125+ return kafka_topic .new_topic ()
126+ return NewTopic (
127+ name = kafka_topic .name ,
128+ num_partitions = 1 ,
129+ replication_factor = 1 ,
130+ )
131+ return kafka_topic
132+
133+ @classmethod
134+ def _get_declaration_topic (
135+ cls ,
136+ topic : TopicType ,
137+ ) -> NewTopic | None :
138+ if isinstance (topic , NewTopic ):
139+ return topic
140+ if isinstance (topic , Topic ) and topic .topic_config .declare :
141+ return topic .new_topic ()
142+ return None
143+
86144 def configure_producer (self , ** producer_parameters : Any ) -> None :
87145 """Configure kafka producer.
88146
@@ -107,21 +165,93 @@ def configure_consumer(self, **consumer_parameters: Any) -> None:
107165 ** consumer_parameters ,
108166 )
109167
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 (
219+ self ,
220+ topic : TopicType ,
221+ task_name : str | Callable [..., Any ] | None = None ,
222+ ** labels : Any ,
223+ ) -> Any :
224+ """Decorate function and bind it to a kafka topic by default."""
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 )
231+
232+ return super ().task (
233+ task_name = task_name ,
234+ ** labels ,
235+ )
236+
110237 async def startup (self ) -> None :
111238 """Setup AIOKafkaProducer, AIOKafkaConsumer and kafka topics.
112239
113- We will have 2 topics for default and high priority.
114-
115240 Also we need to create AIOKafkaProducer and AIOKafkaConsumer
116241 if there are no producer and consumer passed.
117242 """
118243 await super ().startup ()
119- available_condition : bool = (
120- self ._kafka_topic .name not in self ._kafka_admin_client .list_topics ()
121- )
122- if available_condition :
244+ existed_topic_names = set (self ._kafka_admin_client .list_topics ())
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+
252+ if new_topics :
123253 self ._kafka_admin_client .create_topics (
124- new_topics = [ self . _kafka_topic ] ,
254+ new_topics = new_topics ,
125255 validate_only = False ,
126256 )
127257
@@ -145,7 +275,7 @@ async def startup(self) -> None:
145275 partition_assignment_strategy
146276 )
147277 self ._aiokafka_consumer = AIOKafkaConsumer (
148- self ._kafka_topic . name ,
278+ * self ._kafka_topics ,
149279 bootstrap_servers = self ._bootstrap_servers ,
150280 loop = self ._loop ,
151281 ** consumer_kwargs ,
@@ -166,18 +296,16 @@ async def shutdown(self) -> None:
166296 if self ._is_consumer_started :
167297 await self ._aiokafka_consumer .stop ()
168298
169- topic_delete_condition : bool = all (
170- (
171- self ._delete_topic_on_shutdown ,
172- self ._kafka_topic .name in self ._kafka_admin_client .list_topics (),
173- ),
174- )
175-
176299 if self ._kafka_admin_client :
177- if topic_delete_condition :
178- self ._kafka_admin_client .delete_topics (
179- [self ._kafka_topic .name ],
180- )
300+ if self ._delete_topic_on_shutdown :
301+ existed_topic_names = set (self ._kafka_admin_client .list_topics ())
302+ topic_names = [
303+ topic_name
304+ for topic_name in self ._kafka_topics
305+ if topic_name in existed_topic_names
306+ ]
307+ if topic_names :
308+ self ._kafka_admin_client .delete_topics (topic_names )
181309 self ._kafka_admin_client .close ()
182310
183311 async def kick (self , message : BrokerMessage ) -> None :
@@ -194,7 +322,10 @@ async def kick(self, message: BrokerMessage) -> None:
194322 if not self ._is_producer_started :
195323 raise ValueError ("Please run startup before kicking." )
196324
197- topic_name : str = self ._kafka_topic .name
325+ topic_name : str = message .labels .get (
326+ self .task_topic_label ,
327+ self ._kafka_topic .name ,
328+ )
198329
199330 await self ._aiokafka_producer .send_and_wait (
200331 topic = topic_name ,
0 commit comments