buz 2.15.10rc6__py3-none-any.whl → 2.15.12__py3-none-any.whl

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.
Files changed (32) hide show
  1. buz/command/asynchronous/base_command_handler.py +13 -6
  2. buz/command/asynchronous/command_handler.py +8 -5
  3. buz/command/synchronous/base_command_handler.py +14 -6
  4. buz/command/synchronous/command_handler.py +7 -5
  5. buz/event/infrastructure/buz_kafka/kafka_event_async_subscriber_executor.py +20 -3
  6. buz/event/infrastructure/buz_kafka/kafka_event_sync_subscriber_executor.py +20 -5
  7. buz/event/infrastructure/buz_kafka/models/kafka_delivery_context.py +10 -0
  8. buz/event/infrastructure/kombu/kombu_consumer.py +10 -2
  9. buz/event/infrastructure/kombu/models/kombu_delivery_context.py +7 -0
  10. buz/event/infrastructure/models/__init__.py +0 -0
  11. buz/event/infrastructure/models/delivery_context.py +6 -0
  12. buz/event/infrastructure/models/execution_context.py +8 -0
  13. buz/event/middleware/async_consume_middleware.py +9 -2
  14. buz/event/middleware/async_consume_middleware_chain_resolver.py +16 -5
  15. buz/event/middleware/base_async_consume_middleware.py +23 -6
  16. buz/event/middleware/base_consume_middleware.py +9 -6
  17. buz/event/middleware/consume_middleware.py +5 -2
  18. buz/event/middleware/consume_middleware_chain_resolver.py +13 -4
  19. buz/event/sync/models/__init__.py +0 -0
  20. buz/event/sync/models/sync_delivery_context.py +7 -0
  21. buz/event/sync/sync_event_bus.py +10 -2
  22. buz/handler.py +6 -3
  23. buz/kafka/domain/models/kafka_poll_record.py +1 -0
  24. buz/query/asynchronous/base_query_handler.py +15 -6
  25. buz/query/asynchronous/query_handler.py +8 -4
  26. buz/query/synchronous/base_query_handler.py +15 -6
  27. buz/query/synchronous/query_handler.py +8 -5
  28. {buz-2.15.10rc6.dist-info → buz-2.15.12.dist-info}/LICENSE +1 -1
  29. buz-2.15.12.dist-info/METADATA +438 -0
  30. {buz-2.15.10rc6.dist-info → buz-2.15.12.dist-info}/RECORD +31 -24
  31. buz-2.15.10rc6.dist-info/METADATA +0 -41
  32. {buz-2.15.10rc6.dist-info → buz-2.15.12.dist-info}/WHEEL +0 -0
@@ -1,27 +1,34 @@
1
- from typing import Type, get_type_hints, Any
1
+ from typing import Generic, Type, TypeVar, cast, get_type_hints, Any
2
2
 
3
3
  from buz.command import Command
4
4
  from buz.command.asynchronous.command_handler import CommandHandler
5
5
 
6
+ TCommand = TypeVar("TCommand", bound=Command)
6
7
 
7
- class BaseCommandHandler(CommandHandler):
8
+
9
+ class BaseCommandHandler(Generic[TCommand], CommandHandler[TCommand]):
8
10
  @classmethod
9
11
  def fqn(cls) -> str:
10
12
  return f"command_handler.{cls.__module__}.{cls.__name__}"
11
13
 
12
14
  @classmethod
13
- def handles(cls) -> Type[Command]:
15
+ def handles(cls) -> Type[TCommand]:
14
16
  handle_types = get_type_hints(cls.handle)
15
17
 
16
- if "command" not in handle_types:
18
+ t_command = handle_types.get("command")
19
+ if t_command is None:
17
20
  raise TypeError(
18
21
  f"The method 'handle' in '{cls.fqn()}' doesn't have a parameter named 'command'. Found parameters: {cls.__get_method_parameter_names(handle_types)}"
19
22
  )
20
23
 
