eventsourcing 9.4.5__tar.gz → 9.4.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eventsourcing might be problematic. Click here for more details.

Files changed (26) hide show
  1. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/PKG-INFO +1 -1
  2. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/application.py +1 -1
  3. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/domain.py +58 -33
  4. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/persistence.py +40 -20
  5. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/pyproject.toml +1 -1
  6. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/AUTHORS +0 -0
  7. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/LICENSE +0 -0
  8. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/README.md +0 -0
  9. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/__init__.py +0 -0
  10. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/cipher.py +0 -0
  11. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/compressor.py +0 -0
  12. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/cryptography.py +0 -0
  13. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/dispatch.py +0 -0
  14. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/interface.py +0 -0
  15. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/popo.py +0 -0
  16. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/postgres.py +0 -0
  17. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/projection.py +0 -0
  18. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/py.typed +0 -0
  19. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/sqlite.py +0 -0
  20. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/system.py +0 -0
  21. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/tests/__init__.py +0 -0
  22. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/tests/application.py +0 -0
  23. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/tests/domain.py +0 -0
  24. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/tests/persistence.py +0 -0
  25. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/tests/postgres_utils.py +0 -0
  26. {eventsourcing-9.4.5 → eventsourcing-9.4.6}/eventsourcing/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: eventsourcing
3
- Version: 9.4.5
3
+ Version: 9.4.6
4
4
  Summary: Event sourcing in Python
5
5
  License: BSD-3-Clause
6
6
  Keywords: event sourcing,event store,domain driven design,domain-driven design,ddd,cqrs,cqs
@@ -840,7 +840,7 @@ class Application(Generic[TAggregateID]):
840
840
  "application class."
841
841
  )
842
842
  raise AssertionError(msg)
