Example #1
0
 def test_register_topic_rename_class(self):
     register_topic("eventsourcing.domain:OldClass", Aggregate)
     self.assertEqual(Aggregate,
                      resolve_topic("eventsourcing.domain:OldClass"))
     self.assertEqual(
         Aggregate.Created,
         resolve_topic("eventsourcing.domain:OldClass.Created"))
Example #2
0
    def construct(
        cls,
        application_name: str = "",
        env: Optional[Mapping] = None,
    ) -> "InfrastructureFactory":
        """
        Constructs concrete infrastructure factory for given
        named application. Reads and resolves infrastructure
        factory class topic from environment variable 'INFRASTRUCTURE_FACTORY'.
        """
        # noinspection SpellCheckingInspection
        env = env if env is not None else os.environ
        topic = env.get(
            cls.TOPIC,
            "eventsourcing.popo:Factory",
        )
        try:
            factory_cls = resolve_topic(topic)
        except (ModuleNotFoundError, AttributeError):
            raise EnvironmentError(
                "Failed to resolve "
                "infrastructure factory topic: "
                f"'{topic}' from environment "
                f"variable '{cls.TOPIC}'"
            )

        if not issubclass(factory_cls, InfrastructureFactory):
            raise AssertionError(f"Not an infrastructure factory: {topic}")
        return factory_cls(application_name=application_name, env=env)
Example #3
0
    def mutate(self, obj: Optional[TAggregate]) -> TAggregate:
        """
        Constructs aggregate instance defined
        by domain event object attributes.
        """
        assert obj is None
        # Copy the event attributes.
        kwargs = self.__dict__.copy()
        # Resolve originator topic.
        aggregate_class: Type[TAggregate] = resolve_topic(
            kwargs.pop("originator_topic")
        )

        # Construct and return aggregate object.
        agg: TAggregate = aggregate_class.__new__(aggregate_class)
        # Separate the base class keywords arguments.
        base_kwargs = {
            "id": kwargs.pop("originator_id"),
            "version": kwargs.pop("originator_version"),
            "timestamp": kwargs.pop("timestamp"),
        }
        # Call the base class init method.
        Aggregate.__base_init__(agg, **base_kwargs)
        # Call the aggregate class init method.
        # noinspection PyTypeChecker
        init_method = agg.__init__  # type: ignore
        # Provide the id, if the init method expects it.
        if aggregate_class._init_mentions_id:
            kwargs["id"] = base_kwargs["id"]
        # noinspection PyArgumentList
        init_method(**kwargs)
        return agg
Example #4
0
 def compressor(self, application_name: str) -> Optional[Compressor]:
     """
     Reads environment variable 'COMPRESSOR_TOPIC' to
     decide whether or not to construct a compressor.
     """
     compressor: Optional[Compressor] = None
     compressor_topic = self.getenv(self.COMPRESSOR_TOPIC,
                                    application_name=application_name)
     if compressor_topic:
         compressor_cls: Type[Compressor] = resolve_topic(compressor_topic)
         compressor = compressor_cls()
     return compressor
Example #5
0
    def test_topic_errors(self):
        # Wrong module name.
        with self.assertRaises(TopicError) as cm:
            resolve_topic("oldmodule:Aggregate")
        expected_msg = ("Failed to resolve topic 'oldmodule:Aggregate': "
                        "No module named 'oldmodule'")
        self.assertEqual(expected_msg, cm.exception.args[0])

        # Wrong class name.
        with self.assertRaises(TopicError) as cm:
            resolve_topic("eventsourcing.domain:OldClass")
        expected_msg = (
            "Failed to resolve topic 'eventsourcing.domain:OldClass': "
            "module 'eventsourcing.domain' has no attribute 'OldClass'")
        self.assertEqual(expected_msg, cm.exception.args[0])

        # Wrong class attribute.
        with self.assertRaises(TopicError) as cm:
            resolve_topic("eventsourcing.domain:Aggregate.OldClass")
        expected_msg = (
            "Failed to resolve topic 'eventsourcing.domain:Aggregate.OldClass': "
            "type object 'Aggregate' has no attribute 'OldClass'")
        self.assertEqual(expected_msg, cm.exception.args[0])

        # Can register same thing twice.
        register_topic("old", eventsourcing)
        register_topic("old", eventsourcing)

        # Can't overwrite with another thing.
        with self.assertRaises(TopicError) as cm:
            register_topic("old", TestCase)
        self.assertIn("is already registered for topic 'old'",
                      cm.exception.args[0])
Example #6
0
 def compressor(self) -> Optional[Compressor]:
     """
     Reads environment variable 'COMPRESSOR_TOPIC' to
     decide whether or not to construct a compressor.
     """
     compressor: Optional[Compressor] = None
     compressor_topic = self.env.get(self.COMPRESSOR_TOPIC)
     if compressor_topic:
         compressor_cls: Union[Type[Compressor],
                               Compressor] = resolve_topic(compressor_topic)
         if isinstance(compressor_cls, type):
             compressor = compressor_cls()
         else:
             compressor = compressor_cls
     return compressor
