eventsourcing 9.2.22__py3-none-any.whl → 9.3.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/__init__.py +1 -1
- eventsourcing/application.py +106 -135
- eventsourcing/cipher.py +15 -12
- eventsourcing/dispatch.py +31 -91
- eventsourcing/domain.py +138 -143
- eventsourcing/examples/__init__.py +0 -0
- eventsourcing/examples/aggregate1/__init__.py +0 -0
- eventsourcing/examples/aggregate1/application.py +27 -0
- eventsourcing/examples/aggregate1/domainmodel.py +16 -0
- eventsourcing/examples/aggregate1/test_application.py +37 -0
- eventsourcing/examples/aggregate2/__init__.py +0 -0
- eventsourcing/examples/aggregate2/application.py +27 -0
- eventsourcing/examples/aggregate2/domainmodel.py +22 -0
- eventsourcing/examples/aggregate2/test_application.py +37 -0
- eventsourcing/examples/aggregate3/__init__.py +0 -0
- eventsourcing/examples/aggregate3/application.py +27 -0
- eventsourcing/examples/aggregate3/domainmodel.py +38 -0
- eventsourcing/examples/aggregate3/test_application.py +37 -0
- eventsourcing/examples/aggregate4/__init__.py +0 -0
- eventsourcing/examples/aggregate4/application.py +27 -0
- eventsourcing/examples/aggregate4/domainmodel.py +128 -0
- eventsourcing/examples/aggregate4/test_application.py +38 -0
- eventsourcing/examples/aggregate5/__init__.py +0 -0
- eventsourcing/examples/aggregate5/application.py +27 -0
- eventsourcing/examples/aggregate5/domainmodel.py +131 -0
- eventsourcing/examples/aggregate5/test_application.py +38 -0
- eventsourcing/examples/aggregate6/__init__.py +0 -0
- eventsourcing/examples/aggregate6/application.py +30 -0
- eventsourcing/examples/aggregate6/domainmodel.py +123 -0
- eventsourcing/examples/aggregate6/test_application.py +38 -0
- eventsourcing/examples/aggregate6a/__init__.py +0 -0
- eventsourcing/examples/aggregate6a/application.py +40 -0
- eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
- eventsourcing/examples/aggregate6a/test_application.py +45 -0
- eventsourcing/examples/aggregate7/__init__.py +0 -0
- eventsourcing/examples/aggregate7/application.py +48 -0
- eventsourcing/examples/aggregate7/domainmodel.py +144 -0
- eventsourcing/examples/aggregate7/persistence.py +57 -0
- eventsourcing/examples/aggregate7/test_application.py +38 -0
- eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
- eventsourcing/examples/aggregate7a/__init__.py +0 -0
- eventsourcing/examples/aggregate7a/application.py +56 -0
- eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
- eventsourcing/examples/aggregate7a/test_application.py +46 -0
- eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
- eventsourcing/examples/aggregate8/__init__.py +0 -0
- eventsourcing/examples/aggregate8/application.py +47 -0
- eventsourcing/examples/aggregate8/domainmodel.py +65 -0
- eventsourcing/examples/aggregate8/persistence.py +57 -0
- eventsourcing/examples/aggregate8/test_application.py +37 -0
- eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
- eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
- eventsourcing/examples/bankaccounts/__init__.py +0 -0
- eventsourcing/examples/bankaccounts/application.py +70 -0
- eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
- eventsourcing/examples/bankaccounts/test.py +173 -0
- eventsourcing/examples/cargoshipping/__init__.py +0 -0
- eventsourcing/examples/cargoshipping/application.py +126 -0
- eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
- eventsourcing/examples/cargoshipping/interface.py +143 -0
- eventsourcing/examples/cargoshipping/test.py +231 -0
- eventsourcing/examples/contentmanagement/__init__.py +0 -0
- eventsourcing/examples/contentmanagement/application.py +118 -0
- eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
- eventsourcing/examples/contentmanagement/test.py +180 -0
- eventsourcing/examples/contentmanagement/utils.py +26 -0
- eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
- eventsourcing/examples/contentmanagementsystem/application.py +54 -0
- eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
- eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
- eventsourcing/examples/contentmanagementsystem/system.py +14 -0
- eventsourcing/examples/contentmanagementsystem/test_system.py +174 -0
- eventsourcing/examples/searchablecontent/__init__.py +0 -0
- eventsourcing/examples/searchablecontent/application.py +45 -0
- eventsourcing/examples/searchablecontent/persistence.py +23 -0
- eventsourcing/examples/searchablecontent/postgres.py +118 -0
- eventsourcing/examples/searchablecontent/sqlite.py +136 -0
- eventsourcing/examples/searchablecontent/test_application.py +111 -0
- eventsourcing/examples/searchablecontent/test_recorder.py +69 -0
- eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
- eventsourcing/examples/searchabletimestamps/application.py +32 -0
- eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
- eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
- eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
- eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +91 -0
- eventsourcing/examples/test_invoice.py +176 -0
- eventsourcing/examples/test_parking_lot.py +206 -0
- eventsourcing/interface.py +2 -2
- eventsourcing/persistence.py +85 -81
- eventsourcing/popo.py +30 -31
- eventsourcing/postgres.py +361 -578
- eventsourcing/sqlite.py +91 -99
- eventsourcing/system.py +42 -57
- eventsourcing/tests/application.py +20 -32
- eventsourcing/tests/application_tests/__init__.py +0 -0
- eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
- eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
- eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
- eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
- eventsourcing/tests/application_tests/test_cache.py +134 -0
- eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
- eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
- eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
- eventsourcing/tests/application_tests/test_processapplication.py +110 -0
- eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
- eventsourcing/tests/application_tests/test_repository.py +504 -0
- eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
- eventsourcing/tests/application_tests/test_upcasting.py +459 -0
- eventsourcing/tests/docs_tests/__init__.py +0 -0
- eventsourcing/tests/docs_tests/test_docs.py +293 -0
- eventsourcing/tests/domain.py +1 -1
- eventsourcing/tests/domain_tests/__init__.py +0 -0
- eventsourcing/tests/domain_tests/test_aggregate.py +1159 -0
- eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
- eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
- eventsourcing/tests/interface_tests/__init__.py +0 -0
- eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
- eventsourcing/tests/persistence.py +49 -50
- eventsourcing/tests/persistence_tests/__init__.py +0 -0
- eventsourcing/tests/persistence_tests/test_aes.py +93 -0
- eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
- eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
- eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
- eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
- eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
- eventsourcing/tests/persistence_tests/test_popo.py +124 -0
- eventsourcing/tests/persistence_tests/test_postgres.py +1121 -0
- eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
- eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
- eventsourcing/tests/postgres_utils.py +7 -7
- eventsourcing/tests/system_tests/__init__.py +0 -0
- eventsourcing/tests/system_tests/test_runner.py +935 -0
- eventsourcing/tests/system_tests/test_system.py +287 -0
- eventsourcing/tests/utils_tests/__init__.py +0 -0
- eventsourcing/tests/utils_tests/test_utils.py +226 -0
- eventsourcing/utils.py +47 -50
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/METADATA +28 -80
- eventsourcing-9.3.0a1.dist-info/RECORD +144 -0
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/WHEEL +1 -2
- eventsourcing-9.2.22.dist-info/AUTHORS +0 -10
- eventsourcing-9.2.22.dist-info/RECORD +0 -25
- eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
- {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
"""
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
|
@@ -0,0 +1,180 @@
|
|
|
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)
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
|
@@ -0,0 +1,54 @@
|
|
|
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)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from eventsourcing.examples.searchablecontent.postgres import (
|
|
2
|
+
PostgresSearchableContentRecorder,
|
|
3
|
+
)
|
|
4
|
+
from eventsourcing.postgres import Factory, PostgresProcessRecorder
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SearchableContentProcessRecorder(
|
|
8
|
+
PostgresSearchableContentRecorder, PostgresProcessRecorder
|
|
9
|
+
):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SearchableContentInfrastructureFactory(Factory):
|
|
14
|
+
process_recorder_class = SearchableContentProcessRecorder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
del Factory
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from eventsourcing.examples.searchablecontent.sqlite import (
|
|
2
|
+
SQLiteSearchableContentRecorder,
|
|
3
|
+
)
|
|
4
|
+
from eventsourcing.sqlite import Factory, SQLiteProcessRecorder
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SearchableContentProcessRecorder(
|
|
8
|
+
SQLiteSearchableContentRecorder, SQLiteProcessRecorder
|
|
9
|
+
):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SearchableContentInfrastructureFactory(Factory):
|
|
14
|
+
process_recorder_class = SearchableContentProcessRecorder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
del Factory
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from eventsourcing.examples.contentmanagement.application import (
|
|
4
|
+
ContentManagementApplication,
|
|
5
|
+
)
|
|
6
|
+
from eventsourcing.examples.contentmanagementsystem.application import (
|
|
7
|
+
SearchIndexApplication,
|
|
8
|
+
)
|
|
9
|
+
from eventsourcing.system import System
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ContentManagementSystem(System):
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
super().__init__(pipes=[[ContentManagementApplication, SearchIndexApplication]])
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, Dict
|
|
4
|
+
from unittest import TestCase
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from eventsourcing.examples.contentmanagement.application import (
|
|
8
|
+
ContentManagementApplication,
|
|
9
|
+
)
|
|
10
|
+
from eventsourcing.examples.contentmanagement.domainmodel import user_id_cvar
|
|
11
|
+
from eventsourcing.examples.contentmanagementsystem.application import (
|
|
12
|
+
SearchIndexApplication,
|
|
13
|
+
)
|
|
14
|
+
from eventsourcing.examples.contentmanagementsystem.system import (
|
|
15
|
+
ContentManagementSystem,
|
|
16
|
+
)
|
|
17
|
+
from eventsourcing.postgres import PostgresDatastore
|
|
18
|
+
from eventsourcing.system import SingleThreadedRunner
|
|
19
|
+
from eventsourcing.tests.postgres_utils import drop_postgres_table
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContentManagementSystemTestCase(TestCase):
|
|
23
|
+
env: ClassVar[Dict[str, str]] = {}
|
|
24
|
+
|
|
25
|
+
def test_system(self) -> None:
|
|
26
|
+
runner = SingleThreadedRunner(system=ContentManagementSystem(), env=self.env)
|
|
27
|
+
runner.start()
|
|
28
|
+
|
|
29
|
+
content_management_app = runner.get(ContentManagementApplication)
|
|
30
|
+
search_index_app = runner.get(SearchIndexApplication)
|
|
31
|
+
|
|
32
|
+
# Set user_id context variable.
|
|
33
|
+
user_id = uuid4()
|
|
34
|
+
user_id_cvar.set(user_id)
|
|
35
|
+
|
|
36
|
+
# Create empty pages.
|
|
37
|
+
content_management_app.create_page(title="Animals", slug="animals")
|
|
38
|
+
content_management_app.create_page(title="Plants", slug="plants")
|
|
39
|
+
content_management_app.create_page(title="Minerals", slug="minerals")
|
|
40
|
+
|
|
41
|
+
# Search, expect no results.
|
|
42
|
+
self.assertEqual(0, len(search_index_app.search("cat")))
|
|
43
|
+
self.assertEqual(0, len(search_index_app.search("rose")))
|
|
44
|
+
self.assertEqual(0, len(search_index_app.search("calcium")))
|
|
45
|
+
|
|
46
|
+
# Update the pages.
|
|
47
|
+
content_management_app.update_body(slug="animals", body="cat")
|
|
48
|
+
content_management_app.update_body(slug="plants", body="rose")
|
|
49
|
+
content_management_app.update_body(slug="minerals", body="calcium")
|
|
50
|
+
|
|
51
|
+
# Search for single words.
|
|
52
|
+
page_ids = search_index_app.search("cat")
|
|
53
|
+
self.assertEqual(1, len(page_ids))
|
|
54
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
55
|
+
self.assertEqual(page["slug"], "animals")
|
|
56
|
+
self.assertEqual(page["body"], "cat")
|
|
57
|
+
|
|
58
|
+
page_ids = search_index_app.search("rose")
|
|
59
|
+
self.assertEqual(1, len(page_ids))
|
|
60
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
61
|
+
self.assertEqual(page["slug"], "plants")
|
|
62
|
+
self.assertEqual(page["body"], "rose")
|
|
63
|
+
|
|
64
|
+
page_ids = search_index_app.search("calcium")
|
|
65
|
+
self.assertEqual(1, len(page_ids))
|
|
66
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
67
|
+
self.assertEqual(page["slug"], "minerals")
|
|
68
|
+
self.assertEqual(page["body"], "calcium")
|
|
69
|
+
|
|
70
|
+
self.assertEqual(len(search_index_app.search("dog")), 0)
|
|
71
|
+
self.assertEqual(len(search_index_app.search("bluebell")), 0)
|
|
72
|
+
self.assertEqual(len(search_index_app.search("zinc")), 0)
|
|
73
|
+
|
|
74
|
+
# Update the pages again.
|
|
75
|
+
content_management_app.update_body(slug="animals", body="cat dog zebra")
|
|
76
|
+
content_management_app.update_body(slug="plants", body="bluebell rose jasmine")
|
|
77
|
+
content_management_app.update_body(slug="minerals", body="iron zinc calcium")
|
|
78
|
+
|
|
79
|
+
# Search for single words.
|
|
80
|
+
page_ids = search_index_app.search("cat")
|
|
81
|
+
self.assertEqual(1, len(page_ids))
|
|
82
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
83
|
+
self.assertEqual(page["slug"], "animals")
|
|
84
|
+
self.assertEqual(page["body"], "cat dog zebra")
|
|
85
|
+
|
|
86
|
+
page_ids = search_index_app.search("rose")
|
|
87
|
+
self.assertEqual(1, len(page_ids))
|
|
88
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
89
|
+
self.assertEqual(page["slug"], "plants")
|
|
90
|
+
self.assertEqual(page["body"], "bluebell rose jasmine")
|
|
91
|
+
|
|
92
|
+
page_ids = search_index_app.search("calcium")
|
|
93
|
+
self.assertEqual(1, len(page_ids))
|
|
94
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
95
|
+
self.assertEqual(page["slug"], "minerals")
|
|
96
|
+
self.assertEqual(page["body"], "iron zinc calcium")
|
|
97
|
+
|
|
98
|
+
page_ids = search_index_app.search("dog")
|
|
99
|
+
self.assertEqual(1, len(page_ids))
|
|
100
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
101
|
+
self.assertEqual(page["slug"], "animals")
|
|
102
|
+
self.assertEqual(page["body"], "cat dog zebra")
|
|
103
|
+
|
|
104
|
+
page_ids = search_index_app.search("bluebell")
|
|
105
|
+
self.assertEqual(1, len(page_ids))
|
|
106
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
107
|
+
self.assertEqual(page["slug"], "plants")
|
|
108
|
+
self.assertEqual(page["body"], "bluebell rose jasmine")
|
|
109
|
+
|
|
110
|
+
page_ids = search_index_app.search("zinc")
|
|
111
|
+
self.assertEqual(1, len(page_ids))
|
|
112
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
113
|
+
self.assertEqual(page["slug"], "minerals")
|
|
114
|
+
self.assertEqual(page["body"], "iron zinc calcium")
|
|
115
|
+
|
|
116
|
+
# Search for multiple words in same page.
|
|
117
|
+
page_ids = search_index_app.search("dog cat")
|
|
118
|
+
self.assertEqual(1, len(page_ids))
|
|
119
|
+
page = content_management_app.get_page_by_id(page_ids[0])
|
|
120
|
+
self.assertEqual(page["slug"], "animals")
|
|
121
|
+
self.assertEqual(page["body"], "cat dog zebra")
|
|
122
|
+
|
|
123
|
+
# Search for multiple words in same page, expect no results.
|
|
124
|
+
page_ids = search_index_app.search("rose zebra")
|
|
125
|
+
self.assertEqual(0, len(page_ids))
|
|
126
|
+
|
|
127
|
+
# Search for alternative words, expect two results.
|
|
128
|
+
page_ids = search_index_app.search("rose OR zebra")
|
|
129
|
+
pages = [content_management_app.get_page_by_id(page_id) for page_id in page_ids]
|
|
130
|
+
self.assertEqual(2, len(pages))
|
|
131
|
+
self.assertEqual(["animals", "plants"], sorted(p["slug"] for p in pages))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestWithSQLite(ContentManagementSystemTestCase):
|
|
135
|
+
env: ClassVar[Dict[str, str]] = {
|
|
136
|
+
"PERSISTENCE_MODULE": "eventsourcing.examples.contentmanagementsystem.sqlite",
|
|
137
|
+
"SQLITE_DBNAME": ":memory:",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestWithPostgres(ContentManagementSystemTestCase):
|
|
142
|
+
env: ClassVar[Dict[str, str]] = {
|
|
143
|
+
"PERSISTENCE_MODULE": "eventsourcing.examples.contentmanagementsystem.postgres",
|
|
144
|
+
"POSTGRES_DBNAME": "eventsourcing",
|
|
145
|
+
"POSTGRES_HOST": "127.0.0.1",
|
|
146
|
+
"POSTGRES_PORT": "5432",
|
|
147
|
+
"POSTGRES_USER": "eventsourcing",
|
|
148
|
+
"POSTGRES_PASSWORD": "eventsourcing",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def setUp(self) -> None:
|
|
152
|
+
super().setUp()
|
|
153
|
+
self.drop_tables()
|
|
154
|
+
|
|
155
|
+
def tearDown(self) -> None:
|
|
156
|
+
self.drop_tables()
|
|
157
|
+
super().tearDown()
|
|
158
|
+
|
|
159
|
+
def drop_tables(self) -> None:
|
|
160
|
+
db = PostgresDatastore(
|
|
161
|
+
self.env["POSTGRES_DBNAME"],
|
|
162
|
+
self.env["POSTGRES_HOST"],
|
|
163
|
+
self.env["POSTGRES_PORT"],
|
|
164
|
+
self.env["POSTGRES_USER"],
|
|
165
|
+
self.env["POSTGRES_PASSWORD"],
|
|
166
|
+
)
|
|
167
|
+
drop_postgres_table(db, "public.contentmanagementapplication_events")
|
|
168
|
+
drop_postgres_table(db, "public.pages_projection_example")
|
|
169
|
+
drop_postgres_table(db, "public.searchindexapplication_events")
|
|
170
|
+
drop_postgres_table(db, "public.searchindexapplication_tracking")
|
|
171
|
+
db.close()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
del ContentManagementSystemTestCase
|
|
File without changes
|