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.
- eventsourcing/application.py +0 -15
- eventsourcing/system.py +25 -2
- {eventsourcing-9.3.3.dist-info → eventsourcing-9.3.5.dist-info}/METADATA +1 -1
- eventsourcing-9.3.5.dist-info/RECORD +24 -0
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +0 -27
- eventsourcing/examples/aggregate1/domainmodel.py +0 -16
- eventsourcing/examples/aggregate1/test_application.py +0 -37
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +0 -27
- eventsourcing/examples/aggregate2/domainmodel.py +0 -22
- eventsourcing/examples/aggregate2/test_application.py +0 -37
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +0 -27
- eventsourcing/examples/aggregate3/domainmodel.py +0 -38
- eventsourcing/examples/aggregate3/test_application.py +0 -37
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +0 -27
- eventsourcing/examples/aggregate4/domainmodel.py +0 -114
- eventsourcing/examples/aggregate4/test_application.py +0 -38
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +0 -27
- eventsourcing/examples/aggregate5/domainmodel.py +0 -131
- eventsourcing/examples/aggregate5/test_application.py +0 -38
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +0 -30
- eventsourcing/examples/aggregate6/domainmodel.py +0 -123
- eventsourcing/examples/aggregate6/test_application.py +0 -38
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +0 -40
- eventsourcing/examples/aggregate6a/domainmodel.py +0 -149
- eventsourcing/examples/aggregate6a/test_application.py +0 -45
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +0 -53
- eventsourcing/examples/aggregate7/domainmodel.py +0 -142
- eventsourcing/examples/aggregate7/persistence.py +0 -57
- eventsourcing/examples/aggregate7/test_application.py +0 -45
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +0 -45
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +0 -67
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +0 -56
- eventsourcing/examples/aggregate7a/domainmodel.py +0 -168
- eventsourcing/examples/aggregate7a/test_application.py +0 -46
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +0 -45
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +0 -47
- eventsourcing/examples/aggregate8/domainmodel.py +0 -71
- eventsourcing/examples/aggregate8/persistence.py +0 -57
- eventsourcing/examples/aggregate8/test_application.py +0 -44
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +0 -44
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +0 -38
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +0 -70
- eventsourcing/examples/bankaccounts/domainmodel.py +0 -56
- eventsourcing/examples/bankaccounts/test.py +0 -173
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +0 -126
- eventsourcing/examples/cargoshipping/domainmodel.py +0 -330
- eventsourcing/examples/cargoshipping/interface.py +0 -143
- eventsourcing/examples/cargoshipping/test.py +0 -231
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +0 -118
- eventsourcing/examples/contentmanagement/domainmodel.py +0 -69
- eventsourcing/examples/contentmanagement/test.py +0 -180
- eventsourcing/examples/contentmanagement/utils.py +0 -26
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +0 -54
- eventsourcing/examples/contentmanagementsystem/postgres.py +0 -17
- eventsourcing/examples/contentmanagementsystem/sqlite.py +0 -17
- eventsourcing/examples/contentmanagementsystem/system.py +0 -14
- eventsourcing/examples/contentmanagementsystem/test_system.py +0 -180
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +0 -45
- eventsourcing/examples/searchablecontent/persistence.py +0 -23
- eventsourcing/examples/searchablecontent/postgres.py +0 -118
- eventsourcing/examples/searchablecontent/sqlite.py +0 -136
- eventsourcing/examples/searchablecontent/test_application.py +0 -110
- eventsourcing/examples/searchablecontent/test_recorder.py +0 -68
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +0 -32
- eventsourcing/examples/searchabletimestamps/persistence.py +0 -20
- eventsourcing/examples/searchabletimestamps/postgres.py +0 -110
- eventsourcing/examples/searchabletimestamps/sqlite.py +0 -99
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +0 -94
- eventsourcing/examples/test_invoice.py +0 -176
- eventsourcing/examples/test_parking_lot.py +0 -206
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +0 -55
- eventsourcing/tests/application_tests/test_application_with_popo.py +0 -22
- eventsourcing/tests/application_tests/test_application_with_postgres.py +0 -75
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +0 -72
- eventsourcing/tests/application_tests/test_cache.py +0 -134
- eventsourcing/tests/application_tests/test_event_sourced_log.py +0 -162
- eventsourcing/tests/application_tests/test_notificationlog.py +0 -232
- eventsourcing/tests/application_tests/test_notificationlogreader.py +0 -126
- eventsourcing/tests/application_tests/test_processapplication.py +0 -110
- eventsourcing/tests/application_tests/test_processingpolicy.py +0 -109
- eventsourcing/tests/application_tests/test_repository.py +0 -504
- eventsourcing/tests/application_tests/test_snapshotting.py +0 -68
- eventsourcing/tests/application_tests/test_upcasting.py +0 -459
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +0 -293
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +0 -1200
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +0 -1604
- eventsourcing/tests/domain_tests/test_domainevent.py +0 -80
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +0 -258
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +0 -93
- eventsourcing/tests/persistence_tests/test_connection_pool.py +0 -722
- eventsourcing/tests/persistence_tests/test_eventstore.py +0 -72
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +0 -21
- eventsourcing/tests/persistence_tests/test_mapper.py +0 -113
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +0 -69
- eventsourcing/tests/persistence_tests/test_popo.py +0 -124
- eventsourcing/tests/persistence_tests/test_postgres.py +0 -1120
- eventsourcing/tests/persistence_tests/test_sqlite.py +0 -348
- eventsourcing/tests/persistence_tests/test_transcoder.py +0 -44
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +0 -935
- eventsourcing/tests/system_tests/test_system.py +0 -284
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +0 -226
- eventsourcing-9.3.3.dist-info/RECORD +0 -145
- {eventsourcing-9.3.3.dist-info → eventsourcing-9.3.5.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.3.3.dist-info → eventsourcing-9.3.5.dist-info}/LICENSE +0 -0
- {eventsourcing-9.3.3.dist-info → eventsourcing-9.3.5.dist-info}/WHEEL +0 -0
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import unittest
|
|
4
|
-
from datetime import timedelta
|
|
5
|
-
|
|
6
|
-
from eventsourcing.examples.cargoshipping.application import BookingApplication
|
|
7
|
-
from eventsourcing.examples.cargoshipping.domainmodel import Cargo
|
|
8
|
-
from eventsourcing.examples.cargoshipping.interface import (
|
|
9
|
-
BookingService,
|
|
10
|
-
select_preferred_itinerary,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class TestBookingService(unittest.TestCase):
|
|
15
|
-
def setUp(self) -> None:
|
|
16
|
-
self.service = BookingService(BookingApplication())
|
|
17
|
-
|
|
18
|
-
def test_admin_can_book_new_cargo(self) -> None:
|
|
19
|
-
arrival_deadline = Cargo.Event.create_timestamp() + timedelta(weeks=3)
|
|
20
|
-
|
|
21
|
-
cargo_id = self.service.book_new_cargo(
|
|
22
|
-
origin="NLRTM",
|
|
23
|
-
destination="USDAL",
|
|
24
|
-
arrival_deadline=arrival_deadline,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
cargo_details = self.service.get_cargo_details(cargo_id)
|
|
28
|
-
self.assertTrue(cargo_details["id"])
|
|
29
|
-
self.assertEqual(cargo_details["origin"], "NLRTM")
|
|
30
|
-
self.assertEqual(cargo_details["destination"], "USDAL")
|
|
31
|
-
|
|
32
|
-
self.service.change_destination(cargo_id, destination="AUMEL")
|
|
33
|
-
cargo_details = self.service.get_cargo_details(cargo_id)
|
|
34
|
-
self.assertEqual(cargo_details["destination"], "AUMEL")
|
|
35
|
-
self.assertEqual(
|
|
36
|
-
cargo_details["arrival_deadline"],
|
|
37
|
-
arrival_deadline,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
def test_scenario_cargo_from_hongkong_to_stockholm(
|
|
41
|
-
self,
|
|
42
|
-
) -> None:
|
|
43
|
-
# Test setup: A cargo should be shipped from
|
|
44
|
-
# Hongkong to Stockholm, and it should arrive
|
|
45
|
-
# in no more than two weeks.
|
|
46
|
-
origin = "HONGKONG"
|
|
47
|
-
destination = "STOCKHOLM"
|
|
48
|
-
arrival_deadline = Cargo.Event.create_timestamp() + timedelta(weeks=2)
|
|
49
|
-
|
|
50
|
-
# Use case 1: booking.
|
|
51
|
-
|
|
52
|
-
# A new cargo is booked, and the unique tracking
|
|
53
|
-
# id is assigned to the cargo.
|
|
54
|
-
tracking_id = self.service.book_new_cargo(origin, destination, arrival_deadline)
|
|
55
|
-
|
|
56
|
-
# The tracking id can be used to lookup the cargo
|
|
57
|
-
# in the repository.
|
|
58
|
-
# Important: The cargo, and thus the domain model,
|
|
59
|
-
# is responsible for determining the status of the
|
|
60
|
-
# cargo, whether it is on the right track or not
|
|
61
|
-
# and so on. This is core domain logic. Tracking
|
|
62
|
-
# the cargo basically amounts to presenting
|
|
63
|
-
# information extracted from the cargo aggregate
|
|
64
|
-
# in a suitable way.
|
|
65
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
66
|
-
self.assertEqual(
|
|
67
|
-
cargo_details["transport_status"],
|
|
68
|
-
"NOT_RECEIVED",
|
|
69
|
-
)
|
|
70
|
-
self.assertEqual(cargo_details["routing_status"], "NOT_ROUTED")
|
|
71
|
-
self.assertEqual(cargo_details["is_misdirected"], False)
|
|
72
|
-
self.assertEqual(
|
|
73
|
-
cargo_details["estimated_time_of_arrival"],
|
|
74
|
-
None,
|
|
75
|
-
)
|
|
76
|
-
self.assertEqual(cargo_details["next_expected_activity"], None)
|
|
77
|
-
|
|
78
|
-
# Use case 2: routing.
|
|
79
|
-
#
|
|
80
|
-
# A number of possible routes for this cargo is
|
|
81
|
-
# requested and may be presented to the customer
|
|
82
|
-
# in some way for him/her to choose from.
|
|
83
|
-
# Selection could be affected by things like price
|
|
84
|
-
# and time of delivery, but this test simply uses
|
|
85
|
-
# an arbitrary selection to mimic that process.
|
|
86
|
-
itineraries = self.service.request_possible_routes_for_cargo(tracking_id)
|
|
87
|
-
route_details = select_preferred_itinerary(itineraries)
|
|
88
|
-
|
|
89
|
-
# The cargo is then assigned to the selected
|
|
90
|
-
# route, described by an itinerary.
|
|
91
|
-
self.service.assign_route(tracking_id, route_details)
|
|
92
|
-
|
|
93
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
94
|
-
self.assertEqual(
|
|
95
|
-
cargo_details["transport_status"],
|
|
96
|
-
"NOT_RECEIVED",
|
|
97
|
-
)
|
|
98
|
-
self.assertEqual(cargo_details["routing_status"], "ROUTED")
|
|
99
|
-
self.assertEqual(cargo_details["is_misdirected"], False)
|
|
100
|
-
self.assertTrue(cargo_details["estimated_time_of_arrival"])
|
|
101
|
-
self.assertEqual(
|
|
102
|
-
cargo_details["next_expected_activity"],
|
|
103
|
-
("RECEIVE", "HONGKONG", ""),
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Use case 3: handling
|
|
107
|
-
|
|
108
|
-
# A handling event registration attempt will be
|
|
109
|
-
# formed from parsing the data coming in as a
|
|
110
|
-
# handling report either via the web service
|
|
111
|
-
# interface or as an uploaded CSV file. The
|
|
112
|
-
# handling event factory tries to create a
|
|
113
|
-
# HandlingEvent from the attempt, and if the
|
|
114
|
-
# factory decides that this is a plausible
|
|
115
|
-
# handling event, it is stored. If the attempt
|
|
116
|
-
# is invalid, for example if no cargo exists for
|
|
117
|
-
# the specified tracking id, the attempt is
|
|
118
|
-
# rejected.
|
|
119
|
-
#
|
|
120
|
-
# Handling begins: cargo is received in Hongkong.
|
|
121
|
-
self.service.register_handling_event(tracking_id, None, "HONGKONG", "RECEIVE")
|
|
122
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
123
|
-
self.assertEqual(cargo_details["transport_status"], "IN_PORT")
|
|
124
|
-
self.assertEqual(
|
|
125
|
-
cargo_details["last_known_location"],
|
|
126
|
-
"HONGKONG",
|
|
127
|
-
)
|
|
128
|
-
self.assertEqual(
|
|
129
|
-
cargo_details["next_expected_activity"],
|
|
130
|
-
("LOAD", "HONGKONG", "V1"),
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# Load onto voyage V1.
|
|
134
|
-
self.service.register_handling_event(tracking_id, "V1", "HONGKONG", "LOAD")
|
|
135
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
136
|
-
self.assertEqual(cargo_details["current_voyage_number"], "V1")
|
|
137
|
-
self.assertEqual(
|
|
138
|
-
cargo_details["last_known_location"],
|
|
139
|
-
"HONGKONG",
|
|
140
|
-
)
|
|
141
|
-
self.assertEqual(
|
|
142
|
-
cargo_details["transport_status"],
|
|
143
|
-
"ONBOARD_CARRIER",
|
|
144
|
-
)
|
|
145
|
-
self.assertEqual(
|
|
146
|
-
cargo_details["next_expected_activity"],
|
|
147
|
-
("UNLOAD", "NEWYORK", "V1"),
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# Incorrectly unload in Tokyo.
|
|
151
|
-
self.service.register_handling_event(tracking_id, "V1", "TOKYO", "UNLOAD")
|
|
152
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
153
|
-
self.assertEqual(cargo_details["current_voyage_number"], None)
|
|
154
|
-
self.assertEqual(cargo_details["last_known_location"], "TOKYO")
|
|
155
|
-
self.assertEqual(cargo_details["transport_status"], "IN_PORT")
|
|
156
|
-
self.assertEqual(cargo_details["is_misdirected"], True)
|
|
157
|
-
self.assertEqual(cargo_details["next_expected_activity"], None)
|
|
158
|
-
|
|
159
|
-
# Reroute.
|
|
160
|
-
itineraries = self.service.request_possible_routes_for_cargo(tracking_id)
|
|
161
|
-
route_details = select_preferred_itinerary(itineraries)
|
|
162
|
-
self.service.assign_route(tracking_id, route_details)
|
|
163
|
-
|
|
164
|
-
# Load in Tokyo.
|
|
165
|
-
self.service.register_handling_event(tracking_id, "V3", "TOKYO", "LOAD")
|
|
166
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
167
|
-
self.assertEqual(cargo_details["current_voyage_number"], "V3")
|
|
168
|
-
self.assertEqual(cargo_details["last_known_location"], "TOKYO")
|
|
169
|
-
self.assertEqual(
|
|
170
|
-
cargo_details["transport_status"],
|
|
171
|
-
"ONBOARD_CARRIER",
|
|
172
|
-
)
|
|
173
|
-
self.assertEqual(cargo_details["is_misdirected"], False)
|
|
174
|
-
self.assertEqual(
|
|
175
|
-
cargo_details["next_expected_activity"],
|
|
176
|
-
("UNLOAD", "HAMBURG", "V3"),
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
# Unload in Hamburg.
|
|
180
|
-
self.service.register_handling_event(tracking_id, "V3", "HAMBURG", "UNLOAD")
|
|
181
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
182
|
-
self.assertEqual(cargo_details["current_voyage_number"], None)
|
|
183
|
-
self.assertEqual(cargo_details["last_known_location"], "HAMBURG")
|
|
184
|
-
self.assertEqual(cargo_details["transport_status"], "IN_PORT")
|
|
185
|
-
self.assertEqual(cargo_details["is_misdirected"], False)
|
|
186
|
-
self.assertEqual(
|
|
187
|
-
cargo_details["next_expected_activity"],
|
|
188
|
-
("LOAD", "HAMBURG", "V4"),
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
# Load in Hamburg
|
|
192
|
-
self.service.register_handling_event(tracking_id, "V4", "HAMBURG", "LOAD")
|
|
193
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
194
|
-
self.assertEqual(cargo_details["current_voyage_number"], "V4")
|
|
195
|
-
self.assertEqual(cargo_details["last_known_location"], "HAMBURG")
|
|
196
|
-
self.assertEqual(
|
|
197
|
-
cargo_details["transport_status"],
|
|
198
|
-
"ONBOARD_CARRIER",
|
|
199
|
-
)
|
|
200
|
-
self.assertEqual(cargo_details["is_misdirected"], False)
|
|
201
|
-
self.assertEqual(
|
|
202
|
-
cargo_details["next_expected_activity"],
|
|
203
|
-
("UNLOAD", "STOCKHOLM", "V4"),
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
# Unload in Stockholm
|
|
207
|
-
self.service.register_handling_event(tracking_id, "V4", "STOCKHOLM", "UNLOAD")
|
|
208
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
209
|
-
self.assertEqual(cargo_details["current_voyage_number"], None)
|
|
210
|
-
self.assertEqual(
|
|
211
|
-
cargo_details["last_known_location"],
|
|
212
|
-
"STOCKHOLM",
|
|
213
|
-
)
|
|
214
|
-
self.assertEqual(cargo_details["transport_status"], "IN_PORT")
|
|
215
|
-
self.assertEqual(cargo_details["is_misdirected"], False)
|
|
216
|
-
self.assertEqual(
|
|
217
|
-
cargo_details["next_expected_activity"],
|
|
218
|
-
("CLAIM", "STOCKHOLM", ""),
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
# Finally, cargo is claimed in Stockholm.
|
|
222
|
-
self.service.register_handling_event(tracking_id, None, "STOCKHOLM", "CLAIM")
|
|
223
|
-
cargo_details = self.service.get_cargo_details(tracking_id)
|
|
224
|
-
self.assertEqual(cargo_details["current_voyage_number"], None)
|
|
225
|
-
self.assertEqual(
|
|
226
|
-
cargo_details["last_known_location"],
|
|
227
|
-
"STOCKHOLM",
|
|
228
|
-
)
|
|
229
|
-
self.assertEqual(cargo_details["transport_status"], "CLAIMED")
|
|
230
|
-
self.assertEqual(cargo_details["is_misdirected"], False)
|
|
231
|
-
self.assertEqual(cargo_details["next_expected_activity"], None)
|
|
File without changes
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Iterator, Type, Union, cast
|
|
4
|
-
from uuid import NAMESPACE_URL, UUID, uuid5
|
|
5
|
-
|
|
6
|
-
from eventsourcing.application import (
|
|
7
|
-
AggregateNotFoundError,
|
|
8
|
-
Application,
|
|
9
|
-
EventSourcedLog,
|
|
10
|
-
)
|
|
11
|
-
from eventsourcing.examples.contentmanagement.domainmodel import Index, Page, PageLogged
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING: # pragma: nocover
|
|
14
|
-
from eventsourcing.domain import MutableOrImmutableAggregate
|
|
15
|
-
from eventsourcing.utils import EnvType
|
|
16
|
-
|
|
17
|
-
PageDetailsType = Dict[str, Union[str, Any]]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ContentManagementApplication(Application):
|
|
21
|
-
env: ClassVar[Dict[str, str]] = {"COMPRESSOR_TOPIC": "gzip"}
|
|
22
|
-
snapshotting_intervals: ClassVar[Dict[Type[MutableOrImmutableAggregate], int]] = {
|
|
23
|
-
Page: 5
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
def __init__(self, env: EnvType | None = None) -> None:
|
|
27
|
-
super().__init__(env)
|
|
28
|
-
self.page_log: EventSourcedLog[PageLogged] = EventSourcedLog(
|
|
29
|
-
self.events, uuid5(NAMESPACE_URL, "/page_log"), PageLogged
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
def create_page(self, title: str, slug: str) -> None:
|
|
33
|
-
page = Page(title=title, slug=slug)
|
|
34
|
-
page_logged = self.page_log.trigger_event(page_id=page.id)
|
|
35
|
-
index_entry = Index(slug, ref=page.id)
|
|
36
|
-
self.save(page, page_logged, index_entry)
|
|
37
|
-
|
|
38
|
-
def get_page_by_slug(self, slug: str) -> PageDetailsType:
|
|
39
|
-
page = self._get_page_by_slug(slug)
|
|
40
|
-
return self._details_from_page(page)
|
|
41
|
-
|
|
42
|
-
def get_page_by_id(self, page_id: UUID) -> PageDetailsType:
|
|
43
|
-
page = self._get_page_by_id(page_id)
|
|
44
|
-
return self._details_from_page(page)
|
|
45
|
-
|
|
46
|
-
def _details_from_page(self, page: Page) -> PageDetailsType:
|
|
47
|
-
return {
|
|
48
|
-
"title": page.title,
|
|
49
|
-
"slug": page.slug,
|
|
50
|
-
"body": page.body,
|
|
51
|
-
"modified_by": page.modified_by,
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
def update_title(self, slug: str, title: str) -> None:
|
|
55
|
-
page = self._get_page_by_slug(slug)
|
|
56
|
-
page.update_title(title=title)
|
|
57
|
-
self.save(page)
|
|
58
|
-
|
|
59
|
-
def update_slug(self, old_slug: str, new_slug: str) -> None:
|
|
60
|
-
page = self._get_page_by_slug(old_slug)
|
|
61
|
-
page.update_slug(new_slug)
|
|
62
|
-
old_index = self._get_index(old_slug)
|
|
63
|
-
old_index.update_ref(None)
|
|
64
|
-
try:
|
|
65
|
-
new_index = self._get_index(new_slug)
|
|
66
|
-
except AggregateNotFoundError:
|
|
67
|
-
new_index = Index(new_slug, page.id)
|
|
68
|
-
else:
|
|
69
|
-
if new_index.ref is None:
|
|
70
|
-
new_index.update_ref(page.id)
|
|
71
|
-
else:
|
|
72
|
-
raise SlugConflictError
|
|
73
|
-
self.save(page, old_index, new_index)
|
|
74
|
-
|
|
75
|
-
def update_body(self, slug: str, body: str) -> None:
|
|
76
|
-
page = self._get_page_by_slug(slug)
|
|
77
|
-
page.update_body(body)
|
|
78
|
-
self.save(page)
|
|
79
|
-
|
|
80
|
-
def _get_page_by_slug(self, slug: str) -> Page:
|
|
81
|
-
try:
|
|
82
|
-
index = self._get_index(slug)
|
|
83
|
-
except AggregateNotFoundError:
|
|
84
|
-
raise PageNotFoundError(slug) from None
|
|
85
|
-
if index.ref is None:
|
|
86
|
-
raise PageNotFoundError(slug)
|
|
87
|
-
page_id = index.ref
|
|
88
|
-
return self._get_page_by_id(page_id)
|
|
89
|
-
|
|
90
|
-
def _get_page_by_id(self, page_id: UUID) -> Page:
|
|
91
|
-
return cast(Page, self.repository.get(page_id))
|
|
92
|
-
|
|
93
|
-
def _get_index(self, slug: str) -> Index:
|
|
94
|
-
return cast(Index, self.repository.get(Index.create_id(slug)))
|
|
95
|
-
|
|
96
|
-
def get_pages(
|
|
97
|
-
self,
|
|
98
|
-
*,
|
|
99
|
-
gt: int | None = None,
|
|
100
|
-
lte: int | None = None,
|
|
101
|
-
desc: bool = False,
|
|
102
|
-
limit: int | None = None,
|
|
103
|
-
) -> Iterator[PageDetailsType]:
|
|
104
|
-
for page_logged in self.page_log.get(gt=gt, lte=lte, desc=desc, limit=limit):
|
|
105
|
-
page = self._get_page_by_id(page_logged.page_id)
|
|
106
|
-
yield self._details_from_page(page)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class PageNotFoundError(Exception):
|
|
110
|
-
"""
|
|
111
|
-
Raised when a page is not found.
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class SlugConflictError(Exception):
|
|
116
|
-
"""
|
|
117
|
-
Raised when updating a page to a slug used by another page.
|
|
118
|
-
"""
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from contextvars import ContextVar
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
|
-
from typing import cast
|
|
6
|
-
from uuid import NAMESPACE_URL, UUID, uuid5
|
|
7
|
-
|
|
8
|
-
from eventsourcing.domain import Aggregate, DomainEvent, event
|
|
9
|
-
from eventsourcing.examples.contentmanagement.utils import apply_patch, create_diff
|
|
10
|
-
|
|
11
|
-
user_id_cvar: ContextVar[UUID | None] = ContextVar("user_id", default=None)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Page(Aggregate):
|
|
15
|
-
class Event(Aggregate.Event):
|
|
16
|
-
user_id: UUID | None = field(default_factory=user_id_cvar.get, init=False)
|
|
17
|
-
|
|
18
|
-
def apply(self, aggregate: Aggregate) -> None:
|
|
19
|
-
cast(Page, aggregate).modified_by = self.user_id
|
|
20
|
-
|
|
21
|
-
class Created(Event, Aggregate.Created):
|
|
22
|
-
title: str
|
|
23
|
-
slug: str
|
|
24
|
-
body: str
|
|
25
|
-
|
|
26
|
-
def __init__(self, title: str, slug: str, body: str = ""):
|
|
27
|
-
self.title = title
|
|
28
|
-
self.slug = slug
|
|
29
|
-
self.body = body
|
|
30
|
-
self.modified_by: UUID | None = None
|
|
31
|
-
|
|
32
|
-
@event("SlugUpdated")
|
|
33
|
-
def update_slug(self, slug: str) -> None:
|
|
34
|
-
self.slug = slug
|
|
35
|
-
|
|
36
|
-
@event("TitleUpdated")
|
|
37
|
-
def update_title(self, title: str) -> None:
|
|
38
|
-
self.title = title
|
|
39
|
-
|
|
40
|
-
def update_body(self, body: str) -> None:
|
|
41
|
-
self._update_body(create_diff(old=self.body, new=body))
|
|
42
|
-
|
|
43
|
-
class BodyUpdated(Event):
|
|
44
|
-
diff: str
|
|
45
|
-
|
|
46
|
-
@event(BodyUpdated)
|
|
47
|
-
def _update_body(self, diff: str) -> None:
|
|
48
|
-
self.body = apply_patch(old=self.body, diff=diff)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@dataclass
|
|
52
|
-
class Index(Aggregate):
|
|
53
|
-
slug: str
|
|
54
|
-
ref: UUID | None
|
|
55
|
-
|
|
56
|
-
class Event(Aggregate.Event):
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
@staticmethod
|
|
60
|
-
def create_id(slug: str) -> UUID:
|
|
61
|
-
return uuid5(NAMESPACE_URL, f"/slugs/{slug}")
|
|
62
|
-
|
|
63
|
-
@event("RefChanged")
|
|
64
|
-
def update_ref(self, ref: UUID | None) -> None:
|
|
65
|
-
self.ref = ref
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class PageLogged(DomainEvent):
|
|
69
|
-
page_id: UUID
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import cast
|
|
4
|
-
from unittest import TestCase
|
|
5
|
-
from uuid import uuid4
|
|
6
|
-
|
|
7
|
-
from eventsourcing.examples.contentmanagement.application import (
|
|
8
|
-
ContentManagementApplication,
|
|
9
|
-
PageNotFoundError,
|
|
10
|
-
SlugConflictError,
|
|
11
|
-
)
|
|
12
|
-
from eventsourcing.examples.contentmanagement.domainmodel import (
|
|
13
|
-
Index,
|
|
14
|
-
Page,
|
|
15
|
-
user_id_cvar,
|
|
16
|
-
)
|
|
17
|
-
from eventsourcing.system import NotificationLogReader
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class TestContentManagement(TestCase):
|
|
21
|
-
def test(self) -> None:
|
|
22
|
-
# Set user_id context variable.
|
|
23
|
-
user_id = uuid4()
|
|
24
|
-
user_id_cvar.set(user_id)
|
|
25
|
-
|
|
26
|
-
# Construct application.
|
|
27
|
-
app = ContentManagementApplication()
|
|
28
|
-
|
|
29
|
-
# Check the page doesn't exist.
|
|
30
|
-
with self.assertRaises(PageNotFoundError):
|
|
31
|
-
app.get_page_by_slug(slug="welcome")
|
|
32
|
-
|
|
33
|
-
# Check the list of pages is empty.
|
|
34
|
-
pages = list(app.get_pages())
|
|
35
|
-
self.assertEqual(len(pages), 0)
|
|
36
|
-
|
|
37
|
-
# Create a page.
|
|
38
|
-
app.create_page(title="Welcome", slug="welcome")
|
|
39
|
-
|
|
40
|
-
# Present page identified by the given slug.
|
|
41
|
-
page = app.get_page_by_slug(slug="welcome")
|
|
42
|
-
|
|
43
|
-
# Check we got a dict that has the given title and slug.
|
|
44
|
-
self.assertEqual(page["title"], "Welcome")
|
|
45
|
-
self.assertEqual(page["slug"], "welcome")
|
|
46
|
-
self.assertEqual(page["body"], "")
|
|
47
|
-
self.assertEqual(page["modified_by"], user_id)
|
|
48
|
-
|
|
49
|
-
# Update the title.
|
|
50
|
-
app.update_title(slug="welcome", title="Welcome Visitors")
|
|
51
|
-
|
|
52
|
-
# Check the title was updated.
|
|
53
|
-
page = app.get_page_by_slug(slug="welcome")
|
|
54
|
-
self.assertEqual(page["title"], "Welcome Visitors")
|
|
55
|
-
self.assertEqual(page["modified_by"], user_id)
|
|
56
|
-
|
|
57
|
-
# Update the slug.
|
|
58
|
-
app.update_slug(old_slug="welcome", new_slug="welcome-visitors")
|
|
59
|
-
|
|
60
|
-
# Check the index was updated.
|
|
61
|
-
with self.assertRaises(PageNotFoundError):
|
|
62
|
-
app.get_page_by_slug(slug="welcome")
|
|
63
|
-
|
|
64
|
-
# Check we can get the page by the new slug.
|
|
65
|
-
page = app.get_page_by_slug(slug="welcome-visitors")
|
|
66
|
-
self.assertEqual(page["title"], "Welcome Visitors")
|
|
67
|
-
self.assertEqual(page["slug"], "welcome-visitors")
|
|
68
|
-
|
|
69
|
-
# Update the body.
|
|
70
|
-
app.update_body(slug="welcome-visitors", body="Welcome to my wiki")
|
|
71
|
-
|
|
72
|
-
# Check the body was updated.
|
|
73
|
-
page = app.get_page_by_slug(slug="welcome-visitors")
|
|
74
|
-
self.assertEqual(page["body"], "Welcome to my wiki")
|
|
75
|
-
|
|
76
|
-
# Update the body.
|
|
77
|
-
app.update_body(slug="welcome-visitors", body="Welcome to this wiki")
|
|
78
|
-
|
|
79
|
-
# Check the body was updated.
|
|
80
|
-
page = app.get_page_by_slug(slug="welcome-visitors")
|
|
81
|
-
self.assertEqual(page["body"], "Welcome to this wiki")
|
|
82
|
-
|
|
83
|
-
# Update the body.
|
|
84
|
-
app.update_body(
|
|
85
|
-
slug="welcome-visitors",
|
|
86
|
-
body="""
|
|
87
|
-
Welcome to this wiki!
|
|
88
|
-
|
|
89
|
-
This is a wiki about...
|
|
90
|
-
""",
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
# Check the body was updated.
|
|
94
|
-
page = app.get_page_by_slug(slug="welcome-visitors")
|
|
95
|
-
self.assertEqual(
|
|
96
|
-
page["body"],
|
|
97
|
-
"""
|
|
98
|
-
Welcome to this wiki!
|
|
99
|
-
|
|
100
|
-
This is a wiki about...
|
|
101
|
-
""",
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
# Check all the Page events have the user_id.
|
|
105
|
-
for notification in NotificationLogReader(app.notification_log).read(start=1):
|
|
106
|
-
domain_event = app.mapper.to_domain_event(notification)
|
|
107
|
-
if isinstance(domain_event, Page.Event):
|
|
108
|
-
self.assertEqual(domain_event.user_id, user_id)
|
|
109
|
-
|
|
110
|
-
# Change user_id context variable.
|
|
111
|
-
user_id = uuid4()
|
|
112
|
-
user_id_cvar.set(user_id)
|
|
113
|
-
|
|
114
|
-
# Update the body.
|
|
115
|
-
app.update_body(
|
|
116
|
-
slug="welcome-visitors",
|
|
117
|
-
body="""
|
|
118
|
-
Welcome to this wiki!
|
|
119
|
-
|
|
120
|
-
This is a wiki about us!
|
|
121
|
-
""",
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
# Check 'modified_by' changed.
|
|
125
|
-
page = app.get_page_by_slug(slug="welcome-visitors")
|
|
126
|
-
self.assertEqual(page["title"], "Welcome Visitors")
|
|
127
|
-
self.assertEqual(page["modified_by"], user_id)
|
|
128
|
-
|
|
129
|
-
# Check a snapshot was created by now.
|
|
130
|
-
assert app.snapshots
|
|
131
|
-
index = cast(Index, app.repository.get(Index.create_id("welcome-visitors")))
|
|
132
|
-
assert index.ref
|
|
133
|
-
self.assertTrue(len(list(app.snapshots.get(index.ref))))
|
|
134
|
-
|
|
135
|
-
# Create some more pages and list all the pages.
|
|
136
|
-
app.create_page("Page 2", "page-2")
|
|
137
|
-
app.create_page("Page 3", "page-3")
|
|
138
|
-
app.create_page("Page 4", "page-4")
|
|
139
|
-
app.create_page("Page 5", "page-5")
|
|
140
|
-
|
|
141
|
-
pages = list(app.get_pages(desc=True))
|
|
142
|
-
self.assertEqual(pages[0]["title"], "Page 5")
|
|
143
|
-
self.assertEqual(pages[0]["slug"], "page-5")
|
|
144
|
-
self.assertEqual(pages[1]["title"], "Page 4")
|
|
145
|
-
self.assertEqual(pages[1]["slug"], "page-4")
|
|
146
|
-
self.assertEqual(pages[2]["title"], "Page 3")
|
|
147
|
-
self.assertEqual(pages[2]["slug"], "page-3")
|
|
148
|
-
self.assertEqual(pages[3]["title"], "Page 2")
|
|
149
|
-
self.assertEqual(pages[3]["slug"], "page-2")
|
|
150
|
-
self.assertEqual(pages[4]["title"], "Welcome Visitors")
|
|
151
|
-
self.assertEqual(pages[4]["slug"], "welcome-visitors")
|
|
152
|
-
|
|
153
|
-
pages = list(app.get_pages(desc=True, limit=3))
|
|
154
|
-
self.assertEqual(len(pages), 3)
|
|
155
|
-
self.assertEqual(pages[0]["slug"], "page-5")
|
|
156
|
-
self.assertEqual(pages[1]["slug"], "page-4")
|
|
157
|
-
self.assertEqual(pages[2]["slug"], "page-3")
|
|
158
|
-
|
|
159
|
-
pages = list(app.get_pages(desc=True, limit=3, lte=2))
|
|
160
|
-
self.assertEqual(len(pages), 2)
|
|
161
|
-
self.assertEqual(pages[0]["slug"], "page-2")
|
|
162
|
-
self.assertEqual(pages[1]["slug"], "welcome-visitors")
|
|
163
|
-
|
|
164
|
-
pages = list(app.get_pages(desc=True, lte=2))
|
|
165
|
-
self.assertEqual(len(pages), 2)
|
|
166
|
-
self.assertEqual(pages[0]["slug"], "page-2")
|
|
167
|
-
self.assertEqual(pages[1]["slug"], "welcome-visitors")
|
|
168
|
-
|
|
169
|
-
# Check we can't change the slug of a page to one
|
|
170
|
-
# that is being used by another page.
|
|
171
|
-
with self.assertRaises(SlugConflictError):
|
|
172
|
-
app.update_slug("page-2", "page-3")
|
|
173
|
-
|
|
174
|
-
# Check we can change the slug of a page to one
|
|
175
|
-
# that was previously being used.
|
|
176
|
-
app.update_slug("welcome-visitors", "welcome")
|
|
177
|
-
|
|
178
|
-
page = app.get_page_by_slug(slug="welcome")
|
|
179
|
-
self.assertEqual(page["title"], "Welcome Visitors")
|
|
180
|
-
self.assertEqual(page["modified_by"], user_id)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from tempfile import TemporaryDirectory
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def create_diff(old: str, new: str) -> str:
|
|
8
|
-
return run("diff %s %s > %s", old, new)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def apply_patch(old: str, diff: str) -> str:
|
|
12
|
-
return run("patch -s %s %s -o %s", old, diff)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def run(cmd: str, a: str, b: str) -> str:
|
|
16
|
-
with TemporaryDirectory() as td:
|
|
17
|
-
a_path = os.path.join(td, "a")
|
|
18
|
-
b_path = os.path.join(td, "b")
|
|
19
|
-
c_path = os.path.join(td, "c")
|
|
20
|
-
with open(a_path, "w") as a_file:
|
|
21
|
-
a_file.write(a)
|
|
22
|
-
with open(b_path, "w") as b_file:
|
|
23
|
-
b_file.write(b)
|
|
24
|
-
os.system(cmd % (a_path, b_path, c_path)) # noqa: S605
|
|
25
|
-
with open(c_path) as c_file:
|
|
26
|
-
return c_file.read()
|
|
File without changes
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar, Dict, List, cast
|
|
4
|
-
|
|
5
|
-
from eventsourcing.examples.contentmanagement.domainmodel import Page
|
|
6
|
-
from eventsourcing.examples.contentmanagement.utils import apply_patch
|
|
7
|
-
from eventsourcing.examples.searchablecontent.persistence import (
|
|
8
|
-
SearchableContentRecorder,
|
|
9
|
-
)
|
|
10
|
-
from eventsourcing.system import ProcessApplication
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING: # pragma: nocover
|
|
13
|
-
from uuid import UUID
|
|
14
|
-
|
|
15
|
-
from eventsourcing.application import ProcessingEvent
|
|
16
|
-
from eventsourcing.domain import DomainEventProtocol
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SearchIndexApplication(ProcessApplication):
|
|
20
|
-
env: ClassVar[Dict[str, str]] = {
|
|
21
|
-
"COMPRESSOR_TOPIC": "gzip",
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
def policy(
|
|
25
|
-
self,
|
|
26
|
-
domain_event: DomainEventProtocol,
|
|
27
|
-
processing_event: ProcessingEvent,
|
|
28
|
-
) -> None:
|
|
29
|
-
if isinstance(domain_event, Page.Created):
|
|
30
|
-
processing_event.saved_kwargs["insert_pages"] = [
|
|
31
|
-
(
|
|
32
|
-
domain_event.originator_id,
|
|
33
|
-
domain_event.slug,
|
|
34
|
-
domain_event.title,
|
|
35
|
-
domain_event.body,
|
|
36
|
-
)
|
|
37
|
-
]
|
|
38
|
-
elif isinstance(domain_event, Page.BodyUpdated):
|
|
39
|
-
recorder = cast(SearchableContentRecorder, self.recorder)
|
|
40
|
-
page_id = domain_event.originator_id
|
|
41
|
-
page_slug, page_title, page_body = recorder.select_page(page_id)
|
|
42
|
-
page_body = apply_patch(page_body, domain_event.diff)
|
|
43
|
-
processing_event.saved_kwargs["update_pages"] = [
|
|
44
|
-
(
|
|
45
|
-
page_id,
|
|
46
|
-
page_slug,
|
|
47
|
-
page_title,
|
|
48
|
-
page_body,
|
|
49
|
-
)
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
def search(self, query: str) -> List[UUID]:
|
|
53
|
-
recorder = cast(SearchableContentRecorder, self.recorder)
|
|
54
|
-
return recorder.search_pages(query)
|