843
- aggregate: BaseAggregate[UUID | str] = self.repository.get(
843
+ aggregate: BaseAggregate[TAggregateID] = self.repository.get(
844
844
  aggregate_id, version=version, projector_func=projector_func
845
845
  )
846
846
  snapshot_class = getattr(type(aggregate), "Snapshot", type(self).snapshot_class)
@@ -17,6 +17,7 @@ from typing import (
17
17
  Callable,
18
18
  ClassVar,
19
19
  Generic,
20
+ Optional,
20
21
  Protocol,
21
22
  TypeVar,
22
23
  Union,
@@ -237,35 +238,41 @@ class CanCreateTimestamp:
237
238
  TAggregate = TypeVar("TAggregate", bound="BaseAggregate[Any]")
238
239
 
239
240
 
240
- class HasOriginatorIDVersion(Generic[TAggregateID_co]):
241
+ class HasOriginatorIDVersion(Generic[TAggregateID]):
241
242
  """Declares ``originator_id`` and ``originator_version`` attributes."""
242
243
 
243
- originator_id: TAggregateID_co
244
+ originator_id: TAggregateID
244
245
  """UUID identifying an aggregate to which the event belongs."""
245
246
  originator_version: int
246
247
  """Integer identifying the version of the aggregate when the event occurred."""
247
248
 
248
- type_originator_id: ClassVar[type[Union[UUID, str]]] # noqa: UP007
249
+ originator_id_type: ClassVar[Optional[type[Union[UUID, str]]]] = None # noqa: UP007
249
250
 
250
251
  def __init_subclass__(cls) -> None:
251
252
  cls.find_originator_id_type(HasOriginatorIDVersion)
253
+ super().__init_subclass__()
252
254
 
253
255
  @classmethod
254
256
  def find_originator_id_type(cls: type, generic_cls: type) -> None:
255
- """Store the type argument of TAggregateID_co on the subclass."""
256
- for orig_base in cls.__orig_bases__: # type: ignore[attr-defined]
257
- type_originator_id = orig_base.__dict__.get("type_originator_id", "")
258
- if type_originator_id in (UUID, str):
259
- cls.type_originator_id = type_originator_id # type: ignore[attr-defined]
260
- break
261
- if get_origin(orig_base) is generic_cls:
262
- type_originator_id = get_args(orig_base)[0]
263
- if type_originator_id in (UUID, str):
264
- cls.type_originator_id = type_originator_id # type: ignore[attr-defined]
265
- break
257
+ """Store the type argument of TAggregateID on the subclass."""
258
+ if "originator_id_type" not in cls.__dict__:
259
+ for orig_base in cls.__orig_bases__: # type: ignore[attr-defined]
260
+ if "originator_id_type" in orig_base.__dict__:
261
+ cls.originator_id_type = orig_base.__dict__["originator_id_type"] # type: ignore[attr-defined]
262
+ elif get_origin(orig_base) is generic_cls:
263
+ originator_id_type = get_args(orig_base)[0]
264
+ if originator_id_type in (UUID, str):
265
+ cls.originator_id_type = originator_id_type # type: ignore[attr-defined]
266
+ break
267
+ if originator_id_type is Any:
268
+ continue
269
+ if isinstance(originator_id_type, TypeVar):
270
+ continue
271
+ msg = f"Aggregate ID type arg cannot be {originator_id_type}"
272
+ raise TypeError(msg)
266
273
 
267
274
 
268
- class CanMutateAggregate(HasOriginatorIDVersion[TAggregateID_co], CanCreateTimestamp):
275
+ class CanMutateAggregate(HasOriginatorIDVersion[TAggregateID], CanCreateTimestamp):
269
276
  """Implements a :py:func:`~eventsourcing.domain.CanMutateAggregate.mutate`
270
277
  method that evolves the state of an aggregate.
271
278
  """
@@ -276,6 +283,7 @@ class CanMutateAggregate(HasOriginatorIDVersion[TAggregateID_co], CanCreateTimes
276
283
 
277
284
  def __init_subclass__(cls) -> None:
278
285
  cls.find_originator_id_type(CanMutateAggregate)
286
+ super().__init_subclass__()
279
287
 
280
288
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
281
289
  """Validates and adjusts the attributes of the given ``aggregate``
@@ -333,7 +341,7 @@ class CanMutateAggregate(HasOriginatorIDVersion[TAggregateID_co], CanCreateTimes
333
341
  return self.__dict__
334
342
 
335
343
 
336
- class CanInitAggregate(CanMutateAggregate[TAggregateID_co]):
344
+ class CanInitAggregate(CanMutateAggregate[TAggregateID]):
337
345
  """Implements a :func:`~eventsourcing.domain.CanMutateAggregate.mutate`
338
346
  method that constructs the initial state of an aggregate.
339
347
  """
@@ -343,6 +351,7 @@ class CanInitAggregate(CanMutateAggregate[TAggregateID_co]):
343
351
 
344
352
  def __init_subclass__(cls) -> None:
345
353
  cls.find_originator_id_type(CanInitAggregate)
354
+ super().__init_subclass__()
346
355
 
347
356
  def mutate(self, aggregate: TAggregate | None) -> TAggregate | None:
348
357
  """Constructs an aggregate instance according to the attributes of an event.
@@ -1075,7 +1084,7 @@ class BaseAggregate(Generic[TAggregateID], metaclass=MetaAggregate):
1075
1084
  cls: type[Self],
1076
1085
  event_class: type[CanInitAggregate[TAggregateID]],
1077
1086
  *,
1078
- id: UUID | str | None = None, # noqa: A002
1087
+ id: TAggregateID | None = None, # noqa: A002
1079
1088
  **kwargs: Any,
1080
1089
  ) -> Self:
1081
1090
  """Constructs a new aggregate object instance."""
@@ -1253,7 +1262,10 @@ class BaseAggregate(Generic[TAggregateID], metaclass=MetaAggregate):
1253
1262
  cls.__name__ in _module.__dict__
1254
1263
  and ENVVAR_DISABLE_REDEFINITION_CHECK not in os.environ
1255
1264
  ):
1256
- msg = f"Name '{cls.__name__}' already defined in '{cls.__module__}' module"
1265
+ msg = (
1266
+ f"Name '{cls.__name__}' of {cls} already defined in "
1267
+ f"'{cls.__module__}' module: {_module.__dict__[cls.__name__]}"
1268
+ )
1257
1269
  raise ProgrammingError(msg)
1258
1270
 
1259
1271
  # Get the class annotations.
@@ -1343,24 +1355,41 @@ class BaseAggregate(Generic[TAggregateID], metaclass=MetaAggregate):
1343
1355
  if name.lower() == name:
1344
1356
  continue
1345
1357
 
1346
- # Only consider "event" classes (implement "CanMutateAggregate" protocol).
1358
+ # Don't subclass if not "CanMutateAggregate".
1347
1359
  if not isinstance(value, type) or not issubclass(value, CanMutateAggregate):
1348
1360
  continue
1349
1361
 
1362
+ # # Don't subclass generic classes (we don't have a type argument).
1363
+ # # TODO: Maybe also prohibit triggering such things?
1364
+ # if value.__dict__.get("__parameters__", ()):
1365
+ # continue
1366
+
1350
1367
  # Check we have a base event class.
1351
1368
  if base_event_cls is None:
1352
1369
  raise base_event_class_not_defined_error
1353
1370
 
1354
1371
  # Redefine events that aren't already subclass of the base event class.
1355
1372
  if not issubclass(value, base_event_cls):
1373
+ # Identify base classes that were redefined, to preserve hierarchy.
1374
+ redefined_bases = []
1375
+ for base in value.__bases__:
1376
+ if base in redefined_event_classes:
1377
+ redefined_bases.append(redefined_event_classes[base])
1378
+ elif "__pydantic_generic_metadata__" in base.__dict__:
1379
+ pydantic_metadata = base.__dict__[
1380
+ "__pydantic_generic_metadata__"
1381
+ ]
1382
+ for i, key in enumerate(pydantic_metadata):
1383
+ if key == "origin":
1384
+ origin = base.__bases__[i]
1385
+ if origin in redefined_event_classes:
1386
+ redefined_bases.append(
1387
+ redefined_event_classes[origin]
1388
+ )
1389
+
1356
1390
  # Decide base classes of redefined event class: it must be
1357
1391
  # a subclass of the original class, all redefined classes that
1358
1392
  # were in its bases, and the aggregate's base event class.
1359
- redefined_bases = [
1360
- redefined_event_classes[b]
1361
- for b in value.__bases__
1362
- if b in redefined_event_classes
1363
- ]
1364
1393
  event_class_bases = (
1365
1394
  value,
1366
1395
  *redefined_bases,
@@ -1735,17 +1764,13 @@ class SnapshotProtocol(DomainEventProtocol[TAggregateID_co], Protocol):
1735
1764
  """Snapshots have a 'take()' class method."""
1736
1765
 
1737
1766
 
1738
- TCanSnapshotAggregate = TypeVar(
1739
- "TCanSnapshotAggregate", bound="CanSnapshotAggregate[Any]"
1740
- )
1741
-
1742
-
1743
- class CanSnapshotAggregate(HasOriginatorIDVersion[TAggregateID_co], CanCreateTimestamp):
1767
+ class CanSnapshotAggregate(HasOriginatorIDVersion[TAggregateID], CanCreateTimestamp):
1744
1768
  topic: str
1745
1769
  state: Any
1746
1770
 
1747
1771
  def __init_subclass__(cls) -> None:
1748
1772
  cls.find_originator_id_type(CanSnapshotAggregate)
1773
+ super().__init_subclass__()
1749
1774
 
1750
1775
  # def __init__(
1751
1776
  # self,
@@ -1760,7 +1785,7 @@ class CanSnapshotAggregate(HasOriginatorIDVersion[TAggregateID_co], CanCreateTim
1760
1785
  @classmethod
1761
1786
  def take(
1762
1787
  cls,
1763
- aggregate: MutableOrImmutableAggregate[TAggregateID_co],
1788
+ aggregate: MutableOrImmutableAggregate[TAggregateID],
1764
1789
  ) -> Self:
1765
1790
  """Creates a snapshot of the given :class:`Aggregate` object."""
1766
1791
  aggregate_state = dict(aggregate.__dict__)
@@ -1779,9 +1804,9 @@ class CanSnapshotAggregate(HasOriginatorIDVersion[TAggregateID_co], CanCreateTim
1779
1804
  state=aggregate_state, # pyright: ignore[reportCallIssue]
1780
1805
  )
1781
1806
 
1782
- def mutate(self, _: None) -> BaseAggregate[TAggregateID_co]:
1807
+ def mutate(self, _: None) -> BaseAggregate[TAggregateID]:
1783
1808
  """Reconstructs the snapshotted :class:`Aggregate` object."""
1784
- cls = cast(type[BaseAggregate[TAggregateID_co]], resolve_topic(self.topic))
1809
+ cls = cast(type[BaseAggregate[TAggregateID]], resolve_topic(self.topic))
1785
1810
  aggregate_state = dict(self.state)
1786
1811
  from_version = aggregate_state.pop("class_version", 1)
1787
1812
  class_version = getattr(cls, "class_version", 1)
@@ -291,7 +291,7 @@ class Mapper(Generic[TAggregateID]):
291
291
  )
292
292
  raise MapperDeserialisationError(msg) from e
293
293
 
294
- id_convertor = _find_id_convertor(
294
+ id_convertor = find_id_convertor(
295
295
  cls, cast(Hashable, type(stored_event.originator_id))
296
296
  )
297
297
  # print("ID of convertor:", id(convertor))
@@ -309,33 +309,53 @@ class Mapper(Generic[TAggregateID]):
309
309
 
310
310
 
311
311
  @lru_cache
312
- def _find_id_convertor(
312
+ def find_id_convertor(
313
313
  domain_event_cls: type[object], originator_id_cls: type[UUID | str]
314
314
  ) -> Callable[[UUID | str], UUID | str]:
315
315
  # Try to find the originator_id type.
316
- type_originator_id: type[UUID | str] = UUID
317
316
  if issubclass(domain_event_cls, HasOriginatorIDVersion):
318
- type_originator_id = domain_event_cls.type_originator_id
317
+ # For classes that inherit CanMutateAggregate, and don't use a different
318
+ # mapper, then assume they aren't overriding __init_subclass__ is a way
319
+ # that prevents 'originator_id_type' being found from type arguments and
320
+ # set on the class.
321
+ # TODO: Write a test where a custom class does override __init_subclass__
322
+ # so that the next line will cause an AssertionError. Then fix this code.
323
+ if domain_event_cls.originator_id_type is None:
324
+ msg = "originator_id_type cannot be None"
325
+ raise TypeError(msg)
326
+ originator_id_type = domain_event_cls.originator_id_type
319
327
  else:
320
- try:
321
- # Look on plain simple annotations.
322
- originator_id_annotation = typing.get_type_hints(
323
- domain_event_cls, globalns=globals()
324
- ).get("originator_id", None)
325
- assert originator_id_annotation in [UUID, str]
326
- type_originator_id = cast(type[Union[UUID, str]], originator_id_annotation)
327
- except NameError:
328
- pass
329
-
330
- if originator_id_cls is str and type_originator_id is UUID:
328
+ # Otherwise look for annotations.
329
+ for cls in domain_event_cls.__mro__:
330
+ try:
331
+ annotation = cls.__annotations__["originator_id"]
332
+ except (KeyError, AttributeError): # noqa: PERF203
333
+ continue
334
+ else:
335
+ valid_annotations = {
336
+ str: str,
337
+ UUID: UUID,
338
+ "str": str,
339
+ "UUID": UUID,
340
+ "uuid.UUID": UUID,
341
+ }
342
+ if annotation not in valid_annotations:
343
+ msg = f"originator_id annotation on {cls} is not either UUID or str"
344
+ raise TypeError(msg)
345
+ assert annotation in valid_annotations, annotation
346
+ originator_id_type = valid_annotations[annotation]
347
+ break
348
+ else:
349
+ msg = (
350
+ f"Neither event class {domain_event_cls}"
351
+ f"nor its bases have an originator_id annotation"
352
+ )
353
+ raise TypeError(msg)
354
+
355
+ if originator_id_cls is str and originator_id_type is UUID:
331
356
  convertor = str_to_uuid_convertor
332
357
  else:
333
358
  convertor = pass_through_convertor
334
- # print(
335
- # f"Decided {convertor.__name__} "
336
- # f"for {domain_event_cls.__name__} "
337
- # f"and {originator_id_cls.__name__}."
338
- # )
339
359
  return convertor
340
360
 
341
361
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "eventsourcing"
7
- version = "9.4.5"
7
+ version = "9.4.6"
8
8
  description = "Event sourcing in Python"
9
9
  license = "BSD-3-Clause"
10
10
  readme = "README.md"
File without changes
File without changes
File without changes