eventsourcing 9.4.0b3__tar.gz → 9.4.0b4__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 (27) hide show
  1. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/PKG-INFO +1 -1
  2. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/domain.py +182 -125
  3. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/projection.py +2 -1
  4. eventsourcing-9.4.0b4/eventsourcing/tests/__init__.py +3 -0
  5. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/pyproject.toml +1 -1
  6. eventsourcing-9.4.0b3/eventsourcing/tests/__init__.py +0 -0
  7. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/AUTHORS +0 -0
  8. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/LICENSE +0 -0
  9. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/README.md +0 -0
  10. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/__init__.py +0 -0
  11. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/application.py +0 -0
  12. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/cipher.py +0 -0
  13. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/compressor.py +0 -0
  14. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/cryptography.py +0 -0
  15. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/dispatch.py +0 -0
  16. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/interface.py +0 -0
  17. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/persistence.py +0 -0
  18. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/popo.py +0 -0
  19. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/postgres.py +0 -0
  20. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/py.typed +0 -0
  21. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/sqlite.py +0 -0
  22. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/system.py +0 -0
  23. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/tests/application.py +0 -0
  24. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/tests/domain.py +0 -0
  25. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/tests/persistence.py +0 -0
  26. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/tests/postgres_utils.py +0 -0
  27. {eventsourcing-9.4.0b3 → eventsourcing-9.4.0b4}/eventsourcing/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: eventsourcing
3
- Version: 9.4.0b3
3
+ Version: 9.4.0b4
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
@@ -943,12 +943,20 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
943
943
  }
944
944
 
945
945
  # Create the event class object.
946
- return cast("type[CanMutateAggregate]", type(name, bases, event_cls_dict))
946
+ _new_class = type(name, bases, event_cls_dict)
947
+ return cast("type[CanMutateAggregate]", _new_class)
947
948
 
948
949
  def __call__(
949
950
  cls: MetaAggregate[TAggregate], *args: Any, **kwargs: Any
950
951
  ) -> TAggregate:
952
+ if cls is BaseAggregate:
953
+ msg = "BaseAggregate class cannot be instantiated directly"
954
+ raise TypeError(msg)
951
955
  created_event_classes = _created_event_classes[cls]
956
+ # Here, unlike when calling _create(), we don't have a given event class,
957
+ # so we need to check that there is one "created" event class to use here.
958
+ # We don't check this in __init_subclass__ to allow for alternatives that
959
+ # can be selected by developers by calling _create(event_class=...).
952
960
  if len(created_event_classes) > 1:
953
961
  msg = (
954
962
  f"{cls.__qualname__} can't decide which of many "
@@ -975,10 +983,9 @@ class MetaAggregate(EventsourcingType, Generic[TAggregate], type):
975
983
  event_class: type[CanInitAggregate],
976
984
  **kwargs: Any,
977
985
  ) -> TAggregate:
986
+ # Just define method signature for the __call__() method.
978
987
  raise NotImplementedError # pragma: no cover
979
988
 
980
- _created_event_class: type[CanInitAggregate]
981
-
982
989
 
983
990
  class BaseAggregate(metaclass=MetaAggregate):
984
991
  """Base class for aggregates."""
@@ -1141,13 +1148,22 @@ class BaseAggregate(metaclass=MetaAggregate):
1141
1148
  return f"{type(self).__name__}({', '.join(attrs)})"
1142
1149
 
1143
1150
  def __init_subclass__(
1144
- cls: type[BaseAggregate], *, created_event_name: str | None = None
1151
+ cls: type[BaseAggregate], *, created_event_name: str = ""
1145
1152
  ) -> None:
1146
1153
  """
1147
1154
  Initialises aggregate subclass by defining __init__ method and event classes.
1148
1155
  """
1149
1156
  super().__init_subclass__()
1150
1157
 
1158
+ # Ensure we aren't defining another instance of the same class,
1159
+ # because annotations can get confused when using singledispatchmethod
1160
+ # during class definition e.g. on an aggregate projector function.
1161
+ _module = importlib.import_module(cls.__module__)
1162
+ if cls.__name__ in _module.__dict__:
1163
+ msg = f"Name '{cls.__name__}' already defined in '{cls.__module__}' module"
1164
+ raise ProgrammingError(msg)
1165
+
1166
+ # Get the class annotations.
1151
1167
  class_annotations = cls.__dict__.get("__annotations__", {})
1152
1168
  try:
1153
1169
  class_annotations.pop("id")
