eventsourcing 9.3.4__py3-none-any.whl → 9.4.0__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/__init__.py +0 -1
- eventsourcing/application.py +115 -188
- eventsourcing/cipher.py +9 -10
- eventsourcing/compressor.py +2 -6
- eventsourcing/cryptography.py +91 -0
- eventsourcing/dispatch.py +52 -11
- eventsourcing/domain.py +733 -690
- eventsourcing/interface.py +39 -32
- eventsourcing/persistence.py +412 -287
- eventsourcing/popo.py +136 -44
- eventsourcing/postgres.py +404 -187
- eventsourcing/projection.py +428 -0
- eventsourcing/sqlite.py +167 -55
- eventsourcing/system.py +253 -341
- eventsourcing/tests/__init__.py +3 -0
- eventsourcing/tests/application.py +195 -129
- eventsourcing/tests/domain.py +19 -37
- eventsourcing/tests/persistence.py +533 -235
- eventsourcing/tests/postgres_utils.py +12 -9
- eventsourcing/utils.py +39 -47
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/LICENSE +1 -1
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/METADATA +14 -13
- eventsourcing-9.4.0.dist-info/RECORD +26 -0
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/WHEEL +1 -1
- eventsourcing-9.3.4.dist-info/RECORD +0 -24
- {eventsourcing-9.3.4.dist-info → eventsourcing-9.4.0.dist-info}/AUTHORS +0 -0
eventsourcing/cipher.py
CHANGED
|
@@ -5,18 +5,20 @@ from base64 import b64decode, b64encode
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from Crypto.Cipher import AES
|
|
8
|
-
from Crypto.Cipher._mode_gcm import
|
|
8
|
+
from Crypto.Cipher._mode_gcm import (
|
|
9
|
+
GcmMode, # pyright: ignore [reportPrivateImportUsage]
|
|
10
|
+
)
|
|
9
11
|
from Crypto.Cipher.AES import key_size
|
|
10
12
|
|
|
11
13
|
from eventsourcing.persistence import Cipher
|
|
12
14
|
|
|
13
|
-
if TYPE_CHECKING:
|
|
15
|
+
if TYPE_CHECKING:
|
|
14
16
|
from eventsourcing.utils import Environment
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class AESCipher(Cipher):
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
+
"""Cipher strategy that uses AES cipher (in GCM mode)
|
|
21
|
+
from the Python pycryptodome package.
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
24
|
CIPHER_KEY = "CIPHER_KEY"
|
|
@@ -24,8 +26,7 @@ class AESCipher(Cipher):
|
|
|
24
26
|
|
|
25
27
|
@staticmethod
|
|
26
28
|
def create_key(num_bytes: int) -> str:
|
|
27
|
-
"""
|
|
28
|
-
Creates AES cipher key, with length num_bytes.
|
|
29
|
+
"""Creates AES cipher key, with length num_bytes.
|
|
29
30
|
|
|
30
31
|
:param num_bytes: An int value, either 16, 24, or 32.
|
|
31
32
|
|
|
@@ -44,8 +45,7 @@ class AESCipher(Cipher):
|
|
|
44
45
|
return os.urandom(num_bytes)
|
|
45
46
|
|
|
46
47
|
def __init__(self, environment: Environment):
|
|
47
|
-
"""
|
|
48
|
-
Initialises AES cipher with ``cipher_key``.
|
|
48
|
+
"""Initialises AES cipher with ``cipher_key``.
|
|
49
49
|
|
|
50
50
|
:param str cipher_key: 16, 24, or 32 bytes encoded as base64
|
|
51
51
|
"""
|
|
@@ -59,7 +59,6 @@ class AESCipher(Cipher):
|
|
|
59
59
|
|
|
60
60
|
def encrypt(self, plaintext: bytes) -> bytes:
|
|
61
61
|
"""Return ciphertext for given plaintext."""
|
|
62
|
-
|
|
63
62
|
# Construct AES-GCM cipher, with 96-bit nonce.
|
|
64
63
|
nonce = AESCipher.random_bytes(12)
|
|
65
64
|
cipher = self.construct_cipher(nonce)
|
|
@@ -71,6 +70,7 @@ class AESCipher(Cipher):
|
|
|
71
70
|
|
|
72
71
|
# Return ciphertext.
|
|
73
72
|
return nonce + tag + encrypted
|
|
73
|
+
# return nonce + tag + encrypted
|
|
74
74
|
|
|
75
75
|
def construct_cipher(self, nonce: bytes) -> GcmMode:
|
|
76
76
|
cipher = AES.new(
|
|
@@ -83,7 +83,6 @@ class AESCipher(Cipher):
|
|
|
83
83
|
|
|
84
84
|
def decrypt(self, ciphertext: bytes) -> bytes:
|
|
85
85
|
"""Return plaintext for given ciphertext."""
|
|
86
|
-
|
|
87
86
|
# Split out the nonce, tag, and encrypted data.
|
|
88
87
|
nonce = ciphertext[:12]
|
|
89
88
|
if len(nonce) != 12:
|
eventsourcing/compressor.py
CHANGED
|
@@ -7,13 +7,9 @@ from eventsourcing.persistence import Compressor
|
|
|
7
7
|
|
|
8
8
|
class ZlibCompressor(Compressor):
|
|
9
9
|
def compress(self, data: bytes) -> bytes:
|
|
10
|
-
"""
|
|
11
|
-
Compress bytes using zlib.
|
|
12
|
-
"""
|
|
10
|
+
"""Compress bytes using zlib."""
|
|
13
11
|
return zlib.compress(data)
|
|
14
12
|
|
|
15
13
|
def decompress(self, data: bytes) -> bytes:
|
|
16
|
-
"""
|
|
17
|
-
Decompress bytes using zlib.
|
|
18
|
-
"""
|
|
14
|
+
"""Decompress bytes using zlib."""
|
|
19
15
|
return zlib.decompress(data)
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
"""Cipher strategy that uses AES cipher (in GCM mode)
|
|
18
|
+
from the Python cryptography package.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
CIPHER_KEY = "CIPHER_KEY"
|
|
22
|
+
KEY_SIZES = (16, 24, 32)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def create_key(num_bytes: int) -> str:
|
|
26
|
+
"""Creates AES cipher key, with length num_bytes.
|
|
27
|
+
|
|
28
|
+
:param num_bytes: An int value, either 16, 24, or 32.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
AESCipher.check_key_size(num_bytes)
|
|
32
|
+
key = AESGCM.generate_key(num_bytes * 8)
|
|
33
|
+
return b64encode(key).decode("utf8")
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def check_key_size(num_bytes: int) -> None:
|
|
37
|
+
if num_bytes not in AESCipher.KEY_SIZES:
|
|
38
|
+
msg = f"Invalid key size: {num_bytes} not in {AESCipher.KEY_SIZES}"
|
|
39
|
+
raise ValueError(msg)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def random_bytes(num_bytes: int) -> bytes:
|
|
43
|
+
return os.urandom(num_bytes)
|
|
44
|
+
|
|
45
|
+
def __init__(self, environment: Environment):
|
|
46
|
+
"""Initialises AES cipher with ``cipher_key``.
|
|
47
|
+
|
|
48
|
+
:param str cipher_key: 16, 24, or 32 bytes encoded as base64
|
|
49
|
+
"""
|
|
50
|
+
cipher_key = environment.get(self.CIPHER_KEY)
|
|
51
|
+
if not cipher_key:
|
|
52
|
+
msg = f"'{self.CIPHER_KEY}' not in env"
|
|
53
|
+
raise OSError(msg)
|
|
54
|
+
key = b64decode(cipher_key.encode("utf8"))
|
|
55
|
+
AESCipher.check_key_size(len(key))
|
|
56
|
+
self.key = key
|
|
57
|
+
|
|
58
|
+
def encrypt(self, plaintext: bytes) -> bytes:
|
|
59
|
+
"""Return ciphertext for given plaintext."""
|
|
60
|
+
# Construct AES-GCM cipher, with 96-bit nonce.
|
|
61
|
+
aesgcm = AESGCM(self.key)
|
|
62
|
+
nonce = AESCipher.random_bytes(12)
|
|
63
|
+
res = aesgcm.encrypt(nonce, plaintext, None)
|
|
64
|
+
# Put tag at the front for compatibility with eventsourcing.crypto.AESCipher.
|
|
65
|
+
tag = res[-16:]
|
|
66
|
+
encrypted = res[:-16]
|
|
67
|
+
return nonce + tag + encrypted
|
|
68
|
+
|
|
69
|
+
def decrypt(self, ciphertext: bytes) -> bytes:
|
|
70
|
+
"""Return plaintext for given ciphertext."""
|
|
71
|
+
# Split out the nonce, tag, and encrypted data.
|
|
72
|
+
nonce = ciphertext[:12]
|
|
73
|
+
if len(nonce) != 12:
|
|
74
|
+
msg = "Damaged cipher text: invalid nonce length"
|
|
75
|
+
raise ValueError(msg)
|
|
76
|
+
|
|
77
|
+
# Expect tag at the front.
|
|
78
|
+
tag = ciphertext[12:28]
|
|
79
|
+
if len(tag) != 16:
|
|
80
|
+
msg = "Damaged cipher text: invalid tag length"
|
|
81
|
+
raise ValueError(msg)
|
|
82
|
+
encrypted = ciphertext[28:]
|
|
83
|
+
|
|
84
|
+
aesgcm = AESGCM(self.key)
|
|
85
|
+
try:
|
|
86
|
+
plaintext = aesgcm.decrypt(nonce, encrypted + tag, None)
|
|
87
|
+
except InvalidTag as e:
|
|
88
|
+
msg = "Invalid cipher tag"
|
|
89
|
+
raise ValueError(msg) from e
|
|
90
|
+
# Decrypt and verify.
|
|
91
|
+
return plaintext
|
eventsourcing/dispatch.py
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import functools
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, cast, overload
|
|
4
5
|
|
|
6
|
+
_T = TypeVar("_T")
|
|
7
|
+
_S = TypeVar("_S")
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
|
|
11
|
+
class _singledispatchmethod(functools.singledispatchmethod[_T]): # noqa: N801
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
else:
|
|
15
|
+
|
|
16
|
+
class _singledispatchmethod( # noqa: N801
|
|
17
|
+
functools.singledispatchmethod, Generic[_T]
|
|
18
|
+
):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class singledispatchmethod(_singledispatchmethod[_T]): # noqa: N801
|
|
23
|
+
def __init__(self, func: Callable[..., _T]) -> None:
|
|
8
24
|
super().__init__(func)
|
|
9
|
-
self.deferred_registrations
|
|
25
|
+
self.deferred_registrations: list[
|
|
26
|
+
tuple[type[Any] | Callable[..., _T], Callable[..., _T] | None]
|
|
27
|
+
] = []
|
|
28
|
+
|
|
29
|
+
@overload
|
|
30
|
+
def register(
|
|
31
|
+
self, cls: type[Any], method: None = None
|
|
32
|
+
) -> Callable[[Callable[..., _T]], Callable[..., _T]]: ... # pragma: no cover
|
|
33
|
+
@overload
|
|
34
|
+
def register(
|
|
35
|
+
self, cls: Callable[..., _T], method: None = None
|
|
36
|
+
) -> Callable[..., _T]: ... # pragma: no cover
|
|
37
|
+
|
|
38
|
+
@overload
|
|
39
|
+
def register(
|
|
40
|
+
self, cls: type[Any], method: Callable[..., _T]
|
|
41
|
+
) -> Callable[..., _T]: ... # pragma: no cover
|
|
10
42
|
|
|
11
|
-
def register(
|
|
43
|
+
def register(
|
|
44
|
+
self,
|
|
45
|
+
cls: type[Any] | Callable[..., _T],
|
|
46
|
+
method: Callable[..., _T] | None = None,
|
|
47
|
+
) -> Callable[[Callable[..., _T]], Callable[..., _T]] | Callable[..., _T]:
|
|
12
48
|
"""generic_method.register(cls, func) -> func
|
|
13
49
|
|
|
14
50
|
Registers a new implementation for the given *cls* on a *generic_method*.
|
|
@@ -22,17 +58,22 @@ class singledispatchmethod(_singledispatchmethod): # noqa: N801
|
|
|
22
58
|
|
|
23
59
|
# for globals in typing.get_type_hints() in Python 3.8 and 3.9
|
|
24
60
|
if not hasattr(cls, "__wrapped__"):
|
|
25
|
-
cls.__wrapped__ = cls.__func__
|
|
61
|
+
cls.__dict__["__wrapped__"] = cls.__func__
|
|
62
|
+
# cls.__wrapped__ = cls.__func__
|
|
26
63
|
|
|
27
64
|
try:
|
|
28
|
-
return self.dispatcher.register(cls, func=method)
|
|
65
|
+
return self.dispatcher.register(cast("type[Any]", cls), func=method)
|
|
29
66
|
except NameError:
|
|
30
|
-
self.deferred_registrations.append(
|
|
67
|
+
self.deferred_registrations.append(
|
|
68
|
+
(cls, method) # pyright: ignore [reportArgumentType]
|
|
69
|
+
)
|
|
31
70
|
# TODO: Fix this....
|
|
32
|
-
return method or cls
|
|
71
|
+
return method or cls # pyright: ignore [reportReturnType]
|
|
33
72
|
|
|
34
|
-
def __get__(self, obj, cls=None):
|
|
73
|
+
def __get__(self, obj: _S, cls: type[_S] | None = None) -> Callable[..., _T]:
|
|
35
74
|
for registered_cls, registered_method in self.deferred_registrations:
|
|
36
|
-
self.dispatcher.register(
|
|
75
|
+
self.dispatcher.register(
|
|
76
|
+
cast("type[Any]", registered_cls), func=registered_method
|
|
77
|
+
)
|
|
37
78
|
self.deferred_registrations = []
|
|
38
79
|
return super().__get__(obj, cls=cls)
|