python-event-sourcery 0.4.0a14__tar.gz → 0.4.0a15__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.
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/PKG-INFO +1 -1
- python_event_sourcery-0.4.0a15/event_sourcery/event_store/event/__init__.py +32 -0
- python_event_sourcery-0.4.0a14/event_sourcery/event_store/event/dto.py → python_event_sourcery-0.4.0a15/event_sourcery/event_store/event/_dto.py +46 -10
- python_event_sourcery-0.4.0a15/event_sourcery/event_store/event/_encryption.py +135 -0
- python_event_sourcery-0.4.0a15/event_sourcery/event_store/event/_registry.py +104 -0
- python_event_sourcery-0.4.0a14/event_sourcery/event_store/event/serde.py → python_event_sourcery-0.4.0a15/event_sourcery/event_store/event/_serde.py +25 -8
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/event_store.py +1 -1
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/exceptions.py +14 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/factory.py +10 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/in_memory.py +46 -5
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/interfaces.py +30 -1
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/subscription.py +2 -2
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/__init__.py +20 -3
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/__init__.py +20 -3
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/__init__.py +23 -4
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/pyproject.toml +2 -1
- python_event_sourcery-0.4.0a14/event_sourcery/event_store/event/__init__.py +0 -26
- python_event_sourcery-0.4.0a14/event_sourcery/event_store/event/registry.py +0 -48
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/LICENSE +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/__init__.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/aggregate/__init__.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/aggregate/aggregate.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/aggregate/repository.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/__init__.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/dispatcher.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/outbox.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/stream_id.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/tenant_id.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/event_store/versioning.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/py.typed +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/read_model/__init__.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/read_model/cursors_dao.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/read_model/projector.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/dto.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/event_store.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/migrations/0001_initial.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/migrations/__init__.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/models.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/outbox.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/py.typed +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/subscription.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/dto.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/event_store.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/outbox.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/py.typed +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/stream.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/subscription.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/cursors_dao.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/dto.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/event_store.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/guid.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/jsonb.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/models.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/outbox.py +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/py.typed +0 -0
- {python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/subscription.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Context",
|
|
3
|
+
"DataSubject",
|
|
4
|
+
"Encrypted",
|
|
5
|
+
"Encryption",
|
|
6
|
+
"Entry",
|
|
7
|
+
"Event",
|
|
8
|
+
"EventRegistry",
|
|
9
|
+
"Position",
|
|
10
|
+
"RawEvent",
|
|
11
|
+
"Recorded",
|
|
12
|
+
"RecordedRaw",
|
|
13
|
+
"Serde",
|
|
14
|
+
"WrappedEvent",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from event_sourcery.event_store.event._dto import (
|
|
19
|
+
Context,
|
|
20
|
+
DataSubject,
|
|
21
|
+
Encrypted,
|
|
22
|
+
Entry,
|
|
23
|
+
Event,
|
|
24
|
+
Position,
|
|
25
|
+
RawEvent,
|
|
26
|
+
Recorded,
|
|
27
|
+
RecordedRaw,
|
|
28
|
+
WrappedEvent,
|
|
29
|
+
)
|
|
30
|
+
from event_sourcery.event_store.event._encryption import Encryption
|
|
31
|
+
from event_sourcery.event_store.event._registry import EventRegistry
|
|
32
|
+
from event_sourcery.event_store.event._serde import Serde
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from datetime import datetime, timezone
|
|
3
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Generic, TypeAlias, TypeVar
|
|
4
5
|
from uuid import UUID, uuid4
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel
|
|
7
8
|
|
|
8
|
-
from event_sourcery.event_store.event.registry import EventRegistry
|
|
9
9
|
from event_sourcery.event_store.stream_id import StreamId
|
|
10
10
|
from event_sourcery.event_store.tenant_id import DEFAULT_TENANT, TenantId
|
|
11
11
|
|
|
12
|
+
DataSubject = object()
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
@dataclasses.dataclass(frozen=True)
|
|
14
16
|
class RawEvent(BaseModel):
|
|
@@ -16,8 +18,8 @@ class RawEvent(BaseModel):
|
|
|
16
18
|
stream_id: StreamId
|
|
17
19
|
created_at: datetime
|
|
18
20
|
name: str
|
|
19
|
-
data: dict
|
|
20
|
-
context: dict
|
|
21
|
+
data: dict[str, Any]
|
|
22
|
+
context: dict[str, Any]
|
|
21
23
|
version: int | None = None
|
|
22
24
|
|
|
23
25
|
|
|
@@ -41,11 +43,6 @@ class Event(BaseModel, extra="forbid"):
|
|
|
41
43
|
```
|
|
42
44
|
"""
|
|
43
45
|
|
|
44
|
-
__registry__: ClassVar = EventRegistry()
|
|
45
|
-
|
|
46
|
-
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
47
|
-
cls.__registry__.add(cls)
|
|
48
|
-
|
|
49
46
|
|
|
50
47
|
TEvent = TypeVar("TEvent", bound=Event)
|
|
51
48
|
|
|
@@ -86,7 +83,7 @@ class WrappedEvent(Generic[TEvent]):
|
|
|
86
83
|
|
|
87
84
|
@dataclasses.dataclass(frozen=True)
|
|
88
85
|
class Entry:
|
|
89
|
-
wrapped_event: WrappedEvent
|
|
86
|
+
wrapped_event: WrappedEvent[Event]
|
|
90
87
|
stream_id: StreamId
|
|
91
88
|
|
|
92
89
|
|
|
@@ -99,3 +96,42 @@ class Recorded(Entry):
|
|
|
99
96
|
|
|
100
97
|
position: Position
|
|
101
98
|
tenant_id: TenantId = DEFAULT_TENANT
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class Encrypted:
|
|
103
|
+
"""Field annotation for marking event fields that should be encrypted.
|
|
104
|
+
|
|
105
|
+
This annotation is used in combination with DataSubject to implement
|
|
106
|
+
crypto-shredding, allowing for GDPR-compliant data removal in event-sourced systems.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
mask_value: Value to display when the data is shredded (deleted).
|
|
110
|
+
Must match the field type.
|
|
111
|
+
subject_field: Optional. Name of the field containing the subject ID for this
|
|
112
|
+
encrypted field. If not provided, the field marked with
|
|
113
|
+
DataSubject will be used.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
```python
|
|
117
|
+
class UserRegistered(Event):
|
|
118
|
+
user_id: Annotated[str, DataSubject]
|
|
119
|
+
email: Annotated[str, Encrypted(mask_value="[REDACTED]")]
|
|
120
|
+
# Custom subject field
|
|
121
|
+
other_data: Annotated[
|
|
122
|
+
str,
|
|
123
|
+
Encrypted(
|
|
124
|
+
mask_value="[HIDDEN]",
|
|
125
|
+
subject_field="alternative_id",
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Note:
|
|
131
|
+
- Every event with encrypted fields must have exactly one DataSubject field.
|
|
132
|
+
- The mask_value type must match the field type.
|
|
133
|
+
- After shredding, the field will always return its mask_value.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
mask_value: Any
|
|
137
|
+
subject_field: str | None = None
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import UserDict
|
|
3
|
+
from dataclasses import dataclass, field, replace
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from event_sourcery.event_store.event._registry import EventRegistry
|
|
10
|
+
from event_sourcery.event_store.exceptions import KeyNotFoundError, NoSubjectIdFound
|
|
11
|
+
from event_sourcery.event_store.interfaces import (
|
|
12
|
+
EncryptionKeyStorageStrategy,
|
|
13
|
+
EncryptionStrategy,
|
|
14
|
+
)
|
|
15
|
+
from event_sourcery.event_store.stream_id import StreamId
|
|
16
|
+
from event_sourcery.event_store.tenant_id import TenantId
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NoEncryptionStrategy(EncryptionStrategy):
|
|
20
|
+
def encrypt(self, data: Any, key: bytes) -> str:
|
|
21
|
+
raise NotImplementedError("NoEncryptionStrategy does not support encryption.")
|
|
22
|
+
|
|
23
|
+
def decrypt(self, data: str, key: bytes) -> Any:
|
|
24
|
+
raise NotImplementedError("NoEncryptionStrategy does not support decryption.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NoKeyStorageStrategy(EncryptionKeyStorageStrategy):
|
|
28
|
+
def get(self, subject_id: str) -> bytes | None:
|
|
29
|
+
raise NotImplementedError(
|
|
30
|
+
"NoKeyStorageStrategy does not support key retrieval."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def store(self, subject_id: str, key: bytes) -> None:
|
|
34
|
+
raise NotImplementedError("NoKeyStorageStrategy does not support key storage.")
|
|
35
|
+
|
|
36
|
+
def delete(self, subject_id: str) -> None:
|
|
37
|
+
raise NotImplementedError("NoKeyStorageStrategy does not support key deletion.")
|
|
38
|
+
|
|
39
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NestedDict(UserDict):
|
|
44
|
+
def __getitem__(self, item: str) -> Any:
|
|
45
|
+
match item.split(".", 1):
|
|
46
|
+
case [prefix, key]:
|
|
47
|
+
return NestedDict(self.data[prefix])[key]
|
|
48
|
+
case _:
|
|
49
|
+
return self.data[item]
|
|
50
|
+
|
|
51
|
+
def __setitem__(self, item: str, value: Any) -> None:
|
|
52
|
+
match item.rsplit(".", 1):
|
|
53
|
+
case [prefix, key]:
|
|
54
|
+
self[prefix][key] = value
|
|
55
|
+
case _:
|
|
56
|
+
self.data[item] = value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Encryption:
|
|
61
|
+
"""
|
|
62
|
+
Integrates encryption logic and key management.
|
|
63
|
+
Uses the KeyStorageStrategy and EncryptionStrategy interfaces.
|
|
64
|
+
Responsible for:
|
|
65
|
+
- Retrieving the key for a given subject_id
|
|
66
|
+
- Encrypting and decrypting data
|
|
67
|
+
- Masking data when the key is not available (crypto-shredding)
|
|
68
|
+
- Processing event encryption/decryption with field annotations
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
registry: EventRegistry = field(default_factory=EventRegistry)
|
|
72
|
+
strategy: EncryptionStrategy = field(default_factory=NoEncryptionStrategy)
|
|
73
|
+
key_storage: EncryptionKeyStorageStrategy = field(
|
|
74
|
+
default_factory=NoKeyStorageStrategy
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def encrypt(self, event: BaseModel, stream_id: StreamId) -> dict[str, Any]:
|
|
78
|
+
event_type = type(event)
|
|
79
|
+
data = NestedDict(event.model_dump(mode="json"))
|
|
80
|
+
for field_name in self.registry.encrypted_fields(of=event_type).keys():
|
|
81
|
+
subject_field = self.registry.subject_filed(field_name, of=event_type)
|
|
82
|
+
subject_id = (
|
|
83
|
+
subject_field and getattr(event, subject_field)
|
|
84
|
+
) or stream_id.name
|
|
85
|
+
|
|
86
|
+
if subject_id is None:
|
|
87
|
+
raise NoSubjectIdFound(stream_id)
|
|
88
|
+
data[field_name] = self._encrypt_value(data[field_name], subject_id)
|
|
89
|
+
return data.data
|
|
90
|
+
|
|
91
|
+
def decrypt(
|
|
92
|
+
self,
|
|
93
|
+
event_type: type[BaseModel],
|
|
94
|
+
raw: dict[str, Any],
|
|
95
|
+
stream_id: StreamId,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
data = NestedDict(raw)
|
|
98
|
+
encrypted_fields = self.registry.encrypted_fields(of=event_type)
|
|
99
|
+
for field_name, encrypted_config in encrypted_fields.items():
|
|
100
|
+
subject_field = self.registry.subject_filed(field_name, of=event_type)
|
|
101
|
+
subject_id = data[subject_field] if subject_field else stream_id.name or ""
|
|
102
|
+
data[field_name] = self._decrypt_value(
|
|
103
|
+
data[field_name],
|
|
104
|
+
subject_id,
|
|
105
|
+
encrypted_config.mask_value,
|
|
106
|
+
)
|
|
107
|
+
return data.data
|
|
108
|
+
|
|
109
|
+
def _decrypt_value(self, value: str, subject_id: str, mask_value: Any) -> Any:
|
|
110
|
+
key = self.key_storage.get(subject_id)
|
|
111
|
+
if key is None:
|
|
112
|
+
return mask_value
|
|
113
|
+
decrypted = self.strategy.decrypt(value, key)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
return json.loads(decrypted)
|
|
117
|
+
except (json.JSONDecodeError, TypeError):
|
|
118
|
+
return decrypted
|
|
119
|
+
|
|
120
|
+
def _encrypt_value(self, value: Any, subject_id: str) -> str:
|
|
121
|
+
key = self.key_storage.get(subject_id)
|
|
122
|
+
if key is None:
|
|
123
|
+
raise KeyNotFoundError(subject_id)
|
|
124
|
+
match value:
|
|
125
|
+
case str():
|
|
126
|
+
serialized = value
|
|
127
|
+
case _:
|
|
128
|
+
serialized = json.dumps(value)
|
|
129
|
+
return self.strategy.encrypt(serialized, key)
|
|
130
|
+
|
|
131
|
+
def shred(self, subject_id: str) -> None:
|
|
132
|
+
self.key_storage.delete(subject_id)
|
|
133
|
+
|
|
134
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
135
|
+
return replace(self, key_storage=self.key_storage.scoped_for_tenant(tenant_id))
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Annotated, TypeAlias, get_args, get_origin, get_type_hints
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from event_sourcery.event_store.event._dto import DataSubject, Encrypted, Event
|
|
7
|
+
from event_sourcery.event_store.exceptions import (
|
|
8
|
+
ClassModuleUnavailable,
|
|
9
|
+
DuplicatedEvent,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
TEvent: TypeAlias = BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def event_name(cls: type) -> str:
|
|
16
|
+
if name := getattr(cls, "__event_name__", ""):
|
|
17
|
+
return name
|
|
18
|
+
|
|
19
|
+
event_module = inspect.getmodule(cls)
|
|
20
|
+
if event_module is None: # pragma: no cover
|
|
21
|
+
raise ClassModuleUnavailable
|
|
22
|
+
return f"{event_module.__name__}.{cls.__qualname__}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_encrypted_fields(of: type) -> dict[str, Encrypted]:
|
|
26
|
+
fields = {}
|
|
27
|
+
for field, hint in get_type_hints(of, include_extras=True).items():
|
|
28
|
+
if isinstance(hint, type):
|
|
29
|
+
fields.update(
|
|
30
|
+
{
|
|
31
|
+
f"{field}.{nested}": encrypted
|
|
32
|
+
for nested, encrypted in get_encrypted_fields(hint).items()
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
elif get_origin(hint) is Annotated:
|
|
36
|
+
_, *metadata = get_args(hint)
|
|
37
|
+
encrypted = next((m for m in metadata if isinstance(m, Encrypted)), None)
|
|
38
|
+
if encrypted:
|
|
39
|
+
fields[field] = encrypted
|
|
40
|
+
return fields
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_data_subject_filed(of: type[BaseModel]) -> str | None:
|
|
44
|
+
return next(
|
|
45
|
+
(
|
|
46
|
+
field_name
|
|
47
|
+
for field_name, field_info in of.model_fields.items()
|
|
48
|
+
if DataSubject in field_info.metadata
|
|
49
|
+
),
|
|
50
|
+
None,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EventRegistry:
|
|
55
|
+
"""Keeps mappings between event types and their names.
|
|
56
|
+
|
|
57
|
+
Normally, there is no need to use it directly. If one needs to have multiple
|
|
58
|
+
registries or wants more granular control, they can pass an instance
|
|
59
|
+
of EventRegistry to BackendFactory."""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._types_to_names: dict[type[TEvent], str] = {}
|
|
63
|
+
self._names_to_types: dict[str, type[TEvent]] = {}
|
|
64
|
+
self._encrypted_fields: dict[type[TEvent], dict[str, Encrypted]] = {}
|
|
65
|
+
self._subject_fields: dict[type[TEvent], str] = {}
|
|
66
|
+
self._register_defined_events()
|
|
67
|
+
|
|
68
|
+
def _register_defined_events(self) -> None:
|
|
69
|
+
for event_type in Event.__subclasses__():
|
|
70
|
+
event_type not in self._types_to_names and self.add(event_type)
|
|
71
|
+
|
|
72
|
+
def add(self, event: type[TEvent]) -> type[TEvent]:
|
|
73
|
+
"""Add event subclass to the registry."""
|
|
74
|
+
if event in self._types_to_names:
|
|
75
|
+
raise DuplicatedEvent(f"Duplicated Event detected! {event}")
|
|
76
|
+
|
|
77
|
+
name = event_name(event)
|
|
78
|
+
if name in self._names_to_types:
|
|
79
|
+
raise DuplicatedEvent(f"Duplicated Event name detected! {name}")
|
|
80
|
+
|
|
81
|
+
self._types_to_names[event] = name
|
|
82
|
+
self._names_to_types[name] = event
|
|
83
|
+
self._encrypted_fields[event] = get_encrypted_fields(of=event)
|
|
84
|
+
subject_field = get_data_subject_filed(of=event)
|
|
85
|
+
if subject_field is not None:
|
|
86
|
+
self._subject_fields[event] = subject_field
|
|
87
|
+
return event # for use as a decorator
|
|
88
|
+
|
|
89
|
+
def type_for_name(self, name: str) -> type[TEvent]:
|
|
90
|
+
if name not in self._names_to_types:
|
|
91
|
+
self._register_defined_events()
|
|
92
|
+
return self._names_to_types[name]
|
|
93
|
+
|
|
94
|
+
def name_for_type(self, event: type[TEvent]) -> str:
|
|
95
|
+
if event not in self._types_to_names:
|
|
96
|
+
self._register_defined_events()
|
|
97
|
+
return self._types_to_names[event]
|
|
98
|
+
|
|
99
|
+
def encrypted_fields(self, of: type[TEvent]) -> dict[str, Encrypted]:
|
|
100
|
+
return self._encrypted_fields[of]
|
|
101
|
+
|
|
102
|
+
def subject_filed(self, for_field: str, of: type[TEvent]) -> str | None:
|
|
103
|
+
metadata = self._encrypted_fields[of].get(for_field)
|
|
104
|
+
return (metadata and metadata.subject_field) or self._subject_fields.get(of)
|
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
from collections.abc import Mapping, Sequence
|
|
3
|
-
from dataclasses import dataclass
|
|
4
3
|
from typing import cast
|
|
5
4
|
|
|
6
|
-
from event_sourcery.event_store.event.
|
|
5
|
+
from event_sourcery.event_store.event._dto import (
|
|
7
6
|
Context,
|
|
8
|
-
Event,
|
|
9
7
|
RawEvent,
|
|
10
8
|
Recorded,
|
|
11
9
|
RecordedRaw,
|
|
12
10
|
WrappedEvent,
|
|
13
11
|
)
|
|
14
|
-
from event_sourcery.event_store.event.
|
|
12
|
+
from event_sourcery.event_store.event._encryption import Encryption
|
|
13
|
+
from event_sourcery.event_store.event._registry import EventRegistry
|
|
15
14
|
from event_sourcery.event_store.stream_id import StreamId
|
|
15
|
+
from event_sourcery.event_store.tenant_id import TenantId
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
@dataclass(repr=False, frozen=True)
|
|
19
18
|
class Serde:
|
|
20
19
|
registry: EventRegistry
|
|
20
|
+
encryption: Encryption
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
registry: EventRegistry,
|
|
25
|
+
encryption: Encryption | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.registry = registry
|
|
28
|
+
self.encryption = encryption or Encryption(registry)
|
|
21
29
|
|
|
22
30
|
def deserialize(self, event: RawEvent) -> WrappedEvent:
|
|
23
31
|
event_as_dict = dataclasses.asdict(event)
|
|
@@ -27,9 +35,16 @@ class Serde:
|
|
|
27
35
|
context = event_as_dict.pop("context", {})
|
|
28
36
|
event_as_dict["context"] = Context(**context)
|
|
29
37
|
event_type = self.registry.type_for_name(event.name)
|
|
38
|
+
|
|
39
|
+
processed_data = self.encryption.decrypt(
|
|
40
|
+
event_type,
|
|
41
|
+
dict(data),
|
|
42
|
+
event.stream_id,
|
|
43
|
+
)
|
|
44
|
+
|
|
30
45
|
return WrappedEvent[event_type]( # type: ignore[valid-type]
|
|
31
46
|
**event_as_dict,
|
|
32
|
-
event=event_type(**
|
|
47
|
+
event=event_type(**processed_data),
|
|
33
48
|
)
|
|
34
49
|
|
|
35
50
|
def deserialize_many(self, events: Sequence[RawEvent]) -> list[WrappedEvent]:
|
|
@@ -48,14 +63,13 @@ class Serde:
|
|
|
48
63
|
event: WrappedEvent,
|
|
49
64
|
stream_id: StreamId,
|
|
50
65
|
) -> RawEvent:
|
|
51
|
-
model = cast(Event, event.event)
|
|
52
66
|
return RawEvent(
|
|
53
67
|
uuid=event.uuid,
|
|
54
68
|
stream_id=stream_id,
|
|
55
69
|
created_at=event.created_at,
|
|
56
70
|
version=event.version,
|
|
57
71
|
name=self.registry.name_for_type(type(event.event)),
|
|
58
|
-
data=
|
|
72
|
+
data=self.encryption.encrypt(event.event, stream_id),
|
|
59
73
|
context=event.context.model_dump(mode="json"),
|
|
60
74
|
)
|
|
61
75
|
|
|
@@ -63,3 +77,6 @@ class Serde:
|
|
|
63
77
|
self, events: Sequence[WrappedEvent], stream_id: StreamId
|
|
64
78
|
) -> list[RawEvent]:
|
|
65
79
|
return [self.serialize(event, stream_id) for event in events]
|
|
80
|
+
|
|
81
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> "Serde":
|
|
82
|
+
return Serde(self.registry, self.encryption.scoped_for_tenant(tenant_id))
|
|
@@ -47,3 +47,17 @@ class ClassModuleUnavailable(Exception):
|
|
|
47
47
|
|
|
48
48
|
class DuplicatedEvent(Exception):
|
|
49
49
|
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PrivacyError(EventStoreException):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class KeyNotFoundError(PrivacyError):
|
|
58
|
+
subject_id: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class NoSubjectIdFound(PrivacyError):
|
|
63
|
+
stream_id: UUID
|
|
@@ -9,6 +9,8 @@ from event_sourcery.event_store.dispatcher import Dispatcher
|
|
|
9
9
|
from event_sourcery.event_store.event import EventRegistry, RawEvent, RecordedRaw, Serde
|
|
10
10
|
from event_sourcery.event_store.event_store import EventStore
|
|
11
11
|
from event_sourcery.event_store.interfaces import (
|
|
12
|
+
EncryptionKeyStorageStrategy,
|
|
13
|
+
EncryptionStrategy,
|
|
12
14
|
OutboxFiltererStrategy,
|
|
13
15
|
OutboxStorageStrategy,
|
|
14
16
|
)
|
|
@@ -55,3 +57,11 @@ class BackendFactory(abc.ABC):
|
|
|
55
57
|
@abc.abstractmethod
|
|
56
58
|
def without_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
57
59
|
pass
|
|
60
|
+
|
|
61
|
+
@abc.abstractmethod
|
|
62
|
+
def with_encryption(
|
|
63
|
+
self,
|
|
64
|
+
strategy: EncryptionStrategy,
|
|
65
|
+
key_storage: EncryptionKeyStorageStrategy,
|
|
66
|
+
) -> Self:
|
|
67
|
+
pass
|
|
@@ -2,7 +2,7 @@ import time
|
|
|
2
2
|
from collections.abc import Generator, Iterator
|
|
3
3
|
from contextlib import AbstractContextManager, contextmanager
|
|
4
4
|
from copy import copy
|
|
5
|
-
from dataclasses import dataclass, field
|
|
5
|
+
from dataclasses import dataclass, field, replace
|
|
6
6
|
from datetime import timedelta
|
|
7
7
|
from operator import getitem
|
|
8
8
|
|
|
@@ -11,12 +11,17 @@ from typing_extensions import Self
|
|
|
11
11
|
|
|
12
12
|
from event_sourcery.event_store import (
|
|
13
13
|
Dispatcher,
|
|
14
|
-
Event,
|
|
15
14
|
EventRegistry,
|
|
16
15
|
EventStore,
|
|
17
16
|
subscription,
|
|
18
17
|
)
|
|
19
|
-
from event_sourcery.event_store.event import
|
|
18
|
+
from event_sourcery.event_store.event import (
|
|
19
|
+
Encryption,
|
|
20
|
+
Position,
|
|
21
|
+
RawEvent,
|
|
22
|
+
RecordedRaw,
|
|
23
|
+
Serde,
|
|
24
|
+
)
|
|
20
25
|
from event_sourcery.event_store.exceptions import ConcurrentStreamWriteError
|
|
21
26
|
from event_sourcery.event_store.factory import (
|
|
22
27
|
BackendFactory,
|
|
@@ -25,6 +30,8 @@ from event_sourcery.event_store.factory import (
|
|
|
25
30
|
no_filter,
|
|
26
31
|
)
|
|
27
32
|
from event_sourcery.event_store.interfaces import (
|
|
33
|
+
EncryptionKeyStorageStrategy,
|
|
34
|
+
EncryptionStrategy,
|
|
28
35
|
OutboxFiltererStrategy,
|
|
29
36
|
OutboxStorageStrategy,
|
|
30
37
|
StorageStrategy,
|
|
@@ -100,7 +107,7 @@ class InMemorySubscription(Iterator[list[RecordedRaw]]):
|
|
|
100
107
|
record = self._pop_record()
|
|
101
108
|
if record is not None:
|
|
102
109
|
batch.append(record)
|
|
103
|
-
time.sleep(0.
|
|
110
|
+
time.sleep(0.01)
|
|
104
111
|
if time.monotonic() - start > self._timelimit.total_seconds():
|
|
105
112
|
break
|
|
106
113
|
|
|
@@ -311,7 +318,7 @@ class Config(BaseModel):
|
|
|
311
318
|
class InMemoryBackendFactory(BackendFactory):
|
|
312
319
|
"""Lightweight in-memory backend factory for testing and development."""
|
|
313
320
|
|
|
314
|
-
serde = Serde(
|
|
321
|
+
serde = Serde(EventRegistry())
|
|
315
322
|
|
|
316
323
|
_config: Config = field(default_factory=Config)
|
|
317
324
|
_storage: Storage = field(default_factory=Storage)
|
|
@@ -357,3 +364,37 @@ class InMemoryBackendFactory(BackendFactory):
|
|
|
357
364
|
def without_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
358
365
|
self._outbox_strategy = None
|
|
359
366
|
return self
|
|
367
|
+
|
|
368
|
+
def with_encryption(
|
|
369
|
+
self,
|
|
370
|
+
strategy: EncryptionStrategy,
|
|
371
|
+
key_storage: EncryptionKeyStorageStrategy,
|
|
372
|
+
) -> Self:
|
|
373
|
+
registry = self.serde.registry
|
|
374
|
+
self.serde = Serde(
|
|
375
|
+
registry,
|
|
376
|
+
encryption=Encryption(
|
|
377
|
+
registry=registry,
|
|
378
|
+
strategy=strategy,
|
|
379
|
+
key_storage=key_storage,
|
|
380
|
+
),
|
|
381
|
+
)
|
|
382
|
+
return self
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@dataclass
|
|
386
|
+
class InMemoryKeyStorage(EncryptionKeyStorageStrategy):
|
|
387
|
+
_keys: dict[tuple[TenantId, str], bytes] = field(default_factory=dict)
|
|
388
|
+
_tenant_id: TenantId = DEFAULT_TENANT
|
|
389
|
+
|
|
390
|
+
def get(self, subject_id: str) -> bytes | None:
|
|
391
|
+
return self._keys.get((self._tenant_id, subject_id))
|
|
392
|
+
|
|
393
|
+
def store(self, subject_id: str, key: bytes) -> None:
|
|
394
|
+
self._keys[(self._tenant_id, subject_id)] = key
|
|
395
|
+
|
|
396
|
+
def delete(self, subject_id: str) -> None:
|
|
397
|
+
self._keys.pop((self._tenant_id, subject_id), None)
|
|
398
|
+
|
|
399
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
400
|
+
return replace(self, _tenant_id=tenant_id)
|
|
@@ -2,12 +2,13 @@ import abc
|
|
|
2
2
|
from collections.abc import Iterator
|
|
3
3
|
from contextlib import AbstractContextManager
|
|
4
4
|
from datetime import timedelta
|
|
5
|
-
from typing import Protocol
|
|
5
|
+
from typing import Any, Protocol
|
|
6
6
|
|
|
7
7
|
from typing_extensions import Self
|
|
8
8
|
|
|
9
9
|
from event_sourcery.event_store.event import Position, RawEvent, RecordedRaw
|
|
10
10
|
from event_sourcery.event_store.stream_id import StreamId
|
|
11
|
+
from event_sourcery.event_store.tenant_id import TenantId
|
|
11
12
|
from event_sourcery.event_store.versioning import Versioning
|
|
12
13
|
|
|
13
14
|
|
|
@@ -86,3 +87,31 @@ class StorageStrategy(abc.ABC):
|
|
|
86
87
|
@abc.abstractmethod
|
|
87
88
|
def scoped_for_tenant(self, tenant_id: str) -> Self:
|
|
88
89
|
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EncryptionKeyStorageStrategy(abc.ABC):
|
|
93
|
+
@abc.abstractmethod
|
|
94
|
+
def get(self, subject_id: str) -> bytes | None:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@abc.abstractmethod
|
|
98
|
+
def store(self, subject_id: str, key: bytes) -> None:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@abc.abstractmethod
|
|
102
|
+
def delete(self, subject_id: str) -> None:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
@abc.abstractmethod
|
|
106
|
+
def scoped_for_tenant(self, tenant_id: TenantId) -> Self:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class EncryptionStrategy(abc.ABC):
|
|
111
|
+
@abc.abstractmethod
|
|
112
|
+
def encrypt(self, data: Any, key: bytes) -> str:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
@abc.abstractmethod
|
|
116
|
+
def decrypt(self, data: str, key: bytes) -> Any:
|
|
117
|
+
pass
|
|
@@ -84,9 +84,9 @@ class SubscriptionBuilder(PositionPhase, FilterPhase, BuildPhase):
|
|
|
84
84
|
if isinstance(timelimit, timedelta)
|
|
85
85
|
else timedelta(seconds=timelimit)
|
|
86
86
|
)
|
|
87
|
-
if seconds.total_seconds() < 1:
|
|
87
|
+
if seconds.total_seconds() < 0.1:
|
|
88
88
|
raise ValueError(
|
|
89
|
-
f"Timebox must be at least
|
|
89
|
+
f"Timebox must be at least 100 milliseconds. Received: "
|
|
90
90
|
f"{seconds.total_seconds():.02f}",
|
|
91
91
|
)
|
|
92
92
|
return seconds
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/__init__.py
RENAMED
|
@@ -14,17 +14,18 @@ from event_sourcery import event_store as es
|
|
|
14
14
|
from event_sourcery.event_store import (
|
|
15
15
|
BackendFactory,
|
|
16
16
|
Dispatcher,
|
|
17
|
-
Event,
|
|
18
17
|
EventRegistry,
|
|
19
18
|
EventStore,
|
|
20
19
|
)
|
|
21
|
-
from event_sourcery.event_store.event import Serde
|
|
20
|
+
from event_sourcery.event_store.event import Encryption, Serde
|
|
22
21
|
from event_sourcery.event_store.factory import (
|
|
23
22
|
NoOutboxStorageStrategy,
|
|
24
23
|
TransactionalBackend,
|
|
25
24
|
no_filter,
|
|
26
25
|
)
|
|
27
26
|
from event_sourcery.event_store.interfaces import (
|
|
27
|
+
EncryptionKeyStorageStrategy,
|
|
28
|
+
EncryptionStrategy,
|
|
28
29
|
OutboxFiltererStrategy,
|
|
29
30
|
OutboxStorageStrategy,
|
|
30
31
|
)
|
|
@@ -41,7 +42,7 @@ class Config(BaseModel):
|
|
|
41
42
|
@dataclass(repr=False)
|
|
42
43
|
class DjangoBackendFactory(BackendFactory):
|
|
43
44
|
_config: Config = field(default_factory=Config)
|
|
44
|
-
_serde: Serde = field(default_factory=lambda: Serde(
|
|
45
|
+
_serde: Serde = field(default_factory=lambda: Serde(EventRegistry()))
|
|
45
46
|
_outbox_strategy: OutboxStorageStrategy | None = None
|
|
46
47
|
|
|
47
48
|
def build(self) -> TransactionalBackend:
|
|
@@ -78,3 +79,19 @@ class DjangoBackendFactory(BackendFactory):
|
|
|
78
79
|
def without_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
79
80
|
self._outbox_strategy = None
|
|
80
81
|
return self
|
|
82
|
+
|
|
83
|
+
def with_encryption(
|
|
84
|
+
self,
|
|
85
|
+
strategy: EncryptionStrategy,
|
|
86
|
+
key_storage: EncryptionKeyStorageStrategy,
|
|
87
|
+
) -> Self:
|
|
88
|
+
registry = self._serde.registry
|
|
89
|
+
self._serde = Serde(
|
|
90
|
+
registry,
|
|
91
|
+
encryption=Encryption(
|
|
92
|
+
registry=registry,
|
|
93
|
+
strategy=strategy,
|
|
94
|
+
key_storage=key_storage,
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
return self
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/__init__.py
RENAMED
|
@@ -15,13 +15,14 @@ from event_sourcery import event_store as es
|
|
|
15
15
|
from event_sourcery.event_store import (
|
|
16
16
|
Backend,
|
|
17
17
|
BackendFactory,
|
|
18
|
-
Event,
|
|
19
18
|
EventRegistry,
|
|
20
19
|
EventStore,
|
|
21
20
|
)
|
|
22
|
-
from event_sourcery.event_store.event import Serde
|
|
21
|
+
from event_sourcery.event_store.event import Encryption, Serde
|
|
23
22
|
from event_sourcery.event_store.factory import NoOutboxStorageStrategy, no_filter
|
|
24
23
|
from event_sourcery.event_store.interfaces import (
|
|
24
|
+
EncryptionKeyStorageStrategy,
|
|
25
|
+
EncryptionStrategy,
|
|
25
26
|
OutboxFiltererStrategy,
|
|
26
27
|
OutboxStorageStrategy,
|
|
27
28
|
)
|
|
@@ -45,7 +46,7 @@ class Config(BaseModel):
|
|
|
45
46
|
class ESDBBackendFactory(BackendFactory):
|
|
46
47
|
esdb_client: EventStoreDBClient
|
|
47
48
|
config: Config = field(default_factory=Config)
|
|
48
|
-
_serde: Serde = field(default_factory=lambda: Serde(
|
|
49
|
+
_serde: Serde = field(default_factory=lambda: Serde(EventRegistry()))
|
|
49
50
|
_outbox_strategy: OutboxStorageStrategy = field(
|
|
50
51
|
default_factory=NoOutboxStorageStrategy
|
|
51
52
|
)
|
|
@@ -86,3 +87,19 @@ class ESDBBackendFactory(BackendFactory):
|
|
|
86
87
|
def without_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
87
88
|
self._outbox_strategy = NoOutboxStorageStrategy()
|
|
88
89
|
return self
|
|
90
|
+
|
|
91
|
+
def with_encryption(
|
|
92
|
+
self,
|
|
93
|
+
strategy: EncryptionStrategy,
|
|
94
|
+
key_storage: EncryptionKeyStorageStrategy,
|
|
95
|
+
) -> Self:
|
|
96
|
+
registry = self._serde.registry
|
|
97
|
+
self._serde = Serde(
|
|
98
|
+
registry,
|
|
99
|
+
encryption=Encryption(
|
|
100
|
+
registry=registry,
|
|
101
|
+
strategy=strategy,
|
|
102
|
+
key_storage=key_storage,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
return self
|
|
@@ -17,14 +17,17 @@ from event_sourcery import event_store as es
|
|
|
17
17
|
from event_sourcery.event_store import (
|
|
18
18
|
BackendFactory,
|
|
19
19
|
Dispatcher,
|
|
20
|
-
Event,
|
|
21
20
|
EventRegistry,
|
|
22
21
|
EventStore,
|
|
23
22
|
TransactionalBackend,
|
|
24
23
|
)
|
|
25
|
-
from event_sourcery.event_store.event import Serde
|
|
24
|
+
from event_sourcery.event_store.event import Encryption, Serde
|
|
26
25
|
from event_sourcery.event_store.factory import NoOutboxStorageStrategy, no_filter
|
|
27
|
-
from event_sourcery.event_store.interfaces import
|
|
26
|
+
from event_sourcery.event_store.interfaces import (
|
|
27
|
+
EncryptionKeyStorageStrategy,
|
|
28
|
+
EncryptionStrategy,
|
|
29
|
+
OutboxFiltererStrategy,
|
|
30
|
+
)
|
|
28
31
|
from event_sourcery.event_store.outbox import Outbox
|
|
29
32
|
from event_sourcery_sqlalchemy import models
|
|
30
33
|
from event_sourcery_sqlalchemy.event_store import SqlAlchemyStorageStrategy
|
|
@@ -44,7 +47,7 @@ class Config(BaseModel):
|
|
|
44
47
|
class SQLAlchemyBackendFactory(BackendFactory):
|
|
45
48
|
_session: Session
|
|
46
49
|
_config: Config = field(default_factory=Config)
|
|
47
|
-
_serde: Serde = field(default_factory=lambda: Serde(
|
|
50
|
+
_serde: Serde = field(default_factory=lambda: Serde(EventRegistry()))
|
|
48
51
|
_outbox_strategy: SqlAlchemyOutboxStorageStrategy | None = None
|
|
49
52
|
|
|
50
53
|
def build(self) -> TransactionalBackend:
|
|
@@ -86,3 +89,19 @@ class SQLAlchemyBackendFactory(BackendFactory):
|
|
|
86
89
|
def without_outbox(self, filterer: OutboxFiltererStrategy = no_filter) -> Self:
|
|
87
90
|
self._outbox_strategy = None
|
|
88
91
|
return self
|
|
92
|
+
|
|
93
|
+
def with_encryption(
|
|
94
|
+
self,
|
|
95
|
+
strategy: EncryptionStrategy,
|
|
96
|
+
key_storage: EncryptionKeyStorageStrategy,
|
|
97
|
+
) -> Self:
|
|
98
|
+
registry = self._serde.registry
|
|
99
|
+
self._serde = Serde(
|
|
100
|
+
registry,
|
|
101
|
+
encryption=Encryption(
|
|
102
|
+
registry=registry,
|
|
103
|
+
strategy=strategy,
|
|
104
|
+
key_storage=key_storage,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
return self
|
|
@@ -3,7 +3,7 @@ name = "python-event-sourcery"
|
|
|
3
3
|
requires-python = ">=3.10"
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
|
-
version = "0.4.
|
|
6
|
+
version = "0.4.0a15"
|
|
7
7
|
description = ""
|
|
8
8
|
authors = ["Your Name <you@example.com>"]
|
|
9
9
|
license = "MIT"
|
|
@@ -35,6 +35,7 @@ mkdocstrings = {version = "*", extras = ["python"]}
|
|
|
35
35
|
pymdown-extensions = "*"
|
|
36
36
|
time-machine = "*"
|
|
37
37
|
pika = "*"
|
|
38
|
+
deepdiff = "*"
|
|
38
39
|
|
|
39
40
|
[tool.poetry.extras]
|
|
40
41
|
sqlalchemy = ["SQLAlchemy"]
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
__all__ = [
|
|
2
|
-
"Context",
|
|
3
|
-
"Entry",
|
|
4
|
-
"Event",
|
|
5
|
-
"EventRegistry",
|
|
6
|
-
"Position",
|
|
7
|
-
"RawEvent",
|
|
8
|
-
"Recorded",
|
|
9
|
-
"RecordedRaw",
|
|
10
|
-
"Serde",
|
|
11
|
-
"WrappedEvent",
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from event_sourcery.event_store.event.dto import (
|
|
16
|
-
Context,
|
|
17
|
-
Entry,
|
|
18
|
-
Event,
|
|
19
|
-
Position,
|
|
20
|
-
RawEvent,
|
|
21
|
-
Recorded,
|
|
22
|
-
RecordedRaw,
|
|
23
|
-
WrappedEvent,
|
|
24
|
-
)
|
|
25
|
-
from event_sourcery.event_store.event.registry import EventRegistry
|
|
26
|
-
from event_sourcery.event_store.event.serde import Serde
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
3
|
-
|
|
4
|
-
from event_sourcery.event_store.exceptions import (
|
|
5
|
-
ClassModuleUnavailable,
|
|
6
|
-
DuplicatedEvent,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from event_sourcery.event_store.event.dto import Event
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def event_name(cls: type) -> str:
|
|
14
|
-
event_module = inspect.getmodule(cls)
|
|
15
|
-
if event_module is None: # pragma: no cover
|
|
16
|
-
raise ClassModuleUnavailable
|
|
17
|
-
return f"{event_module.__name__}.{cls.__qualname__}"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class EventRegistry:
|
|
21
|
-
"""Keeps mappings between event types and their names.
|
|
22
|
-
|
|
23
|
-
Normally, there is no need to use it directly. If one needs to have multiple
|
|
24
|
-
registries or wants more granular control, they can pass an instance
|
|
25
|
-
of EventRegistry to BackendFactory."""
|
|
26
|
-
|
|
27
|
-
def __init__(self) -> None:
|
|
28
|
-
self._types_to_names: dict[type[Event], str] = {}
|
|
29
|
-
self._names_to_types: dict[str, type[Event]] = {}
|
|
30
|
-
|
|
31
|
-
def add(self, event: type["Event"]) -> type["Event"]:
|
|
32
|
-
"""Add event subclass to the registry."""
|
|
33
|
-
if event in self._types_to_names:
|
|
34
|
-
raise DuplicatedEvent(f"Duplicated Event detected! {event}")
|
|
35
|
-
|
|
36
|
-
name = event_name(event)
|
|
37
|
-
if name in self._names_to_types:
|
|
38
|
-
raise DuplicatedEvent(f"Duplicated Event name detected! {name}")
|
|
39
|
-
|
|
40
|
-
self._types_to_names[event] = name
|
|
41
|
-
self._names_to_types[name] = event
|
|
42
|
-
return event # for use as a decorator
|
|
43
|
-
|
|
44
|
-
def type_for_name(self, name: str) -> type["Event"]:
|
|
45
|
-
return self._names_to_types[name]
|
|
46
|
-
|
|
47
|
-
def name_for_type(self, event: type["Event"]) -> str:
|
|
48
|
-
return self._types_to_names[event]
|
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery/__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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/dto.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/models.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/outbox.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_django/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/dto.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/event_store.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/outbox.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/py.typed
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_esdb/stream.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/dto.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/guid.py
RENAMED
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/jsonb.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_event_sourcery-0.4.0a14 → python_event_sourcery-0.4.0a15}/event_sourcery_sqlalchemy/py.typed
RENAMED
|
File without changes
|
|
File without changes
|