eventsourcing 9.3.0__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.

@@ -915,16 +915,6 @@ class AggregateNotFoundError(EventSourcingError):
915
915
  """
916
916
 
917
917
 
918
- class AggregateNotFound(AggregateNotFoundError): # noqa: N818
919
- def __init__(self, *args: Any, **kwargs: Any) -> None:
920
- warn(
921
- "AggregateNotFound is deprecated, use AggregateNotFoundError instead",
922
- DeprecationWarning,
923
- stacklevel=2,
924
- )
925
- super().__init__(*args, **kwargs)
926
-
927
-
928
918
  class EventSourcedLog(Generic[TDomainEvent]):
929
919
  """
930
920
  Constructs a sequence of domain events, like an aggregate.
eventsourcing/domain.py CHANGED
@@ -941,7 +941,6 @@ class MetaAggregate(type, Generic[TAggregate]):
941
941
  setattr(cls, base_event_name, base_event_cls)
942
942
 
943
943
  # Make sure all events defined on aggregate subclass the base event class.
944
- created_event_classes: Dict[str, Type[CanInitAggregate]] = {}
945
944
  for name, value in tuple(cls.__dict__.items()):
946
945
  if name == base_event_name:
947
946
  # Don't subclass the base event class again.
@@ -956,20 +955,31 @@ class MetaAggregate(type, Generic[TAggregate]):
956
955
  ):
957
956
  sub_class = cls._define_event_class(name, (value, base_event_cls), None)
958
957
  setattr(cls, name, sub_class)
959
- for name, value in tuple(cls.__dict__.items()):
960
- if isinstance(value, type) and issubclass(value, CanInitAggregate):
961
- created_event_classes[name] = value
958
+
959
+ # Identify or define the aggregate's "created" event class.
960
+ created_event_class: Type[CanInitAggregate] | None = None
961
+
962
+ # Has the "created" event class been indicated with '_created_event_class'.
963
+ if "_created_event_class" in cls.__dict__:
964
+ created_event_class = cls.__dict__["_created_event_class"]
965
+ if isinstance(created_event_class, type) and issubclass(
966
+ created_event_class, CanInitAggregate
967
+ ):
968
+ # We just subclassed the event classes, so reassign this.
969
+ created_event_class = getattr(cls, created_event_class.__name__)
970
+ assert created_event_class
971
+ cls._created_event_class = created_event_class
972
+ else:
973
+ msg = (
974
+ f"{created_event_class} not subclass of {CanInitAggregate.__name__}"
975
+ )
976
+ raise TypeError(msg)
962
977
 
963
978
  # Disallow using both '_created_event_class' and 'created_event_name'.
964
- created_event_class: Type[CanInitAggregate] | None = cls.__dict__.get(
965
- "_created_event_class"
966
- )
967
979
  if created_event_class and created_event_name:
968
980
  msg = "Can't use both '_created_event_class' and 'created_event_name'"
969
981
  raise TypeError(msg)
970
982
 
971
- # Identify or define the aggregate's "created" event class.
972
-
973
983
  # Is the init method decorated with a CommandMethodDecorator?
974
984
  if isinstance(cls.__dict__.get("__init__"), CommandMethodDecorator):
975
985
  init_decorator: CommandMethodDecorator = cls.__dict__["__init__"]
@@ -977,20 +987,31 @@ class MetaAggregate(type, Generic[TAggregate]):
977
987
  # Set the original method on the class (un-decorate __init__).
978
988
  cls.__init__ = init_decorator.decorated_method # type: ignore
979
989
 
980
- # Disallow using both 'created_event_name' and decorator on __init__.
990
+ # Disallow using both 'created_event_name' and '_created_event_class'.
981
991
  if created_event_name:
982
992
  msg = "Can't use both 'created_event_name' and decorator on __init__"
983
993
  raise TypeError(msg)
984
- # Disallow using both '_created_event_class' and decorator on __init__.
985
994
  if created_event_class:
986
995
  msg = "Can't use both '_created_event_class' and decorator on __init__"
987
996
  raise TypeError(msg)
988
997
 
989
- # Does the decorator specify a "created" event class?
998
+ # Does the decorator specify a "create" event class?
990
999
  if init_decorator.given_event_cls:
