eventsourcing 9.4.2__py3-none-any.whl → 9.4.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.
- eventsourcing/dispatch.py +1 -1
- eventsourcing/persistence.py +8 -8
- eventsourcing/popo.py +5 -16
- eventsourcing/postgres.py +186 -58
- eventsourcing/projection.py +14 -28
- eventsourcing/sqlite.py +137 -33
- eventsourcing/tests/persistence.py +76 -12
- eventsourcing/tests/postgres_utils.py +4 -8
- {eventsourcing-9.4.2.dist-info → eventsourcing-9.4.4.dist-info}/METADATA +6 -5
- {eventsourcing-9.4.2.dist-info → eventsourcing-9.4.4.dist-info}/RECORD +13 -13
- {eventsourcing-9.4.2.dist-info → eventsourcing-9.4.4.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.4.2.dist-info → eventsourcing-9.4.4.dist-info}/LICENSE +0 -0
- {eventsourcing-9.4.2.dist-info → eventsourcing-9.4.4.dist-info}/WHEEL +0 -0
eventsourcing/dispatch.py
CHANGED
|
@@ -63,7 +63,7 @@ class singledispatchmethod(_singledispatchmethod[_T]): # noqa: N801
|
|
|
63
63
|
|
|
64
64
|
try:
|
|
65
65
|
return self.dispatcher.register(cast("type[Any]", cls), func=method)
|
|
66
|
-
except NameError:
|
|
66
|
+
except (NameError, TypeError): # NameError <= Py3.13, TypeError >= Py3.14
|
|
67
67
|
self.deferred_registrations.append(
|
|
68
68
|
(cls, method) # pyright: ignore [reportArgumentType]
|
|
69
69
|
)
|
eventsourcing/persistence.py
CHANGED
|
@@ -449,14 +449,17 @@ class TrackingRecorder(Recorder, ABC):
|
|
|
449
449
|
for the named application, or None if no tracking objects have been recorded.
|
|
450
450
|
"""
|
|
451
451
|
|
|
452
|
-
@abstractmethod
|
|
453
452
|
def has_tracking_id(
|
|
454
453
|
self, application_name: str, notification_id: int | None
|
|
455
454
|
) -> bool:
|
|
456
|
-
"""Returns True if
|
|
457
|
-
|
|
458
|
-
|
|
455
|
+
"""Returns True if given notification_id is None or a tracking
|
|
456
|
+
object with the given application_name and a notification ID greater
|
|
457
|
+
than or equal to the given notification_id has been recorded.
|
|
459
458
|
"""
|
|
459
|
+
if notification_id is None:
|
|
460
|
+
return True
|
|
461
|
+
max_tracking_id = self.max_tracking_id(application_name)
|
|
462
|
+
return max_tracking_id is not None and max_tracking_id >= notification_id
|
|
460
463
|
|
|
461
464
|
def wait(
|
|
462
465
|
self,
|
|
@@ -483,10 +486,7 @@ class TrackingRecorder(Recorder, ABC):
|
|
|
483
486
|
sleep_interval_ms = 100.0
|
|
484
487
|
max_sleep_interval_ms = 800.0
|
|
485
488
|
while True:
|
|
486
|
-
|
|
487
|
-
if notification_id is None or (
|
|
488
|
-
max_tracking_id is not None and max_tracking_id >= notification_id
|
|
489
|
-
):
|
|
489
|
+
if self.has_tracking_id(application_name, notification_id):
|
|
490
490
|
break
|
|
491
491
|
if interrupt:
|
|
492
492
|
if interrupt.wait(timeout=sleep_interval_ms / 1000):
|
eventsourcing/popo.py
CHANGED
|
@@ -193,14 +193,14 @@ class POPOSubscription(ListenNotifySubscription[POPOApplicationRecorder]):
|
|
|
193
193
|
class POPOTrackingRecorder(POPORecorder, TrackingRecorder):
|
|
194
194
|
def __init__(self) -> None:
|
|
195
195
|
super().__init__()
|
|
196
|
-
self._tracking_table: dict[str, set[int]] = defaultdict(set)
|
|
197
196
|
self._max_tracking_ids: dict[str, int | None] = defaultdict(lambda: None)
|
|
198
197
|
|
|
199
198
|
def _assert_tracking_uniqueness(self, tracking: Tracking) -> None:
|
|
200
|
-
|
|
199
|
+
max_tracking_id = self._max_tracking_ids[tracking.application_name]
|
|
200
|
+
if max_tracking_id is not None and max_tracking_id >= tracking.notification_id:
|
|
201
201
|
msg = (
|
|
202
|
-
f"
|
|
203
|
-
f"
|
|
202
|
+
f"Tracking notification ID {tracking.notification_id} "
|
|
203
|
+
f"not greater than current max tracking ID {max_tracking_id}"
|
|
204
204
|
)
|
|
205
205
|
raise IntegrityError(msg)
|
|
206
206
|
|
|
@@ -210,23 +210,12 @@ class POPOTrackingRecorder(POPORecorder, TrackingRecorder):
|
|
|
210
210
|
self._insert_tracking(tracking)
|
|
211
211
|
|
|
212
212
|
def _insert_tracking(self, tracking: Tracking) -> None:
|
|
213
|
-
self.
|
|
214
|
-
max_tracking_id = self._max_tracking_ids[tracking.application_name]
|
|
215
|
-
if max_tracking_id is None or max_tracking_id < tracking.notification_id:
|
|
216
|
-
self._max_tracking_ids[tracking.application_name] = tracking.notification_id
|
|
213
|
+
self._max_tracking_ids[tracking.application_name] = tracking.notification_id
|
|
217
214
|
|
|
218
215
|
def max_tracking_id(self, application_name: str) -> int | None:
|
|
219
216
|
with self._database_lock:
|
|
220
217
|
return self._max_tracking_ids[application_name]
|
|
221
218
|
|
|
222
|
-
def has_tracking_id(
|
|
223
|
-
self, application_name: str, notification_id: int | None
|
|
224
|
-
) -> bool:
|
|
225
|
-
if notification_id is None:
|
|
226
|
-
return True
|
|
227
|
-
with self._database_lock:
|
|
228
|
-
return notification_id in self._tracking_table[application_name]
|
|
229
|
-
|
|
230
219
|
|
|
231
220
|
class POPOProcessRecorder(
|
|
232
221
|
POPOTrackingRecorder, POPOApplicationRecorder, ProcessRecorder
|
eventsourcing/postgres.py
CHANGED
|
@@ -94,10 +94,12 @@ class PostgresDatastore:
|
|
|
94
94
|
schema: str = "",
|
|
95
95
|
pool_open_timeout: int | None = None,
|
|
96
96
|
get_password_func: Callable[[], str] | None = None,
|
|
97
|
+
single_row_tracking: bool = True,
|
|
97
98
|
):
|
|
98
99
|
self.idle_in_transaction_session_timeout = idle_in_transaction_session_timeout
|
|
99
100
|
self.pre_ping = pre_ping
|
|
100
101
|
self.pool_open_timeout = pool_open_timeout
|
|
102
|
+
self.single_row_tracking = single_row_tracking
|
|
101
103
|
|
|
102
104
|
check = ConnectionPool.check_connection if pre_ping else None
|
|
103
105
|
self.pool = ConnectionPool(
|
|
@@ -189,6 +191,12 @@ class PostgresDatastore:
|
|
|
189
191
|
class PostgresRecorder:
|
|
190
192
|
"""Base class for recorders that use PostgreSQL."""
|
|
191
193
|
|
|
194
|
+
MAX_IDENTIFIER_LEN = 63
|
|
195
|
+
# From the PostgreSQL docs: "The system uses no more than NAMEDATALEN-1 bytes
|
|
196
|
+
# of an identifier; longer names can be written in commands, but they will be
|
|
197
|
+
# truncated. By default, NAMEDATALEN is 64 so the maximum identifier length is
|
|
198
|
+
# 63 bytes." https://www.postgresql.org/docs/current/sql-syntax-lexical.html
|
|
199
|
+
|
|
192
200
|
def __init__(
|
|
193
201
|
self,
|
|
194
202
|
datastore: PostgresDatastore,
|
|
@@ -196,18 +204,22 @@ class PostgresRecorder:
|
|
|
196
204
|
self.datastore = datastore
|
|
197
205
|
self.create_table_statements = self.construct_create_table_statements()
|
|
198
206
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def check_table_name_length(self, table_name: str) -> None:
|
|
203
|
-
if len(table_name) > 63:
|
|
207
|
+
@staticmethod
|
|
208
|
+
def check_table_name_length(table_name: str) -> None:
|
|
209
|
+
if len(table_name) > PostgresRecorder.MAX_IDENTIFIER_LEN:
|
|
204
210
|
msg = f"Table name too long: {table_name}"
|
|
205
211
|
raise ProgrammingError(msg)
|
|
206
212
|
|
|
213
|
+
def construct_create_table_statements(self) -> list[Composed]:
|
|
214
|
+
return []
|
|
215
|
+
|
|
207
216
|
def create_table(self) -> None:
|
|
208
217
|
with self.datastore.transaction(commit=True) as curs:
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
self._create_table(curs)
|
|
219
|
+
|
|
220
|
+
def _create_table(self, curs: Cursor[DictRow]) -> None:
|
|
221
|
+
for statement in self.create_table_statements:
|
|
222
|
+
curs.execute(statement, prepare=False)
|
|
211
223
|
|
|
212
224
|
|
|
213
225
|
class PostgresAggregateRecorder(PostgresRecorder, AggregateRecorder):
|
|
@@ -610,25 +622,56 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
|
|
|
610
622
|
super().__init__(datastore, **kwargs)
|
|
611
623
|
self.check_table_name_length(tracking_table_name)
|
|
612
624
|
self.tracking_table_name = tracking_table_name
|
|
613
|
-
self.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
625
|
+
self.tracking_table_exists: bool = False
|
|
626
|
+
self.tracking_migration_previous: int | None = None
|
|
627
|
+
self.tracking_migration_current: int | None = None
|
|
628
|
+
self.table_migration_identifier = "__migration__"
|
|
629
|
+
self.has_checked_for_multi_row_tracking_table: bool = False
|
|
630
|
+
if self.datastore.single_row_tracking:
|
|
631
|
+
# For single-row tracking.
|
|
632
|
+
self.create_table_statements.append(
|
|
633
|
+
SQL(
|
|
634
|
+
"CREATE TABLE IF NOT EXISTS {0}.{1} ("
|
|
635
|
+
"application_name text, "
|
|
636
|
+
"notification_id bigint, "
|
|
637
|
+
"PRIMARY KEY "
|
|
638
|
+
"(application_name))"
|
|
639
|
+
).format(
|
|
640
|
+
Identifier(self.datastore.schema),
|
|
641
|
+
Identifier(self.tracking_table_name),
|
|
642
|
+
)
|
|
643
|
+
)
|
|
644
|
+
self.insert_tracking_statement = SQL(
|
|
645
|
+
"INSERT INTO {0}.{1} "
|
|
646
|
+
"VALUES (%(application_name)s, %(notification_id)s) "
|
|
647
|
+
"ON CONFLICT (application_name) DO UPDATE "
|
|
648
|
+
"SET notification_id = %(notification_id)s "
|
|
649
|
+
"WHERE {0}.{1}.notification_id < %(notification_id)s "
|
|
650
|
+
"RETURNING notification_id"
|
|
651
|
+
).format(
|
|
652
|
+
Identifier(self.datastore.schema),
|
|
653
|
+
Identifier(self.tracking_table_name),
|
|
654
|
+
)
|
|
655
|
+
else:
|
|
656
|
+
# For legacy multi-row tracking.
|
|
657
|
+
self.create_table_statements.append(
|
|
658
|
+
SQL(
|
|
659
|
+
"CREATE TABLE IF NOT EXISTS {0}.{1} ("
|
|
660
|
+
"application_name text, "
|
|
661
|
+
"notification_id bigint, "
|
|
662
|
+
"PRIMARY KEY "
|
|
663
|
+
"(application_name, notification_id))"
|
|
664
|
+
).format(
|
|
665
|
+
Identifier(self.datastore.schema),
|
|
666
|
+
Identifier(self.tracking_table_name),
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
self.insert_tracking_statement = SQL(
|
|
670
|
+
"INSERT INTO {0}.{1} VALUES (%(application_name)s, %(notification_id)s)"
|
|
620
671
|
).format(
|
|
621
672
|
Identifier(self.datastore.schema),
|
|
622
673
|
Identifier(self.tracking_table_name),
|
|
623
674
|
)
|
|
624
|
-
)
|
|
625
|
-
|
|
626
|
-
self.insert_tracking_statement = SQL(
|
|
627
|
-
"INSERT INTO {0}.{1} VALUES (%s, %s)"
|
|
628
|
-
).format(
|
|
629
|
-
Identifier(self.datastore.schema),
|
|
630
|
-
Identifier(self.tracking_table_name),
|
|
631
|
-
)
|
|
632
675
|
|
|
633
676
|
self.max_tracking_id_statement = SQL(
|
|
634
677
|
"SELECT MAX(notification_id) FROM {0}.{1} WHERE application_name=%s"
|
|
@@ -637,21 +680,85 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
|
|
|
637
680
|
Identifier(self.tracking_table_name),
|
|
638
681
|
)
|
|
639
682
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
683
|
+
def create_table(self) -> None:
|
|
684
|
+
# Get the migration version.
|
|
685
|
+
try:
|
|
686
|
+
self.tracking_migration_current = self.tracking_migration_previous = (
|
|
687
|
+
self.max_tracking_id(self.table_migration_identifier)
|
|
688
|
+
)
|
|
689
|
+
except ProgrammingError:
|
|
690
|
+
pass
|
|
691
|
+
else:
|
|
692
|
+
self.tracking_table_exists = True
|
|
693
|
+
super().create_table()
|
|
694
|
+
if (
|
|
695
|
+
not self.datastore.single_row_tracking
|
|
696
|
+
and self.tracking_migration_current is not None
|
|
697
|
+
):
|
|
698
|
+
msg = "Can't do multi-row tracking with single-row tracking table"
|
|
699
|
+
raise OperationalError(msg)
|
|
700
|
+
|
|
701
|
+
def _create_table(self, curs: Cursor[DictRow]) -> None:
|
|
702
|
+
max_tracking_ids: dict[str, int] = {}
|
|
703
|
+
if (
|
|
704
|
+
self.datastore.single_row_tracking
|
|
705
|
+
and self.tracking_table_exists
|
|
706
|
+
and not self.tracking_migration_previous
|
|
707
|
+
):
|
|
708
|
+
# Migrate the table.
|
|
709
|
+
curs.execute(
|
|
710
|
+
SQL("SET LOCAL lock_timeout = '{0}s'").format(
|
|
711
|
+
self.datastore.lock_timeout
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
curs.execute(
|
|
715
|
+
SQL("LOCK TABLE {0}.{1} IN ACCESS EXCLUSIVE MODE").format(
|
|
716
|
+
Identifier(self.datastore.schema),
|
|
717
|
+
Identifier(self.tracking_table_name),
|
|
718
|
+
)
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
# Get all application names.
|
|
722
|
+
application_names: list[str] = [
|
|
723
|
+
select_row["application_name"]
|
|
724
|
+
for select_row in curs.execute(
|
|
725
|
+
SQL("SELECT DISTINCT application_name FROM {0}.{1}").format(
|
|
726
|
+
Identifier(self.datastore.schema),
|
|
727
|
+
Identifier(self.tracking_table_name),
|
|
728
|
+
)
|
|
729
|
+
)
|
|
730
|
+
]
|
|
731
|
+
|
|
732
|
+
# Get max tracking ID for each application name.
|
|
733
|
+
for application_name in application_names:
|
|
734
|
+
curs.execute(self.max_tracking_id_statement, (application_name,))
|
|
735
|
+
max_tracking_id_row = curs.fetchone()
|
|
736
|
+
assert max_tracking_id_row is not None
|
|
737
|
+
max_tracking_ids[application_name] = max_tracking_id_row["max"]
|
|
738
|
+
# Rename the table.
|
|
739
|
+
rename = f"bkup1_{self.tracking_table_name}"[: self.MAX_IDENTIFIER_LEN]
|
|
740
|
+
drop_table_statement = SQL("ALTER TABLE {0}.{1} RENAME TO {2}").format(
|
|
741
|
+
Identifier(self.datastore.schema),
|
|
742
|
+
Identifier(self.tracking_table_name),
|
|
743
|
+
Identifier(rename),
|
|
744
|
+
)
|
|
745
|
+
curs.execute(drop_table_statement)
|
|
746
|
+
# Create the table.
|
|
747
|
+
super()._create_table(curs)
|
|
748
|
+
# Maybe insert migration tracking record and application tracking records.
|
|
749
|
+
if self.datastore.single_row_tracking and (
|
|
750
|
+
not self.tracking_table_exists
|
|
751
|
+
or (self.tracking_table_exists and not self.tracking_migration_previous)
|
|
752
|
+
):
|
|
753
|
+
# Assume we just created a table for single-row tracking.
|
|
754
|
+
self._insert_tracking(curs, Tracking(self.table_migration_identifier, 1))
|
|
755
|
+
self.tracking_migration_current = 1
|
|
756
|
+
for application_name, max_tracking_id in max_tracking_ids.items():
|
|
757
|
+
self._insert_tracking(curs, Tracking(application_name, max_tracking_id))
|
|
647
758
|
|
|
648
759
|
@retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
|
|
649
760
|
def insert_tracking(self, tracking: Tracking) -> None:
|
|
650
|
-
with (
|
|
651
|
-
self.datastore.get_connection() as conn,
|
|
652
|
-
conn.transaction(),
|
|
653
|
-
conn.cursor() as curs,
|
|
654
|
-
):
|
|
761
|
+
with self.datastore.transaction(commit=True) as curs:
|
|
655
762
|
self._insert_tracking(curs, tracking)
|
|
656
763
|
|
|
657
764
|
def _insert_tracking(
|
|
@@ -659,42 +766,57 @@ class PostgresTrackingRecorder(PostgresRecorder, TrackingRecorder):
|
|
|
659
766
|
curs: Cursor[DictRow],
|
|
660
767
|
tracking: Tracking,
|
|
661
768
|
) -> None:
|
|
769
|
+
self._check_has_multi_row_tracking_table(curs)
|
|
770
|
+
|
|
662
771
|
curs.execute(
|
|
663
772
|
query=self.insert_tracking_statement,
|
|
664
|
-
params=
|
|
665
|
-
tracking.application_name,
|
|
666
|
-
tracking.notification_id,
|
|
667
|
-
|
|
773
|
+
params={
|
|
774
|
+
"application_name": tracking.application_name,
|
|
775
|
+
"notification_id": tracking.notification_id,
|
|
776
|
+
},
|
|
668
777
|
prepare=True,
|
|
669
778
|
)
|
|
779
|
+
if self.datastore.single_row_tracking:
|
|
780
|
+
fetchone = curs.fetchone()
|
|
781
|
+
if fetchone is None:
|
|
782
|
+
msg = (
|
|
783
|
+
"Failed to record tracking for "
|
|
784
|
+
f"{tracking.application_name} {tracking.notification_id}"
|
|
785
|
+
)
|
|
786
|
+
raise IntegrityError(msg)
|
|
787
|
+
|
|
788
|
+
def _check_has_multi_row_tracking_table(self, c: Cursor[DictRow]) -> None:
|
|
789
|
+
if (
|
|
790
|
+
not self.datastore.single_row_tracking
|
|
791
|
+
and not self.has_checked_for_multi_row_tracking_table
|
|
792
|
+
and self._max_tracking_id(self.table_migration_identifier, c)
|
|
793
|
+
):
|
|
794
|
+
msg = "Can't do multi-row tracking with single-row tracking table"
|
|
795
|
+
raise ProgrammingError(msg)
|
|
796
|
+
self.has_checked_for_multi_row_tracking_table = True
|
|
670
797
|
|
|
671
798
|
@retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
|
|
672
799
|
def max_tracking_id(self, application_name: str) -> int | None:
|
|
673
800
|
with self.datastore.get_connection() as conn, conn.cursor() as curs:
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
801
|
+
return self._max_tracking_id(application_name, curs)
|
|
802
|
+
|
|
803
|
+
def _max_tracking_id(
|
|
804
|
+
self, application_name: str, curs: Cursor[DictRow]
|
|
805
|
+
) -> int | None:
|
|
806
|
+
curs.execute(
|
|
807
|
+
query=self.max_tracking_id_statement,
|
|
808
|
+
params=(application_name,),
|
|
809
|
+
prepare=True,
|
|
810
|
+
)
|
|
811
|
+
fetchone = curs.fetchone()
|
|
812
|
+
assert fetchone is not None
|
|
813
|
+
return fetchone["max"]
|
|
682
814
|
|
|
683
815
|
@retry((InterfaceError, OperationalError), max_attempts=10, wait=0.2)
|
|
684
816
|
def has_tracking_id(
|
|
685
817
|
self, application_name: str, notification_id: int | None
|
|
686
818
|
) -> bool:
|
|
687
|
-
|
|
688
|
-
return True
|
|
689
|
-
with self.datastore.get_connection() as conn, conn.cursor() as curs:
|
|
690
|
-
curs.execute(
|
|
691
|
-
query=self.count_tracking_id_statement,
|
|
692
|
-
params=(application_name, notification_id),
|
|
693
|
-
prepare=True,
|
|
694
|
-
)
|
|
695
|
-
fetchone = curs.fetchone()
|
|
696
|
-
assert fetchone is not None
|
|
697
|
-
return bool(fetchone["count"])
|
|
819
|
+
return super().has_tracking_id(application_name, notification_id)
|
|
698
820
|
|
|
699
821
|
|
|
700
822
|
TPostgresTrackingRecorder = TypeVar(
|
|
@@ -750,6 +872,7 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
750
872
|
"POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT"
|
|
751
873
|
)
|
|
752
874
|
POSTGRES_SCHEMA = "POSTGRES_SCHEMA"
|
|
875
|
+
POSTGRES_SINGLE_ROW_TRACKING = "SINGLE_ROW_TRACKING"
|
|
753
876
|
CREATE_TABLE = "CREATE_TABLE"
|
|
754
877
|
|
|
755
878
|
aggregate_recorder_class = PostgresAggregateRecorder
|
|
@@ -911,13 +1034,16 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
911
1034
|
|
|
912
1035
|
schema = self.env.get(self.POSTGRES_SCHEMA) or ""
|
|
913
1036
|
|
|
1037
|
+
single_row_tracking = strtobool(
|
|
1038
|
+
self.env.get(self.POSTGRES_SINGLE_ROW_TRACKING, "t")
|
|
1039
|
+
)
|
|
1040
|
+
|
|
914
1041
|
self.datastore = PostgresDatastore(
|
|
915
1042
|
dbname=dbname,
|
|
916
1043
|
host=host,
|
|
917
1044
|
port=port,
|
|
918
1045
|
user=user,
|
|
919
1046
|
password=password,
|
|
920
|
-
get_password_func=get_password_func,
|
|
921
1047
|
connect_timeout=connect_timeout,
|
|
922
1048
|
idle_in_transaction_session_timeout=idle_in_transaction_session_timeout,
|
|
923
1049
|
pool_size=pool_size,
|
|
@@ -927,6 +1053,8 @@ class PostgresFactory(InfrastructureFactory[PostgresTrackingRecorder]):
|
|
|
927
1053
|
pre_ping=pre_ping,
|
|
928
1054
|
lock_timeout=lock_timeout,
|
|
929
1055
|
schema=schema,
|
|
1056
|
+
get_password_func=get_password_func,
|
|
1057
|
+
single_row_tracking=single_row_tracking,
|
|
930
1058
|
)
|
|
931
1059
|
|
|
932
1060
|
def env_create_table(self) -> bool:
|
eventsourcing/projection.py
CHANGED
|
@@ -16,7 +16,6 @@ from eventsourcing.dispatch import singledispatchmethod
|
|
|
16
16
|
from eventsourcing.domain import DomainEventProtocol
|
|
17
17
|
from eventsourcing.persistence import (
|
|
18
18
|
InfrastructureFactory,
|
|
19
|
-
IntegrityError,
|
|
20
19
|
ProcessRecorder,
|
|
21
20
|
Tracking,
|
|
22
21
|
TrackingRecorder,
|
|
@@ -144,40 +143,27 @@ class EventSourcedProjection(Application, ABC):
|
|
|
144
143
|
def process_event(
|
|
145
144
|
self, domain_event: DomainEventProtocol, tracking: Tracking
|
|
146
145
|
) -> None:
|
|
147
|
-
"""Calls :func:`~eventsourcing.system.Follower.policy` method with
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
the given :class:`~eventsourcing.persistence.Tracking` object.
|
|
146
|
+
"""Calls :func:`~eventsourcing.system.Follower.policy` method with the given
|
|
147
|
+
domain event and a new :class:`~eventsourcing.application.ProcessingEvent`
|
|
148
|
+
constructed with the given tracking object.
|
|
151
149
|
|
|
152
|
-
The policy
|
|
150
|
+
The policy method should collect any new aggregate events on the process
|
|
153
151
|
event object.
|
|
154
152
|
|
|
155
|
-
After the policy method returns, the
|
|
156
|
-
|
|
157
|
-
:
|
|
158
|
-
will return new notifications.
|
|
153
|
+
After the policy method returns, the processing event object will be recorded
|
|
154
|
+
by calling :py:func:`~eventsourcing.application.Application._record`,
|
|
155
|
+
which then returns list of :py:class:`~eventsourcing.persistence.Recording`.
|
|
159
156
|
|
|
160
|
-
After calling
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
:func:`~eventsourcing.application.Application.notify` method.
|
|
157
|
+
After calling :func:`~eventsourcing.application.Application._take_snapshots`,
|
|
158
|
+
the recordings are passed in a call to
|
|
159
|
+
:py:func:`~eventsourcing.application.Application._notify`.
|
|
164
160
|
"""
|
|
165
161
|
processing_event = ProcessingEvent(tracking=tracking)
|
|
166
162
|
self.policy(domain_event, processing_event)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
tracking.application_name,
|
|
172
|
-
tracking.notification_id,
|
|
173
|
-
):
|
|
174
|
-
pass
|
|
175
|
-
else:
|
|
176
|
-
raise
|
|
177
|
-
else:
|
|
178
|
-
self._take_snapshots(processing_event)
|
|
179
|
-
self.notify(processing_event.events)
|
|
180
|
-
self._notify(recordings)
|
|
163
|
+
recordings = self._record(processing_event)
|
|
164
|
+
self._take_snapshots(processing_event)
|
|
165
|
+
self.notify(processing_event.events)
|
|
166
|
+
self._notify(recordings)
|
|
181
167
|
|
|
182
168
|
@singledispatchmethod
|
|
183
169
|
def policy(
|
eventsourcing/sqlite.py
CHANGED
|
@@ -212,6 +212,7 @@ class SQLiteDatastore:
|
|
|
212
212
|
pool_timeout: float = 5.0,
|
|
213
213
|
max_age: float | None = None,
|
|
214
214
|
pre_ping: bool = False,
|
|
215
|
+
single_row_tracking: bool = True,
|
|
215
216
|
):
|
|
216
217
|
self.pool = SQLiteConnectionPool(
|
|
217
218
|
db_name=db_name,
|
|
@@ -222,6 +223,7 @@ class SQLiteDatastore:
|
|
|
222
223
|
max_age=max_age,
|
|
223
224
|
pre_ping=pre_ping,
|
|
224
225
|
)
|
|
226
|
+
self.single_row_tracking = single_row_tracking
|
|
225
227
|
|
|
226
228
|
@contextmanager
|
|
227
229
|
def transaction(self, *, commit: bool) -> Iterator[SQLiteCursor]:
|
|
@@ -260,8 +262,11 @@ class SQLiteRecorder(Recorder):
|
|
|
260
262
|
|
|
261
263
|
def create_table(self) -> None:
|
|
262
264
|
with self.datastore.transaction(commit=True) as c:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
self._create_table(c)
|
|
266
|
+
|
|
267
|
+
def _create_table(self, c: SQLiteCursor) -> None:
|
|
268
|
+
for statement in self.create_table_statements:
|
|
269
|
+
c.execute(statement)
|
|
265
270
|
|
|
266
271
|
|
|
267
272
|
class SQLiteAggregateRecorder(SQLiteRecorder, AggregateRecorder):
|
|
@@ -484,27 +489,104 @@ class SQLiteTrackingRecorder(SQLiteRecorder, TrackingRecorder):
|
|
|
484
489
|
**kwargs: Any,
|
|
485
490
|
):
|
|
486
491
|
super().__init__(datastore, **kwargs)
|
|
487
|
-
self.
|
|
492
|
+
self.tracking_table_exists: bool = False
|
|
493
|
+
self.tracking_migration_previous: int | None = None
|
|
494
|
+
self.tracking_migration_current: int | None = None
|
|
495
|
+
self.table_migration_identifier = "__migration__"
|
|
496
|
+
self.has_checked_for_multi_row_tracking_table: bool = False
|
|
497
|
+
if self.datastore.single_row_tracking:
|
|
498
|
+
self.insert_tracking_statement = (
|
|
499
|
+
"INSERT INTO tracking "
|
|
500
|
+
"VALUES (:application_name, :notification_id) "
|
|
501
|
+
"ON CONFLICT (application_name) DO UPDATE "
|
|
502
|
+
"SET notification_id = :notification_id "
|
|
503
|
+
"WHERE tracking.notification_id < :notification_id "
|
|
504
|
+
"RETURNING notification_id"
|
|
505
|
+
)
|
|
506
|
+
else:
|
|
507
|
+
self.insert_tracking_statement = (
|
|
508
|
+
"INSERT INTO tracking VALUES (:application_name, :notification_id)"
|
|
509
|
+
)
|
|
488
510
|
self.select_max_tracking_id_statement = (
|
|
489
511
|
"SELECT MAX(notification_id) FROM tracking WHERE application_name=?"
|
|
490
512
|
)
|
|
491
|
-
self.count_tracking_id_statement = (
|
|
492
|
-
"SELECT COUNT(*) FROM tracking WHERE "
|
|
493
|
-
"application_name=? AND notification_id=?"
|
|
494
|
-
)
|
|
495
513
|
|
|
496
514
|
def construct_create_table_statements(self) -> list[str]:
|
|
497
515
|
statements = super().construct_create_table_statements()
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
516
|
+
if self.datastore.single_row_tracking:
|
|
517
|
+
statements.append(
|
|
518
|
+
"CREATE TABLE IF NOT EXISTS tracking ("
|
|
519
|
+
"application_name TEXT, "
|
|
520
|
+
"notification_id INTEGER, "
|
|
521
|
+
"PRIMARY KEY "
|
|
522
|
+
"(application_name)) "
|
|
523
|
+
"WITHOUT ROWID"
|
|
524
|
+
)
|
|
525
|
+
else:
|
|
526
|
+
statements.append(
|
|
527
|
+
"CREATE TABLE IF NOT EXISTS tracking ("
|
|
528
|
+
"application_name TEXT, "
|
|
529
|
+
"notification_id INTEGER, "
|
|
530
|
+
"PRIMARY KEY "
|
|
531
|
+
"(application_name, notification_id)) "
|
|
532
|
+
"WITHOUT ROWID"
|
|
533
|
+
)
|
|
506
534
|
return statements
|
|
507
535
|
|
|
536
|
+
def create_table(self) -> None:
|
|
537
|
+
# Get the migration version.
|
|
538
|
+
try:
|
|
539
|
+
self.tracking_migration_current = self.tracking_migration_previous = (
|
|
540
|
+
self.max_tracking_id(self.table_migration_identifier)
|
|
541
|
+
)
|
|
542
|
+
except OperationalError:
|
|
543
|
+
pass
|
|
544
|
+
else:
|
|
545
|
+
self.tracking_table_exists = True
|
|
546
|
+
super().create_table()
|
|
547
|
+
if (
|
|
548
|
+
not self.datastore.single_row_tracking
|
|
549
|
+
and self.tracking_migration_current is not None
|
|
550
|
+
):
|
|
551
|
+
msg = "Can't do multi-row tracking with single-row tracking table"
|
|
552
|
+
raise OperationalError(msg)
|
|
553
|
+
|
|
554
|
+
def _create_table(self, c: SQLiteCursor) -> None:
|
|
555
|
+
max_tracking_ids: dict[str, int] = {}
|
|
556
|
+
if (
|
|
557
|
+
self.datastore.single_row_tracking
|
|
558
|
+
and self.tracking_table_exists
|
|
559
|
+
and not self.tracking_migration_previous
|
|
560
|
+
):
|
|
561
|
+
# Migrate tracking to use single-row per application name.
|
|
562
|
+
# - Get all application names.
|
|
563
|
+
c.execute("SELECT DISTINCT application_name FROM tracking")
|
|
564
|
+
application_names: list[str] = [
|
|
565
|
+
select_row["application_name"] for select_row in c.fetchall()
|
|
566
|
+
]
|
|
567
|
+
|
|
568
|
+
# - Get max tracking ID for each application name.
|
|
569
|
+
for application_name in application_names:
|
|
570
|
+
c.execute(self.select_max_tracking_id_statement, (application_name,))
|
|
571
|
+
max_tracking_id_row = c.fetchone()
|
|
572
|
+
assert max_tracking_id_row is not None
|
|
573
|
+
max_tracking_ids[application_name] = max_tracking_id_row[0]
|
|
574
|
+
# - Rename the table.
|
|
575
|
+
drop_table_statement = "ALTER TABLE tracking RENAME TO old1_tracking"
|
|
576
|
+
c.execute(drop_table_statement)
|
|
577
|
+
# Create the table.
|
|
578
|
+
super()._create_table(c)
|
|
579
|
+
# - Maybe insert migration tracking record and application tracking records.
|
|
580
|
+
if self.datastore.single_row_tracking and (
|
|
581
|
+
not self.tracking_table_exists
|
|
582
|
+
or (self.tracking_table_exists and not self.tracking_migration_previous)
|
|
583
|
+
):
|
|
584
|
+
# - Assume we just created a table for single-row tracking.
|
|
585
|
+
self._insert_tracking(c, Tracking(self.table_migration_identifier, 1))
|
|
586
|
+
self.tracking_migration_current = 1
|
|
587
|
+
for application_name, max_tracking_id in max_tracking_ids.items():
|
|
588
|
+
self._insert_tracking(c, Tracking(application_name, max_tracking_id))
|
|
589
|
+
|
|
508
590
|
def insert_tracking(self, tracking: Tracking) -> None:
|
|
509
591
|
with self.datastore.transaction(commit=True) as c:
|
|
510
592
|
self._insert_tracking(c, tracking)
|
|
@@ -514,29 +596,42 @@ class SQLiteTrackingRecorder(SQLiteRecorder, TrackingRecorder):
|
|
|
514
596
|
c: SQLiteCursor,
|
|
515
597
|
tracking: Tracking,
|
|
516
598
|
) -> None:
|
|
599
|
+
self._check_has_multi_row_tracking_table(c)
|
|
600
|
+
|
|
517
601
|
c.execute(
|
|
518
602
|
self.insert_tracking_statement,
|
|
519
|
-
|
|
520
|
-
tracking.application_name,
|
|
521
|
-
tracking.notification_id,
|
|
522
|
-
|
|
603
|
+
{
|
|
604
|
+
"application_name": tracking.application_name,
|
|
605
|
+
"notification_id": tracking.notification_id,
|
|
606
|
+
},
|
|
523
607
|
)
|
|
608
|
+
if self.datastore.single_row_tracking:
|
|
609
|
+
fetchone = c.fetchone()
|
|
610
|
+
if fetchone is None:
|
|
611
|
+
msg = (
|
|
612
|
+
"Failed to record tracking for "
|
|
613
|
+
f"{tracking.application_name} {tracking.notification_id}"
|
|
614
|
+
)
|
|
615
|
+
raise IntegrityError(msg)
|
|
616
|
+
|
|
617
|
+
def _check_has_multi_row_tracking_table(self, c: SQLiteCursor) -> None:
|
|
618
|
+
if (
|
|
619
|
+
not self.datastore.single_row_tracking
|
|
620
|
+
and not self.has_checked_for_multi_row_tracking_table
|
|
621
|
+
and self._max_tracking_id(self.table_migration_identifier, c)
|
|
622
|
+
):
|
|
623
|
+
msg = "Can't do multi-row tracking with single-row tracking table"
|
|
624
|
+
raise OperationalError(msg)
|
|
625
|
+
self.has_checked_for_multi_row_tracking_table = True
|
|
524
626
|
|
|
525
627
|
def max_tracking_id(self, application_name: str) -> int | None:
|
|
526
|
-
params = [application_name]
|
|
527
|
-
with self.datastore.transaction(commit=False) as c:
|
|
528
|
-
c.execute(self.select_max_tracking_id_statement, params)
|
|
529
|
-
return c.fetchone()[0]
|
|
530
|
-
|
|
531
|
-
def has_tracking_id(
|
|
532
|
-
self, application_name: str, notification_id: int | None
|
|
533
|
-
) -> bool:
|
|
534
|
-
if notification_id is None:
|
|
535
|
-
return True
|
|
536
|
-
params = [application_name, notification_id]
|
|
537
628
|
with self.datastore.transaction(commit=False) as c:
|
|
538
|
-
|
|
539
|
-
|
|
629
|
+
return self._max_tracking_id(application_name, c)
|
|
630
|
+
|
|
631
|
+
def _max_tracking_id(self, application_name: str, c: SQLiteCursor) -> int | None:
|
|
632
|
+
params = [application_name]
|
|
633
|
+
c.execute(self.select_max_tracking_id_statement, params)
|
|
634
|
+
return c.fetchone()[0]
|
|
540
635
|
|
|
541
636
|
|
|
542
637
|
class SQLiteProcessRecorder(
|
|
@@ -568,6 +663,7 @@ class SQLiteProcessRecorder(
|
|
|
568
663
|
class SQLiteFactory(InfrastructureFactory[SQLiteTrackingRecorder]):
|
|
569
664
|
SQLITE_DBNAME = "SQLITE_DBNAME"
|
|
570
665
|
SQLITE_LOCK_TIMEOUT = "SQLITE_LOCK_TIMEOUT"
|
|
666
|
+
SQLITE_SINGLE_ROW_TRACKING = "SINGLE_ROW_TRACKING"
|
|
571
667
|
CREATE_TABLE = "CREATE_TABLE"
|
|
572
668
|
|
|
573
669
|
aggregate_recorder_class = SQLiteAggregateRecorder
|
|
@@ -603,7 +699,15 @@ class SQLiteFactory(InfrastructureFactory[SQLiteTrackingRecorder]):
|
|
|
603
699
|
)
|
|
604
700
|
raise OSError(msg) from None
|
|
605
701
|
|
|
606
|
-
|
|
702
|
+
single_row_tracking = strtobool(
|
|
703
|
+
self.env.get(self.SQLITE_SINGLE_ROW_TRACKING, "t")
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
self.datastore = SQLiteDatastore(
|
|
707
|
+
db_name=db_name,
|
|
708
|
+
lock_timeout=lock_timeout,
|
|
709
|
+
single_row_tracking=single_row_tracking,
|
|
710
|
+
)
|
|
607
711
|
|
|
608
712
|
def aggregate_recorder(self, purpose: str = "events") -> AggregateRecorder:
|
|
609
713
|
events_table_name = "stored_" + purpose
|
|
@@ -761,15 +761,17 @@ class TrackingRecorderTestCase(TestCase, ABC):
|
|
|
761
761
|
tracking_recorder = self.create_recorder()
|
|
762
762
|
|
|
763
763
|
# Construct tracking objects.
|
|
764
|
-
tracking1 = Tracking(
|
|
765
|
-
tracking2 = Tracking(
|
|
766
|
-
tracking3 = Tracking(
|
|
764
|
+
tracking1 = Tracking("upstream1", 21)
|
|
765
|
+
tracking2 = Tracking("upstream1", 22)
|
|
766
|
+
tracking3 = Tracking("upstream2", 21)
|
|
767
767
|
|
|
768
768
|
# Insert tracking objects.
|
|
769
769
|
tracking_recorder.insert_tracking(tracking=tracking1)
|
|
770
770
|
tracking_recorder.insert_tracking(tracking=tracking2)
|
|
771
771
|
tracking_recorder.insert_tracking(tracking=tracking3)
|
|
772
772
|
|
|
773
|
+
# raise Exception(tracking_recorder.max_tracking_id(tracking1.application_name))
|
|
774
|
+
|
|
773
775
|
# Fail to insert same tracking object twice.
|
|
774
776
|
with self.assertRaises(IntegrityError):
|
|
775
777
|
tracking_recorder.insert_tracking(tracking=tracking1)
|
|
@@ -778,16 +780,37 @@ class TrackingRecorderTestCase(TestCase, ABC):
|
|
|
778
780
|
with self.assertRaises(IntegrityError):
|
|
779
781
|
tracking_recorder.insert_tracking(tracking=tracking3)
|
|
780
782
|
|
|
781
|
-
# Get
|
|
783
|
+
# Get max tracking ID.
|
|
782
784
|
self.assertEqual(tracking_recorder.max_tracking_id("upstream1"), 22)
|
|
783
785
|
self.assertEqual(tracking_recorder.max_tracking_id("upstream2"), 21)
|
|
784
786
|
self.assertIsNone(tracking_recorder.max_tracking_id("upstream3"))
|
|
785
787
|
|
|
786
788
|
# Check if an event notification has been processed.
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
789
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream1", None))
|
|
790
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream1", 20))
|
|
791
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream1", 21))
|
|
792
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream1", 22))
|
|
793
|
+
self.assertFalse(tracking_recorder.has_tracking_id("upstream1", 23))
|
|
794
|
+
|
|
795
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream2", None))
|
|
796
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream2", 20))
|
|
797
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream2", 21))
|
|
798
|
+
self.assertFalse(tracking_recorder.has_tracking_id("upstream2", 22))
|
|
799
|
+
|
|
800
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream2", None))
|
|
801
|
+
self.assertFalse(tracking_recorder.has_tracking_id("upstream2", 22))
|
|
802
|
+
|
|
803
|
+
# Construct more tracking objects.
|
|
804
|
+
tracking4 = Tracking("upstream1", 23)
|
|
805
|
+
tracking5 = Tracking("upstream1", 24)
|
|
806
|
+
|
|
807
|
+
tracking_recorder.insert_tracking(tracking5)
|
|
808
|
+
|
|
809
|
+
# Can't fill in the gap.
|
|
810
|
+
with self.assertRaises(IntegrityError):
|
|
811
|
+
tracking_recorder.insert_tracking(tracking4)
|
|
812
|
+
|
|
813
|
+
self.assertTrue(tracking_recorder.has_tracking_id("upstream1", 23))
|
|
791
814
|
|
|
792
815
|
def test_wait(self) -> None:
|
|
793
816
|
tracking_recorder = self.create_recorder()
|
|
@@ -918,14 +941,15 @@ class ProcessRecorderTestCase(TestCase, ABC):
|
|
|
918
941
|
self.assertFalse(recorder.has_tracking_id("upstream_app", 1))
|
|
919
942
|
self.assertFalse(recorder.has_tracking_id("upstream_app", 2))
|
|
920
943
|
self.assertFalse(recorder.has_tracking_id("upstream_app", 3))
|
|
944
|
+
self.assertFalse(recorder.has_tracking_id("upstream_app", 4))
|
|
921
945
|
|
|
922
946
|
tracking1 = Tracking(
|
|
923
947
|
application_name="upstream_app",
|
|
924
948
|
notification_id=1,
|
|
925
949
|
)
|
|
926
|
-
|
|
950
|
+
tracking3 = Tracking(
|
|
927
951
|
application_name="upstream_app",
|
|
928
|
-
notification_id=
|
|
952
|
+
notification_id=3,
|
|
929
953
|
)
|
|
930
954
|
|
|
931
955
|
recorder.insert_events(
|
|
@@ -936,15 +960,55 @@ class ProcessRecorderTestCase(TestCase, ABC):
|
|
|
936
960
|
self.assertTrue(recorder.has_tracking_id("upstream_app", 1))
|
|
937
961
|
self.assertFalse(recorder.has_tracking_id("upstream_app", 2))
|
|
938
962
|
self.assertFalse(recorder.has_tracking_id("upstream_app", 3))
|
|
963
|
+
self.assertFalse(recorder.has_tracking_id("upstream_app", 4))
|
|
939
964
|
|
|
940
965
|
recorder.insert_events(
|
|
941
966
|
stored_events=[],
|
|
942
|
-
tracking=
|
|
967
|
+
tracking=tracking3,
|
|
943
968
|
)
|
|
944
969
|
|
|
945
970
|
self.assertTrue(recorder.has_tracking_id("upstream_app", 1))
|
|
946
971
|
self.assertTrue(recorder.has_tracking_id("upstream_app", 2))
|
|
947
|
-
self.
|
|
972
|
+
self.assertTrue(recorder.has_tracking_id("upstream_app", 3))
|
|
973
|
+
self.assertFalse(recorder.has_tracking_id("upstream_app", 4))
|
|
974
|
+
|
|
975
|
+
def test_raises_when_lower_inserted_later(self) -> None:
|
|
976
|
+
# Construct the recorder.
|
|
977
|
+
recorder = self.create_recorder()
|
|
978
|
+
|
|
979
|
+
tracking1 = Tracking(
|
|
980
|
+
application_name="upstream_app",
|
|
981
|
+
notification_id=1,
|
|
982
|
+
)
|
|
983
|
+
tracking2 = Tracking(
|
|
984
|
+
application_name="upstream_app",
|
|
985
|
+
notification_id=2,
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
# Insert tracking info.
|
|
989
|
+
recorder.insert_events(
|
|
990
|
+
stored_events=[],
|
|
991
|
+
tracking=tracking2,
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
# Get current position.
|
|
995
|
+
self.assertEqual(
|
|
996
|
+
recorder.max_tracking_id("upstream_app"),
|
|
997
|
+
2,
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
# Insert tracking info.
|
|
1001
|
+
with self.assertRaises(IntegrityError):
|
|
1002
|
+
recorder.insert_events(
|
|
1003
|
+
stored_events=[],
|
|
1004
|
+
tracking=tracking1,
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
# Get current position.
|
|
1008
|
+
self.assertEqual(
|
|
1009
|
+
recorder.max_tracking_id("upstream_app"),
|
|
1010
|
+
2,
|
|
1011
|
+
)
|
|
948
1012
|
|
|
949
1013
|
def test_performance(self) -> None:
|
|
950
1014
|
# Construct the recorder.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import psycopg
|
|
2
2
|
from psycopg.sql import SQL, Identifier
|
|
3
3
|
|
|
4
|
-
from eventsourcing.persistence import PersistenceError
|
|
5
4
|
from eventsourcing.postgres import PostgresDatastore
|
|
6
5
|
|
|
7
6
|
|
|
@@ -44,12 +43,9 @@ def pg_close_all_connections(
|
|
|
44
43
|
|
|
45
44
|
|
|
46
45
|
def drop_postgres_table(datastore: PostgresDatastore, table_name: str) -> None:
|
|
47
|
-
|
|
46
|
+
# print(f"Dropping table {datastore.schema}.{table_name}")
|
|
47
|
+
statement = SQL("DROP TABLE IF EXISTS {0}.{1}").format(
|
|
48
48
|
Identifier(datastore.schema), Identifier(table_name)
|
|
49
49
|
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
with datastore.transaction(commit=True) as curs:
|
|
53
|
-
curs.execute(statement, prepare=False)
|
|
54
|
-
except PersistenceError:
|
|
55
|
-
pass
|
|
50
|
+
with datastore.transaction(commit=True) as curs:
|
|
51
|
+
curs.execute(statement, prepare=False)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: eventsourcing
|
|
3
|
-
Version: 9.4.
|
|
3
|
+
Version: 9.4.4
|
|
4
4
|
Summary: Event sourcing in Python
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Keywords: event sourcing,event store,domain driven design,domain-driven design,ddd,cqrs,cqs
|
|
@@ -24,10 +24,11 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
24
24
|
Provides-Extra: crypto
|
|
25
25
|
Provides-Extra: cryptography
|
|
26
26
|
Provides-Extra: postgres
|
|
27
|
-
Requires-Dist: cryptography (>=44.0
|
|
28
|
-
Requires-Dist: psycopg[pool] (>=3.2
|
|
29
|
-
Requires-Dist: pycryptodome (>=3.22
|
|
27
|
+
Requires-Dist: cryptography (>=44.0) ; extra == "cryptography"
|
|
28
|
+
Requires-Dist: psycopg[pool] (>=3.2) ; extra == "postgres"
|
|
29
|
+
Requires-Dist: pycryptodome (>=3.22) ; extra == "crypto"
|
|
30
30
|
Requires-Dist: typing_extensions
|
|
31
|
+
Project-URL: Documentation, https://eventsourcing.readthedocs.io/
|
|
31
32
|
Project-URL: Homepage, https://github.com/pyeventsourcing/eventsourcing
|
|
32
33
|
Project-URL: Repository, https://github.com/pyeventsourcing/eventsourcing
|
|
33
34
|
Description-Content-Type: text/markdown
|
|
@@ -207,7 +208,7 @@ There are projects that adapt popular ORMs such as
|
|
|
207
208
|
and [SQLAlchemy](https://github.com/pyeventsourcing/eventsourcing-sqlalchemy#readme).
|
|
208
209
|
There are projects that adapt specialist event stores such as
|
|
209
210
|
[Axon Server](https://github.com/pyeventsourcing/eventsourcing-axonserver#readme) and
|
|
210
|
-
[
|
|
211
|
+
[KurrentDB](https://github.com/pyeventsourcing/eventsourcing-kurrentdb#readme).
|
|
211
212
|
There are projects that support popular NoSQL databases such as
|
|
212
213
|
[DynamoDB](https://github.com/pyeventsourcing/eventsourcing-dynamodb#readme).
|
|
213
214
|
There are also projects that provide examples of using the
|
|
@@ -3,24 +3,24 @@ eventsourcing/application.py,sha256=K3M9_Rh2jaYzBDPMvmyemLXHQ4GsGfGsxmMfUeVSqXw,
|
|
|
3
3
|
eventsourcing/cipher.py,sha256=ulTBtX5K9ejRAkdUaUbdIaj4H7anYwDOi7JxOolj2uo,3295
|
|
4
4
|
eventsourcing/compressor.py,sha256=qEYWvsUXFLyhKgfuv-HGNJ6VF4sRw4z0IxbNW9ukOfc,385
|
|
5
5
|
eventsourcing/cryptography.py,sha256=aFZLlJxxSb5seVbh94-T8FA_RIGOe-VFu5SJrbOnwUU,2969
|
|
6
|
-
eventsourcing/dispatch.py,sha256
|
|
6
|
+
eventsourcing/dispatch.py,sha256=-yI-0EpyXnpMBkciTHNPlxSHJebUe7Ko9rT-gdOjoIo,2797
|
|
7
7
|
eventsourcing/domain.py,sha256=2c33FfhVIBcUzhJa6TMhGPDwOma-wGiPHUL8RC8ZokQ,62967
|
|
8
8
|
eventsourcing/interface.py,sha256=-VLoqcd9a0PXpD_Bv0LjCiG21xLREG6tXK6phgtShOw,5035
|
|
9
|
-
eventsourcing/persistence.py,sha256=
|
|
10
|
-
eventsourcing/popo.py,sha256=
|
|
11
|
-
eventsourcing/postgres.py,sha256=
|
|
12
|
-
eventsourcing/projection.py,sha256=
|
|
9
|
+
eventsourcing/persistence.py,sha256=LrIjdEmMhM2Pz_ozGO_uOAQ099-yW92htaxO63vntwA,46406
|
|
10
|
+
eventsourcing/popo.py,sha256=8LvOmAdwVRArhvWVsmfeHSmZ4B1CVdlR2KggwVbmeWU,9131
|
|
11
|
+
eventsourcing/postgres.py,sha256=e1UGfc1qIX4RxLmUzneT3rjsV75Cres06Uckbrv4euo,43741
|
|
12
|
+
eventsourcing/projection.py,sha256=X73BHLq37bXNm9FpNYA3O1plqPunxQmi-vu0DuFiugw,14727
|
|
13
13
|
eventsourcing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
eventsourcing/sqlite.py,sha256=
|
|
14
|
+
eventsourcing/sqlite.py,sha256=8GJ6rKhX1z6CN0gXQIn6wh5pWmGvzOuMHtxcp0bO1SE,26696
|
|
15
15
|
eventsourcing/system.py,sha256=WCfuSc45A9A1fFO7zpDum_ddh4pU7x-vEcpVZ_ycAyE,44358
|
|
16
16
|
eventsourcing/tests/__init__.py,sha256=FtOyuj-L-oSisYeByTIrnUw-XzsctSbq76XmjPy5fMc,102
|
|
17
17
|
eventsourcing/tests/application.py,sha256=pAn9Cugp_1rjOtj_nruGhh7PxdrQWibDlrnOAlOKXwo,20614
|
|
18
18
|
eventsourcing/tests/domain.py,sha256=yN-F6gMRumeX6nIXIcZGxAR3RrUslzmEMM8JksnkI8Q,3227
|
|
19
|
-
eventsourcing/tests/persistence.py,sha256=
|
|
20
|
-
eventsourcing/tests/postgres_utils.py,sha256=
|
|
19
|
+
eventsourcing/tests/persistence.py,sha256=TMi4gnlBfcd7XqFlWPSsNxwnEKhaPpXu-dHHgqJUs4I,60816
|
|
20
|
+
eventsourcing/tests/postgres_utils.py,sha256=y-2ZrCZtHPqjfvBQqahtbTvvasFX2GGaMikG1BSK81A,1444
|
|
21
21
|
eventsourcing/utils.py,sha256=pOnczXzaE5q7UbQbPmgcpWaP660fsmfiDJs6Gmo8QCM,8558
|
|
22
|
-
eventsourcing-9.4.
|
|
23
|
-
eventsourcing-9.4.
|
|
24
|
-
eventsourcing-9.4.
|
|
25
|
-
eventsourcing-9.4.
|
|
26
|
-
eventsourcing-9.4.
|
|
22
|
+
eventsourcing-9.4.4.dist-info/AUTHORS,sha256=8aHOM4UbNZcKlD-cHpFRcM6RWyCqtwtxRev6DeUgVRs,137
|
|
23
|
+
eventsourcing-9.4.4.dist-info/LICENSE,sha256=CQEQzcZO8AWXL5i3hIo4yVKrYjh2FBz6hCM7kpXWpw4,1512
|
|
24
|
+
eventsourcing-9.4.4.dist-info/METADATA,sha256=C2sTYGk8meFDKMJn_w7GvoG37_9MHGMmJUgtWYhmvys,10003
|
|
25
|
+
eventsourcing-9.4.4.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
26
|
+
eventsourcing-9.4.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|