eventsourcing 9.3.2__py3-none-any.whl → 9.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eventsourcing might be problematic. Click here for more details.

Files changed (129) hide show
  1. eventsourcing/postgres.py +2 -1
  2. eventsourcing/system.py +3 -1
  3. {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/METADATA +3 -8
  4. eventsourcing-9.3.4.dist-info/RECORD +24 -0
  5. eventsourcing/examples/__init__.py +0 -0
  6. eventsourcing/examples/aggregate1/__init__.py +0 -0
  7. eventsourcing/examples/aggregate1/application.py +0 -27
  8. eventsourcing/examples/aggregate1/domainmodel.py +0 -16
  9. eventsourcing/examples/aggregate1/test_application.py +0 -37
  10. eventsourcing/examples/aggregate2/__init__.py +0 -0
  11. eventsourcing/examples/aggregate2/application.py +0 -27
  12. eventsourcing/examples/aggregate2/domainmodel.py +0 -22
  13. eventsourcing/examples/aggregate2/test_application.py +0 -37
  14. eventsourcing/examples/aggregate3/__init__.py +0 -0
  15. eventsourcing/examples/aggregate3/application.py +0 -27
  16. eventsourcing/examples/aggregate3/domainmodel.py +0 -38
  17. eventsourcing/examples/aggregate3/test_application.py +0 -37
  18. eventsourcing/examples/aggregate4/__init__.py +0 -0
  19. eventsourcing/examples/aggregate4/application.py +0 -27
  20. eventsourcing/examples/aggregate4/domainmodel.py +0 -114
  21. eventsourcing/examples/aggregate4/test_application.py +0 -38
  22. eventsourcing/examples/aggregate5/__init__.py +0 -0
  23. eventsourcing/examples/aggregate5/application.py +0 -27
  24. eventsourcing/examples/aggregate5/domainmodel.py +0 -131
  25. eventsourcing/examples/aggregate5/test_application.py +0 -38
  26. eventsourcing/examples/aggregate6/__init__.py +0 -0
  27. eventsourcing/examples/aggregate6/application.py +0 -30
  28. eventsourcing/examples/aggregate6/domainmodel.py +0 -123
  29. eventsourcing/examples/aggregate6/test_application.py +0 -38
  30. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  31. eventsourcing/examples/aggregate6a/application.py +0 -40
  32. eventsourcing/examples/aggregate6a/domainmodel.py +0 -149
  33. eventsourcing/examples/aggregate6a/test_application.py +0 -45
  34. eventsourcing/examples/aggregate7/__init__.py +0 -0
  35. eventsourcing/examples/aggregate7/application.py +0 -48
  36. eventsourcing/examples/aggregate7/domainmodel.py +0 -144
  37. eventsourcing/examples/aggregate7/persistence.py +0 -57
  38. eventsourcing/examples/aggregate7/test_application.py +0 -38
  39. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +0 -45
  40. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +0 -67
  41. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  42. eventsourcing/examples/aggregate7a/application.py +0 -56
  43. eventsourcing/examples/aggregate7a/domainmodel.py +0 -170
  44. eventsourcing/examples/aggregate7a/test_application.py +0 -46
  45. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +0 -45
  46. eventsourcing/examples/aggregate8/__init__.py +0 -0
  47. eventsourcing/examples/aggregate8/application.py +0 -47
  48. eventsourcing/examples/aggregate8/domainmodel.py +0 -65
  49. eventsourcing/examples/aggregate8/persistence.py +0 -57
  50. eventsourcing/examples/aggregate8/test_application.py +0 -37
  51. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +0 -44
  52. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +0 -38
  53. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  54. eventsourcing/examples/bankaccounts/application.py +0 -70
  55. eventsourcing/examples/bankaccounts/domainmodel.py +0 -56
  56. eventsourcing/examples/bankaccounts/test.py +0 -173
  57. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  58. eventsourcing/examples/cargoshipping/application.py +0 -126
  59. eventsourcing/examples/cargoshipping/domainmodel.py +0 -330
  60. eventsourcing/examples/cargoshipping/interface.py +0 -143
  61. eventsourcing/examples/cargoshipping/test.py +0 -231
  62. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  63. eventsourcing/examples/contentmanagement/application.py +0 -118
  64. eventsourcing/examples/contentmanagement/domainmodel.py +0 -69
  65. eventsourcing/examples/contentmanagement/test.py +0 -180
  66. eventsourcing/examples/contentmanagement/utils.py +0 -26
  67. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  68. eventsourcing/examples/contentmanagementsystem/application.py +0 -54
  69. eventsourcing/examples/contentmanagementsystem/postgres.py +0 -17
  70. eventsourcing/examples/contentmanagementsystem/sqlite.py +0 -17
  71. eventsourcing/examples/contentmanagementsystem/system.py +0 -14
  72. eventsourcing/examples/contentmanagementsystem/test_system.py +0 -180
  73. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  74. eventsourcing/examples/searchablecontent/application.py +0 -45
  75. eventsourcing/examples/searchablecontent/persistence.py +0 -23
  76. eventsourcing/examples/searchablecontent/postgres.py +0 -118
  77. eventsourcing/examples/searchablecontent/sqlite.py +0 -136
  78. eventsourcing/examples/searchablecontent/test_application.py +0 -110
  79. eventsourcing/examples/searchablecontent/test_recorder.py +0 -68
  80. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  81. eventsourcing/examples/searchabletimestamps/application.py +0 -32
  82. eventsourcing/examples/searchabletimestamps/persistence.py +0 -20
  83. eventsourcing/examples/searchabletimestamps/postgres.py +0 -110
  84. eventsourcing/examples/searchabletimestamps/sqlite.py +0 -99
  85. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +0 -94
  86. eventsourcing/examples/test_invoice.py +0 -176
  87. eventsourcing/examples/test_parking_lot.py +0 -206
  88. eventsourcing/tests/application_tests/__init__.py +0 -0
  89. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +0 -55
  90. eventsourcing/tests/application_tests/test_application_with_popo.py +0 -22
  91. eventsourcing/tests/application_tests/test_application_with_postgres.py +0 -75
  92. eventsourcing/tests/application_tests/test_application_with_sqlite.py +0 -72
  93. eventsourcing/tests/application_tests/test_cache.py +0 -134
  94. eventsourcing/tests/application_tests/test_event_sourced_log.py +0 -162
  95. eventsourcing/tests/application_tests/test_notificationlog.py +0 -232
  96. eventsourcing/tests/application_tests/test_notificationlogreader.py +0 -126
  97. eventsourcing/tests/application_tests/test_processapplication.py +0 -110
  98. eventsourcing/tests/application_tests/test_processingpolicy.py +0 -109
  99. eventsourcing/tests/application_tests/test_repository.py +0 -504
  100. eventsourcing/tests/application_tests/test_snapshotting.py +0 -68
  101. eventsourcing/tests/application_tests/test_upcasting.py +0 -459
  102. eventsourcing/tests/docs_tests/__init__.py +0 -0
  103. eventsourcing/tests/docs_tests/test_docs.py +0 -293
  104. eventsourcing/tests/domain_tests/__init__.py +0 -0
  105. eventsourcing/tests/domain_tests/test_aggregate.py +0 -1200
  106. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +0 -1604
  107. eventsourcing/tests/domain_tests/test_domainevent.py +0 -80
  108. eventsourcing/tests/interface_tests/__init__.py +0 -0
  109. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +0 -258
  110. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  111. eventsourcing/tests/persistence_tests/test_aes.py +0 -93
  112. eventsourcing/tests/persistence_tests/test_connection_pool.py +0 -722
  113. eventsourcing/tests/persistence_tests/test_eventstore.py +0 -72
  114. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +0 -21
  115. eventsourcing/tests/persistence_tests/test_mapper.py +0 -113
  116. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +0 -69
  117. eventsourcing/tests/persistence_tests/test_popo.py +0 -124
  118. eventsourcing/tests/persistence_tests/test_postgres.py +0 -1119
  119. eventsourcing/tests/persistence_tests/test_sqlite.py +0 -348
  120. eventsourcing/tests/persistence_tests/test_transcoder.py +0 -44
  121. eventsourcing/tests/system_tests/__init__.py +0 -0
  122. eventsourcing/tests/system_tests/test_runner.py +0 -935
  123. eventsourcing/tests/system_tests/test_system.py +0 -284
  124. eventsourcing/tests/utils_tests/__init__.py +0 -0
  125. eventsourcing/tests/utils_tests/test_utils.py +0 -226
  126. eventsourcing-9.3.2.dist-info/RECORD +0 -145
  127. {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/AUTHORS +0 -0
  128. {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.dist-info}/LICENSE +0 -0
  129. {eventsourcing-9.3.2.dist-info → eventsourcing-9.3.4.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()
@@ -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)