eventsourcing 9.4.0a1__tar.gz → 9.4.0a2__tar.gz
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-9.4.0a1 → eventsourcing-9.4.0a2}/PKG-INFO +8 -6
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/README.md +1 -1
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/application.py +2 -50
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/cipher.py +4 -2
- eventsourcing-9.4.0a2/eventsourcing/cryptography.py +96 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/domain.py +1 -7
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/persistence.py +1 -1
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/popo.py +1 -1
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/postgres.py +1 -1
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/projection.py +50 -7
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/sqlite.py +1 -1
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/system.py +1 -1
- eventsourcing-9.4.0a2/eventsourcing/tests/__init__.py +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/tests/application.py +0 -41
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/utils.py +1 -1
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/pyproject.toml +29 -7
- eventsourcing-9.4.0a1/eventsourcing/__init__.py +0 -1
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/AUTHORS +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/LICENSE +0 -0
- {eventsourcing-9.4.0a1/eventsourcing/tests → eventsourcing-9.4.0a2/eventsourcing}/__init__.py +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/compressor.py +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/dispatch.py +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/interface.py +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/py.typed +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/tests/domain.py +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/tests/persistence.py +0 -0
- {eventsourcing-9.4.0a1 → eventsourcing-9.4.0a2}/eventsourcing/tests/postgres_utils.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: eventsourcing
|
|
3
|
-
Version: 9.4.
|
|
3
|
+
Version: 9.4.0a2
|
|
4
4
|
Summary: Event sourcing in Python
|
|
5
5
|
Home-page: https://github.com/pyeventsourcing/eventsourcing
|
|
6
6
|
License: BSD 3-Clause
|
|
7
7
|
Keywords: event sourcing,event store,domain driven design,domain-driven design,ddd,cqrs,cqs
|
|
8
8
|
Author: John Bywater
|
|
9
9
|
Author-email: john.bywater@appropriatesoftware.net
|
|
10
|
-
Requires-Python: >=3.8
|
|
10
|
+
Requires-Python: >=3.8, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Intended Audience :: Education
|
|
@@ -18,22 +18,24 @@ Classifier: Operating System :: OS Independent
|
|
|
18
18
|
Classifier: Programming Language :: Python
|
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
27
|
Provides-Extra: crypto
|
|
28
|
+
Provides-Extra: cryptography
|
|
28
29
|
Provides-Extra: postgres
|
|
29
30
|
Requires-Dist: backports.zoneinfo ; python_version < "3.9"
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: cryptography (>=44.0,<44.1) ; extra == "cryptography"
|
|
32
|
+
Requires-Dist: psycopg[pool] (<=3.2.99999) ; extra == "postgres"
|
|
33
|
+
Requires-Dist: pycryptodome (>=3.22,<3.23) ; extra == "crypto"
|
|
32
34
|
Requires-Dist: typing_extensions
|
|
33
35
|
Project-URL: Repository, https://github.com/pyeventsourcing/eventsourcing
|
|
34
36
|
Description-Content-Type: text/markdown
|
|
35
37
|
|
|
36
|
-
[](https://github.com/pyeventsourcing/eventsourcing)
|
|
37
39
|
[](https://coveralls.io/github/pyeventsourcing/eventsourcing?branch=main)
|
|
38
40
|
[](https://eventsourcing.readthedocs.io/en/stable/)
|
|
39
41
|
[](https://pypi.org/project/eventsourcing/)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[](https://github.com/pyeventsourcing/eventsourcing)
|
|
2
2
|
[](https://coveralls.io/github/pyeventsourcing/eventsourcing?branch=main)
|
|
3
3
|
[](https://eventsourcing.readthedocs.io/en/stable/)
|
|
4
4
|
[](https://pypi.org/project/eventsourcing/)
|
|
@@ -25,7 +25,7 @@ from typing import (
|
|
|
25
25
|
)
|
|
26
26
|
from warnings import warn
|
|
27
27
|
|
|
28
|
-
from typing_extensions import
|
|
28
|
+
from typing_extensions import deprecated
|
|
29
29
|
|
|
30
30
|
from eventsourcing.domain import (
|
|
31
31
|
Aggregate,
|
|
@@ -57,7 +57,7 @@ from eventsourcing.persistence import (
|
|
|
57
57
|
)
|
|
58
58
|
from eventsourcing.utils import Environment, EnvType, strtobool
|
|
59
59
|
|
|
60
|
-
if TYPE_CHECKING:
|
|
60
|
+
if TYPE_CHECKING:
|
|
61
61
|
from uuid import UUID
|
|
62
62
|
|
|
63
63
|
ProjectorFunction = Callable[
|
|
@@ -903,21 +903,6 @@ class Application:
|
|
|
903
903
|
need to take action when new domain events have been saved.
|
|
904
904
|
"""
|
|
905
905
|
|
|
906
|
-
def subscribe(self, gt: int | None = None) -> ApplicationSubscription:
|
|
907
|
-
"""
|
|
908
|
-
Returns an iterator that yields all domain events recorded in an application
|
|
909
|
-
sequence that have notification IDs greater than a given value. The iterator
|
|
910
|
-
will block when all recorded domain events have been yielded, and then
|
|
911
|
-
continue when new events are recorded. Domain events are returned along
|
|
912
|
-
with tracking objects that identify the position in the application sequence.
|
|
913
|
-
"""
|
|
914
|
-
return ApplicationSubscription(
|
|
915
|
-
name=self.name,
|
|
916
|
-
recorder=self.recorder,
|
|
917
|
-
mapper=self.mapper,
|
|
918
|
-
gt=gt,
|
|
919
|
-
)
|
|
920
|
-
|
|
921
906
|
def close(self) -> None:
|
|
922
907
|
self.closing.set()
|
|
923
908
|
self.factory.close()
|
|
@@ -1042,36 +1027,3 @@ class EventSourcedLog(Generic[TDomainEvent]):
|
|
|
1042
1027
|
limit=limit,
|
|
1043
1028
|
),
|
|
1044
1029
|
)
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
class ApplicationSubscription:
|
|
1048
|
-
def __init__(
|
|
1049
|
-
self,
|
|
1050
|
-
name: str,
|
|
1051
|
-
recorder: ApplicationRecorder,
|
|
1052
|
-
mapper: Mapper,
|
|
1053
|
-
gt: int | None = None,
|
|
1054
|
-
):
|
|
1055
|
-
self.name = name
|
|
1056
|
-
self.recorder = recorder
|
|
1057
|
-
self.mapper = mapper
|
|
1058
|
-
self.subscription = self.recorder.subscribe(gt=gt)
|
|
1059
|
-
|
|
1060
|
-
def __enter__(self) -> Self:
|
|
1061
|
-
self.subscription.__enter__()
|
|
1062
|
-
return self
|
|
1063
|
-
|
|
1064
|
-
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
1065
|
-
self.subscription.__exit__(*args, **kwargs)
|
|
1066
|
-
|
|
1067
|
-
def __iter__(self) -> Self:
|
|
1068
|
-
return self
|
|
1069
|
-
|
|
1070
|
-
def __next__(self) -> Tuple[DomainEventProtocol, Tracking]:
|
|
1071
|
-
notification = next(self.subscription)
|
|
1072
|
-
tracking = Tracking(self.name, notification.id)
|
|
1073
|
-
domain_event = self.mapper.to_domain_event(notification)
|
|
1074
|
-
return domain_event, tracking
|
|
1075
|
-
|
|
1076
|
-
def __del__(self) -> None:
|
|
1077
|
-
self.subscription.stop()
|
|
@@ -10,13 +10,14 @@ from Crypto.Cipher.AES import key_size
|
|
|
10
10
|
|
|
11
11
|
from eventsourcing.persistence import Cipher
|
|
12
12
|
|
|
13
|
-
if TYPE_CHECKING:
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
14
|
from eventsourcing.utils import Environment
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class AESCipher(Cipher):
|
|
18
18
|
"""
|
|
19
|
-
Cipher strategy that uses AES cipher in GCM mode
|
|
19
|
+
Cipher strategy that uses AES cipher (in GCM mode)
|
|
20
|
+
from the Python pycryptodome package.
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
CIPHER_KEY = "CIPHER_KEY"
|
|
@@ -71,6 +72,7 @@ class AESCipher(Cipher):
|
|
|
71
72
|
|
|
72
73
|
# Return ciphertext.
|
|
73
74
|
return nonce + tag + encrypted
|
|
75
|
+
# return nonce + tag + encrypted
|
|
74
76
|
|
|
75
77
|
def construct_cipher(self, nonce: bytes) -> GcmMode:
|
|
76
78
|
cipher = AES.new(
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from base64 import b64decode, b64encode
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from cryptography.exceptions import InvalidTag
|
|
8
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
9
|
+
|
|
10
|
+
from eventsourcing.persistence import Cipher
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from eventsourcing.utils import Environment
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AESCipher(Cipher):
|
|
17
|
+
"""
|
|
18
|
+
Cipher strategy that uses AES cipher (in GCM mode)
|
|
19
|
+
from the Python cryptography package.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
CIPHER_KEY = "CIPHER_KEY"
|
|
23
|
+
KEY_SIZES = (16, 24, 32)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def create_key(num_bytes: int) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Creates AES cipher key, with length num_bytes.
|
|
29
|
+
|
|
30
|
+
:param num_bytes: An int value, either 16, 24, or 32.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
AESCipher.check_key_size(num_bytes)
|
|
34
|
+
key = AESGCM.generate_key(num_bytes * 8)
|
|
35
|
+
return b64encode(key).decode("utf8")
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def check_key_size(num_bytes: int) -> None:
|
|
39
|
+
if num_bytes not in AESCipher.KEY_SIZES:
|
|
40
|
+
msg = f"Invalid key size: {num_bytes} not in {AESCipher.KEY_SIZES}"
|
|
41
|
+
raise ValueError(msg)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def random_bytes(num_bytes: int) -> bytes:
|
|
45
|
+
return os.urandom(num_bytes)
|
|
46
|
+
|
|
47
|
+
def __init__(self, environment: Environment):
|
|
48
|
+
"""
|
|
49
|
+
Initialises AES cipher with ``cipher_key``.
|
|
50
|
+
|
|
51
|
+
:param str cipher_key: 16, 24, or 32 bytes encoded as base64
|
|
52
|
+
"""
|
|
53
|
+
cipher_key = environment.get(self.CIPHER_KEY)
|
|
54
|
+
if not cipher_key:
|
|
55
|
+
msg = f"'{self.CIPHER_KEY}' not in env"
|
|
56
|
+
raise OSError(msg)
|
|
57
|
+
key = b64decode(cipher_key.encode("utf8"))
|
|
58
|
+
AESCipher.check_key_size(len(key))
|
|
59
|
+
self.key = key
|
|
60
|
+
|
|
61
|
+
def encrypt(self, plaintext: bytes) -> bytes:
|
|
62
|
+
"""Return ciphertext for given plaintext."""
|
|
63
|
+
|
|
64
|
+
# Construct AES-GCM cipher, with 96-bit nonce.
|
|
65
|
+
aesgcm = AESGCM(self.key)
|
|
66
|
+
nonce = AESCipher.random_bytes(12)
|
|
67
|
+
res = aesgcm.encrypt(nonce, plaintext, None)
|
|
68
|
+
# Put tag at the front for compatibility with eventsourcing.crypto.AESCipher.
|
|
69
|
+
tag = res[-16:]
|
|
70
|
+
encrypted = res[:-16]
|
|
71
|
+
return nonce + tag + encrypted
|
|
72
|
+
|
|
73
|
+
def decrypt(self, ciphertext: bytes) -> bytes:
|
|
74
|
+
"""Return plaintext for given ciphertext."""
|
|
75
|
+
|
|
76
|
+
# Split out the nonce, tag, and encrypted data.
|
|
77
|
+
nonce = ciphertext[:12]
|
|
78
|
+
if len(nonce) != 12:
|
|
79
|
+
msg = "Damaged cipher text: invalid nonce length"
|
|
80
|
+
raise ValueError(msg)
|
|
81
|
+
|
|
82
|
+
# Expect tag at the front.
|
|
83
|
+
tag = ciphertext[12:28]
|
|
84
|
+
if len(tag) != 16:
|
|
85
|
+
msg = "Damaged cipher text: invalid tag length"
|
|
86
|
+
raise ValueError(msg)
|
|
87
|
+
encrypted = ciphertext[28:]
|
|
88
|
+
|
|
89
|
+
aesgcm = AESGCM(self.key)
|
|
90
|
+
try:
|
|
91
|
+
plaintext = aesgcm.decrypt(nonce, encrypted + tag, None)
|
|
92
|
+
except InvalidTag as e:
|
|
93
|
+
msg = "Invalid cipher tag"
|
|
94
|
+
raise ValueError(msg) from e
|
|
95
|
+
# Decrypt and verify.
|
|
96
|
+
return plaintext
|
|
@@ -410,7 +410,7 @@ def _spec_filter_kwargs_for_method_params(method: Callable[..., Any]) -> set[str
|
|
|
410
410
|
return set(method_signature.parameters)
|
|
411
411
|
|
|
412
412
|
|
|
413
|
-
if TYPE_CHECKING:
|
|
413
|
+
if TYPE_CHECKING:
|
|
414
414
|
EventSpecType = Union[str, Type[CanMutateAggregate]]
|
|
415
415
|
|
|
416
416
|
CommandMethod = Callable[..., None]
|
|
@@ -1552,12 +1552,6 @@ class VersionError(OriginatorVersionError):
|
|
|
1552
1552
|
|
|
1553
1553
|
|
|
1554
1554
|
class SnapshotProtocol(DomainEventProtocol, Protocol):
|
|
1555
|
-
@property
|
|
1556
|
-
def topic(self) -> str:
|
|
1557
|
-
"""
|
|
1558
|
-
Snapshots have a read-only 'topic'.
|
|
1559
|
-
"""
|
|
1560
|
-
|
|
1561
1555
|
@property
|
|
1562
1556
|
def state(self) -> Dict[str, Any]:
|
|
1563
1557
|
"""
|
|
@@ -5,11 +5,12 @@ import weakref
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from threading import Event, Thread
|
|
7
7
|
from traceback import format_exc
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Dict, Generic, Type, TypeVar
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, Generic, Iterator, Tuple, Type, TypeVar
|
|
9
9
|
from warnings import warn
|
|
10
10
|
|
|
11
11
|
from eventsourcing.application import Application
|
|
12
12
|
from eventsourcing.dispatch import singledispatchmethod
|
|
13
|
+
from eventsourcing.domain import DomainEventProtocol
|
|
13
14
|
from eventsourcing.persistence import (
|
|
14
15
|
InfrastructureFactory,
|
|
15
16
|
Tracking,
|
|
@@ -19,11 +20,50 @@ from eventsourcing.persistence import (
|
|
|
19
20
|
)
|
|
20
21
|
from eventsourcing.utils import Environment, EnvType
|
|
21
22
|
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
+
if TYPE_CHECKING:
|
|
23
24
|
from typing_extensions import Self
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
class ApplicationSubscription(Iterator[Tuple[DomainEventProtocol, Tracking]]):
|
|
28
|
+
"""
|
|
29
|
+
An iterator that yields all domain events recorded in an application
|
|
30
|
+
sequence that have notification IDs greater than a given value. The iterator
|
|
31
|
+
will block when all recorded domain events have been yielded, and then
|
|
32
|
+
continue when new events are recorded. Domain events are returned along
|
|
33
|
+
with tracking objects that identify the position in the application sequence.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
app: Application,
|
|
39
|
+
gt: int | None = None,
|
|
40
|
+
):
|
|
41
|
+
self.name = app.name
|
|
42
|
+
self.recorder = app.recorder
|
|
43
|
+
self.mapper = app.mapper
|
|
44
|
+
self.subscription = self.recorder.subscribe(gt=gt)
|
|
45
|
+
|
|
46
|
+
def __enter__(self) -> Self:
|
|
47
|
+
self.subscription.__enter__()
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
51
|
+
self.subscription.__exit__(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
def __iter__(self) -> Self:
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def __next__(self) -> Tuple[DomainEventProtocol, Tracking]:
|
|
57
|
+
notification = next(self.subscription)
|
|
58
|
+
tracking = Tracking(self.name, notification.id)
|
|
59
|
+
domain_event = self.mapper.to_domain_event(notification)
|
|
60
|
+
return domain_event, tracking
|
|
61
|
+
|
|
62
|
+
def __del__(self) -> None:
|
|
63
|
+
self.stop()
|
|
64
|
+
|
|
65
|
+
def stop(self) -> None:
|
|
66
|
+
self.subscription.stop()
|
|
27
67
|
|
|
28
68
|
|
|
29
69
|
class Projection(ABC, Generic[TTrackingRecorder]):
|
|
@@ -46,6 +86,8 @@ class Projection(ABC, Generic[TTrackingRecorder]):
|
|
|
46
86
|
|
|
47
87
|
|
|
48
88
|
TProjection = TypeVar("TProjection", bound=Projection[Any])
|
|
89
|
+
|
|
90
|
+
|
|
49
91
|
TApplication = TypeVar("TApplication", bound=Application)
|
|
50
92
|
|
|
51
93
|
|
|
@@ -70,8 +112,9 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
70
112
|
self.projection_factory.tracking_recorder(tracking_recorder_class)
|
|
71
113
|
)
|
|
72
114
|
|
|
73
|
-
self.subscription =
|
|
74
|
-
|
|
115
|
+
self.subscription = ApplicationSubscription(
|
|
116
|
+
app=self.app,
|
|
117
|
+
gt=self.tracking_recorder.max_tracking_id(self.app.name),
|
|
75
118
|
)
|
|
76
119
|
self.projection = projection_class(
|
|
77
120
|
tracking_recorder=self.tracking_recorder,
|
|
@@ -100,7 +143,7 @@ class ProjectionRunner(Generic[TApplication, TTrackingRecorder]):
|
|
|
100
143
|
return Environment(name, _env)
|
|
101
144
|
|
|
102
145
|
def stop(self) -> None:
|
|
103
|
-
self.subscription.
|
|
146
|
+
self.subscription.stop()
|
|
104
147
|
|
|
105
148
|
@staticmethod
|
|
106
149
|
def _process_events_loop(
|
|
File without changes
|
|
@@ -20,7 +20,6 @@ from eventsourcing.persistence import (
|
|
|
20
20
|
InfrastructureFactory,
|
|
21
21
|
IntegrityError,
|
|
22
22
|
JSONTranscoder,
|
|
23
|
-
Tracking,
|
|
24
23
|
Transcoding,
|
|
25
24
|
)
|
|
26
25
|
from eventsourcing.tests.domain import BankAccount, EmailAddress
|
|
@@ -490,43 +489,3 @@ class ApplicationTestCase(TestCase):
|
|
|
490
489
|
self.assertEqual(
|
|
491
490
|
"'log' is deprecated, use 'notifications' instead", w[-1].message.args[0]
|
|
492
491
|
)
|
|
493
|
-
|
|
494
|
-
def test_catchup_subscription(self):
|
|
495
|
-
app = Application()
|
|
496
|
-
|
|
497
|
-
max_notification_id = app.recorder.max_notification_id()
|
|
498
|
-
|
|
499
|
-
aggregate = Aggregate()
|
|
500
|
-
aggregate.trigger_event(Aggregate.Event)
|
|
501
|
-
aggregate.trigger_event(Aggregate.Event)
|
|
502
|
-
aggregate.trigger_event(Aggregate.Event)
|
|
503
|
-
app.save(aggregate)
|
|
504
|
-
|
|
505
|
-
subscription = app.subscribe(gt=max_notification_id)
|
|
506
|
-
|
|
507
|
-
# Catch up.
|
|
508
|
-
for domain_event, tracking in subscription:
|
|
509
|
-
self.assertIsInstance(domain_event, Aggregate.Event)
|
|
510
|
-
self.assertIsInstance(tracking, Tracking)
|
|
511
|
-
self.assertEqual(tracking.application_name, app.name)
|
|
512
|
-
if max_notification_id is not None:
|
|
513
|
-
self.assertGreater(tracking.notification_id, max_notification_id)
|
|
514
|
-
if tracking.notification_id == app.recorder.max_notification_id():
|
|
515
|
-
break
|
|
516
|
-
|
|
517
|
-
max_notification_id = app.recorder.max_notification_id()
|
|
518
|
-
|
|
519
|
-
aggregate.trigger_event(Aggregate.Event)
|
|
520
|
-
aggregate.trigger_event(Aggregate.Event)
|
|
521
|
-
aggregate.trigger_event(Aggregate.Event)
|
|
522
|
-
app.save(aggregate)
|
|
523
|
-
|
|
524
|
-
# Continue.
|
|
525
|
-
for domain_event, tracking in subscription:
|
|
526
|
-
self.assertIsInstance(domain_event, Aggregate.Event)
|
|
527
|
-
self.assertIsInstance(tracking, Tracking)
|
|
528
|
-
self.assertEqual(tracking.application_name, app.name)
|
|
529
|
-
if max_notification_id is not None:
|
|
530
|
-
self.assertGreater(tracking.notification_id, max_notification_id)
|
|
531
|
-
if tracking.notification_id == app.recorder.max_notification_id():
|
|
532
|
-
break
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "eventsourcing"
|
|
3
|
-
version = "9.4.
|
|
3
|
+
version = "9.4.0a2"
|
|
4
4
|
|
|
5
5
|
description = "Event sourcing in Python"
|
|
6
6
|
authors = [
|
|
@@ -41,14 +41,16 @@ keywords=[
|
|
|
41
41
|
]
|
|
42
42
|
|
|
43
43
|
[tool.poetry.dependencies]
|
|
44
|
-
python = ">=3.8,<4.0"
|
|
44
|
+
python = ">=3.8,<4.0,!=3.9.0,!=3.9.1"
|
|
45
45
|
typing_extensions = "*"
|
|
46
|
-
"backports.zoneinfo" = { version = "*", python =
|
|
47
|
-
pycryptodome = { version = "~3.
|
|
48
|
-
|
|
46
|
+
"backports.zoneinfo" = { version = "*", python = "<3.9" }
|
|
47
|
+
pycryptodome = { version = "~3.22", optional = true }
|
|
48
|
+
cryptography = { version = "~44.0", optional = true }
|
|
49
|
+
psycopg = { version = "<=3.2.99999", optional = true, extras=["pool"]}
|
|
49
50
|
|
|
50
51
|
[tool.poetry.extras]
|
|
51
52
|
crypto = ["pycryptodome"]
|
|
53
|
+
cryptography = ["cryptography"]
|
|
52
54
|
postgres = ["psycopg"]
|
|
53
55
|
|
|
54
56
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -56,8 +58,13 @@ black = { version = "*", allow-prereleases = true }
|
|
|
56
58
|
coverage = "^7.2.7"
|
|
57
59
|
isort = "*"
|
|
58
60
|
mypy = "*"
|
|
59
|
-
psycopg = { version = "<=3.2.99999", extras = ["binary", "pool"] }
|
|
60
61
|
ruff = "^0.1.14"
|
|
62
|
+
psycopg = { version = "<=3.2.99999", extras = ["pool"] }
|
|
63
|
+
psycopg-c = {version="*", python = "<3.9", markers="sys_platform == 'darwin' and platform_machine == 'arm64'"}
|
|
64
|
+
psycopg-binary = [
|
|
65
|
+
{version="*", python = "<3.9", markers="sys_platform != 'darwin' or platform_machine != 'arm64'"},
|
|
66
|
+
{version="*", python = ">=3.9"},
|
|
67
|
+
]
|
|
61
68
|
|
|
62
69
|
[tool.poetry.group.docs.dependencies]
|
|
63
70
|
Sphinx = { version = "*"}
|
|
@@ -94,7 +101,19 @@ exclude = '''
|
|
|
94
101
|
|
|
95
102
|
[tool.coverage.run]
|
|
96
103
|
branch = true
|
|
97
|
-
|
|
104
|
+
source = [
|
|
105
|
+
"eventsourcing"
|
|
106
|
+
]
|
|
107
|
+
relative_files = true
|
|
108
|
+
omit = [
|
|
109
|
+
"eventsourcing/tests/*",
|
|
110
|
+
"eventsourcing/examples/bankaccounts/test.py",
|
|
111
|
+
"eventsourcing/examples/cargoshipping/domainmodel.py",
|
|
112
|
+
"eventsourcing/examples/cargoshipping/application.py",
|
|
113
|
+
"eventsourcing/examples/cargoshipping/interface.py",
|
|
114
|
+
"eventsourcing/examples/cargoshipping/test.py",
|
|
115
|
+
"eventsourcing/dispatch.py",
|
|
116
|
+
]
|
|
98
117
|
|
|
99
118
|
[tool.coverage.report]
|
|
100
119
|
exclude_lines = [
|
|
@@ -118,6 +137,9 @@ exclude_lines = [
|
|
|
118
137
|
]
|
|
119
138
|
#ignore_errors = true
|
|
120
139
|
#precision = 2
|
|
140
|
+
exclude_also = [
|
|
141
|
+
'if TYPE_CHECKING:'
|
|
142
|
+
]
|
|
121
143
|
|
|
122
144
|
[tool.isort]
|
|
123
145
|
multi_line_output = 3
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "9.3.0dev0"
|
|
File without changes
|
|
File without changes
|
{eventsourcing-9.4.0a1/eventsourcing/tests → eventsourcing-9.4.0a2/eventsourcing}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|