21
- if not issubclass(handle_types["command"], Command):
24
+ # TypeVar mask the actual bound type
25
+ if hasattr(t_command, "__bound__"):
26
+ t_command = t_command.__bound__
27
+
28
+ if not issubclass(t_command, Command):
22
29
  raise TypeError(f"The parameter 'command' in '{cls.fqn()}.handle' is not a 'buz.command.Command' subclass")
23
30
 
24
- return handle_types["command"]
31
+ return cast(Type[TCommand], t_command)
25
32
 
26
33
  @classmethod
27
34
  def __get_method_parameter_names(cls, handle_types: dict[str, Any]) -> list[str]:
@@ -1,16 +1,19 @@
1
- from abc import abstractmethod
2
- from typing import Type
1
+ from abc import ABC, abstractmethod
2
+ from typing import Generic, Type, TypeVar
3
3
 
4
4
  from buz import Handler
5
5
  from buz.command.command import Command
6
6
 
7
7
 
8
- class CommandHandler(Handler):
8
+ TCommand = TypeVar("TCommand", bound=Command)
9
+
10
+
11
+ class CommandHandler(Generic[TCommand], Handler[TCommand], ABC):
9
12
  @classmethod
10
13
  @abstractmethod
11
- def handles(cls) -> Type[Command]:
14
+ def handles(cls) -> Type[TCommand]:
12
15
  pass
13
16
 
14
17
  @abstractmethod
15
- async def handle(self, command: Command) -> None:
18
+ async def handle(self, command: TCommand) -> None:
16
19
  pass
@@ -1,27 +1,35 @@
1
- from typing import Type, get_type_hints, Any
1
+ from typing import Generic, Type, TypeVar, get_type_hints, Any, cast
2
2
 
3
3
  from buz.command import Command
4
4
  from buz.command.synchronous.command_handler import CommandHandler
5
5
 
6
6
 
7
- class BaseCommandHandler(CommandHandler):
7
+ TCommand = TypeVar("TCommand", bound=Command)
8
+
9
+
10
+ class BaseCommandHandler(Generic[TCommand], CommandHandler[TCommand]):
8
11
  @classmethod
9
12
  def fqn(cls) -> str:
10
13
  return f"command_handler.{cls.__module__}.{cls.__name__}"
11
14
 
12
15
  @classmethod
13
- def handles(cls) -> Type[Command]:
16
+ def handles(cls) -> Type[TCommand]:
14
17
  handle_types = get_type_hints(cls.handle)
15
18
 
16
- if "command" not in handle_types:
19
+ t_command = handle_types.get("command")
20
+ if t_command is None:
17
21
  raise TypeError(
18
22
  f"The method 'handle' in '{cls.fqn()}' doesn't have a parameter named 'command'. Found parameters: {cls.__get_method_parameter_names(handle_types)}"
19
23
  )
20
24
 
21
- if not issubclass(handle_types["command"], Command):
25
+ # TypeVar mask the actual bound type
26
+ if hasattr(t_command, "__bound__"):
27
+ t_command = t_command.__bound__
28
+
29
+ if not issubclass(t_command, Command):
22
30
  raise TypeError(f"The parameter 'command' in '{cls.fqn()}.handle' is not a 'buz.command.Command' subclass")
23
31
 
24
- return handle_types["command"]
32
+ return cast(Type[TCommand], t_command)
25
33
 
26
34
  @classmethod
27
35
  def __get_method_parameter_names(cls, handle_types: dict[str, Any]) -> list[str]:
@@ -1,16 +1,18 @@
1
- from abc import abstractmethod
2
- from typing import Type
1
+ from abc import ABC, abstractmethod
2
+ from typing import Generic, Type, TypeVar
3
3
 
4
4
  from buz import Handler
5
5
  from buz.command import Command
6
6
 
7
+ TCommand = TypeVar("TCommand", bound=Command)
7
8
 
8
- class CommandHandler(Handler):
9
+
10
+ class CommandHandler(Generic[TCommand], Handler[TCommand], ABC):
9
11
  @classmethod
10
12
  @abstractmethod
11
- def handles(cls) -> Type[Command]:
13
+ def handles(cls) -> Type[TCommand]:
12
14
  pass
