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,17 +0,0 @@
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
@@ -1,17 +0,0 @@
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
@@ -1,14 +0,0 @@
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]])
@@ -1,180 +0,0 @@
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
- with SingleThreadedRunner(
27
- system=ContentManagementSystem(), env=self.env
28
- ) as runner:
29
-
30
- content_management_app = runner.get(ContentManagementApplication)
31
- search_index_app = runner.get(SearchIndexApplication)
32
-
33
- # Set user_id context variable.
34
- user_id = uuid4()
35
- user_id_cvar.set(user_id)
36
-
37
- # Create empty pages.
38
- content_management_app.create_page(title="Animals", slug="animals")
39
- content_management_app.create_page(title="Plants", slug="plants")
40
- content_management_app.create_page(title="Minerals", slug="minerals")
41
-
42
- # Search, expect no results.
43
- self.assertEqual(0, len(search_index_app.search("cat")))
44
- self.assertEqual(0, len(search_index_app.search("rose")))
45
- self.assertEqual(0, len(search_index_app.search("calcium")))
46
-
47
- # Update the pages.
48
- content_management_app.update_body(slug="animals", body="cat")
49
- content_management_app.update_body(slug="plants", body="rose")
50
- content_management_app.update_body(slug="minerals", body="calcium")
51
-
52
- # Search for single words.
53
- page_ids = search_index_app.search("cat")
54
- self.assertEqual(1, len(page_ids))
55
- page = content_management_app.get_page_by_id(page_ids[0])
56
- self.assertEqual(page["slug"], "animals")
57
- self.assertEqual(page["body"], "cat")
58
-
59
- page_ids = search_index_app.search("rose")
60
- self.assertEqual(1, len(page_ids))
61
- page = content_management_app.get_page_by_id(page_ids[0])
62
- self.assertEqual(page["slug"], "plants")
63
- self.assertEqual(page["body"], "rose")
64
-
65
- page_ids = search_index_app.search("calcium")
66
- self.assertEqual(1, len(page_ids))
67
- page = content_management_app.get_page_by_id(page_ids[0])
68
- self.assertEqual(page["slug"], "minerals")
69
- self.assertEqual(page["body"], "calcium")
70
-
71
- self.assertEqual(len(search_index_app.search("dog")), 0)
72
- self.assertEqual(len(search_index_app.search("bluebell")), 0)
73
- self.assertEqual(len(search_index_app.search("zinc")), 0)
74
-
75
- # Update the pages again.
76
- content_management_app.update_body(slug="animals", body="cat dog zebra")
77
- content_management_app.update_body(
78
- slug="plants", body="bluebell rose jasmine"
79
- )
80
- content_management_app.update_body(
81
- slug="minerals", body="iron zinc calcium"
82
- )
83
-
84
- # Search for single words.
85
- page_ids = search_index_app.search("cat")
86
- self.assertEqual(1, len(page_ids))
87
- page = content_management_app.get_page_by_id(page_ids[0])
88
- self.assertEqual(page["slug"], "animals")
89
- self.assertEqual(page["body"], "cat dog zebra")
90
-
91
- page_ids = search_index_app.search("rose")
92
- self.assertEqual(1, len(page_ids))
93
- page = content_management_app.get_page_by_id(page_ids[0])
94
- self.assertEqual(page["slug"], "plants")
95
- self.assertEqual(page["body"], "bluebell rose jasmine")
96
-
97
- page_ids = search_index_app.search("calcium")
98
- self.assertEqual(1, len(page_ids))
99
- page = content_management_app.get_page_by_id(page_ids[0])
100
- self.assertEqual(page["slug"], "minerals")
101
- self.assertEqual(page["body"], "iron zinc calcium")
102
-
103
- page_ids = search_index_app.search("dog")
104
- self.assertEqual(1, len(page_ids))
105
- page = content_management_app.get_page_by_id(page_ids[0])
106
- self.assertEqual(page["slug"], "animals")
107
- self.assertEqual(page["body"], "cat dog zebra")
108
-
109
- page_ids = search_index_app.search("bluebell")
110
- self.assertEqual(1, len(page_ids))
111
- page = content_management_app.get_page_by_id(page_ids[0])
112
- self.assertEqual(page["slug"], "plants")
113
- self.assertEqual(page["body"], "bluebell rose jasmine")
114
-
115
- page_ids = search_index_app.search("zinc")
116
- self.assertEqual(1, len(page_ids))
117
- page = content_management_app.get_page_by_id(page_ids[0])
118
- self.assertEqual(page["slug"], "minerals")
119
- self.assertEqual(page["body"], "iron zinc calcium")
120
-
121
- # Search for multiple words in same page.
122
- page_ids = search_index_app.search("dog cat")
123
- self.assertEqual(1, len(page_ids))
124
- page = content_management_app.get_page_by_id(page_ids[0])
125
- self.assertEqual(page["slug"], "animals")
126
- self.assertEqual(page["body"], "cat dog zebra")
127
-
128
- # Search for multiple words in same page, expect no results.
129
- page_ids = search_index_app.search("rose zebra")
130
- self.assertEqual(0, len(page_ids))
131
-
132
- # Search for alternative words, expect two results.
133
- page_ids = search_index_app.search("rose OR zebra")
134
- pages = [
135
- content_management_app.get_page_by_id(page_id) for page_id in page_ids
136
- ]
137
- self.assertEqual(2, len(pages))
138
- self.assertEqual(["animals", "plants"], sorted(p["slug"] for p in pages))
139
-
140
-
141
- class TestWithSQLite(ContentManagementSystemTestCase):
142
- env: ClassVar[Dict[str, str]] = {
143
- "PERSISTENCE_MODULE": "eventsourcing.examples.contentmanagementsystem.sqlite",
144
- "SQLITE_DBNAME": ":memory:",
145
- }
146
-
147
-
148
- class TestWithPostgres(ContentManagementSystemTestCase):
149
- env: ClassVar[Dict[str, str]] = {
150
- "PERSISTENCE_MODULE": "eventsourcing.examples.contentmanagementsystem.postgres",
151
- "POSTGRES_DBNAME": "eventsourcing",
152
- "POSTGRES_HOST": "127.0.0.1",
153
- "POSTGRES_PORT": "5432",
154
- "POSTGRES_USER": "eventsourcing",
155
- "POSTGRES_PASSWORD": "eventsourcing",
156
- }
157
-
158
- def setUp(self) -> None:
159
- super().setUp()
160
- self.drop_tables()
161
-
162
- def tearDown(self) -> None:
163
- self.drop_tables()
164
- super().tearDown()
165
-
166
- def drop_tables(self) -> None:
167
- with PostgresDatastore(
168
- self.env["POSTGRES_DBNAME"],
169
- self.env["POSTGRES_HOST"],
170
- self.env["POSTGRES_PORT"],
171
- self.env["POSTGRES_USER"],
172
- self.env["POSTGRES_PASSWORD"],
173
- ) as datastore:
174
- drop_postgres_table(datastore, "public.contentmanagementapplication_events")
175
- drop_postgres_table(datastore, "public.pages_projection_example")
176
- drop_postgres_table(datastore, "public.searchindexapplication_events")
177
- drop_postgres_table(datastore, "public.searchindexapplication_tracking")
178
-
179
-
180
- del ContentManagementSystemTestCase
File without changes
@@ -1,45 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any, List, Tuple, cast
4
-
5
- from eventsourcing.examples.contentmanagement.application import (
6
- ContentManagementApplication,
7
- PageDetailsType,
8
- )
9
- from eventsourcing.examples.contentmanagement.domainmodel import Page
10
- from eventsourcing.examples.searchablecontent.persistence import (
11
- SearchableContentRecorder,
12
- )
13
-
14
- if TYPE_CHECKING: # pragma: nocover
15
- from uuid import UUID
16
-
17
- from eventsourcing.domain import DomainEventProtocol, MutableOrImmutableAggregate
18
- from eventsourcing.persistence import Recording
19
-
20
-
21
- class SearchableContentApplication(ContentManagementApplication):
22
- def save(
23
- self,
24
- *objs: MutableOrImmutableAggregate | DomainEventProtocol | None,
25
- **kwargs: Any,
26
- ) -> List[Recording]:
27
- insert_pages: List[Tuple[UUID, str, str, str]] = []
28
- update_pages: List[Tuple[UUID, str, str, str]] = []
29
- for obj in objs:
30
- if isinstance(obj, Page):
31
- if obj.version == len(obj.pending_events):
32
- insert_pages.append((obj.id, obj.slug, obj.title, obj.body))
33
- else:
34
- update_pages.append((obj.id, obj.slug, obj.title, obj.body))
35
- kwargs["insert_pages"] = insert_pages
36
- kwargs["update_pages"] = update_pages
37
- return super().save(*objs, **kwargs)
38
-
39
- def search(self, query: str) -> List[PageDetailsType]:
40
- pages = []
41
- recorder = cast(SearchableContentRecorder, self.recorder)
42
- for page_id in recorder.search_pages(query):
43
- page = self.get_page_by_id(page_id)
44
- pages.append(page)
45
- return pages
@@ -1,23 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import abstractmethod
4
- from typing import TYPE_CHECKING, List, Tuple
5
-
6
- from eventsourcing.persistence import AggregateRecorder
7
-
8
- if TYPE_CHECKING: # pragma: nocover
9
- from uuid import UUID
10
-
11
-
12
- class SearchableContentRecorder(AggregateRecorder):
13
- @abstractmethod
14
- def search_pages(self, query: str) -> List[UUID]:
15
- """
16
- Returns IDs for pages that match query.
17
- """
18
-
19
- @abstractmethod
20
- def select_page(self, page_id: UUID) -> Tuple[str, str, str]:
21
- """
22
- Returns slug, title and body for given ID.
23
- """
@@ -1,118 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any, List, Sequence, Tuple
4
-
5
- from eventsourcing.examples.contentmanagement.application import PageNotFoundError
6
- from eventsourcing.examples.searchablecontent.persistence import (
7
- SearchableContentRecorder,
8
- )
9
- from eventsourcing.postgres import (
10
- Factory,
11
- PostgresAggregateRecorder,
12
- PostgresApplicationRecorder,
13
- )
14
-
15
- if TYPE_CHECKING: # pragma: nocover
16
- from uuid import UUID
17
-
18
- from psycopg import Cursor
19
- from psycopg.rows import DictRow
20
-
21
- from eventsourcing.persistence import StoredEvent
22
-
23
-
24
- class PostgresSearchableContentRecorder(
25
- SearchableContentRecorder,
26
- PostgresAggregateRecorder,
27
- ):
28
- pages_table_name = "pages_projection_example"
29
- select_page_statement = (
30
- f"SELECT page_slug, page_title, page_body FROM {pages_table_name}"
31
- " WHERE page_id = %s"
32
- )
33
-
34
- insert_page_statement = f"INSERT INTO {pages_table_name} VALUES (%s, %s, %s, %s)"
35
-
36
- update_page_statement = (
37
- f"UPDATE {pages_table_name}"
38
- " SET page_slug = %s, page_title = %s, page_body = %s WHERE page_id = %s"
39
- )
40
-
41
- search_pages_statement = (
42
- f"SELECT page_id FROM {pages_table_name} WHERE"
43
- " to_tsvector('english', page_body) @@ websearch_to_tsquery('english', %s)"
44
- )
45
-
46
- def construct_create_table_statements(self) -> List[str]:
47
- statements = super().construct_create_table_statements()
48
- statements.append(
49
- "CREATE TABLE IF NOT EXISTS "
50
- f"{self.pages_table_name} ("
51
- "page_id uuid, "
52
- "page_slug text, "
53
- "page_title text, "
54
- "page_body text, "
55
- "PRIMARY KEY "
56
- "(page_id))"
57
- )
58
- statements.append(
59
- f"CREATE INDEX IF NOT EXISTS {self.pages_table_name}_idx "
60
- f"ON {self.pages_table_name} "
61
- "USING GIN (to_tsvector('english', page_body))"
62
- )
63
- return statements
64
-
65
- def _insert_events(
66
- self,
67
- c: Cursor[DictRow],
68
- stored_events: List[StoredEvent],
69
- **kwargs: Any,
70
- ) -> None:
71
- self._insert_pages(c, **kwargs)
72
- self._update_pages(c, **kwargs)
73
- super()._insert_events(c, stored_events, **kwargs)
74
-
75
- def _insert_pages(
76
- self,
77
- c: Cursor[DictRow],
78
- insert_pages: Sequence[Tuple[UUID, str, str, str]] = (),
79
- **_: Any,
80
- ) -> None:
81
- for params in insert_pages:
82
- c.execute(self.insert_page_statement, params, prepare=True)
83
-
84
- def _update_pages(
85
- self,
86
- c: Cursor[DictRow],
87
- update_pages: Sequence[Tuple[UUID, str, str, str]] = (),
88
- **_: Any,
89
- ) -> None:
90
- for page_id, page_slug, page_title, page_body in update_pages:
91
- params = (page_slug, page_title, page_body, page_id)
92
- c.execute(self.update_page_statement, params, prepare=True)
93
-
94
- def search_pages(self, query: str) -> List[UUID]:
95
- with self.datastore.transaction(commit=False) as curs:
96
- curs.execute(self.search_pages_statement, [query], prepare=True)
97
- return [row["page_id"] for row in curs.fetchall()]
98
-
99
- def select_page(self, page_id: UUID) -> Tuple[str, str, str]:
100
- with self.datastore.transaction(commit=False) as curs:
101
- curs.execute(self.select_page_statement, [str(page_id)], prepare=True)
102
- for row in curs.fetchall():
103
- return row["page_slug"], row["page_title"], row["page_body"]
104
- msg = f"Page ID {page_id} not found"
105
- raise PageNotFoundError(msg)
106
-
107
-
108
- class SearchableContentApplicationRecorder(
109
- PostgresSearchableContentRecorder, PostgresApplicationRecorder
110
- ):
111
- pass
112
-
113
-
114
- class SearchableContentInfrastructureFactory(Factory):
115
- application_recorder_class = SearchableContentApplicationRecorder
116
-
117
-
118
- del Factory
@@ -1,136 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any, List, Sequence, Tuple
4
- from uuid import UUID
5
-
6
- from eventsourcing.examples.contentmanagement.application import PageNotFoundError
7
- from eventsourcing.examples.searchablecontent.persistence import (
8
- SearchableContentRecorder,
9
- )
10
- from eventsourcing.sqlite import (
11
- Factory,
12
- SQLiteAggregateRecorder,
13
- SQLiteApplicationRecorder,
14
- SQLiteCursor,
15
- )
16
-
17
- if TYPE_CHECKING: # pragma: nocover
18
- from eventsourcing.persistence import StoredEvent
19
-
20
-
21
- class SQLiteSearchableContentRecorder(
22
- SearchableContentRecorder, SQLiteAggregateRecorder
23
- ):
24
- pages_table_name = "pages_projection_example"
25
- pages_virtual_table_name = pages_table_name + "_fts"
26
- select_page_statement = (
27
- "SELECT page_slug, page_title, page_body FROM "
28
- f"{pages_table_name} WHERE page_id = ?"
29
- )
30
- insert_page_statement = f"INSERT INTO {pages_table_name} VALUES (?, ?, ?, ?)"
31
- update_page_statement = (
32
- f"UPDATE {pages_table_name} "
33
- "SET page_slug = ?, page_title = ?, page_body = ? WHERE page_id = ?"
34
- )
35
- search_pages_statement = (
36
- f"SELECT page_id FROM {pages_virtual_table_name} WHERE page_body MATCH ?"
37
- )
38
-
39
- def construct_create_table_statements(self) -> List[str]:
40
- statements = super().construct_create_table_statements()
41
- statements.append(
42
- "CREATE TABLE IF NOT EXISTS "
43
- f"{self.pages_table_name} ("
44
- "page_id TEXT, "
45
- "page_slug TEXT, "
46
- "page_title TEXT, "
47
- "page_body TEXT, "
48
- "PRIMARY KEY "
49
- "(page_id)) "
50
- )
51
- statements.append(
52
- f"CREATE VIRTUAL TABLE {self.pages_virtual_table_name} USING fts5("
53
- f"page_id, page_body, content='{self.pages_table_name}')"
54
- )
55
- statements.append(
56
- "CREATE TRIGGER projection_ai AFTER INSERT ON "
57
- f"{self.pages_table_name} BEGIN "
58
- f"INSERT INTO {self.pages_virtual_table_name} "
59
- "(rowid, page_id, page_body) "
60
- "VALUES (new.rowid, new.page_id, new.page_body); "
61
- "END"
62
- )
63
- statements.append(
64
- "CREATE TRIGGER projection_au AFTER UPDATE ON "
65
- f"{self.pages_table_name} "
66
- "BEGIN "
67
- f"INSERT INTO {self.pages_virtual_table_name} "
68
- f"({self.pages_virtual_table_name}, rowid, page_id, page_body) "
69
- "VALUES ('delete', old.rowid, old.page_id, old.page_body);"
70
- f"INSERT INTO {self.pages_virtual_table_name} "
71
- "(rowid, page_id, page_body) "
72
- "VALUES (new.rowid, new.page_id, new.page_body); "
73
- "END"
74
- )
75
- return statements
76
-
77
- def _insert_events(
78
- self,
79
- c: SQLiteCursor,
80
- stored_events: List[StoredEvent],
81
- **kwargs: Any,
82
- ) -> Sequence[int] | None:
83
- notification_ids = super()._insert_events(c, stored_events, **kwargs)
84
- self._insert_pages(c, **kwargs)
85
- self._update_pages(c, **kwargs)
86
- return notification_ids
87
-
88
- def _insert_pages(
89
- self,
90
- c: SQLiteCursor,
91
- insert_pages: Sequence[Tuple[UUID, str, str, str]] = (),
92
- **_: Any,
93
- ) -> None:
94
- for page_id, page_slug, page_title, page_body in insert_pages:
95
- c.execute(
96
- self.insert_page_statement,
97
- (str(page_id), page_slug, page_title, page_body),
98
- )
99
-
100
- def _update_pages(
101
- self,
102
- c: SQLiteCursor,
103
- update_pages: Sequence[Tuple[UUID, str, str, str]] = (),
104
- **_: Any,
105
- ) -> None:
106
- for page_id, page_slug, page_title, page_body in update_pages:
107
- c.execute(
108
- self.update_page_statement,
109
- (page_slug, page_title, page_body, str(page_id)),
110
- )
111
-
112
- def search_pages(self, query: str) -> List[UUID]:
113
- with self.datastore.transaction(commit=False) as c:
114
- c.execute(self.search_pages_statement, [query])
115
- return [UUID(row["page_id"]) for row in c.fetchall()]
116
-
117
- def select_page(self, page_id: UUID) -> Tuple[str, str, str]:
118
- with self.datastore.transaction(commit=False) as c:
119
- c.execute(self.select_page_statement, [str(page_id)])
120
- for row in c.fetchall():
121
- return row["page_slug"], row["page_title"], row["page_body"]
122
- msg = f"Page ID {page_id} not found"
123
- raise PageNotFoundError(msg)
124
-
125
-
126
- class SearchableContentApplicationRecorder(
127
- SQLiteSearchableContentRecorder, SQLiteApplicationRecorder
128
- ):
129
- pass
130
-
131
-
132
- class SearchableContentInfrastructureFactory(Factory):
133
- application_recorder_class = SearchableContentApplicationRecorder
134
-
135
-
136
- del Factory
@@ -1,110 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- from typing import ClassVar, Dict
5
- from unittest import TestCase
6
- from uuid import uuid4
7
-
8
- from eventsourcing.examples.contentmanagement.domainmodel import user_id_cvar
9
- from eventsourcing.examples.searchablecontent.application import (
10
- SearchableContentApplication,
11
- )
12
- from eventsourcing.postgres import PostgresDatastore
13
- from eventsourcing.tests.postgres_utils import drop_postgres_table
14
-
15
-
16
- class SearchableContentApplicationTestCase(TestCase):
17
- env: ClassVar[Dict[str, str]] = {}
18
-
19
- def test_app(self) -> None:
20
- app = SearchableContentApplication(env=self.env)
21
-
22
- # Set user_id context variable.
23
- user_id = uuid4()
24
- user_id_cvar.set(user_id)
25
-
26
- # Create empty pages.
27
- app.create_page(title="Animals", slug="animals")
28
- app.create_page(title="Plants", slug="plants")
29
- app.create_page(title="Minerals", slug="minerals")
30
-
31
- # Search, expect no results.
32
- self.assertEqual(0, len(app.search("dog")))
33
- self.assertEqual(0, len(app.search("rose")))
34
- self.assertEqual(0, len(app.search("zinc")))
35
-
36
- # Update the pages.
37
- app.update_body(slug="animals", body="cat dog zebra")
38
- app.update_body(slug="plants", body="bluebell rose jasmine")
39
- app.update_body(slug="minerals", body="iron zinc calcium")
40
-
41
- # Search for single words, expect results.
42
- pages = app.search("dog")
43
- self.assertEqual(1, len(pages))
44
- self.assertEqual(pages[0]["slug"], "animals")
45
- self.assertEqual(pages[0]["body"], "cat dog zebra")
46
-
47
- pages = app.search("rose")
48
- self.assertEqual(1, len(pages))
49
- self.assertEqual(pages[0]["slug"], "plants")
50
- self.assertEqual(pages[0]["body"], "bluebell rose jasmine")
51
-
52
- pages = app.search("zinc")
53
- self.assertEqual(1, len(pages))
54
- self.assertEqual(pages[0]["slug"], "minerals")
55
- self.assertEqual(pages[0]["body"], "iron zinc calcium")
56
-
57
- # Search for multiple words in same page.
58
- pages = app.search("dog cat")
59
- self.assertEqual(1, len(pages))
60
- self.assertEqual(pages[0]["slug"], "animals")
61
- self.assertEqual(pages[0]["body"], "cat dog zebra")
62
-
63
- # Search for multiple words in same page, expect no results.
64
- pages = app.search("rose zebra")
65
- self.assertEqual(0, len(pages))
66
-
67
- # Search for alternative words, expect two results.
68
- pages = app.search("rose OR zebra")
69
- self.assertEqual(2, len(pages))
70
- self.assertEqual(["animals", "plants"], sorted(p["slug"] for p in pages))
71
-
72
-
73
- class TestWithSQLite(SearchableContentApplicationTestCase):
74
- env: ClassVar[Dict[str, str]] = {
75
- "PERSISTENCE_MODULE": "eventsourcing.examples.searchablecontent.sqlite",
76
- "SQLITE_DBNAME": ":memory:",
77
- }
78
-
79
-
80
- class TestWithPostgres(SearchableContentApplicationTestCase):
81
- env: ClassVar[Dict[str, str]] = {
82
- "PERSISTENCE_MODULE": "eventsourcing.examples.searchablecontent.postgres"
83
- }
84
-
85
- def setUp(self) -> None:
86
- super().setUp()
87
- os.environ["POSTGRES_DBNAME"] = "eventsourcing"
88
- os.environ["POSTGRES_HOST"] = "127.0.0.1"
89
- os.environ["POSTGRES_PORT"] = "5432"
90
- os.environ["POSTGRES_USER"] = "eventsourcing"
91
- os.environ["POSTGRES_PASSWORD"] = "eventsourcing" # noqa: S105
92
- self.drop_tables()
93
-
94
- def tearDown(self) -> None:
95
- self.drop_tables()
96
- super().tearDown()
97
-
98
- def drop_tables(self) -> None:
99
- with PostgresDatastore(
100
- os.environ["POSTGRES_DBNAME"],
101
- os.environ["POSTGRES_HOST"],
102
- os.environ["POSTGRES_PORT"],
103
- os.environ["POSTGRES_USER"],
104
- os.environ["POSTGRES_PASSWORD"],
105
- ) as datastore:
106
- drop_postgres_table(datastore, "public.searchablecontentapplication_events")
107
- drop_postgres_table(datastore, "public.pages_projection_example")
108
-
109
-
110
- del SearchableContentApplicationTestCase