@@ -1155,6 +1171,11 @@ class BaseAggregate(metaclass=MetaAggregate):
1155
1171
  except KeyError:
1156
1172
  pass
1157
1173
 
1174
+ if "id" in cls.__dict__:
1175
+ msg = f"Setting attribute 'id' on class '{cls.__name__}' is not allowed"
1176
+ raise ProgrammingError(msg)
1177
+
1178
+ # Process the class as a dataclass, if there are annotations.
1158
1179
  if (
1159
1180
  class_annotations
1160
1181
  or cls in _annotations_mention_id
@@ -1162,6 +1183,29 @@ class BaseAggregate(metaclass=MetaAggregate):
1162
1183
  ):
1163
1184
  dataclasses.dataclass(eq=False, repr=False)(cls)
1164
1185
 
1186
+ # Remember if __init__ mentions ID.
1187
+ for param_name in inspect.signature(cls.__init__).parameters:
1188
+ if param_name == "id":
1189
+ _init_mentions_id.add(cls)
1190
+ break
1191
+
1192
+ # Analyse __init__ attribute, to get __init__ method and @event decorator.
1193
+ init_attr: FunctionType | CommandMethodDecorator | None = cls.__dict__.get(
1194
+ "__init__"
1195
+ )
1196
+ if init_attr is None:
1197
+ # No method, no decorator.
1198
+ init_method: CallableType | None = None
1199
+ init_decorator: CommandMethodDecorator | None = None
1200
+ elif isinstance(init_attr, CommandMethodDecorator):
1201
+ # Method decorated with @event.
1202
+ init_method = init_attr.decorated_func
1203
+ init_decorator = init_attr
1204
+ else:
1205
+ # Undecorated __init__ method.
1206
+ init_decorator = None
1207
+ init_method = init_attr
1208
+
1165
1209
  # Identify or define a base event class for this aggregate.
1166
1210
  base_event_name = "Event"
1167
1211
  base_event_cls: type[CanMutateAggregate]
@@ -1175,142 +1219,150 @@ class BaseAggregate(metaclass=MetaAggregate):
1175
1219
  )
1176
1220
  setattr(cls, base_event_name, base_event_cls)
1177
1221
 
1178
- # Make sure all events defined on aggregate subclass the base event class.
1222
+ # Ensure all events defined on this class are subclasses of base event class.
1223
+ created_event_classes: dict[str, type[CanInitAggregate]] = {}
1179
1224
  for name, value in tuple(cls.__dict__.items()):
1180
1225
  if name == base_event_name:
1181
1226
  # Don't subclass the base event class again.
1182
1227
  continue
1183
1228
  if name.lower() == name:
1184
- # Don't subclass lowercase named attributes that have classes.
1229
+ # Don't subclass lowercase named attributes.
1185
1230
  continue
1186
- if (
1187
- isinstance(value, type)
1188
- and issubclass(value, AggregateEvent)
1189
- and not issubclass(value, base_event_cls)
1190
- ):
1191
- sub_class = cls._define_event_class(name, (value, base_event_cls), None)
1192
- setattr(cls, name, sub_class)
1193
- created_event_classes: dict[str, type[CanInitAggregate]] = {
1194
- name: value
1195
- for name, value in cls.__dict__.items()
1196
- if isinstance(value, type) and issubclass(value, CanInitAggregate)
1197
- }
1198
- # Analyse the __init__ attribute.
1199
- init_attr: FunctionType | CommandMethodDecorator | None = cls.__dict__.get(
1200
- "__init__"
1201
- )
1202
- if init_attr is None:
1203
- init_decorator: CommandMethodDecorator | None = None
1204
- init_method: CallableType | None = None
1205
- elif isinstance(init_attr, CommandMethodDecorator):
1206
- init_decorator = init_attr
1207
- init_method = init_attr.decorated_func
1208
- else:
1209
- init_decorator = None
1210
- init_method = init_attr
1231
+ if isinstance(value, type) and issubclass(value, CanMutateAggregate):
1232
+ if not issubclass(value, base_event_cls):
1233
+ event_class = cls._define_event_class(
1234
+ name, (value, base_event_cls), None
1235
+ )
1236
+ setattr(cls, name, event_class)
1237
+ else:
1238
+ event_class = value
1239
+
1240
+ # Remember all "created" event classes defined on this class.
1241
+ if issubclass(event_class, CanInitAggregate):
1242
+ created_event_classes[name] = event_class
1211
1243
 
1212
1244
  # Identify or define the aggregate's "created" event class.
