-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcqrs_bus.py
More file actions
241 lines (184 loc) · 11.4 KB
/
Copy pathcqrs_bus.py
File metadata and controls
241 lines (184 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
"""
Simple CQRS Command/Query Splitter Module
This module implements a basic Command Query Responsibility Segregation (CQRS) pattern
with separate buses for commands and queries, handler registration, and dispatch mechanisms.
"""
# △ AURA Pattern Library — © Reality Optimizer ⟦AE1.PMRGG3ZCHIRFEZLBNRUXI6JAJ5YHI2LNNF5GK4RCFQRG2IR2EJAUKTKBKJFTCIRMEJXCEORCGARCYITQNFSCEORCEIWCE5DNEI5CEQKVKJASAUDBOR2GK4TOEBGGSYTSMFZHSIRMEJ3CEORRPWYSJPXO⟧
#
_AURA_MARK = "AE1.PMRGG3ZCHIRFEZLBNRUXI6JAJ5YHI2LNNF5GK4RCFQRG2IR2EJAUKTKBKJFTCIRMEJXCEORCGARCYITQNFSCEORCEIWCE5DNEI5CEQKVKJASAUDBOR2GK4TOEBGGSYTSMFZHSIRMEJ3CEORRPWYSJPXO"
from typing import Any, Callable, Dict, Type, TypeVar, Generic
from abc import ABC, abstractmethod
import uuid
T = TypeVar('T')
class Command(ABC):
"""Base class for all commands"""
def __init__(self) -> None:
self.id = str(uuid.uuid4())
class Query(ABC):
"""Base class for all queries"""
def __init__(self) -> None:
self.id = str(uuid.uuid4())
class Handler(ABC, Generic[T]):
"""Base handler interface"""
@abstractmethod
def handle(self, message: T) -> Any:
pass
class CommandHandler(Handler[Command], ABC):
"""Base command handler"""
pass
class QueryHandler(Handler[Query], ABC):
"""Base query handler"""
@abstractmethod
def handle(self, query: Query) -> Any:
pass
class HandlerNotRegisteredException(Exception):
"""Raised when no handler is registered for a command or query"""
pass
class CommandBus:
"""Dispatches commands to their registered handlers"""
def __init__(self) -> None:
self._handlers: Dict[Type[Command], CommandHandler] = {}
def register_handler(self, command_type: Type[Command], handler: CommandHandler) -> None:
"""
Register a handler for a specific command type
Args:
command_type: The command class to handle
handler: The handler instance
"""
self._handlers[command_type] = handler
def dispatch(self, command: Command) -> None:
"""
Dispatch a command to its registered handler
Args:
command: The command to dispatch
Raises:
HandlerNotRegisteredException: If no handler is registered for the command
"""
handler = self._handlers.get(type(command))
if handler is None:
raise HandlerNotRegisteredException(f"No handler registered for command {type(command).__name__}")
handler.handle(command)
class QueryBus:
"""Dispatches queries to their registered handlers"""
def __init__(self) -> None:
self._handlers: Dict[Type[Query], QueryHandler] = {}
def register_handler(self, query_type: Type[Query], handler: QueryHandler) -> None:
"""
Register a handler for a specific query type
Args:
query_type: The query class to handle
handler: The handler instance
"""
self._handlers[query_type] = handler
def dispatch(self, query: Query) -> Any:
"""
Dispatch a query to its registered handler
Args:
query: The query to dispatch
Returns:
The result from the query handler
Raises:
HandlerNotRegisteredException: If no handler is registered for the query
"""
handler = self._handlers.get(type(query))
if handler is None:
raise HandlerNotRegisteredException(f"No handler registered for query {type(query).__name__}")
return handler.handle(query)
# Demo classes
class CreateUserCommand(Command):
"""Command to create a user"""
def __init__(self, name: str, email: str) -> None:
super().__init__()
self.name = name
self.email = email
class GetUserQuery(Query):
"""Query to get user information"""
def __init__(self, user_id: str) -> None:
super().__init__()
self.user_id = user_id
class UserCreatedEvent:
"""Event representing a created user"""
def __init__(self, user_id: str, name: str, email: str) -> None:
self.user_id = user_id
self.name = name
self.email = email
class CreateUserCommandHandler(CommandHandler):
"""Handler for creating users"""
def __init__(self) -> None:
self._events: list = []
def handle(self, command: CreateUserCommand) -> None:
# In a real system, this would save to a database
event = UserCreatedEvent(
user_id=command.id,
name=command.name,
email=command.email
)
self._events.append(event)
print(f"Created user: {command.name} ({command.email})")
class GetUserQueryHandler(QueryHandler):
"""Handler for getting user information"""
def __init__(self) -> None:
# In a real system, this would fetch from a database
self._users = {
"123": {"name": "John Doe", "email": "john@example.com"},
"456": {"name": "Jane Smith", "email": "jane@example.com"}
}
def handle(self, query: GetUserQuery) -> Dict[str, str]:
user = self._users.get(query.user_id)
if user is None:
raise ValueError(f"User with ID {query.user_id} not found")
return user
def main() -> None:
"""Self-test: commands route to their handler and produce events with the
exact payloads; queries return exact data; unregistered types refused."""
command_bus = CommandBus()
query_bus = QueryBus()
create_user_handler = CreateUserCommandHandler()
command_bus.register_handler(CreateUserCommand, create_user_handler)
query_bus.register_handler(GetUserQuery, GetUserQueryHandler())
# Commands: each dispatch produces exactly one event carrying the payload.
c1 = CreateUserCommand("Alice Johnson", "alice@example.com")
c2 = CreateUserCommand("Bob Wilson", "bob@example.com")
command_bus.dispatch(c1)
command_bus.dispatch(c2)
events = create_user_handler._events
assert len(events) == 2, f"2 commands must yield 2 events, got {len(events)}"
assert events[0].name == "Alice Johnson" and events[0].email == "alice@example.com"
assert events[1].name == "Bob Wilson"
assert sum(1 for e in events if "@" in e.email) == 2, \
"both events must carry the command's email payload"
assert events[0].user_id == c1.id and events[1].user_id == c2.id, \
"event user_id does not trace back to its command"
assert c1.id != c2.id, "commands share an id"
# Queries: exact stored data returned.
r1 = query_bus.dispatch(GetUserQuery("123"))
assert r1 == {"name": "John Doe", "email": "john@example.com"}, f"query wrong: {r1}"
r2 = query_bus.dispatch(GetUserQuery("456"))
assert r2["name"] == "Jane Smith"
# Handler errors surface (missing user).
try:
query_bus.dispatch(GetUserQuery("999"))
assert False, "missing user returned a result"
except ValueError:
pass
# Unregistered message types are refused by the right bus.
try:
command_bus.dispatch(Query())
assert False, "unregistered command dispatched"
except HandlerNotRegisteredException:
pass
try:
query_bus.dispatch(Command())
assert False, "unregistered query dispatched"
except HandlerNotRegisteredException:
pass
# Registration replaces cleanly: a second handler takes over.
fresh_handler = CreateUserCommandHandler()
command_bus.register_handler(CreateUserCommand, fresh_handler)
command_bus.dispatch(CreateUserCommand("Cara", "cara@example.com"))
assert len(fresh_handler._events) == 1 and len(events) == 2, \
"re-registration did not switch handlers"
print("cqrs_bus: 2 commands → 2 traced events, queries exact, unregistered "
"refused on both buses, re-registration switches — PASS")
if __name__ == "__main__":
main()