def test_construct_message_from_either_event_or_command(): identifier = str(uuid4()) command = Register(id=identifier, email="*****@*****.**", name="John Doe") message = Message.to_message(command) assert message is not None assert type(message) is Message # Verify Message Content assert message.type == fully_qualified_name(Register) assert message.stream_name == f"{User.meta_.stream_name}:command-{identifier}" assert message.metadata.kind == "COMMAND" assert message.data == command.to_dict() event = Registered(id=identifier, email="*****@*****.**", name="John Doe") # This simulates the call by UnitOfWork message = Message.to_message(event) assert message is not None assert type(message) is Message # Verify Message Content assert message.type == fully_qualified_name(Registered) assert message.stream_name == f"{User.meta_.stream_name}-{identifier}" assert message.metadata.kind == "EVENT" assert message.data == event.to_dict() assert message.time is None
async def test_that_any_message_can_be_handled_with_any_handler(test_domain): test_domain.register(User) test_domain.register(Registered) test_domain.register(Post) test_domain.register(Created) test_domain.register(SystemMetrics) identifier = str(uuid4()) registered = Registered( id=identifier, email="*****@*****.**", name="John Doe", password_hash="hash", ) user = User(**registered.to_dict()) message1 = Message.to_aggregate_event_message(user, registered) post_identifier = str(uuid4()) created = Created(id=post_identifier, topic="Foo", content="Bar") post = Post(**created.to_dict()) test_domain.event_store.store.append_aggregate_event(post, created) message2 = Message.to_aggregate_event_message(post, created) engine = Engine(domain=test_domain, test_mode=True) await engine.handle_message(SystemMetrics, message1) await engine.handle_message(SystemMetrics, message2) global counter assert counter == 2
def test_construct_message_from_event(test_domain): identifier = str(uuid4()) event = Registered(id=identifier, email="*****@*****.**", name="John Doe") user = User(**event.to_dict()) # This simulates the call by UnitOfWork message = Message.to_aggregate_event_message(user, event) assert message is not None assert type(message) is Message # Verify Message Content assert message.type == fully_qualified_name(Registered) assert message.stream_name == f"{User.meta_.stream_name}-{identifier}" assert message.metadata.kind == "EVENT" assert message.metadata.owner == test_domain.domain_name assert message.data == event.to_dict() assert message.time is None assert message.expected_version == user._version # Verify Message Dict message_dict = message.to_dict() assert message_dict["type"] == fully_qualified_name(Registered) assert message_dict["metadata"]["kind"] == "EVENT" assert message_dict["metadata"]["owner"] == test_domain.domain_name assert message_dict[ "stream_name"] == f"{User.meta_.stream_name}-{identifier}" assert message_dict["data"] == event.to_dict() assert message_dict["time"] is None assert message_dict["expected_version"] == user._version
def test_construct_message_from_command(test_domain): identifier = str(uuid4()) command = Register(id=identifier, email="*****@*****.**", name="John Doe") message = Message.to_command_message(command) assert message is not None assert type(message) is Message # Verify Message Content assert message.type == fully_qualified_name(Register) assert message.stream_name == f"{User.meta_.stream_name}:command-{identifier}" assert message.metadata.kind == "COMMAND" assert message.metadata.owner == test_domain.domain_name assert message.data == command.to_dict() assert message.time is None # Verify Message Dict message_dict = message.to_dict() assert message_dict["type"] == fully_qualified_name(Register) assert message_dict["metadata"]["kind"] == "COMMAND" assert message_dict["metadata"]["owner"] == test_domain.domain_name assert (message_dict["stream_name"] == f"{User.meta_.stream_name}:command-{identifier}") assert message_dict["data"] == command.to_dict() assert message_dict["time"] is None
async def test_that_an_event_handler_can_be_associated_with_an_all_stream(test_domain): test_domain.register(User) test_domain.register(Registered) test_domain.register(UserEventHandler, aggregate_cls=User) identifier = str(uuid4()) user = User( id=identifier, email="*****@*****.**", name="John Doe", password_hash="hash", ) event = Registered( id=identifier, email="*****@*****.**", name="John Doe", password_hash="hash", ) message = Message.to_aggregate_event_message(user, event) engine = Engine(domain=test_domain, test_mode=True) await engine.handle_message(UserEventHandler, message) global counter assert counter == 1
def registered_event_message(user_id): return Message.to_event_message( Registered( user_id=user_id, email="*****@*****.**", name="John Doe", ))
def register_command_message(user_id): return Message.to_command_message( Register( user_id=user_id, email="*****@*****.**", name="John Doe", ))
def append_event(self, event: BaseEvent) -> int: message = Message.to_event_message(event) return self._write( message.stream_name, message.type, message.data, metadata=message.metadata.to_dict(), )
def append_command(self, command: BaseCommand) -> int: message = Message.to_command_message(command) return self._write( message.stream_name, message.type, message.data, metadata=message.metadata.to_dict(), )
def test_origin_stream_name_in_command_from_event(user_id): g.message_in_context = registered_event_message(user_id) command_message = Message.to_command_message( Register( user_id=user_id, email="*****@*****.**", name="John Doe", )) assert command_message.metadata.origin_stream_name == f"user-{user_id}"
def test_construct_command_from_message(test_domain): identifier = str(uuid4()) command = Register(id=identifier, email="*****@*****.**", name="John Doe") message = Message.to_command_message(command) reconstructed_command = message.to_object() assert isinstance(reconstructed_command, Register) assert reconstructed_command.id == identifier
def test_construct_event_from_message(test_domain): identifier = str(uuid4()) event = Registered(id=identifier, email="*****@*****.**", name="John Doe") user = User(**event.to_dict()) message = Message.to_aggregate_event_message(user, event) reconstructed_event = message.to_object() assert isinstance(reconstructed_event, Registered) assert reconstructed_event.id == identifier
def test_origin_stream_name_in_event_from_command_without_origin_stream_name( user_id): g.message_in_context = register_command_message(user_id) event_message = Message.to_event_message( Registered( user_id=user_id, email="*****@*****.**", name="John Doe", )) assert event_message.metadata.origin_stream_name is None
def last_event_of_type(self, event_cls: Type[BaseEvent], stream_name: str = None) -> BaseEvent: stream_name = stream_name or "$all" events = [ event for event in self.domain.event_store.store._read(stream_name) if event["type"] == fqn(event_cls) ] return Message.from_dict( events[-1]).to_object() if len(events) > 0 else None
async def test_message_filtering_for_event_handlers_with_defined_origin_stream( test_domain, ): test_domain.register(UserEventHandler, aggregate_cls=User) test_domain.register(EmailEventHandler, stream_name="email", source_stream="user") engine = Engine(test_domain, test_mode=True) email_event_handler_subscription = engine._subscriptions[fqn( EmailEventHandler)] identifier = str(uuid4()) user = User(id=identifier, email="*****@*****.**", name="John Doe") email = Email(id=identifier, email="*****@*****.**") # Construct 3 dummy messages and modify Sent message to have originated from the user stream messages = [ Message.to_aggregate_event_message( user, Registered(id=identifier, email="*****@*****.**", name="John Doe")), Message.to_aggregate_event_message( user, Activated(id=identifier, activated_at=datetime.utcnow())), Message.to_aggregate_event_message( email, Sent(email="*****@*****.**", sent_at=datetime.utcnow())), ] messages[2].metadata.origin_stream_name = f"user-{identifier}" # Mock `read` method and have it return the 3 messages mock_store_read = mock.Mock() mock_store_read.return_value = messages email_event_handler_subscription.store.read = mock_store_read filtered_messages = ( await email_event_handler_subscription.get_next_batch_of_messages()) assert len(filtered_messages) == 1 assert filtered_messages[0].type == fqn(Sent)
def test_applying_events(): identifier = str(uuid4()) registered = UserRegistered(user_id=identifier, name="John Doe", email="*****@*****.**") activated = UserActivated(user_id=identifier) renamed = UserRenamed(user_id=identifier, name="Jane Doe") user = User.register(**registered.to_dict()) msg_registered = Message.to_aggregate_event_message(user, registered) user._apply(msg_registered.to_dict()) assert user.status == UserStatus.INACTIVE.value msg_activated = Message.to_aggregate_event_message(user, activated) user._apply(msg_activated.to_dict()) assert user.status == UserStatus.ACTIVE.value msg_renamed = Message.to_aggregate_event_message(user, renamed) user._apply(msg_renamed.to_dict()) assert user.name == "Jane Doe"
def read( self, stream_name: str, sql: str = None, position: int = 0, no_of_messages: int = 1000, ): raw_messages = self._read(stream_name, sql=sql, position=position, no_of_messages=no_of_messages) messages = [] for raw_message in raw_messages: messages.append(Message.from_dict(raw_message)) return messages
def append_aggregate_event(self, aggregate: BaseEventSourcedAggregate, event: BaseEvent) -> int: message = Message.to_aggregate_event_message(aggregate, event) position = self._write( message.stream_name, message.type, message.data, metadata=message.metadata.to_dict(), expected_version=message.expected_version, ) # Increment aggregate's version as we process events # to correctly handle expected version aggregate._version += 1 return position
def test_origin_stream_name_in_aggregate_event_from_command_without_origin_stream_name( user_id, ): g.message_in_context = register_command_message(user_id) user = User( id=user_id, email="*****@*****.**", name="John Doe", ) event_message = Message.to_aggregate_event_message( user, Register( user_id=user_id, email="*****@*****.**", name="John Doe", ), ) assert event_message.metadata.origin_stream_name is None
async def test_handler_invocation(test_domain): test_domain.register(User) test_domain.register(Register) test_domain.register(Activate) test_domain.register(UserCommandHandler, aggregate_cls=User) identifier = str(uuid4()) command = Register( user_id=identifier, email="*****@*****.**", ) message = Message.to_command_message(command) engine = Engine(domain=test_domain, test_mode=True) await engine.handle_message(UserCommandHandler, message) global counter assert counter == 1
def publish(self, object: BaseEvent) -> None: """Publish an object to all registered brokers""" if self._brokers is None: self._initialize() message = Message.to_message(object) # Follow a naive strategy and dispatch event directly to message broker # If the operation is enclosed in a Unit of Work, delegate the responsibility # of publishing the message to the UoW if current_uow: logger.debug(f"Recording {object.__class__.__name__} " f"with values {object.to_dict()} in {current_uow}") current_uow.register_message(message) else: logger.debug( f"Publishing {object.__class__.__name__} with values {object.to_dict()}" ) for _, broker in self._brokers.items(): broker.publish(message)
def test_construct_message_from_command_without_identifier(): """Test that a new UUID is used as identifier when there is no explicit identifier specified""" identifier = str(uuid4()) command = SendEmailCommand(to="*****@*****.**", subject="Foo", content="Bar") message = Message.to_command_message(command) assert message is not None assert type(message) is Message message_dict = message.to_dict() identifier = message_dict["stream_name"].split( f"{SendEmail.meta_.stream_name}:command-", 1)[1] try: UUID(identifier, version=4) except ValueError: pytest.fail("Command identifier is not a valid UUID")
def events_of_type(self, event_cls: Type[BaseEvent], stream_name: str = None) -> List[BaseEvent]: """Read events of a specific type in a given stream. This is a utility method, especially useful for testing purposes, that retrives events of a specific type from the event store. If no stream is specified, events of the requested type will be retrieved from all streams. :param event_cls: Class of the event type to be retrieved :param stream_name: Stream from which events are to be retrieved :type event_cls: BaseEvent Class :type stream_name: String, optional, default is `None` :return: A list of events of `event_cls` type :rtype: list """ stream_name = stream_name or "$all" return [ Message.from_dict(event).to_object() for event in self.domain.event_store.store._read(stream_name) if event["type"] == fqn(event_cls) ]
def get_next(self) -> Message: bytes_message = self.redis_instance.lpop("messages") if bytes_message: return Message(json.loads(bytes_message)) return None
def publish(self, message: Message) -> None: # FIXME Accept configuration for database and list name self.redis_instance.rpush("messages", json.dumps(message.to_dict()))
def publish(self, message: Message) -> None: initiator_obj = message.to_object() for subscriber in self._subscribers[fully_qualified_name( initiator_obj.__class__)]: subscriber()(message.data)
def read_last_message(self, stream_name) -> Message: # FIXME Rename to read_last_stream_message raw_message = self._read_last_message(stream_name) return Message.from_dict(raw_message)