eventsourcing 9.2.21__py3-none-any.whl → 9.3.0__py3-none-any.whl

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 (145) hide show
  1. eventsourcing/__init__.py +1 -1
  2. eventsourcing/application.py +137 -132
  3. eventsourcing/cipher.py +17 -12
  4. eventsourcing/compressor.py +2 -0
  5. eventsourcing/dispatch.py +30 -56
  6. eventsourcing/domain.py +221 -227
  7. eventsourcing/examples/__init__.py +0 -0
  8. eventsourcing/examples/aggregate1/__init__.py +0 -0
  9. eventsourcing/examples/aggregate1/application.py +27 -0
  10. eventsourcing/examples/aggregate1/domainmodel.py +16 -0
  11. eventsourcing/examples/aggregate1/test_application.py +37 -0
  12. eventsourcing/examples/aggregate2/__init__.py +0 -0
  13. eventsourcing/examples/aggregate2/application.py +27 -0
  14. eventsourcing/examples/aggregate2/domainmodel.py +22 -0
  15. eventsourcing/examples/aggregate2/test_application.py +37 -0
  16. eventsourcing/examples/aggregate3/__init__.py +0 -0
  17. eventsourcing/examples/aggregate3/application.py +27 -0
  18. eventsourcing/examples/aggregate3/domainmodel.py +38 -0
  19. eventsourcing/examples/aggregate3/test_application.py +37 -0
  20. eventsourcing/examples/aggregate4/__init__.py +0 -0
  21. eventsourcing/examples/aggregate4/application.py +27 -0
  22. eventsourcing/examples/aggregate4/domainmodel.py +114 -0
  23. eventsourcing/examples/aggregate4/test_application.py +38 -0
  24. eventsourcing/examples/aggregate5/__init__.py +0 -0
  25. eventsourcing/examples/aggregate5/application.py +27 -0
  26. eventsourcing/examples/aggregate5/domainmodel.py +131 -0
  27. eventsourcing/examples/aggregate5/test_application.py +38 -0
  28. eventsourcing/examples/aggregate6/__init__.py +0 -0
  29. eventsourcing/examples/aggregate6/application.py +30 -0
  30. eventsourcing/examples/aggregate6/domainmodel.py +123 -0
  31. eventsourcing/examples/aggregate6/test_application.py +38 -0
  32. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  33. eventsourcing/examples/aggregate6a/application.py +40 -0
  34. eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
  35. eventsourcing/examples/aggregate6a/test_application.py +45 -0
  36. eventsourcing/examples/aggregate7/__init__.py +0 -0
  37. eventsourcing/examples/aggregate7/application.py +48 -0
  38. eventsourcing/examples/aggregate7/domainmodel.py +144 -0
  39. eventsourcing/examples/aggregate7/persistence.py +57 -0
  40. eventsourcing/examples/aggregate7/test_application.py +38 -0
  41. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
  42. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
  43. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  44. eventsourcing/examples/aggregate7a/application.py +56 -0
  45. eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
  46. eventsourcing/examples/aggregate7a/test_application.py +46 -0
  47. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
  48. eventsourcing/examples/aggregate8/__init__.py +0 -0
  49. eventsourcing/examples/aggregate8/application.py +47 -0
  50. eventsourcing/examples/aggregate8/domainmodel.py +65 -0
  51. eventsourcing/examples/aggregate8/persistence.py +57 -0
  52. eventsourcing/examples/aggregate8/test_application.py +37 -0
  53. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
  54. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
  55. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  56. eventsourcing/examples/bankaccounts/application.py +70 -0
  57. eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
  58. eventsourcing/examples/bankaccounts/test.py +173 -0
  59. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  60. eventsourcing/examples/cargoshipping/application.py +126 -0
  61. eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
  62. eventsourcing/examples/cargoshipping/interface.py +143 -0
  63. eventsourcing/examples/cargoshipping/test.py +231 -0
  64. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  65. eventsourcing/examples/contentmanagement/application.py +118 -0
  66. eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
  67. eventsourcing/examples/contentmanagement/test.py +180 -0
  68. eventsourcing/examples/contentmanagement/utils.py +26 -0
  69. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  70. eventsourcing/examples/contentmanagementsystem/application.py +54 -0
  71. eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
  72. eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
  73. eventsourcing/examples/contentmanagementsystem/system.py +14 -0
  74. eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
  75. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  76. eventsourcing/examples/searchablecontent/application.py +45 -0
  77. eventsourcing/examples/searchablecontent/persistence.py +23 -0
  78. eventsourcing/examples/searchablecontent/postgres.py +118 -0
  79. eventsourcing/examples/searchablecontent/sqlite.py +136 -0
  80. eventsourcing/examples/searchablecontent/test_application.py +110 -0
  81. eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
  82. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  83. eventsourcing/examples/searchabletimestamps/application.py +32 -0
  84. eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
  85. eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
  86. eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
  87. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
  88. eventsourcing/examples/test_invoice.py +176 -0
  89. eventsourcing/examples/test_parking_lot.py +206 -0
  90. eventsourcing/interface.py +4 -2
  91. eventsourcing/persistence.py +88 -82
  92. eventsourcing/popo.py +32 -31
  93. eventsourcing/postgres.py +388 -593
  94. eventsourcing/sqlite.py +100 -102
  95. eventsourcing/system.py +66 -71
  96. eventsourcing/tests/application.py +20 -32
  97. eventsourcing/tests/application_tests/__init__.py +0 -0
  98. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
  99. eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
  100. eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
  101. eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
  102. eventsourcing/tests/application_tests/test_cache.py +134 -0
  103. eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
  104. eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
  105. eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
  106. eventsourcing/tests/application_tests/test_processapplication.py +110 -0
  107. eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
  108. eventsourcing/tests/application_tests/test_repository.py +504 -0
  109. eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
  110. eventsourcing/tests/application_tests/test_upcasting.py +459 -0
  111. eventsourcing/tests/docs_tests/__init__.py +0 -0
  112. eventsourcing/tests/docs_tests/test_docs.py +293 -0
  113. eventsourcing/tests/domain.py +1 -1
  114. eventsourcing/tests/domain_tests/__init__.py +0 -0
  115. eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
  116. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
  117. eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
  118. eventsourcing/tests/interface_tests/__init__.py +0 -0
  119. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
  120. eventsourcing/tests/persistence.py +52 -50
  121. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  122. eventsourcing/tests/persistence_tests/test_aes.py +93 -0
  123. eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
  124. eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
  125. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
  126. eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
  127. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
  128. eventsourcing/tests/persistence_tests/test_popo.py +124 -0
  129. eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
  130. eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
  131. eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
  132. eventsourcing/tests/postgres_utils.py +7 -7
  133. eventsourcing/tests/system_tests/__init__.py +0 -0
  134. eventsourcing/tests/system_tests/test_runner.py +935 -0
  135. eventsourcing/tests/system_tests/test_system.py +284 -0
  136. eventsourcing/tests/utils_tests/__init__.py +0 -0
  137. eventsourcing/tests/utils_tests/test_utils.py +226 -0
  138. eventsourcing/utils.py +49 -50
  139. {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +30 -33
  140. eventsourcing-9.3.0.dist-info/RECORD +145 -0
  141. {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
  142. eventsourcing-9.2.21.dist-info/RECORD +0 -25
  143. eventsourcing-9.2.21.dist-info/top_level.txt +0 -1
  144. {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
  145. {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,1180 @@
1
+ import inspect
2
+ from dataclasses import _DataclassParams, dataclass
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from unittest.case import TestCase
6
+ from uuid import NAMESPACE_URL, UUID, uuid4, uuid5
7
+
8
+ from eventsourcing.application import AggregateNotFound, AggregateNotFoundError
9
+ from eventsourcing.domain import (
10
+ Aggregate,
11
+ AggregateCreated,
12
+ AggregateEvent,
13
+ OriginatorIDError,
14
+ OriginatorVersionError,
15
+ TAggregate,
16
+ )
17
+ from eventsourcing.tests.domain import (
18
+ AccountClosedError,
19
+ BankAccount,
20
+ InsufficientFundsError,
21
+ )
22
+ from eventsourcing.utils import get_method_name
23
+
24
+
25
+ class TestMetaAggregate(TestCase):
26
+ def test_aggregate_class_has_a_created_event_class(self):
27
+ self.assertTrue(hasattr(Aggregate, "_created_event_class"))
28
+ self.assertTrue(issubclass(Aggregate._created_event_class, AggregateCreated))
29
+ self.assertEqual(Aggregate._created_event_class, Aggregate.Created)
30
+
31
+ def test_aggregate_subclass_is_a_dataclass_iff_decorated_or_has_annotations(self):
32
+ # No dataclass decorator, no annotations.
33
+ class MyAggregate(Aggregate):
34
+ pass
35
+
36
+ self.assertFalse("__dataclass_params__" in MyAggregate.__dict__)
37
+
38
+ # Has a dataclass decorator (helps IDE know what's going on with annotations).
39
+ @dataclass
40
+ class MyAggregate(Aggregate):
41
+ pass
42
+
43
+ self.assertTrue("__dataclass_params__" in MyAggregate.__dict__)
44
+ self.assertIsInstance(MyAggregate.__dataclass_params__, _DataclassParams)
45
+ self.assertFalse(MyAggregate.__dataclass_params__.frozen)
46
+
47
+ # Has annotations but no decorator.
48
+ @dataclass
49
+ class MyAggregate(Aggregate):
50
+ a: int
51
+
52
+ self.assertTrue("__dataclass_params__" in MyAggregate.__dict__)
53
+ self.assertIsInstance(MyAggregate.__dataclass_params__, _DataclassParams)
54
+ self.assertFalse(MyAggregate.__dataclass_params__.frozen)
55
+
56
+ def test_aggregate_subclass_gets_a_default_created_event_class(self):
57
+ class MyAggregate(Aggregate):
58
+ pass
59
+
60
+ self.assertTrue(hasattr(MyAggregate, "_created_event_class"))
61
+ self.assertTrue(issubclass(MyAggregate._created_event_class, AggregateCreated))
62
+ self.assertEqual(MyAggregate._created_event_class, MyAggregate.Created)
63
+
64
+ def test_aggregate_subclass_has_a_custom_created_event_class(self):
65
+ class MyAggregate(Aggregate):
66
+ class Started(AggregateCreated):
67
+ pass
68
+
69
+ self.assertTrue(hasattr(MyAggregate, "_created_event_class"))
70
+ self.assertTrue(issubclass(MyAggregate._created_event_class, AggregateCreated))
71
+ self.assertEqual(MyAggregate._created_event_class, MyAggregate.Started)
72
+
73
+ def test_aggregate_subclass_has_a_custom_created_event_class_name(self):
74
+ @dataclass
75
+ class MyAggregate(Aggregate, created_event_name="Started"):
76
+ pass
77
+
78
+ a = MyAggregate()
79
+ self.assertEqual(type(a.pending_events[0]).__name__, "Started")
80
+
81
+ self.assertTrue(hasattr(MyAggregate, "_created_event_class"))
82
+ created_event_cls = MyAggregate._created_event_class
83
+ self.assertEqual(created_event_cls.__name__, "Started")
84
+ self.assertTrue(created_event_cls.__qualname__.endswith("MyAggregate.Started"))
85
+ self.assertTrue(issubclass(created_event_cls, AggregateCreated))
86
+ self.assertEqual(created_event_cls, MyAggregate.Started)
87
+
88
+ def test_can_define_initial_version_number(self):
89
+ class MyAggregate1(Aggregate):
90
+ INITIAL_VERSION = 0
91
+
92
+ a = MyAggregate1()
93
+ self.assertEqual(a.version, 0)
94
+
95
+ class MyAggregate2(Aggregate):
96
+ pass
97
+
98
+ a = MyAggregate2()
99
+ self.assertEqual(a.version, 1)
100
+
101
+ class MyAggregate3(Aggregate):
102
+ INITIAL_VERSION = 2
103
+
104
+ a = MyAggregate3()
105
+ self.assertEqual(a.version, 2)
106
+
107
+
108
+ class TestAggregateCreation(TestCase):
109
+ def test_call_class_method_create(self):
110
+ # Check the _create() method creates a new aggregate.
111
+ before_created = Aggregate.Event.create_timestamp()
112
+ uuid = uuid4()
113
+ a = Aggregate._create(
114
+ event_class=AggregateCreated,
115
+ id=uuid,
116
+ )
117
+ after_created = Aggregate.Event.create_timestamp()
118
+ self.assertIsInstance(a, Aggregate)
119
+ self.assertEqual(a.id, uuid)
120
+ self.assertEqual(a.version, 1)
121
+ self.assertEqual(a.created_on, a.modified_on)
122
+ self.assertGreater(a.created_on, before_created)
123
+ self.assertGreater(after_created, a.created_on)
124
+
125
+ def test_raises_when_create_args_mismatch_created_event(self):
126
+ class BrokenAggregate(Aggregate):
127
+ @classmethod
128
+ def create(cls, name):
129
+ return cls._create(event_class=cls.Created, id=uuid4(), name=name)
130
+
131
+ with self.assertRaises(TypeError) as cm:
132
+ BrokenAggregate.create("name")
133
+
134
+ method_name = get_method_name(BrokenAggregate.Created.__init__)
135
+
136
+ self.assertEqual(
137
+ "Unable to construct 'Created' event: "
138
+ f"{method_name}() got an unexpected keyword argument 'name'",
139
+ cm.exception.args[0],
140
+ )
141
+
142
+ def test_call_base_class(self):
143
+ before_created = Aggregate.Event.create_timestamp()
144
+ a = Aggregate()
145
+ after_created = Aggregate.Event.create_timestamp()
146
+ self.assertIsInstance(a, Aggregate)
147
+ self.assertIsInstance(a.id, UUID)
148
+ self.assertIsInstance(a.version, int)
149
+ self.assertEqual(a.version, 1)
150
+ self.assertIsInstance(a.created_on, datetime)
151
+ self.assertIsInstance(a.modified_on, datetime)
152
+ self.assertEqual(a.created_on, a.modified_on)
153
+ self.assertGreater(a.created_on, before_created)
154
+ self.assertGreater(after_created, a.created_on)
155
+
156
+ events = a.collect_events()
157
+ self.assertIsInstance(events[0], AggregateCreated)
158
+ self.assertEqual("Aggregate.Created", type(events[0]).__qualname__)
159
+
160
+ def test_call_subclass_with_no_init(self):
161
+ qualname = type(self).__qualname__
162
+ prefix = f"{qualname}.test_call_subclass_with_no_init.<locals>."
163
+
164
+ class MyAggregate1(Aggregate):
165
+ pass
166
+
167
+ a = MyAggregate1()
168
+ self.assertIsInstance(a.id, UUID)
169
+ self.assertIsInstance(a.version, int)
170
+ self.assertEqual(a.version, 1)
171
+ self.assertIsInstance(a.created_on, datetime)
172
+ self.assertIsInstance(a.modified_on, datetime)
173
+
174
+ events = a.collect_events()
175
+ self.assertEqual(len(events), 1)
176
+ self.assertIsInstance(events[0], AggregateCreated)
177
+ self.assertEqual(f"{prefix}MyAggregate1.Created", type(events[0]).__qualname__)
178
+
179
+ # Do it again using @dataclass
180
+ @dataclass # ...this just makes the code completion work in the IDE.
181
+ class MyAggregate2(Aggregate):
182
+ pass
183
+
184
+ # Check the init method takes no args (except "self").
185
+ init_params = inspect.signature(MyAggregate2.__init__).parameters
186
+ self.assertEqual(len(init_params), 1)
187
+ self.assertEqual(next(iter(init_params)), "self")
188
+
189
+ #
190
+ # Do it again with custom "created" event.
191
+ @dataclass
192
+ class MyAggregate3(Aggregate):
193
+ class Started(AggregateCreated):
194
+ pass
195
+
196
+ a = MyAggregate3()
197
+ self.assertIsInstance(a.id, UUID)
198
+ self.assertIsInstance(a.version, int)
199
+ self.assertIsInstance(a.created_on, datetime)
200
+ self.assertIsInstance(a.modified_on, datetime)
201
+
202
+ events = a.collect_events()
203
+ self.assertEqual(len(events), 1)
204
+ self.assertIsInstance(events[0], AggregateCreated)
205
+ self.assertEqual(f"{prefix}MyAggregate3.Started", type(events[0]).__qualname__)
206
+
207
+ def test_init_no_args(self):
208
+ qualname = type(self).__qualname__
209
+ prefix = f"{qualname}.test_init_no_args.<locals>."
210
+
211
+ class MyAggregate1(Aggregate):
212
+ def __init__(self):
213
+ pass
214
+
215
+ a = MyAggregate1()
216
+ self.assertIsInstance(a.id, UUID)
217
+ self.assertIsInstance(a.version, int)
218
+ self.assertIsInstance(a.created_on, datetime)
219
+ self.assertIsInstance(a.modified_on, datetime)
220
+
221
+ events = a.collect_events()
222
+ self.assertEqual(len(events), 1)
223
+ self.assertIsInstance(events[0], AggregateCreated)
224
+ self.assertEqual(f"{prefix}MyAggregate1.Created", type(events[0]).__qualname__)
225
+
226
+ #
227
+ # Do it again using @dataclass (makes no difference)...
228
+ @dataclass # ...this just makes the code completion work in the IDE.
229
+ class MyAggregate2(Aggregate):
230
+ def __init__(self):
231
+ pass
232
+
233
+ # Check the init method takes no args (except "self").
234
+ init_params = inspect.signature(MyAggregate2.__init__).parameters
235
+ self.assertEqual(len(init_params), 1)
236
+ self.assertEqual(next(iter(init_params)), "self")
237
+
238
+ #
239
+ # Do it again with custom "created" event.
240
+ @dataclass
241
+ class MyAggregate3(Aggregate):
242
+ class Started(AggregateCreated):
243
+ pass
244
+
245
+ a = MyAggregate3()
246
+ self.assertIsInstance(a.id, UUID)
247
+ self.assertIsInstance(a.version, int)
248
+ self.assertIsInstance(a.created_on, datetime)
249
+ self.assertIsInstance(a.modified_on, datetime)
250
+
251
+ events = a.collect_events()
252
+ self.assertEqual(len(events), 1)
253
+ self.assertIsInstance(events[0], AggregateCreated)
254
+ self.assertEqual(f"{prefix}MyAggregate3.Started", type(events[0]).__qualname__)
255
+
256
+ def test_raises_when_init_with_no_args_called_with_args(self):
257
+ # First, with a normal dataclass, to document the errors.
258
+ @dataclass
259
+ class Data(Aggregate):
260
+ pass
261
+
262
+ # Second, with an aggregate class, to replicate same errors.
263
+ @dataclass
264
+ class MyAgg(Aggregate):
265
+ pass
266
+
267
+ def assert_raises(cls):
268
+ method_name = get_method_name(cls.__init__)
269
+
270
+ with self.assertRaises(TypeError) as cm:
271
+ cls(0)
272
+
273
+ self.assertEqual(
274
+ cm.exception.args[0],
275
+ f"{method_name}() takes 1 positional argument but 2 were given",
276
+ )
277
+
278
+ with self.assertRaises(TypeError) as cm:
279
+ cls(value=0)
280
+
281
+ self.assertEqual(
282
+ cm.exception.args[0],
283
+ f"{method_name}() got an unexpected keyword argument 'value'",
284
+ )
285
+
286
+ assert_raises(Data)
287
+ assert_raises(MyAgg)
288
+
289
+ def test_init_defined_with_positional_or_keyword_arg(self):
290
+ class MyAgg(Aggregate):
291
+ def __init__(self, value):
292
+ self.value = value
293
+
294
+ a = MyAgg(1)
295
+ self.assertIsInstance(a, MyAgg)
296
+ self.assertEqual(a.value, 1)
297
+ self.assertIsInstance(a, Aggregate)
298
+ self.assertEqual(len(a.pending_events), 1)
299
+
300
+ a = MyAgg(value=1)
301
+ self.assertIsInstance(a, MyAgg)
302
+ self.assertEqual(a.value, 1)
303
+ self.assertIsInstance(a, Aggregate)
304
+ self.assertEqual(len(a.pending_events), 1)
305
+
306
+ def test_init_defined_with_default_keyword_arg(self):
307
+ class MyAgg(Aggregate):
308
+ def __init__(self, value=0):
309
+ self.value = value
310
+
311
+ a = MyAgg()
312
+ self.assertIsInstance(a, MyAgg)
313
+ self.assertEqual(a.value, 0)
314
+ self.assertIsInstance(a, Aggregate)
315
+ self.assertEqual(len(a.pending_events), 1)
316
+
317
+ def test_init_with_default_keyword_arg_required_positional_and_keyword_only(self):
318
+ class MyAgg(Aggregate):
319
+ def __init__(self, a, b=0, *, c):
320
+ self.a = a
321
+ self.b = b
322
+ self.c = c
323
+
324
+ x = MyAgg(1, c=2)
325
+ self.assertEqual(x.a, 1)
326
+ self.assertEqual(x.b, 0)
327
+ self.assertEqual(x.c, 2)
328
+
329
+ def test_raises_when_init_missing_1_required_positional_arg(self):
330
+ class MyAgg(Aggregate):
331
+ def __init__(self, value):
332
+ self.value = value
333
+
334
+ with self.assertRaises(TypeError) as cm:
335
+ MyAgg()
336
+
337
+ self.assertEqual(
338
+ cm.exception.args[0],
339
+ f"{get_method_name(MyAgg.__init__)}() missing 1 required "
340
+ "positional argument: 'value'",
341
+ )
342
+
343
+ def test_raises_when_init_missing_1_required_keyword_only_arg(self):
344
+ class MyAgg(Aggregate):
345
+ def __init__(self, *, value):
346
+ self.value = value
347
+
348
+ with self.assertRaises(TypeError) as cm:
349
+ MyAgg()
350
+
351
+ self.assertEqual(
352
+ cm.exception.args[0],
353
+ f"{get_method_name(MyAgg.__init__)}() missing 1 required "
354
+ "keyword-only argument: 'value'",
355
+ )
356
+
357
+ def test_raises_when_init_missing_required_positional_and_keyword_only_arg(self):
358
+ class MyAgg(Aggregate):
359
+ def __init__(self, a, *, b):
360
+ pass
361
+
362
+ with self.assertRaises(TypeError) as cm:
363
+ MyAgg()
364
+
365
+ method_name = get_method_name(MyAgg.__init__)
366
+
367
+ self.assertEqual(
368
+ cm.exception.args[0],
369
+ f"{method_name}() missing 1 required positional argument: 'a'",
370
+ )
371
+
372
+ class MyAgg(Aggregate):
373
+ def __init__(self, a, b=0, *, c):
374
+ self.a = a
375
+ self.b = b
376
+ self.c = c
377
+
378
+ with self.assertRaises(TypeError) as cm:
379
+ MyAgg(c=2)
380
+
381
+ self.assertEqual(
382
+ cm.exception.args[0],
383
+ f"{method_name}() missing 1 required positional argument: 'a'",
384
+ )
385
+
386
+ def test_raises_when_init_missing_2_required_positional_args(self):
387
+ class MyAgg(Aggregate):
388
+ def __init__(self, a, b, *, c):
389
+ pass
390
+
391
+ with self.assertRaises(TypeError) as cm:
392
+ MyAgg()
393
+
394
+ method_name = get_method_name(MyAgg.__init__)
395
+
396
+ self.assertEqual(
397
+ cm.exception.args[0],
398
+ f"{method_name}() missing 2 required positional arguments: 'a' and 'b'",
399
+ )
400
+
401
+ def test_raises_when_init_gets_unexpected_keyword_argument(self):
402
+ class MyAgg(Aggregate):
403
+ def __init__(self, a=1):
404
+ pass
405
+
406
+ with self.assertRaises(TypeError) as cm:
407
+ MyAgg(b=1)
408
+
409
+ method_name = get_method_name(MyAgg.__init__)
410
+
411
+ self.assertEqual(
412
+ cm.exception.args[0],
413
+ f"{method_name}() got an unexpected keyword argument 'b'",
414
+ )
415
+
416
+ with self.assertRaises(TypeError) as cm:
417
+ MyAgg(c=1)
418
+
419
+ self.assertEqual(
420
+ cm.exception.args[0],
421
+ f"{method_name}() got an unexpected keyword argument 'c'",
422
+ )
423
+
424
+ with self.assertRaises(TypeError) as cm:
425
+ MyAgg(b=1, c=1)
426
+
427
+ self.assertEqual(
428
+ cm.exception.args[0],
429
+ f"{method_name}() got an unexpected keyword argument 'b'",
430
+ )
431
+
432
+ def test_init_defined_as_dataclass_no_default(self):
433
+ class MyAgg(Aggregate):
434
+ value: int
435
+
436
+ a = MyAgg(1)
437
+ self.assertIsInstance(a, MyAgg)
438
+ self.assertEqual(a.value, 1)
439
+ self.assertIsInstance(a, Aggregate)
440
+ self.assertEqual(len(a.pending_events), 1)
441
+
442
+ a = MyAgg(value=1)
443
+ self.assertIsInstance(a, MyAgg)
444
+ self.assertEqual(a.value, 1)
445
+ self.assertIsInstance(a, Aggregate)
446
+ self.assertEqual(len(a.pending_events), 1)
447
+
448
+ def test_init_defined_as_dataclass_with_default(self):
449
+ class MyAgg(Aggregate):
450
+ value: int = 0
451
+
452
+ a = MyAgg(1)
453
+ self.assertIsInstance(a, MyAgg)
454
+ self.assertEqual(a.value, 1)
455
+ self.assertIsInstance(a, Aggregate)
456
+ self.assertEqual(len(a.pending_events), 1)
457
+
458
+ a = MyAgg(value=1)
459
+ self.assertIsInstance(a, MyAgg)
460
+ self.assertEqual(a.value, 1)
461
+ self.assertIsInstance(a, Aggregate)
462
+ self.assertEqual(len(a.pending_events), 1)
463
+
464
+ a = MyAgg()
465
+ self.assertIsInstance(a, MyAgg)
466
+ self.assertEqual(a.value, 0)
467
+ self.assertIsInstance(a, Aggregate)
468
+ self.assertEqual(len(a.pending_events), 1)
469
+
470
+ with self.assertRaises(TypeError) as cm:
471
+ MyAgg(wrong=1)
472
+
473
+ method_name = get_method_name(MyAgg.__init__)
474
+
475
+ self.assertEqual(
476
+ f"{method_name}() got an unexpected keyword argument 'wrong'",
477
+ cm.exception.args[0],
478
+ )
479
+
480
+ def test_init_defined_as_dataclass_mixture_of_nondefault_and_default_values(self):
481
+ @dataclass
482
+ class MyAgg(Aggregate):
483
+ a: int
484
+ b: int
485
+ c: int = 1
486
+ d: int = 2
487
+
488
+ # This to check aggregate performs the same behaviour.
489
+ @dataclass
490
+ class Data:
491
+ a: int
492
+ b: int
493
+ c: int = 1
494
+ d: int = 2
495
+
496
+ def test_init(cls):
497
+ obj = cls(b=1, a=2)
498
+ self.assertEqual(obj.a, 2)
499
+ self.assertEqual(obj.b, 1)
500
+ self.assertEqual(obj.c, 1)
501
+ self.assertEqual(obj.d, 2)
502
+
503
+ obj = cls(1, 2, 3, 4)
504
+ self.assertEqual(obj.a, 1)
505
+ self.assertEqual(obj.b, 2)
506
+ self.assertEqual(obj.c, 3)
507
+ self.assertEqual(obj.d, 4)
508
+
509
+ with self.assertRaises(TypeError) as cm:
510
+ obj = cls(1, 2, 3, c=4)
511
+ self.assertEqual(obj.a, 1)
512
+ self.assertEqual(obj.b, 2)
513
+ self.assertEqual(obj.c, 4)
514
+ self.assertEqual(obj.d, 3)
515
+
516
+ method_name = get_method_name(cls.__init__)
517
+
518
+ self.assertEqual(
519
+ f"{method_name}() got multiple values for argument 'c'",
520
+ cm.exception.args[0],
521
+ )
522
+
523
+ with self.assertRaises(TypeError) as cm:
524
+ obj = cls(1, a=2, d=3, c=4)
525
+ self.assertEqual(obj.a, 2)
526
+ self.assertEqual(obj.b, 1)
527
+ self.assertEqual(obj.c, 4)
528
+ self.assertEqual(obj.d, 3)
529
+
530
+ self.assertEqual(
531
+ f"{method_name}() got multiple values for argument 'a'",
532
+ cm.exception.args[0],
533
+ )
534
+
535
+ test_init(Data)
536
+ test_init(MyAgg)
537
+
538
+ def test_raises_when_init_has_variable_positional_params(self):
539
+ with self.assertRaises(TypeError) as cm:
540
+
541
+ class _(Aggregate): # noqa: N801
542
+ def __init__(self, *values):
543
+ pass
544
+
545
+ self.assertEqual(
546
+ cm.exception.args[0], "*values not supported by decorator on __init__()"
547
+ )
548
+
549
+ def test_raises_when_init_has_variable_keyword_params(self):
550
+ with self.assertRaises(TypeError) as cm:
551
+
552
+ class _(Aggregate): # noqa: N801
553
+ def __init__(self, **values):
554
+ pass
555
+
556
+ self.assertEqual(
557
+ cm.exception.args[0], "**values not supported by decorator on __init__()"
558
+ )
559
+
560
+ def test_define_custom_create_id_as_uuid5(self):
561
+ class MyAggregate1(Aggregate):
562
+ def __init__(self, name):
563
+ self.name = name
564
+
565
+ @classmethod
566
+ def create_id(cls, name):
567
+ return uuid5(NAMESPACE_URL, f"/names/{name}")
568
+
569
+ a = MyAggregate1("name")
570
+ self.assertEqual(a.name, "name")
571
+ self.assertEqual(a.id, MyAggregate1.create_id("name"))
572
+
573
+ # Do it again with method defined as staticmethod.
574
+ @dataclass
575
+ class MyAggregate2(Aggregate):
576
+ name: str
577
+
578
+ @staticmethod
579
+ def create_id(name):
580
+ return uuid5(NAMESPACE_URL, f"/names/{name}")
581
+
582
+ a = MyAggregate2("name")
583
+ self.assertEqual(a.name, "name")
584
+ self.assertEqual(a.id, MyAggregate2.create_id("name"))
585
+
586
+ def test_raises_type_error_if_create_id_not_staticmethod_or_classmethod(self):
587
+ with self.assertRaises(TypeError):
588
+
589
+ class MyAggregate(Aggregate):
590
+ def create_id(self, myarg):
591
+ pass
592
+
593
+ def test_raises_type_error_if_created_event_class_not_aggregate_created(self):
594
+ with self.assertRaises(TypeError):
595
+
596
+ class MyAggregate(Aggregate):
597
+ _created_event_class = Aggregate.Event
598
+
599
+ def test_refuse_implicit_choice_of_alternative_created_events(self):
600
+ # In case aggregates were created with old Created event,
601
+ # there may need to be several defined. Then, when calling
602
+ # aggregate class, require explicit statement of which to use.
603
+
604
+ # Don't specify created event class.
605
+ class MyAggregate1(Aggregate):
606
+ class Started(AggregateCreated):
607
+ pass
608
+
609
+ class Opened(AggregateCreated):
610
+ pass
611
+
612
+ # This is okay.
613
+ MyAggregate1._create(event_class=MyAggregate1.Started)
614
+ MyAggregate1._create(event_class=MyAggregate1.Opened)
615
+
616
+ with self.assertRaises(TypeError) as cm:
617
+ # This is not okay.
618
+ MyAggregate1()
619
+
620
+ self.assertTrue(
621
+ cm.exception.args[0].startswith(
622
+ "Can't decide which of many "
623
+ '"created" event classes to '
624
+ "use: 'Started', 'Opened'"
625
+ )
626
+ )
627
+
628
+ # Specify created event class using _created_event_class.
629
+ class MyAggregate2(Aggregate):
630
+ class Started(AggregateCreated):
631
+ pass
632
+
633
+ class Opened(AggregateCreated):
634
+ pass
635
+
636
+ _created_event_class = Started
637
+
638
+ # Call class, and expect Started event will be used.
639
+ a = MyAggregate2()
640
+ events = a.collect_events()
641
+ self.assertIsInstance(events[0], MyAggregate2.Started, type(events[0]))
642
+
643
+ # Specify created event class using created_event_name.
644
+ class MyAggregate3(Aggregate, created_event_name="Started"):
645
+ class Started(AggregateCreated):
646
+ pass
647
+
648
+ class Opened(AggregateCreated):
649
+ pass
650
+
651
+ # Call class, and expect Started event will be used.
652
+ a = MyAggregate3()
653
+ events = a.collect_events()
654
+ self.assertIsInstance(events[0], MyAggregate3.Started)
655
+
656
+ def test_refuse_implicit_choice_of_alternative_created_events_on_subclass(self):
657
+ # In case aggregates were created with old Created event,
658
+ # there may need to be several defined. Then, when calling
659
+ # aggregate class, require explicit statement of which to use.
660
+ class MyBaseAggregate(Aggregate, created_event_name="Opened"):
661
+ class Started(AggregateCreated):
662
+ pass
663
+
664
+ class Opened(AggregateCreated):
665
+ pass
666
+
667
+ class MyAggregate1(MyBaseAggregate):
668
+ class Started(AggregateCreated):
669
+ pass
670
+
671
+ class Opened(AggregateCreated):
672
+ pass
673
+
674
+ # This is okay.
675
+ MyAggregate1._create(event_class=MyAggregate1.Started)
676
+ MyAggregate1._create(event_class=MyAggregate1.Opened)
677
+
678
+ with self.assertRaises(TypeError) as cm:
679
+ MyAggregate1() # This is not okay.
680
+
681
+ self.assertTrue(
682
+ cm.exception.args[0].startswith(
683
+ "Can't decide which of many "
684
+ '"created" event classes to '
685
+ "use: 'Started', 'Opened'"
686
+ )
687
+ )
688
+
689
+ # Specify created event class using _created_event_class.
690
+ class MyAggregate2(MyBaseAggregate):
691
+ class Started(AggregateCreated):
692
+ pass
693
+
694
+ class Opened(AggregateCreated):
695
+ pass
696
+
697
+ _created_event_class = Started
698
+
699
+ # Call class, and expect Started event will be used.
700
+ a = MyAggregate2()
701
+ events = a.collect_events()
702
+ self.assertIsInstance(events[0], MyAggregate2.Started)
703
+
704
+ def test_uses_defined_created_event_when_given_name_matches(self):
705
+ class Order(Aggregate, created_event_name="Started"):
706
+ def __init__(self, name):
707
+ self.name = name
708
+ self.confirmed_at = None
709
+ self.pickedup_at = None
710
+
711
+ class Created(AggregateCreated):
712
+ name: str
713
+
714
+ class Started(AggregateCreated):
715
+ name: str
716
+
717
+ order = Order("name")
718
+ pending = order.collect_events()
719
+ self.assertEqual(type(pending[0]).__name__, "Started")
720
+
721
+ def test_defines_created_event_when_given_name_does_not_match(self):
722
+ class Order(Aggregate, created_event_name="Started"):
723
+ def __init__(self, name):
724
+ self.name = name
725
+ self.confirmed_at = None
726
+ self.pickedup_at = None
727
+
728
+ class Created(AggregateCreated):
729
+ name: str
730
+
731
+ order = Order("name")
732
+ pending = order.collect_events()
733
+ self.assertEqual(type(pending[0]).__name__, "Started")
734
+ self.assertTrue(isinstance(pending[0], Order.Created))
735
+
736
+ def test_raises_when_given_created_event_name_conflicts_with_created_event_class(
737
+ self,
738
+ ):
739
+ with self.assertRaises(TypeError) as cm:
740
+
741
+ class Order(Aggregate, created_event_name="Started"):
742
+ def __init__(self, name):
743
+ self.name = name
744
+ self.confirmed_at = None
745
+ self.pickedup_at = None
746
+
747
+ class Created(AggregateCreated):
748
+ name: str
749
+
750
+ class Started(AggregateCreated):
751
+ name: str
752
+
753
+ _created_event_class = Created
754
+
755
+ self.assertEqual(
756
+ cm.exception.args[0],
757
+ "Can't use both '_created_event_class' and 'created_event_name'",
758
+ )
759
+
760
+ def test_define_create_id(self):
761
+ @dataclass
762
+ class Index(Aggregate):
763
+ name: str
764
+
765
+ @staticmethod
766
+ def create_id(name: str):
767
+ return uuid5(NAMESPACE_URL, f"/pages/{name}")
768
+
769
+ index = Index(name="name")
770
+ self.assertEqual(index.name, "name")
771
+ self.assertEqual(index.id, Index.create_id("name"))
772
+
773
+ def test_id_dataclass_style(self):
774
+ @dataclass
775
+ class MyDataclass:
776
+ id: UUID
777
+ name: str
778
+
779
+ @dataclass
780
+ class Index(Aggregate):
781
+ id: UUID
782
+ name: str
783
+
784
+ @staticmethod
785
+ def create_id(name: str):
786
+ return uuid5(NAMESPACE_URL, f"/pages/{name}")
787
+
788
+ def assert_id_dataclass_style(cls):
789
+ with self.assertRaises(TypeError) as cm:
790
+ cls()
791
+ self.assertEqual(
792
+ cm.exception.args[0],
793
+ f"{get_method_name(cls.__init__)}() missing 2 "
794
+ "required positional arguments: 'id' and 'name'",
795
+ )
796
+
797
+ # Just check it works if used properly.
798
+ name = "name"
799
+ index_id = Index.create_id(name)
800
+ obj = cls(name=name, id=index_id)
801
+ self.assertEqual(obj.id, index_id)
802
+ self.assertEqual(obj.id, index_id)
803
+
804
+ assert_id_dataclass_style(MyDataclass)
805
+ assert_id_dataclass_style(Index)
806
+
807
+ def test_init_has_id_explicitly(self):
808
+ class Index(Aggregate):
809
+ def __init__(self, id: UUID, name: str): # noqa: A002
810
+ self._id = id
811
+ self.name = name
812
+
813
+ @staticmethod
814
+ def create_id(name: str):
815
+ return uuid5(NAMESPACE_URL, f"/pages/{name}")
816
+
817
+ name = "name"
818
+ index_id = Index.create_id(name)
819
+ index = Index(name=name, id=index_id)
820
+ self.assertEqual(index.id, index_id)
821
+
822
+
823
+ class TestSubsequentEvents(TestCase):
824
+ def test_trigger_event(self):
825
+ a = Aggregate()
826
+
827
+ # Check the aggregate can trigger further events.
828
+ a.trigger_event(AggregateEvent)
829
+ self.assertLess(a.created_on, a.modified_on)
830
+
831
+ pending = a.collect_events()
832
+ self.assertEqual(len(pending), 2)
833
+ self.assertIsInstance(pending[0], AggregateCreated)
834
+ self.assertEqual(pending[0].originator_version, 1)
835
+ self.assertIsInstance(pending[1], AggregateEvent)
836
+ self.assertEqual(pending[1].originator_version, 2)
837
+
838
+ def test_event_mutate_raises_originator_version_error(self):
839
+ a = Aggregate()
840
+
841
+ # Try to mutate aggregate with an invalid domain event.
842
+ event = AggregateEvent(
843
+ originator_id=a.id,
844
+ originator_version=a.version, # NB not +1.
845
+ timestamp=AggregateEvent.create_timestamp(),
846
+ )
847
+ # Check raises "VersionError".
848
+ with self.assertRaises(OriginatorVersionError):
849
+ event.mutate(a)
850
+
851
+ def test_event_mutate_raises_originator_id_error(self):
852
+ a = Aggregate()
853
+
854
+ # Try to mutate aggregate with an invalid domain event.
855
+ event = AggregateEvent(
856
+ originator_id=uuid4(),
857
+ originator_version=a.version + 1,
858
+ timestamp=AggregateEvent.create_timestamp(),
859
+ )
860
+ # Check raises "VersionError".
861
+ with self.assertRaises(OriginatorIDError):
862
+ event.mutate(a)
863
+
864
+ def test_raises_when_triggering_event_with_mismatched_args(self):
865
+ class MyAgg(Aggregate):
866
+ @classmethod
867
+ def create(cls):
868
+ return cls._create(event_class=cls.Created, id=uuid4())
869
+
870
+ class ValueUpdated(AggregateEvent):
871
+ a: int
872
+
873
+ a = MyAgg.create()
874
+
875
+ with self.assertRaises(TypeError) as cm:
876
+ a.trigger_event(MyAgg.ValueUpdated)
877
+ self.assertTrue(
878
+ cm.exception.args[0].startswith("Can't construct event"),
879
+ cm.exception.args[0],
880
+ )
881
+ self.assertTrue(
882
+ cm.exception.args[0].endswith(
883
+ "__init__() missing 1 required positional argument: 'a'"
884
+ ),
885
+ cm.exception.args[0],
886
+ )
887
+
888
+ # def test_raises_when_apply_method_returns_value(self):
889
+ # class MyAgg(Aggregate):
890
+ # class ValueUpdated(AggregateEvent):
891
+ # a: int
892
+ #
893
+ # def apply(self, aggregate: TAggregate) -> None:
894
+ # return 1
895
+ #
896
+ # a = MyAgg()
897
+ # with self.assertRaises(TypeError) as cm:
898
+ # a.trigger_event(MyAgg.ValueUpdated, a=1)
899
+ # msg = str(cm.exception.args[0])
900
+ #
901
+ # self.assertTrue(msg.startswith("Unexpected value returned from "), msg)
902
+ # self.assertTrue(
903
+ # msg.endswith(
904
+ # "MyAgg.ValueUpdated.apply(). Values returned from 'apply' methods are"
905
+ # " discarded."
906
+ # ),
907
+ # msg,
908
+ # )
909
+
910
+ def test_eq(self):
911
+ class MyAggregate1(Aggregate):
912
+ id: UUID
913
+
914
+ id_a = uuid4()
915
+ id_b = uuid4()
916
+ a = MyAggregate1(id=id_a)
917
+ self.assertEqual(a, a)
918
+
919
+ b = MyAggregate1(id=id_b)
920
+ self.assertNotEqual(a, b)
921
+
922
+ c = MyAggregate1(id=id_a)
923
+ self.assertNotEqual(a, c)
924
+
925
+ a_copy = a.collect_events()[0].mutate(None)
926
+ self.assertEqual(a, a_copy)
927
+
928
+ # Check the aggregate can trigger further events.
929
+ a.trigger_event(AggregateEvent)
930
+ self.assertNotEqual(a, a_copy)
931
+ a.collect_events()
932
+ self.assertNotEqual(a, a_copy)
933
+
934
+ @dataclass(eq=False)
935
+ class MyAggregate2(Aggregate):
936
+ id: UUID
937
+
938
+ id_a = uuid4()
939
+ id_b = uuid4()
940
+ a = MyAggregate2(id=id_a)
941
+ self.assertEqual(a, a)
942
+
943
+ b = MyAggregate2(id=id_b)
944
+ self.assertNotEqual(a, b)
945
+
946
+ c = MyAggregate2(id=id_a)
947
+ self.assertNotEqual(a, c)
948
+
949
+ a_copy = a.collect_events()[0].mutate(None)
950
+ self.assertEqual(a, a_copy)
951
+
952
+ # Check the aggregate can trigger further events.
953
+ a.trigger_event(AggregateEvent)
954
+ self.assertNotEqual(a, a_copy)
955
+ a.collect_events()
956
+ self.assertNotEqual(a, a_copy)
957
+
958
+ def test_repr_baseclass(self):
959
+ a = Aggregate()
960
+
961
+ expect = (
962
+ f"Aggregate(id={a.id!r}, "
963
+ "version=1, "
964
+ f"created_on={a.created_on!r}, "
965
+ f"modified_on={a.modified_on!r}"
966
+ ")"
967
+ )
968
+ self.assertEqual(expect, repr(a))
969
+
970
+ a.trigger_event(AggregateEvent)
971
+
972
+ expect = (
973
+ f"Aggregate(id={a.id!r}, "
974
+ "version=2, "
975
+ f"created_on={a.created_on!r}, "
976
+ f"modified_on={a.modified_on!r}"
977
+ ")"
978
+ )
979
+ self.assertEqual(expect, repr(a))
980
+
981
+ def test_repr_subclass(self):
982
+ class MyAggregate1(Aggregate):
983
+ a: int
984
+
985
+ class ValueAssigned(AggregateEvent):
986
+ b: int
987
+
988
+ def apply(self, aggregate: TAggregate) -> None:
989
+ aggregate.b = self.b
990
+
991
+ a = MyAggregate1(a=1)
992
+ expect = (
993
+ f"MyAggregate1(id={a.id!r}, "
994
+ "version=1, "
995
+ f"created_on={a.created_on!r}, "
996
+ f"modified_on={a.modified_on!r}, "
997
+ "a=1"
998
+ ")"
999
+ )
1000
+ self.assertEqual(expect, repr(a))
1001
+
1002
+ a.trigger_event(MyAggregate1.ValueAssigned, b=2)
1003
+
1004
+ expect = (
1005
+ f"MyAggregate1(id={a.id!r}, "
1006
+ "version=2, "
1007
+ f"created_on={a.created_on!r}, "
1008
+ f"modified_on={a.modified_on!r}, "
1009
+ "a=1, "
1010
+ "b=2"
1011
+ ")"
1012
+ )
1013
+ self.assertEqual(expect, repr(a))
1014
+
1015
+ @dataclass(repr=False)
1016
+ class MyAggregate2(Aggregate):
1017
+ a: int
1018
+
1019
+ class ValueAssigned(AggregateEvent):
1020
+ b: int
1021
+
1022
+ def apply(self, aggregate: TAggregate) -> None:
1023
+ aggregate.b = self.b
1024
+
1025
+ a = MyAggregate2(a=1)
1026
+ expect = (
1027
+ f"MyAggregate2(id={a.id!r}, "
1028
+ "version=1, "
1029
+ f"created_on={a.created_on!r}, "
1030
+ f"modified_on={a.modified_on!r}, "
1031
+ "a=1"
1032
+ ")"
1033
+ )
1034
+ self.assertEqual(expect, repr(a))
1035
+
1036
+ a.trigger_event(MyAggregate2.ValueAssigned, b=2)
1037
+
1038
+ expect = (
1039
+ f"MyAggregate2(id={a.id!r}, "
1040
+ "version=2, "
1041
+ f"created_on={a.created_on!r}, "
1042
+ f"modified_on={a.modified_on!r}, "
1043
+ "a=1, "
1044
+ "b=2"
1045
+ ")"
1046
+ )
1047
+ self.assertEqual(expect, repr(a))
1048
+
1049
+
1050
+ class TestAggregateEventsAreSubclassed(TestCase):
1051
+ def test_base_event_class_is_defined_if_missing(self):
1052
+ class MyAggregate(Aggregate):
1053
+ pass
1054
+
1055
+ self.assertTrue(MyAggregate.Event.__qualname__.endswith("MyAggregate.Event"))
1056
+ self.assertTrue(issubclass(MyAggregate.Event, Aggregate.Event))
1057
+ self.assertNotEqual(MyAggregate.Event, Aggregate.Event)
1058
+
1059
+ def test_base_event_class_is_not_redefined_if_exists(self):
1060
+ class MyAggregate(Aggregate):
1061
+ class Event(Aggregate.Event):
1062
+ pass
1063
+
1064
+ my_event_cls = Event
1065
+
1066
+ self.assertTrue(MyAggregate.Event.__qualname__.endswith("MyAggregate.Event"))
1067
+ self.assertEqual(MyAggregate.my_event_cls, MyAggregate.Event)
1068
+
1069
+ def test_aggregate_events_are_subclassed(self):
1070
+ class MyAggregate(Aggregate):
1071
+ class Created(Aggregate.Created):
1072
+ pass
1073
+
1074
+ class Started(Aggregate.Created):
1075
+ pass
1076
+
1077
+ class Ended(Aggregate.Event):
1078
+ pass
1079
+
1080
+ _created_event_class = Started
1081
+
1082
+ self.assertTrue(MyAggregate.Event.__qualname__.endswith("MyAggregate.Event"))
1083
+ self.assertTrue(issubclass(MyAggregate.Created, MyAggregate.Event))
1084
+ self.assertTrue(issubclass(MyAggregate.Started, MyAggregate.Event))
1085
+ self.assertTrue(issubclass(MyAggregate.Ended, MyAggregate.Event))
1086
+ self.assertEqual(MyAggregate._created_event_class, MyAggregate.Started)
1087
+
1088
+ class MySubclass(MyAggregate):
1089
+ class Opened(MyAggregate.Started):
1090
+ pass
1091
+
1092
+ self.assertTrue(MySubclass.Event.__qualname__.endswith("MySubclass.Event"))
1093
+ self.assertTrue(MySubclass.Created.__qualname__.endswith("MySubclass.Created"))
1094
+ self.assertTrue(
1095
+ MySubclass.Started.__qualname__.endswith("MySubclass.Started"),
1096
+ MySubclass.Started.__qualname__,
1097
+ )
1098
+ self.assertTrue(
1099
+ MySubclass.Ended.__qualname__.endswith("MySubclass.Ended"),
1100
+ MySubclass.Ended.__qualname__,
1101
+ )
1102
+
1103
+ self.assertTrue(
1104
+ MySubclass._created_event_class.__qualname__.endswith("MySubclass.Opened")
1105
+ )
1106
+
1107
+ class MySubSubClass(MySubclass):
1108
+ pass
1109
+
1110
+ self.assertTrue(
1111
+ MySubSubClass._created_event_class.__qualname__.endswith(
1112
+ "MySubSubClass.Opened"
1113
+ )
1114
+ )
1115
+
1116
+
1117
+ class TestBankAccount(TestCase):
1118
+ def test_subclass_bank_account(self):
1119
+ # Open an account.
1120
+ account: BankAccount = BankAccount.open(
1121
+ full_name="Alice",
1122
+ email_address="alice@example.com",
1123
+ )
1124
+
1125
+ # Check the created_on.
1126
+ self.assertEqual(account.created_on, account.modified_on)
1127
+
1128
+ # Check the initial balance.
1129
+ self.assertEqual(account.balance, 0)
1130
+
1131
+ # Credit the account.
1132
+ account.append_transaction(Decimal("10.00"))
1133
+
1134
+ # Check the modified_on time was updated.
1135
+ assert account.created_on < account.modified_on
1136
+
1137
+ # Check the balance.
1138
+ self.assertEqual(account.balance, Decimal("10.00"))
1139
+
1140
+ # Credit the account again.
1141
+ account.append_transaction(Decimal("10.00"))
1142
+
1143
+ # Check the balance.
1144
+ self.assertEqual(account.balance, Decimal("20.00"))
1145
+
1146
+ # Debit the account.
1147
+ account.append_transaction(Decimal("-15.00"))
1148
+
1149
+ # Check the balance.
1150
+ self.assertEqual(account.balance, Decimal("5.00"))
1151
+
1152
+ # Fail to debit account (insufficient funds).
1153
+ with self.assertRaises(InsufficientFundsError):
1154
+ account.append_transaction(Decimal("-15.00"))
1155
+
1156
+ # Increase the overdraft limit.
1157
+ account.set_overdraft_limit(Decimal("100.00"))
1158
+
1159
+ # Debit the account.
1160
+ account.append_transaction(Decimal("-15.00"))
1161
+
1162
+ # Check the balance.
1163
+ self.assertEqual(account.balance, Decimal("-10.00"))
1164
+
1165
+ # Close the account.
1166
+ account.close()
1167
+
1168
+ # Fail to debit account (account closed).
1169
+ with self.assertRaises(AccountClosedError):
1170
+ account.append_transaction(Decimal("-15.00"))
1171
+
1172
+ # Collect pending events.
1173
+ pending = account.collect_events()
1174
+ self.assertEqual(len(pending), 7)
1175
+
1176
+
1177
+ class TestAggregateNotFound(TestCase):
1178
+ def test(self):
1179
+ e = AggregateNotFound()
1180
+ self.assertIsInstance(e, AggregateNotFoundError)