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,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()