Ejemplo n.º 1
0
    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,
        )
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
 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()
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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)
Ejemplo n.º 8
0
    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]
Ejemplo n.º 9
0
    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"))
Ejemplo n.º 10
0
    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)
                }))
Ejemplo n.º 12
0
    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])
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
0
    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)
Ejemplo n.º 16
0
 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()
Ejemplo n.º 17
0
    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
Ejemplo n.º 18
0
 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,
     )
Ejemplo n.º 19
0
 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,
     )
Ejemplo n.º 20
0
    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
Ejemplo n.º 21
0
    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)
Ejemplo n.º 23
0
 def test_get_topic(self):
     self.assertEqual("eventsourcing.domain:Aggregate",
                      get_topic(Aggregate))
Ejemplo n.º 24
0
 class MyEmailProcess(EmailProcess):
     follow_topics = [get_topic(AggregateEvent)]  # follow nothing
Ejemplo n.º 25
0
 def setUp(self) -> None:
     os.environ[InfrastructureFactory.TOPIC] = get_topic(Factory)
     os.environ[Factory.SQLITE_DBNAME] = ":memory:"
     super().setUp()