13
15
 
14
16
  @abstractmethod
15
- def handle(self, command: Command) -> None:
17
+ def handle(self, command: TCommand) -> None:
16
18
  pass
@@ -8,6 +8,7 @@ from buz.event.infrastructure.buz_kafka.consume_strategy.kafka_on_fail_strategy
8
8
  from buz.event.infrastructure.buz_kafka.exceptions.max_consumer_retry_exception import MaxConsumerRetryException
9
9
  from buz.event.infrastructure.buz_kafka.exceptions.retry_exception import ConsumerRetryException
10
10
  from buz.event.infrastructure.buz_kafka.kafka_event_subscriber_executor import KafkaEventSubscriberExecutor
11
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
11
12
  from buz.event.middleware.async_consume_middleware import AsyncConsumeMiddleware
12
13
  from buz.event.middleware.async_consume_middleware_chain_resolver import AsyncConsumeMiddlewareChainResolver
13
14
  from buz.event.strategies.retry.consume_retrier import ConsumeRetrier
@@ -15,6 +16,7 @@ from buz.event.strategies.retry.reject_callback import RejectCallback
15
16
  from buz.kafka.domain.exceptions.not_valid_kafka_message_exception import NotValidKafkaMessageException
16
17
  from buz.kafka.domain.models.kafka_consumer_record import KafkaConsumerRecord
17
18
  from buz.kafka.domain.models.kafka_poll_record import KafkaPollRecord
19
+ from buz.event.infrastructure.buz_kafka.models.kafka_delivery_context import KafkaDeliveryContext
18
20
  from buz.kafka.infrastructure.deserializers.byte_deserializer import ByteDeserializer
19
21
  from buz.kafka.infrastructure.serializers.kafka_header_serializer import KafkaHeaderSerializer
20
22
 
@@ -67,18 +69,33 @@ class KafkaEventAsyncSubscriberExecutor(KafkaEventSubscriberExecutor):
67
69
  value=deserialized_value,
68
70
  headers=self.__header_deserializer.deserialize(kafka_poll_record.headers),
69
71
  ),
72
+ ExecutionContext(
73
+ delivery_context=KafkaDeliveryContext(
74
+ topic=kafka_poll_record.topic,
75
+ consumer_group=self.__subscriber.fqn(),
76
+ partition=kafka_poll_record.partition,
77
+ timestamp=kafka_poll_record.timestamp,
78
+ )
79
+ ),
70
80
  )
71
81
  except NotValidKafkaMessageException:
72
82
  self.__logger.error(
73
83
  f'The message "{str(kafka_poll_record.value)}" is not valid, it will be consumed but not processed'
74
84
  )
75
85
 
76
- async def __consumption_callback(self, subscriber: AsyncSubscriber, message: KafkaConsumerRecord[Event]) -> None:
86
+ async def __consumption_callback(
87
+ self, subscriber: AsyncSubscriber, message: KafkaConsumerRecord[Event], execution_context: ExecutionContext
88
+ ) -> None:
77
89
  await self.__consume_middleware_chain_resolver.resolve(
78
- event=message.value, subscriber=subscriber, consume=self.__perform_consume
90
+ event=message.value,
91
+ subscriber=subscriber,
92
+ execution_context=execution_context,
93
+ consume=self.__perform_consume,
79
94
  )
80
95
 
81
- async def __perform_consume(self, event: Event, subscriber: AsyncSubscriber) -> None:
96
+ async def __perform_consume(
97
+ self, event: Event, subscriber: AsyncSubscriber, execution_context: ExecutionContext
98
+ ) -> None:
82
99
  number_of_executions = 0
83
100
  should_retry = True
84
101
 
@@ -7,6 +7,8 @@ from buz.event.infrastructure.buz_kafka.consume_strategy.kafka_on_fail_strategy
7
7
  from buz.event.infrastructure.buz_kafka.exceptions.max_consumer_retry_exception import MaxConsumerRetryException
8
8
  from buz.event.infrastructure.buz_kafka.exceptions.retry_exception import ConsumerRetryException
