buz 2.15.11__py3-none-any.whl → 2.16.0__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.
@@ -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
@@ -1,6 +1,8 @@
1
1
  from typing import Optional, Iterable
2
2
 
3
3
  from buz.event import Event, EventBus
4
+ from buz.event.middleware.publish_middleware import PublishMiddleware
5
+ from buz.event.middleware.publish_middleware_chain_resolver import PublishMiddlewareChainResolver
4
6
  from buz.event.transactional_outbox import OutboxRecord
5
7
  from buz.event.transactional_outbox.event_to_outbox_record_translator import EventToOutboxRecordTranslator
6
8
  from buz.event.transactional_outbox.outbox_record_validation.outbox_record_validator import OutboxRecordValidator
@@ -13,22 +15,40 @@ class TransactionalOutboxEventBus(EventBus):
13
15
  outbox_repository: OutboxRepository,
14
16
  event_to_outbox_record_translator: EventToOutboxRecordTranslator,
15
17
  outbox_record_validator: Optional[OutboxRecordValidator] = None,
16
- ):
18
+ publish_middlewares: Optional[list[PublishMiddleware]] = None,
19
+ ) -> None:
17
20
  self.__outbox_repository = outbox_repository
18
21
  self.__event_to_outbox_record_translator = event_to_outbox_record_translator
19
22
  self.__outbox_record_validator = outbox_record_validator
23
+ self.__publish_middleware_chain_resolver = PublishMiddlewareChainResolver(publish_middlewares or [])
20
24
 
21
25
  def publish(self, event: Event) -> None:
22
- outbox_record = self.__translate_and_validate(event)
26
+ self.__publish_middleware_chain_resolver.resolve(event, self.__perform_publish)
27
+
28
+ def __perform_publish(self, event: Event) -> None:
29
+ outbox_record = self.__process_event(event)
23
30
  self.__outbox_repository.save(outbox_record)
24
31
 
25
32
  def bulk_publish(self, events: Iterable[Event]) -> None:
26
- outbox_records = map(self.__translate_and_validate, events)
33
+ outbox_records: list[OutboxRecord] = []
34
+ for event in events:
35
+ self.__publish_middleware_chain_resolver.resolve(
36
+ event, lambda resolved_event: outbox_records.append(self.__process_event(resolved_event))
37
+ )
38
+
39
+ if len(outbox_records) == 0:
40
+ return None
41
+
27
42
  self.__outbox_repository.bulk_create(outbox_records)
28
43
 
29
- # Raises OutboxRecordValidationException: If any validation inside outbox_record_validator fails
30
- def __translate_and_validate(self, event: Event) -> OutboxRecord:
31
- outbox_record = self.__event_to_outbox_record_translator.translate(event)
44
+ def __process_event(self, event: Event) -> OutboxRecord:
45
+ outbox_record = self.__translate_event(event)
46
+ self.__validate_outbox_record(outbox_record)
47
+ return outbox_record
48
+
49
+ def __translate_event(self, event: Event) -> OutboxRecord:
50
+ return self.__event_to_outbox_record_translator.translate(event)
51
+
52
+ def __validate_outbox_record(self, outbox_record: OutboxRecord) -> None:
32
53
  if self.__outbox_record_validator is not None:
33
54
  self.__outbox_record_validator.validate(record=outbox_record)
34
- return outbox_record
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
@@ -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
@@ -1,27 +1,36 @@
1
- from typing import Type, get_type_hints, Any
1
+ from typing import Generic, Type, get_type_hints, Any, cast, TypeVar
2
2
 
3
3
  from buz.query import Query
4
+ from buz.query.query_response import QueryResponse
4
5
  from buz.query.synchronous.query_handler import QueryHandler
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,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.query import Query, QueryResponse
6
6
 
7
+ TQuery = TypeVar("TQuery", bound=Query)
8
+ TQueryResponse = TypeVar("TQueryResponse", bound=QueryResponse)
7
9
 
8
- class QueryHandler(Handler):
10
+
11
+ class QueryHandler(Generic[TQuery, TQueryResponse], Handler[TQuery], ABC):
9
12
  @classmethod
10
13
  @abstractmethod
11
- def handles(cls) -> Type[Query]:
14
+ def handles(cls) -> Type[TQuery]:
12
15
  pass
13
16
 
14
17
  @abstractmethod