Example #7
0
    def construct(cls: Type[TF], env: Environment) -> TF:
        """
        Constructs concrete infrastructure factory for given
        named application. Reads and resolves persistence
        topic from environment variable 'PERSISTENCE_MODULE'.
        """
        factory_cls: Type[TF]
        # noinspection SpellCheckingInspection
        topic = (
            env.get(
                "INFRASTRUCTURE_FACTORY",  # Legacy.
                "",
            ) or env.get(
                "FACTORY_TOPIC",  # Legacy.
                "",
            ) or env.get(
                cls.PERSISTENCE_MODULE,
                "eventsourcing.popo:Factory",
            ))
        try:
            obj: Union[Type[TF], ModuleType] = resolve_topic(topic)
        except TopicError as e:
            raise EnvironmentError(
                "Failed to resolve persistence module topic: "
                f"'{topic}' from environment "
                f"variable '{cls.PERSISTENCE_MODULE}'") from e

        if isinstance(obj, ModuleType):
            # Find the factory in the module.
            factory_classes: List[Type[TF]] = []
            for member in obj.__dict__.values():
                if (isinstance(member, type)
                        and issubclass(member, InfrastructureFactory)
                        and member != InfrastructureFactory):
                    factory_classes.append(cast(Type[TF], member))
            if len(factory_classes) == 1:
                factory_cls = factory_classes[0]
            else:
                raise AssertionError(
                    f"Found {len(factory_classes)} infrastructure factory classes in"
                    f" '{topic}', expected 1.")
        elif isinstance(obj, type) and issubclass(obj, InfrastructureFactory):
            factory_cls = obj
        else:
            raise AssertionError(
                f"Not an infrastructure factory class or module: {topic}")
        return factory_cls(env=env)
Example #8
0
    def cipher(self) -> Optional[Cipher]:
        """
        Reads environment variables 'CIPHER_TOPIC'
        and 'CIPHER_KEY' to decide whether or not
        to construct a cipher.
        """
        cipher_topic = self.env.get(self.CIPHER_TOPIC)
        cipher: Optional[Cipher] = None
        default_cipher_topic = "eventsourcing.cipher:AESCipher"
        if self.env.get("CIPHER_KEY") and not cipher_topic:
            cipher_topic = default_cipher_topic

        if cipher_topic:
            cipher_cls: Type[Cipher] = resolve_topic(cipher_topic)
            cipher = cipher_cls(self.env)

        return cipher
Example #9
0
 def cipher(self, application_name: str) -> Optional[Cipher]:
     """
     Reads environment variables 'CIPHER_TOPIC'
     and 'CIPHER_KEY' to decide whether or not
     to construct a cipher.
     """
     cipher_topic = self.getenv(self.CIPHER_TOPIC,
                                application_name=application_name)
     cipher_key = self.getenv(self.CIPHER_KEY,
                              application_name=application_name)
     cipher: Optional[Cipher] = None
     if cipher_topic:
         if cipher_key:
             cipher_cls: Type[Cipher] = resolve_topic(cipher_topic)
             cipher = cipher_cls(cipher_key=cipher_key)
         else:
             raise EnvironmentError("Cipher key was not found in env, "
                                    "although cipher topic was found")
     return cipher
Example #10
0
    def mutate(self, _: Optional[TAggregate]) -> TAggregate:
        """
        Reconstructs the snapshotted :class:`Aggregate` object.
        """
        cls = resolve_topic(self.topic)
        assert issubclass(cls, Aggregate)
        aggregate_state = dict(self.state)
        from_version = aggregate_state.pop("class_version", 1)
        class_version = getattr(cls, "class_version", 1)
        while from_version < class_version:
            upcast_name = f"upcast_v{from_version}_v{from_version + 1}"
            upcast = getattr(cls, upcast_name)
            upcast(aggregate_state)
            from_version += 1

        aggregate_state["_id"] = self.originator_id
        aggregate_state["_version"] = self.originator_version
        aggregate_state["_pending_events"] = []
        aggregate: TAggregate = object.__new__(cls)

        aggregate.__dict__.update(aggregate_state)
        return aggregate