9
9
  from buz.event.infrastructure.buz_kafka.kafka_event_subscriber_executor import KafkaEventSubscriberExecutor
10
+ from buz.event.infrastructure.buz_kafka.models.kafka_delivery_context import KafkaDeliveryContext
11
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
10
12
  from buz.event.middleware.consume_middleware import ConsumeMiddleware
11
13
  from buz.event.middleware.consume_middleware_chain_resolver import ConsumeMiddlewareChainResolver
12
14
  from buz.event.strategies.retry.consume_retrier import ConsumeRetrier
@@ -64,11 +66,19 @@ class KafkaEventSyncSubscriberExecutor(KafkaEventSubscriberExecutor):
64
66
  await get_running_loop().run_in_executor(
65
67
  None,
66
68
  lambda: self.__execution_callback(
67
- self.__subscriber,
68
- KafkaConsumerRecord(
69
+ subscriber=self.__subscriber,
70
+ message=KafkaConsumerRecord(
69
71
  value=deserialized_value,
70
72
  headers=self.__header_deserializer.deserialize(kafka_poll_record.headers),
71
73
  ),
74
+ execution_context=ExecutionContext(
75
+ delivery_context=KafkaDeliveryContext(
76
+ topic=kafka_poll_record.topic,
77
+ consumer_group=self.__subscriber.fqn(),
78
+ partition=kafka_poll_record.partition,
79
+ timestamp=kafka_poll_record.timestamp,
80
+ )
81
+ ),
72
82
  ),
73
83
  )
74
84
 
@@ -77,12 +87,17 @@ class KafkaEventSyncSubscriberExecutor(KafkaEventSubscriberExecutor):
77
87
  f'The message "{str(kafka_poll_record.value)}" is not valid, it will be consumed but not processed'
78
88
  )
79
89
 
80
- def __execution_callback(self, subscriber: Subscriber, message: KafkaConsumerRecord[Event]) -> None:
90
+ def __execution_callback(
91
+ self, subscriber: Subscriber, message: KafkaConsumerRecord[Event], execution_context: ExecutionContext
92
+ ) -> None:
81
93
  self.__consume_middleware_chain_resolver.resolve(
82
- event=message.value, subscriber=subscriber, consume=self.__perform_consume
94
+ event=message.value,
95
+ subscriber=subscriber,
96
+ execution_context=execution_context,
97
+ consume=self.__perform_consume,
83
98
  )
84
99
 
85
- def __perform_consume(self, event: Event, subscriber: Subscriber) -> None:
100
+ def __perform_consume(self, event: Event, subscriber: Subscriber, execution_context: ExecutionContext) -> None:
86
101
  number_of_executions = 0
87
102
  should_retry = True
88
103
 
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+ from buz.event.infrastructure.models.delivery_context import DeliveryContext
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class KafkaDeliveryContext(DeliveryContext):
7
+ topic: str
8
+ consumer_group: str
9
+ partition: int
10
+ timestamp: int
@@ -2,6 +2,8 @@ import asyncio
2
2
  from logging import Logger
3
3
  from typing import Optional, Callable, cast
4
4
 
5
+ from buz.event.infrastructure.kombu.models.kombu_delivery_context import KombuDeliveryContext
6
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
5
7
  from kombu import Connection, Queue, Consumer as MessageConsumer, Message
6
8
  from kombu.mixins import ConsumerMixin
7
9
  from kombu.transport.pyamqp import Channel
@@ -110,12 +112,18 @@ class KombuConsumer(ConsumerMixin, Consumer):
110
112
  # The problem here is that the chain resolver works with syncsubscribers, an asyncsubscriber would require of a async function,
111
113
  # but we are using run-until-complete to run the async function, so we are not really using the async function, we are just running it as a sync function, so we can cast the asyncsubscriber to a subscriber
112
114
  subscriber = cast(Subscriber, meta_subscriber)