15
- def handle(self, query: Query) -> QueryResponse:
18
+ def handle(self, query: TQuery) -> TQueryResponse:
16
19
  pass
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Fever
3
+ Copyright (c) 2025 Fever
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,438 @@
1
+ Metadata-Version: 2.1
2
+ Name: buz
3
+ Version: 2.16.0
4
+ Summary: Buz is a set of light, simple and extensible implementations of event, command and query buses.
5
+ License: MIT
6
+ Author: Luis Pintado Lozano
7
+ Author-email: luis.pintado.lozano@gmail.com
8
+ Maintainer: Fever - Platform Squad
9
+ Maintainer-email: platform@feverup.com
10
+ Requires-Python: >=3.9
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Typing :: Typed
22
+ Provides-Extra: aiokafka
23
+ Provides-Extra: kombu
24
+ Provides-Extra: pypendency
25
+ Requires-Dist: aiohttp (>=3.12.15,<4.0.0)
26
+ Requires-Dist: aiokafka[lz4] (==0.12.0) ; extra == "aiokafka"
27
+ Requires-Dist: asgiref (>=3.8.1,<4.0.0) ; extra == "aiokafka"
28
+ Requires-Dist: cachetools (>=5.5.0,<6.0.0)
29
+ Requires-Dist: dacite (>=1.8.1,<2.0.0)
30
+ Requires-Dist: kafka-python-ng (==2.2.3)
31
+ Requires-Dist: kombu (>=4.6.11) ; extra == "kombu"
32
+ Requires-Dist: orjson (>=3.10.1,<4.0.0)
33
+ Requires-Dist: pympler (==1.0.1)
34
+ Requires-Dist: pypendency (>=0,<1) ; extra == "pypendency"
35
+ Requires-Dist: uuid-utils (>=0.9.0,<0.10.0)
36
+ Description-Content-Type: text/markdown
37
+
38
+ # Buz
39
+
40
+ [![PyPI version](https://badge.fury.io/py/buz.svg)](https://badge.fury.io/py/buz)
41
+ [![Python Support](https://img.shields.io/pypi/pyversions/buz.svg)](https://pypi.org/project/buz/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
43
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
44
+
45
+ **Buz** is a lightweight, simple, and extensible Python library that provides implementations of **Event**, **Command**, and **Query** buses following CQRS and Event-Driven Architecture patterns.
46
+
47
+ ## 📋 Table of Contents
48
+
49
+ - [Buz](#buz)
50
+ - [📋 Table of Contents](#-table-of-contents)
51
+ - [✨ Key Features](#-key-features)
52
+ - [🚀 Quick Start](#-quick-start)
53
+ - [Installation](#installation)
54
+ - [Basic Usage](#basic-usage)
55
+ - [Event Bus Example](#event-bus-example)
56
+ - [Command Bus Example](#command-bus-example)
57
+ - [Query Bus Example](#query-bus-example)
58
+ - [🏗️ Architecture](#️-architecture)
59
+ - [Event Bus](#event-bus)
60
+ - [Command Bus](#command-bus)
61
+ - [Query Bus](#query-bus)
62
+ - [🔧 Advanced Features](#-advanced-features)
63
+ - [Middleware System](#middleware-system)
64
+ - [Transactional Outbox Pattern](#transactional-outbox-pattern)
65
+ - [RabbitMQ](#rabbitmq)
66
+ - [Kafka Integration](#kafka-integration)
67
+ - [Async Support](#async-support)
68
+ - [📦 Message Brokers](#-message-brokers)
69
+ - [Supported Brokers](#supported-brokers)
70
+ - [🧪 Testing](#-testing)
71
+ - [🔗 Related Projects](#-related-projects)
72
+ - [📋 Requirements](#-requirements)
73
+ - [🤝 Contributing](#-contributing)
74
+ - [Development Setup](#development-setup)
75
+ - [📄 License](#-license)
76
+ - [📚 Documentation](#-documentation)
77
+ - [🙋‍♀️ Support](#️-support)
78
+
79
+ ## ✨ Key Features
80
+
81
+ - 🚌 **Bus Types**: Event, Command, and Query buses for clean architecture
82
+ - 🔄 **Sync & Async Support**: Both synchronous and asynchronous implementations
83
+ - 🔧 **Middleware System**: Extensible middleware for cross-cutting concerns
84
+ - 📦 **Message Brokers**: Support for Kafka, RabbitMQ (via Kombu), and in-memory
85
+ - 🔒 **Transactional Outbox**: Reliable event publishing with transactional guarantees
86
+ - 🎯 **Dependency Injection**: Built-in locator pattern for handler resolution
87
+ - 📝 **Type Safety**: Fully typed with mypy support
88
+ - 🪶 **Lightweight**: Minimal dependencies, maximum flexibility
89
+
90
+ ## 🚀 Quick Start
91
+
92
+ ### Installation
93
+
94
+ ```bash
95
+ # Basic installation
96
+ pip install buz
97
+
98
+ # With Kafka support
99
+ pip install buz[aiokafka]
100
+
101
+ # With RabbitMQ support
102
+ pip install buz[kombu]
103
+
104
+ # With dependency injection
105
+ pip install buz[pypendency]
106
+ ```
107
+
108
+ ### Basic Usage
109
+
110
+ #### Event Bus Example
111
+
112
+ ```python
113
+ from dataclasses import dataclass
114
+ from buz import Message
115
+ from buz.event import Event, BaseSubscriber
116
+ from buz.event.sync import SyncEventBus
117
+ from buz.locator.sync import InstanceLocator
118
+
119
+ @dataclass(frozen=True)
120
+ class UserCreated(Event):
121
+ user_id: str
122
+ email: str
123
+
124
+ class EmailSubscriber(BaseSubscriber):
125
+ def consume(self, event: UserCreated) -> None:
126
+ print(f"Sending welcome email to {event.email}")
127
+
128
+ class AnalyticsSubscriber(BaseSubscriber):
129
+ def consume(self, event: UserCreated) -> None:
130
+ print(f"Tracking user creation: {event.user_id}")
131
+
132
+ # Setup
133
+ locator: InstanceLocator = InstanceLocator()
134
+ locator.register(EmailSubscriber())
135
+ locator.register(AnalyticsSubscriber())
136
+
137
+ event_bus = SyncEventBus(locator)
138
+
139
+ # Usage
140
+ event = UserCreated(user_id="123", email="user@example.com")
141
+ event_bus.publish(event)
142
+ ```
143
+
144
+ #### Command Bus Example
145
+
146
+ ```python
147
+ from dataclasses import dataclass
148
+ from buz.command import Command
149
+ from buz.command.synchronous import BaseCommandHandler
150
+ from buz.command.synchronous.self_process import SelfProcessCommandBus
151
+ from buz.locator.sync import InstanceLocator
152
+
153
+ @dataclass(frozen=True)
154
+ class CreateUser(Command):
155
+ email: str
156
+ name: str
157
+
158
+ class CreateUserCommandHandler(BaseCommandHandler):
159
+ def handle(self, command: CreateUser) -> None:
160
+ # Business logic here
161
+ print(f"Creating user: {command.name} ({command.email})")
162
+
163
+ # Setup
164
+ locator = InstanceLocator()
165
+ locator.register(CreateUserCommandHandler())
166
+
167
+ command_bus = SelfProcessCommandBus(locator)
168
+
169
+ # Usage
170
+ command = CreateUser(email="user@example.com", name="John Doe")
171
+ command_bus.handle(command)
172
+ ```
173
+
174
+ #### Query Bus Example
175
+
176
+ ```python
177
+ from dataclasses import dataclass
178
+ from buz.query import Query, QueryResponse
179
+ from buz.query.synchronous import BaseQueryHandler
180
+ from buz.query.synchronous.self_process import SelfProcessQueryBus
181
+ from buz.locator.sync import InstanceLocator
182
+
183
+ @dataclass(frozen=True)
184
+ class GetUser(Query):
185
+ user_id: str
186
+
187
+ @dataclass(frozen=True)
188
+ class User:
189
+ user_id: str
190
+ name: str
191
+ email: str
192
+
193
+ class GetUserQueryHandler(BaseQueryHandler):
194
+ def handle(self, query: GetUser) -> QueryResponse:
195
+ # Business logic here
196
+ return QueryResponse(
197
+ content=User(
198
+ user_id=query.user_id,
199
+ name="John Doe",
200
+ email="john@example.com"
201
+ )
202
+ )
203
+
204
+ # Setup
205
+ locator = InstanceLocator()
206
+ locator.register(GetUserQueryHandler())
207
+
208
+ query_bus = SelfProcessQueryBus(locator)
209
+
210
+ # Usage
211
+ query = GetUser(user_id="123")
212
+ query_response = query_bus.handle(query)
213
+ user = query_response.content
214
+ print(f"User: {user.name}")
215
+ ```
216
+
217
+ ## 🏗️ Architecture
218
+
219
+ Buz implements the **Command Query Responsibility Segregation (CQRS)** pattern with distinct buses:
220
+
221
+ ### Event Bus
222
+
223
+ - **Purpose**: Publish domain events and notify multiple subscribers
224
+ - **Pattern**: Pub/Sub with multiple handlers per event
225
+ - **Use Cases**: Domain event broadcasting, eventual consistency, integration events
226
+
227
+ ### Command Bus
228
+
229
+ - **Purpose**: Execute business operations and commands
230
+ - **Pattern**: Single handler per command
231
+ - **Use Cases**: Business logic execution, write operations, state changes
232
+
233
+ ### Query Bus
234
+
235
+ - **Purpose**: Retrieve data and execute queries
236
+ - **Pattern**: Single handler per query with typed responses
237
+ - **Use Cases**: Data retrieval, read operations, projections
238
+
239
+ ## 🔧 Advanced Features
240
+
241
+ ### Middleware System
242
+
243
+ Add cross-cutting concerns like logging, validation, and metrics:
244
+
245
+ ```python
246
+ from datetime import datetime
247
+ from buz.event import Event, Subscriber
248
+ from buz.event.middleware import BasePublishMiddleware, BaseConsumeMiddleware
249
+ from buz.event.infrastructure.models.execution_context import ExecutionContext
250
+
251
+ class LoggingPublishMiddleware(BasePublishMiddleware):
252
+ def _before_on_publish(self, event: Event) -> None:
253
+ print(f"Publishing event {event}")
254
+
255
+ def _after_on_publish(self, event: Event) -> None:
256
+ return
257
+
258
+ class MetricsConsumeMiddleware(BaseConsumeMiddleware):
259
+ def __init__(self) -> None:
260
+ self.__consumption_start_time: datetime = datetime.now()
261
+
262
+ def _before_on_consume(
263
+ self,
264
+ event: Event,
265
+ subscriber: Subscriber,
266
+ execution_context: ExecutionContext,
267
+ ) -> None:
268
+ self.__consumption_start_time = datetime.now()
269
+
270
+ def _after_on_consume(
271
+ self,
272
+ event: Event,
273
+ subscriber: Subscriber,
274
+ execution_context: ExecutionContext,
275
+ ) -> None:
276
+ consumption_time_ms = int((datetime.now() - self.__consumption_start_time).total_seconds() * 1000)
277
+ print(
278
+ f"Subscriber {subscriber.fqn()} consumed event {event.id} successfully in {consumption_time_ms} ms"
279
+ )
280
+
281
+ # Apply middleware
282
+ event_bus = SyncEventBus(
283
+ locator=locator,
284
+ publish_middlewares=[LoggingPublishMiddleware()],
285
+ consume_middlewares=[MetricsConsumeMiddleware()]
286
+ )
287
+
288
+ # Usage
289
+ event = UserCreated(user_id="123", email="user@example.com")
290
+ event_bus.publish(event)
291
+ ```
292
+
293
+ ### Transactional Outbox Pattern
294
+
295
+ Ensure reliable event publishing with database transactions:
296
+
297
+ ```python
298
+ from buz.event.transactional_outbox import TransactionalOutboxEventBus
299
+
300
+ # Configure with your database and event bus
301
+ transactional_outbox_bus = TransactionalOutboxEventBus(
302
+ outbox_repository=your_outbox_repository,
303
+ event_to_outbox_record_translator=your_outbox_record_translator,
304
+ ...
305
+ )
306
+
307
+ # Events are stored in database, published later by worker
308
+ transactional_outbox_bus.publish(event)
309
+ ```
310
+
311
+ ### RabbitMQ
312
+
313
+ ```python
314
+ from buz.event.infrastructure.kombu.kombu_event_bus import KombuEventBus
315
+
316
+ kombu_event_bus = KombuEventBus(
317
+ connection=your_connection,
318
+ publish_strategy=your_publish_strategy,
319
+ publish_retry_policy=you_publish_retry_policy,
320
+ ...
321
+ )
322
+
323
+ # Published and consumed in RabbitMQ
324
+ kombu_event_bus.publish(event)
325
+ ```
326
+
327
+ ### Kafka Integration
328
+
329
+ ```python
330
+ from buz.kafka import BuzKafkaEventBus
331
+
332
+ kafka_bus = KafkaEventBus(
333
+ publish_strategy=your_publish_strategy,
334
+ producer=your_producer,
335
+ logger=your_logger,
336
+ ...
337
+ )
338
+
339
+ # Published and consumed in Kafka
340
+ kafka_bus.publish(event)
341
+ ```
342
+
343
+ ### Async Support
344
+
345
+ ```python
346
+ from buz.event.async_event_bus import AsyncEventBus
347
+ from buz.query.asynchronous import QueryBus as AsyncQueryBus
348
+ from buz.command.asynchronous import CommandHandler as AsyncCommandHandler
349
+
350
+
351
+ # Async event bus
352
+ async_event_bus = AsyncEventBus(locator)
353
+ await async_event_bus.publish(event)
354
+
355
+ # Async query bus
356
+ async_query_bus = AsyncQueryBus(locator)
357
+ await async_query_bus.handle(event)
358
+
359
+ # Async command bus
360
+ async_command_bus = AsyncCommandBus(locator)
361
+ await async_command_bus.handle(command)
362
+ ```
363
+
364
+ ## 📦 Message Brokers
365
+
366
+ ### Supported Brokers
367
+
368
+ | Broker | Sync | Async | Installation |
369
+ | --------- | ---- | ----- | --------------------------- |
370
+ | In-Memory | ✅ | ✅ | Built-in |
371
+ | Kafka | ✅ | ✅ | `pip install buz[aiokafka]` |
372
+ | RabbitMQ | ✅ | ❌ | `pip install buz[kombu]` |
373
+
374
+ ## 🧪 Testing
375
+
376
+ Buz includes testing utilities for unit and integration tests:
377
+
378
+ ```python
379
+ from buz.event.sync import SyncEventBus
380
+ from buz.locator.sync import InstanceLocator
381
+
382
+ test_locator = InstanceLocator()
383
+ test_bus = SyncEventBus(test_locator)
384
+
385
+ test_locator.register(EmailSubscriber())
386
+ test_bus.publish(UserCreated(user_id="123", email="test@example.com"))
387
+ ```
388
+
389
+ ## 🔗 Related Projects
390
+
391
+ - **[buz-fever-shared](https://github.com/Feverup/buz-fever-shared)**: Opinionated utilities and standards for Buz
392
+ - **[buz-basic-example](https://github.com/Feverup/buz-basic-example)**: Complete example project with Docker setup
393
+
394
+ ## 📋 Requirements
395
+
396
+ - Python 3.9+
397
+ - Optional dependencies based on features used
398
+
399
+ ## 🤝 Contributing
400
+
401
+ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.
402
+
403
+ ### Development Setup
404
+
405
+ ```bash
406
+ # Clone the repository
407
+ git clone https://github.com/Feverup/buz.git
408
+ cd buz
409
+
410
+ # Install with development dependencies
411
+ make build
412
+
413
+ # Run tests
414
+ make test
415
+
416
+ # Run linting
417
+ make lint
418
+
419
+ # Format code
420
+ make format
421
+ ```
422
+
423
+ ## 📄 License
424
+
425
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
426
+
427
+ ## 📚 Documentation
428
+
429
+ - [Changelog](CHANGELOG.md) - Release notes and version history
430
+
431
+ ## 🙋‍♀️ Support
432
+
433
+ - Create an [Issue](https://github.com/Feverup/buz/issues) for bug reports or feature requests
434
+
435
+ ---
436
+
437
+ Made with ❤️ by the [Fever Platform Team](platform@feverup.com)
438
+
@@ -1,9 +1,9 @@
1
1
  buz/__init__.py,sha256=ctEyEjRm2Z2te3dZ1CLTbJw8_MmVFFqTflZowKeMSvM,109
2
2
  buz/command/__init__.py,sha256=iwaODtzgrlWc1IyD-3YPDN2jbTtwqog9_rS-P-Gmi6k,223
3
3
  buz/command/asynchronous/__init__.py,sha256=aMvcBRDvIP9SUXh1B8BTQS_VPkntem3BomBI_ypEs4U,271
4
- buz/command/asynchronous/base_command_handler.py,sha256=Fm5SYqoki2DXcZUhQmuDhSnZpgxyaid0PRT8rtxZVl4,1128
4
+ buz/command/asynchronous/base_command_handler.py,sha256=8ADx07Rwtnxbmz4f0L65uI8cJRkHr3itzyykC--AmBA,1393
5
5
  buz/command/asynchronous/command_bus.py,sha256=mQPzemGvC14fRADUv6_jA8qlBLSIGmXVorM6xo_V6_A,181
6
- buz/command/asynchronous/command_handler.py,sha256=lHCS4FyoAwq9aDpfy1u11tAtBRxYMXwSxAWhX7-qyX8,330
6
+ buz/command/asynchronous/command_handler.py,sha256=ffVqrE_rK_nN4Yf82A6MQxb7O4OeEfkgR2ioBVGVNCs,437
7
7
  buz/command/asynchronous/middleware/__init__.py,sha256=1CGYsALue16-2nkxg7lHhxiS5EQWX6R6Hr4Lr0qwOcQ,409
8
8
  buz/command/asynchronous/middleware/base_handle_middleware.py,sha256=xvGZW5rzPz13f08bw0QigSszzSwO16hKqL4oKW_IoSs,788
9
9
  buz/command/asynchronous/middleware/handle_middleware.py,sha256=2lCgRRqWkiJLbys4YzWSUupVt8S50vwfEWzVu8PSqjw,449
@@ -13,9 +13,9 @@ buz/command/asynchronous/self_process/self_process_command_bus.py,sha256=PjP46OI
13
13
  buz/command/command.py,sha256=wYpjD16PH23r2VapFEMa-wnm3xNjkR22FXtuLoKfnu0,193
14
14
  buz/command/more_than_one_command_handler_related_exception.py,sha256=dNCMXCAKVckKgrgROyJWqaEyWntyByQKsa6gQfaKE4U,554
15
15
  buz/command/synchronous/__init__.py,sha256=ZAH35jpte6zpxb70iQfZfGSg_OsXqVNYwNkCJy7Qnzk,268
16
- buz/command/synchronous/base_command_handler.py,sha256=P0tyjCh4mKKcBV67JR4m5qosXpTk6qemG-7Zpdu6PD8,1127
16
+ buz/command/synchronous/base_command_handler.py,sha256=GkmxttA1_Wu2X5O7iR-tLRdnmT-JDgeyURPwkjLPGhg,1393
17
17
  buz/command/synchronous/command_bus.py,sha256=FHJH4lerThu5fWRISM0VIK8ebyfqgDGqNNCqItbmGPI,175
18
- buz/command/synchronous/command_handler.py,sha256=nswGhsUBtCHjXVHCN3pToa9WYPab0bvTcTuSPZZbxBU,316
18
+ buz/command/synchronous/command_handler.py,sha256=pP11Njbth89NQiVPqlALtKmabVtOX4sucdbiCp-sceI,422
19
19
  buz/command/synchronous/middleware/__init__.py,sha256=wE97veOW2cCGpTZ-3bvc3aU6NdQmSOYfdX9kAUETJ0w,406
20
20
  buz/command/synchronous/middleware/base_handle_middleware.py,sha256=qHnbucL7j6Dnw7E8ACBf32pzDD9squYPp6UiK2heb5A,774
21
21
  buz/command/synchronous/middleware/handle_middleware.py,sha256=oIsmB30mu-Cd9IQc1qMKbUIaIMHipSEXOw2swVrtbkk,420
@@ -140,10 +140,10 @@ buz/event/transactional_outbox/outbox_record_validation/outbox_record_validation
140
140
  buz/event/transactional_outbox/outbox_record_validation/outbox_record_validator.py,sha256=XGHTT1dH2CJOqhYYnyPJHmZsAuVXuDOeqgJzK7mRidc,328
141
141
  buz/event/transactional_outbox/outbox_record_validation/size_outbox_record_validator.py,sha256=f8sQ5IHfO4J8m5l7rS3JYUoBvx0B1EAFMRsJ0HPQKG8,2436
142
142
  buz/event/transactional_outbox/outbox_repository.py,sha256=Sn7aWaq1G6uiKXcV09l9L1eVQ_bPUTqY-OSD12_H2jU,628
143
- buz/event/transactional_outbox/transactional_outbox_event_bus.py,sha256=S2VIrKCyZG8vztgBagKOJUhp2oJhbLx6oGVHPBplRZ4,1676
143
+ buz/event/transactional_outbox/transactional_outbox_event_bus.py,sha256=9eUevDAKEwouKrSEhf5dpqFwI-FbB5z9R7gIxaFCLno,2551
144
144
  buz/event/transactional_outbox/transactional_outbox_worker.py,sha256=x6kf-Oc4oYKu9S4MTcCqd3VqPNURScTReYJ3Ahx4rKA,2221
145
145
  buz/event/worker.py,sha256=BL9TXB_kyr0Avql9fIcFm3CDNnXPvZB6O6BxVwjtCdA,942
146
- buz/handler.py,sha256=cZqV1NDPGVZQgJ3YSBDhOQ1sdJGdUopxi57yQ6fbPvc,272
146
+ buz/handler.py,sha256=W6jSTo5BNV9u9QKBaEMhLIa3tgQocd6oYEJf5K4EfEU,358
147
147
  buz/kafka/__init__.py,sha256=R3fcyET-SNEAvk_XlBQbHIbQVb63Qiz6lVrif3nDhNU,3435
148
148
  buz/kafka/domain/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
149
149
  buz/kafka/domain/exceptions/not_all_partition_assigned_exception.py,sha256=1Ky6gDh_baD6cGB0MBnjbkkLcw2zQU_kFXPpDZn56z0,400
@@ -223,26 +223,26 @@ buz/middleware/middleware_chain_builder.py,sha256=D9zl5XSF5P65QpnPcbtyFaaVHqBTb6
223
223
  buz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
224
224
  buz/query/__init__.py,sha256=Eq0ppBYFYaDp-Z7uViSqhuOBxjh_q8VSCbFdpMz3FsU,274
225
225
  buz/query/asynchronous/__init__.py,sha256=YhCudd43otBYDi6ZdisSjnAZ0psCkp_ZZ_pb36fWoMY,247
226
- buz/query/asynchronous/base_query_handler.py,sha256=Sj2tnluRvlImHyN6MrlKftLoavVkCWKxm55p2r64ej4,1094
226
+ buz/query/asynchronous/base_query_handler.py,sha256=148JQwx6Yvp_pCDJEdw98r1K0YSQBdNEq9GqwA_Q368,1484
227
227
  buz/query/asynchronous/middleware/__init__.py,sha256=aoei9p2Ti1biFCOAQpsDQ7H_s0Dt5hkWgALJHhfjpoE,404
228
228
  buz/query/asynchronous/middleware/base_handle_middleware.py,sha256=51FliRBVrobao3TSJnYKJz1fJPg8dxSymCFQRiClz9g,874
229
229
  buz/query/asynchronous/middleware/handle_middleware.py,sha256=LfQLKc4jIlUegbGbnMXqCSXgAl6NaRGqvKDN0GuvgY4,462
230
230
  buz/query/asynchronous/middleware/handle_middleware_chain_resolver.py,sha256=iA2faVRvNkuS5S4dB1buP25JGSqfBKBQIKg14hWis2U,1123
231
231
  buz/query/asynchronous/query_bus.py,sha256=L3VIw6VofJzJYH2oie5uFKH-dQ-M6a2VECTd6Seos_o,195
232
- buz/query/asynchronous/query_handler.py,sha256=E8fd49ZvguJLY7WvtbieTS_ksVt154uLVFggenTeZLw,334
232
+ buz/query/asynchronous/query_handler.py,sha256=Me80YBdGP368WYUhEY7n5AmwacFmdqFYyiM6TyDrqKE,501
233
233
  buz/query/asynchronous/self_process/__init__.py,sha256=5y0dGgeDq0ZPCrexVjvJWT6gix8lKd-7Iw7Z3H8dirc,126
234
234
  buz/query/asynchronous/self_process/self_process_query_bus.py,sha256=2PqbJGrjv7ULEAl0yIj-Gn4ylVc3_EvJ0VXIlzUlhNc,1217
235
235
  buz/query/more_than_one_query_handler_related_exception.py,sha256=sEfShwCB1VHdUbf02NSEAyv6pXx4GO9ram0AenBF_dE,516
236
236
  buz/query/query.py,sha256=_YGlCOfywRv4JaHBweIn2AdXMwM5g9UJCWLojNg-qCA,189
237
237
  buz/query/query_response.py,sha256=XaHBi0Vw_3Tw5IxnuMgenZCRH0B-IBR86mtyYwj_eWw,153
238
238
  buz/query/synchronous/__init__.py,sha256=pc56XeFQnTbXB-HEDa2DtvYo9wgKUUHHB3lsCKjv-L4,244
239
- buz/query/synchronous/base_query_handler.py,sha256=0Up77CW0Zk8Dr92u-pT712cGDj5OFUxCIHd6ewZJcNY,1093
239
+ buz/query/synchronous/base_query_handler.py,sha256=a78B0GVeAFScMbef4ii8cjJbVZR-m8WwkxBiWQ2Cfik,1483
240
240
  buz/query/synchronous/middleware/__init__.py,sha256=Yo66O_HODdEfJ-MR5FvWTu7_4UFQooYG8M2IB0crd6U,400
241
241
  buz/query/synchronous/middleware/base_handle_middleware.py,sha256=4lqJB2C-GHl3T5S5dVXp8qgwA-48Cmp8mfQy21-GxNI,846
242
242
  buz/query/synchronous/middleware/handle_middleware.py,sha256=Xr2HSlRrW3slluyUiJ6zH0Rw0oeWD3uMN-1hjuFbC_k,433
243
243
  buz/query/synchronous/middleware/handle_middleware_chain_resolver.py,sha256=OIpGLJ_2a8cGsp-LmA3Q1Lvb1pB65MIuA-geiPrQKHM,1070
244
244
  buz/query/synchronous/query_bus.py,sha256=eYl_sGH5TcELkOXc4RnV4Tkmcs-VLc1vzd-YMduQ1YI,189
245
- buz/query/synchronous/query_handler.py,sha256=2miSCBzVuD861mldG-XflkoKtpKO16Cdvb2bQBLQq9w,328
245
+ buz/query/synchronous/query_handler.py,sha256=q8zqXjU9btid_q4wbL73QgaiWjMzGDFvwZ5AQN4q0CA,505
246
246
  buz/query/synchronous/self_process/__init__.py,sha256=fU1OoXXXH5dMGKz8y7mwTVvyhNj6BCKDTxuxH_q-leM,125
247
247
  buz/query/synchronous/self_process/self_process_query_bus.py,sha256=pKGJxXBWtqU4i0fzb30OCNhAVPCkUh7IlfNzgAhCUC8,1157
248
248
  buz/query/synchronous/synced_async/__init__.py,sha256=TdFmIBeFIpl3Tvmh_FJpJMXJdPdfRxOstVqnPUi23mo,125
@@ -257,7 +257,7 @@ buz/serializer/message_to_json_bytes_serializer.py,sha256=RGZJ64t4t4Pz2FCASZZCv-
257
257
  buz/wrapper/__init__.py,sha256=GnRdJFcncn-qp0hzDG9dBHLmTJSbHFVjE_yr-MdW_n4,77
258
258
  buz/wrapper/async_to_sync.py,sha256=OfK-vrVUhuN-LLLvekLdMbQYtH0ue5lfbvuasj6ovMI,698
259
259
  buz/wrapper/event_loop.py,sha256=pfBJ1g-8A2a3YgW8Gf9Fg0kkewoh3-wgTy2KIFDyfHk,266
260
- buz-2.15.11.dist-info/LICENSE,sha256=Jytu2S-2SPEgsB0y6BF-_LUxIWY7402fl0JSh36TLZE,1062
261
- buz-2.15.11.dist-info/METADATA,sha256=BuIeKxWGRrmcHeBDjBgw9ZW9Et1fisfy0hbqnGjKqHU,1598
262
- buz-2.15.11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
263
- buz-2.15.11.dist-info/RECORD,,
260
+ buz-2.16.0.dist-info/LICENSE,sha256=jcLgcIIVaBqaZNwe0kzGWSU99YgwMcI0IGv142wkYSM,1062
261
+ buz-2.16.0.dist-info/METADATA,sha256=NUJDqpVmTFd9vDNXIhjiaN1N9jKAiUOisQfBFUB6q_Y,12679
262
+ buz-2.16.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
263
+ buz-2.16.0.dist-info/RECORD,,
@@ -1,41 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: buz
3
- Version: 2.15.11
4
- Summary: Buz is a set of light, simple and extensible implementations of event, command and query buses.
5
- License: MIT
6
- Author: Luis Pintado Lozano
7
- Author-email: luis.pintado.lozano@gmail.com
8
- Maintainer: Fever - Platform Squad
9
- Maintainer-email: platform@feverup.com
10
- Requires-Python: >=3.9
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.9
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.8
20
- Classifier: Topic :: Software Development :: Libraries
21
- Classifier: Typing :: Typed
22
- Provides-Extra: aiokafka
23
- Provides-Extra: kombu
24
- Provides-Extra: pypendency
25
- Requires-Dist: aiohttp (>=3.12.15,<4.0.0)
26
- Requires-Dist: aiokafka[lz4] (==0.12.0) ; extra == "aiokafka"
27
- Requires-Dist: asgiref (>=3.8.1,<4.0.0) ; extra == "aiokafka"
28
- Requires-Dist: cachetools (>=5.5.0,<6.0.0)
29
- Requires-Dist: dacite (>=1.8.1,<2.0.0)
30
- Requires-Dist: kafka-python-ng (==2.2.3)
31
- Requires-Dist: kombu (>=4.6.11) ; extra == "kombu"
32
- Requires-Dist: orjson (>=3.10.1,<4.0.0)
33
- Requires-Dist: pympler (==1.0.1)
34
- Requires-Dist: pypendency (>=0,<1) ; extra == "pypendency"
35
- Requires-Dist: uuid-utils (>=0.9.0,<0.10.0)
36
- Description-Content-Type: text/markdown
37
-
38
- # Buz
39
-
40
- Buz is a set of light, simple and extensible implementations of event, command and query buses.
41
-
File without changes