eventsourcing 9.2.22__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 (144) hide show
  1. eventsourcing/__init__.py +1 -1
  2. eventsourcing/application.py +116 -135
  3. eventsourcing/cipher.py +15 -12
  4. eventsourcing/dispatch.py +31 -91
  5. eventsourcing/domain.py +220 -226
  6. eventsourcing/examples/__init__.py +0 -0
  7. eventsourcing/examples/aggregate1/__init__.py +0 -0
  8. eventsourcing/examples/aggregate1/application.py +27 -0
  9. eventsourcing/examples/aggregate1/domainmodel.py +16 -0
  10. eventsourcing/examples/aggregate1/test_application.py +37 -0
  11. eventsourcing/examples/aggregate2/__init__.py +0 -0
  12. eventsourcing/examples/aggregate2/application.py +27 -0
  13. eventsourcing/examples/aggregate2/domainmodel.py +22 -0
  14. eventsourcing/examples/aggregate2/test_application.py +37 -0
  15. eventsourcing/examples/aggregate3/__init__.py +0 -0
  16. eventsourcing/examples/aggregate3/application.py +27 -0
  17. eventsourcing/examples/aggregate3/domainmodel.py +38 -0
  18. eventsourcing/examples/aggregate3/test_application.py +37 -0
  19. eventsourcing/examples/aggregate4/__init__.py +0 -0
  20. eventsourcing/examples/aggregate4/application.py +27 -0
  21. eventsourcing/examples/aggregate4/domainmodel.py +114 -0
  22. eventsourcing/examples/aggregate4/test_application.py +38 -0
  23. eventsourcing/examples/aggregate5/__init__.py +0 -0
  24. eventsourcing/examples/aggregate5/application.py +27 -0
  25. eventsourcing/examples/aggregate5/domainmodel.py +131 -0
  26. eventsourcing/examples/aggregate5/test_application.py +38 -0
  27. eventsourcing/examples/aggregate6/__init__.py +0 -0
  28. eventsourcing/examples/aggregate6/application.py +30 -0
  29. eventsourcing/examples/aggregate6/domainmodel.py +123 -0
  30. eventsourcing/examples/aggregate6/test_application.py +38 -0
  31. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  32. eventsourcing/examples/aggregate6a/application.py +40 -0
  33. eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
  34. eventsourcing/examples/aggregate6a/test_application.py +45 -0
  35. eventsourcing/examples/aggregate7/__init__.py +0 -0
  36. eventsourcing/examples/aggregate7/application.py +48 -0
  37. eventsourcing/examples/aggregate7/domainmodel.py +144 -0
  38. eventsourcing/examples/aggregate7/persistence.py +57 -0
  39. eventsourcing/examples/aggregate7/test_application.py +38 -0
  40. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
  41. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
  42. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  43. eventsourcing/examples/aggregate7a/application.py +56 -0
  44. eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
  45. eventsourcing/examples/aggregate7a/test_application.py +46 -0
  46. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
  47. eventsourcing/examples/aggregate8/__init__.py +0 -0
  48. eventsourcing/examples/aggregate8/application.py +47 -0
  49. eventsourcing/examples/aggregate8/domainmodel.py +65 -0
  50. eventsourcing/examples/aggregate8/persistence.py +57 -0
  51. eventsourcing/examples/aggregate8/test_application.py +37 -0
  52. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
  53. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
  54. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  55. eventsourcing/examples/bankaccounts/application.py +70 -0
  56. eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
  57. eventsourcing/examples/bankaccounts/test.py +173 -0
  58. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  59. eventsourcing/examples/cargoshipping/application.py +126 -0
  60. eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
  61. eventsourcing/examples/cargoshipping/interface.py +143 -0
  62. eventsourcing/examples/cargoshipping/test.py +231 -0
  63. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  64. eventsourcing/examples/contentmanagement/application.py +118 -0
  65. eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
  66. eventsourcing/examples/contentmanagement/test.py +180 -0
  67. eventsourcing/examples/contentmanagement/utils.py +26 -0
  68. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  69. eventsourcing/examples/contentmanagementsystem/application.py +54 -0
  70. eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
  71. eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
  72. eventsourcing/examples/contentmanagementsystem/system.py +14 -0
  73. eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
  74. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  75. eventsourcing/examples/searchablecontent/application.py +45 -0
  76. eventsourcing/examples/searchablecontent/persistence.py +23 -0
  77. eventsourcing/examples/searchablecontent/postgres.py +118 -0
  78. eventsourcing/examples/searchablecontent/sqlite.py +136 -0
  79. eventsourcing/examples/searchablecontent/test_application.py +110 -0
  80. eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
  81. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  82. eventsourcing/examples/searchabletimestamps/application.py +32 -0
  83. eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
  84. eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
  85. eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
  86. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
  87. eventsourcing/examples/test_invoice.py +176 -0
  88. eventsourcing/examples/test_parking_lot.py +206 -0
  89. eventsourcing/interface.py +2 -2
  90. eventsourcing/persistence.py +85 -81
  91. eventsourcing/popo.py +30 -31
  92. eventsourcing/postgres.py +379 -590
  93. eventsourcing/sqlite.py +91 -99
  94. eventsourcing/system.py +52 -57
  95. eventsourcing/tests/application.py +20 -32
  96. eventsourcing/tests/application_tests/__init__.py +0 -0
  97. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
  98. eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
  99. eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
  100. eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
  101. eventsourcing/tests/application_tests/test_cache.py +134 -0
  102. eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
  103. eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
  104. eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
  105. eventsourcing/tests/application_tests/test_processapplication.py +110 -0
  106. eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
  107. eventsourcing/tests/application_tests/test_repository.py +504 -0
  108. eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
  109. eventsourcing/tests/application_tests/test_upcasting.py +459 -0
  110. eventsourcing/tests/docs_tests/__init__.py +0 -0
  111. eventsourcing/tests/docs_tests/test_docs.py +293 -0
  112. eventsourcing/tests/domain.py +1 -1
  113. eventsourcing/tests/domain_tests/__init__.py +0 -0
  114. eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
  115. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
  116. eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
  117. eventsourcing/tests/interface_tests/__init__.py +0 -0
  118. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
  119. eventsourcing/tests/persistence.py +52 -50
  120. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  121. eventsourcing/tests/persistence_tests/test_aes.py +93 -0
  122. eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
  123. eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
  124. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
  125. eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
  126. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
  127. eventsourcing/tests/persistence_tests/test_popo.py +124 -0
  128. eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
  129. eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
  130. eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
  131. eventsourcing/tests/postgres_utils.py +7 -7
  132. eventsourcing/tests/system_tests/__init__.py +0 -0
  133. eventsourcing/tests/system_tests/test_runner.py +935 -0
  134. eventsourcing/tests/system_tests/test_system.py +284 -0
  135. eventsourcing/tests/utils_tests/__init__.py +0 -0
  136. eventsourcing/tests/utils_tests/test_utils.py +226 -0
  137. eventsourcing/utils.py +47 -50
  138. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +29 -79
  139. eventsourcing-9.3.0.dist-info/RECORD +145 -0
  140. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
  141. eventsourcing-9.2.22.dist-info/RECORD +0 -25
  142. eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
  143. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
  144. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,504 @@