113
- self.__consume_middleware_chain_resolver.resolve(event, subscriber, self.__perform_consume)
115
+ execution_context = ExecutionContext(delivery_context=KombuDeliveryContext())
116
+ self.__consume_middleware_chain_resolver.resolve(
117
+ event=event,
118
+ subscriber=subscriber,
119
+ execution_context=execution_context,
120
+ consume=self.__perform_consume,
121
+ )
114
122
  message.ack()
115
123
  except Exception as exc:
116
124
  self.__on_consume_exception(message, event, meta_subscribers, exc)
117
125
 
118
- def __perform_consume(self, event: Event, subscriber: MetaSubscriber) -> None:
126
+ def __perform_consume(self, event: Event, subscriber: MetaSubscriber, execution_context: ExecutionContext) -> None:
119
127
  if isinstance(subscriber, AsyncSubscriber):
120
128
  self.__get_or_create_event_loop().run_until_complete(subscriber.consume(event))
121
129
  return
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+ from buz.event.infrastructure.models.delivery_context import DeliveryContext
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class KombuDeliveryContext(DeliveryContext):
7
+ pass
File without changes
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class DeliveryContext:
6
+ pass
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ from buz.event.infrastructure.models.delivery_context import DeliveryContext
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class ExecutionContext:
8
+ delivery_context: DeliveryContext
@@ -3,12 +3,19 @@ from typing import Awaitable, Callable
3
3
 
4
4
  from buz.event import Event
5
5
  from buz.event.async_subscriber import AsyncSubscriber
6
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
6
7
  from buz.middleware import Middleware
7
8
 
8
- AsyncConsumeCallable = Callable[[Event, AsyncSubscriber], Awaitable[None]]
9
+ AsyncConsumeCallable = Callable[[Event, AsyncSubscriber, ExecutionContext], Awaitable[None]]
9
10
 
10
11
 
11
12
  class AsyncConsumeMiddleware(Middleware):
12
13
  @abstractmethod
13
- async def on_consume(self, event: Event, subscriber: AsyncSubscriber, consume: AsyncConsumeCallable) -> None:
14
+ async def on_consume(
15
+ self,
16
+ event: Event,
17
+ subscriber: AsyncSubscriber,
18
+ consume: AsyncConsumeCallable,
19
+ execution_context: ExecutionContext,
20
+ ) -> None:
14
21
  pass
@@ -2,6 +2,7 @@ from typing import Sequence
2
2
  from buz.event import Event
3
3
  from buz.event.async_subscriber import AsyncSubscriber
4
4
 
5
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
5
6
  from buz.event.middleware.async_consume_middleware import AsyncConsumeCallable, AsyncConsumeMiddleware
6
7
  from buz.middleware import MiddlewareChainBuilder
7
8
 
@@ -14,16 +15,26 @@ class AsyncConsumeMiddlewareChainResolver:
14
15
  self.__middlewares = middlewares
15
16
  self.__middleware_chain_builder: MiddlewareChainBuilder[
16
17
  AsyncConsumeCallable, AsyncConsumeMiddleware
17
- ] = MiddlewareChainBuilder(middlewares)
18
+ ] = MiddlewareChainBuilder(self.__middlewares)
18
19
 
19
- async def resolve(self, event: Event, subscriber: AsyncSubscriber, consume: AsyncConsumeCallable) -> None:
20
+ async def resolve(
21
+ self,
22
+ event: Event,
23
+ subscriber: AsyncSubscriber,
24
+ consume: AsyncConsumeCallable,
25
+ execution_context: ExecutionContext,
26
+ ) -> None:
20
27
  chain_callable: AsyncConsumeCallable = self.__middleware_chain_builder.get_chain_callable(
21
28
  consume, self.__get_middleware_callable
22
29
  )
23
30
 
24
- await chain_callable(event, subscriber)
31
+ await chain_callable(event, subscriber, execution_context)
25
32
 
26
33
  def __get_middleware_callable(
27
- self, middleware: AsyncConsumeMiddleware, consume_callable: AsyncConsumeCallable
34
+ self,
35
+ middleware: AsyncConsumeMiddleware,
36
+ consume_callable: AsyncConsumeCallable,
28
37
  ) -> AsyncConsumeCallable:
29
- return lambda event, subscriber: middleware.on_consume(event, subscriber, consume_callable)
38
+ return lambda event, subscriber, execution_context: middleware.on_consume(
39
+ event, subscriber, consume_callable, execution_context
40
+ )
@@ -2,19 +2,36 @@ from abc import abstractmethod
2
2
 
3
3
  from buz.event import Event
4
4
  from buz.event.async_subscriber import AsyncSubscriber
5
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
5
6
  from buz.event.middleware.async_consume_middleware import AsyncConsumeMiddleware, AsyncConsumeCallable
6
7
 
7
8
 
8
9
  class BaseAsyncConsumeMiddleware(AsyncConsumeMiddleware):
9
- async def on_consume(self, event: Event, subscriber: AsyncSubscriber, consume: AsyncConsumeCallable) -> None:
10
- await self._before_on_consume(event, subscriber)
11
- await consume(event, subscriber)
12
- await self._after_on_consume(event, subscriber)
10
+ async def on_consume(
11
+ self,
12
+ event: Event,
13
+ subscriber: AsyncSubscriber,
14
+ consume: AsyncConsumeCallable,
15
+ execution_context: ExecutionContext,
16
+ ) -> None:
17
+ await self._before_on_consume(event, subscriber, execution_context)
18
+ await consume(event, subscriber, execution_context)
19
+ await self._after_on_consume(event, subscriber, execution_context)
13
20
 
14
21
  @abstractmethod
15
- async def _before_on_consume(self, event: Event, subscriber: AsyncSubscriber) -> None:
22
+ async def _before_on_consume(
23
+ self,
24
+ event: Event,
25
+ subscriber: AsyncSubscriber,
26
+ execution_context: ExecutionContext,
27
+ ) -> None:
16
28
  pass
17
29
 
18
30
  @abstractmethod
19
- async def _after_on_consume(self, event: Event, subscriber: AsyncSubscriber) -> None:
31
+ async def _after_on_consume(
32
+ self,
33
+ event: Event,
34
+ subscriber: AsyncSubscriber,
35
+ execution_context: ExecutionContext,
36
+ ) -> None:
20
37
  pass
@@ -1,19 +1,22 @@
1
1
  from abc import abstractmethod
2
2
 
3
3
  from buz.event import Event, Subscriber
4
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
4
5
  from buz.event.middleware import ConsumeMiddleware, ConsumeCallable
5
6
 
6
7
 
7
8
  class BaseConsumeMiddleware(ConsumeMiddleware):
8
- def on_consume(self, event: Event, subscriber: Subscriber, consume: ConsumeCallable) -> None:
9
- self._before_on_consume(event, subscriber)
10
- consume(event, subscriber)
11
- self._after_on_consume(event, subscriber)
9
+ def on_consume(
10
+ self, event: Event, subscriber: Subscriber, consume: ConsumeCallable, execution_context: ExecutionContext
11
+ ) -> None:
12
+ self._before_on_consume(event, subscriber, execution_context)
13
+ consume(event, subscriber, execution_context)
14
+ self._after_on_consume(event, subscriber, execution_context)
12
15
 
13
16
  @abstractmethod
14
- def _before_on_consume(self, event: Event, subscriber: Subscriber) -> None:
17
+ def _before_on_consume(self, event: Event, subscriber: Subscriber, execution_context: ExecutionContext) -> None:
15
18
  pass
16
19
 
17
20
  @abstractmethod
18
- def _after_on_consume(self, event: Event, subscriber: Subscriber) -> None:
21
+ def _after_on_consume(self, event: Event, subscriber: Subscriber, execution_context: ExecutionContext) -> None:
19
22
  pass
@@ -2,12 +2,15 @@ from abc import abstractmethod
2
2
  from typing import Callable
3
3
 
4
4
  from buz.event import Event, Subscriber
5
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
5
6
  from buz.middleware import Middleware
6
7
 
7
- ConsumeCallable = Callable[[Event, Subscriber], None]
8
+ ConsumeCallable = Callable[[Event, Subscriber, ExecutionContext], None]
8
9
 
