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