Example #11
0
    def mutate(self, aggregate: Optional[TAggregate]) -> Optional[TAggregate]:
        """
        Constructs aggregate instance defined
        by domain event object attributes.
        """
        assert aggregate is None

        # Resolve originator topic.
        aggregate_class: Type[TAggregate] = resolve_topic(
            self.__dict__["originator_topic"]
        )

        # Construct and return aggregate object.
        agg = aggregate_class.__new__(aggregate_class)

        # Separate the base class keywords arguments.
        base_kwargs = _filter_kwargs_for_method_params(
            self.__dict__, type(agg).__base_init__
        )

        # Call the base class init method.
        agg.__base_init__(**base_kwargs)

        # Select values that aren't mentioned in the method signature.
        init_kwargs = _filter_kwargs_for_method_params(
            self.__dict__, type(agg).__init__
        )

        # Provide the id, if the init method expects it.
        if aggregate_class in _init_mentions_id:
            init_kwargs["id"] = self.__dict__["originator_id"]

        # Call the aggregate class init method.
        agg.__init__(**init_kwargs)  # type: ignore

        self.apply(agg)

        return agg
Example #12
0
    def to_domain_event(self, stored: StoredEvent) -> TDomainEvent:
        """
        Converts the given :class:`StoredEvent` to a domain event object.
        """
        stored_state: bytes = stored.state
        if self.cipher:
            stored_state = self.cipher.decrypt(stored_state)
        if self.compressor:
            stored_state = self.compressor.decompress(stored_state)
        event_state: dict = self.transcoder.decode(stored_state)
        event_state["originator_id"] = stored.originator_id
        event_state["originator_version"] = stored.originator_version
        cls = resolve_topic(stored.topic)
        assert issubclass(cls, DomainEvent)
        class_version = getattr(cls, "class_version", 1)
        from_version = event_state.pop("class_version", 1)
        while from_version < class_version:
            getattr(cls, f"upcast_v{from_version}_v{from_version + 1}")(event_state)
            from_version += 1

        domain_event = object.__new__(cls)
        domain_event.__dict__.update(event_state)
        return domain_event
Example #13
0
    def cipher(self, application_name: str) -> Optional[Cipher]:
        """
        Reads environment variables 'CIPHER_TOPIC'
        and 'CIPHER_KEY' to decide whether or not
        to construct a cipher.
        """
        cipher_topic = self.getenv(self.CIPHER_TOPIC, application_name=application_name)
        cipher_key = self.getenv(self.CIPHER_KEY, application_name=application_name)
        cipher: Optional[Cipher] = None
        if cipher_topic:
            if not cipher_key:
                raise EnvironmentError(
                    f"'{self.CIPHER_KEY}' not set in env, "
                    f"although '{self.CIPHER_TOPIC}' was set"
                )
        elif cipher_key:
            cipher_topic = "eventsourcing.cipher:AESCipher"

        if cipher_topic and cipher_key:
            cipher_cls: Type[Cipher] = resolve_topic(cipher_topic)
            cipher = cipher_cls(cipher_key=cipher_key)

        return cipher
Example #14
0
    List,
    Optional,
    Set,
    Tuple,
    Type,
    TypeVar,
    Union,
    cast,
    overload,
)
from uuid import UUID, uuid4

from eventsourcing.utils import get_method_name, get_topic, resolve_topic

# noinspection SpellCheckingInspection
TZINFO: tzinfo = resolve_topic(os.getenv("TZINFO_TOPIC", "datetime:timezone.utc"))


class MetaDomainEvent(ABCMeta):
    def __new__(
        mcs, name: str, bases: Tuple[type, ...], cls_dict: Dict[str, Any]
    ) -> "MetaDomainEvent":
        event_cls = super().__new__(mcs, name, bases, cls_dict)
        event_cls = dataclass(frozen=True)(event_cls)  # type: ignore
        return event_cls


T = TypeVar("T")


class DomainEvent(ABC, Generic[T], metaclass=MetaDomainEvent):
Example #15
0
 def test_register_topic_move_module_into_package(self):
     register_topic("oldmodule", eventsourcing.domain)
     self.assertEqual(Aggregate, resolve_topic("oldmodule:Aggregate"))
     self.assertEqual(Aggregate.Created,
                      resolve_topic("oldmodule:Aggregate.Created"))
Example #16
0
 def test_register_topic_rename_package(self):
     register_topic("oldpackage", eventsourcing)
     self.assertEqual(Aggregate,
                      resolve_topic("oldpackage.domain:Aggregate"))
     self.assertEqual(Aggregate.Created,
                      resolve_topic("oldpackage.domain:Aggregate.Created"))
Example #17
0
 def test_register_topic_move_package(self):
     register_topic("old.eventsourcing.domain", eventsourcing.domain)
     self.assertEqual(Aggregate,
                      resolve_topic("old.eventsourcing.domain:Aggregate"))
Example #18
0
 def test_register_topic_rename_package_and_module(self):
     register_topic("old.old", eventsourcing.domain)
     self.assertEqual(Aggregate, resolve_topic("old.old:Aggregate"))
Example #19
0
 def get_app_cls(self, name: str) -> Type[Application]:
     cls = resolve_topic(self.nodes[name])
     assert issubclass(cls, Application)
     return cls
Example #20
0
 def test_resolve_topic(self):
     self.assertEqual(Aggregate,
                      resolve_topic("eventsourcing.domain:Aggregate"))