991
- created_event_class = cast(
992
- Type[CanInitAggregate], init_decorator.given_event_cls
1000
+ created_event_class = getattr(
1001
+ cls, init_decorator.given_event_cls.__name__
993
1002
  )
1003
+ if isinstance(created_event_class, type) and issubclass(
1004
+ created_event_class, CanInitAggregate
1005
+ ):
1006
+ assert created_event_class
1007
+ cls._created_event_class = created_event_class
1008
+ else:
1009
+ msg = (
1010
+ f"{created_event_class} not subclass of "
1011
+ f"{CanInitAggregate.__name__}"
1012
+ )
1013
+ raise TypeError(msg)
1014
+
994
1015
  # Does the decorator specify a "created" event name?
995
1016
  elif init_decorator.event_cls_name:
996
1017
  created_event_name = init_decorator.event_cls_name
@@ -1007,85 +1028,65 @@ class MetaAggregate(type, Generic[TAggregate]):
1007
1028
  _init_mentions_id.add(cls)
1008
1029
  break
1009
1030
 
1010
- if created_event_class:
1011
- # Check specified "created" event class can init aggregate.
1012
- if not issubclass(created_event_class, CanInitAggregate):
1013
- msg = (
1014
- f"{created_event_class} not subclass of {CanInitAggregate.__name__}"
1015
- )
1016
- raise TypeError(msg)
1017
-
1018
- for sub_class in created_event_classes.values():
1019
- if issubclass(sub_class, created_event_class):
1020
- # We just subclassed the created event class, so reassign it.
1021
- created_event_class = sub_class
1022
-
1023
- # Is a "created" event class already defined that matches the name?
1024
- elif created_event_name and created_event_name in created_event_classes:
1025
- created_event_class = created_event_classes[created_event_name]
1026
-
1027
- # If there is only one class defined, then use it.
1028
- elif len(created_event_classes) == 1 and not created_event_name:
1029
- created_event_class = next(iter(created_event_classes.values()))
1030
-
1031
- # If there are no "created" event classes already defined, or a name is
1032
- # specified that hasn't matched, then define a "created" event class.
1033
- elif len(created_event_classes) == 0 or created_event_name:
1034
-
1035
- # Decide the base classes for the new "created" event class.
1036
- if created_event_name and len(created_event_classes) == 1:
1037
- base_created_event_cls = next(iter(created_event_classes.values()))
1038
- else:
1039
- for base_cls in cls.__mro__:
1040
- if base_cls is cls:
1041
- continue
1042
- base_created_event_cls = base_cls.__dict__.get(
1043
- "_created_event_class",
1044
- base_cls.__dict__.get("Created"),
1045
- )
1046
- if base_created_event_cls:
1047
- break
1048
- else: # pragma: no cover
1049
- msg = "Can't decide base class for new 'created' event class"
1050
- raise TypeError(msg)
1051
-
1052
- if not created_event_name:
1053
- created_event_name = base_created_event_cls.__name__
1054
-
1055
- # Disallow init method from having variable params if
1056
- # we are using it to define a "created" event class.
1057
- try:
1058
- init_method = cls.__dict__["__init__"]
1059
- except KeyError:
1060
- init_method = None
1061
- else:
1031
+ # If no "created" event class has been specified, find or create one.
1032
+ if created_event_class is None:
1033
+ # Discover all the "created" event classes already defined.
1034
+ created_event_classes: Dict[str, Type[AggregateCreated]] = {}
1035
+ for name, value in tuple(cls.__dict__.items()):
1036
+ if isinstance(value, type) and issubclass(value, AggregateCreated):
1037
+ created_event_classes[name] = value
1038
+
1039
+ # Is a "created" event class already defined that matches the name?
1040
+ if created_event_name in created_event_classes:
1041
+ cls._created_event_class = created_event_classes[created_event_name]
1042
+
1043
+ # If there is only one class defined, and we have no name, use it.
1044
+ elif len(created_event_classes) == 1 and not created_event_name:
1045
+ cls._created_event_class = next(iter(created_event_classes.values()))
1046
+
1047
+ # If there are no "created" event classes already defined, or a name is
1048
+ # specified that hasn't matched, then define a "created" event class.
1049
+ elif len(created_event_classes) == 0 or created_event_name:
1050
+ # If no "created" event name has been specified, use default name.
1051
+ if not created_event_name:
1052
+ # This is safe because len(created_event_classes) == 0.
1053
+ created_event_name = "Created"
1054
+
1055
+ # Disallow init method from having variable params if
1056
+ # we are using it to define a "created" event class.
1062
1057
  try:
1063
- _check_no_variable_params(init_method)
1064
- except TypeError:
1065
- raise
1066
-
1067
- # Define a "created" event class for this aggregate.
1068
- if issubclass(base_created_event_cls, base_event_cls):
1069
- # Don't subclass from base event class twice.
1070
- bases: Tuple[Type[CanMutateAggregate], ...] = (base_created_event_cls,)
1071
- else:
1072
- bases = (base_created_event_cls, base_event_cls)
1073
- created_event_class = cast(
1074
- Type[CanInitAggregate],
1075
- cls._define_event_class(
1058
+ init_method = cls.__dict__["__init__"]
1059
+ except KeyError:
1060
+ init_method = None
1061
+ else:
1062
+ try:
1063
+ _check_no_variable_params(init_method)
1064
+ except TypeError:
1065
+ raise
1066
+
1067
+ # Define a "created" event class for this aggregate.
1068
+ if issubclass(cls.Created, base_event_cls):
1069
+ # Don't subclass from base event class twice.
1070
+ bases: Tuple[Type[CanMutateAggregate], ...] = (cls.Created,)
1071
+ else:
1072
+ bases = (cls.Created, base_event_cls)
1073
+ event_cls = cls._define_event_class(
1076
1074
  created_event_name,
1077
1075
  bases,
1078
1076
  init_method,
1079
- ),
1080
- )
1081
- # Set the event class as an attribute of the aggregate class.
1082
- setattr(cls, created_event_name, created_event_class)
1077
+ )
1078
+
1079
+ # Set the event class as an attribute of the aggregate class.
1080
+ setattr(cls, created_event_name, event_cls)
1081
+
1082
+ # Remember which is the "created" event class.
1083
+ cls._created_event_class = cast(Type[AggregateCreated], event_cls)
1083
1084
 
1084
- if created_event_class:
1085
- cls._created_event_class = created_event_class
1086
- else:
1087
1085
  # Prepare to disallow ambiguity of choice between created event classes.
1088
- aggregate_has_many_created_event_classes[cls] = list(created_event_classes)
1086
+ else:
1087
+ aggregate_has_many_created_event_classes[cls] = list(
1088
+ created_event_classes
1089
+ )
1089
1090
 
1090
1091
  # Prepare the subsequent event classes.
1091
1092
  for attr_name, attr_value in tuple(cls.__dict__.items()):
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
4
+ from collections import defaultdict
3
5
  from dataclasses import dataclass
4
6
  from datetime import datetime, timezone
5
- from typing import Any, Iterable, List, Type, TypeVar, cast
7
+ from typing import Any, ClassVar, Dict, Iterable, List, Type, TypeVar, cast
6
8
  from uuid import UUID, uuid4
7
9
 
8
10
  from eventsourcing.dispatch import singledispatchmethod
@@ -27,7 +29,6 @@ class Aggregate:
27
29
  id: UUID
28
30
  version: int
29
31
  created_on: datetime
30
- _pending_events: List[DomainEvent]
31
32
 
32
33
  def __init__(self, event: DomainEvent):
33
34
  self.id = event.originator_id
@@ -46,15 +47,15 @@ class Aggregate:
46
47
  timestamp=event_class.create_timestamp(),
47
48
  )
