eventsourcing 9.3.5__py3-none-any.whl → 9.4.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of eventsourcing might be problematic. Click here for more details.
- eventsourcing/application.py +75 -11
- eventsourcing/cipher.py +1 -1
- eventsourcing/domain.py +29 -3
- eventsourcing/interface.py +23 -5
- eventsourcing/persistence.py +292 -71
- eventsourcing/popo.py +113 -32
- eventsourcing/postgres.py +265 -103
- eventsourcing/projection.py +157 -0
- eventsourcing/sqlite.py +143 -36
- eventsourcing/system.py +64 -42
- eventsourcing/tests/application.py +48 -12
- eventsourcing/tests/persistence.py +304 -75
- eventsourcing/utils.py +1 -1
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0a1.dist-info}/LICENSE +1 -1
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0a1.dist-info}/METADATA +2 -2
- eventsourcing-9.4.0a1.dist-info/RECORD +25 -0
- eventsourcing-9.3.5.dist-info/RECORD +0 -24
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0a1.dist-info}/AUTHORS +0 -0
- {eventsourcing-9.3.5.dist-info → eventsourcing-9.4.0a1.dist-info}/WHEEL +0 -0
eventsourcing/persistence.py
CHANGED
|
@@ -7,10 +7,12 @@ from collections import deque
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from decimal import Decimal
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from queue import Queue
|
|
11
|
+
from threading import Condition, Event, Lock, Semaphore, Thread, Timer
|
|
12
|
+
from time import monotonic, sleep, time
|
|
12
13
|
from types import ModuleType
|
|
13
14
|
from typing import (
|
|
15
|
+
TYPE_CHECKING,
|
|
14
16
|
Any,
|
|
15
17
|
Dict,
|
|
16
18
|
Generic,
|
|
@@ -35,6 +37,9 @@ from eventsourcing.utils import (
|
|
|
35
37
|
strtobool,
|
|
36
38
|
)
|
|
37
39
|
|
|
40
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
41
|
+
from typing_extensions import Self
|
|
42
|
+
|
|
38
43
|
|
|
39
44
|
class Transcoding(ABC):
|
|
40
45
|
"""
|
|
@@ -58,17 +63,6 @@ class Transcoder(ABC):
|
|
|
58
63
|
Abstract base class for transcoders.
|
|
59
64
|
"""
|
|
60
65
|
|
|
61
|
-
def __init__(self) -> None:
|
|
62
|
-
self.types: Dict[type, Transcoding] = {}
|
|
63
|
-
self.names: Dict[str, Transcoding] = {}
|
|
64
|
-
|
|
65
|
-
def register(self, transcoding: Transcoding) -> None:
|
|
66
|
-
"""
|
|
67
|
-
Registers given transcoding with the transcoder.
|
|
68
|
-
"""
|
|
69
|
-
self.types[transcoding.type] = transcoding
|
|
70
|
-
self.names[transcoding.name] = transcoding
|
|
71
|
-
|
|
72
66
|
@abstractmethod
|
|
73
67
|
def encode(self, obj: Any) -> bytes:
|
|
74
68
|
"""Encodes obj as bytes."""
|
|
@@ -84,7 +78,8 @@ class JSONTranscoder(Transcoder):
|
|
|
84
78
|
"""
|
|
85
79
|
|
|
86
80
|
def __init__(self) -> None:
|
|
87
|
-
|
|
81
|
+
self.types: Dict[type, Transcoding] = {}
|
|
82
|
+
self.names: Dict[str, Transcoding] = {}
|
|
88
83
|
self.encoder = json.JSONEncoder(
|
|
89
84
|
default=self._encode_obj,
|
|
90
85
|
separators=(",", ":"),
|
|
@@ -92,6 +87,13 @@ class JSONTranscoder(Transcoder):
|
|
|
92
87
|
)
|
|
93
88
|
self.decoder = json.JSONDecoder(object_hook=self._decode_obj)
|
|
94
89
|
|
|
90
|
+
def register(self, transcoding: Transcoding) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Registers given transcoding with the transcoder.
|
|
93
|
+
"""
|
|
94
|
+
self.types[transcoding.type] = transcoding
|
|
95
|
+
self.names[transcoding.name] = transcoding
|
|
96
|
+
|
|
95
97
|
def encode(self, obj: Any) -> bytes:
|
|
96
98
|
"""
|
|
97
99
|
Encodes given object as a bytes array.
|
|
@@ -200,19 +202,16 @@ class StoredEvent:
|
|
|
200
202
|
Frozen dataclass that represents :class:`~eventsourcing.domain.DomainEvent`
|
|
201
203
|
objects, such as aggregate :class:`~eventsourcing.domain.Aggregate.Event`
|
|
202
204
|
objects and :class:`~eventsourcing.domain.Snapshot` objects.
|
|
203
|
-
|
|
204
|
-
Constructor parameters:
|
|
205
|
-
|
|
206
|
-
:param UUID originator_id: ID of the originating aggregate
|
|
207
|
-
:param int originator_version: version of the originating aggregate
|
|
208
|
-
:param str topic: topic of the domain event object class
|
|
209
|
-
:param bytes state: serialised state of the domain event object
|
|
210
205
|
"""
|
|
211
206
|
|
|
212
207
|
originator_id: uuid.UUID
|
|
208
|
+
"""ID of the originating aggregate."""
|
|
213
209
|
originator_version: int
|
|
210
|
+
"""Position in an aggregate sequence."""
|
|
214
211
|
topic: str
|
|
212
|
+
"""Topic of a domain event object class."""
|
|
215
213
|
state: bytes
|
|
214
|
+
"""Serialised state of a domain event object."""
|
|
216
215
|
|
|
217
216
|
|
|
218
217
|
class Compressor(ABC):
|
|
@@ -406,10 +405,19 @@ class NotSupportedError(DatabaseError):
|
|
|
406
405
|
"""
|
|
407
406
|
|
|
408
407
|
|
|
408
|
+
class WaitInterruptedError(PersistenceError):
|
|
409
|
+
"""
|
|
410
|
+
Raised when waiting for a tracking record is interrupted.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class Recorder:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
|
|
409
418
|
class AggregateRecorder(ABC):
|
|
410
419
|
"""
|
|
411
|
-
Abstract base class for
|
|
412
|
-
retrieve stored events for domain model aggregates.
|
|
420
|
+
Abstract base class for inserting and selecting stored events.
|
|
413
421
|
"""
|
|
414
422
|
|
|
415
423
|
@abstractmethod
|
|
@@ -442,71 +450,135 @@ class Notification(StoredEvent):
|
|
|
442
450
|
"""
|
|
443
451
|
|
|
444
452
|
id: int
|
|
453
|
+
"""Position in an application sequence."""
|
|
445
454
|
|
|
446
455
|
|
|
447
456
|
class ApplicationRecorder(AggregateRecorder):
|
|
448
457
|
"""
|
|
449
|
-
Abstract base class for
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
Extends the behaviour of aggregate recorders by
|
|
453
|
-
recording aggregate events in a total order that
|
|
454
|
-
allows the stored events also to be retrieved
|
|
455
|
-
as event notifications.
|
|
458
|
+
Abstract base class for recording events in both aggregate
|
|
459
|
+
and application sequences.
|
|
456
460
|
"""
|
|
457
461
|
|
|
458
462
|
@abstractmethod
|
|
459
463
|
def select_notifications(
|
|
460
464
|
self,
|
|
461
|
-
start: int,
|
|
465
|
+
start: int | None,
|
|
462
466
|
limit: int,
|
|
463
467
|
stop: int | None = None,
|
|
464
468
|
topics: Sequence[str] = (),
|
|
469
|
+
*,
|
|
470
|
+
inclusive_of_start: bool = True,
|
|
465
471
|
) -> List[Notification]:
|
|
466
472
|
"""
|
|
467
|
-
Returns a list of
|
|
468
|
-
|
|
469
|
-
|
|
473
|
+
Returns a list of Notification objects representing events from an
|
|
474
|
+
application sequence. If `inclusive_of_start` is True (the default),
|
|
475
|
+
the returned Notification objects will have IDs greater than or equal
|
|
476
|
+
to `start` and less than or equal to `stop`. If `inclusive_of_start`
|
|
477
|
+
is False, the Notification objects will have IDs greater than `start`
|
|
478
|
+
and less than or equal to `stop`.
|
|
470
479
|
"""
|
|
471
480
|
|
|
472
481
|
@abstractmethod
|
|
473
|
-
def max_notification_id(self) -> int:
|
|
482
|
+
def max_notification_id(self) -> int | None:
|
|
483
|
+
"""
|
|
484
|
+
Returns the largest notification ID in an application sequence,
|
|
485
|
+
or None if no stored events have been recorded.
|
|
474
486
|
"""
|
|
475
|
-
|
|
487
|
+
|
|
488
|
+
@abstractmethod
|
|
489
|
+
def subscribe(self, gt: int | None = None) -> Subscription[ApplicationRecorder]:
|
|
476
490
|
"""
|
|
491
|
+
Returns an iterator of Notification objects representing events from an
|
|
492
|
+
application sequence.
|
|
477
493
|
|
|
494
|
+
The iterator will block after the last recorded event has been yielded, but
|
|
495
|
+
will then continue yielding newly recorded events when they are recorded.
|
|
478
496
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
Abstract base class for recorders that record and
|
|
482
|
-
retrieve stored events for domain model aggregates.
|
|
497
|
+
Notifications will have IDs greater than the optional `gt` argument.
|
|
498
|
+
"""
|
|
483
499
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
500
|
+
|
|
501
|
+
class TrackingRecorder(Recorder, ABC):
|
|
502
|
+
"""
|
|
503
|
+
Abstract base class for recorders that record tracking
|
|
504
|
+
objects atomically with other state.
|
|
488
505
|
"""
|
|
489
506
|
|
|
490
507
|
@abstractmethod
|
|
491
|
-
def
|
|
508
|
+
def insert_tracking(self, tracking: Tracking) -> None:
|
|
509
|
+
"""
|
|
510
|
+
Records a tracking object.
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
@abstractmethod
|
|
514
|
+
def max_tracking_id(self, application_name: str) -> int | None:
|
|
492
515
|
"""
|
|
493
|
-
Returns the largest notification ID across all tracking
|
|
494
|
-
for the named application
|
|
495
|
-
records.
|
|
516
|
+
Returns the largest notification ID across all recorded tracking objects
|
|
517
|
+
for the named application, or None if no tracking objects have been recorded.
|
|
496
518
|
"""
|
|
497
519
|
|
|
498
520
|
@abstractmethod
|
|
499
521
|
def has_tracking_id(self, application_name: str, notification_id: int) -> bool:
|
|
500
522
|
"""
|
|
501
|
-
Returns
|
|
502
|
-
and notification ID
|
|
523
|
+
Returns True if a tracking object with the given application name
|
|
524
|
+
and notification ID has been recorded, otherwise returns False.
|
|
503
525
|
"""
|
|
504
526
|
|
|
527
|
+
def wait(
|
|
528
|
+
self,
|
|
529
|
+
application_name: str,
|
|
530
|
+
notification_id: int,
|
|
531
|
+
timeout: float = 1.0,
|
|
532
|
+
interrupt: Event | None = None,
|
|
533
|
+
) -> None:
|
|
534
|
+
"""
|
|
535
|
+
Block until a tracking object with the given application name and
|
|
536
|
+
notification ID has been recorded.
|
|
537
|
+
|
|
538
|
+
Polls has_tracking_id() with exponential backoff until the timeout
|
|
539
|
+
is reached, or until the optional interrupt event is set.
|
|
540
|
+
|
|
541
|
+
The timeout argument should be a floating point number specifying a
|
|
542
|
+
timeout for the operation in seconds (or fractions thereof). The default
|
|
543
|
+
is 1.0 seconds.
|
|
544
|
+
|
|
545
|
+
Raises TimeoutError if the timeout is reached.
|
|
546
|
+
|
|
547
|
+
Raises WaitInterruptError if the `interrupt` is set before `timeout` is reached.
|
|
548
|
+
"""
|
|
549
|
+
deadline = monotonic() + timeout
|
|
550
|
+
delay_ms = 1.0
|
|
551
|
+
while True:
|
|
552
|
+
if self.has_tracking_id(application_name, notification_id):
|
|
553
|
+
break
|
|
554
|
+
if interrupt:
|
|
555
|
+
if interrupt.wait(timeout=delay_ms / 1000):
|
|
556
|
+
raise WaitInterruptedError
|
|
557
|
+
else:
|
|
558
|
+
sleep(delay_ms / 1000)
|
|
559
|
+
delay_ms *= 2
|
|
560
|
+
remaining = deadline - monotonic()
|
|
561
|
+
if remaining < 0:
|
|
562
|
+
msg = (
|
|
563
|
+
f"Timed out waiting for notification {notification_id} "
|
|
564
|
+
f"from application '{application_name}' to be processed"
|
|
565
|
+
)
|
|
566
|
+
raise TimeoutError(msg)
|
|
567
|
+
delay_ms = min(delay_ms, remaining * 1000)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class ProcessRecorder(TrackingRecorder, ApplicationRecorder, ABC):
|
|
571
|
+
pass
|
|
572
|
+
|
|
505
573
|
|
|
506
574
|
@dataclass(frozen=True)
|
|
507
575
|
class Recording:
|
|
576
|
+
"""Represents the recording of a domain event."""
|
|
577
|
+
|
|
508
578
|
domain_event: DomainEventProtocol
|
|
579
|
+
"""The domain event that has been recorded."""
|
|
509
580
|
notification: Notification
|
|
581
|
+
"""A Notification that represents the domain event in the application sequence."""
|
|
510
582
|
|
|
511
583
|
|
|
512
584
|
class EventStore:
|
|
@@ -573,31 +645,39 @@ class EventStore:
|
|
|
573
645
|
|
|
574
646
|
|
|
575
647
|
TInfrastructureFactory = TypeVar(
|
|
576
|
-
"TInfrastructureFactory", bound="InfrastructureFactory"
|
|
648
|
+
"TInfrastructureFactory", bound="InfrastructureFactory[Any]"
|
|
577
649
|
)
|
|
578
650
|
|
|
651
|
+
TTrackingRecorder = TypeVar("TTrackingRecorder", bound=TrackingRecorder)
|
|
652
|
+
|
|
579
653
|
|
|
580
|
-
class InfrastructureFactory(ABC):
|
|
654
|
+
class InfrastructureFactory(ABC, Generic[TTrackingRecorder]):
|
|
581
655
|
"""
|
|
582
656
|
Abstract base class for infrastructure factories.
|
|
583
657
|
"""
|
|
584
658
|
|
|
585
659
|
PERSISTENCE_MODULE = "PERSISTENCE_MODULE"
|
|
660
|
+
TRANSCODER_TOPIC = "TRANSCODER_TOPIC"
|
|
586
661
|
MAPPER_TOPIC = "MAPPER_TOPIC"
|
|
587
662
|
CIPHER_TOPIC = "CIPHER_TOPIC"
|
|
588
663
|
COMPRESSOR_TOPIC = "COMPRESSOR_TOPIC"
|
|
589
664
|
IS_SNAPSHOTTING_ENABLED = "IS_SNAPSHOTTING_ENABLED"
|
|
665
|
+
APPLICATION_RECORDER_TOPIC = "APPLICATION_RECORDER_TOPIC"
|
|
666
|
+
TRACKING_RECORDER_TOPIC = "TRACKING_RECORDER_TOPIC"
|
|
667
|
+
PROCESS_RECORDER_TOPIC = "PROCESS_RECORDER_TOPIC"
|
|
590
668
|
|
|
591
669
|
@classmethod
|
|
592
670
|
def construct(
|
|
593
|
-
cls: Type[TInfrastructureFactory], env: Environment
|
|
671
|
+
cls: Type[TInfrastructureFactory], env: Environment | None = None
|
|
594
672
|
) -> TInfrastructureFactory:
|
|
595
673
|
"""
|
|
596
674
|
Constructs concrete infrastructure factory for given
|
|
597
675
|
named application. Reads and resolves persistence
|
|
598
676
|
topic from environment variable 'PERSISTENCE_MODULE'.
|
|
599
677
|
"""
|
|
600
|
-
factory_cls: Type[InfrastructureFactory]
|
|
678
|
+
factory_cls: Type[InfrastructureFactory[TrackingRecorder]]
|
|
679
|
+
if env is None:
|
|
680
|
+
env = Environment()
|
|
601
681
|
topic = (
|
|
602
682
|
env.get(
|
|
603
683
|
cls.PERSISTENCE_MODULE,
|
|
@@ -614,7 +694,9 @@ class InfrastructureFactory(ABC):
|
|
|
614
694
|
or "eventsourcing.popo"
|
|
615
695
|
)
|
|
616
696
|
try:
|
|
617
|
-
obj: Type[InfrastructureFactory] | ModuleType =
|
|
697
|
+
obj: Type[InfrastructureFactory[TrackingRecorder]] | ModuleType = (
|
|
698
|
+
resolve_topic(topic)
|
|
699
|
+
)
|
|
618
700
|
except TopicError as e:
|
|
619
701
|
msg = (
|
|
620
702
|
"Failed to resolve persistence module topic: "
|
|
@@ -625,15 +707,15 @@ class InfrastructureFactory(ABC):
|
|
|
625
707
|
|
|
626
708
|
if isinstance(obj, ModuleType):
|
|
627
709
|
# Find the factory in the module.
|
|
628
|
-
factory_classes: List[Type[InfrastructureFactory]] = [
|
|
629
|
-
|
|
630
|
-
for member in obj.__dict__.values()
|
|
710
|
+
factory_classes: List[Type[InfrastructureFactory[TrackingRecorder]]] = []
|
|
711
|
+
for member in obj.__dict__.values():
|
|
631
712
|
if (
|
|
632
713
|
member is not InfrastructureFactory
|
|
633
|
-
and isinstance(member, type)
|
|
634
|
-
and issubclass(member, InfrastructureFactory)
|
|
635
|
-
|
|
636
|
-
|
|
714
|
+
and isinstance(member, type) # Look for classes...
|
|
715
|
+
and issubclass(member, InfrastructureFactory) # Ignore base class.
|
|
716
|
+
and member not in factory_classes # Forgive aliases.
|
|
717
|
+
):
|
|
718
|
+
factory_classes.append(member)
|
|
637
719
|
if len(factory_classes) == 1:
|
|
638
720
|
factory_cls = factory_classes[0]
|
|
639
721
|
else:
|
|
@@ -661,18 +743,27 @@ class InfrastructureFactory(ABC):
|
|
|
661
743
|
"""
|
|
662
744
|
Constructs a transcoder.
|
|
663
745
|
"""
|
|
664
|
-
|
|
665
|
-
|
|
746
|
+
transcoder_topic = self.env.get(self.TRANSCODER_TOPIC)
|
|
747
|
+
if transcoder_topic:
|
|
748
|
+
transcoder_class: Type[Transcoder] = resolve_topic(transcoder_topic)
|
|
749
|
+
else:
|
|
750
|
+
transcoder_class = JSONTranscoder
|
|
751
|
+
return transcoder_class()
|
|
666
752
|
|
|
667
753
|
def mapper(
|
|
668
|
-
self,
|
|
754
|
+
self,
|
|
755
|
+
transcoder: Transcoder | None = None,
|
|
756
|
+
mapper_class: Type[Mapper] | None = None,
|
|
669
757
|
) -> Mapper:
|
|
670
758
|
"""
|
|
671
759
|
Constructs a mapper.
|
|
672
760
|
"""
|
|
673
|
-
|
|
761
|
+
if mapper_class is None:
|
|
762
|
+
mapper_topic = self.env.get(self.MAPPER_TOPIC)
|
|
763
|
+
mapper_class = resolve_topic(mapper_topic) if mapper_topic else Mapper
|
|
764
|
+
|
|
674
765
|
return mapper_class(
|
|
675
|
-
transcoder=transcoder,
|
|
766
|
+
transcoder=transcoder or self.transcoder(),
|
|
676
767
|
cipher=self.cipher(),
|
|
677
768
|
compressor=self.compressor(),
|
|
678
769
|
)
|
|
@@ -712,12 +803,18 @@ class InfrastructureFactory(ABC):
|
|
|
712
803
|
compressor = compressor_cls
|
|
713
804
|
return compressor
|
|
714
805
|
|
|
715
|
-
|
|
716
|
-
|
|
806
|
+
def event_store(
|
|
807
|
+
self,
|
|
808
|
+
mapper: Mapper | None = None,
|
|
809
|
+
recorder: AggregateRecorder | None = None,
|
|
810
|
+
) -> EventStore:
|
|
717
811
|
"""
|
|
718
812
|
Constructs an event store.
|
|
719
813
|
"""
|
|
720
|
-
return EventStore(
|
|
814
|
+
return EventStore(
|
|
815
|
+
mapper=mapper or self.mapper(),
|
|
816
|
+
recorder=recorder or self.application_recorder(),
|
|
817
|
+
)
|
|
721
818
|
|
|
722
819
|
@abstractmethod
|
|
723
820
|
def aggregate_recorder(self, purpose: str = "events") -> AggregateRecorder:
|
|
@@ -731,6 +828,14 @@ class InfrastructureFactory(ABC):
|
|
|
731
828
|
Constructs an application recorder.
|
|
732
829
|
"""
|
|
733
830
|
|
|
831
|
+
@abstractmethod
|
|
832
|
+
def tracking_recorder(
|
|
833
|
+
self, tracking_recorder_class: Type[TTrackingRecorder] | None = None
|
|
834
|
+
) -> TTrackingRecorder:
|
|
835
|
+
"""
|
|
836
|
+
Constructs a tracking recorder.
|
|
837
|
+
"""
|
|
838
|
+
|
|
734
839
|
@abstractmethod
|
|
735
840
|
def process_recorder(self) -> ProcessRecorder:
|
|
736
841
|
"""
|
|
@@ -747,7 +852,7 @@ class InfrastructureFactory(ABC):
|
|
|
747
852
|
|
|
748
853
|
def close(self) -> None:
|
|
749
854
|
"""
|
|
750
|
-
Closes any database connections,
|
|
855
|
+
Closes any database connections, and anything else that needs closing.
|
|
751
856
|
"""
|
|
752
857
|
|
|
753
858
|
|
|
@@ -1197,3 +1302,119 @@ class ConnectionPool(ABC, Generic[TConnection]):
|
|
|
1197
1302
|
|
|
1198
1303
|
def __del__(self) -> None:
|
|
1199
1304
|
self.close()
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
TApplicationRecorder_co = TypeVar(
|
|
1308
|
+
"TApplicationRecorder_co", bound=ApplicationRecorder, covariant=True
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
class Subscription(Iterator[Notification], Generic[TApplicationRecorder_co]):
|
|
1313
|
+
def __init__(
|
|
1314
|
+
self, recorder: TApplicationRecorder_co, gt: int | None = None
|
|
1315
|
+
) -> None:
|
|
1316
|
+
self._recorder = recorder
|
|
1317
|
+
self._last_notification_id = gt
|
|
1318
|
+
self._has_been_entered = False
|
|
1319
|
+
self._has_been_stopped = False
|
|
1320
|
+
|
|
1321
|
+
def __enter__(self) -> Self:
|
|
1322
|
+
if self._has_been_entered:
|
|
1323
|
+
msg = "Already entered subscription context manager"
|
|
1324
|
+
raise ProgrammingError(msg)
|
|
1325
|
+
self._has_been_entered = True
|
|
1326
|
+
return self
|
|
1327
|
+
|
|
1328
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
1329
|
+
if not self._has_been_entered:
|
|
1330
|
+
msg = "Not already entered subscription context manager"
|
|
1331
|
+
raise ProgrammingError(msg)
|
|
1332
|
+
self.stop()
|
|
1333
|
+
|
|
1334
|
+
def stop(self) -> None:
|
|
1335
|
+
"""
|
|
1336
|
+
Stops the subscription.
|
|
1337
|
+
"""
|
|
1338
|
+
self._has_been_stopped = True
|
|
1339
|
+
|
|
1340
|
+
def __iter__(self) -> Self:
|
|
1341
|
+
return self
|
|
1342
|
+
|
|
1343
|
+
@abstractmethod
|
|
1344
|
+
def __next__(self) -> Notification:
|
|
1345
|
+
"""
|
|
1346
|
+
Returns the next Notification object in the application sequence.
|
|
1347
|
+
"""
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
class ListenNotifySubscription(Subscription[TApplicationRecorder_co]):
|
|
1351
|
+
def __init__(
|
|
1352
|
+
self, recorder: TApplicationRecorder_co, gt: int | None = None
|
|
1353
|
+
) -> None:
|
|
1354
|
+
super().__init__(recorder=recorder, gt=gt)
|
|
1355
|
+
self._select_limit = 500
|
|
1356
|
+
self._notifications: List[Notification] = []
|
|
1357
|
+
self._notifications_index: int = 0
|
|
1358
|
+
self._notifications_queue: Queue[List[Notification]] = Queue(maxsize=10)
|
|
1359
|
+
self._has_been_notified = Event()
|
|
1360
|
+
self._thread_error: BaseException | None = None
|
|
1361
|
+
self._pull_thread = Thread(target=self._loop_on_pull)
|
|
1362
|
+
self._pull_thread.start()
|
|
1363
|
+
|
|
1364
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
1365
|
+
super().__exit__(*args, **kwargs)
|
|
1366
|
+
self._pull_thread.join()
|
|
1367
|
+
|
|
1368
|
+
def stop(self) -> None:
|
|
1369
|
+
"""
|
|
1370
|
+
Stops the subscription.
|
|
1371
|
+
"""
|
|
1372
|
+
super().stop()
|
|
1373
|
+
self._notifications_queue.put([])
|
|
1374
|
+
self._has_been_notified.set()
|
|
1375
|
+
|
|
1376
|
+
def __next__(self) -> Notification:
|
|
1377
|
+
# If necessary, get a new list of notifications from the recorder.
|
|
1378
|
+
if (
|
|
1379
|
+
self._notifications_index == len(self._notifications)
|
|
1380
|
+
and not self._has_been_stopped
|
|
1381
|
+
):
|
|
1382
|
+
self._notifications = self._notifications_queue.get()
|
|
1383
|
+
self._notifications_index = 0
|
|
1384
|
+
|
|
1385
|
+
# Stop the iteration if necessary, maybe raise thread error.
|
|
1386
|
+
if self._has_been_stopped or not self._notifications:
|
|
1387
|
+
if self._thread_error is not None:
|
|
1388
|
+
raise self._thread_error
|
|
1389
|
+
raise StopIteration
|
|
1390
|
+
|
|
1391
|
+
# Return a notification from previously obtained list.
|
|
1392
|
+
notification = self._notifications[self._notifications_index]
|
|
1393
|
+
self._notifications_index += 1
|
|
1394
|
+
return notification
|
|
1395
|
+
|
|
1396
|
+
def _loop_on_pull(self) -> None:
|
|
1397
|
+
try:
|
|
1398
|
+
self._pull() # Already recorded events.
|
|
1399
|
+
while not self._has_been_stopped:
|
|
1400
|
+
self._has_been_notified.wait()
|
|
1401
|
+
self._pull() # Newly recorded events.
|
|
1402
|
+
except BaseException as e:
|
|
1403
|
+
if self._thread_error is None:
|
|
1404
|
+
self._thread_error = e
|
|
1405
|
+
self.stop()
|
|
1406
|
+
|
|
1407
|
+
def _pull(self) -> None:
|
|
1408
|
+
while not self._has_been_stopped:
|
|
1409
|
+
self._has_been_notified.clear()
|
|
1410
|
+
notifications = self._recorder.select_notifications(
|
|
1411
|
+
start=self._last_notification_id or 0,
|
|
1412
|
+
limit=self._select_limit,
|
|
1413
|
+
inclusive_of_start=False,
|
|
1414
|
+
)
|
|
1415
|
+
if len(notifications) > 0:
|
|
1416
|
+
# print("Putting", len(notifications), "notifications into queue")
|
|
1417
|
+
self._notifications_queue.put(notifications)
|
|
1418
|
+
self._last_notification_id = notifications[-1].id
|
|
1419
|
+
if len(notifications) < self._select_limit:
|
|
1420
|
+
break
|