1
+ from decimal import Decimal
2
+ from functools import reduce
3
+ from unittest.case import TestCase
4
+ from uuid import uuid4
5
+
6
+ from eventsourcing.application import (
7
+ AggregateNotFoundError,
8
+ Cache,
9
+ LRUCache,
10
+ Repository,
11
+ )
12
+ from eventsourcing.domain import Aggregate, Snapshot
13
+ from eventsourcing.persistence import (
14
+ DatetimeAsISO,
15
+ DecimalAsStr,
16
+ EventStore,
17
+ JSONTranscoder,
18
+ Mapper,
19
+ UUIDAsHex,
20
+ )
21
+ from eventsourcing.popo import POPOAggregateRecorder
22
+ from eventsourcing.sqlite import SQLiteAggregateRecorder, SQLiteDatastore
23
+ from eventsourcing.tests.application import EmailAddressAsStr
24
+ from eventsourcing.tests.domain import BankAccount
25
+ from eventsourcing.utils import get_topic
26
+
27
+
28
+ class TestRepository(TestCase):
29
+ def test_with_snapshot_store(self) -> None:
30
+ transcoder = JSONTranscoder()
31
+ transcoder.register(UUIDAsHex())
32
+ transcoder.register(DecimalAsStr())
33
+ transcoder.register(DatetimeAsISO())
34
+ transcoder.register(EmailAddressAsStr())
35
+
36
+ event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
37
+ event_recorder.create_table()
38
+ event_store = EventStore(
39
+ mapper=Mapper(transcoder=transcoder),
40
+ recorder=event_recorder,
41
+ )
42
+ snapshot_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
43
+ snapshot_recorder.create_table()
44
+ snapshot_store = EventStore(
45
+ mapper=Mapper(transcoder=transcoder),
46
+ recorder=snapshot_recorder,
47
+ )
48
+ repository = Repository(event_store, snapshot_store=snapshot_store)
49
+
50
+ # Check key error.
51
+ with self.assertRaises(AggregateNotFoundError):
52
+ repository.get(uuid4())
53
+
54
+ # Open an account.
55
+ account = BankAccount.open(
56
+ full_name="Alice",
57
+ email_address="alice@example.com",
58
+ )
59
+
60
+ # Credit the account.
61
+ account.append_transaction(Decimal("10.00"))
62
+ account.append_transaction(Decimal("25.00"))
63
+ account.append_transaction(Decimal("30.00"))
64
+
65
+ # Collect pending events.
66
+ pending = account.collect_events()
67
+
68
+ # Store pending events.
69
+ event_store.put(pending)
70
+
71
+ copy = repository.get(account.id)
72
+ assert isinstance(copy, BankAccount)
73
+ # Check copy has correct attribute values.
74
+ assert copy.id == account.id
75
+ assert copy.balance == Decimal("65.00")
76
+
77
+ snapshot = Snapshot(
78
+ originator_id=account.id,
79
+ originator_version=account.version,
80
+ timestamp=Snapshot.create_timestamp(),
81
+ topic=get_topic(type(account)),
82
+ state=account.__dict__,
83
+ )
84
+ snapshot_store.put([snapshot])
85
+
86
+ copy2 = repository.get(account.id)
87
+ assert isinstance(copy2, BankAccount)
88
+
89
+ # Check copy has correct attribute values.
90
+ assert copy2.id == account.id
91
+ assert copy2.balance == Decimal("65.00")
92
+
93
+ # Credit the account.
94
+ account.append_transaction(Decimal("10.00"))
95
+ event_store.put(account.collect_events())
96
+
97
+ # Check copy has correct attribute values.
98
+ copy3 = repository.get(account.id)
99
+ assert isinstance(copy3, BankAccount)
100
+
101
+ assert copy3.id == account.id
102
+ assert copy3.balance == Decimal("75.00")
103
+
104
+ # Check can get old version of account.
105
+ copy4 = repository.get(account.id, version=copy.version)
106
+ assert isinstance(copy4, BankAccount)
107
+ assert copy4.balance == Decimal("65.00")
108
+
109
+ copy5 = repository.get(account.id, version=1)
110
+ assert isinstance(copy5, BankAccount)
111
+ assert copy5.balance == Decimal("0.00")
112
+
113
+ copy6 = repository.get(account.id, version=2)
114
+ assert isinstance(copy6, BankAccount)
115
+ assert copy6.balance == Decimal("10.00")
116
+
117
+ copy7 = repository.get(account.id, version=3)
118
+ assert isinstance(copy7, BankAccount)
119
+ assert copy7.balance == Decimal("35.00"), copy7.balance
120
+
121
+ copy8 = repository.get(account.id, version=4)
122
+ assert isinstance(copy8, BankAccount)
123
+ assert copy8.balance == Decimal("65.00"), copy8.balance
124
+
125
+ # # Check the __getitem__ method is working
126
+ # copy9 = repository[account.uuid]
127
+ # self.assertEqual(copy9.balance, Decimal("75.00"))
128
+ #
129
+ # copy10 = repository[account.uuid, 3]
130
+ # # assert isinstance(copy7, BankAccount)
131
+ #
132
+ # self.assertEqual(copy10.balance, Decimal("35.00"))
133
+
134
+ def test_without_snapshot_store(self) -> None:
135
+ transcoder = JSONTranscoder()
136
+ transcoder.register(UUIDAsHex())
137
+ transcoder.register(DecimalAsStr())
138
+ transcoder.register(DatetimeAsISO())
139
+ transcoder.register(EmailAddressAsStr())
140
+
141
+ event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
142
+ event_recorder.create_table()
143
+ event_store = EventStore(
144
+ mapper=Mapper(transcoder=transcoder),
145
+ recorder=event_recorder,
146
+ )
147
+ repository = Repository(event_store)
148
+
149
+ # Check key error.
150
+ with self.assertRaises(AggregateNotFoundError):
151
+ repository.get(uuid4())
152
+
153
+ # Open an account.
154
+ account = BankAccount.open(
155
+ full_name="Alice",
156
+ email_address="alice@example.com",
157
+ )
158
+
159
+ # Credit the account.
160
+ account.append_transaction(Decimal("10.00"))
161
+ account.append_transaction(Decimal("25.00"))
162
+ account.append_transaction(Decimal("30.00"))
163
+
164
+ # Collect pending events.
165
+ pending = account.collect_events()
166
+
167
+ # Store pending events.
168
+ event_store.put(pending)
169
+
170
+ copy = repository.get(account.id)
171
+ assert isinstance(copy, BankAccount)
172
+ # Check copy has correct attribute values.
173
+ assert copy.id == account.id
174
+ assert copy.balance == Decimal("65.00")
175
+
176
+ # Credit the account.
177
+ account.append_transaction(Decimal("10.00"))
178
+ event_store.put(account.collect_events())
179
+
180
+ # Check copy has correct attribute values.
181
+ copy2 = repository.get(account.id)
182
+ assert isinstance(copy2, BankAccount)
183
+
184
+ assert copy2.id == account.id
185
+ assert copy2.balance == Decimal("75.00")
186
+
187
+ # Check can get old version of account.
188
+ copy3 = repository.get(account.id, version=copy.version)
189
+ assert isinstance(copy3, BankAccount)
190
+ assert copy3.balance == Decimal("65.00")
191
+
192
+ copy4 = repository.get(account.id, version=1)
193
+ assert isinstance(copy4, BankAccount)
194
+ assert copy4.balance == Decimal("0.00")
195
+
196
+ copy5 = repository.get(account.id, version=2)
197
+ assert isinstance(copy5, BankAccount)
198
+ assert copy5.balance == Decimal("10.00")
199
+
200
+ copy6 = repository.get(account.id, version=3)
201
+ assert isinstance(copy6, BankAccount)
202
+ assert copy6.balance == Decimal("35.00"), copy6.balance
203
+
204
+ copy7 = repository.get(account.id, version=4)
205
+ assert isinstance(copy7, BankAccount)
206
+ assert copy7.balance == Decimal("65.00"), copy7.balance
207
+
208
+ def test_with_alternative_mutator_function(self):
209
+ def mutator(initial, domain_events):
210
+ return reduce(lambda a, e: e.mutate(a), domain_events, initial)
211
+
212
+ transcoder = JSONTranscoder()
213
+ transcoder.register(UUIDAsHex())
214
+ transcoder.register(DecimalAsStr())
215
+ transcoder.register(DatetimeAsISO())
216
+ transcoder.register(EmailAddressAsStr())
217
+
218
+ event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
219
+ event_recorder.create_table()
220
+ event_store = EventStore(
221
+ mapper=Mapper(transcoder=transcoder),
222
+ recorder=event_recorder,
223
+ )
224
+ snapshot_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
225
+ snapshot_recorder.create_table()
226
+ snapshot_store = EventStore(
227
+ mapper=Mapper(transcoder=transcoder),
228
+ recorder=snapshot_recorder,
229
+ )
230
+ repository = Repository(event_store, snapshot_store=snapshot_store)
231
+
232
+ # Check key error.
233
+ with self.assertRaises(AggregateNotFoundError):
234
+ repository.get(uuid4())
235
+
236
+ # Open an account.
237
+ account = BankAccount.open(
238
+ full_name="Alice",
239
+ email_address="alice@example.com",
240
+ )
241
+
242
+ # Credit the account.
243
+ account.append_transaction(Decimal("10.00"))
244
+ account.append_transaction(Decimal("25.00"))
245
+ account.append_transaction(Decimal("30.00"))
246
+
247
+ # Collect pending events.
248
+ pending = account.collect_events()
249
+
250
+ # Store pending events.
251
+ event_store.put(pending)
252
+
253
+ copy = repository.get(account.id, projector_func=mutator)
254
+ assert isinstance(copy, BankAccount)
255
+ # Check copy has correct attribute values.
256
+ assert copy.id == account.id
257
+ assert copy.balance == Decimal("65.00")
258
+
259
+ snapshot = Snapshot(
260
+ originator_id=account.id,
261
+ originator_version=account.version,
262
+ timestamp=Snapshot.create_timestamp(),
263
+ topic=get_topic(type(account)),
264
+ state=account.__dict__,
265
+ )
266
+ snapshot_store.put([snapshot])
267
+
268
+ copy2 = repository.get(account.id)
269
+ assert isinstance(copy2, BankAccount)
270
+
271
+ # Check copy has correct attribute values.
272
+ assert copy2.id == account.id
273
+ assert copy2.balance == Decimal("65.00")
274
+
275
+ # Credit the account.
276
+ account.append_transaction(Decimal("10.00"))
277
+ event_store.put(account.collect_events())
278
+
279
+ # Check copy has correct attribute values.
280
+ copy3 = repository.get(account.id)
281
+ assert isinstance(copy3, BankAccount)
282
+
283
+ assert copy3.id == account.id
284
+ assert copy3.balance == Decimal("75.00")
285
+
286
+ # Check can get old version of account.
287
+ copy4 = repository.get(account.id, version=copy.version)
288
+ assert isinstance(copy4, BankAccount)
289
+ assert copy4.balance == Decimal("65.00")
290
+
291
+ copy5 = repository.get(account.id, version=1)
292
+ assert isinstance(copy5, BankAccount)
293
+ assert copy5.balance == Decimal("0.00")
294
+
295
+ copy6 = repository.get(account.id, version=2)
296
+ assert isinstance(copy6, BankAccount)
297
+ assert copy6.balance == Decimal("10.00")
298
+
299
+ copy7 = repository.get(account.id, version=3)
300
+ assert isinstance(copy7, BankAccount)
301
+ assert copy7.balance == Decimal("35.00"), copy7.balance
302
+
303
+ copy8 = repository.get(account.id, version=4)
304
+ assert isinstance(copy8, BankAccount)
305
+ assert copy8.balance == Decimal("65.00"), copy8.balance
306
+
307
+ def test_contains(self):
308
+ transcoder = JSONTranscoder()
309
+ transcoder.register(UUIDAsHex())
310
+ transcoder.register(DecimalAsStr())
311
+ transcoder.register(DatetimeAsISO())
312
+
313
+ event_recorder = POPOAggregateRecorder()
314
+ event_store = EventStore(
315
+ mapper=Mapper(transcoder=transcoder),
316
+ recorder=event_recorder,
317
+ )
318
+
319
+ aggregate = Aggregate()
320
+ event_store.put(aggregate.collect_events())
321
+
322
+ repository = Repository(event_store)
323
+ self.assertTrue(aggregate.id in repository)
324
+ self.assertFalse(uuid4() in repository)
325
+
326
+ def test_cache_maxsize_zero(self):
327
+ transcoder = JSONTranscoder()
328
+ transcoder.register(UUIDAsHex())
329
+ transcoder.register(DecimalAsStr())
330
+ transcoder.register(DatetimeAsISO())
331
+ transcoder.register(EmailAddressAsStr())
332
+
333
+ event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
334
+ event_recorder.create_table()
335
+ event_store = EventStore(
336
+ mapper=Mapper(transcoder=transcoder),
337
+ recorder=event_recorder,
338
+ )
339
+ repository = Repository(event_store, cache_maxsize=0)
340
+ self.assertEqual(type(repository.cache), Cache)
341
+
342
+ aggregate = Aggregate()
343
+
344
+ self.assertFalse(aggregate.id in repository)
345
+ event_store.put(aggregate.collect_events())
346
+ self.assertTrue(aggregate.id in repository)
347
+
348
+ self.assertEqual(1, repository.get(aggregate.id).version)
349
+
350
+ aggregate.trigger_event(Aggregate.Event)
351
+ event_store.put(aggregate.collect_events())
352
+ self.assertEqual(2, repository.get(aggregate.id).version)
353
+
354
+ def test_cache_maxsize_nonzero(self):
355
+ transcoder = JSONTranscoder()
356
+ transcoder.register(UUIDAsHex())
357
+ transcoder.register(DecimalAsStr())
358
+ transcoder.register(DatetimeAsISO())
359
+ transcoder.register(EmailAddressAsStr())
360
+
361
+ event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
362
+ event_recorder.create_table()
363
+ event_store = EventStore(
364
+ mapper=Mapper(transcoder=transcoder),
365
+ recorder=event_recorder,
366
+ )
367
+ repository = Repository(event_store, cache_maxsize=2)
368
+ self.assertEqual(type(repository.cache), LRUCache)
369
+
370
+ aggregate1 = Aggregate()
371
+ self.assertFalse(aggregate1.id in repository)
372
+ event_store.put(aggregate1.collect_events())
373
+ self.assertTrue(aggregate1.id in repository)
374
+
375
+ aggregate2 = Aggregate()
376
+ self.assertFalse(aggregate2.id in repository)
377
+ event_store.put(aggregate2.collect_events())
378
+ self.assertTrue(aggregate2.id in repository)
379
+
380
+ aggregate3 = Aggregate()
381
+ self.assertFalse(aggregate3.id in repository)
382
+ event_store.put(aggregate3.collect_events())
383
+ self.assertTrue(aggregate3.id in repository)
384
+
385
+ self.assertFalse(aggregate1.id in repository.cache.cache)
386
+
387
+ self.assertEqual(1, repository.get(aggregate1.id).version)
388
+ self.assertEqual(1, repository.get(aggregate2.id).version)
389
+ self.assertEqual(1, repository.get(aggregate3.id).version)
390
+
391
+ aggregate1.trigger_event(Aggregate.Event)
392
+ event_store.put(aggregate1.collect_events())
393
+ self.assertEqual(2, repository.get(aggregate1.id).version)
394
+
395
+ def test_cache_fastforward_false(self):
396
+ transcoder = JSONTranscoder()
397
+ transcoder.register(UUIDAsHex())
398
+ transcoder.register(DecimalAsStr())
399
+ transcoder.register(DatetimeAsISO())
400
+ transcoder.register(EmailAddressAsStr())
401
+
402
+ event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
403
+ event_recorder.create_table()
404
+ event_store = EventStore(
405
+ mapper=Mapper(transcoder=transcoder),
406
+ recorder=event_recorder,
407
+ )
408
+ repository = Repository(
409
+ event_store,
410
+ cache_maxsize=2,
411
+ fastforward=False,
412
+ )
413
+
414
+ aggregate = Aggregate()
415
+ event_store.put(aggregate.collect_events())
416
+ self.assertEqual(1, repository.get(aggregate.id).version)
417
+
418
+ aggregate.trigger_event(Aggregate.Event)
419
+ event_store.put(aggregate.collect_events())
420
+ self.assertEqual(1, repository.get(aggregate.id).version)
421
+
422
+ def test_cache_raises_aggregate_not_found_when_projector_func_returns_none(self):
423
+ transcoder = JSONTranscoder()
424
+ transcoder.register(UUIDAsHex())
425
+ transcoder.register(DecimalAsStr())
426
+ transcoder.register(DatetimeAsISO())
427
+ transcoder.register(EmailAddressAsStr())
428
+
429
+ event_recorder = SQLiteAggregateRecorder(SQLiteDatastore(":memory:"))
430
+ event_recorder.create_table()
431
+ event_store = EventStore(
432
+ mapper=Mapper(transcoder=transcoder),
433
+ recorder=event_recorder,
434
+ )
435
+ repository = Repository(
436
+ event_store,
437
+ cache_maxsize=2,
438
+ )
439
+
440
+ aggregate = Aggregate()
441
+ event_store.put(aggregate.collect_events())
442
+ self.assertEqual(1, repository.get(aggregate.id).version)
443
+
444
+ aggregate.trigger_event(Aggregate.Event)
445
+ event_store.put(aggregate.collect_events())
446
+ with self.assertRaises(AggregateNotFoundError):
447
+ repository.get(aggregate.id, projector_func=lambda _, __: None)
448
+
449
+ def test_fastforward_lock(self):
450
+ repository = Repository(
451
+ EventStore(
452
+ mapper=Mapper(transcoder=JSONTranscoder()),
453
+ recorder=POPOAggregateRecorder(),
454
+ ),
455
+ cache_maxsize=2,
456
+ )
457
+ cache_maxsize = repository._fastforward_locks_cache.maxsize
458
+ aggregate_ids = [uuid4() for i in range(cache_maxsize + 1)]
459
+ self.assertEqual(0, len(repository._fastforward_locks_inuse))
460
+ self.assertEqual(0, len(repository._fastforward_locks_cache.cache))
461
+
462
+ # Use a lock and check it's "in use".
463
+ repository._use_fastforward_lock(aggregate_ids[0])
464
+ self.assertEqual(1, len(repository._fastforward_locks_inuse))
465
+ self.assertEqual(0, len(repository._fastforward_locks_cache.cache))
466
+ self.assertEqual(1, repository._fastforward_locks_inuse[aggregate_ids[0]][1])
467
+
468
+ # Disuse a lock and check it's "cached" and not "in use".
469
+ repository._disuse_fastforward_lock(aggregate_ids[0])
470
+ self.assertEqual(0, len(repository._fastforward_locks_inuse))
471
+ self.assertEqual(1, len(repository._fastforward_locks_cache.cache))
472
+
473
+ # Use two locks and check it's "in use" by two users.
474
+ repository._use_fastforward_lock(aggregate_ids[0])
475
+ repository._use_fastforward_lock(aggregate_ids[0])
476
+ self.assertEqual(1, len(repository._fastforward_locks_inuse))
477
+ self.assertEqual(0, len(repository._fastforward_locks_cache.cache))
478
+ self.assertEqual(2, repository._fastforward_locks_inuse[aggregate_ids[0]][1])
479
+
480
+ # Disuse the lock and check it's still "in use" by one user.
481
+ repository._disuse_fastforward_lock(aggregate_ids[0])
482
+ self.assertEqual(1, len(repository._fastforward_locks_inuse))
483
+ self.assertEqual(0, len(repository._fastforward_locks_cache.cache))
484
+ self.assertEqual(1, repository._fastforward_locks_inuse[aggregate_ids[0]][1])
485
+
486
+ # Disuse the lock and check it's cached and not "in use".
487
+ repository._disuse_fastforward_lock(aggregate_ids[0])
488
+ self.assertEqual(0, len(repository._fastforward_locks_inuse))
489
+ self.assertEqual(1, len(repository._fastforward_locks_cache.cache))
490
+
491
+ # Use more locks that the cache holds.
492
+ for aggregate_id in aggregate_ids:
493
+ repository._use_fastforward_lock(aggregate_id)
494
+ self.assertEqual(len(aggregate_ids), len(repository._fastforward_locks_inuse))
495
+ self.assertEqual(0, len(repository._fastforward_locks_cache.cache))
496
+
497
+ # Disuse all the locks and check the cache has evicted one.
498
+ self.assertEqual(len(aggregate_ids), cache_maxsize + 1)
499
+ for aggregate_id in aggregate_ids:
500
+ repository._disuse_fastforward_lock(aggregate_id)
501
+ self.assertEqual(0, len(repository._fastforward_locks_inuse))
502
+ self.assertEqual(
503
+ len(aggregate_ids) - 1, len(repository._fastforward_locks_cache.cache)
504
+ )
@@ -0,0 +1,68 @@
1
+ from decimal import Decimal
2
+ from unittest import TestCase
3
+
4
+ from eventsourcing.domain import Snapshot
5
+ from eventsourcing.persistence import (
6
+ DatetimeAsISO,
7
+ DecimalAsStr,
8
+ EventStore,
9
+ JSONTranscoder,
10
+ Mapper,
11
+ UUIDAsHex,
12
+ )
13
+ from eventsourcing.sqlite import SQLiteAggregateRecorder, SQLiteDatastore
14
+ from eventsourcing.tests.application import EmailAddressAsStr
15
+ from eventsourcing.tests.domain import BankAccount
16
+
17
+
18
+ class TestSnapshotting(TestCase):
19
+ def test(self):
20
+ # Open an account.
21
+ account = BankAccount.open(
22
+ full_name="Alice",
23
+ email_address="alice@example.com",
24
+ )
25
+
26
+ # Credit the account.
27
+ account.append_transaction(Decimal("10.00"))
28
+ account.append_transaction(Decimal("25.00"))
29
+ account.append_transaction(Decimal("30.00"))
30
+
31
+ transcoder = JSONTranscoder()
32
+ transcoder.register(UUIDAsHex())
33
+ transcoder.register(DecimalAsStr())
34
+ transcoder.register(DatetimeAsISO())
35
+ transcoder.register(EmailAddressAsStr())
36
+
37
+ snapshot_store = EventStore(
38
+ mapper=Mapper(transcoder=transcoder),
39
+ recorder=SQLiteAggregateRecorder(
40
+ SQLiteDatastore(":memory:"),
41
+ events_table_name="snapshots",
42
+ ),
43
+ )
44
+ snapshot_store.recorder.create_table()
45
+
46
+ # Clear pending events.
47
+ account.collect_events()
48
+
49
+ # Take a snapshot.
50
+ snapshot = Snapshot.take(account)
51
+
52
+ self.assertNotIn("pending_events", snapshot.state)
53
+
54
+ # Store snapshot.
55
+ snapshot_store.put([snapshot])
56
+
57
+ # Get snapshot.
58
+ snapshots = snapshot_store.get(account.id, desc=True, limit=1)
59
+ snapshot = next(snapshots)
60
+ assert isinstance(snapshot, Snapshot)
61
+
62
+ # Reconstruct the bank account.
63
+ copy = snapshot.mutate(None)
64
+ assert isinstance(copy, BankAccount)
65
+
66
+ # Check copy has correct attribute values.
67
+ assert copy.id == account.id
68
+ assert copy.balance == Decimal("65.00")