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"))
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)
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
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
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])
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
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)
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
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
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
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
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
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
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):
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"))
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"))
def test_register_topic_move_package(self): register_topic("old.eventsourcing.domain", eventsourcing.domain) self.assertEqual(Aggregate, resolve_topic("old.eventsourcing.domain:Aggregate"))
def test_register_topic_rename_package_and_module(self): register_topic("old.old", eventsourcing.domain) self.assertEqual(Aggregate, resolve_topic("old.old:Aggregate"))
def get_app_cls(self, name: str) -> Type[Application]: cls = resolve_topic(self.nodes[name]) assert issubclass(cls, Application) return cls
def test_resolve_topic(self): self.assertEqual(Aggregate, resolve_topic("eventsourcing.domain:Aggregate"))