modmex-lambda 0.5.2__py3-none-any.whl → 0.5.4__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/connectors/dynamodb.py +189 -30
- modmex_lambda/connectors/module.py +4 -1
- modmex_lambda/persistence/__init__.py +2 -0
- modmex_lambda/persistence/dynamodb/__init__.py +39 -0
- modmex_lambda/persistence/dynamodb/expressions.py +62 -0
- modmex_lambda/persistence/dynamodb/keys.py +133 -0
- modmex_lambda/persistence/dynamodb/materialized_views.py +80 -0
- modmex_lambda/persistence/dynamodb/stream_fields.py +121 -0
- modmex_lambda/stream/utils/dynamodb.py +5 -60
- {modmex_lambda-0.5.2.dist-info → modmex_lambda-0.5.4.dist-info}/METADATA +46 -15
- {modmex_lambda-0.5.2.dist-info → modmex_lambda-0.5.4.dist-info}/RECORD +13 -7
- {modmex_lambda-0.5.2.dist-info → modmex_lambda-0.5.4.dist-info}/WHEEL +0 -0
- {modmex_lambda-0.5.2.dist-info → modmex_lambda-0.5.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -19,53 +19,60 @@ class Connector(IDynamodbConnector):
|
|
|
19
19
|
self.table_name = table_name
|
|
20
20
|
self._client = client
|
|
21
21
|
self.retry_config = retry_config
|
|
22
|
+
self._serializer = None
|
|
23
|
+
self._deserializer = None
|
|
22
24
|
|
|
23
25
|
@property
|
|
24
26
|
def client(self):
|
|
25
27
|
if not self._client:
|
|
26
28
|
import boto3
|
|
27
|
-
self._client = boto3.
|
|
29
|
+
self._client = boto3.client('dynamodb')
|
|
28
30
|
return self._client
|
|
29
31
|
|
|
30
32
|
def get(self, input_params):
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
response = self.client.get_item(
|
|
34
|
+
TableName=self.table_name,
|
|
35
|
+
**self._marshall_request(input_params),
|
|
33
36
|
)
|
|
37
|
+
return self._unmarshall_response(response)
|
|
34
38
|
|
|
35
39
|
def update(self, input_params):
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
response = self.client.update_item(
|
|
41
|
+
TableName=self.table_name,
|
|
42
|
+
**self._marshall_request(input_params),
|
|
38
43
|
)
|
|
44
|
+
return self._unmarshall_response(response)
|
|
39
45
|
|
|
40
46
|
def put(self, input_params):
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
response = self.client.put_item(
|
|
48
|
+
TableName=self.table_name,
|
|
49
|
+
**self._marshall_request(input_params),
|
|
43
50
|
)
|
|
51
|
+
return self._unmarshall_response(response)
|
|
44
52
|
|
|
45
53
|
def query(self, input_params):
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
response = self.client.query(
|
|
55
|
+
TableName=self.table_name,
|
|
56
|
+
**self._marshall_request(input_params),
|
|
48
57
|
)
|
|
58
|
+
return self._unmarshall_response(response)
|
|
49
59
|
|
|
50
60
|
def query_page(self, input_params):
|
|
51
61
|
return self.query(input_params)
|
|
52
62
|
|
|
53
63
|
def scan(self, input_params):
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
response = self.client.scan(
|
|
65
|
+
TableName=self.table_name,
|
|
66
|
+
**self._marshall_request(input_params),
|
|
56
67
|
)
|
|
68
|
+
return self._unmarshall_response(response)
|
|
57
69
|
|
|
58
70
|
def query_all(self, input_params):
|
|
59
71
|
items = []
|
|
60
72
|
while True:
|
|
61
|
-
|
|
62
|
-
result
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
for item in result['Items']:
|
|
66
|
-
items.append(item)
|
|
67
|
-
|
|
68
|
-
if 'LastEvaluatedKey' in result and result['LastEvaluatedKey']:
|
|
73
|
+
result = self.query(input_params)
|
|
74
|
+
items.extend(result.get('Items', []))
|
|
75
|
+
if result.get('LastEvaluatedKey'):
|
|
69
76
|
input_params['ExclusiveStartKey'] = result['LastEvaluatedKey']
|
|
70
77
|
else:
|
|
71
78
|
break
|
|
@@ -77,23 +84,175 @@ class Connector(IDynamodbConnector):
|
|
|
77
84
|
def _batch_get(self, params, attempts):
|
|
78
85
|
assert_max_retries(attempts, self.retry_config['max_retries'])
|
|
79
86
|
wait(get_delay(self.retry_config['retry_wait'], len(attempts)))
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
response = self.client.batch_get_item(**self._marshall_batch_get_request(params))
|
|
88
|
+
response = self._unmarshall_response(response)
|
|
89
|
+
if response.get('UnprocessedKeys'):
|
|
82
90
|
return self._batch_get(
|
|
83
|
-
unprocessed(params,
|
|
84
|
-
[*attempts,
|
|
91
|
+
unprocessed(params, response),
|
|
92
|
+
[*attempts, response]
|
|
85
93
|
)
|
|
86
|
-
return accumulate(attempts,
|
|
94
|
+
return accumulate(attempts, response)
|
|
87
95
|
|
|
88
96
|
def bulk_insert(self, items: Iterable):
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
self._batch_write([
|
|
98
|
+
{
|
|
99
|
+
'PutRequest': {
|
|
100
|
+
'Item': item,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
for item in items
|
|
104
|
+
])
|
|
92
105
|
|
|
93
106
|
def bulk_delete(self, items: Iterable):
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
self._batch_write([
|
|
108
|
+
{
|
|
109
|
+
'DeleteRequest': {
|
|
110
|
+
'Key': key,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
for key in items
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
def _marshall_request(self, params):
|
|
117
|
+
return {
|
|
118
|
+
key: self._marshall_value(key, value)
|
|
119
|
+
for key, value in params.items()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def _marshall_batch_get_request(self, params):
|
|
123
|
+
return {
|
|
124
|
+
**params,
|
|
125
|
+
'RequestItems': {
|
|
126
|
+
table_name: {
|
|
127
|
+
**request,
|
|
128
|
+
'Keys': [
|
|
129
|
+
self._marshall_item(key)
|
|
130
|
+
for key in request.get('Keys', [])
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
for table_name, request in params.get('RequestItems', {}).items()
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def _marshall_value(self, key, value):
|
|
138
|
+
if key in {'Key', 'Item', 'LastEvaluatedKey', 'ExclusiveStartKey'}:
|
|
139
|
+
return self._marshall_item(value)
|
|
140
|
+
if key == 'ExpressionAttributeValues':
|
|
141
|
+
return {
|
|
142
|
+
attr: self.serializer.serialize(attr_value)
|
|
143
|
+
for attr, attr_value in value.items()
|
|
144
|
+
}
|
|
145
|
+
return value
|
|
146
|
+
|
|
147
|
+
def _marshall_item(self, item):
|
|
148
|
+
return {
|
|
149
|
+
key: self.serializer.serialize(value)
|
|
150
|
+
for key, value in item.items()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def _unmarshall_response(self, response):
|
|
154
|
+
return {
|
|
155
|
+
key: self._unmarshall_value(key, value)
|
|
156
|
+
for key, value in response.items()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def _unmarshall_value(self, key, value):
|
|
160
|
+
if key in {'Item', 'LastEvaluatedKey'}:
|
|
161
|
+
return self._unmarshall_item(value)
|
|
162
|
+
if key == 'Items':
|
|
163
|
+
return [self._unmarshall_item(item) for item in value]
|
|
164
|
+
if key == 'Attributes':
|
|
165
|
+
return self._unmarshall_item(value)
|
|
166
|
+
if key == 'Responses':
|
|
167
|
+
return {
|
|
168
|
+
table_name: [self._unmarshall_item(item) for item in items]
|
|
169
|
+
for table_name, items in value.items()
|
|
170
|
+
}
|
|
171
|
+
if key == 'UnprocessedKeys':
|
|
172
|
+
return {
|
|
173
|
+
table_name: {
|
|
174
|
+
**request,
|
|
175
|
+
'Keys': [self._unmarshall_item(item) for item in request.get('Keys', [])],
|
|
176
|
+
}
|
|
177
|
+
for table_name, request in value.items()
|
|
178
|
+
}
|
|
179
|
+
return value
|
|
180
|
+
|
|
181
|
+
def _unmarshall_item(self, item):
|
|
182
|
+
return {
|
|
183
|
+
key: self.deserializer.deserialize(value)
|
|
184
|
+
for key, value in item.items()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def serializer(self):
|
|
189
|
+
if not self._serializer:
|
|
190
|
+
self._load_dynamodb_types()
|
|
191
|
+
return self._serializer
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def deserializer(self):
|
|
195
|
+
if not self._deserializer:
|
|
196
|
+
self._load_dynamodb_types()
|
|
197
|
+
return self._deserializer
|
|
198
|
+
|
|
199
|
+
def _load_dynamodb_types(self):
|
|
200
|
+
from boto3.dynamodb.types import TypeDeserializer, TypeSerializer
|
|
201
|
+
self._serializer = TypeSerializer()
|
|
202
|
+
self._deserializer = TypeDeserializer()
|
|
203
|
+
|
|
204
|
+
def _batch_write(self, requests):
|
|
205
|
+
for index in range(0, len(requests), 25):
|
|
206
|
+
self._batch_write_chunk(requests[index:index + 25], [])
|
|
207
|
+
|
|
208
|
+
def _batch_write_chunk(self, requests, attempts):
|
|
209
|
+
if not requests:
|
|
210
|
+
return
|
|
211
|
+
assert_max_retries(attempts, self.retry_config['max_retries'])
|
|
212
|
+
wait(get_delay(self.retry_config['retry_wait'], len(attempts)))
|
|
213
|
+
response = self.client.batch_write_item(
|
|
214
|
+
RequestItems={
|
|
215
|
+
self.table_name: [
|
|
216
|
+
self._marshall_write_request(request)
|
|
217
|
+
for request in requests
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
unprocessed_items = response.get('UnprocessedItems', {}).get(self.table_name, [])
|
|
222
|
+
if unprocessed_items:
|
|
223
|
+
self._batch_write_chunk(
|
|
224
|
+
[
|
|
225
|
+
self._unmarshall_write_request(request)
|
|
226
|
+
for request in unprocessed_items
|
|
227
|
+
],
|
|
228
|
+
[*attempts, response],
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _marshall_write_request(self, request):
|
|
232
|
+
if 'PutRequest' in request:
|
|
233
|
+
return {
|
|
234
|
+
'PutRequest': {
|
|
235
|
+
'Item': self._marshall_item(request['PutRequest']['Item']),
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
'DeleteRequest': {
|
|
240
|
+
'Key': self._marshall_item(request['DeleteRequest']['Key']),
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def _unmarshall_write_request(self, request):
|
|
245
|
+
if 'PutRequest' in request:
|
|
246
|
+
return {
|
|
247
|
+
'PutRequest': {
|
|
248
|
+
'Item': self._unmarshall_item(request['PutRequest']['Item']),
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
'DeleteRequest': {
|
|
253
|
+
'Key': self._unmarshall_item(request['DeleteRequest']['Key']),
|
|
254
|
+
},
|
|
255
|
+
}
|
|
97
256
|
|
|
98
257
|
|
|
99
258
|
def unprocessed(params, resp):
|
|
@@ -17,9 +17,12 @@ class AwsConnectorsModule(Module):
|
|
|
17
17
|
@singleton
|
|
18
18
|
@provider
|
|
19
19
|
def provide_dynamodb(self) -> IDynamodbConnector:
|
|
20
|
+
import os
|
|
20
21
|
from modmex_lambda.connectors.dynamodb import Connector
|
|
21
22
|
|
|
22
|
-
return Connector(
|
|
23
|
+
return Connector(
|
|
24
|
+
table_name=os.getenv('ENTITY_TABLE_NAME') or os.getenv('EVENT_TABLE_NAME'),
|
|
25
|
+
)
|
|
23
26
|
|
|
24
27
|
@singleton
|
|
25
28
|
@provider
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""DynamoDB persistence helpers."""
|
|
2
|
+
|
|
3
|
+
from modmex_lambda.persistence.dynamodb.expressions import (
|
|
4
|
+
pk_condition,
|
|
5
|
+
timestamp_condition,
|
|
6
|
+
update_expression,
|
|
7
|
+
)
|
|
8
|
+
from modmex_lambda.persistence.dynamodb.keys import (
|
|
9
|
+
AggregateKeyStrategy,
|
|
10
|
+
KeyStrategy,
|
|
11
|
+
SingleEntityKeyStrategy,
|
|
12
|
+
TenantPartitionKeyStrategy,
|
|
13
|
+
TenantScopedSortKeyStrategy,
|
|
14
|
+
)
|
|
15
|
+
from modmex_lambda.persistence.dynamodb.materialized_views import (
|
|
16
|
+
DynamoDBUpdateRequestMixin,
|
|
17
|
+
MaterializedViewMixin,
|
|
18
|
+
)
|
|
19
|
+
from modmex_lambda.persistence.dynamodb.stream_fields import (
|
|
20
|
+
DefaultStreamFieldsStrategy,
|
|
21
|
+
StreamFieldsStrategy,
|
|
22
|
+
stream_entity_fields,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AggregateKeyStrategy",
|
|
27
|
+
"DynamoDBUpdateRequestMixin",
|
|
28
|
+
"KeyStrategy",
|
|
29
|
+
"MaterializedViewMixin",
|
|
30
|
+
"SingleEntityKeyStrategy",
|
|
31
|
+
"DefaultStreamFieldsStrategy",
|
|
32
|
+
"StreamFieldsStrategy",
|
|
33
|
+
"TenantPartitionKeyStrategy",
|
|
34
|
+
"TenantScopedSortKeyStrategy",
|
|
35
|
+
"pk_condition",
|
|
36
|
+
"stream_entity_fields",
|
|
37
|
+
"timestamp_condition",
|
|
38
|
+
"update_expression",
|
|
39
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from functools import reduce
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def update_expression(item: dict):
|
|
5
|
+
keys = item.keys()
|
|
6
|
+
result = {}
|
|
7
|
+
result['ExpressionAttributeNames'] = reduce(
|
|
8
|
+
lambda accumulator, el: {**accumulator, **el},
|
|
9
|
+
map(
|
|
10
|
+
lambda attrName: {f"#{attrName}": attrName },
|
|
11
|
+
keys
|
|
12
|
+
),
|
|
13
|
+
{}
|
|
14
|
+
)
|
|
15
|
+
result['ExpressionAttributeValues'] = reduce(
|
|
16
|
+
lambda accumulator, el: {**accumulator, **el},
|
|
17
|
+
map(
|
|
18
|
+
lambda attrName: {f":{attrName}": item[attrName] },
|
|
19
|
+
filter(
|
|
20
|
+
lambda attrName: item[attrName] is not None,
|
|
21
|
+
keys
|
|
22
|
+
)
|
|
23
|
+
),
|
|
24
|
+
{}
|
|
25
|
+
)
|
|
26
|
+
result['UpdateExpression'] = "SET "+", ".join(map(
|
|
27
|
+
lambda attrName: f"#{attrName} = :{attrName}",
|
|
28
|
+
filter(
|
|
29
|
+
lambda attrName: item[attrName] is not None,
|
|
30
|
+
keys
|
|
31
|
+
)
|
|
32
|
+
))
|
|
33
|
+
update_expression_remove = ", ".join(map(
|
|
34
|
+
lambda attrName: f"#{attrName}",
|
|
35
|
+
filter(
|
|
36
|
+
lambda attrName: item[attrName] is None,
|
|
37
|
+
keys
|
|
38
|
+
)
|
|
39
|
+
))
|
|
40
|
+
if update_expression_remove:
|
|
41
|
+
result['UpdateExpression'] = "{} REMOVE {}".format(
|
|
42
|
+
result['UpdateExpression'],
|
|
43
|
+
update_expression_remove
|
|
44
|
+
)
|
|
45
|
+
result['ReturnValues'] = 'ALL_NEW'
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def timestamp_condition(field_name = 'timestamp'):
|
|
50
|
+
return {
|
|
51
|
+
'ConditionExpression': "attribute_not_exists(#{fn}) OR #{fn} < :{fn}".format(
|
|
52
|
+
fn=field_name
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pk_condition(field_name = 'pk'):
|
|
58
|
+
return {
|
|
59
|
+
'ConditionExpression': "attribute_not_exists({})".format(
|
|
60
|
+
field_name
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -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,80 @@
|
|
|
1
|
+
"""DynamoDB helpers for materialized view update requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from modmex_lambda.persistence.dynamodb.expressions import timestamp_condition, update_expression
|
|
8
|
+
from modmex_lambda.persistence.dynamodb.stream_fields import stream_entity_fields
|
|
9
|
+
from modmex_lambda.stream.utils.time import ttl as stream_ttl
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DynamoDBUpdateRequestMixin:
|
|
13
|
+
"""Build DynamoDB UpdateItem requests with a timestamp guard by default."""
|
|
14
|
+
|
|
15
|
+
def build_update_request(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
key: dict[str, Any],
|
|
19
|
+
fields: dict[str, Any],
|
|
20
|
+
timestamp_condition_enabled: bool = True,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
request = {
|
|
23
|
+
"Key": key,
|
|
24
|
+
**update_expression(fields),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if timestamp_condition_enabled:
|
|
28
|
+
request.update(timestamp_condition())
|
|
29
|
+
|
|
30
|
+
return request
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MaterializedViewMixin(DynamoDBUpdateRequestMixin):
|
|
34
|
+
"""Map stream unit-of-work events into DynamoDB materialized view updates."""
|
|
35
|
+
|
|
36
|
+
discriminator: str
|
|
37
|
+
materialized_source_name: str | None = None
|
|
38
|
+
materialized_last_modified_by: str | None = "system"
|
|
39
|
+
use_ttl: bool = False
|
|
40
|
+
days_ttl: int = 30
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def materialized_update_request_mapper(cls) -> Callable[[dict[str, Any]], dict[str, Any]]:
|
|
44
|
+
materializer = cls.__new__(cls)
|
|
45
|
+
return materializer.to_materialized_update_request
|
|
46
|
+
|
|
47
|
+
def to_materialized_update_request(self, uow: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
entity_name = self.materialized_source_name or self.discriminator
|
|
49
|
+
entity = uow["event"][entity_name]
|
|
50
|
+
|
|
51
|
+
return self.build_update_request(
|
|
52
|
+
key=self.materialized_key(uow, entity),
|
|
53
|
+
fields=self.materialized_fields(uow, entity),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def materialized_key(self, uow: dict[str, Any], entity: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"pk": entity["id"],
|
|
59
|
+
"sk": entity.get("sk", self.discriminator),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def materialized_fields(self, uow: dict[str, Any], entity: dict[str, Any]) -> dict[str, Any]:
|
|
63
|
+
timestamp = uow["event"]["timestamp"]
|
|
64
|
+
source_name = self.materialized_source_name or self.discriminator
|
|
65
|
+
fields = {
|
|
66
|
+
**{key: value for key, value in entity.items() if key not in ["pk", "sk"]},
|
|
67
|
+
**stream_entity_fields(
|
|
68
|
+
self.discriminator,
|
|
69
|
+
timestamp=timestamp,
|
|
70
|
+
deleted=True if uow["event"]["type"] == f"{source_name}-deleted" else None,
|
|
71
|
+
latched=True,
|
|
72
|
+
ttl=stream_ttl(timestamp, self.days_ttl) if self.use_ttl else None,
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
if self.materialized_last_modified_by is not None:
|
|
76
|
+
fields["last_modified_by"] = self.materialized_last_modified_by
|
|
77
|
+
return fields
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ["DynamoDBUpdateRequestMixin", "MaterializedViewMixin"]
|
|
@@ -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
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import Union
|
|
3
|
-
from functools import reduce
|
|
4
3
|
from boto3.dynamodb.types import TypeDeserializer, TypeSerializer
|
|
5
4
|
from modmex_lambda.connectors.dynamodb import Connector
|
|
5
|
+
from modmex_lambda.persistence.dynamodb.expressions import (
|
|
6
|
+
pk_condition,
|
|
7
|
+
timestamp_condition,
|
|
8
|
+
update_expression,
|
|
9
|
+
)
|
|
6
10
|
from modmex_lambda.stream.operators.dynamodb import (
|
|
7
11
|
BatchGetDynamoDB,
|
|
8
12
|
PutDynamoDB,
|
|
@@ -20,65 +24,6 @@ def serialize_number(number: str) -> Union[float, int]:
|
|
|
20
24
|
|
|
21
25
|
setattr(TypeDeserializer, '_deserialize_n', lambda _, number: serialize_number(number))
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
def update_expression(item: dict):
|
|
25
|
-
keys = item.keys()
|
|
26
|
-
result = {}
|
|
27
|
-
result['ExpressionAttributeNames'] = reduce(
|
|
28
|
-
lambda accumulator, el: {**accumulator, **el},
|
|
29
|
-
map(
|
|
30
|
-
lambda attrName: {f"#{attrName}": attrName },
|
|
31
|
-
keys
|
|
32
|
-
),
|
|
33
|
-
{}
|
|
34
|
-
)
|
|
35
|
-
result['ExpressionAttributeValues'] = reduce(
|
|
36
|
-
lambda accumulator, el: {**accumulator, **el},
|
|
37
|
-
map(
|
|
38
|
-
lambda attrName: {f":{attrName}": item[attrName] },
|
|
39
|
-
filter(
|
|
40
|
-
lambda attrName: item[attrName] is not None,
|
|
41
|
-
keys
|
|
42
|
-
)
|
|
43
|
-
),
|
|
44
|
-
{}
|
|
45
|
-
)
|
|
46
|
-
result['UpdateExpression'] = "SET "+", ".join(map(
|
|
47
|
-
lambda attrName: f"#{attrName} = :{attrName}",
|
|
48
|
-
filter(
|
|
49
|
-
lambda attrName: item[attrName] is not None,
|
|
50
|
-
keys
|
|
51
|
-
)
|
|
52
|
-
))
|
|
53
|
-
update_expression_remove = ", ".join(map(
|
|
54
|
-
lambda attrName: f"#{attrName}",
|
|
55
|
-
filter(
|
|
56
|
-
lambda attrName: item[attrName] is None,
|
|
57
|
-
keys
|
|
58
|
-
)
|
|
59
|
-
))
|
|
60
|
-
if update_expression_remove:
|
|
61
|
-
result['UpdateExpression'] = "{} REMOVE {}".format(
|
|
62
|
-
result['UpdateExpression'],
|
|
63
|
-
update_expression_remove
|
|
64
|
-
)
|
|
65
|
-
result['ReturnValues'] = 'ALL_NEW'
|
|
66
|
-
return result
|
|
67
|
-
|
|
68
|
-
def timestamp_condition(field_name = 'timestamp'):
|
|
69
|
-
return {
|
|
70
|
-
'ConditionExpression': "attribute_not_exists(#{fn}) OR #{fn} < :{fn}".format(
|
|
71
|
-
fn=field_name
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
def pk_condition(field_name = 'pk'):
|
|
76
|
-
return {
|
|
77
|
-
'ConditionExpression': "attribute_not_exists({})".format(
|
|
78
|
-
field_name
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
27
|
def update_dynamodb(
|
|
83
28
|
table_name=os.getenv('ENTITY_TABLE_NAME') or os.getenv('EVENT_TABLE_NAME'),
|
|
84
29
|
update_request_field='update_request',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modmex-lambda
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Ultra-lightweight AWS Lambda utilities for API Gateway routing and event handling.
|
|
5
5
|
Author: Modmex
|
|
6
6
|
License: MIT
|
|
@@ -36,6 +36,8 @@ API Gateway proxy events, fast routing, request binding, response serialization,
|
|
|
36
36
|
middleware, dependency injection, stream sources, parsing, and structured
|
|
37
37
|
logging.
|
|
38
38
|
|
|
39
|
+
The fuller documentation site lives in [`docs/`](docs/).
|
|
40
|
+
|
|
39
41
|
## Install
|
|
40
42
|
|
|
41
43
|
```bash
|
|
@@ -979,37 +981,66 @@ materialize so queries stay local and fast, and each service owns the model it
|
|
|
979
981
|
needs instead of querying another service synchronously.
|
|
980
982
|
|
|
981
983
|
```python
|
|
984
|
+
from modmex_lambda.persistence.dynamodb import MaterializedViewMixin
|
|
982
985
|
from modmex_lambda.stream.flavors.materialize import Materialize
|
|
983
986
|
from modmex_lambda.stream.rules_registry import RulesRegistry
|
|
984
987
|
from modmex_lambda.stream.sources import KinesisSource
|
|
985
|
-
from modmex_lambda.stream.utils.dynamodb import update_expression
|
|
986
988
|
|
|
987
989
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
"sk": "THING",
|
|
994
|
-
},
|
|
995
|
-
**update_expression({
|
|
996
|
-
"name": thing["name"],
|
|
997
|
-
"timestamp": uow["event"]["timestamp"],
|
|
998
|
-
}),
|
|
999
|
-
}
|
|
990
|
+
class ThingViewRepository(MaterializedViewMixin):
|
|
991
|
+
discriminator = "thing"
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
to_update_request = ThingViewRepository.materialized_update_request_mapper()
|
|
1000
995
|
|
|
1001
996
|
|
|
1002
997
|
registry = RulesRegistry().registry(
|
|
1003
998
|
Materialize({
|
|
1004
999
|
"id": "materialize-thing",
|
|
1005
1000
|
"event_type": "thing-created",
|
|
1006
|
-
"to_update_request":
|
|
1001
|
+
"to_update_request": to_update_request,
|
|
1007
1002
|
})
|
|
1008
1003
|
)
|
|
1009
1004
|
|
|
1010
1005
|
handler = KinesisSource(registry, concurrency=False).handle
|
|
1011
1006
|
```
|
|
1012
1007
|
|
|
1008
|
+
`MaterializedViewMixin` is a persistence helper for the common DynamoDB case:
|
|
1009
|
+
copy the entity from `uow["event"][discriminator]`, build a `Key` with
|
|
1010
|
+
`pk=entity["id"]` and `sk=discriminator`, add stream-compatible fields, and
|
|
1011
|
+
protect the write with `timestamp_condition()` so older events do not overwrite
|
|
1012
|
+
newer records. It adds stream-compatible fields such as `discriminator`,
|
|
1013
|
+
`timestamp`, `deleted`, `latched`, `ttl`, and `awsregion` to the update
|
|
1014
|
+
expression.
|
|
1015
|
+
|
|
1016
|
+
Override the hooks when the event source, key, projection, or enrichment is
|
|
1017
|
+
different from the default:
|
|
1018
|
+
|
|
1019
|
+
```python
|
|
1020
|
+
from modmex_lambda.persistence.dynamodb import MaterializedViewMixin
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
class ThingSearchViewRepository(MaterializedViewMixin):
|
|
1024
|
+
discriminator = "thing-search"
|
|
1025
|
+
materialized_source_name = "thing"
|
|
1026
|
+
|
|
1027
|
+
def materialized_key(self, uow, thing):
|
|
1028
|
+
return {
|
|
1029
|
+
"pk": thing["tenant_id"],
|
|
1030
|
+
"sk": f"thing-search#{thing['id']}",
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
def materialized_fields(self, uow, thing):
|
|
1034
|
+
return {
|
|
1035
|
+
**super().materialized_fields(uow, thing),
|
|
1036
|
+
"search_text": f"{thing['name']} {thing['tenant_id']}",
|
|
1037
|
+
}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
Use `DynamoDBUpdateRequestMixin.build_update_request()` directly when you already
|
|
1041
|
+
have the key and fields but still want the standard `UpdateItem` expression and
|
|
1042
|
+
timestamp guard.
|
|
1043
|
+
|
|
1013
1044
|
Use `split_on` and `split_target_field` when one event updates several records,
|
|
1014
1045
|
for example one `order-created` event materializing each order item.
|
|
1015
1046
|
|
|
@@ -13,7 +13,7 @@ modmex_lambda/tracing.py,sha256=WWA2xAqfa23phxoQzRnr6UtdK3S770jPODYh4CwFk0M,5739
|
|
|
13
13
|
modmex_lambda/validation.py,sha256=64uWzSnkxzN1t7DAqC_0TYzhupQUtAWnrLATEFAEfzs,7130
|
|
14
14
|
modmex_lambda/connectors/__init__.py,sha256=-3G8Q5Qo_O6xlD8oWLYW7XCgMln50BTjhrUncAePnUA,716
|
|
15
15
|
modmex_lambda/connectors/cloudwatch.py,sha256=8NnP6m7IGZwwFZaHw6EO2Q1g4yHUfFqagpkVpzlgW8E,455
|
|
16
|
-
modmex_lambda/connectors/dynamodb.py,sha256=
|
|
16
|
+
modmex_lambda/connectors/dynamodb.py,sha256=QTUtud8pmsmPER2N0foQB9dBg3yCrfEacYk8FfU1XfQ,8821
|
|
17
17
|
modmex_lambda/connectors/eventbridge.py,sha256=atY6R4XqRL-n6y5-ajn3b-t0gDwaBvz1w5rU4yMmq1U,473
|
|
18
18
|
modmex_lambda/connectors/icloudwatch.py,sha256=LAxYgMgzda4wtGGRwhsPlZikpGlA3oiUsk73jlJ7ghk,463
|
|
19
19
|
modmex_lambda/connectors/idynamodb.py,sha256=oZiOztqY59U9O5VpxsAG_b1H2L4VaMH0lWhGGapHfYo,2538
|
|
@@ -23,7 +23,7 @@ modmex_lambda/connectors/is3.py,sha256=o3NCwCUqyK7vchSBVusVxhnTjaWaej1TaCmAKiwFA
|
|
|
23
23
|
modmex_lambda/connectors/isns.py,sha256=7EgdIUuVC1V0wsdNaZthdepRDZkpw6XBUUzWr6C8NW8,717
|
|
24
24
|
modmex_lambda/connectors/isqs.py,sha256=qd-MPGuJKeKFJ3wK-RvvPCl_f0x6NC9n8GRsiqI5krQ,706
|
|
25
25
|
modmex_lambda/connectors/lambda_.py,sha256=kHpQxGSX-5tAtSZlI0VdR3WLP9KCCPIjcnACqTI9vqE,421
|
|
26
|
-
modmex_lambda/connectors/module.py,sha256=
|
|
26
|
+
modmex_lambda/connectors/module.py,sha256=u1wLIt7JF7ncCJA8qX5tTwhJ906u-XnE84B8CzM7yxc,1967
|
|
27
27
|
modmex_lambda/connectors/s3.py,sha256=sEf4_xV-kQXkOTIwo_Ez9RJxmJWDzRu0mLe0SLjY8-I,1135
|
|
28
28
|
modmex_lambda/connectors/sns.py,sha256=1X_5laGHTtyOH5gXqhDWk9YnclHzEac1R76xaijZQ_U,879
|
|
29
29
|
modmex_lambda/connectors/sqs.py,sha256=LwzO3gkQ6bfE6WMJW1KrhsymUqtdZ94RK1suXL7G-k0,806
|
|
@@ -55,6 +55,12 @@ 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=f_R3NHWfHvTWkCYfBHqY-z75r7pESsUv5FmFf05Glgg,1026
|
|
60
|
+
modmex_lambda/persistence/dynamodb/expressions.py,sha256=E_FDebLgROv63DBRQXKuO1hXjTt9g1kGzbgH2YBNu-Y,1694
|
|
61
|
+
modmex_lambda/persistence/dynamodb/keys.py,sha256=_vjpHYzRmi1OmCzHaJNpq8vefjH7kq8yxE1t7RhmmQg,4676
|
|
62
|
+
modmex_lambda/persistence/dynamodb/materialized_views.py,sha256=CnSWZMYkEHGEpTnWkurB2uxxTN_tPrGFgRthF_0ILng,2892
|
|
63
|
+
modmex_lambda/persistence/dynamodb/stream_fields.py,sha256=b6wRRq6Ag9W59GCI8616IlBQXeBIwj5RKEQE0lfD4YE,3323
|
|
58
64
|
modmex_lambda/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
65
|
modmex_lambda/shared/cookies.py,sha256=E2hTuht5_Xljw9zOQLW6vjsOWEpQSIqD2D3Tl1Co_s0,2076
|
|
60
66
|
modmex_lambda/shared/headers_serializer.py,sha256=Ao-wpx0cmy9E2ACrh7R7RIDUF0h24gax1AMtY_MjIWQ,2505
|
|
@@ -113,7 +119,7 @@ modmex_lambda/stream/utils/cloudwatch.py,sha256=B9y-Ab5EaZeso5ph3RxRaK8tU-mQyEvA
|
|
|
113
119
|
modmex_lambda/stream/utils/concurrency.py,sha256=7fHyQF4SkOkcao8XmG6UOJQersJhf0n5kauk9Yr_-F0,1336
|
|
114
120
|
modmex_lambda/stream/utils/contracts.py,sha256=SpGX4JGukHmeb1y2ChahuNUrHtHA_rA61VXz_13VvbU,931
|
|
115
121
|
modmex_lambda/stream/utils/decorators.py,sha256=-Z1ebvN2m07-lfiEkgRPTEZDL9OI6GSI3w7LWq2Xn88,163
|
|
116
|
-
modmex_lambda/stream/utils/dynamodb.py,sha256=
|
|
122
|
+
modmex_lambda/stream/utils/dynamodb.py,sha256=vi9OEU9DJDzxFdwQkObk-kuZrT0Z1DFOmMrs4eUoXZ8,2670
|
|
117
123
|
modmex_lambda/stream/utils/eventbridge.py,sha256=_3Oe6eHFdBBuE-9XowGjPbXxQb60N3v-47ieBWp7tv0,745
|
|
118
124
|
modmex_lambda/stream/utils/faults.py,sha256=iXIzzSOHIW-kW4J2PKW_v1KviFpao2btvSrnRUer-d4,2660
|
|
119
125
|
modmex_lambda/stream/utils/filters.py,sha256=8OmVfHt_xmczgZYyq6OkwEByMg5kQKioPQKA0le1CbY,433
|
|
@@ -132,7 +138,7 @@ modmex_lambda/stream/utils/time.py,sha256=dnsL2xeWpa1FXm2p8y9rWXtICMBlCBkgLaJ_en
|
|
|
132
138
|
modmex_lambda/stream/utils/uow.py,sha256=8WSPVE7AiHInkFDfBEeKOvdirY6GAgBoS7qkF1f1JVM,3207
|
|
133
139
|
modmex_lambda/stream/utils/data_classes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
134
140
|
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.
|
|
141
|
+
modmex_lambda-0.5.4.dist-info/METADATA,sha256=Vt6fiHLcaMqIv2D8dAYERIo2891Az0v_GFLDN_Q0Y5k,33046
|
|
142
|
+
modmex_lambda-0.5.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
143
|
+
modmex_lambda-0.5.4.dist-info/licenses/LICENSE,sha256=_C2TDTOsYeJvE4vn9VB51laKvleBKbdNnn96wJVtXhQ,1063
|
|
144
|
+
modmex_lambda-0.5.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|