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.
- eventsourcing/__init__.py +1 -1
- eventsourcing/application.py +137 -132
- eventsourcing/cipher.py +17 -12
- eventsourcing/compressor.py +2 -0
- eventsourcing/dispatch.py +30 -56
- eventsourcing/domain.py +221 -227
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +114 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +180 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +110 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +68 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +94 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +4 -2
- eventsourcing/persistence.py +88 -82
- eventsourcing/popo.py +32 -31
- eventsourcing/postgres.py +388 -593
- eventsourcing/sqlite.py +100 -102
- eventsourcing/system.py +66 -71
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1180 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +52 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1119 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +284 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +49 -50
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/METADATA +30 -33
- eventsourcing-9.3.0.dist-info/RECORD +145 -0
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.21.dist-info/RECORD +0 -25
- eventsourcing-9.2.21.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.2.21.dist-info → eventsourcing-9.3.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, Sequence
|
|
4
|
+
from unittest.case import TestCase
|
|
5
|
+
from uuid import NAMESPACE_URL, uuid4, uuid5
|
|
6
|
+
|
|
7
|
+
from eventsourcing.application import Application, RecordingEvent
|
|
8
|
+
from eventsourcing.domain import Aggregate
|
|
9
|
+
from eventsourcing.persistence import IntegrityError, Notification, Tracking
|
|
10
|
+
from eventsourcing.system import (
|
|
11
|
+
Follower,
|
|
12
|
+
Leader,
|
|
13
|
+
ProcessApplication,
|
|
14
|
+
RecordingEventReceiver,
|
|
15
|
+
System,
|
|
16
|
+
)
|
|
17
|
+
from eventsourcing.tests.application import BankAccounts
|
|
18
|
+
from eventsourcing.tests.application_tests.test_processapplication import EmailProcess
|
|
19
|
+
from eventsourcing.tests.domain import BankAccount
|
|
20
|
+
from eventsourcing.utils import get_topic, resolve_topic
|
|
21
|
+
|
|
22
|
+
system_defined_as_global = System(
|
|
23
|
+
pipes=[
|
|
24
|
+
[
|
|
25
|
+
BankAccounts,
|
|
26
|
+
EmailProcess,
|
|
27
|
+
],
|
|
28
|
+
[Application],
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestSystem(TestCase):
|
|
34
|
+
def test_graph_nodes_and_edges(self):
|
|
35
|
+
system = System(
|
|
36
|
+
pipes=[
|
|
37
|
+
[
|
|
38
|
+
BankAccounts,
|
|
39
|
+
EmailProcess,
|
|
40
|
+
],
|
|
41
|
+
[Application],
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
self.assertEqual(len(system.nodes), 3)
|
|
45
|
+
self.assertEqual(system.nodes["BankAccounts"], get_topic(BankAccounts))
|
|
46
|
+
self.assertEqual(system.nodes["EmailProcess"], get_topic(EmailProcess))
|
|
47
|
+
self.assertEqual(system.nodes["Application"], get_topic(Application))
|
|
48
|
+
|
|
49
|
+
self.assertEqual(system.leaders, ["BankAccounts"])
|
|
50
|
+
self.assertEqual(system.followers, ["EmailProcess"])
|
|
51
|
+
self.assertEqual(system.singles, ["Application"])
|
|
52
|
+
|
|
53
|
+
self.assertEqual(len(system.edges), 1)
|
|
54
|
+
self.assertIn(
|
|
55
|
+
(
|
|
56
|
+
"BankAccounts",
|
|
57
|
+
"EmailProcess",
|
|
58
|
+
),
|
|
59
|
+
system.edges,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.assertEqual(len(system.singles), 1)
|
|
63
|
+
|
|
64
|
+
def test_duplicate_edges_are_eliminated(self):
|
|
65
|
+
system = System(
|
|
66
|
+
pipes=[
|
|
67
|
+
[
|
|
68
|
+
BankAccounts,
|
|
69
|
+
EmailProcess,
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
BankAccounts,
|
|
73
|
+
EmailProcess,
|
|
74
|
+
],
|
|
75
|
+
[Application],
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
self.assertEqual(len(system.nodes), 3)
|
|
79
|
+
self.assertEqual(system.nodes["BankAccounts"], get_topic(BankAccounts))
|
|
80
|
+
self.assertEqual(system.nodes["EmailProcess"], get_topic(EmailProcess))
|
|
81
|
+
self.assertEqual(system.nodes["Application"], get_topic(Application))
|
|
82
|
+
|
|
83
|
+
self.assertEqual(system.leaders, ["BankAccounts"])
|
|
84
|
+
self.assertEqual(system.followers, ["EmailProcess"])
|
|
85
|
+
self.assertEqual(system.singles, ["Application"])
|
|
86
|
+
|
|
87
|
+
self.assertEqual(len(system.edges), 1)
|
|
88
|
+
self.assertIn(
|
|
89
|
+
(
|
|
90
|
+
"BankAccounts",
|
|
91
|
+
"EmailProcess",
|
|
92
|
+
),
|
|
93
|
+
system.edges,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self.assertEqual(len(system.singles), 1)
|
|
97
|
+
|
|
98
|
+
def test_raises_type_error_not_a_follower(self):
|
|
99
|
+
with self.assertRaises(TypeError) as cm:
|
|
100
|
+
System(
|
|
101
|
+
pipes=[
|
|
102
|
+
[
|
|
103
|
+
BankAccounts,
|
|
104
|
+
Leader,
|
|
105
|
+
],
|
|
106
|
+
]
|
|
107
|
+
)
|
|
108
|
+
exception = cm.exception
|
|
109
|
+
self.assertEqual(
|
|
110
|
+
exception.args[0],
|
|
111
|
+
"Not a follower class: <class 'eventsourcing.system.Leader'>",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def test_raises_type_error_not_a_processor(self):
|
|
115
|
+
with self.assertRaises(TypeError) as cm:
|
|
116
|
+
System(
|
|
117
|
+
pipes=[
|
|
118
|
+
[
|
|
119
|
+
BankAccounts,
|
|
120
|
+
Follower,
|
|
121
|
+
EmailProcess,
|
|
122
|
+
],
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
exception = cm.exception
|
|
126
|
+
self.assertEqual(
|
|
127
|
+
exception.args[0],
|
|
128
|
+
"Not a process application class: <class 'eventsourcing.system.Follower'>",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def test_is_leaders_only(self):
|
|
132
|
+
system = System(
|
|
133
|
+
pipes=[
|
|
134
|
+
[
|
|
135
|
+
Leader,
|
|
136
|
+
ProcessApplication,
|
|
137
|
+
ProcessApplication,
|
|
138
|
+
],
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
self.assertEqual(list(system.leaders_only), ["Leader"])
|
|
142
|
+
|
|
143
|
+
def test_leader_class(self):
|
|
144
|
+
system = System(
|
|
145
|
+
pipes=[
|
|
146
|
+
[
|
|
147
|
+
Application,
|
|
148
|
+
ProcessApplication,
|
|
149
|
+
ProcessApplication,
|
|
150
|
+
],
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
self.assertTrue(issubclass(system.leader_cls("Application"), Leader))
|
|
154
|
+
self.assertTrue(issubclass(system.leader_cls("ProcessApplication"), Leader))
|
|
155
|
+
|
|
156
|
+
def test_system_has_topic_if_defined_as_module_attribute(self):
|
|
157
|
+
system_topic = system_defined_as_global.topic
|
|
158
|
+
self.assertTrue(system_topic.endswith("test_system:system_defined_as_global"))
|
|
159
|
+
self.assertEqual(resolve_topic(system_topic), system_defined_as_global)
|
|
160
|
+
|
|
161
|
+
def test_system_topic_is_none_if_defined_in_function_body(self):
|
|
162
|
+
system = System([[]])
|
|
163
|
+
self.assertIsNone(system.topic)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestLeader(TestCase):
|
|
167
|
+
def test(self):
|
|
168
|
+
# Define fixture that receives prompts.
|
|
169
|
+
class FollowerFixture(RecordingEventReceiver):
|
|
170
|
+
def __init__(self):
|
|
171
|
+
self.num_received = 0
|
|
172
|
+
|
|
173
|
+
def receive_recording_event(self, _: RecordingEvent) -> None:
|
|
174
|
+
self.num_received += 1
|
|
175
|
+
|
|
176
|
+
# Test fixture is working.
|
|
177
|
+
follower = FollowerFixture()
|
|
178
|
+
follower.receive_recording_event(RecordingEvent("Leader", [], 1))
|
|
179
|
+
self.assertEqual(follower.num_received, 1)
|
|
180
|
+
|
|
181
|
+
# Construct leader.
|
|
182
|
+
leader = Leader()
|
|
183
|
+
leader.lead(follower)
|
|
184
|
+
|
|
185
|
+
# Check follower receives a prompt when there are new events.
|
|
186
|
+
leader.save(Aggregate())
|
|
187
|
+
self.assertEqual(follower.num_received, 2)
|
|
188
|
+
|
|
189
|
+
# Check follower doesn't receive prompt when no new events.
|
|
190
|
+
leader.save()
|
|
191
|
+
self.assertEqual(follower.num_received, 2)
|
|
192
|
+
|
|
193
|
+
# Check follower doesn't receive prompt when recordings are filtered out.
|
|
194
|
+
leader.notify_topics = ["topic1"]
|
|
195
|
+
leader.save(Aggregate())
|
|
196
|
+
self.assertEqual(follower.num_received, 2)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestFollower(TestCase):
|
|
200
|
+
def test_process_event(self):
|
|
201
|
+
class UUID5EmailNotification(Aggregate):
|
|
202
|
+
def __init__(self, to, subject, message):
|
|
203
|
+
self.to = to
|
|
204
|
+
self.subject = subject
|
|
205
|
+
self.message = message
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def create_id(to: str):
|
|
209
|
+
return uuid5(NAMESPACE_URL, f"/emails/{to}")
|
|
210
|
+
|
|
211
|
+
class UUID5EmailProcess(EmailProcess):
|
|
212
|
+
def policy(self, domain_event, processing_event):
|
|
213
|
+
if isinstance(domain_event, BankAccount.Opened):
|
|
214
|
+
notification = UUID5EmailNotification(
|
|
215
|
+
to=domain_event.email_address,
|
|
216
|
+
subject="Your New Account",
|
|
217
|
+
message=f"Dear {domain_event.full_name}, ...",
|
|
218
|
+
)
|
|
219
|
+
processing_event.collect_events(notification)
|
|
220
|
+
|
|
221
|
+
bank_accounts = BankAccounts()
|
|
222
|
+
email_process = UUID5EmailProcess()
|
|
223
|
+
|
|
224
|
+
account = BankAccount.open(
|
|
225
|
+
full_name="Alice",
|
|
226
|
+
email_address="alice@example.com",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
recordings = bank_accounts.save(account)
|
|
230
|
+
|
|
231
|
+
self.assertEqual(len(recordings), 1)
|
|
232
|
+
|
|
233
|
+
aggregate_event = recordings[0].domain_event
|
|
234
|
+
notification = recordings[0].notification
|
|
235
|
+
tracking = Tracking(bank_accounts.name, notification.id)
|
|
236
|
+
|
|
237
|
+
# Process the event.
|
|
238
|
+
email_process.process_event(aggregate_event, tracking)
|
|
239
|
+
self.assertEqual(
|
|
240
|
+
email_process.recorder.max_tracking_id(bank_accounts.name), notification.id
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Process the event again, ignore tracking integrity error.
|
|
244
|
+
email_process.process_event(aggregate_event, tracking)
|
|
245
|
+
self.assertEqual(
|
|
246
|
+
email_process.recorder.max_tracking_id(bank_accounts.name), notification.id
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Create another event that will cause conflict with email processing.
|
|
250
|
+
account = BankAccount.open(
|
|
251
|
+
full_name="Alice",
|
|
252
|
+
email_address="alice@example.com",
|
|
253
|
+
)
|
|
254
|
+
recordings = bank_accounts.save(account)
|
|
255
|
+
|
|
256
|
+
# Process the event and expect an integrity error.
|
|
257
|
+
aggregate_event = recordings[0].domain_event
|
|
258
|
+
notification = recordings[0].notification
|
|
259
|
+
tracking = Tracking(bank_accounts.name, notification.id)
|
|
260
|
+
with self.assertRaises(IntegrityError):
|
|
261
|
+
email_process.process_event(aggregate_event, tracking)
|
|
262
|
+
|
|
263
|
+
def test_filter_received_notifications(self):
|
|
264
|
+
class MyFollower(Follower):
|
|
265
|
+
follow_topics: ClassVar[Sequence[str]] = []
|
|
266
|
+
|
|
267
|
+
def policy(self, *args, **kwargs):
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
follower = MyFollower()
|
|
271
|
+
notifications = [
|
|
272
|
+
Notification(
|
|
273
|
+
id=1,
|
|
274
|
+
originator_id=uuid4(),
|
|
275
|
+
originator_version=1,
|
|
276
|
+
state=b"",
|
|
277
|
+
topic="topic1",
|
|
278
|
+
)
|
|
279
|
+
]
|
|
280
|
+
self.assertEqual(len(follower.filter_received_notifications(notifications)), 1)
|
|
281
|
+
follower.follow_topics = ["topic1"]
|
|
282
|
+
self.assertEqual(len(follower.filter_received_notifications(notifications)), 1)
|
|
283
|
+
follower.follow_topics = ["topic2"]
|
|
284
|
+
self.assertEqual(len(follower.filter_received_notifications(notifications)), 0)
|
|
File without changes
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
|
|
4
|
+
import eventsourcing
|
|
5
|
+
from eventsourcing.domain import Aggregate
|
|
6
|
+
from eventsourcing.utils import (
|
|
7
|
+
TopicError,
|
|
8
|
+
clear_topic_cache,
|
|
9
|
+
get_topic,
|
|
10
|
+
register_topic,
|
|
11
|
+
resolve_topic,
|
|
12
|
+
retry,
|
|
13
|
+
strtobool,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestRetryDecorator(TestCase):
|
|
18
|
+
def test_bare(self):
|
|
19
|
+
@retry
|
|
20
|
+
def f():
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
f()
|
|
24
|
+
|
|
25
|
+
def test_no_args(self):
|
|
26
|
+
@retry()
|
|
27
|
+
def f():
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
f()
|
|
31
|
+
|
|
32
|
+
def test_exception_single_value(self):
|
|
33
|
+
@retry(ValueError)
|
|
34
|
+
def f():
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
f()
|
|
38
|
+
|
|
39
|
+
def test_exception_sequence(self):
|
|
40
|
+
@retry((ValueError, TypeError))
|
|
41
|
+
def f():
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
f()
|
|
45
|
+
|
|
46
|
+
def test_exception_type_error(self):
|
|
47
|
+
with self.assertRaises(TypeError):
|
|
48
|
+
|
|
49
|
+
@retry(1)
|
|
50
|
+
def _():
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
with self.assertRaises(TypeError):
|
|
54
|
+
|
|
55
|
+
@retry((ValueError, 1))
|
|
56
|
+
def _():
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def test_exception_raised_no_retry(self):
|
|
60
|
+
self.call_count = 0
|
|
61
|
+
|
|
62
|
+
@retry(ValueError)
|
|
63
|
+
def f():
|
|
64
|
+
self.call_count += 1
|
|
65
|
+
raise ValueError
|
|
66
|
+
|
|
67
|
+
with self.assertRaises(ValueError):
|
|
68
|
+
f()
|
|
69
|
+
|
|
70
|
+
self.assertEqual(self.call_count, 1)
|
|
71
|
+
|
|
72
|
+
def test_max_attempts(self):
|
|
73
|
+
self.call_count = 0
|
|
74
|
+
|
|
75
|
+
@retry(ValueError, max_attempts=2)
|
|
76
|
+
def f():
|
|
77
|
+
self.call_count += 1
|
|
78
|
+
raise ValueError
|
|
79
|
+
|
|
80
|
+
with self.assertRaises(ValueError):
|
|
81
|
+
f()
|
|
82
|
+
|
|
83
|
+
self.assertEqual(self.call_count, 2)
|
|
84
|
+
|
|
85
|
+
def test_max_attempts_not_int(self):
|
|
86
|
+
with self.assertRaises(TypeError):
|
|
87
|
+
|
|
88
|
+
@retry(ValueError, max_attempts="a")
|
|
89
|
+
def f():
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
def test_wait(self):
|
|
93
|
+
self.call_count = 0
|
|
94
|
+
|
|
95
|
+
@retry(ValueError, max_attempts=2, wait=0.001)
|
|
96
|
+
def f():
|
|
97
|
+
self.call_count += 1
|
|
98
|
+
raise ValueError
|
|
99
|
+
|
|
100
|
+
with self.assertRaises(ValueError):
|
|
101
|
+
f()
|
|
102
|
+
|
|
103
|
+
self.assertEqual(self.call_count, 2)
|
|
104
|
+
|
|
105
|
+
def test_wait_not_float(self):
|
|
106
|
+
with self.assertRaises(TypeError):
|
|
107
|
+
|
|
108
|
+
@retry(ValueError, max_attempts=1, wait="a")
|
|
109
|
+
def f():
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
def test_stall(self):
|
|
113
|
+
self.call_count = 0
|
|
114
|
+
|
|
115
|
+
@retry(ValueError, max_attempts=2, stall=0.001)
|
|
116
|
+
def f():
|
|
117
|
+
self.call_count += 1
|
|
118
|
+
raise ValueError
|
|
119
|
+
|
|
120
|
+
with self.assertRaises(ValueError):
|
|
121
|
+
f()
|
|
122
|
+
|
|
123
|
+
self.assertEqual(self.call_count, 2)
|
|
124
|
+
|
|
125
|
+
def test_stall_not_float(self):
|
|
126
|
+
with self.assertRaises(TypeError):
|
|
127
|
+
|
|
128
|
+
@retry(ValueError, max_attempts=1, stall="a")
|
|
129
|
+
def f():
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestStrtobool(TestCase):
|
|
134
|
+
def test_true_values(self):
|
|
135
|
+
for s in ("y", "yes", "t", "true", "on", "1"):
|
|
136
|
+
self.assertTrue(strtobool(s), s)
|
|
137
|
+
|
|
138
|
+
def test_false_values(self):
|
|
139
|
+
for s in ("n", "no", "f", "false", "off", "0"):
|
|
140
|
+
self.assertFalse(strtobool(s), s)
|
|
141
|
+
|
|
142
|
+
def test_raises_value_error(self):
|
|
143
|
+
for s in ("", "a", "b", "c"):
|
|
144
|
+
with self.assertRaises(ValueError):
|
|
145
|
+
strtobool(s)
|
|
146
|
+
|
|
147
|
+
def test_raises_type_error(self):
|
|
148
|
+
for x in (None, True, False, 1, 2, 3):
|
|
149
|
+
with self.assertRaises(TypeError):
|
|
150
|
+
strtobool(cast(str, x))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestTopics(TestCase):
|
|
154
|
+
def test_get_topic(self):
|
|
155
|
+
self.assertEqual("eventsourcing.domain:Aggregate", get_topic(Aggregate))
|
|
156
|
+
|
|
157
|
+
def test_resolve_topic(self):
|
|
158
|
+
self.assertEqual(Aggregate, resolve_topic("eventsourcing.domain:Aggregate"))
|
|
159
|
+
|
|
160
|
+
def test_register_topic_rename_class(self):
|
|
161
|
+
register_topic("eventsourcing.domain:OldClass", Aggregate)
|
|
162
|
+
self.assertEqual(Aggregate, resolve_topic("eventsourcing.domain:OldClass"))
|
|
163
|
+
self.assertEqual(
|
|
164
|
+
Aggregate.Created, resolve_topic("eventsourcing.domain:OldClass.Created")
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def test_register_topic_move_module_into_package(self):
|
|
168
|
+
register_topic("oldmodule", eventsourcing.domain)
|
|
169
|
+
self.assertEqual(Aggregate, resolve_topic("oldmodule:Aggregate"))
|
|
170
|
+
self.assertEqual(
|
|
171
|
+
Aggregate.Created, resolve_topic("oldmodule:Aggregate.Created")
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def test_register_topic_rename_package(self):
|
|
175
|
+
register_topic("oldpackage", eventsourcing)
|
|
176
|
+
self.assertEqual(Aggregate, resolve_topic("oldpackage.domain:Aggregate"))
|
|
177
|
+
self.assertEqual(
|
|
178
|
+
Aggregate.Created, resolve_topic("oldpackage.domain:Aggregate.Created")
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def test_register_topic_move_package(self):
|
|
182
|
+
register_topic("old.eventsourcing.domain", eventsourcing.domain)
|
|
183
|
+
self.assertEqual(Aggregate, resolve_topic("old.eventsourcing.domain:Aggregate"))
|
|
184
|
+
|
|
185
|
+
def test_register_topic_rename_package_and_module(self):
|
|
186
|
+
register_topic("old.old", eventsourcing.domain)
|
|
187
|
+
self.assertEqual(Aggregate, resolve_topic("old.old:Aggregate"))
|
|
188
|
+
|
|
189
|
+
def test_topic_errors(self):
|
|
190
|
+
# Wrong module name.
|
|
191
|
+
with self.assertRaises(TopicError) as cm:
|
|
192
|
+
resolve_topic("oldmodule:Aggregate")
|
|
193
|
+
expected_msg = (
|
|
194
|
+
"Failed to resolve topic 'oldmodule:Aggregate': No module named 'oldmodule'"
|
|
195
|
+
)
|
|
196
|
+
self.assertEqual(expected_msg, cm.exception.args[0])
|
|
197
|
+
|
|
198
|
+
# Wrong class name.
|
|
199
|
+
with self.assertRaises(TopicError) as cm:
|
|
200
|
+
resolve_topic("eventsourcing.domain:OldClass")
|
|
201
|
+
expected_msg = (
|
|
202
|
+
"Failed to resolve topic 'eventsourcing.domain:OldClass': "
|
|
203
|
+
"module 'eventsourcing.domain' has no attribute 'OldClass'"
|
|
204
|
+
)
|
|
205
|
+
self.assertEqual(expected_msg, cm.exception.args[0])
|
|
206
|
+
|
|
207
|
+
# Wrong class attribute.
|
|
208
|
+
with self.assertRaises(TopicError) as cm:
|
|
209
|
+
resolve_topic("eventsourcing.domain:Aggregate.OldClass")
|
|
210
|
+
expected_msg = (
|
|
211
|
+
"Failed to resolve topic 'eventsourcing.domain:Aggregate.OldClass': "
|
|
212
|
+
"type object 'Aggregate' has no attribute 'OldClass'"
|
|
213
|
+
)
|
|
214
|
+
self.assertEqual(expected_msg, cm.exception.args[0])
|
|
215
|
+
|
|
216
|
+
# Can register same thing twice.
|
|
217
|
+
register_topic("old", eventsourcing)
|
|
218
|
+
register_topic("old", eventsourcing)
|
|
219
|
+
|
|
220
|
+
# Can't overwrite with another thing.
|
|
221
|
+
with self.assertRaises(TopicError) as cm:
|
|
222
|
+
register_topic("old", TestCase)
|
|
223
|
+
self.assertIn("is already registered for topic 'old'", cm.exception.args[0])
|
|
224
|
+
|
|
225
|
+
def tearDown(self) -> None:
|
|
226
|
+
clear_topic_cache()
|