9
10
 
10
11
  class ConsumeMiddleware(Middleware):
11
12
  @abstractmethod
12
- def on_consume(self, event: Event, subscriber: Subscriber, consume: ConsumeCallable) -> None:
13
+ def on_consume(
14
+ self, event: Event, subscriber: Subscriber, consume: ConsumeCallable, execution_context: ExecutionContext
15
+ ) -> None:
13
16
  pass
@@ -1,5 +1,6 @@
1
1
  from typing import Sequence
2
2
  from buz.event import Event, Subscriber
3
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
3
4
  from buz.event.middleware import ConsumeMiddleware, ConsumeCallable
4
5
  from buz.middleware import MiddlewareChainBuilder
5
6
 
@@ -9,15 +10,23 @@ class ConsumeMiddlewareChainResolver:
9
10
  self.__middlewares = middlewares
10
11
  self.__middleware_chain_builder: MiddlewareChainBuilder[
11
12
  ConsumeCallable, ConsumeMiddleware
12
- ] = MiddlewareChainBuilder(middlewares)
13
+ ] = MiddlewareChainBuilder(self.__middlewares)
13
14
 
14
- def resolve(self, event: Event, subscriber: Subscriber, consume: ConsumeCallable) -> None:
15
+ def resolve(
16
+ self,
17
+ event: Event,
18
+ subscriber: Subscriber,
19
+ consume: ConsumeCallable,
20
+ execution_context: ExecutionContext,
21
+ ) -> None:
15
22
  chain_callable: ConsumeCallable = self.__middleware_chain_builder.get_chain_callable(
16
23
  consume, self.__get_middleware_callable
17
24
  )
18
- chain_callable(event, subscriber)
25
+ chain_callable(event, subscriber, execution_context)
19
26
 
20
27
  def __get_middleware_callable(
21
28
  self, middleware: ConsumeMiddleware, consume_callable: ConsumeCallable
22
29
  ) -> ConsumeCallable:
23
- return lambda event, subscriber: middleware.on_consume(event, subscriber, consume_callable)
30
+ return lambda event, subscriber, execution_context: middleware.on_consume(
31
+ event, subscriber, consume_callable, execution_context
32
+ )
File without changes
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+ from buz.event.infrastructure.models.delivery_context import DeliveryContext
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class SyncDeliveryContext(DeliveryContext):
7
+ pass
@@ -1,12 +1,14 @@
1
1
  from typing import Optional, Iterable
2
2
 
3
3
  from buz.event import Event, EventBus, Subscriber
4
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
4
5
  from buz.event.middleware import (
5
6
  PublishMiddleware,
6
7
  ConsumeMiddleware,
7
8
  PublishMiddlewareChainResolver,
8
9
  ConsumeMiddlewareChainResolver,
9
10
  )
11
+ from buz.event.sync.models.sync_delivery_context import SyncDeliveryContext
10
12
  from buz.locator import Locator
11
13
 
12
14
 
@@ -26,10 +28,16 @@ class SyncEventBus(EventBus):
26
28
 
27
29
  def __perform_publish(self, event: Event) -> None:
28
30
  subscribers = self.__locator.get(event)
31
+ execution_context = ExecutionContext(delivery_context=SyncDeliveryContext())
29
32
  for subscriber in subscribers:
30
- self.__consume_middleware_chain_resolver.resolve(event, subscriber, self.__perform_consume)
33
+ self.__consume_middleware_chain_resolver.resolve(
34
+ event=event,
35
+ subscriber=subscriber,
36
+ execution_context=execution_context,
37
+ consume=self.__perform_consume,
38
+ )
31
39
 
32
- def __perform_consume(self, event: Event, subscriber: Subscriber) -> None:
40
+ def __perform_consume(self, event: Event, subscriber: Subscriber, execution_context: ExecutionContext) -> None:
33
41
  subscriber.consume(event)
34
42
 
35
43
  def bulk_publish(self, events: Iterable[Event]) -> None:
