modmex-lambda 0.5.2__py3-none-any.whl → 0.5.3__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.
- modmex_lambda/persistence/__init__.py +2 -0
- modmex_lambda/persistence/dynamodb/__init__.py +25 -0
- modmex_lambda/persistence/dynamodb/keys.py +133 -0
- modmex_lambda/persistence/dynamodb/stream_fields.py +121 -0
- {modmex_lambda-0.5.2.dist-info → modmex_lambda-0.5.3.dist-info}/METADATA +1 -1
- {modmex_lambda-0.5.2.dist-info → modmex_lambda-0.5.3.dist-info}/RECORD +8 -4
- {modmex_lambda-0.5.2.dist-info → modmex_lambda-0.5.3.dist-info}/WHEEL +0 -0
- {modmex_lambda-0.5.2.dist-info → modmex_lambda-0.5.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""DynamoDB persistence helpers."""
|
|
2
|
+
|
|
3
|
+
from modmex_lambda.persistence.dynamodb.keys import (
|
|
4
|
+
AggregateKeyStrategy,
|
|
5
|
+
KeyStrategy,
|
|
6
|
+
SingleEntityKeyStrategy,
|
|
7
|
+
TenantPartitionKeyStrategy,
|
|
8
|
+
TenantScopedSortKeyStrategy,
|
|
9
|
+
)
|
|
10
|
+
from modmex_lambda.persistence.dynamodb.stream_fields import (
|
|
11
|
+
DefaultStreamFieldsStrategy,
|
|
12
|
+
StreamFieldsStrategy,
|
|
13
|
+
stream_entity_fields,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AggregateKeyStrategy",
|
|
18
|
+
"KeyStrategy",
|
|
19
|
+
"SingleEntityKeyStrategy",
|
|
20
|
+
"DefaultStreamFieldsStrategy",
|
|
21
|
+
"StreamFieldsStrategy",
|
|
22
|
+
"TenantPartitionKeyStrategy",
|
|
23
|
+
"TenantScopedSortKeyStrategy",
|
|
24
|
+
"stream_entity_fields",
|
|
25
|
+
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Reusable DynamoDB key strategies.
|
|
2
|
+
|
|
3
|
+
These helpers keep key-shaping decisions explicit while letting repositories
|
|
4
|
+
stay focused on persistence behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KeyStrategy(ABC):
|
|
15
|
+
"""Build DynamoDB primary keys for ids and entities."""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class SingleEntityKeyStrategy(KeyStrategy):
|
|
28
|
+
"""Use the entity id as pk and a fixed discriminator as sk."""
|
|
29
|
+
|
|
30
|
+
discriminator: str
|
|
31
|
+
|
|
32
|
+
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
33
|
+
return {
|
|
34
|
+
"pk": str(entity_id),
|
|
35
|
+
"sk": self.discriminator,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
39
|
+
return self.key_for_id(_entity_attr(entity, "id"), **context)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class TenantScopedSortKeyStrategy(KeyStrategy):
|
|
44
|
+
"""Use entity id as pk and discriminator plus tenant id as sk."""
|
|
45
|
+
|
|
46
|
+
discriminator: str
|
|
47
|
+
separator: str = "#"
|
|
48
|
+
tenant_field: str = "tenant_id"
|
|
49
|
+
|
|
50
|
+
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
51
|
+
tenant_id = _context_value(context, self.tenant_field)
|
|
52
|
+
return {
|
|
53
|
+
"pk": str(entity_id),
|
|
54
|
+
"sk": self._sort_key(tenant_id),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
58
|
+
tenant_id = _context_or_entity_value(context, entity, self.tenant_field)
|
|
59
|
+
return self.key_for_id(_entity_attr(entity, "id"), **{self.tenant_field: tenant_id})
|
|
60
|
+
|
|
61
|
+
def _sort_key(self, tenant_id: Any) -> str:
|
|
62
|
+
return f"{self.discriminator}{self.separator}{tenant_id}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class TenantPartitionKeyStrategy(KeyStrategy):
|
|
67
|
+
"""Use tenant id as pk and discriminator plus entity id as sk."""
|
|
68
|
+
|
|
69
|
+
discriminator: str
|
|
70
|
+
separator: str = "#"
|
|
71
|
+
tenant_field: str = "tenant_id"
|
|
72
|
+
|
|
73
|
+
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
74
|
+
tenant_id = _context_value(context, self.tenant_field)
|
|
75
|
+
return {
|
|
76
|
+
"pk": str(tenant_id),
|
|
77
|
+
"sk": self._sort_key(entity_id),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
81
|
+
tenant_id = _context_or_entity_value(context, entity, self.tenant_field)
|
|
82
|
+
return self.key_for_id(_entity_attr(entity, "id"), **{self.tenant_field: tenant_id})
|
|
83
|
+
|
|
84
|
+
def _sort_key(self, entity_id: Any) -> str:
|
|
85
|
+
return f"{self.discriminator}{self.separator}{entity_id}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class AggregateKeyStrategy(KeyStrategy):
|
|
90
|
+
"""Use aggregate id as pk and entity name plus entity id as sk."""
|
|
91
|
+
|
|
92
|
+
aggregate_name: str
|
|
93
|
+
entity_name: str
|
|
94
|
+
separator: str = "#"
|
|
95
|
+
aggregate_field: str = "aggregate_id"
|
|
96
|
+
|
|
97
|
+
def key_for_id(self, entity_id: Any, **context: Any) -> dict[str, str]:
|
|
98
|
+
aggregate_id = _context_value(context, self.aggregate_field)
|
|
99
|
+
return {
|
|
100
|
+
"pk": f"{self.aggregate_name}{self.separator}{aggregate_id}",
|
|
101
|
+
"sk": f"{self.entity_name}{self.separator}{entity_id}",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def key_for_entity(self, entity: Any, **context: Any) -> dict[str, str]:
|
|
105
|
+
aggregate_id = _context_or_entity_value(context, entity, self.aggregate_field)
|
|
106
|
+
return self.key_for_id(_entity_attr(entity, "id"), **{self.aggregate_field: aggregate_id})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _context_value(context: dict[str, Any], field_name: str) -> Any:
|
|
110
|
+
if field_name in context and context[field_name] is not None:
|
|
111
|
+
return context[field_name]
|
|
112
|
+
raise KeyError(f"Missing required key context: {field_name}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _context_or_entity_value(context: dict[str, Any], entity: Any, field_name: str) -> Any:
|
|
116
|
+
if field_name in context and context[field_name] is not None:
|
|
117
|
+
return context[field_name]
|
|
118
|
+
value = _entity_attr(entity, field_name)
|
|
119
|
+
if value is not None:
|
|
120
|
+
return value
|
|
121
|
+
raise AttributeError(f"Entity is missing required field: {field_name}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _entity_attr(entity: Any, field_name: str) -> Any:
|
|
125
|
+
if isinstance(entity, dict):
|
|
126
|
+
try:
|
|
127
|
+
return entity[field_name]
|
|
128
|
+
except KeyError as exc:
|
|
129
|
+
raise AttributeError(f"Entity is missing required field: {field_name}") from exc
|
|
130
|
+
try:
|
|
131
|
+
return getattr(entity, field_name)
|
|
132
|
+
except AttributeError as exc:
|
|
133
|
+
raise AttributeError(f"Entity is missing required field: {field_name}") from exc
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Fields used by modmex-lambda stream-compatible DynamoDB items."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from modmex_lambda.stream.utils.time import now, ttl as stream_ttl
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def stream_entity_fields(
|
|
14
|
+
discriminator: str,
|
|
15
|
+
*,
|
|
16
|
+
timestamp: int,
|
|
17
|
+
deleted: bool | None = None,
|
|
18
|
+
latched: bool = False,
|
|
19
|
+
ttl: int | None = None,
|
|
20
|
+
awsregion: str | None = None,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
"""Build standard fields consumed by modmex-lambda stream processors."""
|
|
23
|
+
|
|
24
|
+
fields = {
|
|
25
|
+
"discriminator": discriminator,
|
|
26
|
+
"deleted": deleted,
|
|
27
|
+
"latched": latched,
|
|
28
|
+
"ttl": ttl,
|
|
29
|
+
"awsregion": awsregion if awsregion is not None else os.getenv("REGION"),
|
|
30
|
+
"timestamp": timestamp,
|
|
31
|
+
}
|
|
32
|
+
return fields
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StreamFieldsStrategy(ABC):
|
|
36
|
+
"""Build stream-compatible item fields for DynamoDB writes."""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def fields_for_save(
|
|
40
|
+
self,
|
|
41
|
+
data: dict[str, Any],
|
|
42
|
+
*,
|
|
43
|
+
timestamp: int | None = None,
|
|
44
|
+
ttl: int | None = None,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def fields_for_delete(
|
|
50
|
+
self,
|
|
51
|
+
data: dict[str, Any],
|
|
52
|
+
*,
|
|
53
|
+
timestamp: int | None = None,
|
|
54
|
+
ttl: int | None = None,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class DefaultStreamFieldsStrategy(StreamFieldsStrategy):
|
|
61
|
+
"""Default stream field contract used by modmex-lambda stream processors."""
|
|
62
|
+
|
|
63
|
+
discriminator: str
|
|
64
|
+
key_fields: tuple[str, ...] = field(default=("pk", "sk"))
|
|
65
|
+
use_ttl: bool = False
|
|
66
|
+
days_ttl: int = 30
|
|
67
|
+
|
|
68
|
+
def fields_for_save(
|
|
69
|
+
self,
|
|
70
|
+
data: dict[str, Any],
|
|
71
|
+
*,
|
|
72
|
+
timestamp: int | None = None,
|
|
73
|
+
ttl: int | None = None,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
timestamp = self._timestamp(timestamp)
|
|
76
|
+
return {
|
|
77
|
+
**self._without_key_fields(data),
|
|
78
|
+
**stream_entity_fields(
|
|
79
|
+
self.discriminator,
|
|
80
|
+
timestamp=timestamp,
|
|
81
|
+
deleted=None,
|
|
82
|
+
latched=False,
|
|
83
|
+
ttl=self._ttl(timestamp, ttl),
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def fields_for_delete(
|
|
88
|
+
self,
|
|
89
|
+
data: dict[str, Any],
|
|
90
|
+
*,
|
|
91
|
+
timestamp: int | None = None,
|
|
92
|
+
ttl: int | None = None,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
timestamp = self._timestamp(timestamp)
|
|
95
|
+
return {
|
|
96
|
+
**self._without_key_fields(data),
|
|
97
|
+
**stream_entity_fields(
|
|
98
|
+
self.discriminator,
|
|
99
|
+
timestamp=timestamp,
|
|
100
|
+
deleted=True,
|
|
101
|
+
latched=False,
|
|
102
|
+
ttl=self._ttl(timestamp, ttl),
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def _without_key_fields(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
107
|
+
return {
|
|
108
|
+
key: value
|
|
109
|
+
for key, value in data.items()
|
|
110
|
+
if key not in self.key_fields
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def _timestamp(self, timestamp: int | None) -> int:
|
|
114
|
+
return timestamp if timestamp is not None else now()
|
|
115
|
+
|
|
116
|
+
def _ttl(self, timestamp: int, ttl: int | None) -> int | None:
|
|
117
|
+
if ttl is not None:
|
|
118
|
+
return ttl
|
|
119
|
+
if self.use_ttl:
|
|
120
|
+
return stream_ttl(timestamp, self.days_ttl)
|
|
121
|
+
return None
|
|
@@ -55,6 +55,10 @@ modmex_lambda/event_handler/dependencies/dependency_middleware.py,sha256=c_N7Ugt
|
|
|
55
55
|
modmex_lambda/event_handler/dependencies/depends.py,sha256=pJ1rcMoA99vSaQTPxJ0zkYppPDKU0-lm9URc9Gifmdw,7110
|
|
56
56
|
modmex_lambda/event_handler/dependencies/params.py,sha256=bc208GHiXCfm1V-GLeIMm8-ypZ8P8Q9qUKQkK2Vh1Fg,10085
|
|
57
57
|
modmex_lambda/event_handler/dependencies/types.py,sha256=5VA8zl-EjQDNKXOrNT7sRv_vNs40Lz8aMP-sf-lf8sk,376
|
|
58
|
+
modmex_lambda/persistence/__init__.py,sha256=oa4FHCvitEkoagJ059jGATj6GSm-maazzf_QNi4WdQs,74
|
|
59
|
+
modmex_lambda/persistence/dynamodb/__init__.py,sha256=3zryiwKKSb-eLO82Mh3bFJAgBXco4hjgSFx32Vbm0F0,633
|
|
60
|
+
modmex_lambda/persistence/dynamodb/keys.py,sha256=_vjpHYzRmi1OmCzHaJNpq8vefjH7kq8yxE1t7RhmmQg,4676
|
|
61
|
+
modmex_lambda/persistence/dynamodb/stream_fields.py,sha256=b6wRRq6Ag9W59GCI8616IlBQXeBIwj5RKEQE0lfD4YE,3323
|
|
58
62
|
modmex_lambda/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
63
|
modmex_lambda/shared/cookies.py,sha256=E2hTuht5_Xljw9zOQLW6vjsOWEpQSIqD2D3Tl1Co_s0,2076
|
|
60
64
|
modmex_lambda/shared/headers_serializer.py,sha256=Ao-wpx0cmy9E2ACrh7R7RIDUF0h24gax1AMtY_MjIWQ,2505
|
|
@@ -132,7 +136,7 @@ modmex_lambda/stream/utils/time.py,sha256=dnsL2xeWpa1FXm2p8y9rWXtICMBlCBkgLaJ_en
|
|
|
132
136
|
modmex_lambda/stream/utils/uow.py,sha256=8WSPVE7AiHInkFDfBEeKOvdirY6GAgBoS7qkF1f1JVM,3207
|
|
133
137
|
modmex_lambda/stream/utils/data_classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
134
138
|
modmex_lambda/stream/utils/data_classes/dynamodb.py,sha256=h_7MtUyRkvvV1r2QECrRDu7RR4Hh94SwSjc_8K2_XGM,363
|
|
135
|
-
modmex_lambda-0.5.
|
|
136
|
-
modmex_lambda-0.5.
|
|
137
|
-
modmex_lambda-0.5.
|
|
138
|
-
modmex_lambda-0.5.
|
|
139
|
+
modmex_lambda-0.5.3.dist-info/METADATA,sha256=WEuUKYajpwTKaggo3RPcyEoT6qIjPBd0fnkvzsPJ1SU,31809
|
|
140
|
+
modmex_lambda-0.5.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
141
|
+
modmex_lambda-0.5.3.dist-info/licenses/LICENSE,sha256=_C2TDTOsYeJvE4vn9VB51laKvleBKbdNnn96wJVtXhQ,1063
|
|
142
|
+
modmex_lambda-0.5.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|