def test_graph(self): system = System(pipes=[ [ BankAccounts, EmailNotifications, ], ]) self.assertEqual(len(system.nodes), 2) self.assertIn( get_topic(BankAccounts), system.nodes.values(), ) self.assertIn( get_topic(EmailNotifications), system.nodes.values(), ) self.assertEqual(len(system.edges), 1) self.assertIn( ( "BankAccounts", "EmailNotifications", ), system.edges, )
def test_graph(self): system = System(pipes=[ [ BankAccounts, EmailProcess, ], [Application], ]) self.assertEqual(len(system.nodes), 3) self.assertEqual(system.nodes["BankAccounts"], get_topic(BankAccounts)) self.assertEqual(system.nodes["EmailProcess"], get_topic(EmailProcess)) self.assertEqual(system.nodes["Application"], get_topic(Application)) self.assertEqual(system.leaders, ["BankAccounts"]) self.assertEqual(system.followers, ["EmailProcess"]) self.assertEqual(system.singles, ["Application"]) self.assertEqual(len(system.edges), 1) self.assertIn( ( "BankAccounts", "EmailProcess", ), system.edges, ) self.assertEqual(len(system.singles), 1)
def test_createmapper_with_cipher_and_compressor(self, ): # Create mapper with cipher and compressor. self.env[self.factory.COMPRESSOR_TOPIC] = get_topic(ZlibCompressor) self.env[self.factory.CIPHER_TOPIC] = get_topic(AESCipher) cipher_key = AESCipher.create_key(16) self.env[AESCipher.CIPHER_KEY] = cipher_key mapper = self.factory.mapper(transcoder=self.transcoder) self.assertIsInstance(mapper, Mapper) self.assertIsNotNone(mapper.cipher) self.assertIsNotNone(mapper.compressor)
def setUp(self) -> None: os.environ[InfrastructureFactory.TOPIC] = get_topic(Factory) os.environ["POSTGRES_DBNAME"] = "eventsourcing" os.environ["POSTGRES_HOST"] = "127.0.0.1" os.environ["POSTGRES_USER"] = "******" os.environ["POSTGRES_PASSWORD"] = "******" super().setUp()
def test_mapper_with_wrong_cipher_key(self): self.env.name = "App1" self.env[self.factory.CIPHER_TOPIC] = get_topic(AESCipher) cipher_key1 = AESCipher.create_key(16) cipher_key2 = AESCipher.create_key(16) self.env["APP1_" + AESCipher.CIPHER_KEY] = cipher_key1 self.env["APP2_" + AESCipher.CIPHER_KEY] = cipher_key2 mapper1: Mapper = self.factory.mapper(transcoder=self.transcoder, ) domain_event = DomainEvent( originator_id=uuid4(), originator_version=1, timestamp=DomainEvent.create_timestamp(), ) stored_event = mapper1.from_domain_event(domain_event) copy = mapper1.to_domain_event(stored_event) self.assertEqual(domain_event.originator_id, copy.originator_id) self.env.name = "App2" mapper2: Mapper = self.factory.mapper(transcoder=self.transcoder, ) # This should fail because the infrastructure factory # should read different cipher keys from the environment. with self.assertRaises(ValueError): mapper2.to_domain_event(stored_event)
def test_upcast_created_event_from_v1(self): app = Application() topic_v1 = get_topic(self.UpcastFixtureV1) topic_v1_created = get_topic(self.UpcastFixtureV1.Created) aggregate = self.UpcastFixtureV1.create(a="text") app.save(aggregate) copy = app.repository.get(aggregate.id) self.assertEqual(copy.a, "text") self.assertFalse(hasattr(copy, "b")) self.assertFalse(hasattr(copy, "c")) self.assertFalse(hasattr(copy, "d")) # "Deploy" v2. del _topic_cache[topic_v1] del _topic_cache[topic_v1_created] type(self).UpcastFixtureV1 = self.UpcastFixtureV2 copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 0) self.assertFalse(hasattr(copy, "c")) self.assertFalse(hasattr(copy, "d")) # "Deploy" v3. del _topic_cache[topic_v1] del _topic_cache[topic_v1_created] type(self).UpcastFixtureV1 = self.UpcastFixtureV3 copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 0) self.assertEqual(copy.c, []) # "Deploy" v4. del _topic_cache[topic_v1] type(self).UpcastFixtureV1 = self.UpcastFixtureV4 copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 0) self.assertEqual(copy.c, []) self.assertEqual(copy.d, None)
def test_createmapper_with_compressor(self): # Create mapper with compressor. os.environ[self.factory.COMPRESSOR_TOPIC] = get_topic(ZlibCompressor) mapper = self.factory.mapper(transcoder=self.transcoder) self.assertIsInstance(mapper, Mapper) self.assertIsNone(mapper.cipher) self.assertIsNotNone(mapper.compressor)
def tearDown(self) -> None: del os.environ["IS_SNAPSHOTTING_ENABLED"] type(self).UpcastFixtureV1 = type(self).original_cls_v1 type(self).UpcastFixtureV2 = type(self).original_cls_v2 type(self).UpcastFixtureV3 = type(self).original_cls_v3 topic_v1 = get_topic(self.UpcastFixtureV1) topic_v1_created = get_topic(self.UpcastFixtureV1.Created) if topic_v1 in _topic_cache: del _topic_cache[topic_v1] if topic_v1_created in _topic_cache: del _topic_cache[topic_v1_created] topic_v2 = get_topic(self.UpcastFixtureV2) topic_v2_created = get_topic(self.UpcastFixtureV2.Created) if topic_v2 in _topic_cache: del _topic_cache[topic_v2] if topic_v2_created in _topic_cache: del _topic_cache[topic_v2_created] topic_v3 = get_topic(self.UpcastFixtureV3) topic_v3_created = get_topic(self.UpcastFixtureV3.Created) if topic_v3 in _topic_cache: del _topic_cache[topic_v3] if topic_v3_created in _topic_cache: del _topic_cache[topic_v3_created]
def test_example_application(self): app = BankAccounts(env={"IS_SNAPSHOTTING_ENABLED": "y"}) self.assertEqual(get_topic(type(app.factory)), self.expected_factory_topic) # Check AccountNotFound exception. with self.assertRaises(BankAccounts.AccountNotFoundError): app.get_account(uuid4()) # Open an account. account_id = app.open_account( full_name="Alice", email_address="*****@*****.**", ) # Credit the account. app.credit_account(account_id, Decimal("10.00")) app.credit_account(account_id, Decimal("25.00")) app.credit_account(account_id, Decimal("30.00")) # Check balance. self.assertEqual( app.get_balance(account_id), Decimal("65.00"), ) section = app.notification_log["1,10"] self.assertEqual(len(section.items), 4) # Take snapshot (specify version). app.take_snapshot(account_id, version=2) snapshots = list(app.snapshots.get(account_id, desc=True, limit=1)) self.assertEqual(len(snapshots), 1) self.assertEqual(snapshots[0].originator_version, 2) from_snapshot = app.repository.get(account_id, version=3) self.assertIsInstance(from_snapshot, BankAccount) self.assertEqual(from_snapshot.version, 3) self.assertEqual(from_snapshot.balance, Decimal("35.00")) # Take snapshot (don't specify version). app.take_snapshot(account_id) snapshots = list(app.snapshots.get(account_id, desc=True, limit=1)) self.assertEqual(len(snapshots), 1) self.assertEqual(snapshots[0].originator_version, 4) from_snapshot = app.repository.get(account_id) self.assertIsInstance(from_snapshot, BankAccount) self.assertEqual(from_snapshot.version, 4) self.assertEqual(from_snapshot.balance, Decimal("65.00"))
def test_upcast_created_event_from_v2(self): app = Application() topic_v2 = get_topic(self.UpcastFixtureV2) topic_v2_created = get_topic(self.UpcastFixtureV2.Created) aggregate = self.UpcastFixtureV2.create(A="TEXT", b=1) app.save(aggregate) copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 1) self.assertFalse(hasattr(copy, "c")) self.assertFalse(hasattr(copy, "d")) # "Deploy" v3. del _topic_cache[topic_v2] del _topic_cache[topic_v2_created] type(self).UpcastFixtureV2 = self.UpcastFixtureV3 copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 1) self.assertEqual(copy.c, []) self.assertFalse(hasattr(copy, "d")) # "Deploy" v4. del _topic_cache[topic_v2] del _topic_cache[topic_v2_created] type(self).UpcastFixtureV2 = self.UpcastFixtureV4 copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 1) self.assertEqual(copy.c, []) self.assertEqual(copy.d, None)
def test_construct_raises_exception(self): with self.assertRaises(EnvironmentError): InfrastructureFactory.construct( Environment(env={ InfrastructureFactory.PERSISTENCE_MODULE: "invalid topic" })) with self.assertRaises(AssertionError): InfrastructureFactory.construct( Environment(env={ InfrastructureFactory.PERSISTENCE_MODULE: get_topic(object) }))
def __init__( self, pipes: Iterable[Iterable[Type[Application]]], ): classes: Dict[str, Type[Application]] = {} edges: Set[Tuple[str, str]] = set() # Build nodes and edges. for pipe in pipes: follower_cls = None for cls in pipe: classes[cls.name] = cls if follower_cls is None: follower_cls = cls else: leader_cls = follower_cls follower_cls = cls edges.add(( leader_cls.name, follower_cls.name, )) self.edges = list(edges) self.nodes: Dict[str, str] = {} for name in classes: topic = get_topic(classes[name]) self.nodes[name] = topic # Identify leaders and followers. self.follows: Dict[str, List[str]] = defaultdict(list) self.leads: Dict[str, List[str]] = defaultdict(list) for edge in edges: self.leads[edge[0]].append(edge[1]) self.follows[edge[1]].append(edge[0]) # Identify singles. self.singles = [] for name in classes: if name not in self.leads and name not in self.follows: self.singles.append(name) # Check followers are followers. for name in self.follows: if not issubclass(classes[name], Follower): raise TypeError("Not a follower class: %s" % classes[name]) # Check each process is a process application class. for name in self.processors: if not issubclass(classes[name], ProcessApplication): raise TypeError("Not a process application class: %s" % classes[name])
def test_upcast_created_event_from_v3(self): app = Application() topic_v3 = get_topic(self.UpcastFixtureV3) topic_v3_created = get_topic(self.UpcastFixtureV3.Created) aggregate = self.UpcastFixtureV3.create(A="TEXT", b=1, c=[1, 2]) app.save(aggregate) copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 1) self.assertEqual(copy.c, [1, 2]) self.assertFalse(hasattr(copy, "d")) # "Deploy" v3. del _topic_cache[topic_v3] del _topic_cache[topic_v3_created] type(self).UpcastFixtureV3 = self.UpcastFixtureV4 copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 1) self.assertEqual(copy.c, [1, 2]) self.assertEqual(copy.d, None) copy.set_d(value=Decimal("10.0")) app.save(copy) copy = app.repository.get(aggregate.id) self.assertFalse(hasattr(copy, "a")) self.assertEqual(copy.A, "TEXT") self.assertEqual(copy.b, 1) self.assertEqual(copy.c, [1, 2]) self.assertEqual(copy.d, 10)
def test_createmapper_with_compressor(self): # Create mapper with compressor class as topic. self.env[self.factory.COMPRESSOR_TOPIC] = get_topic(ZlibCompressor) mapper = self.factory.mapper(transcoder=self.transcoder) self.assertIsInstance(mapper, Mapper) self.assertIsInstance(mapper.compressor, ZlibCompressor) self.assertIsNone(mapper.cipher) # Create mapper with compressor module as topic. self.env[self.factory.COMPRESSOR_TOPIC] = "zlib" mapper = self.factory.mapper(transcoder=self.transcoder) self.assertIsInstance(mapper, Mapper) self.assertEqual(mapper.compressor, zlib) self.assertIsNone(mapper.cipher)
def test_createmapper_with_cipher(self): # Check cipher needs a key. os.environ[self.factory.CIPHER_TOPIC] = get_topic(AESCipher) with self.assertRaises(EnvironmentError): self.factory.mapper(transcoder=self.transcoder) cipher_key = AESCipher.create_key(16) os.environ[self.factory.CIPHER_KEY] = cipher_key # Create mapper with cipher. mapper = self.factory.mapper(transcoder=self.transcoder) self.assertIsInstance(mapper, Mapper) self.assertIsNotNone(mapper.cipher) self.assertIsNone(mapper.compressor)
def setUp(self) -> None: os.environ[InfrastructureFactory.TOPIC] = get_topic(Factory) os.environ[Factory.POSTGRES_DBNAME] = "eventsourcing" os.environ[Factory.POSTGRES_HOST] = "127.0.0.1" os.environ[Factory.POSTGRES_PORT] = "5432" os.environ[Factory.POSTGRES_USER] = "eventsourcing" os.environ[Factory.POSTGRES_PASSWORD] = "eventsourcing" if Factory.POSTGRES_CONN_MAX_AGE in os.environ: del os.environ[Factory.POSTGRES_CONN_MAX_AGE] if Factory.POSTGRES_PRE_PING in os.environ: del os.environ[Factory.POSTGRES_PRE_PING] if Factory.POSTGRES_LOCK_TIMEOUT in os.environ: del os.environ[Factory.POSTGRES_LOCK_TIMEOUT] if Factory.POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT in os.environ: del os.environ[ Factory.POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT] self.drop_tables() super().setUp()
def _create( cls, event_class: Type[AggregateCreated[TAggregate]], *, id: Optional[UUID] = None, **kwargs: Any, ) -> TAggregate: """ Factory method to construct a new aggregate object instance. """ # Construct the domain event class, # with an ID and version, and the # a topic for the aggregate class. create_id_kwargs = { k: v for k, v in kwargs.items() if k in cls._create_id_param_names } originator_id = id or cls.create_id(**create_id_kwargs) # Impose the required common "created" event attribute values. kwargs = kwargs.copy() kwargs.update( originator_topic=get_topic(cls), originator_id=originator_id, originator_version=cls.INITIAL_VERSION, timestamp=event_class.create_timestamp(), ) try: # noinspection PyArgumentList created_event = event_class( **kwargs, ) except TypeError as e: msg = f"Unable to construct '{event_class.__name__}' event: {e}" raise TypeError(msg) # Construct the aggregate object. agg = created_event.mutate(None) assert agg is not None # Append the domain event to pending list. agg.pending_events.append(created_event) # Return the aggregate. return agg
def take(cls, aggregate: TAggregate) -> "Snapshot[TAggregate]": """ Creates a snapshot of the given :class:`Aggregate` object. """ aggregate_state = dict(aggregate.__dict__) aggregate_state.pop("_pending_events") class_version = getattr(type(aggregate), "class_version", 1) if class_version > 1: aggregate_state["class_version"] = class_version originator_id = aggregate_state.pop("_id") originator_version = aggregate_state.pop("_version") # noinspection PyArgumentList return cls( # type: ignore originator_id=originator_id, originator_version=originator_version, timestamp=cls.create_timestamp(), topic=get_topic(type(aggregate)), state=aggregate_state, )
def from_domain_event(self, domain_event: TDomainEvent) -> StoredEvent: """ Converts the given domain event to a :class:`StoredEvent` object. """ topic: str = get_topic(domain_event.__class__) event_state = copy(domain_event.__dict__) originator_id = event_state.pop("originator_id") originator_version = event_state.pop("originator_version") class_version = getattr(type(domain_event), "class_version", 1) if class_version > 1: event_state["class_version"] = class_version stored_state: bytes = self.transcoder.encode(event_state) if self.compressor: stored_state = self.compressor.compress(stored_state) if self.cipher: stored_state = self.cipher.encrypt(stored_state) return StoredEvent( originator_id=originator_id, originator_version=originator_version, topic=topic, state=stored_state, )
def _create( cls, event_class: Type[TAggregateCreated], *, id: Optional[UUID] = None, **kwargs: Any, ) -> TAggregate: """ Factory method to construct a new aggregate object instance. """ # Construct the domain event class, # with an ID and version, and the # a topic for the aggregate class. create_id_kwargs = { k: v for k, v in kwargs.items() if k in cls._create_id_param_names } try: created_event: TAggregateCreated = event_class( # type: ignore originator_topic=get_topic(cls), originator_id=id or cls.create_id(**create_id_kwargs), originator_version=1, timestamp=datetime.now(tz=TZINFO), **kwargs, ) except TypeError as e: msg = ( f"Unable to construct 'aggregate created' " f"event with class {event_class.__qualname__} " f"and keyword args {kwargs}: {e}" ) raise TypeError(msg) # Construct the aggregate object. agg: TAggregate = created_event.mutate(None) # Append the domain event to pending list. agg.pending_events.append(created_event) # Return the aggregate. return agg
def test_with_snapshot_store(self) -> None: transcoder = JSONTranscoder() transcoder.register(UUIDAsHex()) transcoder.register(DecimalAsStr()) transcoder.register(DatetimeAsISO()) event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:")) event_recorder.create_table() event_store: EventStore[Aggregate.Event] = EventStore( mapper=Mapper(transcoder=transcoder), recorder=event_recorder, ) snapshot_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:")) snapshot_recorder.create_table() snapshot_store: EventStore[Snapshot] = EventStore( mapper=Mapper(transcoder=transcoder), recorder=snapshot_recorder, ) repository: Repository = Repository(event_store, snapshot_store) # Check key error. with self.assertRaises(AggregateNotFound): repository.get(uuid4()) # Open an account. account = BankAccount.open( full_name="Alice", email_address="*****@*****.**", ) # Credit the account. account.append_transaction(Decimal("10.00")) account.append_transaction(Decimal("25.00")) account.append_transaction(Decimal("30.00")) # Collect pending events. pending = account.collect_events() # Store pending events. event_store.put(pending) copy = repository.get(account.id) assert isinstance(copy, BankAccount) # Check copy has correct attribute values. assert copy.id == account.id assert copy.balance == Decimal("65.00") snapshot = Snapshot( originator_id=account.id, originator_version=account.version, timestamp=datetime.now(tz=TZINFO), topic=get_topic(type(account)), state=account.__dict__, ) snapshot_store.put([snapshot]) copy2 = repository.get(account.id) assert isinstance(copy2, BankAccount) # Check copy has correct attribute values. assert copy2.id == account.id assert copy2.balance == Decimal("65.00") # Credit the account. account.append_transaction(Decimal("10.00")) event_store.put(account.collect_events()) # Check copy has correct attribute values. copy3 = repository.get(account.id) assert isinstance(copy3, BankAccount) assert copy3.id == account.id assert copy3.balance == Decimal("75.00") # Check can get old version of account. copy4 = repository.get(account.id, version=copy.version) assert isinstance(copy4, BankAccount) assert copy4.balance == Decimal("65.00") copy5 = repository.get(account.id, version=1) assert isinstance(copy5, BankAccount) assert copy5.balance == Decimal("0.00") copy6 = repository.get(account.id, version=2) assert isinstance(copy6, BankAccount) assert copy6.balance == Decimal("10.00") copy7 = repository.get(account.id, version=3) assert isinstance(copy7, BankAccount) assert copy7.balance == Decimal("35.00"), copy7.balance copy8 = repository.get(account.id, version=4) assert isinstance(copy8, BankAccount) assert copy8.balance == Decimal("65.00"), copy8.balance
def assertFactoryTopic(self, app, expected_topic): self.assertEqual(get_topic(type(app.factory)), expected_topic)
def test_get_topic(self): self.assertEqual("eventsourcing.domain:Aggregate", get_topic(Aggregate))
class MyEmailProcess(EmailProcess): follow_topics = [get_topic(AggregateEvent)] # follow nothing
def setUp(self) -> None: os.environ[InfrastructureFactory.TOPIC] = get_topic(Factory) os.environ[Factory.SQLITE_DBNAME] = ":memory:" super().setUp()