buz/handler.py CHANGED
@@ -1,13 +1,16 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Type
2
+ from typing import Generic, Type, TypeVar
3
3
 
4
4
  from buz import Message
5
5
 
6
6
 
7
- class Handler(ABC):
7
+ TMessage = TypeVar("TMessage", bound=Message)
8
+
9
+
10
+ class Handler(Generic[TMessage], ABC):
8
11
  @classmethod
9
12
  @abstractmethod
10
- def handles(cls) -> Type[Message]:
13
+ def handles(cls) -> Type[TMessage]:
11
14
  pass
12
15
 
13
16
  @classmethod
@@ -9,6 +9,7 @@ class KafkaPollRecord:
9
9
  key: Optional[Union[str, bytes]]
10
10
  headers: list[tuple[str, bytes]]
11
11
  value: Optional[bytes]
12
+ timestamp: int
12
13
  partition: int
13
14
  topic: str
14
15
  offset: int
@@ -1,27 +1,36 @@
1
- from typing import Type, get_type_hints, Any
1
+ from typing import Generic, Type, TypeVar, cast, get_type_hints, Any
2
2
 
3
3
  from buz.query import Query
4
4
  from buz.query.asynchronous.query_handler import QueryHandler
5
+ from buz.query.query_response import QueryResponse
5
6
 
7
+ TQuery = TypeVar("TQuery", bound=Query)
8
+ TQueryResponse = TypeVar("TQueryResponse", bound=QueryResponse)
6
9
 
7
- class BaseQueryHandler(QueryHandler):
10
+
11
+ class BaseQueryHandler(Generic[TQuery, TQueryResponse], QueryHandler[TQuery, TQueryResponse]):
8
12
  @classmethod
9
13
  def fqn(cls) -> str:
10
14
  return f"query_handler.{cls.__module__}.{cls.__name__}"
11
15
 
12
16
  @classmethod
13
- def handles(cls) -> Type[Query]:
17
+ def handles(cls) -> Type[TQuery]:
14
18
  handle_types = get_type_hints(cls.handle)
15
19
 
16
- if "query" not in handle_types:
20
+ t_query = handle_types.get("query")
21
+ if t_query is None:
17
22
  raise TypeError(
18
23
  f"The method 'handle' in '{cls.fqn()}' doesn't have a parameter named 'query'. Found parameters: {cls.__get_method_parameter_names(handle_types)}"
19
24
  )
20
25
 
21
- if not issubclass(handle_types["query"], Query):
26
+ # TypeVar mask the actual bound type
27
+ if hasattr(t_query, "__bound__"):
28
+ t_query = t_query.__bound__
29
+
30
+ if not issubclass(t_query, Query):
22
31
  raise TypeError(f"The parameter 'query' in '{cls.fqn()}.handle' is not a 'buz.query.Query' subclass")
23
32
 
24
- return handle_types["query"]
33
+ return cast(Type[TQuery], t_query)
25
34
 
26
35
  @classmethod
27
36
  def __get_method_parameter_names(cls, handle_types: dict[str, Any]) -> list[str]:
@@ -1,16 +1,20 @@
1
1
  from abc import abstractmethod
2
- from typing import Type
2
+ from typing import Generic, Type, TypeVar
3
3
 
4
4
  from buz import Handler
5
5
  from buz.query import Query, QueryResponse
6
6
 
7
7
 
8
- class QueryHandler(Handler):
8
+ TQuery = TypeVar("TQuery", bound=Query)
9
+ TQueryResponse = TypeVar("TQueryResponse", bound=QueryResponse)
10
+
11
+
12
+ class QueryHandler(Generic[TQuery, TQueryResponse], Handler[TQuery]):
9
13
  @classmethod
10
14
  @abstractmethod
11
- def handles(cls) -> Type[Query]:
15
+ def handles(cls) -> Type[TQuery]:
12
16
  pass
13
17
 
14
18
  @abstractmethod
15
- async def handle(self, query: Query) -> QueryResponse:
19
+ async def handle(self, query: Query) -> TQueryResponse:
16
20
  pass