48
49
  new_event = event_class(**kwargs)
49
- self._apply(new_event)
50
- self._pending_events.append(new_event)
50
+ self.apply(new_event)
51
+ self.pending_events.append(new_event)
51
52
 
52
53
  @singledispatchmethod
53
- def _apply(self, event: DomainEvent) -> None:
54
+ def apply(self, event: DomainEvent) -> None:
54
55
  """Applies event to aggregate."""
55
56
 
56
57
  def collect_events(self) -> List[DomainEvent]:
57
- events, self._pending_events = self._pending_events, []
58
+ events, self.pending_events = self.pending_events, []
58
59
  return events
59
60
 
60
61
  @classmethod
@@ -63,12 +64,25 @@ class Aggregate:
63
64
  _: TAggregate | None,
64
65
  events: Iterable[DomainEvent],
65
66
  ) -> TAggregate | None:
66
- aggregate: TAggregate = object.__new__(cls)
67
- aggregate._pending_events = []
67
+ aggregate = object.__new__(cls)
68
68
  for event in events:
69
- aggregate._apply(event)
69
+ aggregate.apply(event)
70
70
  return aggregate
71
71
 
72
+ @property
73
+ def pending_events(self) -> List[DomainEvent]:
74
+ return type(self).__pending_events[id(self)]
75
+
76
+ @pending_events.setter
77
+ def pending_events(self, pending_events: List[DomainEvent]) -> None:
78
+ type(self).__pending_events[id(self)] = pending_events
79
+
80
+ __pending_events: ClassVar[Dict[int, List[DomainEvent]]] = defaultdict(list)
81
+
82
+ def __del__(self) -> None:
83
+ with contextlib.suppress(KeyError):
84
+ type(self).__pending_events.pop(id(self))
85
+
72
86
 