1213
1245
  created_event_class: type[CanInitAggregate] | None = None
1214
1246
 
1215
- # Does class have an init method decorated with a CommandMethodDecorator?
1247
+ # Analyse __init__ method decorator.
1216
1248
  if init_decorator:
1217
1249
 
1218
- # Disallow using both 'created_event_name' and decorator on __init__.
1219
- if created_event_name:
1220
- msg = "Can't use both 'created_event_name' and decorator on __init__"
1221
- raise TypeError(msg)
1222
-
1223
- # Does the decorator specify a "created" event class?
1250
+ # Does the decorator specify an event class?
1224
1251
  if init_decorator.given_event_cls:
1225
- created_event_class = cast(
1226
- "type[CanInitAggregate]", init_decorator.given_event_cls
1227
- )
1228
-
1229
- # Does the decorator specify a "created" event name?
1230
- elif init_decorator.event_cls_name:
1231
- created_event_name = init_decorator.event_cls_name
1232
-
1233
- # Disallow using decorator on __init__ without event spec.
1234
- else:
1235
- msg = "Decorator on __init__ has neither event name nor class"
1236
- raise TypeError(msg)
1237
-
1238
- # Check if init mentions ID.
1239
- for param_name in inspect.signature(cls.__init__).parameters:
1240
- if param_name == "id":
1241
- _init_mentions_id.add(cls)
1242
- break
1243
1252
 
1244
- if created_event_class:
1245
- # Check specified "created" event class can init aggregate.
1246
- if not issubclass(created_event_class, CanInitAggregate):
1247
- msg = (
1248
- f"{created_event_class} not subclass of {CanInitAggregate.__name__}"
1249
- )
1250
- raise TypeError(msg)
1253
+ # Disallow conflicts between 'created_event_name' and given class.
1254
+ if (
1255
+ created_event_name
1256
+ and created_event_name != init_decorator.given_event_cls.__name__
1257
+ ):
1258
+ msg = (
1259
+ "Given 'created_event_name' conflicts "
1260
+ "with decorator on __init__"
1261
+ )
1262
+ raise TypeError(msg)
1251
1263
 
1252
- for sub_class in created_event_classes.values():
1253
- if issubclass(sub_class, created_event_class):
1254
- # We just subclassed the created event class, so reassign it.
1255
- created_event_class = sub_class
1264
+ # Check given event class can init aggregate.
1265
+ if not issubclass(init_decorator.given_event_cls, CanInitAggregate):
1266
+ msg = (
1267
+ f"class '{init_decorator.given_event_cls.__name__}' "
1268
+ f'not a "created" event class'
1269
+ )
1270
+ raise TypeError(msg)
1256
1271
 
1257
- # Is a "created" event class already defined that matches the name?
1258
- elif created_event_name and created_event_name in created_event_classes:
1259
- created_event_class = created_event_classes[created_event_name]
1272
+ # Have we already subclassed the given event class?
1273
+ for sub_class in created_event_classes.values():
1274
+ if issubclass(sub_class, init_decorator.given_event_cls):
1275
+ created_event_class = sub_class
1276
+ break
1277
+ else:
1278
+ created_event_class = init_decorator.given_event_cls
1260
1279
 
1261
- # If there is only one class defined, then use it.
1262
- elif len(created_event_classes) == 1 and not created_event_name:
1263
- created_event_class = next(iter(created_event_classes.values()))
1280
+ # Does the decorator specify an event name?
1281
+ elif init_decorator.event_cls_name:
1282
+ # Disallow conflicts between 'created_event_name' and given name.
1283
+ if (
1284
+ created_event_name
1285
+ and created_event_name != init_decorator.event_cls_name
1286
+ ):
1287
+ msg = (
1288
+ "Given 'created_event_name' conflicts "
1289
+ "with decorator on __init__"
1290
+ )
1291
+ raise TypeError(msg)
1264
1292
 
1265
- # If there are no "created" event classes already defined, or a name is
1266
- # specified that hasn't matched, then define a "created" event class.
1267
- elif len(created_event_classes) == 0 or created_event_name:
1293
+ created_event_name = init_decorator.event_cls_name
1268
1294
 
1269
- # Decide the base classes for the new "created" event class.
1270
- if created_event_name and len(created_event_classes) == 1:
1271
- base_created_event_cls = next(iter(created_event_classes.values()))
1295
+ # Disallow using decorator on __init__ without event name or class.
1272
1296
  else:
1273
- # TODO: This could probably be improved.
1274
- # Look for first class in MRO that has one specified "created" class.
1275
- for base_cls in cls.__mro__:
1276
- if (
1277
- base_cls in _created_event_classes
1278
- and len(_created_event_classes[base_cls]) == 1
1279
- ):
1280
- base_created_event_cls = _created_event_classes[base_cls][0]
1281
- break
1282
- else: # pragma: no cover
1283
- # TODO: Write a test to cover this.
1284
- msg = (
1285
- "Can't find base aggregate class with "
1286
- "a specified 'created' event class"
1297
+ msg = "@event decorator on __init__ has neither event name nor class"
1298
+ raise TypeError(msg)
1299
+
1300
+ # Do we need to define a created event class?
1301
+ if not created_event_class:
1302
+ # If we have a "created" event class that matches the name, then use it.
1303
+ if created_event_name in created_event_classes:
1304
+ created_event_class = created_event_classes[created_event_name]
1305
+ # Otherwise, if we have no name and only one class defined, then use it.
1306
+ elif not created_event_name and len(created_event_classes) == 1:
1307
+ created_event_class = next(iter(created_event_classes.values()))
1308
+
1309
+ # Otherwise, if there are no "created" events, or a name is
1310
+ # specified that hasn't matched, then define a "created" event class.
1311
+ elif len(created_event_classes) == 0 or created_event_name:
1312
+ # Decide the base "created" event class.
1313
+
1314
+ try:
1315
+ # Look for a base class with the same name.
1316
+ base_created_event_cls = cast(
1317
+ "type[CanInitAggregate]",
1318
+ getattr(cls, created_event_name),
1287
1319
  )
1288
- raise TypeError(msg)
1320
+ except AttributeError:
1321
+ # Look for base class with one nominated "created" event.
1322
+ for base_cls in cls.__mro__:
1323
+ if (
1324
+ base_cls in _created_event_classes
1325
+ and len(_created_event_classes[base_cls]) == 1
1326
+ ):
1327
+ base_created_event_cls = _created_event_classes[base_cls][0]
1328
+ break
1329
+ else:
1330
+ msg = (
1331
+ "Can't identify suitable base class for "
1332
+ f"\"created\" event class on class '{cls.__name__}'"
1333
+ )
1334
+ raise TypeError(msg) from None
1289
1335
 
1290
- if not created_event_name:
1291
- created_event_name = base_created_event_cls.__name__
1336
+ if not created_event_name:
1337
+ created_event_name = base_created_event_cls.__name__
1292
1338
 
1293
- # Disallow init method from having variable params if
1294
- # we are using it to define a "created" event class.
1295
- if init_method:
1296
- _raise_type_error_if_func_has_variable_params(init_method)
1339
+ # Disallow init method from having variable params, because
1340
+ # we are using it to define a "created" event class.
1341
+ if init_method:
1342
+ _raise_type_error_if_func_has_variable_params(init_method)
1297
1343
 
1298
- # Define a "created" event class for this aggregate.
1299
- if issubclass(base_created_event_cls, base_event_cls):
1300
1344
  # Don't subclass from base event class twice.
1301
- bases: tuple[type[CanMutateAggregate], ...] = (base_created_event_cls,)
1302
- else:
1303
- bases = (base_created_event_cls, base_event_cls)
1304
- created_event_class = cast(
1305
- "type[CanInitAggregate]",
1306
- cls._define_event_class(
1307
- created_event_name,
1308
- bases,
1309
- init_method,
1310
- ),
1311
- )
1312
- # Set the event class as an attribute of the aggregate class.
1313
- setattr(cls, created_event_name, created_event_class)
1345
+ assert isinstance(base_created_event_cls, type), base_created_event_cls
1346
+ assert not issubclass(
1347
+ base_created_event_cls, base_event_cls
1348
+ ), base_created_event_cls
1349
+
1350
+ # Define "created" event class.
1351
+ assert created_event_name
1352
+ assert issubclass(base_created_event_cls, CanInitAggregate)
1353
+ created_event_class_bases = (base_created_event_cls, base_event_cls)
1354
+ created_event_class = cast(
1355
+ "type[CanInitAggregate]",
1356
+ cls._define_event_class(
1357
+ created_event_name,
1358
+ created_event_class_bases,
1359
+ init_method,
1360
+ ),
1361
+ )
1362
+ # Set the event class as an attribute of the aggregate class.
1363
+ setattr(cls, created_event_name, created_event_class)
1364
+
1365
+ assert created_event_class or len(created_event_classes) > 1
1314
1366
 
1315
1367
  if created_event_class:
1316
1368
  _created_event_classes[cls] = [created_event_class]
@@ -1318,16 +1370,18 @@ class BaseAggregate(metaclass=MetaAggregate):
1318
1370
  # Prepare to disallow ambiguity of choice between created event classes.
1319
1371
  _created_event_classes[cls] = list(created_event_classes.values())
1320
1372
 
1321
- # Prepare the subsequent event classes.
1373
+ # Find and analyse any @event decorators.
1322
1374
  for attr_name, attr_value in tuple(cls.__dict__.items()):
1323
1375
  event_decorator: CommandMethodDecorator | None = None
1324
1376
 
1325
- if isinstance(attr_value, CommandMethodDecorator):
1326
- event_decorator = attr_value
1327
- if event_decorator.decorated_func.__name__ == "__init__":
1328
- continue
1377
+ # Ignore a decorator on the __init__ method.
1378
+ if isinstance(attr_value, CommandMethodDecorator) and (
1379
+ attr_value.decorated_func.__name__ == "__init__"
1380
+ ):
1381
+ continue
1329
1382
 
1330
- elif isinstance(attr_value, property) and isinstance(
1383
+ # Handle @property.setter decorator on top of @event decorator.
1384
+ if isinstance(attr_value, property) and isinstance(
1331
1385
  attr_value.fset, CommandMethodDecorator
1332
1386
  ):
1333
1387
  event_decorator = attr_value.fset
@@ -1355,6 +1409,9 @@ class BaseAggregate(metaclass=MetaAggregate):
1355
1409
  )
1356
1410
  raise TypeError(msg)
1357
1411
 
1412
+ elif isinstance(attr_value, CommandMethodDecorator):
1413
+ event_decorator = attr_value
1414
+
1358
1415
  if event_decorator is not None:
1359
1416
  if event_decorator.given_event_cls:
1360
1417
  # Check this is not a "created" event class.
@@ -1404,7 +1461,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1404
1461
  "type[DecoratorEvent]", event_cls
1405
1462
  )
