fastkafka2 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.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastkafka2
3
+ Version: 0.1.0
4
+ Summary: Next-generation FastAPI-like DX for Kafka (version 2)
5
+ Author: Ruslan Ramazanov
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: pydantic>=2.6
9
+ Requires-Dist: anyio>=4
10
+ Requires-Dist: aiokafka>=0.10
11
+ Requires-Dist: typing-extensions>=4.8
12
+
13
+ # fastkafka2
14
+
15
+ Next-generation FastAPI-like DX for Kafka (version 2).
16
+
17
+ ## Installation
18
+
19
+ ``` bash
20
+ pip install fastkafka2
21
+ ```
22
+
23
+ ## Use example
24
+ ### Project arch
25
+ ```shell
26
+ ├── api/
27
+ │ ├── kafka/
28
+ │ │ ├── handlers/
29
+ │ │ │ ├── example/
30
+ │ │ │ │ ├─ schemas.py
31
+ │ │ │ │ └─ handler.py
32
+ │ │ │ └── base_handler.py
33
+ │ │ └── lifespan.py
34
+
35
+ ├── main.py
36
+ ```
37
+
38
+ ### Schemas
39
+ ``` python
40
+ # api\kafka\handlers\example\schemas.py
41
+ from pydantic import BaseModel
42
+
43
+
44
+ class ExampleSchema(BaseModel):
45
+ msg: str
46
+ ```
47
+
48
+ ### Handler
49
+ ``` python
50
+ # api\kafka\handlers\example\handler.py
51
+ import logging
52
+
53
+ from fastkafka.handler import KafkaHandler
54
+ from fastkafka.message import KafkaMessage
55
+ from fastkafka.producer import KafkaProducer
56
+
57
+
58
+ handler = KafkaHandler()
59
+
60
+ kafka_producer = KafkaProducer(bootstrap_servers="127.0.0.1:9092")
61
+
62
+
63
+ @handler("example")
64
+ async def example_handler(message: KafkaMessage):
65
+ t = int(message.headers.get("try")) + 1
66
+ logging.info(f"Пришло: {message}")
67
+ await kafka_producer.send_message(
68
+ topic="example-2", data={"msg": "wddwd"}, headers={"try": f"{t}"}, key=None
69
+ )
70
+ logging.info(f"Отправил: {f'{t}'}")
71
+ ```
72
+
73
+
74
+ ### Grouping of handlers
75
+ ``` python
76
+ # api\kafka\handlers\base_handler.py
77
+ from api.kafka.handlers.example.handler import handler as example_handler
78
+
79
+ from fastkafka.handler import KafkaHandler
80
+
81
+ base_handler = KafkaHandler()
82
+
83
+ base_handler.include_handler(example_handler)
84
+ ```
85
+
86
+
87
+ ### Lifespan fastkafka app
88
+ ``` python
89
+ # api/kafka/lifespan.py
90
+ import logging
91
+ from contextlib import asynccontextmanager
92
+ from fastkafka.app import KafkaApp
93
+ from api.kafka.handlers.base_handler import base_handler
94
+
95
+ from api.kafka.handlers.example.handler import kafka_producer
96
+
97
+
98
+ @asynccontextmanager
99
+ async def lifespan(app: KafkaApp):
100
+ logging.info("Lifespan: запуск")
101
+ try:
102
+ await kafka_producer.start()
103
+ yield
104
+ logging.info("Lifespan: выполнен")
105
+ finally:
106
+ await kafka_producer.stop()
107
+ logging.info("Lifespan: остановка")
108
+
109
+
110
+ app = KafkaApp(
111
+ title="Kafka Gateway",
112
+ description="Kafka-based microservice",
113
+ bootstrap_servers="127.0.0.1:9092",
114
+ lifespan=lifespan,
115
+ )
116
+
117
+ app.include_handler(base_handler)
118
+ ```
119
+
120
+
121
+ ### Entry point main app
122
+ ``` python
123
+ # main.py
124
+ import asyncio
125
+ from logging_config import setup_logging
126
+ from api.kafka.lifespan import app
127
+
128
+ if __name__ == "__main__":
129
+ setup_logging()
130
+ asyncio.run(app.run())
131
+ ```
132
+
133
+
@@ -0,0 +1,121 @@
1
+ # fastkafka2
2
+
3
+ Next-generation FastAPI-like DX for Kafka (version 2).
4
+
5
+ ## Installation
6
+
7
+ ``` bash
8
+ pip install fastkafka2
9
+ ```
10
+
11
+ ## Use example
12
+ ### Project arch
13
+ ```shell
14
+ ├── api/
15
+ │ ├── kafka/
16
+ │ │ ├── handlers/
17
+ │ │ │ ├── example/
18
+ │ │ │ │ ├─ schemas.py
19
+ │ │ │ │ └─ handler.py
20
+ │ │ │ └── base_handler.py
21
+ │ │ └── lifespan.py
22
+
23
+ ├── main.py
24
+ ```
25
+
26
+ ### Schemas
27
+ ``` python
28
+ # api\kafka\handlers\example\schemas.py
29
+ from pydantic import BaseModel
30
+
31
+
32
+ class ExampleSchema(BaseModel):
33
+ msg: str
34
+ ```
35
+
36
+ ### Handler
37
+ ``` python
38
+ # api\kafka\handlers\example\handler.py
39
+ import logging
40
+
41
+ from fastkafka.handler import KafkaHandler
42
+ from fastkafka.message import KafkaMessage
43
+ from fastkafka.producer import KafkaProducer
44
+
45
+
46
+ handler = KafkaHandler()
47
+
48
+ kafka_producer = KafkaProducer(bootstrap_servers="127.0.0.1:9092")
49
+
50
+
51
+ @handler("example")
52
+ async def example_handler(message: KafkaMessage):
53
+ t = int(message.headers.get("try")) + 1
54
+ logging.info(f"Пришло: {message}")
55
+ await kafka_producer.send_message(
56
+ topic="example-2", data={"msg": "wddwd"}, headers={"try": f"{t}"}, key=None
57
+ )
58
+ logging.info(f"Отправил: {f'{t}'}")
59
+ ```
60
+
61
+
62
+ ### Grouping of handlers
63
+ ``` python
64
+ # api\kafka\handlers\base_handler.py
65
+ from api.kafka.handlers.example.handler import handler as example_handler
66
+
67
+ from fastkafka.handler import KafkaHandler
68
+
69
+ base_handler = KafkaHandler()
70
+
71
+ base_handler.include_handler(example_handler)
72
+ ```
73
+
74
+
75
+ ### Lifespan fastkafka app
76
+ ``` python
77
+ # api/kafka/lifespan.py
78
+ import logging
79
+ from contextlib import asynccontextmanager
80
+ from fastkafka.app import KafkaApp
81
+ from api.kafka.handlers.base_handler import base_handler
82
+
83
+ from api.kafka.handlers.example.handler import kafka_producer
84
+
85
+
86
+ @asynccontextmanager
87
+ async def lifespan(app: KafkaApp):
88
+ logging.info("Lifespan: запуск")
89
+ try:
90
+ await kafka_producer.start()
91
+ yield
92
+ logging.info("Lifespan: выполнен")
93
+ finally:
94
+ await kafka_producer.stop()
95
+ logging.info("Lifespan: остановка")
96
+
97
+
98
+ app = KafkaApp(
99
+ title="Kafka Gateway",
100
+ description="Kafka-based microservice",
101
+ bootstrap_servers="127.0.0.1:9092",
102
+ lifespan=lifespan,
103
+ )
104
+
105
+ app.include_handler(base_handler)
106
+ ```
107
+
108
+
109
+ ### Entry point main app
110
+ ``` python
111
+ # main.py
112
+ import asyncio
113
+ from logging_config import setup_logging
114
+ from api.kafka.lifespan import app
115
+
116
+ if __name__ == "__main__":
117
+ setup_logging()
118
+ asyncio.run(app.run())
119
+ ```
120
+
121
+
@@ -0,0 +1,19 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .logging_utils import suppress_external_logs
4
+ from .app import KafkaApp
5
+ from .handler import KafkaHandler
6
+ from .message import KafkaMessage
7
+ from .producer import KafkaProducer
8
+
9
+ suppress_external_logs()
10
+
11
+ __all__ = ["KafkaApp", "KafkaHandler", "KafkaMessage", "KafkaProducer"]
12
+
13
+ def __getattr__(name: str):
14
+ if name in __all__:
15
+ return globals()[name]
16
+ raise AttributeError(f"module {__name__!r} has no attribute {name}")
17
+
18
+ def __dir__():
19
+ return __all__ + [n for n in globals() if n.startswith("_")]
@@ -0,0 +1,47 @@
1
+ # fastkafka\admin.py
2
+ import logging
3
+ from aiokafka.admin import AIOKafkaAdminClient, NewTopic
4
+ from aiokafka.errors import TopicAlreadyExistsError
5
+ from .services.base import BaseKafkaService
6
+ from .services.retry import retry_on_connection
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class KafkaAdminService(BaseKafkaService):
12
+ def __init__(self, bootstrap_servers: str = "localhost:9092") -> None:
13
+ self._bootstrap = bootstrap_servers
14
+ self._client: AIOKafkaAdminClient | None = None
15
+
16
+ async def start(self) -> None:
17
+ await self._start()
18
+
19
+ async def stop(self) -> None:
20
+ await self._stop()
21
+
22
+ @retry_on_connection()
23
+ async def _start(self) -> None:
24
+ self._client = AIOKafkaAdminClient(bootstrap_servers=self._bootstrap)
25
+ await self._client.start()
26
+ logger.info("KafkaAdminService started")
27
+
28
+ async def create_topic(self, topic: str) -> None:
29
+ if not self._client:
30
+ raise RuntimeError("Admin client not initialized")
31
+ try:
32
+ await self._client.create_topics(
33
+ new_topics=[
34
+ NewTopic(name=topic, num_partitions=1, replication_factor=1)
35
+ ]
36
+ )
37
+ logger.info("Topic created: %s", topic)
38
+ except TopicAlreadyExistsError:
39
+ logger.debug("Topic already exists: %s", topic)
40
+ except Exception:
41
+ logger.exception("Error creating topic %s", topic)
42
+ raise
43
+
44
+ async def _stop(self) -> None:
45
+ if self._client:
46
+ await self._client.close()
47
+ logger.info("KafkaAdminService stopped")
@@ -0,0 +1,79 @@
1
+ # fastkafka\app.py
2
+ import asyncio
3
+ import logging
4
+ import signal
5
+ from typing import Callable
6
+ from contextlib import AbstractAsyncContextManager
7
+
8
+ from .producer import KafkaProducer
9
+ from .admin import KafkaAdminService
10
+ from .consumer import KafkaConsumerService
11
+ from .registry import handlers_registry
12
+ from .handler import KafkaHandler
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class KafkaApp:
18
+ def __init__(
19
+ self,
20
+ title: str,
21
+ description: str,
22
+ bootstrap_servers: str = "localhost:9092",
23
+ lifespan: Callable[["KafkaApp"], AbstractAsyncContextManager] | None = None,
24
+ ) -> None:
25
+ self.title = title
26
+ self.description = description
27
+ self.bootstrap = bootstrap_servers
28
+ self._lifespan = lifespan(self) if lifespan else None
29
+
30
+ self._producer = KafkaProducer(self.bootstrap)
31
+ self._admin = KafkaAdminService(self.bootstrap)
32
+ self._consumer: KafkaConsumerService | None = None
33
+ self._groups: list[KafkaHandler] = []
34
+
35
+ def include_handler(self, handler: KafkaHandler) -> None:
36
+ self._groups.append(handler)
37
+ logger.debug("Included handler group %s", handler.prefix)
38
+
39
+ async def start(self) -> None:
40
+ logger.info("Starting %s", self.title)
41
+ try:
42
+ if self._lifespan:
43
+ await self._lifespan.__aenter__()
44
+ await self._admin.start()
45
+ for topic in handlers_registry:
46
+ await self._admin.create_topic(topic)
47
+ self._consumer = KafkaConsumerService(
48
+ topics=list(handlers_registry), bootstrap_servers=self.bootstrap
49
+ )
50
+ await self._consumer.start()
51
+ logger.info("%s started", self.title)
52
+ except Exception:
53
+ logger.exception("KafkaApp.start failed")
54
+ raise
55
+
56
+ async def stop(self) -> None:
57
+ logger.info("Stopping %s", self.title)
58
+ try:
59
+ if self._consumer:
60
+ await self._consumer.stop()
61
+ await self._admin.stop()
62
+ if self._lifespan:
63
+ await self._lifespan.__aexit__(None, None, None)
64
+ logger.info("%s stopped", self.title)
65
+ except Exception:
66
+ logger.exception("KafkaApp.stop failed")
67
+ raise
68
+
69
+ async def run(self) -> None:
70
+ loop = asyncio.get_running_loop()
71
+ shutdown_event = asyncio.Event()
72
+ try:
73
+ for sig in (signal.SIGINT, signal.SIGTERM):
74
+ loop.add_signal_handler(sig, shutdown_event.set)
75
+ except (NotImplementedError, AttributeError):
76
+ signal.signal(signal.SIGINT, lambda *_: shutdown_event.set())
77
+ await self.start()
78
+ await shutdown_event.wait()
79
+ await self.stop()
@@ -0,0 +1,87 @@
1
+ # fastkafka\consumer.py
2
+ import asyncio
3
+ import orjson
4
+ import logging
5
+ from aiokafka import AIOKafkaConsumer
6
+ from .services.base import BaseKafkaService
7
+ from .registry import handlers_registry
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class KafkaConsumerService(BaseKafkaService):
13
+ def __init__(
14
+ self,
15
+ topics: list[str],
16
+ bootstrap_servers: str,
17
+ worker_count: int = 4,
18
+ max_concurrency: int = 100,
19
+ ):
20
+ self._topics = topics
21
+ self._bootstrap = bootstrap_servers
22
+ self._worker_count = worker_count
23
+ self._consumer: AIOKafkaConsumer | None = None
24
+ self._queue: asyncio.Queue = asyncio.Queue(maxsize=1000)
25
+ self._workers: list[asyncio.Task] = []
26
+ self._semaphore = asyncio.Semaphore(max_concurrency)
27
+ self._metrics_task: asyncio.Task | None = None
28
+
29
+ async def start(self) -> None:
30
+ self._consumer = AIOKafkaConsumer(
31
+ *self._topics,
32
+ bootstrap_servers=self._bootstrap,
33
+ group_id="fastkafka_group",
34
+ auto_offset_reset="latest",
35
+ value_deserializer=lambda v: orjson.loads(v),
36
+ )
37
+ await self._consumer.start()
38
+ logger.info("KafkaConsumerService started on topics %s", self._topics)
39
+
40
+ asyncio.create_task(self._consume_loop())
41
+ self._workers = [
42
+ asyncio.create_task(self._worker_loop()) for _ in range(self._worker_count)
43
+ ]
44
+ self._metrics_task = asyncio.create_task(self._metrics_loop())
45
+
46
+ async def stop(self) -> None:
47
+ if self._consumer:
48
+ await self._consumer.stop()
49
+ logger.info("Consumer stopped")
50
+ for w in self._workers:
51
+ w.cancel()
52
+ await asyncio.gather(*self._workers, return_exceptions=True)
53
+ if self._metrics_task:
54
+ self._metrics_task.cancel()
55
+ await asyncio.gather(self._metrics_task, return_exceptions=True)
56
+
57
+ async def _consume_loop(self) -> None:
58
+ try:
59
+ async for msg in self._consumer:
60
+ headers = {k: (v.decode() if v else "") for k, v in (msg.headers or [])}
61
+ key = msg.key.decode() if msg.key else None
62
+ await self._queue.put((msg.topic, msg.value, headers, key))
63
+ except Exception:
64
+ logger.exception("Error in consume loop")
65
+
66
+ async def _worker_loop(self) -> None:
67
+ while True:
68
+ topic, data, headers, key = await self._queue.get()
69
+ for handler in handlers_registry.get(topic, []):
70
+ await self._semaphore.acquire()
71
+ asyncio.create_task(self._safe_invoke(handler, data, headers, key))
72
+ self._queue.task_done()
73
+
74
+ async def _safe_invoke(self, handler, data, headers, key):
75
+ try:
76
+ await handler.handle(data, headers, key)
77
+ except Exception as e:
78
+ logger.warning("Handler error on topic %s: %s", handler.topic, e)
79
+ finally:
80
+ self._semaphore.release()
81
+
82
+ async def _metrics_loop(self):
83
+ while True:
84
+ logger.debug(
85
+ f"[Kafka] Queue size: {self._queue.qsize()} | Workers: {len(self._workers)}"
86
+ )
87
+ await asyncio.sleep(10)
@@ -0,0 +1,36 @@
1
+ # fastkafka\dependencies.py
2
+ import logging
3
+ from .admin import KafkaAdminService
4
+ from .consumer import KafkaConsumerService
5
+ from .registry import handlers_registry
6
+
7
+ logger = logging.getLogger(__name__)
8
+ _admin = KafkaAdminService()
9
+ _consumer: KafkaConsumerService | None = None
10
+
11
+
12
+ async def start_kafka(bootstrap_servers: str) -> None:
13
+ global _consumer
14
+ try:
15
+ await _admin.start()
16
+ for topic in handlers_registry:
17
+ await _admin.create_topic(topic)
18
+ _consumer = KafkaConsumerService(
19
+ topics=list(handlers_registry), bootstrap_servers=bootstrap_servers
20
+ )
21
+ await _consumer.start()
22
+ logger.info("Dependencies started")
23
+ except Exception:
24
+ logger.exception("Failed to start Kafka dependencies")
25
+ raise
26
+
27
+
28
+ async def stop_kafka() -> None:
29
+ try:
30
+ if _consumer:
31
+ await _consumer.stop()
32
+ await _admin.stop()
33
+ logger.info("Dependencies stopped")
34
+ except Exception:
35
+ logger.exception("Failed to stop Kafka dependencies")
36
+ raise
@@ -0,0 +1 @@
1
+ # fastkafka\di\__init__.py
@@ -0,0 +1,43 @@
1
+ # fastkafka\di\di_container.py
2
+ import logging
3
+ from inspect import isclass, signature
4
+ from typing import Any, Callable, Type
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ _singletons: dict[Type[Any], Any] = {}
9
+ _factories: dict[Type[Any], Callable[..., Any]] = {}
10
+ _resolving_stack: set[Type[Any]] = set()
11
+ _sig_cache: dict[Type[Any], Any] = {}
12
+
13
+
14
+ def register_singleton(cls: Type[Any], instance: Any) -> None:
15
+ _singletons[cls] = instance
16
+
17
+
18
+ def register_factory(cls: Type[Any], factory: Callable[..., Any]) -> None:
19
+ _factories[cls] = factory
20
+
21
+
22
+ def resolve(cls: Type[Any]) -> Any:
23
+ if cls in _singletons:
24
+ return _singletons[cls]
25
+ if cls in _factories:
26
+ return _factories[cls]()
27
+ if not isclass(cls):
28
+ raise TypeError(f"Cannot resolve non-class type: {cls}")
29
+ if cls in _resolving_stack:
30
+ raise RuntimeError(f"Circular dependency detected: {cls.__name__}")
31
+
32
+ _resolving_stack.add(cls)
33
+ try:
34
+ sig = _sig_cache.get(cls) or signature(cls.__init__)
35
+ _sig_cache[cls] = sig
36
+ kwargs = {
37
+ name: resolve(param.annotation)
38
+ for name, param in sig.parameters.items()
39
+ if name != "self"
40
+ }
41
+ return cls(**kwargs)
42
+ finally:
43
+ _resolving_stack.remove(cls)
@@ -0,0 +1,19 @@
1
+ # fastkafka\handler.py
2
+ import logging
3
+ from typing import Callable, Any
4
+ from .registry import kafka_handler
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class KafkaHandler:
10
+ def __init__(self, prefix: str = "") -> None:
11
+ self.prefix = prefix
12
+
13
+ def __call__(
14
+ self, topic: str, data_model: Any = None
15
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
16
+ return kafka_handler(topic, data_model)
17
+
18
+ def include_handler(self, other: "KafkaHandler") -> None:
19
+ self.prefix += f".{other.prefix}" if other.prefix else ""
@@ -0,0 +1,19 @@
1
+ # fastkafka\logging_utils.py
2
+ import logging
3
+
4
+
5
+ def suppress_external_logs() -> None:
6
+ suppressed = [
7
+ "aiokafka",
8
+ "kafka",
9
+ "asyncio",
10
+ "aiokafka.consumer.fetcher",
11
+ "aiokafka.cluster",
12
+ "aiokafka.producer.producer",
13
+ "aiokafka.consumer.group_coordinator",
14
+ "aiokafka.consumer.subscription_state",
15
+ ]
16
+ for name in suppressed:
17
+ lg = logging.getLogger(name)
18
+ lg.setLevel(logging.CRITICAL)
19
+ lg.propagate = False
@@ -0,0 +1,11 @@
1
+ # fastkafka\message.py
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class KafkaMessage(BaseModel):
6
+ topic: str
7
+ data: object
8
+ headers: dict[str, str] = {}
9
+ key: str | None = None
10
+
11
+ model_config = {"extra": "forbid", "frozen": True, "slots": True}
@@ -0,0 +1,57 @@
1
+ # fastkafka\producer.py
2
+ import orjson
3
+ import logging
4
+ from typing import Any
5
+ from aiokafka import AIOKafkaProducer
6
+ from .services.base import BaseKafkaService
7
+ from .services.retry import retry_on_connection
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def _serialize_headers(headers: dict[str, str]) -> list[tuple[str, bytes]]:
13
+ return [(k, v.encode()) for k, v in headers.items()]
14
+
15
+
16
+ class KafkaProducer(BaseKafkaService):
17
+ def __init__(self, bootstrap_servers: str) -> None:
18
+ self.bootstrap_servers = bootstrap_servers
19
+ self._producer: AIOKafkaProducer | None = None
20
+
21
+ @retry_on_connection()
22
+ async def start(self) -> None:
23
+ self._producer = AIOKafkaProducer(
24
+ bootstrap_servers=self.bootstrap_servers,
25
+ value_serializer=lambda v: orjson.dumps(v),
26
+ )
27
+ await self._producer.start()
28
+ logger.info("KafkaProducer started")
29
+
30
+ @retry_on_connection()
31
+ async def send_message(
32
+ self,
33
+ topic: str,
34
+ data: dict[str, Any],
35
+ headers: dict[str, str] | None = None,
36
+ key: str | None = None,
37
+ ) -> None:
38
+ if not self._producer:
39
+ raise RuntimeError("Producer not started")
40
+ try:
41
+ kafka_headers = _serialize_headers(headers or {})
42
+ key_bytes = key.encode() if key else None
43
+ await self._producer.send_and_wait(
44
+ topic=topic,
45
+ value=data,
46
+ headers=kafka_headers,
47
+ key=key_bytes,
48
+ )
49
+ except Exception:
50
+ logger.exception("Failed to send message")
51
+ raise
52
+
53
+ async def stop(self) -> None:
54
+ if self._producer:
55
+ await self._producer.flush()
56
+ await self._producer.stop()
57
+ logger.info("KafkaProducer stopped")
File without changes
@@ -0,0 +1,61 @@
1
+ # fastkafka\registry.py
2
+ import logging
3
+ from inspect import signature, iscoroutinefunction
4
+ from typing import Any, Callable
5
+ from pydantic import BaseModel
6
+ from fastkafka.message import KafkaMessage
7
+ from fastkafka.di.di_container import resolve
8
+
9
+ __all__ = ["kafka_handler"]
10
+
11
+ logger = logging.getLogger(__name__)
12
+ handlers_registry: dict[str, list["CompiledHandler"]] = {}
13
+
14
+
15
+ class CompiledHandler:
16
+ __slots__ = ("topic", "func", "sig", "data_model", "dependencies")
17
+
18
+ def __init__(
19
+ self, topic: str, func: Callable[..., Any], data_model: type[BaseModel] | None
20
+ ):
21
+ self.topic = topic
22
+ self.func = func
23
+ self.sig = signature(func)
24
+ self.data_model = data_model
25
+
26
+ self.dependencies: dict[str, Any] = {}
27
+ for name, param in self.sig.parameters.items():
28
+ if param.annotation not in (KafkaMessage, data_model):
29
+ self.dependencies[name] = resolve(param.annotation)
30
+
31
+ async def handle(
32
+ self, raw_data: Any, headers: dict[str, str] | None, key: str | None
33
+ ):
34
+ msg_data = self.data_model(**raw_data) if self.data_model else raw_data
35
+ message = KafkaMessage(
36
+ topic=self.topic, data=msg_data, headers=headers or {}, key=key
37
+ )
38
+
39
+ kwargs = {
40
+ name: (
41
+ self.dependencies.get(name)
42
+ if param.annotation not in (KafkaMessage, self.data_model)
43
+ else message
44
+ )
45
+ for name, param in self.sig.parameters.items()
46
+ }
47
+
48
+ return await self.func(**kwargs)
49
+
50
+
51
+ def kafka_handler(topic: str, data_model: type[BaseModel] | None = None):
52
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
53
+ if not iscoroutinefunction(func):
54
+ raise TypeError("Handler must be async")
55
+ handlers_registry.setdefault(topic, []).append(
56
+ CompiledHandler(topic, func, data_model)
57
+ )
58
+ logger.debug("Registered handler %s for topic %s", func.__name__, topic)
59
+ return func
60
+
61
+ return decorator
@@ -0,0 +1 @@
1
+ # fastkafka\services\__init__.py
@@ -0,0 +1,12 @@
1
+ # fastkafka\services\base.py
2
+ from abc import ABC, abstractmethod
3
+
4
+ __all__ = ["BaseKafkaService"]
5
+
6
+
7
+ class BaseKafkaService(ABC):
8
+ @abstractmethod
9
+ async def start(self) -> None: ...
10
+
11
+ @abstractmethod
12
+ async def stop(self) -> None: ...
@@ -0,0 +1,27 @@
1
+ # fastkafka\services\retry.py
2
+ import asyncio
3
+ import logging
4
+ from aiokafka.errors import KafkaConnectionError
5
+ from typing import Callable, TypeVar, Awaitable
6
+
7
+ __all__ = ["retry_on_connection"]
8
+
9
+ Logger = logging.getLogger(__name__)
10
+ F = TypeVar("F", bound=Callable[..., Awaitable[None]])
11
+
12
+
13
+ def retry_on_connection(delay: int = 5) -> Callable[[F], F]:
14
+ def decorator(fn: F) -> F:
15
+ async def wrapper(*args, **kwargs):
16
+ while True:
17
+ try:
18
+ return await fn(*args, **kwargs)
19
+ except KafkaConnectionError as e:
20
+ Logger.warning(
21
+ "Retrying %s due to connection error: %s", fn.__name__, e
22
+ )
23
+ await asyncio.sleep(delay)
24
+
25
+ return wrapper # type: ignore
26
+
27
+ return decorator
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastkafka2
3
+ Version: 0.1.0
4
+ Summary: Next-generation FastAPI-like DX for Kafka (version 2)
5
+ Author: Ruslan Ramazanov
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: pydantic>=2.6
9
+ Requires-Dist: anyio>=4
10
+ Requires-Dist: aiokafka>=0.10
11
+ Requires-Dist: typing-extensions>=4.8
12
+
13
+ # fastkafka2
14
+
15
+ Next-generation FastAPI-like DX for Kafka (version 2).
16
+
17
+ ## Installation
18
+
19
+ ``` bash
20
+ pip install fastkafka2
21
+ ```
22
+
23
+ ## Use example
24
+ ### Project arch
25
+ ```shell
26
+ ├── api/
27
+ │ ├── kafka/
28
+ │ │ ├── handlers/
29
+ │ │ │ ├── example/
30
+ │ │ │ │ ├─ schemas.py
31
+ │ │ │ │ └─ handler.py
32
+ │ │ │ └── base_handler.py
33
+ │ │ └── lifespan.py
34
+
35
+ ├── main.py
36
+ ```
37
+
38
+ ### Schemas
39
+ ``` python
40
+ # api\kafka\handlers\example\schemas.py
41
+ from pydantic import BaseModel
42
+
43
+
44
+ class ExampleSchema(BaseModel):
45
+ msg: str
46
+ ```
47
+
48
+ ### Handler
49
+ ``` python
50
+ # api\kafka\handlers\example\handler.py
51
+ import logging
52
+
53
+ from fastkafka.handler import KafkaHandler
54
+ from fastkafka.message import KafkaMessage
55
+ from fastkafka.producer import KafkaProducer
56
+
57
+
58
+ handler = KafkaHandler()
59
+
60
+ kafka_producer = KafkaProducer(bootstrap_servers="127.0.0.1:9092")
61
+
62
+
63
+ @handler("example")
64
+ async def example_handler(message: KafkaMessage):
65
+ t = int(message.headers.get("try")) + 1
66
+ logging.info(f"Пришло: {message}")
67
+ await kafka_producer.send_message(
68
+ topic="example-2", data={"msg": "wddwd"}, headers={"try": f"{t}"}, key=None
69
+ )
70
+ logging.info(f"Отправил: {f'{t}'}")
71
+ ```
72
+
73
+
74
+ ### Grouping of handlers
75
+ ``` python
76
+ # api\kafka\handlers\base_handler.py
77
+ from api.kafka.handlers.example.handler import handler as example_handler
78
+
79
+ from fastkafka.handler import KafkaHandler
80
+
81
+ base_handler = KafkaHandler()
82
+
83
+ base_handler.include_handler(example_handler)
84
+ ```
85
+
86
+
87
+ ### Lifespan fastkafka app
88
+ ``` python
89
+ # api/kafka/lifespan.py
90
+ import logging
91
+ from contextlib import asynccontextmanager
92
+ from fastkafka.app import KafkaApp
93
+ from api.kafka.handlers.base_handler import base_handler
94
+
95
+ from api.kafka.handlers.example.handler import kafka_producer
96
+
97
+
98
+ @asynccontextmanager
99
+ async def lifespan(app: KafkaApp):
100
+ logging.info("Lifespan: запуск")
101
+ try:
102
+ await kafka_producer.start()
103
+ yield
104
+ logging.info("Lifespan: выполнен")
105
+ finally:
106
+ await kafka_producer.stop()
107
+ logging.info("Lifespan: остановка")
108
+
109
+
110
+ app = KafkaApp(
111
+ title="Kafka Gateway",
112
+ description="Kafka-based microservice",
113
+ bootstrap_servers="127.0.0.1:9092",
114
+ lifespan=lifespan,
115
+ )
116
+
117
+ app.include_handler(base_handler)
118
+ ```
119
+
120
+
121
+ ### Entry point main app
122
+ ``` python
123
+ # main.py
124
+ import asyncio
125
+ from logging_config import setup_logging
126
+ from api.kafka.lifespan import app
127
+
128
+ if __name__ == "__main__":
129
+ setup_logging()
130
+ asyncio.run(app.run())
131
+ ```
132
+
133
+
@@ -0,0 +1,23 @@
1
+ README.md
2
+ pyproject.toml
3
+ fastkafka/__init__.py
4
+ fastkafka/admin.py
5
+ fastkafka/app.py
6
+ fastkafka/consumer.py
7
+ fastkafka/dependencies.py
8
+ fastkafka/handler.py
9
+ fastkafka/logging_utils.py
10
+ fastkafka/message.py
11
+ fastkafka/producer.py
12
+ fastkafka/py.typed
13
+ fastkafka/registry.py
14
+ fastkafka2.egg-info/PKG-INFO
15
+ fastkafka2.egg-info/SOURCES.txt
16
+ fastkafka2.egg-info/dependency_links.txt
17
+ fastkafka2.egg-info/requires.txt
18
+ fastkafka2.egg-info/top_level.txt
19
+ fastkafka/di/__init__.py
20
+ fastkafka/di/di_container.py
21
+ fastkafka/services/__init__.py
22
+ fastkafka/services/base.py
23
+ fastkafka/services/retry.py
@@ -0,0 +1,4 @@
1
+ pydantic>=2.6
2
+ anyio>=4
3
+ aiokafka>=0.10
4
+ typing-extensions>=4.8
@@ -0,0 +1 @@
1
+ fastkafka
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fastkafka2"
7
+ version = "0.1.0"
8
+ description = "Next-generation FastAPI-like DX for Kafka (version 2)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ { name = "Ruslan Ramazanov" }
13
+ ]
14
+ dependencies = [
15
+ "pydantic>=2.6",
16
+ "anyio>=4",
17
+ "aiokafka>=0.10",
18
+ "typing-extensions>=4.8",
19
+ ]
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["."]
23
+ include = ["fastkafka*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+