73
87
  class Dog(Aggregate):
74
88
  @dataclass(frozen=True)
@@ -88,27 +102,27 @@ class Dog(Aggregate):
88
102
  name=name,
89
103
  )
90
104
  dog = cast(Dog, cls.projector(None, [event]))
91
- dog._pending_events.append(event)
105
+ dog.pending_events.append(event)
92
106
  return dog
93
107
 
94
108
  def add_trick(self, trick: str) -> None:
95
109
  self.trigger_event(self.TrickAdded, trick=trick)
96
110
 
97
111
  @singledispatchmethod
98
- def _apply(self, event: DomainEvent) -> None:
112
+ def apply(self, event: DomainEvent) -> None:
99
113
  """Applies event to aggregate."""
100
114
 
101
- @_apply.register(Registered)
115
+ @apply.register(Registered)
102
116
  def _(self, event: Registered) -> None:
103
117
  super().__init__(event)
104
118
  self.name = event.name
105
119
  self.tricks: List[str] = []
106
120
 
107
- @_apply.register(TrickAdded)
121
+ @apply.register(TrickAdded)
108
122
  def _(self, event: TrickAdded) -> None:
109
123
  self.tricks.append(event.trick)
110
124
  self.version = event.originator_version
111
125
 
112
- @_apply.register(Snapshot)
126
+ @apply.register(Snapshot)
113
127
  def _(self, event: Snapshot) -> None:
114
128
  self.__dict__.update(event.state)
@@ -23,119 +23,112 @@ class ContentManagementSystemTestCase(TestCase):
23
23
  env: ClassVar[Dict[str, str]] = {}
24
24
 
25
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))
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))
139
132
 
140
133
 
141
134
  class TestWithSQLite(ContentManagementSystemTestCase):
@@ -164,17 +157,18 @@ class TestWithPostgres(ContentManagementSystemTestCase):
164
157
  super().tearDown()
165
158
 
166
159
  def drop_tables(self) -> None:
167
- with PostgresDatastore(
160
+ db = PostgresDatastore(
168
161
  self.env["POSTGRES_DBNAME"],
169
162
  self.env["POSTGRES_HOST"],
170
163
  self.env["POSTGRES_PORT"],
171
164
  self.env["POSTGRES_USER"],
172
165
  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")
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()
178
172
 
179
173
 
180
174
  del ContentManagementSystemTestCase
@@ -96,15 +96,16 @@ class TestWithPostgres(SearchableContentApplicationTestCase):
96
96
  super().tearDown()
97
97
 
98
98
  def drop_tables(self) -> None:
99
- with PostgresDatastore(
99
+ db = PostgresDatastore(
100
100
  os.environ["POSTGRES_DBNAME"],
101
101
  os.environ["POSTGRES_HOST"],
102
102
  os.environ["POSTGRES_PORT"],
103
103
  os.environ["POSTGRES_USER"],
104
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")
105
+ )
106
+ drop_postgres_table(db, "public.searchablecontentapplication_events")
107
+ drop_postgres_table(db, "public.pages_projection_example")
108
+ db.close()
108
109
 
109
110
 
110
111
  del SearchableContentApplicationTestCase
@@ -54,15 +54,16 @@ class TestWithPostgres(SearchableContentRecorderTestCase):
54
54
  super().tearDown()
55
55
 
56
56
  def drop_tables(self) -> None:
57
- with PostgresDatastore(
57
+ db = PostgresDatastore(
58
58
  os.environ["POSTGRES_DBNAME"],
59
59
  os.environ["POSTGRES_HOST"],
60
60
  os.environ["POSTGRES_PORT"],
61
61
  os.environ["POSTGRES_USER"],
62
62
  os.environ["POSTGRES_PASSWORD"],
63
- ) as datastore:
64
- drop_postgres_table(datastore, "public.searchablecontentapplication_events")
65
- drop_postgres_table(datastore, "public.pages_projection_example")
63
+ )
64
+ drop_postgres_table(db, "public.searchablecontentapplication_events")
65
+ drop_postgres_table(db, "public.pages_projection_example")
66
+ db.close()
66
67
 
67
68
 
68
69
  del SearchableContentRecorderTestCase