1406
1463
 
1407
- # Check any create_id method defined on this class is static or class method.
1464
+ # Check any create_id() method defined on this class is static or class method.
1408
1465
  if "create_id" in cls.__dict__ and not isinstance(
1409
1466
  cls.__dict__["create_id"], (staticmethod, classmethod)
1410
1467
  ):
@@ -1419,7 +1476,7 @@ class BaseAggregate(metaclass=MetaAggregate):
1419
1476
  if param.kind in [param.KEYWORD_ONLY, param.POSITIONAL_OR_KEYWORD]:
1420
1477
  _create_id_param_names[cls].append(name)
1421
1478
 
1422
- # Define event classes for all events on bases.
1479
+ # Define event classes for all events on all bases if not defined on this class.
1423
1480
  for aggregate_base_class in cls.__bases__:
1424
1481
  for name, value in aggregate_base_class.__dict__.items():
1425
1482
  if (
@@ -1428,10 +1485,10 @@ class BaseAggregate(metaclass=MetaAggregate):
1428
1485
  and name not in cls.__dict__
1429
1486
  and name.lower() != name
1430
1487
  ):
1431
- sub_class = cls._define_event_class(
1488
+ event_class = cls._define_event_class(
1432
1489
  name, (base_event_cls, value), None
1433
1490
  )
1434
- setattr(cls, name, sub_class)
1491
+ setattr(cls, name, event_class)
1435
1492
 
1436
1493
 
1437
1494
  class Aggregate(BaseAggregate):
@@ -79,7 +79,8 @@ class ApplicationSubscription(Iterator[tuple[DomainEventProtocol, Tracking]]):
79
79
 
80
80
  def __del__(self) -> None:
81
81
  """Stops the stored event subscription."""
82
- self.stop()
82
+ with contextlib.suppress(AttributeError):
83
+ self.stop()
83
84
 
84
85
 
85
86
  class Projection(ABC, Generic[TTrackingRecorder]):
@@ -0,0 +1,3 @@
1
+ import warnings
2
+
3
+ warnings.resetwarnings() # VS Code unittest runner somehow adds warning filters :-/
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "eventsourcing"
3
- version = "9.4.0b3"
3
+ version = "9.4.0b4"
4
4
 
5
5
  description = "Event sourcing in Python"
6
6
  authors = [
File without changes
File without changes
File without changes