From 1a93783438141116340ca4c3ed04828f483a164f Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Fri, 19 Jul 2024 13:32:54 -0700 Subject: [PATCH 1/4] Nest Event Sourcing functionality within Base Aggregate So far, Event Sourced Aggregate was a segarate domain element with its own class and factory. This causes a few issues: - A separate API for event sourced aggregates - Duplication of code between aggregate types - When a bug is fixed in one place, the other has to be take care too - Dissimilarities in functionality (Event sourced aggregates did not support invariants and entity nesting) This commit merges the functionality and introduces an option called `is_event_sourced` on BaseAggregate to control event sourcing behavior. The repository will remain as-is for now, because that's where event sourced aggregates primarily differ from regular aggregates. This will need to be revisited if we want to support custom event sourced repositories. --- src/protean/container.py | 4 +- src/protean/core/aggregate.py | 69 +++++++++++++++++-- src/protean/core/entity.py | 50 +++++++++----- src/protean/core/event_sourced_repository.py | 4 +- src/protean/domain/__init__.py | 25 ++++--- .../test_event_association_with_aggregate.py | 8 +-- tests/command/test_command_basics.py | 5 +- tests/command/test_command_meta.py | 20 +++--- .../test_retrieving_handlers_by_command.py | 14 ++-- .../test_aggregate_cluster_assignment.py | 5 +- tests/event/test_event_meta.py | 6 +- tests/event/test_event_metadata.py | 12 ++-- tests/event/test_event_part_of_resolution.py | 8 +-- tests/event/test_event_payload.py | 10 +-- tests/event/test_event_properties.py | 6 +- tests/event/test_raising_events.py | 10 +-- .../test_retrieving_handlers_by_event.py | 10 +-- .../test_event_es_metadata_id_and_sequence.py | 8 +-- .../events/test_fact_event_generation.py | 18 +++-- tests/event_sourced_aggregates/test_apply.py | 14 ++-- .../test_applying_events.py | 6 +- .../test_automatic_id_field.py | 8 +-- .../test_event_association_with_aggregate.py | 12 ++-- .../test_event_sourced_aggregate_options.py | 32 ++++----- ...test_event_sourced_aggregate_properties.py | 16 ++--- ...st_event_sourced_aggregate_registration.py | 12 ++-- .../test_expected_version_error.py | 8 +-- .../test_generated_event_version.py | 12 ++-- .../test_initialization_from_events.py | 6 +- ...t_raising_events_from_within_aggregates.py | 14 ++-- ...tiple_events_for_one_aggregate_in_a_uow.py | 6 +- .../test_validations.py | 2 +- tests/event_sourced_repository/test_add.py | 10 +-- .../event_sourced_repository/test_add_uow.py | 6 +- .../test_loading_aggregates.py | 6 +- ...est_retrieving_event_sourced_repository.py | 7 +- .../test_appending_aggregate_events.py | 14 ++-- tests/event_store/test_appending_commands.py | 12 ++-- tests/event_store/test_appending_events.py | 10 +-- ...test_inline_event_processing_on_publish.py | 8 +-- tests/event_store/test_reading_all_streams.py | 10 +-- .../test_reading_events_of_type.py | 10 +-- .../test_reading_last_event_of_type.py | 8 +-- tests/event_store/test_reading_messages.py | 32 +++++---- tests/event_store/test_snapshotting.py | 12 ++-- .../test_streams_initialization.py | 16 ++--- tests/message/test_message_to_object.py | 10 +-- tests/message/test_object_to_message.py | 10 +-- .../test_origin_stream_name_in_metadata.py | 8 +-- tests/server/test_any_event_handler.py | 6 +- .../test_command_handler_subscription.py | 9 +-- tests/server/test_command_handling.py | 6 +- tests/server/test_error_handling.py | 8 +-- .../server/test_event_handler_subscription.py | 32 +++++---- tests/server/test_event_handling.py | 6 +- tests/server/test_handling_all_events.py | 10 +-- ...st_message_filtering_with_origin_stream.py | 10 +-- .../test_message_handover_to_engine.py | 8 +-- .../subscription/test_no_message_filtering.py | 10 +-- .../test_read_position_updates.py | 12 ++-- .../test_inline_event_processing.py | 6 +- .../test_nested_inline_event_processing.py | 8 +-- .../test_storing_events_on_commit.py | 8 +-- 63 files changed, 429 insertions(+), 339 deletions(-) diff --git a/src/protean/container.py b/src/protean/container.py index 92c8b932..46b6aac9 100644 --- a/src/protean/container.py +++ b/src/protean/container.py @@ -382,7 +382,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._events = [] - def raise_(self, event, fact_event=False) -> None: + def raise_(self, event) -> None: """Raise an event in the aggregate cluster. The version of the aggregate is incremented with every event raised, which is true @@ -398,7 +398,7 @@ def raise_(self, event, fact_event=False) -> None: f"aggregate `{self.__class__.__name__}`" ) - if not fact_event: + if not self.meta_.fact_events: self._version += 1 identifier = getattr(self, id_field(self).field_name) diff --git a/src/protean/core/aggregate.py b/src/protean/core/aggregate.py index 65f83c81..a27e73e1 100644 --- a/src/protean/core/aggregate.py +++ b/src/protean/core/aggregate.py @@ -2,14 +2,17 @@ import inspect import logging +from collections import defaultdict +from typing import List from protean.core.entity import BaseEntity from protean.core.event import BaseEvent from protean.core.value_object import BaseValueObject from protean.exceptions import NotSupportedError -from protean.fields import HasMany, HasOne, Integer, List, Reference, ValueObject +from protean.fields import HasMany, HasOne, Integer, Reference, ValueObject +from protean.fields import List as ProteanList from protean.reflection import fields -from protean.utils import DomainObjects, derive_element_class, inflection +from protean.utils import DomainObjects, derive_element_class, fqn, inflection logger = logging.getLogger(__name__) @@ -56,6 +59,19 @@ def __new__(cls, *args, **kwargs): # a single aggregate update could have triggered multiple events. _event_position = -1 + def __init_subclass__(subclass) -> None: + super().__init_subclass__() + + # Associate a `_projections` map with subclasses. + # It needs to be initialized here because if it + # were initialized in __init__, the same collection object + # would be made available across all subclasses, + # defeating its purpose. + setattr(subclass, "_projections", defaultdict(set)) + + # Store associated events + setattr(subclass, "_events_cls_map", {}) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -73,12 +89,46 @@ def _default_options(cls): ("aggregate_cluster", None), ("auto_add_id_field", True), ("fact_events", False), + ("is_event_sourced", False), ("model", None), ("provider", "default"), ("schema_name", inflection.underscore(cls.__name__)), ("stream_category", inflection.underscore(cls.__name__)), ] + def _apply(self, event: BaseEvent) -> None: + """Apply the event onto the aggregate by calling the appropriate projection. + + Args: + event (BaseEvent): Event object to apply + """ + # FIXME Handle case of missing projection + event_name = fqn(event.__class__) + + # FIXME Handle case of missing projection method + if event_name not in self._projections: + raise NotImplementedError( + f"No handler registered for event `{event_name}` in `{self.__class__.__name__}`" + ) + + for fn in self._projections[event_name]: + # Call event handler method + fn(self, event) + self._version += 1 + + @classmethod + def from_events(cls, events: List[BaseEvent]) -> "BaseAggregate": + """Reconstruct an aggregate from a list of events.""" + # Initialize the aggregate with the first event's payload and apply it + aggregate = cls(**events[0].payload) + aggregate._apply(events[0]) + + # Apply the rest of the events + for event in events[1:]: + aggregate._apply(event) + + return aggregate + def element_to_fact_event(element_cls): """Convert an Element to a Fact Event. @@ -94,9 +144,7 @@ def element_to_fact_event(element_cls): The target class of associations is constructed as the Value Object. """ # Gather all fields defined in the element, except References. - # We ignore references in event payloads. We also ignore - # the `_next_version` field because it is a temporary in-flight - # field used to track the next version of the aggregate. + # We ignore references in event payloads. attrs = { key: value for key, value in fields(element_cls).items() @@ -108,7 +156,7 @@ def element_to_fact_event(element_cls): if isinstance(value, HasOne): attrs[key] = element_to_fact_event(value.to_cls) elif isinstance(value, HasMany): - attrs[key] = List(content_type=element_to_fact_event(value.to_cls)) + attrs[key] = ProteanList(content_type=element_to_fact_event(value.to_cls)) # If we are dealing with an Entity, we convert it to a Value Object # and return it. @@ -160,6 +208,15 @@ def aggregate_factory(element_cls, domain, **opts): f"{domain.normalized_name}::{element_cls.meta_.stream_category}" ) + # Iterate through methods marked as `@apply` and construct a projections map + methods = inspect.getmembers(element_cls, predicate=inspect.isroutine) + for method_name, method in methods: + if not ( + method_name.startswith("__") and method_name.endswith("__") + ) and hasattr(method, "_event_cls"): + element_cls._projections[fqn(method._event_cls)].add(method) + element_cls._events_cls_map[fqn(method._event_cls)] = method._event_cls + return element_cls diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py index 5964000c..a101f616 100644 --- a/src/protean/core/entity.py +++ b/src/protean/core/entity.py @@ -440,21 +440,6 @@ def raise_(self, event) -> None: f" aggregate `{self._root.__class__.__name__}`" ) - # Events are sometimes raised from within the aggregate, well-before persistence. - # In that case, the aggregate's next version has to be considered in events, - # because we want to associate the event with the version that will be persisted. - # - # Other times, an event is generated after persistence, like in the case of - # fact events. In this case, the aggregate's current version and next version - # will be the same. - # - # So we simply take the latest version, among `_version` and `_next_version`. - aggregate_version = max(self._root._version, self._root._next_version) - - # This is just a counter to uniquely gather all events generated - # in the same edit session - event_number = len(self._root._events) + 1 - identifier = getattr(self._root, id_field(self._root).field_name) # Set Fact Event stream to be `-fact` @@ -463,11 +448,42 @@ def raise_(self, event) -> None: else: stream = f"{self._root.meta_.stream_category}-{identifier}" + if self._root.meta_.is_event_sourced: + # The version of the aggregate is incremented with every event raised, which is true + # in the case of Event Sourced Aggregates. + # + # Except for Fact Events. Fact Events are raised after the aggregate has been persisted, + if not event.__class__.__name__.endswith("FactEvent"): + self._version += 1 + + event_identity = f"{stream}-{self._version}" + sequence_id = f"{self._version}" + else: + # Events are sometimes raised from within the aggregate, well-before persistence. + # In that case, the aggregate's next version has to be considered in events, + # because we want to associate the event with the version that will be persisted. + # + # Other times, an event is generated after persistence, like in the case of + # fact events. In this case, the aggregate's current version and next version + # will be the same. + # + # So we simply take the latest version, among `_version` and `_next_version`. + aggregate_version = max(self._root._version, self._root._next_version) + + # This is just a counter to uniquely gather all events generated + # in the same edit session + event_number = len(self._root._events) + 1 + + event_identity = f"{stream}-{aggregate_version}.{event_number}" + sequence_id = f"{aggregate_version}.{event_number}" + + # Event is immutable, so we clone a new event object from the event raised, + # and add the enhanced metadata to it. event_with_metadata = event.__class__( event.to_dict(), _expected_version=self._root._event_position, _metadata={ - "id": (f"{stream}-{aggregate_version}.{event_number}"), + "id": event_identity, "type": event._metadata.type, "fqn": event._metadata.fqn, "kind": event._metadata.kind, @@ -475,7 +491,7 @@ def raise_(self, event) -> None: "origin_stream": event._metadata.origin_stream, "timestamp": event._metadata.timestamp, "version": event._metadata.version, - "sequence_id": f"{aggregate_version}.{event_number}", + "sequence_id": sequence_id, "payload_hash": hash( json.dumps( event.payload, diff --git a/src/protean/core/event_sourced_repository.py b/src/protean/core/event_sourced_repository.py index 949109dc..acb4f1fe 100644 --- a/src/protean/core/event_sourced_repository.py +++ b/src/protean/core/event_sourced_repository.py @@ -55,7 +55,7 @@ def add(self, aggregate: BaseEventSourcedAggregate) -> None: # Construct and raise the Fact Event fact_event_obj = aggregate._fact_event_cls(**payload) - aggregate.raise_(fact_event_obj, fact_event=True) + aggregate.raise_(fact_event_obj) uow._add_to_identity_map(aggregate) @@ -113,7 +113,7 @@ def event_sourced_repository_factory(element_cls, domain, **opts): } ) - if not issubclass(element_cls.meta_.part_of, BaseEventSourcedAggregate): + if not element_cls.meta_.part_of.meta_.is_event_sourced: raise IncorrectUsageError( { "_entity": [ diff --git a/src/protean/domain/__init__.py b/src/protean/domain/__init__.py index 56fb406c..badc6c98 100644 --- a/src/protean/domain/__init__.py +++ b/src/protean/domain/__init__.py @@ -763,9 +763,13 @@ def _validate_domain(self): # Check that no two event sourced aggregates have the same event class in their # `_events_cls_map`. - event_sourced_aggregates = self.registry._elements[ - DomainObjects.EVENT_SOURCED_AGGREGATE.value - ] + event_sourced_aggregates = { + name: record + for (name, record) in self.registry._elements[ + DomainObjects.AGGREGATE.value + ].items() + if record.cls.meta_.is_event_sourced is True + } # Collect all event class names from `_events_cls_map` of all event sourced aggregates event_class_names = list() for event_sourced_aggregate in event_sourced_aggregates.values(): @@ -1211,20 +1215,23 @@ def handlers_for(self, event: BaseEvent) -> List[BaseEventHandler]: ############################ # FIXME Optimize calls to this method with cache, but also with support for Multitenancy - def repository_for(self, part_of) -> BaseRepository: - if isinstance(part_of, str): + def repository_for(self, element_cls) -> BaseRepository: + if isinstance(element_cls, str): raise IncorrectUsageError( { "element": [ - f"Element {part_of} is not registered in domain {self.name}" + f"Element {element_cls} is not registered in domain {self.name}" ] } ) - if part_of.element_type == DomainObjects.EVENT_SOURCED_AGGREGATE: - return self.event_store.repository_for(part_of) + if ( + element_cls.element_type == DomainObjects.AGGREGATE + and element_cls.meta_.is_event_sourced + ): + return self.event_store.repository_for(element_cls) else: - return self.providers.repository_for(part_of) + return self.providers.repository_for(element_cls) ####################### # Cache Functionality # diff --git a/tests/aggregate/events/test_event_association_with_aggregate.py b/tests/aggregate/events/test_event_association_with_aggregate.py index a77d0144..dcd0d6c5 100644 --- a/tests/aggregate/events/test_event_association_with_aggregate.py +++ b/tests/aggregate/events/test_event_association_with_aggregate.py @@ -1,6 +1,6 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.exceptions import ConfigurationError from protean.fields import Identifier, String @@ -24,7 +24,7 @@ class UserArchived(BaseEvent): user_id = Identifier(required=True) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) name = String(max_length=50, required=True) email = String(required=True) @@ -53,7 +53,7 @@ class UserUnknownEvent(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) @@ -69,7 +69,7 @@ def test_an_unassociated_event_throws_error(test_domain): def test_that_event_associated_with_another_aggregate_throws_error(test_domain): - test_domain.register(User2) + test_domain.register(User2, is_event_sourced=True) test_domain.register(UserUnknownEvent, part_of=User2) test_domain.init(traverse=False) diff --git a/tests/command/test_command_basics.py b/tests/command/test_command_basics.py index 3b346fa4..086efdbb 100644 --- a/tests/command/test_command_basics.py +++ b/tests/command/test_command_basics.py @@ -1,8 +1,8 @@ -from protean import BaseCommand, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseCommand from protean.fields import Identifier, String -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -15,6 +15,7 @@ class Register(BaseCommand): def test_domain_stores_command_type_for_easy_retrieval(test_domain): + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.init(traverse=False) diff --git a/tests/command/test_command_meta.py b/tests/command/test_command_meta.py index b252ff46..70cb15f1 100644 --- a/tests/command/test_command_meta.py +++ b/tests/command/test_command_meta.py @@ -2,13 +2,13 @@ import pytest -from protean import BaseCommand, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseCommand from protean.exceptions import IncorrectUsageError from protean.fields import String from protean.fields.basic import Identifier -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -21,7 +21,7 @@ class Register(BaseCommand): def test_command_definition_without_aggregate_or_stream(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) with pytest.raises(IncorrectUsageError) as exc: test_domain.register(Register) @@ -47,7 +47,7 @@ class AbstractCommand(BaseCommand): @pytest.mark.eventstore def test_command_associated_with_aggregate(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.init(traverse=False) @@ -60,15 +60,15 @@ def test_command_associated_with_aggregate(test_domain): ) ) - messages = test_domain.event_store.store.read("user:command") + messages = test_domain.event_store.store.read("test::user:command") assert len(messages) == 1 - messages[0].stream_name == f"user:command-{identifier}" + messages[0].stream_name == f"test::user:command-{identifier}" @pytest.mark.eventstore def test_command_associated_with_aggregate_with_custom_stream_name(test_domain): - test_domain.register(User, stream_category="foo") + test_domain.register(User, is_event_sourced=True, stream_category="foo") test_domain.register(Register, part_of=User) test_domain.init(traverse=False) @@ -81,14 +81,14 @@ def test_command_associated_with_aggregate_with_custom_stream_name(test_domain): ) ) - messages = test_domain.event_store.store.read("foo:command") + messages = test_domain.event_store.store.read("test::foo:command") assert len(messages) == 1 - messages[0].stream_name == f"foo:command-{identifier}" + messages[0].stream_name == f"test::foo:command-{identifier}" def test_aggregate_cluster_of_event(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.init(traverse=False) diff --git a/tests/command_handler/test_retrieving_handlers_by_command.py b/tests/command_handler/test_retrieving_handlers_by_command.py index c08e9013..e57c88b7 100644 --- a/tests/command_handler/test_retrieving_handlers_by_command.py +++ b/tests/command_handler/test_retrieving_handlers_by_command.py @@ -1,6 +1,6 @@ import pytest -from protean import BaseCommand, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseCommand, handle from protean.core.command_handler import BaseCommandHandler from protean.exceptions import ConfigurationError, NotSupportedError from protean.fields import Identifier, String, Text @@ -10,7 +10,7 @@ class UnknownCommand(BaseCommand): foo = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) # FIXME Auto-associate ID email = String() name = String() @@ -38,7 +38,7 @@ def register(self, _: Register) -> None: pass -class Post(BaseEventSourcedAggregate): +class Post(BaseAggregate): topic = String() content = Text() @@ -56,11 +56,11 @@ def create_new_post(self, _: Create): def test_retrieving_handler_by_command(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(ChangeAddress, part_of=User) test_domain.register(UserCommandHandlers, part_of=User) - test_domain.register(Post) + test_domain.register(Post, is_event_sourced=True) test_domain.register(Create, part_of=Post) test_domain.register(PostCommandHandler, part_of=Post) test_domain.init(traverse=False) @@ -72,7 +72,7 @@ def test_retrieving_handler_by_command(test_domain): def test_for_no_errors_when_no_handler_method_has_not_been_defined_for_a_command( test_domain, ): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(ChangeAddress, part_of=User) test_domain.register(UserCommandHandlers, part_of=User) @@ -92,7 +92,7 @@ def test_retrieving_handlers_for_unknown_command(test_domain): def test_error_on_defining_multiple_handlers_for_a_command(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserCommandHandlers, part_of=User) test_domain.register(AdminUserCommandHandlers, part_of=User) test_domain.init(traverse=False) diff --git a/tests/domain/test_aggregate_cluster_assignment.py b/tests/domain/test_aggregate_cluster_assignment.py index a224fd92..6e6f3250 100644 --- a/tests/domain/test_aggregate_cluster_assignment.py +++ b/tests/domain/test_aggregate_cluster_assignment.py @@ -5,7 +5,6 @@ BaseCommand, BaseEntity, BaseEvent, - BaseEventSourcedAggregate, ) from protean.fields import HasMany, HasOne, Identifier, Integer, String @@ -55,7 +54,7 @@ def test_aggregate_cluster_assignment(self): assert DepartmentClosed.meta_.aggregate_cluster == University -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -77,7 +76,7 @@ class Registered(BaseEvent): class TestEventSourcedAggregateClusterAssignment: @pytest.fixture(autouse=True) def register_elements(self, test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Registered, part_of=User) test_domain.init(traverse=False) diff --git a/tests/event/test_event_meta.py b/tests/event/test_event_meta.py index 81457de5..363a6d39 100644 --- a/tests/event/test_event_meta.py +++ b/tests/event/test_event_meta.py @@ -1,12 +1,12 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.exceptions import IncorrectUsageError from protean.fields import String from protean.fields.basic import Identifier -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -18,7 +18,7 @@ class UserLoggedIn(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserLoggedIn, part_of=User) test_domain.init(traverse=False) diff --git a/tests/event/test_event_metadata.py b/tests/event/test_event_metadata.py index 6699eeda..8e132afc 100644 --- a/tests/event/test_event_metadata.py +++ b/tests/event/test_event_metadata.py @@ -3,14 +3,14 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import String, ValueObject from protean.fields.basic import Identifier from protean.reflection import fields from protean.utils import fqn -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -25,7 +25,7 @@ class UserLoggedIn(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserLoggedIn, part_of=User) test_domain.init(traverse=False) @@ -133,15 +133,15 @@ def test_event_metadata(): assert event._metadata is not None assert isinstance(event._metadata.timestamp, datetime) - assert event._metadata.id == f"user-{user.id}-0" + assert event._metadata.id == f"test::user-{user.id}-0" assert event.to_dict() == { "_metadata": { - "id": f"user-{user.id}-0", + "id": f"test::user-{user.id}-0", "type": "Test.UserLoggedIn.v1", "fqn": fqn(UserLoggedIn), "kind": "EVENT", - "stream": f"user-{user.id}", + "stream": f"test::user-{user.id}", "origin_stream": None, "timestamp": str(event._metadata.timestamp), "version": "v1", diff --git a/tests/event/test_event_part_of_resolution.py b/tests/event/test_event_part_of_resolution.py index 7d30a076..5f21ff51 100644 --- a/tests/event/test_event_part_of_resolution.py +++ b/tests/event/test_event_part_of_resolution.py @@ -1,11 +1,11 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import String from protean.fields.basic import Identifier -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -17,7 +17,7 @@ class UserLoggedIn(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserLoggedIn, part_of="User") @@ -29,4 +29,4 @@ def test_event_has_stream_category_after_domain_init(test_domain): test_domain.init(traverse=False) assert UserLoggedIn.meta_.part_of == User - assert UserLoggedIn.meta_.part_of.meta_.stream_category == "user" + assert UserLoggedIn.meta_.part_of.meta_.stream_category == "test::user" diff --git a/tests/event/test_event_payload.py b/tests/event/test_event_payload.py index 79082fb2..552d9ecd 100644 --- a/tests/event/test_event_payload.py +++ b/tests/event/test_event_payload.py @@ -2,13 +2,13 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import String from protean.fields.basic import Identifier from protean.utils import fqn -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -23,7 +23,7 @@ class UserLoggedIn(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserLoggedIn, part_of=User) test_domain.init(traverse=False) @@ -37,11 +37,11 @@ def test_event_payload(): assert event.to_dict() == { "_metadata": { - "id": f"user-{user_id}-0", + "id": f"test::user-{user_id}-0", "type": "Test.UserLoggedIn.v1", "fqn": fqn(UserLoggedIn), "kind": "EVENT", - "stream": f"user-{user_id}", + "stream": f"test::user-{user_id}", "origin_stream": None, "timestamp": str(event._metadata.timestamp), "version": "v1", diff --git a/tests/event/test_event_properties.py b/tests/event/test_event_properties.py index f72b826b..f97afde2 100644 --- a/tests/event/test_event_properties.py +++ b/tests/event/test_event_properties.py @@ -2,13 +2,13 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.exceptions import IncorrectUsageError from protean.fields import Identifier, String from protean.reflection import id_field -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) email = String() name = String() @@ -22,7 +22,7 @@ class Registered(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.init(traverse=False) diff --git a/tests/event/test_raising_events.py b/tests/event/test_raising_events.py index c2b5f895..ff5dab55 100644 --- a/tests/event/test_raising_events.py +++ b/tests/event/test_raising_events.py @@ -2,12 +2,12 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import String from protean.fields.basic import Identifier -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -19,7 +19,7 @@ class UserLoggedIn(BaseEvent): @pytest.mark.eventstore def test_raising_event(test_domain): - test_domain.register(User, stream_category="authentication") + test_domain.register(User, is_event_sourced=True, stream_category="authentication") test_domain.register(UserLoggedIn, part_of=User) test_domain.init(traverse=False) @@ -29,7 +29,7 @@ def test_raising_event(test_domain): test_domain.repository_for(User).add(user) - messages = test_domain.event_store.store.read("authentication") + messages = test_domain.event_store.store.read("test::authentication") assert len(messages) == 1 - assert messages[0].stream_name == f"authentication-{identifier}" + assert messages[0].stream_name == f"test::authentication-{identifier}" diff --git a/tests/event_handler/test_retrieving_handlers_by_event.py b/tests/event_handler/test_retrieving_handlers_by_event.py index 49ac95b4..50b68a57 100644 --- a/tests/event_handler/test_retrieving_handlers_by_event.py +++ b/tests/event_handler/test_retrieving_handlers_by_event.py @@ -2,17 +2,17 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import DateTime, Identifier, String -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() -class Email(BaseEventSourcedAggregate): +class Email(BaseAggregate): email = String() sent_at = DateTime() @@ -73,13 +73,13 @@ def universal_handler(self, _: BaseEvent) -> None: @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(Renamed, part_of=User) test_domain.register(UserEventHandler, part_of=User) test_domain.register(UserMetrics, part_of=User) - test_domain.register(Email) + test_domain.register(Email, is_event_sourced=True) test_domain.register(Sent, part_of=Email) test_domain.register(EmailEventHandler, part_of=Email) diff --git a/tests/event_sourced_aggregates/events/test_event_es_metadata_id_and_sequence.py b/tests/event_sourced_aggregates/events/test_event_es_metadata_id_and_sequence.py index 9bad33bc..07731c4f 100644 --- a/tests/event_sourced_aggregates/events/test_event_es_metadata_id_and_sequence.py +++ b/tests/event_sourced_aggregates/events/test_event_es_metadata_id_and_sequence.py @@ -2,12 +2,12 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import String from protean.fields.basic import Identifier -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -22,7 +22,7 @@ class UserLoggedIn(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserLoggedIn, part_of=User) test_domain.init(traverse=False) @@ -33,5 +33,5 @@ def test_event_is_generated_with_unique_id(): user.login() event = user._events[0] - assert event._metadata.id == f"user-{identifier}-0" + assert event._metadata.id == f"test::user-{identifier}-0" assert event._metadata.sequence_id == "0" diff --git a/tests/event_sourced_aggregates/events/test_fact_event_generation.py b/tests/event_sourced_aggregates/events/test_fact_event_generation.py index 62b3db58..6125ba32 100644 --- a/tests/event_sourced_aggregates/events/test_fact_event_generation.py +++ b/tests/event_sourced_aggregates/events/test_fact_event_generation.py @@ -1,6 +1,6 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate, apply +from protean import BaseAggregate, BaseEvent, apply from protean.fields import Identifier, String from protean.utils.mixins import Message @@ -16,7 +16,7 @@ class Renamed(BaseEvent): name = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() @@ -32,7 +32,7 @@ def renamed(self, event: Renamed) -> None: @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User, fact_events=True) + test_domain.register(User, is_event_sourced=True, fact_events=True) test_domain.register(Registered, part_of=User) test_domain.register(Renamed, part_of=User) test_domain.init(traverse=False) @@ -49,11 +49,13 @@ def test_generation_of_first_fact_event_on_persistence(test_domain): test_domain.repository_for(User).add(user) # Read event from event store - event_messages = test_domain.event_store.store.read(f"user-{user.id}") + event_messages = test_domain.event_store.store.read(f"test::user-{user.id}") assert len(event_messages) == 1 # Read fact events from event store - fact_event_messages = test_domain.event_store.store.read(f"user-fact-{user.id}") + fact_event_messages = test_domain.event_store.store.read( + f"test::user-fact-{user.id}" + ) assert len(fact_event_messages) == 1 # Deserialize event @@ -88,11 +90,13 @@ def test_generation_of_subsequent_fact_events_after_fetch(test_domain): test_domain.repository_for(User).add(refreshed_user) # Read event from event store - event_messages = test_domain.event_store.store.read(f"user-{user.id}") + event_messages = test_domain.event_store.store.read(f"test::user-{user.id}") assert len(event_messages) == 2 # Read fact events from event store - fact_event_messages = test_domain.event_store.store.read(f"user-fact-{user.id}") + fact_event_messages = test_domain.event_store.store.read( + f"test::user-fact-{user.id}" + ) assert len(fact_event_messages) == 2 # Deserialize 1st event and verify diff --git a/tests/event_sourced_aggregates/test_apply.py b/tests/event_sourced_aggregates/test_apply.py index 26a98b27..e9f23e6d 100644 --- a/tests/event_sourced_aggregates/test_apply.py +++ b/tests/event_sourced_aggregates/test_apply.py @@ -3,7 +3,7 @@ import pytest -from protean import BaseCommand, BaseEvent, BaseEventSourcedAggregate, apply +from protean import BaseAggregate, BaseCommand, BaseEvent, apply from protean.exceptions import IncorrectUsageError from protean.fields import Identifier, String from protean.utils import fqn @@ -30,7 +30,7 @@ class UserRenamed(BaseEvent): name = String(required=True, max_length=50) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) name = String(max_length=50, required=True) email = String(required=True) @@ -63,7 +63,7 @@ def renamed(self, event: UserRenamed): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) @@ -89,7 +89,7 @@ def test_apply_decorator_method_should_have_exactly_one_argument(): class Sent(BaseEvent): email_id = Identifier() - class _(BaseEventSourcedAggregate): + class _(BaseAggregate): email_id = Identifier(identifier=True) @apply @@ -108,7 +108,7 @@ class Send(BaseCommand): # Argument should be an event class with pytest.raises(IncorrectUsageError) as exc: - class _(BaseEventSourcedAggregate): + class _(BaseAggregate): email_id = Identifier(identifier=True) @apply @@ -124,7 +124,7 @@ def sent(self, _: Send) -> None: # Argument should be annotated with pytest.raises(IncorrectUsageError) as exc: - class _(BaseEventSourcedAggregate): + class _(BaseAggregate): email_id = Identifier(identifier=True) @apply @@ -140,7 +140,7 @@ def sent(self, _) -> None: # Argument should be supplied with pytest.raises(IncorrectUsageError) as exc: - class _(BaseEventSourcedAggregate): + class _(BaseAggregate): email_id = Identifier(identifier=True) @apply diff --git a/tests/event_sourced_aggregates/test_applying_events.py b/tests/event_sourced_aggregates/test_applying_events.py index ed909f05..0bd893e0 100644 --- a/tests/event_sourced_aggregates/test_applying_events.py +++ b/tests/event_sourced_aggregates/test_applying_events.py @@ -3,7 +3,7 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate, apply +from protean import BaseAggregate, BaseEvent, apply from protean.fields import Identifier, String @@ -28,7 +28,7 @@ class UserRenamed(BaseEvent): name = String(required=True, max_length=50) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) name = String(max_length=50, required=True) email = String(required=True) @@ -61,7 +61,7 @@ def renamed(self, event: UserRenamed): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) diff --git a/tests/event_sourced_aggregates/test_automatic_id_field.py b/tests/event_sourced_aggregates/test_automatic_id_field.py index 65ab95fb..b27f4c6e 100644 --- a/tests/event_sourced_aggregates/test_automatic_id_field.py +++ b/tests/event_sourced_aggregates/test_automatic_id_field.py @@ -1,15 +1,15 @@ -from protean import BaseEventSourcedAggregate +from protean import BaseAggregate from protean.fields import Auto, DateTime, Identifier, Integer, String from protean.reflection import declared_fields, fields, id_field from protean.utils import utcnow_func -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): name = String() age = Integer() -class Order(BaseEventSourcedAggregate): +class Order(BaseAggregate): order_id = Identifier(identifier=True) placed_at = DateTime() @@ -31,7 +31,7 @@ def test_no_auto_id_field_generation_when_an_identifier_is_provided(): def test_that_an_aggregate_can_opt_to_have_no_id_field_by_default(test_domain): - @test_domain.event_sourced_aggregate(auto_add_id_field=False) + @test_domain.aggregate(is_event_sourced=True, auto_add_id_field=False) class TimeStamped: created_at = DateTime(default=utcnow_func) updated_at = DateTime(default=utcnow_func) diff --git a/tests/event_sourced_aggregates/test_event_association_with_aggregate.py b/tests/event_sourced_aggregates/test_event_association_with_aggregate.py index cca03b21..85c49fc0 100644 --- a/tests/event_sourced_aggregates/test_event_association_with_aggregate.py +++ b/tests/event_sourced_aggregates/test_event_association_with_aggregate.py @@ -2,7 +2,7 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate, apply +from protean import BaseAggregate, BaseEvent, apply from protean.exceptions import ConfigurationError, IncorrectUsageError from protean.fields import Identifier, String @@ -32,7 +32,7 @@ class UserArchived(BaseEvent): user_id = Identifier(required=True) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) name = String(max_length=50, required=True) email = String(required=True) @@ -63,7 +63,7 @@ def renamed(self, event: UserRenamed): self.name = event.name -class Email(BaseEventSourcedAggregate): +class Email(BaseAggregate): email_id = Identifier(identifier=True) @apply @@ -73,11 +73,11 @@ def registered(self, _: UserRegistered): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) - test_domain.register(Email) + test_domain.register(Email, is_event_sourced=True) @pytest.mark.eventstore @@ -91,7 +91,7 @@ def test_that_event_is_associated_with_aggregate(): def test_that_trying_to_associate_an_event_with_multiple_aggregates_throws_an_error( test_domain, ): - test_domain.register(Email) + test_domain.register(Email, is_event_sourced=True) with pytest.raises(IncorrectUsageError) as exc: test_domain.init(traverse=False) diff --git a/tests/event_sourced_aggregates/test_event_sourced_aggregate_options.py b/tests/event_sourced_aggregates/test_event_sourced_aggregate_options.py index 260291cf..1c0c3f90 100644 --- a/tests/event_sourced_aggregates/test_event_sourced_aggregate_options.py +++ b/tests/event_sourced_aggregates/test_event_sourced_aggregate_options.py @@ -1,53 +1,53 @@ import pytest -from protean import BaseEventSourcedAggregate +from protean import BaseAggregate from protean.fields import Integer, String -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): name = String() age = Integer() -class AdminUser(BaseEventSourcedAggregate): +class AdminUser(BaseAggregate): name = String() -class Person(BaseEventSourcedAggregate): +class Person(BaseAggregate): name = String() age = Integer() @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) - test_domain.register(AdminUser) - test_domain.register(Person, stream_category="people") + test_domain.register(User, is_event_sourced=True) + test_domain.register(AdminUser, is_event_sourced=True) + test_domain.register(Person, is_event_sourced=True, stream_category="people") def test_stream_category_option_of_an_event_sourced_aggregate(): - assert User.meta_.stream_category == "user" + assert User.meta_.stream_category == "test::user" # Verify snake-casing the Aggregate name - assert AdminUser.meta_.stream_category == "admin_user" + assert AdminUser.meta_.stream_category == "test::admin_user" # Verify manually set stream_category - assert Person.meta_.stream_category == "people" + assert Person.meta_.stream_category == "test::people" def test_stream_category_option_of_an_event_sourced_aggregate_defined_via_annotation( test_domain, ): - @test_domain.event_sourced_aggregate - class Adult(BaseEventSourcedAggregate): + @test_domain.aggregate(is_event_sourced=True) + class Adult(BaseAggregate): name = String() age = Integer() - assert Adult.meta_.stream_category == "adult" + assert Adult.meta_.stream_category == "test::adult" - @test_domain.event_sourced_aggregate(stream_category="children") - class Child(BaseEventSourcedAggregate): + @test_domain.aggregate(is_event_sourced=True, stream_category="children") + class Child(BaseAggregate): name = String() age = Integer() - assert Child.meta_.stream_category == "children" + assert Child.meta_.stream_category == "test::children" diff --git a/tests/event_sourced_aggregates/test_event_sourced_aggregate_properties.py b/tests/event_sourced_aggregates/test_event_sourced_aggregate_properties.py index 46eb9547..e7ebf38a 100644 --- a/tests/event_sourced_aggregates/test_event_sourced_aggregate_properties.py +++ b/tests/event_sourced_aggregates/test_event_sourced_aggregate_properties.py @@ -2,27 +2,19 @@ import pytest -from protean import BaseEventSourcedAggregate -from protean.exceptions import NotSupportedError +from protean import BaseAggregate from protean.fields import Integer, String -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): name = String() age = Integer() -def test_event_sourced_aggregate_cannot_be_initialized(): - with pytest.raises(NotSupportedError) as exc: - BaseEventSourcedAggregate() - - assert str(exc.value) == "BaseEventSourcedAggregate cannot be instantiated" - - class TestEventSourcedAggregateEquivalence: @pytest.fixture(autouse=True) def register_elements(self, test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.init(traverse=False) def test_event_sourced_aggregate_are_not_equivalent_based_on_data(test_domain): @@ -50,7 +42,7 @@ class Person(User): def test_event_sourced_aggregate_hash(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) user1 = User(name="John Doe", age=25) assert hash(user1) == hash(user1.id) diff --git a/tests/event_sourced_aggregates/test_event_sourced_aggregate_registration.py b/tests/event_sourced_aggregates/test_event_sourced_aggregate_registration.py index 5f67e375..4ec906a6 100644 --- a/tests/event_sourced_aggregates/test_event_sourced_aggregate_registration.py +++ b/tests/event_sourced_aggregates/test_event_sourced_aggregate_registration.py @@ -1,28 +1,28 @@ import pytest -from protean import BaseEventSourcedAggregate +from protean import BaseAggregate from protean.fields import Integer, String from protean.utils import fully_qualified_name -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): name = String() age = Integer() def test_registering_an_event_sourced_aggregate_manually(test_domain): try: - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) except Exception: pytest.fail("Failed to register an Event Sourced Aggregate") - assert fully_qualified_name(User) in test_domain.registry.event_sourced_aggregates + assert fully_qualified_name(User) in test_domain.registry.aggregates def test_registering_an_event_sourced_aggregate_via_annotation(test_domain): try: - @test_domain.event_sourced_aggregate + @test_domain.aggregate(is_event_sourced=True) class Person: name = String() age = Integer() @@ -30,4 +30,4 @@ class Person: except Exception: pytest.fail("Failed to register an Event Sourced Aggregate via annotation") - assert fully_qualified_name(Person) in test_domain.registry.event_sourced_aggregates + assert fully_qualified_name(Person) in test_domain.registry.aggregates diff --git a/tests/event_sourced_aggregates/test_expected_version_error.py b/tests/event_sourced_aggregates/test_expected_version_error.py index 180c5789..3912ead9 100644 --- a/tests/event_sourced_aggregates/test_expected_version_error.py +++ b/tests/event_sourced_aggregates/test_expected_version_error.py @@ -3,7 +3,7 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate, UnitOfWork, apply +from protean import BaseAggregate, BaseEvent, UnitOfWork, apply from protean.exceptions import ExpectedVersionError from protean.fields import Identifier, String @@ -29,7 +29,7 @@ class UserRenamed(BaseEvent): name = String(required=True, max_length=50) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) name = String(max_length=50, required=True) email = String(required=True) @@ -62,7 +62,7 @@ def renamed(self, event: UserRenamed): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) @@ -94,5 +94,5 @@ def test_expected_version_error(test_domain): assert ( exc.value.args[0] - == f"Wrong expected version: 0 (Stream: user-{identifier}, Stream Version: 1)" + == f"Wrong expected version: 0 (Stream: test::user-{identifier}, Stream Version: 1)" ) diff --git a/tests/event_sourced_aggregates/test_generated_event_version.py b/tests/event_sourced_aggregates/test_generated_event_version.py index afc90683..00b28043 100644 --- a/tests/event_sourced_aggregates/test_generated_event_version.py +++ b/tests/event_sourced_aggregates/test_generated_event_version.py @@ -2,7 +2,7 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate, apply +from protean import BaseAggregate, BaseEvent, apply from protean.fields import Identifier, String from protean.utils.mixins import Message @@ -28,7 +28,7 @@ class UserRenamed(BaseEvent): name = String(required=True, max_length=50) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) name = String(max_length=50, required=True) email = String(required=True) @@ -61,7 +61,7 @@ def renamed(self, event: UserRenamed): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) @@ -79,7 +79,7 @@ def test_aggregate_and_event_version_after_first_persistence(test_domain): user = User.register(user_id="1", name="John Doe", email="john.doe@example.com") test_domain.repository_for(User).add(user) - event_messages = test_domain.event_store.store.read(f"user-{user.user_id}") + event_messages = test_domain.event_store.store.read(f"test::user-{user.user_id}") assert len(event_messages) == 1 refreshed_user = test_domain.repository_for(User).get(user.user_id) @@ -103,7 +103,7 @@ def test_aggregate_and_event_version_after_first_persistence_after_multiple_pers refreshed_user.change_name(f"John Doe {i}") test_domain.repository_for(User).add(refreshed_user) - event_messages = test_domain.event_store.store.read(f"user-{user.user_id}") + event_messages = test_domain.event_store.store.read(f"test::user-{user.user_id}") assert len(event_messages) == 11 refreshed_user = test_domain.repository_for(User).get(user.user_id) @@ -137,7 +137,7 @@ def test_aggregate_and_event_version_after_multiple_event_generation_in_one_upda assert refreshed_user._version == 1 - event_messages = test_domain.event_store.store.read(f"user-{user.user_id}") + event_messages = test_domain.event_store.store.read(f"test::user-{user.user_id}") assert len(event_messages) == 2 event1 = Message.to_object(event_messages[0]) diff --git a/tests/event_sourced_aggregates/test_initialization_from_events.py b/tests/event_sourced_aggregates/test_initialization_from_events.py index 532eefcd..745d4f9b 100644 --- a/tests/event_sourced_aggregates/test_initialization_from_events.py +++ b/tests/event_sourced_aggregates/test_initialization_from_events.py @@ -2,7 +2,7 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate, apply +from protean import BaseAggregate, BaseEvent, apply from protean.fields import Identifier, String @@ -21,7 +21,7 @@ class UserRenamed(BaseEvent): name = String(required=True, max_length=50) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) name = String(max_length=50, required=True) email = String(required=True) @@ -56,7 +56,7 @@ def renamed(self, event: UserRenamed): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) diff --git a/tests/event_sourced_aggregates/test_raising_events_from_within_aggregates.py b/tests/event_sourced_aggregates/test_raising_events_from_within_aggregates.py index 72864a13..16b937ac 100644 --- a/tests/event_sourced_aggregates/test_raising_events_from_within_aggregates.py +++ b/tests/event_sourced_aggregates/test_raising_events_from_within_aggregates.py @@ -4,7 +4,7 @@ import pytest -from protean import BaseCommandHandler, BaseEvent, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseCommandHandler, BaseEvent, handle from protean.core.command import BaseCommand from protean.core.event_sourced_aggregate import apply from protean.fields import Identifier, String @@ -25,7 +25,7 @@ class Registered(BaseEvent): password_hash = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() @@ -64,7 +64,7 @@ def register_user(self, command: Register) -> None: @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Registered, part_of=User) test_domain.register(UserCommandHandler, part_of=User) @@ -83,14 +83,14 @@ def test_that_events_can_be_raised_from_within_aggregates(test_domain): ) ) - messages = test_domain.event_store.store._read("user") + messages = test_domain.event_store.store._read("test::user") assert len(messages) == 1 - assert messages[0]["stream_name"] == f"user-{identifier}" + assert messages[0]["stream_name"] == f"test::user-{identifier}" assert messages[0]["type"] == Registered.__type__ - messages = test_domain.event_store.store._read("user:command") + messages = test_domain.event_store.store._read("test::user:command") assert len(messages) == 1 - assert messages[0]["stream_name"] == f"user:command-{identifier}" + assert messages[0]["stream_name"] == f"test::user:command-{identifier}" assert messages[0]["type"] == Register.__type__ diff --git a/tests/event_sourced_aggregates/test_raising_multiple_events_for_one_aggregate_in_a_uow.py b/tests/event_sourced_aggregates/test_raising_multiple_events_for_one_aggregate_in_a_uow.py index c3548e80..62dee530 100644 --- a/tests/event_sourced_aggregates/test_raising_multiple_events_for_one_aggregate_in_a_uow.py +++ b/tests/event_sourced_aggregates/test_raising_multiple_events_for_one_aggregate_in_a_uow.py @@ -5,10 +5,10 @@ from uuid import uuid4 from protean import ( + BaseAggregate, BaseCommand, BaseCommandHandler, BaseEvent, - BaseEventSourcedAggregate, apply, handle, ) @@ -35,7 +35,7 @@ class Renamed(BaseEvent): name = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): name = String() email = String() @@ -74,7 +74,7 @@ def rename_user(self, command: RenameNameTwice) -> None: def test_that_multiple_events_are_raised_per_aggregate_in_the_same_uow(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(RenameNameTwice, part_of=User) test_domain.register(UserCommandHandler, part_of=User) diff --git a/tests/event_sourced_aggregates/test_validations.py b/tests/event_sourced_aggregates/test_validations.py index 3b8141e7..6e358894 100644 --- a/tests/event_sourced_aggregates/test_validations.py +++ b/tests/event_sourced_aggregates/test_validations.py @@ -7,7 +7,7 @@ def test_exception_on_multiple_identifiers(test_domain): with pytest.raises(NotSupportedError) as exc: - @test_domain.event_sourced_aggregate + @test_domain.aggregate(is_event_sourced=True) class Person: email = String(identifier=True) username = String(identifier=True) diff --git a/tests/event_sourced_repository/test_add.py b/tests/event_sourced_repository/test_add.py index c4f31332..f90c93ef 100644 --- a/tests/event_sourced_repository/test_add.py +++ b/tests/event_sourced_repository/test_add.py @@ -2,7 +2,7 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.exceptions import IncorrectUsageError from protean.fields import Identifier, String @@ -13,7 +13,7 @@ class UserRegistered(BaseEvent): email = String(required=True) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -27,7 +27,7 @@ def register(cls, id, name, email): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.init(traverse=False) @@ -48,7 +48,7 @@ def test_successful_persistence_of_aggregate(test_domain): test_domain.repository_for(User).add(user) assert len(user._events) == 0 - event_messages = test_domain.event_store.store.read(f"user-{user.id}") + event_messages = test_domain.event_store.store.read(f"test::user-{user.id}") assert len(event_messages) == 1 @@ -57,5 +57,5 @@ def test_aggregate_with_no_changes_is_not_acted_on(test_domain): assert len(user._events) == 0 test_domain.repository_for(User).add(user) - event_messages = test_domain.event_store.store.read(f"user-{user.id}") + event_messages = test_domain.event_store.store.read(f"test::user-{user.id}") assert len(event_messages) == 0 diff --git a/tests/event_sourced_repository/test_add_uow.py b/tests/event_sourced_repository/test_add_uow.py index 4d47b45f..72f8b5c6 100644 --- a/tests/event_sourced_repository/test_add_uow.py +++ b/tests/event_sourced_repository/test_add_uow.py @@ -1,11 +1,11 @@ import mock import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import Identifier, String -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -19,7 +19,7 @@ class Registered(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.init(traverse=False) diff --git a/tests/event_sourced_repository/test_loading_aggregates.py b/tests/event_sourced_repository/test_loading_aggregates.py index c2c7f151..69037e17 100644 --- a/tests/event_sourced_repository/test_loading_aggregates.py +++ b/tests/event_sourced_repository/test_loading_aggregates.py @@ -5,9 +5,9 @@ import pytest from protean import ( + BaseAggregate, BaseCommandHandler, BaseEvent, - BaseEventSourcedAggregate, apply, handle, ) @@ -42,7 +42,7 @@ class AddressChanged(BaseEvent): address = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) email = String() name = String() @@ -102,7 +102,7 @@ def change_address(self, command: ChangeAddress) -> None: @pytest.fixture(autouse=True) def register(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Registered, part_of=User) test_domain.register(ChangeAddress, part_of=User) diff --git a/tests/event_sourced_repository/test_retrieving_event_sourced_repository.py b/tests/event_sourced_repository/test_retrieving_event_sourced_repository.py index 1eb53561..43e7503c 100644 --- a/tests/event_sourced_repository/test_retrieving_event_sourced_repository.py +++ b/tests/event_sourced_repository/test_retrieving_event_sourced_repository.py @@ -1,13 +1,13 @@ import pytest -from protean import BaseAggregate, BaseEventSourcedAggregate +from protean import BaseAggregate from protean.core.event_sourced_repository import BaseEventSourcedRepository from protean.exceptions import IncorrectUsageError from protean.fields import Integer, String from protean.utils import DomainObjects -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): name = String() age = Integer() @@ -15,7 +15,7 @@ class User(BaseEventSourcedAggregate): def test_that_event_sourced_repository_is_returned_for_event_sourced_aggregate( test_domain, ): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) assert ( test_domain.repository_for(User).element_type @@ -49,6 +49,7 @@ class CustomRepository(BaseEventSourcedRepository): pass with pytest.raises(IncorrectUsageError) as exc: + test_domain.register(CustomAggregate) test_domain.register(CustomRepository, part_of=CustomAggregate) assert exc.value.messages == { diff --git a/tests/event_store/test_appending_aggregate_events.py b/tests/event_store/test_appending_aggregate_events.py index 0f5053ea..408a95fc 100644 --- a/tests/event_store/test_appending_aggregate_events.py +++ b/tests/event_store/test_appending_aggregate_events.py @@ -4,7 +4,7 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.core.event_sourced_aggregate import apply from protean.fields import String from protean.fields.basic import Identifier @@ -25,7 +25,7 @@ class Renamed(BaseEvent): name = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() status = String(default="INACTIVE") @@ -59,7 +59,7 @@ def renamed(self, event: Renamed) -> None: @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(Renamed, part_of=User) @@ -72,7 +72,7 @@ def test_appending_messages_to_aggregate(test_domain): user = User.register(id=identifier, email="john.doe@example.com", name="John Doe") test_domain.event_store.store.append(user._events[0]) - messages = test_domain.event_store.store._read("user") + messages = test_domain.event_store.store._read("test::user") assert len(messages) == 1 @@ -83,17 +83,17 @@ def test_version_increment_on_new_event(test_domain): user = User.register(id=identifier, email="john.doe@example.com", name="John Doe") test_domain.event_store.store.append(user._events[0]) - events = test_domain.event_store.store._read(f"user-{identifier}") + events = test_domain.event_store.store._read(f"test::user-{identifier}") assert events[0]["position"] == 0 user.activate() test_domain.event_store.store.append(user._events[1]) - events = test_domain.event_store.store._read(f"user-{identifier}") + events = test_domain.event_store.store._read(f"test::user-{identifier}") assert events[-1]["position"] == 1 user.rename(name="John Doe 2") test_domain.event_store.store.append(user._events[2]) - events = test_domain.event_store.store._read(f"user-{identifier}") + events = test_domain.event_store.store._read(f"test::user-{identifier}") assert events[-1]["position"] == 2 diff --git a/tests/event_store/test_appending_commands.py b/tests/event_store/test_appending_commands.py index c31092a9..1e1c4026 100644 --- a/tests/event_store/test_appending_commands.py +++ b/tests/event_store/test_appending_commands.py @@ -2,13 +2,13 @@ import pytest -from protean import BaseCommand, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseCommand from protean.exceptions import IncorrectUsageError from protean.fields import String from protean.fields.basic import Identifier -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -21,7 +21,7 @@ class Register(BaseCommand): def test_command_submission_without_aggregate(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.init(traverse=False) with pytest.raises(IncorrectUsageError) as exc: @@ -36,7 +36,7 @@ def test_command_submission_without_aggregate(test_domain): @pytest.mark.eventstore def test_command_submission(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.init(traverse=False) @@ -49,7 +49,7 @@ def test_command_submission(test_domain): ) ) - messages = test_domain.event_store.store.read("user:command") + messages = test_domain.event_store.store.read("test::user:command") assert len(messages) == 1 - messages[0].stream_name == f"user:command-{identifier}" + messages[0].stream_name == f"test::user:command-{identifier}" diff --git a/tests/event_store/test_appending_events.py b/tests/event_store/test_appending_events.py index d606fb8d..af23215a 100644 --- a/tests/event_store/test_appending_events.py +++ b/tests/event_store/test_appending_events.py @@ -4,12 +4,12 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields.basic import Identifier from protean.utils.mixins import Message -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) @@ -19,7 +19,7 @@ class UserLoggedIn(BaseEvent): @pytest.mark.eventstore def test_appending_raw_events(test_domain): - test_domain.register(User, stream_category="authentication") + test_domain.register(User, is_event_sourced=True, stream_category="authentication") test_domain.register(UserLoggedIn, part_of=User) test_domain.init(traverse=False) @@ -29,14 +29,14 @@ def test_appending_raw_events(test_domain): event = user._events[0] # Remember event for later comparison test_domain.repository_for(User).add(user) - messages = test_domain.event_store.store.read("authentication") + messages = test_domain.event_store.store.read("test::authentication") assert len(messages) == 1 message = messages[0] assert isinstance(message, Message) - assert message.stream_name == f"authentication-{identifier}" + assert message.stream_name == f"test::authentication-{identifier}" assert message.metadata.kind == "EVENT" assert message.data == event.payload assert message.metadata == event._metadata diff --git a/tests/event_store/test_inline_event_processing_on_publish.py b/tests/event_store/test_inline_event_processing_on_publish.py index 5d577fda..6fd22e7f 100644 --- a/tests/event_store/test_inline_event_processing_on_publish.py +++ b/tests/event_store/test_inline_event_processing_on_publish.py @@ -9,7 +9,7 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import Identifier, String from protean.globals import current_domain @@ -21,7 +21,7 @@ def count_up(): counter += 1 -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) email = String() name = String() @@ -43,9 +43,9 @@ def registered(self, _: Registered) -> None: @pytest.mark.eventstore def test_inline_event_processing_on_publish_in_sync_mode(test_domain): - test_domain.register(User, stream_category="user") + test_domain.register(User, is_event_sourced=True, stream_category="user") test_domain.register(Registered, part_of=User) - test_domain.register(UserEventHandler, stream_category="user") + test_domain.register(UserEventHandler, stream_category="test::user") test_domain.init(traverse=False) user = User( diff --git a/tests/event_store/test_reading_all_streams.py b/tests/event_store/test_reading_all_streams.py index 58ec623a..5277b932 100644 --- a/tests/event_store/test_reading_all_streams.py +++ b/tests/event_store/test_reading_all_streams.py @@ -4,12 +4,12 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import DateTime, Identifier, String, Text from protean.utils import utcnow_func -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String(max_length=50) @@ -43,7 +43,7 @@ class Renamed(BaseEvent): name = String(required=True, max_length=50) -class Post(BaseEventSourcedAggregate): +class Post(BaseAggregate): topic = String() content = Text() @@ -71,12 +71,12 @@ class Published(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(Renamed, part_of=User) - test_domain.register(Post) + test_domain.register(Post, is_event_sourced=True) test_domain.register(Created, part_of=Post) test_domain.register(Published, part_of=Post) diff --git a/tests/event_store/test_reading_events_of_type.py b/tests/event_store/test_reading_events_of_type.py index 5185debc..31753a7d 100644 --- a/tests/event_store/test_reading_events_of_type.py +++ b/tests/event_store/test_reading_events_of_type.py @@ -4,12 +4,12 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import String from protean.fields.basic import Identifier -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String(max_length=50) @@ -45,7 +45,7 @@ class Renamed(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(Renamed, part_of=User) @@ -98,7 +98,7 @@ def test_reading_events_of_type_with_multiple_events(self, test_domain): @pytest.mark.eventstore def test_reading_events_of_type_with_multiple_events_in_stream(self, test_domain): - events = test_domain.event_store.events_of_type(Renamed, "user") + events = test_domain.event_store.events_of_type(Renamed, "test::user") assert len(events) == 10 assert events[-1].name == "John Doe 9" @@ -106,5 +106,5 @@ def test_reading_events_of_type_with_multiple_events_in_stream(self, test_domain def test_reading_events_of_type_with_multiple_events_in_different_stream( self, test_domain ): - events = test_domain.event_store.events_of_type(Renamed, "group") + events = test_domain.event_store.events_of_type(Renamed, "test::group") assert len(events) == 0 diff --git a/tests/event_store/test_reading_last_event_of_type.py b/tests/event_store/test_reading_last_event_of_type.py index 40de226a..c6fda9f4 100644 --- a/tests/event_store/test_reading_last_event_of_type.py +++ b/tests/event_store/test_reading_last_event_of_type.py @@ -4,12 +4,12 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import String from protean.fields.basic import Identifier -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String(max_length=50) @@ -45,7 +45,7 @@ class Renamed(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(Renamed, part_of=User) @@ -112,7 +112,7 @@ def test_reading_the_last_event_of_type_with_multiple_events_in_stream( registered_user.rename(name=f"John Doe {i}") test_domain.event_store.store.append(registered_user._events[-1]) - event = test_domain.event_store.last_event_of_type(Renamed, "user") + event = test_domain.event_store.last_event_of_type(Renamed, "test::user") assert event.name == "John Doe 9" @pytest.mark.eventstore diff --git a/tests/event_store/test_reading_messages.py b/tests/event_store/test_reading_messages.py index 2d190742..01ee680d 100644 --- a/tests/event_store/test_reading_messages.py +++ b/tests/event_store/test_reading_messages.py @@ -4,13 +4,13 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseEvent from protean.fields import String from protean.fields.basic import Identifier from protean.utils.mixins import Message -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String(max_length=50) @@ -31,7 +31,7 @@ class Renamed(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(Renamed, part_of=User) @@ -67,13 +67,13 @@ def renamed_user(test_domain, activated_user): @pytest.mark.eventstore def test_reading_a_message(test_domain, registered_user): - messages = test_domain.event_store.store.read("user") + messages = test_domain.event_store.store.read("test::user") assert len(messages) == 1 message = messages[0] assert isinstance(message, Message) - assert message.stream_name == f"user-{registered_user.id}" + assert message.stream_name == f"test::user-{registered_user.id}" assert message.metadata.kind == "EVENT" assert message.data == registered_user._events[-1].payload assert message.metadata == registered_user._events[-1]._metadata @@ -81,11 +81,11 @@ def test_reading_a_message(test_domain, registered_user): @pytest.mark.eventstore def test_reading_many_messages(test_domain, activated_user): - messages = test_domain.event_store.store.read(f"user-{activated_user.id}") + messages = test_domain.event_store.store.read(f"test::user-{activated_user.id}") assert len(messages) == 2 - assert messages[0].stream_name == f"user-{activated_user.id}" + assert messages[0].stream_name == f"test::user-{activated_user.id}" assert messages[0].metadata.kind == "EVENT" assert messages[0].data == activated_user._events[0].payload assert messages[0].metadata == activated_user._events[0]._metadata @@ -95,18 +95,20 @@ def test_reading_many_messages(test_domain, activated_user): @pytest.mark.eventstore def test_limiting_no_of_messages(test_domain, renamed_user): - messages = test_domain.event_store.store.read(f"user-{renamed_user.id}") + messages = test_domain.event_store.store.read(f"test::user-{renamed_user.id}") assert len(messages) == 12 messages = test_domain.event_store.store.read( - f"user-{renamed_user.id}", no_of_messages=5 + f"test::user-{renamed_user.id}", no_of_messages=5 ) assert len(messages) == 5 @pytest.mark.eventstore def test_reading_messages_from_position(test_domain, renamed_user): - messages = test_domain.event_store.store.read(f"user-{renamed_user.id}", position=5) + messages = test_domain.event_store.store.read( + f"test::user-{renamed_user.id}", position=5 + ) assert len(messages) == 7 # Read until end, 1000 messages by default assert messages[0].data["name"] == "John Doe 3" @@ -115,7 +117,7 @@ def test_reading_messages_from_position(test_domain, renamed_user): @pytest.mark.eventstore def test_reading_messages_from_position_with_limit(test_domain, renamed_user): messages = test_domain.event_store.store.read( - f"user-{renamed_user.id}", position=5, no_of_messages=2 + f"test::user-{renamed_user.id}", position=5, no_of_messages=2 ) assert len(messages) == 2 @@ -124,11 +126,11 @@ def test_reading_messages_from_position_with_limit(test_domain, renamed_user): @pytest.mark.eventstore def test_reading_messages_by_category(test_domain, activated_user): - messages = test_domain.event_store.store.read("user") + messages = test_domain.event_store.store.read("test::user") assert len(messages) == 2 - assert messages[0].stream_name == f"user-{activated_user.id}" + assert messages[0].stream_name == f"test::user-{activated_user.id}" assert messages[0].metadata.kind == "EVENT" assert messages[0].data == activated_user._events[0].payload assert messages[0].metadata == activated_user._events[0]._metadata @@ -139,6 +141,8 @@ def test_reading_messages_by_category(test_domain, activated_user): @pytest.mark.eventstore def test_reading_last_message(test_domain, renamed_user): # Reading by stream - message = test_domain.event_store.store.read_last_message(f"user-{renamed_user.id}") + message = test_domain.event_store.store.read_last_message( + f"test::user-{renamed_user.id}" + ) assert message.type == Renamed.__type__ assert message.data["name"] == "John Doe 9" diff --git a/tests/event_store/test_snapshotting.py b/tests/event_store/test_snapshotting.py index ff517e70..44fcdd40 100644 --- a/tests/event_store/test_snapshotting.py +++ b/tests/event_store/test_snapshotting.py @@ -3,7 +3,7 @@ import pytest -from protean import BaseEvent, BaseEventSourcedAggregate, UnitOfWork, apply +from protean import BaseAggregate, BaseEvent, UnitOfWork, apply from protean.fields import Identifier, String @@ -28,7 +28,7 @@ class UserRenamed(BaseEvent): name = String(required=True, max_length=50) -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) name = String(max_length=50, required=True) email = String(required=True) @@ -61,7 +61,7 @@ def renamed(self, event: UserRenamed): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(UserRegistered, part_of=User) test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) @@ -109,7 +109,7 @@ def test_that_snapshot_is_constructed_after_threshold(test_domain): repo.add(user) snapshot = test_domain.event_store.store._read_last_message( - f"user:snapshot-{identifier}" + f"test::user:snapshot-{identifier}" ) assert snapshot is not None assert User(**snapshot["data"]) == user @@ -137,7 +137,7 @@ def test_that_a_stream_can_have_multiple_snapshots_but_latest_is_considered( repo.add(user) snapshot = test_domain.event_store.store._read_last_message( - f"user:snapshot-{identifier}" + f"test::user:snapshot-{identifier}" ) assert snapshot is not None assert User(**snapshot["data"]) == user @@ -165,7 +165,7 @@ def test_that_a_stream_with_a_snapshop_and_no_further_events_is_reconstructed_co repo.add(user) snapshot = test_domain.event_store.store._read_last_message( - f"user:snapshot-{identifier}" + f"test::user:snapshot-{identifier}" ) assert snapshot is not None assert User(**snapshot["data"]) == user diff --git a/tests/event_store/test_streams_initialization.py b/tests/event_store/test_streams_initialization.py index 3511e504..b10fa9ff 100644 --- a/tests/event_store/test_streams_initialization.py +++ b/tests/event_store/test_streams_initialization.py @@ -2,17 +2,17 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import DateTime, Identifier, String -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() -class Email(BaseEventSourcedAggregate): +class Email(BaseAggregate): email = String() sent_at = DateTime() @@ -56,10 +56,10 @@ def record_sent_email(self, event: Sent) -> None: @pytest.fixture(autouse=True) def register(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) - test_domain.register(Email) + test_domain.register(Email, is_event_sourced=True) test_domain.register(Sent, part_of=Email) test_domain.register(UserEventHandler, part_of=User) test_domain.register(EmailEventHandler, part_of=Email) @@ -70,8 +70,8 @@ def test_streams_initialization(test_domain): assert len(test_domain.event_store._event_streams) == 2 assert all( stream_category in test_domain.event_store._event_streams - for stream_category in ["user", "email"] + for stream_category in ["test::user", "test::email"] ) - assert test_domain.event_store._event_streams["user"] == {UserEventHandler} - assert test_domain.event_store._event_streams["email"] == {EmailEventHandler} + assert test_domain.event_store._event_streams["test::user"] == {UserEventHandler} + assert test_domain.event_store._event_streams["test::email"] == {EmailEventHandler} diff --git a/tests/message/test_message_to_object.py b/tests/message/test_message_to_object.py index 8b650aa6..11046476 100644 --- a/tests/message/test_message_to_object.py +++ b/tests/message/test_message_to_object.py @@ -2,14 +2,14 @@ import pytest -from protean import BaseCommand, BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseCommand, BaseEvent from protean.core.event import Metadata from protean.exceptions import InvalidDataError from protean.fields import Identifier, String from protean.utils.mixins import Message -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() @@ -30,7 +30,7 @@ class Registered(BaseEvent): name = String() -class SendEmail(BaseEventSourcedAggregate): +class SendEmail(BaseAggregate): to = String() subject = String() content = String() @@ -44,10 +44,10 @@ class SendEmailCommand(BaseCommand): @pytest.fixture(autouse=True) def register(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Registered, part_of=User) - test_domain.register(SendEmail) + test_domain.register(SendEmail, is_event_sourced=True) test_domain.register(SendEmailCommand, part_of=SendEmail) test_domain.init(traverse=False) diff --git a/tests/message/test_object_to_message.py b/tests/message/test_object_to_message.py index 6c566256..80a84d59 100644 --- a/tests/message/test_object_to_message.py +++ b/tests/message/test_object_to_message.py @@ -2,13 +2,13 @@ import pytest -from protean import BaseCommand, BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseCommand, BaseEvent from protean.exceptions import ConfigurationError from protean.fields import Identifier, String from protean.utils.mixins import Message -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() @@ -29,7 +29,7 @@ class Registered(BaseEvent): name = String() -class SendEmail(BaseEventSourcedAggregate): +class SendEmail(BaseAggregate): to = String() subject = String() content = String() @@ -43,10 +43,10 @@ class SendEmailCommand(BaseCommand): @pytest.fixture(autouse=True) def register(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Registered, part_of=User) - test_domain.register(SendEmail) + test_domain.register(SendEmail, is_event_sourced=True) test_domain.register(SendEmailCommand, part_of=SendEmail) test_domain.init(traverse=False) diff --git a/tests/message/test_origin_stream_name_in_metadata.py b/tests/message/test_origin_stream_name_in_metadata.py index 49b5fc2e..314598d7 100644 --- a/tests/message/test_origin_stream_name_in_metadata.py +++ b/tests/message/test_origin_stream_name_in_metadata.py @@ -2,7 +2,7 @@ import pytest -from protean import BaseCommand, BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseCommand, BaseEvent from protean.core.event import Metadata from protean.fields import String from protean.fields.basic import Identifier @@ -10,7 +10,7 @@ from protean.utils.mixins import Message -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -30,7 +30,7 @@ class Registered(BaseEvent): @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Registered, part_of=User) test_domain.init(traverse=False) @@ -167,4 +167,4 @@ def test_origin_stream_in_command_from_event( enriched_command = test_domain._enrich_command(command) command_message = Message.to_message(enriched_command) - assert command_message.metadata.origin_stream == f"user-{user_id}" + assert command_message.metadata.origin_stream == f"test::user-{user_id}" diff --git a/tests/server/test_any_event_handler.py b/tests/server/test_any_event_handler.py index 8333b223..57dc75e7 100644 --- a/tests/server/test_any_event_handler.py +++ b/tests/server/test_any_event_handler.py @@ -2,7 +2,7 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import Identifier, String from protean.server import Engine from protean.utils.mixins import Message @@ -15,7 +15,7 @@ def count_up(): counter += 1 -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() @@ -36,7 +36,7 @@ def increment(self, event: BaseEventHandler) -> None: @pytest.mark.asyncio async def test_that_an_event_handler_can_be_associated_with_an_all_stream(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(UserEventHandler, part_of=User) test_domain.init(traverse=False) diff --git a/tests/server/test_command_handler_subscription.py b/tests/server/test_command_handler_subscription.py index 8cec2be3..fe6238c7 100644 --- a/tests/server/test_command_handler_subscription.py +++ b/tests/server/test_command_handler_subscription.py @@ -1,12 +1,12 @@ import pytest -from protean import BaseCommand, BaseCommandHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseCommand, BaseCommandHandler, handle from protean.fields import Identifier, String from protean.server import Engine from protean.utils import fully_qualified_name -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -37,10 +37,11 @@ def activate(self, command: Activate) -> None: @pytest.fixture(autouse=True) def register(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Activate, part_of=User) test_domain.register(UserCommandHandler, part_of=User) + test_domain.init(traverse=False) @pytest.fixture @@ -54,5 +55,5 @@ def test_command_handler_subscriptions(engine): assert fully_qualified_name(UserCommandHandler) in engine._subscriptions assert ( engine._subscriptions[fully_qualified_name(UserCommandHandler)].stream_category - == "user:command" + == "test::user:command" ) diff --git a/tests/server/test_command_handling.py b/tests/server/test_command_handling.py index 94ccc758..208e288f 100644 --- a/tests/server/test_command_handling.py +++ b/tests/server/test_command_handling.py @@ -2,7 +2,7 @@ import pytest -from protean import BaseCommand, BaseCommandHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseCommand, BaseCommandHandler, handle from protean.fields import Identifier, String from protean.server import Engine from protean.utils.mixins import Message @@ -10,7 +10,7 @@ counter = 0 -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): id = Identifier(identifier=True) email = String() name = String() @@ -42,7 +42,7 @@ def activate(self, command: Activate) -> None: @pytest.mark.asyncio async def test_handler_invocation(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Activate, part_of=User) test_domain.register(UserCommandHandler, part_of=User) diff --git a/tests/server/test_error_handling.py b/tests/server/test_error_handling.py index 9552968d..b58dac29 100644 --- a/tests/server/test_error_handling.py +++ b/tests/server/test_error_handling.py @@ -3,13 +3,13 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import Identifier, String from protean.server import Engine from protean.utils.mixins import Message -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() @@ -48,7 +48,7 @@ def auto_set_and_close_loop(): @pytest.mark.asyncio async def test_that_exception_is_raised(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(UserEventHandler, part_of=User) test_domain.init(traverse=False) @@ -78,7 +78,7 @@ async def test_that_exception_is_raised(test_domain): def test_exceptions_stop_processing(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(UserEventHandler, part_of=User) diff --git a/tests/server/test_event_handler_subscription.py b/tests/server/test_event_handler_subscription.py index be70cc97..6a31d448 100644 --- a/tests/server/test_event_handler_subscription.py +++ b/tests/server/test_event_handler_subscription.py @@ -4,7 +4,7 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import DateTime, Identifier, String from protean.server import Engine from protean.utils import fqn @@ -27,7 +27,7 @@ class Sent(BaseEvent): sent_on = DateTime() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() @@ -72,41 +72,49 @@ def setup_event_loop(): def test_event_subscriptions(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(UserEventHandler, part_of=User) + test_domain.init(traverse=False) engine = Engine(test_domain, test_mode=True) assert len(engine._subscriptions) == 1 assert fqn(UserEventHandler) in engine._subscriptions - assert engine._subscriptions[fqn(UserEventHandler)].stream_category == "user" + assert engine._subscriptions[fqn(UserEventHandler)].stream_category == "test::user" def test_origin_stream_category_in_subscription(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Sent, part_of=User) - test_domain.register(EmailEventHandler, part_of=User, source_stream="email") + test_domain.register(EmailEventHandler, part_of=User, source_stream="test::email") + test_domain.init(traverse=False) engine = Engine(test_domain, test_mode=True) assert len(engine._subscriptions) == 1 - assert engine._subscriptions[fqn(EmailEventHandler)].stream_category == "user" - assert engine._subscriptions[fqn(EmailEventHandler)].origin_stream == "email" + assert engine._subscriptions[fqn(EmailEventHandler)].stream_category == "test::user" + assert engine._subscriptions[fqn(EmailEventHandler)].origin_stream == "test::email" def test_that_stream_name_overrides_the_derived_stream_name_from_owning_aggregate( test_domain, ): + test_domain.register(User, is_event_sourced=True) + test_domain.register(Sent, part_of=User) test_domain.register( EmailEventHandler, part_of=User, - stream_category="identity", - source_stream="email", + stream_category="test::identity", + source_stream="test::email", ) + test_domain.init(traverse=False) engine = Engine(test_domain, test_mode=True) assert len(engine._subscriptions) == 1 - assert engine._subscriptions[fqn(EmailEventHandler)].stream_category == "identity" - assert engine._subscriptions[fqn(EmailEventHandler)].origin_stream == "email" + assert ( + engine._subscriptions[fqn(EmailEventHandler)].stream_category + == "test::identity" + ) + assert engine._subscriptions[fqn(EmailEventHandler)].origin_stream == "test::email" diff --git a/tests/server/test_event_handling.py b/tests/server/test_event_handling.py index c42718a2..67c944f8 100644 --- a/tests/server/test_event_handling.py +++ b/tests/server/test_event_handling.py @@ -4,7 +4,7 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import Identifier, String from protean.server import Engine from protean.utils.mixins import Message @@ -12,7 +12,7 @@ counter = 0 -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() @@ -38,7 +38,7 @@ def send_notification(self, event: Registered) -> None: @pytest.mark.asyncio async def test_handler_invocation(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(UserEventHandler, part_of=User) test_domain.init(traverse=False) diff --git a/tests/server/test_handling_all_events.py b/tests/server/test_handling_all_events.py index 9dca5254..4dedb9c5 100644 --- a/tests/server/test_handling_all_events.py +++ b/tests/server/test_handling_all_events.py @@ -2,7 +2,7 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import Identifier, String, Text from protean.server import Engine from protean.utils.mixins import Message @@ -15,7 +15,7 @@ def count_up(): counter += 1 -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() @@ -28,7 +28,7 @@ class Registered(BaseEvent): password_hash = String() -class Post(BaseEventSourcedAggregate): +class Post(BaseAggregate): topic = String() content = Text() @@ -48,9 +48,9 @@ def increment(self, event: BaseEventHandler) -> None: @pytest.mark.asyncio @pytest.mark.eventstore async def test_that_any_message_can_be_handled_with_any_handler(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) - test_domain.register(Post) + test_domain.register(Post, is_event_sourced=True) test_domain.register(Created, part_of=Post) test_domain.register(SystemMetrics, stream_category="$all") test_domain.init(traverse=False) diff --git a/tests/subscription/test_message_filtering_with_origin_stream.py b/tests/subscription/test_message_filtering_with_origin_stream.py index 0a606d16..79d257dc 100644 --- a/tests/subscription/test_message_filtering_with_origin_stream.py +++ b/tests/subscription/test_message_filtering_with_origin_stream.py @@ -6,7 +6,7 @@ import mock import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.core.event import Metadata from protean.fields import DateTime, Identifier, String from protean.server import Engine @@ -14,13 +14,13 @@ from protean.utils.mixins import Message -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() -class Email(BaseEventSourcedAggregate): +class Email(BaseAggregate): email = String() sent_at = DateTime() @@ -68,12 +68,12 @@ def record_sent_email(self, event: Sent) -> None: @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(UserEventHandler, part_of=User) - test_domain.register(Email) + test_domain.register(Email, is_event_sourced=True) test_domain.register(Sent, part_of=Email) test_domain.register( EmailEventHandler, stream_category="email", source_stream="user" diff --git a/tests/subscription/test_message_handover_to_engine.py b/tests/subscription/test_message_handover_to_engine.py index 062fa384..c56dc3d6 100644 --- a/tests/subscription/test_message_handover_to_engine.py +++ b/tests/subscription/test_message_handover_to_engine.py @@ -5,7 +5,7 @@ import mock import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import Identifier, String from protean.server import Engine from protean.server.subscription import Subscription @@ -22,7 +22,7 @@ class Registered(BaseEvent): password_hash = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() @@ -54,7 +54,7 @@ def send_notification(self, event: Registered) -> None: async def test_that_subscription_invokes_engine_handler_on_message( mock_handle_message, test_domain ): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(UserEventHandler, part_of=User) test_domain.init(traverse=False) @@ -70,7 +70,7 @@ async def test_that_subscription_invokes_engine_handler_on_message( engine = Engine(test_domain, test_mode=True) subscription = Subscription( - engine, fully_qualified_name(UserEventHandler), "user", UserEventHandler + engine, fully_qualified_name(UserEventHandler), "test::user", UserEventHandler ) await subscription.poll() diff --git a/tests/subscription/test_no_message_filtering.py b/tests/subscription/test_no_message_filtering.py index b85f74c2..1afb4d15 100644 --- a/tests/subscription/test_no_message_filtering.py +++ b/tests/subscription/test_no_message_filtering.py @@ -6,7 +6,7 @@ import mock import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.core.event import Metadata from protean.fields import DateTime, Identifier, String from protean.server import Engine @@ -14,13 +14,13 @@ from protean.utils.mixins import Message -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() -class Email(BaseEventSourcedAggregate): +class Email(BaseAggregate): email = String() sent_at = DateTime() @@ -68,11 +68,11 @@ def record_sent_email(self, event: Sent) -> None: @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) test_domain.register(UserEventHandler, part_of=User) - test_domain.register(Email) + test_domain.register(Email, is_event_sourced=True) test_domain.register(Sent, part_of=Email) test_domain.register(EmailEventHandler, stream_category="email") test_domain.init(traverse=False) diff --git a/tests/subscription/test_read_position_updates.py b/tests/subscription/test_read_position_updates.py index 8fe7c578..0c372bc3 100644 --- a/tests/subscription/test_read_position_updates.py +++ b/tests/subscription/test_read_position_updates.py @@ -5,19 +5,19 @@ import pytest -from protean import BaseEvent, BaseEventHandler, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseEvent, BaseEventHandler, handle from protean.fields import DateTime, Identifier, String from protean.server import Engine from protean.utils import fqn -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() -class Email(BaseEventSourcedAggregate): +class Email(BaseAggregate): email = String() sent_at = DateTime() @@ -65,12 +65,12 @@ def record_sent_email(self, event: Sent) -> None: @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) - test_domain.register(Email) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Activated, part_of=User) - test_domain.register(Sent, part_of=Email) test_domain.register(UserEventHandler, part_of=User) + test_domain.register(Email, is_event_sourced=True) + test_domain.register(Sent, part_of=Email) test_domain.register(EmailEventHandler, stream_category="email") test_domain.init(traverse=False) diff --git a/tests/unit_of_work/test_inline_event_processing.py b/tests/unit_of_work/test_inline_event_processing.py index b8fb0a8c..43db627c 100644 --- a/tests/unit_of_work/test_inline_event_processing.py +++ b/tests/unit_of_work/test_inline_event_processing.py @@ -5,11 +5,11 @@ import pytest from protean import ( + BaseAggregate, BaseCommand, BaseCommandHandler, BaseEvent, BaseEventHandler, - BaseEventSourcedAggregate, apply, handle, ) @@ -38,7 +38,7 @@ class Registered(BaseEvent): password_hash = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): user_id = Identifier(identifier=True) email = String() name = String() @@ -92,7 +92,7 @@ def count_registrations(self, _: BaseEventHandler) -> None: @pytest.mark.eventstore def test_inline_event_processing_in_sync_mode(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Register, part_of=User) test_domain.register(Registered, part_of=User) test_domain.register(UserCommandHandler, part_of=User) diff --git a/tests/unit_of_work/test_nested_inline_event_processing.py b/tests/unit_of_work/test_nested_inline_event_processing.py index cdf0dc5a..7ffbb1f2 100644 --- a/tests/unit_of_work/test_nested_inline_event_processing.py +++ b/tests/unit_of_work/test_nested_inline_event_processing.py @@ -5,11 +5,11 @@ import pytest from protean import ( + BaseAggregate, BaseCommand, BaseCommandHandler, BaseEvent, BaseEventHandler, - BaseEventSourcedAggregate, apply, handle, ) @@ -38,7 +38,7 @@ class Published(BaseEvent): published_time = DateTime(default=utcnow_func) -class Post(BaseEventSourcedAggregate): +class Post(BaseAggregate): topic = String() content = Text() is_published = Boolean(default=False) @@ -93,12 +93,12 @@ def record_publishing(self, _: Published) -> None: @pytest.mark.eventstore def test_nested_uow_processing(test_domain): - test_domain.register(Post) + test_domain.register(Post, is_event_sourced=True) test_domain.register(Create, part_of=Post) test_domain.register(Created, part_of=Post) test_domain.register(Published, part_of=Post) test_domain.register(PostEventHandler, part_of=Post) - test_domain.register(Metrics, stream_category="post") + test_domain.register(Metrics, stream_category="test::post") test_domain.init(traverse=False) identifier = str(uuid4()) diff --git a/tests/unit_of_work/test_storing_events_on_commit.py b/tests/unit_of_work/test_storing_events_on_commit.py index a9cf1f78..003e1dc6 100644 --- a/tests/unit_of_work/test_storing_events_on_commit.py +++ b/tests/unit_of_work/test_storing_events_on_commit.py @@ -4,7 +4,7 @@ import pytest -from protean import BaseCommandHandler, BaseEvent, BaseEventSourcedAggregate, handle +from protean import BaseAggregate, BaseCommandHandler, BaseEvent, handle from protean.core.command import BaseCommand from protean.fields import String from protean.fields.basic import Identifier @@ -18,7 +18,7 @@ class Register(BaseCommand): password_hash = String() -class User(BaseEventSourcedAggregate): +class User(BaseAggregate): email = String() name = String() password_hash = String() @@ -60,7 +60,7 @@ def register_user(self, command: Register) -> None: @pytest.fixture(autouse=True) def register_elements(test_domain): - test_domain.register(User) + test_domain.register(User, is_event_sourced=True) test_domain.register(Registered, part_of=User) test_domain.register(Register, part_of=User) test_domain.register(UserCommandHandler, part_of=User) @@ -79,6 +79,6 @@ def test_persisting_events_on_commit(test_domain): ) ) - events = test_domain.event_store.store._read(f"user-{identifier}") + events = test_domain.event_store.store._read(f"test::user-{identifier}") assert len(events) == 1 From eea02e51dd2b8f4f94be6e0d4b33459dab9ce840 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Fri, 19 Jul 2024 13:51:16 -0700 Subject: [PATCH 2/4] Remove EventSourcedAggregate related files, attrs and methods --- src/protean/__init__.py | 4 +- src/protean/core/aggregate.py | 44 ++++- src/protean/core/event_sourced_aggregate.py | 183 ------------------ src/protean/core/event_sourced_repository.py | 6 +- src/protean/domain/__init__.py | 26 +-- src/protean/port/event_store.py | 6 +- src/protean/utils/__init__.py | 1 - ...t_raising_events_from_within_aggregates.py | 3 +- .../test_appending_aggregate_events.py | 3 +- tests/test_registry.py | 1 - 10 files changed, 55 insertions(+), 222 deletions(-) delete mode 100644 src/protean/core/event_sourced_aggregate.py diff --git a/src/protean/__init__.py b/src/protean/__init__.py index e44a6720..185a038d 100644 --- a/src/protean/__init__.py +++ b/src/protean/__init__.py @@ -1,6 +1,6 @@ __version__ = "0.12.1" -from .core.aggregate import BaseAggregate, atomic_change +from .core.aggregate import BaseAggregate, apply, atomic_change from .core.application_service import BaseApplicationService from .core.command import BaseCommand from .core.command_handler import BaseCommandHandler @@ -9,7 +9,6 @@ from .core.entity import BaseEntity, invariant from .core.event import BaseEvent from .core.event_handler import BaseEventHandler -from .core.event_sourced_aggregate import BaseEventSourcedAggregate, apply from .core.model import BaseModel from .core.queryset import Q, QuerySet from .core.repository import BaseRepository @@ -33,7 +32,6 @@ "BaseEntity", "BaseEvent", "BaseEventHandler", - "BaseEventSourcedAggregate", "BaseModel", "BaseRepository", "BaseSerializer", diff --git a/src/protean/core/aggregate.py b/src/protean/core/aggregate.py index a27e73e1..c88d05f8 100644 --- a/src/protean/core/aggregate.py +++ b/src/protean/core/aggregate.py @@ -1,14 +1,16 @@ """Aggregate Functionality and Classes""" +import functools import inspect import logging +import typing from collections import defaultdict from typing import List from protean.core.entity import BaseEntity from protean.core.event import BaseEvent from protean.core.value_object import BaseValueObject -from protean.exceptions import NotSupportedError +from protean.exceptions import IncorrectUsageError, NotSupportedError from protean.fields import HasMany, HasOne, Integer, Reference, ValueObject from protean.fields import List as ProteanList from protean.reflection import fields @@ -235,3 +237,43 @@ def __exit__(self, *args): # Validate on exit to trigger invariant checks self.aggregate._disable_invariant_checks = False self.aggregate._postcheck() + + +def apply(fn): + """Decorator to mark methods in EventHandler classes.""" + + if len(typing.get_type_hints(fn)) > 2: + raise IncorrectUsageError( + { + "_entity": [ + f"Handler method `{fn.__name__}` has incorrect number of arguments" + ] + } + ) + + try: + _event_cls = next( + iter( + { + value + for value in typing.get_type_hints(fn).values() + if inspect.isclass(value) and issubclass(value, BaseEvent) + } + ) + ) + except StopIteration: + raise IncorrectUsageError( + { + "_entity": [ + f"Apply method `{fn.__name__}` should accept an argument annotated with the Event class" + ] + } + ) + + @functools.wraps(fn) + def wrapper(*args): + fn(*args) + + setattr(wrapper, "_event_cls", _event_cls) + + return wrapper diff --git a/src/protean/core/event_sourced_aggregate.py b/src/protean/core/event_sourced_aggregate.py deleted file mode 100644 index 37ab28e2..00000000 --- a/src/protean/core/event_sourced_aggregate.py +++ /dev/null @@ -1,183 +0,0 @@ -import functools -import inspect -import logging -import typing -from collections import defaultdict -from typing import List - -from protean.container import BaseContainer, EventedMixin, IdentityMixin, OptionsMixin -from protean.core.event import BaseEvent -from protean.exceptions import IncorrectUsageError, NotSupportedError -from protean.fields import Integer -from protean.reflection import id_field -from protean.utils import ( - DomainObjects, - derive_element_class, - fully_qualified_name, - inflection, -) - -logger = logging.getLogger(__name__) - - -class BaseEventSourcedAggregate( - OptionsMixin, IdentityMixin, EventedMixin, BaseContainer -): - """Base Event Sourced Aggregate class that all EventSourced Aggregates should inherit from. - - The order of inheritance is important. We want BaseContainer to be initialised first followed by - OptionsMixin (so that `meta_` is in place) before inheriting other mixins.""" - - element_type = DomainObjects.EVENT_SOURCED_AGGREGATE - - # Track current version of Aggregate - _version = Integer(default=-1) - - # Temporary variable to track version of events of Aggregate - # This can be different from the version of the Aggregate itself because - # a single aggregate update could have triggered multiple events. - _event_position = -1 - - def __new__(cls, *args, **kwargs): - if cls is BaseEventSourcedAggregate: - raise NotSupportedError("BaseEventSourcedAggregate cannot be instantiated") - return super().__new__(cls) - - @classmethod - def _default_options(cls): - return [ - ("aggregate_cluster", None), - ("auto_add_id_field", True), - ("fact_events", False), - ("stream_category", inflection.underscore(cls.__name__)), - ] - - def __init_subclass__(subclass) -> None: - super().__init_subclass__() - - # Associate a `_projections` map with subclasses. - # It needs to be initialized here because if it - # were initialized in __init__, the same collection object - # would be made available across all subclasses, - # defeating its purpose. - setattr(subclass, "_projections", defaultdict(set)) - - # Store associated events - setattr(subclass, "_events_cls_map", {}) - - def __eq__(self, other): - """Equivalence check to be based only on Identity""" - - # FIXME Enhanced Equality Checks - # * Ensure IDs have values and both of them are not null - # * Ensure that the ID is of the right type - # * Ensure that Objects belong to the same `type` - # * Check Reference equality - - # FIXME Check if `==` and `in` operator work with __eq__ - - if type(other) is type(self): - self_id = getattr(self, id_field(self).field_name) - other_id = getattr(other, id_field(other).field_name) - - return self_id == other_id - - return False - - def __hash__(self): - """Overrides the default implementation and bases hashing on identity""" - - # FIXME Add Object Class Type to hash - return hash(getattr(self, id_field(self).field_name)) - - def _apply(self, event: BaseEvent) -> None: - """Apply the event onto the aggregate by calling the appropriate projection. - - Args: - event (BaseEvent): Event object to apply - """ - # FIXME Handle case of missing projection - event_name = fully_qualified_name(event.__class__) - - # FIXME Handle case of missing projection method - if event_name not in self._projections: - raise NotImplementedError( - f"No handler registered for event `{event_name}` in `{self.__class__.__name__}`" - ) - - for fn in self._projections[event_name]: - # Call event handler method - fn(self, event) - self._version += 1 - - @classmethod - def from_events(cls, events: List[BaseEvent]) -> "BaseEventSourcedAggregate": - """Reconstruct an aggregate from a list of events.""" - # Initialize the aggregate with the first event's payload and apply it - aggregate = cls(**events[0].payload) - aggregate._apply(events[0]) - - # Apply the rest of the events - for event in events[1:]: - aggregate._apply(event) - - return aggregate - - -def apply(fn): - """Decorator to mark methods in EventHandler classes.""" - - if len(typing.get_type_hints(fn)) > 2: - raise IncorrectUsageError( - { - "_entity": [ - f"Handler method `{fn.__name__}` has incorrect number of arguments" - ] - } - ) - - try: - _event_cls = next( - iter( - { - value - for value in typing.get_type_hints(fn).values() - if inspect.isclass(value) and issubclass(value, BaseEvent) - } - ) - ) - except StopIteration: - raise IncorrectUsageError( - { - "_entity": [ - f"Apply method `{fn.__name__}` should accept an argument annotated with the Event class" - ] - } - ) - - @functools.wraps(fn) - def wrapper(*args): - fn(*args) - - setattr(wrapper, "_event_cls", _event_cls) - - return wrapper - - -def event_sourced_aggregate_factory(element_cls, domain, **opts): - element_cls = derive_element_class(element_cls, BaseEventSourcedAggregate, **opts) - - # Iterate through methods marked as `@apply` and construct a projections map - methods = inspect.getmembers(element_cls, predicate=inspect.isroutine) - for method_name, method in methods: - if not ( - method_name.startswith("__") and method_name.endswith("__") - ) and hasattr(method, "_event_cls"): - element_cls._projections[fully_qualified_name(method._event_cls)].add( - method - ) - element_cls._events_cls_map[fully_qualified_name(method._event_cls)] = ( - method._event_cls - ) - - return element_cls diff --git a/src/protean/core/event_sourced_repository.py b/src/protean/core/event_sourced_repository.py index acb4f1fe..f605504d 100644 --- a/src/protean/core/event_sourced_repository.py +++ b/src/protean/core/event_sourced_repository.py @@ -1,6 +1,6 @@ import logging -from protean import BaseEventSourcedAggregate, UnitOfWork +from protean import BaseAggregate, UnitOfWork from protean.container import Element, OptionsMixin from protean.exceptions import ( IncorrectUsageError, @@ -30,7 +30,7 @@ def __new__(cls, *args, **kwargs): def __init__(self, domain) -> None: self._domain = domain - def add(self, aggregate: BaseEventSourcedAggregate) -> None: + def add(self, aggregate: BaseAggregate) -> None: if aggregate is None: raise IncorrectUsageError( {"_entity": ["Aggregate object to persist is invalid"]} @@ -63,7 +63,7 @@ def add(self, aggregate: BaseEventSourcedAggregate) -> None: if own_current_uow: own_current_uow.commit() - def get(self, identifier: Identifier) -> BaseEventSourcedAggregate: + def get(self, identifier: Identifier) -> BaseAggregate: """Retrieve a fully-formed Aggregate from a stream of Events. If the aggregate was already loaded in the current UnitOfWork, diff --git a/src/protean/domain/__init__.py b/src/protean/domain/__init__.py index badc6c98..d45ea172 100644 --- a/src/protean/domain/__init__.py +++ b/src/protean/domain/__init__.py @@ -445,7 +445,6 @@ def factory_for(self, domain_object_type): from protean.core.entity import entity_factory from protean.core.event import domain_event_factory from protean.core.event_handler import event_handler_factory - from protean.core.event_sourced_aggregate import event_sourced_aggregate_factory from protean.core.event_sourced_repository import ( event_sourced_repository_factory, ) @@ -463,7 +462,6 @@ def factory_for(self, domain_object_type): DomainObjects.COMMAND_HANDLER.value: command_handler_factory, DomainObjects.EVENT.value: domain_event_factory, DomainObjects.EVENT_HANDLER.value: event_handler_factory, - DomainObjects.EVENT_SOURCED_AGGREGATE.value: event_sourced_aggregate_factory, DomainObjects.EVENT_SOURCED_REPOSITORY.value: event_sourced_repository_factory, DomainObjects.DOMAIN_SERVICE.value: domain_service_factory, DomainObjects.EMAIL.value: email_factory, @@ -568,7 +566,6 @@ def _resolve_references(self): field_obj.to_cls, ( DomainObjects.AGGREGATE, - DomainObjects.EVENT_SOURCED_AGGREGATE, DomainObjects.ENTITY, ), ) @@ -584,10 +581,7 @@ def _resolve_references(self): cls = params to_cls = self.fetch_element_cls_from_registry( cls.meta_.part_of, - ( - DomainObjects.AGGREGATE, - DomainObjects.EVENT_SOURCED_AGGREGATE, - ), + (DomainObjects.AGGREGATE,), ) cls.meta_.part_of = to_cls case _: @@ -810,12 +804,10 @@ def _validate_domain(self): def _assign_aggregate_clusters(self): """Assign Aggregate Clusters to all relevant elements""" from protean.core.aggregate import BaseAggregate - from protean.core.event_sourced_aggregate import BaseEventSourcedAggregate # Assign Aggregates and EventSourcedAggregates to their own cluster for element_type in [ DomainObjects.AGGREGATE, - DomainObjects.EVENT_SOURCED_AGGREGATE, ]: for _, element in self.registry._elements[element_type.value].items(): element.cls.meta_.aggregate_cluster = element.cls @@ -830,9 +822,7 @@ def _assign_aggregate_clusters(self): part_of = element.cls.meta_.part_of if part_of: # Traverse up the graph tree to find the root aggregate - while not issubclass( - part_of, (BaseAggregate, BaseEventSourcedAggregate) - ): + while not issubclass(part_of, BaseAggregate): part_of = part_of.meta_.part_of element.cls.meta_.aggregate_cluster = part_of @@ -978,10 +968,7 @@ def _setup_event_handlers(self): def _generate_fact_event_classes(self): """Generate FactEvent classes for all aggregates with `fact_events` enabled""" - for element_type in [ - DomainObjects.AGGREGATE, - DomainObjects.EVENT_SOURCED_AGGREGATE, - ]: + for element_type in [DomainObjects.AGGREGATE]: for _, element in self.registry._elements[element_type.value].items(): if element.cls.meta_.fact_events: event_cls = element_to_fact_event(element.cls) @@ -1029,13 +1016,6 @@ def event_handler(self, _cls=None, **kwargs): **kwargs, ) - def event_sourced_aggregate(self, _cls=None, **kwargs): - return self._domain_element( - DomainObjects.EVENT_SOURCED_AGGREGATE, - _cls=_cls, - **kwargs, - ) - def domain_service(self, _cls=None, **kwargs): return self._domain_element( DomainObjects.DOMAIN_SERVICE, diff --git a/src/protean/port/event_store.py b/src/protean/port/event_store.py index 586dcc1d..2d1e0b1c 100644 --- a/src/protean/port/event_store.py +++ b/src/protean/port/event_store.py @@ -2,7 +2,7 @@ from collections import deque from typing import Any, Dict, List, Optional, Type, Union -from protean import BaseCommand, BaseEvent, BaseEventSourcedAggregate +from protean import BaseAggregate, BaseCommand, BaseEvent from protean.fields import Identifier from protean.utils.mixins import Message @@ -104,8 +104,8 @@ def append(self, object: Union[BaseEvent, BaseCommand]) -> int: return position def load_aggregate( - self, part_of: Type[BaseEventSourcedAggregate], identifier: Identifier - ) -> Optional[BaseEventSourcedAggregate]: + self, part_of: Type[BaseAggregate], identifier: Identifier + ) -> Optional[BaseAggregate]: """Load an aggregate from underlying events. The first event is used to initialize the aggregate, after which each event is diff --git a/src/protean/utils/__init__.py b/src/protean/utils/__init__.py index b2b29712..9182cbda 100644 --- a/src/protean/utils/__init__.py +++ b/src/protean/utils/__init__.py @@ -118,7 +118,6 @@ class DomainObjects(Enum): COMMAND_HANDLER = "COMMAND_HANDLER" EVENT = "EVENT" EVENT_HANDLER = "EVENT_HANDLER" - EVENT_SOURCED_AGGREGATE = "EVENT_SOURCED_AGGREGATE" EVENT_SOURCED_REPOSITORY = "EVENT_SOURCED_REPOSITORY" DOMAIN_SERVICE = "DOMAIN_SERVICE" EMAIL = "EMAIL" diff --git a/tests/event_sourced_aggregates/test_raising_events_from_within_aggregates.py b/tests/event_sourced_aggregates/test_raising_events_from_within_aggregates.py index 16b937ac..76a39588 100644 --- a/tests/event_sourced_aggregates/test_raising_events_from_within_aggregates.py +++ b/tests/event_sourced_aggregates/test_raising_events_from_within_aggregates.py @@ -4,9 +4,8 @@ import pytest -from protean import BaseAggregate, BaseCommandHandler, BaseEvent, handle +from protean import BaseAggregate, BaseCommandHandler, BaseEvent, apply, handle from protean.core.command import BaseCommand -from protean.core.event_sourced_aggregate import apply from protean.fields import Identifier, String from protean.globals import current_domain diff --git a/tests/event_store/test_appending_aggregate_events.py b/tests/event_store/test_appending_aggregate_events.py index 408a95fc..62e54a30 100644 --- a/tests/event_store/test_appending_aggregate_events.py +++ b/tests/event_store/test_appending_aggregate_events.py @@ -4,8 +4,7 @@ import pytest -from protean import BaseAggregate, BaseEvent -from protean.core.event_sourced_aggregate import apply +from protean import BaseAggregate, BaseEvent, apply from protean.fields import String from protean.fields.basic import Identifier diff --git a/tests/test_registry.py b/tests/test_registry.py index 804e4982..31382ba5 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -96,7 +96,6 @@ def test_properties_method_returns_a_dictionary_of_all_protean_elements(): "emails": DomainObjects.EMAIL.value, "entities": DomainObjects.ENTITY.value, "event_handlers": DomainObjects.EVENT_HANDLER.value, - "event_sourced_aggregates": DomainObjects.EVENT_SOURCED_AGGREGATE.value, "event_sourced_repositories": DomainObjects.EVENT_SOURCED_REPOSITORY.value, "events": DomainObjects.EVENT.value, "models": DomainObjects.MODEL.value, From 807cc4dfc8936ecdd9e5c63afdd5af69b4ceeb58 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Fri, 19 Jul 2024 15:20:38 -0700 Subject: [PATCH 3/4] Support multi-level event sourced aggregates Event sourced aggregates so far were only limited to on aggregate, with no support for underlying entities. This commit allows event sourced aggregates to behave just like regular aggregates with multi-level heirarchies. --- src/protean/container.py | 74 -------- src/protean/fields/association.py | 15 +- .../test_multi_level_aggregate_persistence.py | 164 ++++++++++++++++++ 3 files changed, 177 insertions(+), 76 deletions(-) create mode 100644 tests/event_sourced_aggregates/test_multi_level_aggregate_persistence.py diff --git a/src/protean/container.py b/src/protean/container.py index 46b6aac9..5954a2c2 100644 --- a/src/protean/container.py +++ b/src/protean/container.py @@ -2,19 +2,16 @@ import copy import inspect -import json import logging from collections import defaultdict from typing import Any, Type, Union from protean.exceptions import ( - ConfigurationError, InvalidDataError, NotSupportedError, ValidationError, ) from protean.fields import Auto, Field, FieldBase, ValueObject -from protean.reflection import id_field from protean.utils import generate_identity from .reflection import ( @@ -366,77 +363,6 @@ def _default_options(cls): return [] -class EventedMixin: - def __init__(self, *args, **kwargs) -> None: - """Initialize an instance-level variable named `_events` to track events - raised in the aggregate cluster. - - This method cannot have a super invocation, because we don't want it to - invoke BaseContainer's `__init__` method. But there is a conflict regarding - this between BaseAggregate and BaseEventSourcedAggregate. So this Mixin's - functionality has been replicated temporarily in BaseAggregate class. - - Other mixins that are inherited by BaseEntity and BaseEventSourcedAggregate - work with `__init_subclass__`, and do not have this issue. - """ - super().__init__(*args, **kwargs) - self._events = [] - - def raise_(self, event) -> None: - """Raise an event in the aggregate cluster. - - The version of the aggregate is incremented with every event raised, which is true - in the case of Event Sourced Aggregates. - - Event is immutable, so we clone a new event object from the event raised, - and add the enhanced metadata to it. - """ - # Verify that event is indeed associated with this aggregate - if event.meta_.part_of != self.__class__: - raise ConfigurationError( - f"Event `{event.__class__.__name__}` is not associated with " - f"aggregate `{self.__class__.__name__}`" - ) - - if not self.meta_.fact_events: - self._version += 1 - - identifier = getattr(self, id_field(self).field_name) - - # Set Fact Event stream to be `-fact` - if event.__class__.__name__.endswith("FactEvent"): - stream = f"{self.meta_.stream_category}-fact-{identifier}" - else: - stream = f"{self.meta_.stream_category}-{identifier}" - - event_with_metadata = event.__class__( - event.to_dict(), - _expected_version=self._event_position, - _metadata={ - "id": (f"{stream}-{self._version}"), - "type": event._metadata.type, - "fqn": event._metadata.fqn, - "kind": event._metadata.kind, - "stream": stream, - "origin_stream": event._metadata.origin_stream, - "timestamp": event._metadata.timestamp, - "version": event._metadata.version, - "sequence_id": self._version, - "payload_hash": hash( - json.dumps( - event.payload, - sort_keys=True, - ) - ), - }, - ) - - # Increment the event position after generating event - self._event_position = self._event_position + 1 - - self._events.append(event_with_metadata) - - class IdentityMixin: def __init_subclass__(subclass) -> None: super().__init_subclass__() diff --git a/src/protean/fields/association.py b/src/protean/fields/association.py index e7884244..4278c165 100644 --- a/src/protean/fields/association.py +++ b/src/protean/fields/association.py @@ -377,6 +377,8 @@ def __set__(self, instance, value): The `temp_cache` we set up here is eventually used by the `Repository` to determine the changes to be persisted. """ + if isinstance(value, dict): + value = self.to_cls(**value) super().__set__(instance, value) @@ -481,10 +483,19 @@ def __set__(self, instance, value): """This supports direct assignment of values to HasMany fields, like: `order.items = [item1, item2, item3]` """ - super().__set__(instance, value) + value = value if isinstance(value, list) else [value] + + values = [] + for item in value: + if isinstance(item, dict): + values.append(self.to_cls(**item)) + else: + values.append(item) + + super().__set__(instance, values) if value is not None: - self.add(instance, value) + self.add(instance, values) def add(self, instance, items) -> None: """ diff --git a/tests/event_sourced_aggregates/test_multi_level_aggregate_persistence.py b/tests/event_sourced_aggregates/test_multi_level_aggregate_persistence.py new file mode 100644 index 00000000..39c655c8 --- /dev/null +++ b/tests/event_sourced_aggregates/test_multi_level_aggregate_persistence.py @@ -0,0 +1,164 @@ +import pytest + +from protean import BaseAggregate, BaseEntity, BaseEvent, BaseValueObject, apply +from protean.fields import ( + HasMany, + HasOne, + Identifier, + Integer, + List, + String, + ValueObject, +) + + +class Department(BaseEntity): + name = String(max_length=50) + dean = HasOne("Dean") + + +class Dean(BaseEntity): + name = String(max_length=50) + age = Integer(min_value=21) + office = HasOne("Office") + + +class Office(BaseEntity): + building = String(max_length=25) + room = Integer(min_value=1) + + +class OfficeVO(BaseValueObject): + id = Identifier() + building = String(max_length=25) + room = Integer(min_value=1) + + +class DeanVO(BaseValueObject): + id = Identifier() + name = String(max_length=50) + age = Integer(min_value=21) + office = ValueObject(OfficeVO) + + +class DepartmentVO(BaseValueObject): + id = Identifier() + name = String(max_length=50) + dean = ValueObject(DeanVO) + + +class UniversityCreated(BaseEvent): + id = Identifier(identifier=True) + _version = Integer() + name = String(max_length=50) + departments = List(content_type=ValueObject(DepartmentVO)) + + +class NameChanged(BaseEvent): + id = Identifier(identifier=True) + name = String(max_length=50) + + +class University(BaseAggregate): + name = String(max_length=50) + departments = HasMany(Department) + + def raise_event(self): + self.raise_(UniversityCreated(**self.to_dict())) + + def change_name(self, name): + self.name = name + self.raise_(NameChanged(id=self.id, name=name)) + + @apply + def on_university_created(self, event: UniversityCreated): + # We are not doing anything here, because Protean applies + # the first event automatically, with from_events + pass + + @apply + def on_name_changed(self, event: NameChanged): + self.name = event.name + + +@pytest.fixture(autouse=True) +def register_elements(test_domain): + test_domain.register(University, is_event_sourced=True) + test_domain.register(Department, part_of=University) + test_domain.register(Dean, part_of=Department) + test_domain.register(Office, part_of=Dean) + test_domain.register(UniversityCreated, part_of=University) + test_domain.register(NameChanged, part_of=University) + test_domain.init(traverse=False) + + +@pytest.fixture +def university(test_domain): + university = University( + name="MIT", + departments=[ + Department( + name="Computer Science", + dean=Dean( + name="John Doe", age=45, office=Office(building="NE43", room=123) + ), + ), + Department( + name="Electrical Engineering", + dean=Dean( + name="Jane Smith", age=50, office=Office(building="NE43", room=124) + ), + ), + ], + ) + university.raise_event() + test_domain.repository_for(University).add(university) + + return university + + +def test_aggregate_persistence(test_domain, university): + refreshed_university = test_domain.repository_for(University).get(university.id) + + assert refreshed_university.id == university.id + assert refreshed_university.name == university.name + assert len(refreshed_university.departments) == 2 + assert refreshed_university.departments[0].name == university.departments[0].name + assert refreshed_university.departments[1].name == university.departments[1].name + assert ( + refreshed_university.departments[0].dean.name + == university.departments[0].dean.name + ) + assert ( + refreshed_university.departments[1].dean.name + == university.departments[1].dean.name + ) + assert ( + refreshed_university.departments[0].dean.office.building + == university.departments[0].dean.office.building + ) + assert ( + refreshed_university.departments[1].dean.office.building + == university.departments[1].dean.office.building + ) + assert ( + refreshed_university.departments[0].dean.office.room + == university.departments[0].dean.office.room + ) + assert ( + refreshed_university.departments[1].dean.office.room + == university.departments[1].dean.office.room + ) + + +def test_aggregate_persistence_after_update(test_domain, university): + refreshed_university = test_domain.repository_for(University).get(university.id) + + refreshed_university.change_name("Harvard") + test_domain.repository_for(University).add(refreshed_university) + + refreshed_university = test_domain.repository_for(University).get( + refreshed_university.id + ) + + assert refreshed_university.name == "Harvard" From 81939546dad4e968c333798eae7f49fe83ebce90 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Fri, 19 Jul 2024 15:58:20 -0700 Subject: [PATCH 4/4] Additional tests for coverage and documentation --- src/protean/core/aggregate.py | 19 +++++++++-- src/protean/domain/__init__.py | 13 ++++--- src/protean/fields/association.py | 2 ++ src/protean/utils/__init__.py | 26 +------------- tests/test_containers.py | 7 +++- tests/test_utils.py | 56 +++++++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 34 deletions(-) create mode 100644 tests/test_utils.py diff --git a/src/protean/core/aggregate.py b/src/protean/core/aggregate.py index c88d05f8..387345bb 100644 --- a/src/protean/core/aggregate.py +++ b/src/protean/core/aggregate.py @@ -64,6 +64,8 @@ def __new__(cls, *args, **kwargs): def __init_subclass__(subclass) -> None: super().__init_subclass__() + # Event-Sourcing Functionality + # # Associate a `_projections` map with subclasses. # It needs to be initialized here because if it # were initialized in __init__, the same collection object @@ -71,6 +73,8 @@ def __init_subclass__(subclass) -> None: # defeating its purpose. setattr(subclass, "_projections", defaultdict(set)) + # Event-Sourcing Functionality + # # Store associated events setattr(subclass, "_events_cls_map", {}) @@ -99,7 +103,9 @@ def _default_options(cls): ] def _apply(self, event: BaseEvent) -> None: - """Apply the event onto the aggregate by calling the appropriate projection. + """Event-Sourcing Functionality + + Apply the event onto the aggregate by calling the appropriate projection. Args: event (BaseEvent): Event object to apply @@ -120,7 +126,10 @@ def _apply(self, event: BaseEvent) -> None: @classmethod def from_events(cls, events: List[BaseEvent]) -> "BaseAggregate": - """Reconstruct an aggregate from a list of events.""" + """Event-Sourcing Functionality + + Reconstruct an aggregate from a list of events. + """ # Initialize the aggregate with the first event's payload and apply it aggregate = cls(**events[0].payload) aggregate._apply(events[0]) @@ -210,6 +219,7 @@ def aggregate_factory(element_cls, domain, **opts): f"{domain.normalized_name}::{element_cls.meta_.stream_category}" ) + # Event-Sourcing Functionality # Iterate through methods marked as `@apply` and construct a projections map methods = inspect.getmembers(element_cls, predicate=inspect.isroutine) for method_name, method in methods: @@ -240,7 +250,10 @@ def __exit__(self, *args): def apply(fn): - """Decorator to mark methods in EventHandler classes.""" + """Event-Sourcing Functionality + + Decorator to mark methods in EventHandler classes. + """ if len(typing.get_type_hints(fn)) > 2: raise IncorrectUsageError( diff --git a/src/protean/domain/__init__.py b/src/protean/domain/__init__.py index d45ea172..a0c2545f 100644 --- a/src/protean/domain/__init__.py +++ b/src/protean/domain/__init__.py @@ -968,11 +968,12 @@ def _setup_event_handlers(self): def _generate_fact_event_classes(self): """Generate FactEvent classes for all aggregates with `fact_events` enabled""" - for element_type in [DomainObjects.AGGREGATE]: - for _, element in self.registry._elements[element_type.value].items(): - if element.cls.meta_.fact_events: - event_cls = element_to_fact_event(element.cls) - self.register(event_cls, part_of=element.cls) + for _, element in self.registry._elements[ + DomainObjects.AGGREGATE.value + ].items(): + if element.cls.meta_.fact_events: + event_cls = element_to_fact_event(element.cls) + self.register(event_cls, part_of=element.cls) ###################### # Element Decorators # @@ -1209,8 +1210,10 @@ def repository_for(self, element_cls) -> BaseRepository: element_cls.element_type == DomainObjects.AGGREGATE and element_cls.meta_.is_event_sourced ): + # Return an Event Sourced repository return self.event_store.repository_for(element_cls) else: + # This is a regular aggregate or a view return self.providers.repository_for(element_cls) ####################### diff --git a/src/protean/fields/association.py b/src/protean/fields/association.py index 4278c165..83c3a78c 100644 --- a/src/protean/fields/association.py +++ b/src/protean/fields/association.py @@ -377,6 +377,7 @@ def __set__(self, instance, value): The `temp_cache` we set up here is eventually used by the `Repository` to determine the changes to be persisted. """ + # Accept dictionary values and convert them to Entity objects if isinstance(value, dict): value = self.to_cls(**value) @@ -485,6 +486,7 @@ def __set__(self, instance, value): """ value = value if isinstance(value, list) else [value] + # Accept dictionary values and convert them to Entity objects values = [] for item in value: if isinstance(item, dict): diff --git a/src/protean/utils/__init__.py b/src/protean/utils/__init__.py index 9182cbda..7fed2290 100644 --- a/src/protean/utils/__init__.py +++ b/src/protean/utils/__init__.py @@ -4,7 +4,6 @@ to the maximum extent possible. """ -import functools import importlib import logging from datetime import UTC, datetime @@ -73,14 +72,6 @@ def get_version(): return importlib.metadata.version("protean") -def import_from_full_path(domain, path): - spec = importlib.util.spec_from_file_location(domain, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - - return getattr(mod, domain) - - def fully_qualified_name(cls): """Return Fully Qualified name along with module""" return ".".join([cls.__module__, cls.__qualname__]) @@ -89,19 +80,6 @@ def fully_qualified_name(cls): fqn = fully_qualified_name -def singleton(cls): - """Make a class a Singleton class (only one instance)""" - - @functools.wraps(cls) - def wrapper_singleton(*args, **kwargs): - if not wrapper_singleton.instance: - wrapper_singleton.instance = cls(*args, **kwargs) - return wrapper_singleton.instance - - wrapper_singleton.instance = None - return wrapper_singleton - - def convert_str_values_to_list(value): if not value: return [] @@ -184,7 +162,7 @@ def generate_identity( elif id_type == IdentityType.UUID.value: id_value = uuid4() else: - raise ConfigurationError(f"Unknown Identity Type {id_type}") + raise ConfigurationError(f"Unknown Identity Type '{id_type}'") # Function Strategy elif id_strategy == IdentityStrategy.FUNCTION.value: @@ -214,7 +192,5 @@ def generate_identity( "fully_qualified_name", "generate_identity", "get_version", - "import_from_full_path", - "singleton", "utcnow_func", ] diff --git a/tests/test_containers.py b/tests/test_containers.py index 4a5bd44b..40eebb53 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -1,7 +1,7 @@ import pytest from protean.container import BaseContainer, OptionsMixin -from protean.exceptions import InvalidDataError +from protean.exceptions import InvalidDataError, NotSupportedError from protean.fields import Integer, String from protean.reflection import declared_fields @@ -18,6 +18,11 @@ class CustomContainer(CustomContainerMeta, OptionsMixin): bar = String() +def test_that_base_container_class_cannot_be_instantiated(): + with pytest.raises(NotSupportedError): + BaseContainer() + + class TestContainerInitialization: def test_that_base_container_class_cannot_be_instantiated(self): with pytest.raises(TypeError): diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..0eece996 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,56 @@ +import pytest + +from protean.exceptions import ConfigurationError +from protean.utils import convert_str_values_to_list, generate_identity + + +def test_convert_str_values_to_list(): + # Test when value is None + assert convert_str_values_to_list(None) == [] + + # Test when value is an empty string + assert convert_str_values_to_list("") == [] + + # Test when value is a non-empty string + assert convert_str_values_to_list("test") == ["test"] + + # Test when value is a list of strings + assert convert_str_values_to_list(["a", "b"]) == ["a", "b"] + + # Test when value is a list of integers + assert convert_str_values_to_list([1, 2, 3]) == [1, 2, 3] + + # Test when value is a tuple + assert convert_str_values_to_list((1, 2, 3)) == [1, 2, 3] + + # Test when value is a set + assert convert_str_values_to_list({1, 2, 3}) == [1, 2, 3] + + # Test when value is a dictionary + assert convert_str_values_to_list({"a": 1, "b": 2}) == ["a", "b"] + + # Test when value is an integer (not iterable) + with pytest.raises(TypeError): + convert_str_values_to_list(10) + + # Test when value is a float (not iterable) + with pytest.raises(TypeError): + convert_str_values_to_list(10.5) + + # Test when value is a boolean + with pytest.raises(TypeError): + convert_str_values_to_list(True) + + # Test when value is an object + class TestObject: + pass + + with pytest.raises(TypeError): + convert_str_values_to_list(TestObject()) + + +def test_unknown_identity_type_raises_exception(): + with pytest.raises(ConfigurationError) as exc: + generate_identity(identity_type="foo") + + assert str(exc.value) == "Unknown Identity Type 'foo'"