aws-python-helper 0.36.0__tar.gz → 0.38.0__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.
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/PKG-INFO +170 -1
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/README.md +169 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/__init__.py +4 -0
- aws_python_helper-0.38.0/aws_python_helper/model_query/__init__.py +3 -0
- aws_python_helper-0.38.0/aws_python_helper/model_query/base.py +179 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper.egg-info/PKG-INFO +170 -1
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper.egg-info/SOURCES.txt +2 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/pyproject.toml +1 -1
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/dispatcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/context/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/context/session.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/context/state_validator.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/fargate/executor.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/fargate/task_base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/invoker/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/invoker/api_client.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/invoker/exceptions.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/invoker/lambda_invoker.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/lambda_standalone/base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/repository/base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/sns/publisher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/sqs/consumer_base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-python-helper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.38.0
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -38,6 +38,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
38
38
|
- **Pydantic schema validation**: Declare a `schema` property on any `API` or `Lambda` and the framework validates automatically before `validate()` — returns 400 on failure
|
|
39
39
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
40
40
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
41
|
+
- **ModelQueryLambda**: Base class for MongoDB query-proxy Lambdas — declare an `allowed_collections` whitelist and the framework handles `find` and `aggregate` operations generically
|
|
41
42
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
42
43
|
- **Type hints**: Modern Python with type annotations
|
|
43
44
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -65,6 +66,7 @@ All available classes and functions:
|
|
|
65
66
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
66
67
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
67
68
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
69
|
+
| `ModelQueryLambda` | `aws_python_helper` | Generic MongoDB query-proxy Lambda base class |
|
|
68
70
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
69
71
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
70
72
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1164,6 +1166,173 @@ module "my_lambda" {
|
|
|
1164
1166
|
|
|
1165
1167
|
---
|
|
1166
1168
|
|
|
1169
|
+
## 🔍 ModelQueryLambda
|
|
1170
|
+
|
|
1171
|
+
`ModelQueryLambda` is a ready-to-use base class for creating MongoDB query-proxy Lambdas. Instead of writing a custom Lambda every time another microservice needs to read data from your MongoDB, you subclass `ModelQueryLambda`, declare an `allowed_collections` whitelist, and the framework exposes both `find` and `aggregate` operations automatically.
|
|
1172
|
+
|
|
1173
|
+
**Use case:** microservice A needs to query data owned by microservice B. Microservice B deploys one `model-query` Lambda; microservice A calls it via `LambdaInvoker` passing the collection name and query parameters.
|
|
1174
|
+
|
|
1175
|
+
### Creating a model-query Lambda
|
|
1176
|
+
|
|
1177
|
+
Create `src/lambda/model-query/main.py` in the microservice that owns the data:
|
|
1178
|
+
|
|
1179
|
+
```python
|
|
1180
|
+
from aws_python_helper import ModelQueryLambda
|
|
1181
|
+
|
|
1182
|
+
class ModelQueryLambda(ModelQueryLambda):
|
|
1183
|
+
|
|
1184
|
+
@property
|
|
1185
|
+
def allowed_collections(self) -> list:
|
|
1186
|
+
return ["orders", "customers"] # whitelist — unlisted collections are rejected
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
Register the handler in `src/handlers/lambda_handler.py`:
|
|
1190
|
+
|
|
1191
|
+
```python
|
|
1192
|
+
from aws_python_helper.lambda_standalone.handler import lambda_handler
|
|
1193
|
+
|
|
1194
|
+
model_query_handler = lambda_handler('model-query')
|
|
1195
|
+
|
|
1196
|
+
__all__ = ["model_query_handler"]
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
### Query modes
|
|
1200
|
+
|
|
1201
|
+
#### Aggregate pipeline
|
|
1202
|
+
|
|
1203
|
+
Pass a `pipeline` list to run a MongoDB aggregation:
|
|
1204
|
+
|
|
1205
|
+
```python
|
|
1206
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1207
|
+
"session": self.session.to_dict(),
|
|
1208
|
+
"collection": "orders",
|
|
1209
|
+
"pipeline": [
|
|
1210
|
+
{"$match": {"status": "pending"}},
|
|
1211
|
+
{"$sort": {"created_at": -1}},
|
|
1212
|
+
{"$limit": 50},
|
|
1213
|
+
],
|
|
1214
|
+
})
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
#### Find with filter
|
|
1218
|
+
|
|
1219
|
+
Pass `filter`, `fields`, `limit`, and/or `skip` to run a `find`:
|
|
1220
|
+
|
|
1221
|
+
```python
|
|
1222
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1223
|
+
"session": self.session.to_dict(),
|
|
1224
|
+
"collection": "orders",
|
|
1225
|
+
"filter": {"customer_id": "abc123", "status": "active"},
|
|
1226
|
+
"fields": {"_id": 1, "total": 1, "status": 1}, # projection
|
|
1227
|
+
"limit": 20,
|
|
1228
|
+
"skip": 0,
|
|
1229
|
+
})
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
**Note:** `pipeline` and `fields` are mutually exclusive — the framework rejects payloads that include both.
|
|
1233
|
+
|
|
1234
|
+
#### Querying by `_id`
|
|
1235
|
+
|
|
1236
|
+
ObjectId hex strings in `_id` filters are automatically coerced to `ObjectId` so callers can serialize queries as plain JSON:
|
|
1237
|
+
|
|
1238
|
+
```python
|
|
1239
|
+
# Plain value
|
|
1240
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1241
|
+
"session": self.session.to_dict(),
|
|
1242
|
+
"collection": "orders",
|
|
1243
|
+
"filter": {"_id": "65f1a2b3c4d5e6f7a8b9c0d1"},
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
# Operator with list
|
|
1247
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1248
|
+
"session": self.session.to_dict(),
|
|
1249
|
+
"collection": "orders",
|
|
1250
|
+
"filter": {"_id": {"$in": ["65f1...", "65f2...", "65f3..."]}},
|
|
1251
|
+
})
|
|
1252
|
+
|
|
1253
|
+
# 'id' is accepted as an alias for '_id'
|
|
1254
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1255
|
+
"session": self.session.to_dict(),
|
|
1256
|
+
"collection": "orders",
|
|
1257
|
+
"filter": {"id": "65f1a2b3c4d5e6f7a8b9c0d1"},
|
|
1258
|
+
})
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
Supported operators on `_id`: scalar (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) and list (`$in`, `$nin`). The same coercion is applied to `$match` stages inside a `pipeline`. If the string is not a valid 24-char ObjectId hex, the Lambda raises a clear validation error.
|
|
1262
|
+
|
|
1263
|
+
### Payload reference
|
|
1264
|
+
|
|
1265
|
+
| Field | Type | Required | Description |
|
|
1266
|
+
|-------|------|----------|-------------|
|
|
1267
|
+
| `session` | `dict` | Yes | Session dict from `self.session.to_dict()` — drives state-scoped DB routing |
|
|
1268
|
+
| `collection` | `str` | Yes | Collection name (must be in `allowed_collections`) |
|
|
1269
|
+
| `pipeline` | `list` | One of | MongoDB aggregation pipeline |
|
|
1270
|
+
| `filter` | `dict` | One of | MongoDB filter document (defaults to `{}`). `_id`/`id` string values are auto-coerced to `ObjectId` |
|
|
1271
|
+
| `fields` | `dict` | No | MongoDB projection (only valid with `filter` mode) |
|
|
1272
|
+
| `limit` | `int` | No | Max documents to return |
|
|
1273
|
+
| `skip` | `int` | No | Documents to skip (default `0`) |
|
|
1274
|
+
|
|
1275
|
+
### Response format
|
|
1276
|
+
|
|
1277
|
+
The Lambda always returns the standard framework envelope:
|
|
1278
|
+
|
|
1279
|
+
```json
|
|
1280
|
+
{
|
|
1281
|
+
"success": true,
|
|
1282
|
+
"data": [ ... ]
|
|
1283
|
+
}
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
On validation error (unknown collection, invalid payload):
|
|
1287
|
+
|
|
1288
|
+
```json
|
|
1289
|
+
{
|
|
1290
|
+
"success": false,
|
|
1291
|
+
"error": "Collection 'unknown' is not allowed. Allowed: orders, customers"
|
|
1292
|
+
}
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
### Calling it from another microservice
|
|
1296
|
+
|
|
1297
|
+
```python
|
|
1298
|
+
import os
|
|
1299
|
+
from aws_python_helper import LambdaInvoker, LambdaInvocationError
|
|
1300
|
+
|
|
1301
|
+
class OrdersPostAPI(API):
|
|
1302
|
+
|
|
1303
|
+
async def process(self):
|
|
1304
|
+
try:
|
|
1305
|
+
response = LambdaInvoker().invoke(
|
|
1306
|
+
os.getenv("ORDERS_MODEL_QUERY_LAMBDA_NAME"),
|
|
1307
|
+
payload={
|
|
1308
|
+
"session": self.session.to_dict(),
|
|
1309
|
+
"collection": "orders",
|
|
1310
|
+
"filter": {"status": "pending"},
|
|
1311
|
+
"limit": 100,
|
|
1312
|
+
},
|
|
1313
|
+
)
|
|
1314
|
+
except LambdaInvocationError as e:
|
|
1315
|
+
self.logger.error(f"model-query invocation failed: {e}")
|
|
1316
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1317
|
+
|
|
1318
|
+
if not response.get("success"):
|
|
1319
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1320
|
+
|
|
1321
|
+
orders = response.get("data", [])
|
|
1322
|
+
self.set_body({"orders": orders})
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Session and database routing
|
|
1326
|
+
|
|
1327
|
+
The `session` field in the payload is required. The `ModelQueryLambda` uses `session.state` to resolve the target database — the same state-scoped routing used by all framework components. This means:
|
|
1328
|
+
|
|
1329
|
+
- `{"session": {"state": "connecticut"}, ...}` → queries the `connecticut` database
|
|
1330
|
+
- `{"session": {"state": "new_jersey"}, ...}` → queries the `new_jersey` database
|
|
1331
|
+
|
|
1332
|
+
Always pass `self.session.to_dict()` when calling from an API or Lambda to ensure state propagates correctly.
|
|
1333
|
+
|
|
1334
|
+
---
|
|
1335
|
+
|
|
1167
1336
|
## 🏗️ Architecture Overview
|
|
1168
1337
|
|
|
1169
1338
|
Typical flow for event-driven architectures using this framework:
|
|
@@ -18,6 +18,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
18
18
|
- **Pydantic schema validation**: Declare a `schema` property on any `API` or `Lambda` and the framework validates automatically before `validate()` — returns 400 on failure
|
|
19
19
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
20
20
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
21
|
+
- **ModelQueryLambda**: Base class for MongoDB query-proxy Lambdas — declare an `allowed_collections` whitelist and the framework handles `find` and `aggregate` operations generically
|
|
21
22
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
22
23
|
- **Type hints**: Modern Python with type annotations
|
|
23
24
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -45,6 +46,7 @@ All available classes and functions:
|
|
|
45
46
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
46
47
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
47
48
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
49
|
+
| `ModelQueryLambda` | `aws_python_helper` | Generic MongoDB query-proxy Lambda base class |
|
|
48
50
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
49
51
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
50
52
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1144,6 +1146,173 @@ module "my_lambda" {
|
|
|
1144
1146
|
|
|
1145
1147
|
---
|
|
1146
1148
|
|
|
1149
|
+
## 🔍 ModelQueryLambda
|
|
1150
|
+
|
|
1151
|
+
`ModelQueryLambda` is a ready-to-use base class for creating MongoDB query-proxy Lambdas. Instead of writing a custom Lambda every time another microservice needs to read data from your MongoDB, you subclass `ModelQueryLambda`, declare an `allowed_collections` whitelist, and the framework exposes both `find` and `aggregate` operations automatically.
|
|
1152
|
+
|
|
1153
|
+
**Use case:** microservice A needs to query data owned by microservice B. Microservice B deploys one `model-query` Lambda; microservice A calls it via `LambdaInvoker` passing the collection name and query parameters.
|
|
1154
|
+
|
|
1155
|
+
### Creating a model-query Lambda
|
|
1156
|
+
|
|
1157
|
+
Create `src/lambda/model-query/main.py` in the microservice that owns the data:
|
|
1158
|
+
|
|
1159
|
+
```python
|
|
1160
|
+
from aws_python_helper import ModelQueryLambda
|
|
1161
|
+
|
|
1162
|
+
class ModelQueryLambda(ModelQueryLambda):
|
|
1163
|
+
|
|
1164
|
+
@property
|
|
1165
|
+
def allowed_collections(self) -> list:
|
|
1166
|
+
return ["orders", "customers"] # whitelist — unlisted collections are rejected
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
Register the handler in `src/handlers/lambda_handler.py`:
|
|
1170
|
+
|
|
1171
|
+
```python
|
|
1172
|
+
from aws_python_helper.lambda_standalone.handler import lambda_handler
|
|
1173
|
+
|
|
1174
|
+
model_query_handler = lambda_handler('model-query')
|
|
1175
|
+
|
|
1176
|
+
__all__ = ["model_query_handler"]
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
### Query modes
|
|
1180
|
+
|
|
1181
|
+
#### Aggregate pipeline
|
|
1182
|
+
|
|
1183
|
+
Pass a `pipeline` list to run a MongoDB aggregation:
|
|
1184
|
+
|
|
1185
|
+
```python
|
|
1186
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1187
|
+
"session": self.session.to_dict(),
|
|
1188
|
+
"collection": "orders",
|
|
1189
|
+
"pipeline": [
|
|
1190
|
+
{"$match": {"status": "pending"}},
|
|
1191
|
+
{"$sort": {"created_at": -1}},
|
|
1192
|
+
{"$limit": 50},
|
|
1193
|
+
],
|
|
1194
|
+
})
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
#### Find with filter
|
|
1198
|
+
|
|
1199
|
+
Pass `filter`, `fields`, `limit`, and/or `skip` to run a `find`:
|
|
1200
|
+
|
|
1201
|
+
```python
|
|
1202
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1203
|
+
"session": self.session.to_dict(),
|
|
1204
|
+
"collection": "orders",
|
|
1205
|
+
"filter": {"customer_id": "abc123", "status": "active"},
|
|
1206
|
+
"fields": {"_id": 1, "total": 1, "status": 1}, # projection
|
|
1207
|
+
"limit": 20,
|
|
1208
|
+
"skip": 0,
|
|
1209
|
+
})
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
**Note:** `pipeline` and `fields` are mutually exclusive — the framework rejects payloads that include both.
|
|
1213
|
+
|
|
1214
|
+
#### Querying by `_id`
|
|
1215
|
+
|
|
1216
|
+
ObjectId hex strings in `_id` filters are automatically coerced to `ObjectId` so callers can serialize queries as plain JSON:
|
|
1217
|
+
|
|
1218
|
+
```python
|
|
1219
|
+
# Plain value
|
|
1220
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1221
|
+
"session": self.session.to_dict(),
|
|
1222
|
+
"collection": "orders",
|
|
1223
|
+
"filter": {"_id": "65f1a2b3c4d5e6f7a8b9c0d1"},
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1226
|
+
# Operator with list
|
|
1227
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1228
|
+
"session": self.session.to_dict(),
|
|
1229
|
+
"collection": "orders",
|
|
1230
|
+
"filter": {"_id": {"$in": ["65f1...", "65f2...", "65f3..."]}},
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
# 'id' is accepted as an alias for '_id'
|
|
1234
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1235
|
+
"session": self.session.to_dict(),
|
|
1236
|
+
"collection": "orders",
|
|
1237
|
+
"filter": {"id": "65f1a2b3c4d5e6f7a8b9c0d1"},
|
|
1238
|
+
})
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
Supported operators on `_id`: scalar (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) and list (`$in`, `$nin`). The same coercion is applied to `$match` stages inside a `pipeline`. If the string is not a valid 24-char ObjectId hex, the Lambda raises a clear validation error.
|
|
1242
|
+
|
|
1243
|
+
### Payload reference
|
|
1244
|
+
|
|
1245
|
+
| Field | Type | Required | Description |
|
|
1246
|
+
|-------|------|----------|-------------|
|
|
1247
|
+
| `session` | `dict` | Yes | Session dict from `self.session.to_dict()` — drives state-scoped DB routing |
|
|
1248
|
+
| `collection` | `str` | Yes | Collection name (must be in `allowed_collections`) |
|
|
1249
|
+
| `pipeline` | `list` | One of | MongoDB aggregation pipeline |
|
|
1250
|
+
| `filter` | `dict` | One of | MongoDB filter document (defaults to `{}`). `_id`/`id` string values are auto-coerced to `ObjectId` |
|
|
1251
|
+
| `fields` | `dict` | No | MongoDB projection (only valid with `filter` mode) |
|
|
1252
|
+
| `limit` | `int` | No | Max documents to return |
|
|
1253
|
+
| `skip` | `int` | No | Documents to skip (default `0`) |
|
|
1254
|
+
|
|
1255
|
+
### Response format
|
|
1256
|
+
|
|
1257
|
+
The Lambda always returns the standard framework envelope:
|
|
1258
|
+
|
|
1259
|
+
```json
|
|
1260
|
+
{
|
|
1261
|
+
"success": true,
|
|
1262
|
+
"data": [ ... ]
|
|
1263
|
+
}
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
On validation error (unknown collection, invalid payload):
|
|
1267
|
+
|
|
1268
|
+
```json
|
|
1269
|
+
{
|
|
1270
|
+
"success": false,
|
|
1271
|
+
"error": "Collection 'unknown' is not allowed. Allowed: orders, customers"
|
|
1272
|
+
}
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
### Calling it from another microservice
|
|
1276
|
+
|
|
1277
|
+
```python
|
|
1278
|
+
import os
|
|
1279
|
+
from aws_python_helper import LambdaInvoker, LambdaInvocationError
|
|
1280
|
+
|
|
1281
|
+
class OrdersPostAPI(API):
|
|
1282
|
+
|
|
1283
|
+
async def process(self):
|
|
1284
|
+
try:
|
|
1285
|
+
response = LambdaInvoker().invoke(
|
|
1286
|
+
os.getenv("ORDERS_MODEL_QUERY_LAMBDA_NAME"),
|
|
1287
|
+
payload={
|
|
1288
|
+
"session": self.session.to_dict(),
|
|
1289
|
+
"collection": "orders",
|
|
1290
|
+
"filter": {"status": "pending"},
|
|
1291
|
+
"limit": 100,
|
|
1292
|
+
},
|
|
1293
|
+
)
|
|
1294
|
+
except LambdaInvocationError as e:
|
|
1295
|
+
self.logger.error(f"model-query invocation failed: {e}")
|
|
1296
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1297
|
+
|
|
1298
|
+
if not response.get("success"):
|
|
1299
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1300
|
+
|
|
1301
|
+
orders = response.get("data", [])
|
|
1302
|
+
self.set_body({"orders": orders})
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
### Session and database routing
|
|
1306
|
+
|
|
1307
|
+
The `session` field in the payload is required. The `ModelQueryLambda` uses `session.state` to resolve the target database — the same state-scoped routing used by all framework components. This means:
|
|
1308
|
+
|
|
1309
|
+
- `{"session": {"state": "connecticut"}, ...}` → queries the `connecticut` database
|
|
1310
|
+
- `{"session": {"state": "new_jersey"}, ...}` → queries the `new_jersey` database
|
|
1311
|
+
|
|
1312
|
+
Always pass `self.session.to_dict()` when calling from an API or Lambda to ensure state propagates correctly.
|
|
1313
|
+
|
|
1314
|
+
---
|
|
1315
|
+
|
|
1147
1316
|
## 🏗️ Architecture Overview
|
|
1148
1317
|
|
|
1149
1318
|
Typical flow for event-driven architectures using this framework:
|
|
@@ -31,6 +31,9 @@ from .repository.base import Repository
|
|
|
31
31
|
from .context.session import Session, get_session, set_session
|
|
32
32
|
from .context.state_validator import StateValidator, InvalidStateError
|
|
33
33
|
|
|
34
|
+
# Model Query
|
|
35
|
+
from .model_query import ModelQueryLambda
|
|
36
|
+
|
|
34
37
|
# Invoker
|
|
35
38
|
from .invoker import (
|
|
36
39
|
LambdaInvoker,
|
|
@@ -65,6 +68,7 @@ __all__ = [
|
|
|
65
68
|
'set_session',
|
|
66
69
|
'StateValidator',
|
|
67
70
|
'InvalidStateError',
|
|
71
|
+
'ModelQueryLambda',
|
|
68
72
|
'LambdaInvoker',
|
|
69
73
|
'ApiClient',
|
|
70
74
|
'LambdaInvocationError',
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ModelQueryLambda - Generic MongoDB query proxy Lambda for inter-service communication.
|
|
3
|
+
|
|
4
|
+
Allows other microservices to query any whitelisted collection in this service's
|
|
5
|
+
MongoDB database by invoking this Lambda directly, passing standard MongoDB
|
|
6
|
+
query parameters (filter, pipeline, fields, limit, skip).
|
|
7
|
+
|
|
8
|
+
Usage — subclass and declare which collections are exposed:
|
|
9
|
+
|
|
10
|
+
from aws_python_helper import ModelQueryLambda
|
|
11
|
+
|
|
12
|
+
class DocketsModelQueryLambda(ModelQueryLambda):
|
|
13
|
+
@property
|
|
14
|
+
def allowed_collections(self) -> list:
|
|
15
|
+
return ["dockets", "tax_sales"]
|
|
16
|
+
|
|
17
|
+
Caller payload:
|
|
18
|
+
{
|
|
19
|
+
"session": {"state": "connecticut"},
|
|
20
|
+
"collection": "dockets",
|
|
21
|
+
"filter": {"case_type_code": "M10"}, # simple find — option A
|
|
22
|
+
"pipeline": [...], # aggregate — option B (takes precedence)
|
|
23
|
+
"fields": {"docket_id": 1, "_id": 0}, # projection (only with filter)
|
|
24
|
+
"limit": 100, # optional
|
|
25
|
+
"skip": 0 # optional, default 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ObjectId coercion:
|
|
29
|
+
String values for '_id' (or 'id' as alias) inside `filter` and pipeline
|
|
30
|
+
`$match` stages are automatically converted to bson.ObjectId. Supports
|
|
31
|
+
scalar operators ($eq, $ne, $gt, $gte, $lt, $lte) and list operators
|
|
32
|
+
($in, $nin). A clear ValueError is raised when the string is not a valid
|
|
33
|
+
24-char ObjectId hex.
|
|
34
|
+
|
|
35
|
+
{"filter": {"_id": "65f1a2b3c4d5e6f7a8b9c0d1"}}
|
|
36
|
+
{"filter": {"_id": {"$in": ["65f1...", "65f2..."]}}}
|
|
37
|
+
{"filter": {"id": "65f1a2b3c4d5e6f7a8b9c0d1"}} # 'id' is aliased to '_id'
|
|
38
|
+
|
|
39
|
+
Response (via Lambda base run()):
|
|
40
|
+
{"success": True, "data": [...]}
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from typing import Any, List, Optional
|
|
44
|
+
from bson import ObjectId
|
|
45
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
46
|
+
|
|
47
|
+
from ..lambda_standalone.base import Lambda
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_OBJECT_ID_SCALAR_OPERATORS = {"$eq", "$ne", "$gt", "$gte", "$lt", "$lte"}
|
|
51
|
+
_OBJECT_ID_LIST_OPERATORS = {"$in", "$nin"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _coerce_object_id(value: Any) -> Any:
|
|
55
|
+
"""Convert a string (or operator dict) to ObjectId where applicable for an '_id' field."""
|
|
56
|
+
if isinstance(value, ObjectId):
|
|
57
|
+
return value
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
if not ObjectId.is_valid(value):
|
|
60
|
+
raise ValueError(f"'_id' value '{value}' is not a valid ObjectId")
|
|
61
|
+
return ObjectId(value)
|
|
62
|
+
if isinstance(value, dict):
|
|
63
|
+
coerced = {}
|
|
64
|
+
for op, op_value in value.items():
|
|
65
|
+
if op in _OBJECT_ID_LIST_OPERATORS:
|
|
66
|
+
if not isinstance(op_value, list):
|
|
67
|
+
raise ValueError(f"'{op}' value for '_id' must be a list")
|
|
68
|
+
coerced[op] = [_coerce_object_id(v) for v in op_value]
|
|
69
|
+
elif op in _OBJECT_ID_SCALAR_OPERATORS:
|
|
70
|
+
coerced[op] = _coerce_object_id(op_value)
|
|
71
|
+
else:
|
|
72
|
+
coerced[op] = op_value
|
|
73
|
+
return coerced
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Unsupported type for '_id' filter: {type(value).__name__}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _normalize_id_key(d: dict) -> None:
|
|
80
|
+
"""Rename 'id' → '_id' (if no _id present) and coerce its value to ObjectId in-place."""
|
|
81
|
+
if "id" in d and "_id" not in d:
|
|
82
|
+
d["_id"] = d.pop("id")
|
|
83
|
+
if "_id" in d:
|
|
84
|
+
d["_id"] = _coerce_object_id(d["_id"])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _ModelQuerySchema(BaseModel):
|
|
88
|
+
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True)
|
|
89
|
+
|
|
90
|
+
collection: str
|
|
91
|
+
filter: Optional[dict] = None
|
|
92
|
+
pipeline: Optional[list] = None
|
|
93
|
+
fields: Optional[dict] = None
|
|
94
|
+
limit: Optional[int] = None
|
|
95
|
+
skip: int = 0
|
|
96
|
+
|
|
97
|
+
@model_validator(mode="after")
|
|
98
|
+
def _pipeline_fields_exclusive(self):
|
|
99
|
+
if self.pipeline is not None and self.fields is not None:
|
|
100
|
+
raise ValueError("'fields' cannot be used together with 'pipeline'")
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
@model_validator(mode="after")
|
|
104
|
+
def _normalize_object_ids(self):
|
|
105
|
+
if self.filter is not None:
|
|
106
|
+
_normalize_id_key(self.filter)
|
|
107
|
+
if self.pipeline is not None:
|
|
108
|
+
for stage in self.pipeline:
|
|
109
|
+
if isinstance(stage, dict) and isinstance(stage.get("$match"), dict):
|
|
110
|
+
_normalize_id_key(stage["$match"])
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ModelQueryLambda(Lambda):
|
|
115
|
+
"""
|
|
116
|
+
Base class for cross-service MongoDB query Lambdas.
|
|
117
|
+
|
|
118
|
+
Subclass and override `allowed_collections` to declare which collections
|
|
119
|
+
this Lambda exposes. Everything else is handled automatically.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def allowed_collections(self) -> List[str]:
|
|
124
|
+
"""
|
|
125
|
+
Whitelist of collection names this Lambda is allowed to query.
|
|
126
|
+
|
|
127
|
+
Must be overridden — an empty list rejects all requests.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
@property
|
|
131
|
+
def allowed_collections(self):
|
|
132
|
+
return ["dockets", "tax_sales"]
|
|
133
|
+
"""
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def schema(self):
|
|
138
|
+
return _ModelQuerySchema
|
|
139
|
+
|
|
140
|
+
async def validate(self):
|
|
141
|
+
collection = self.data.get("collection", "")
|
|
142
|
+
|
|
143
|
+
if not self.allowed_collections:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"allowed_collections is not configured on this ModelQueryLambda"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if collection not in self.allowed_collections:
|
|
149
|
+
allowed = ", ".join(self.allowed_collections)
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Collection '{collection}' is not allowed. Allowed: {allowed}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def process(self) -> Any:
|
|
155
|
+
collection_name: str = self.data["collection"]
|
|
156
|
+
database: str = self.data.get("database") or self.session.state
|
|
157
|
+
|
|
158
|
+
collection = getattr(getattr(self.db, database), collection_name)
|
|
159
|
+
|
|
160
|
+
pipeline: Optional[list] = self.data.get("pipeline")
|
|
161
|
+
|
|
162
|
+
if pipeline is not None:
|
|
163
|
+
results = await collection.aggregate(pipeline).to_list(length=None)
|
|
164
|
+
else:
|
|
165
|
+
mongo_filter: dict = self.data.get("filter") or {}
|
|
166
|
+
fields: Optional[dict] = self.data.get("fields")
|
|
167
|
+
limit: int = self.data.get("limit") or 0
|
|
168
|
+
skip: int = self.data.get("skip") or 0
|
|
169
|
+
|
|
170
|
+
cursor = collection.find(mongo_filter, fields or {})
|
|
171
|
+
|
|
172
|
+
if skip:
|
|
173
|
+
cursor = cursor.skip(skip)
|
|
174
|
+
if limit:
|
|
175
|
+
cursor = cursor.limit(limit)
|
|
176
|
+
|
|
177
|
+
results = await cursor.to_list(length=None)
|
|
178
|
+
|
|
179
|
+
return results
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-python-helper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.38.0
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -38,6 +38,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
38
38
|
- **Pydantic schema validation**: Declare a `schema` property on any `API` or `Lambda` and the framework validates automatically before `validate()` — returns 400 on failure
|
|
39
39
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
40
40
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
41
|
+
- **ModelQueryLambda**: Base class for MongoDB query-proxy Lambdas — declare an `allowed_collections` whitelist and the framework handles `find` and `aggregate` operations generically
|
|
41
42
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
42
43
|
- **Type hints**: Modern Python with type annotations
|
|
43
44
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -65,6 +66,7 @@ All available classes and functions:
|
|
|
65
66
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
66
67
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
67
68
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
69
|
+
| `ModelQueryLambda` | `aws_python_helper` | Generic MongoDB query-proxy Lambda base class |
|
|
68
70
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
69
71
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
70
72
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1164,6 +1166,173 @@ module "my_lambda" {
|
|
|
1164
1166
|
|
|
1165
1167
|
---
|
|
1166
1168
|
|
|
1169
|
+
## 🔍 ModelQueryLambda
|
|
1170
|
+
|
|
1171
|
+
`ModelQueryLambda` is a ready-to-use base class for creating MongoDB query-proxy Lambdas. Instead of writing a custom Lambda every time another microservice needs to read data from your MongoDB, you subclass `ModelQueryLambda`, declare an `allowed_collections` whitelist, and the framework exposes both `find` and `aggregate` operations automatically.
|
|
1172
|
+
|
|
1173
|
+
**Use case:** microservice A needs to query data owned by microservice B. Microservice B deploys one `model-query` Lambda; microservice A calls it via `LambdaInvoker` passing the collection name and query parameters.
|
|
1174
|
+
|
|
1175
|
+
### Creating a model-query Lambda
|
|
1176
|
+
|
|
1177
|
+
Create `src/lambda/model-query/main.py` in the microservice that owns the data:
|
|
1178
|
+
|
|
1179
|
+
```python
|
|
1180
|
+
from aws_python_helper import ModelQueryLambda
|
|
1181
|
+
|
|
1182
|
+
class ModelQueryLambda(ModelQueryLambda):
|
|
1183
|
+
|
|
1184
|
+
@property
|
|
1185
|
+
def allowed_collections(self) -> list:
|
|
1186
|
+
return ["orders", "customers"] # whitelist — unlisted collections are rejected
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
Register the handler in `src/handlers/lambda_handler.py`:
|
|
1190
|
+
|
|
1191
|
+
```python
|
|
1192
|
+
from aws_python_helper.lambda_standalone.handler import lambda_handler
|
|
1193
|
+
|
|
1194
|
+
model_query_handler = lambda_handler('model-query')
|
|
1195
|
+
|
|
1196
|
+
__all__ = ["model_query_handler"]
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
### Query modes
|
|
1200
|
+
|
|
1201
|
+
#### Aggregate pipeline
|
|
1202
|
+
|
|
1203
|
+
Pass a `pipeline` list to run a MongoDB aggregation:
|
|
1204
|
+
|
|
1205
|
+
```python
|
|
1206
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1207
|
+
"session": self.session.to_dict(),
|
|
1208
|
+
"collection": "orders",
|
|
1209
|
+
"pipeline": [
|
|
1210
|
+
{"$match": {"status": "pending"}},
|
|
1211
|
+
{"$sort": {"created_at": -1}},
|
|
1212
|
+
{"$limit": 50},
|
|
1213
|
+
],
|
|
1214
|
+
})
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
#### Find with filter
|
|
1218
|
+
|
|
1219
|
+
Pass `filter`, `fields`, `limit`, and/or `skip` to run a `find`:
|
|
1220
|
+
|
|
1221
|
+
```python
|
|
1222
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1223
|
+
"session": self.session.to_dict(),
|
|
1224
|
+
"collection": "orders",
|
|
1225
|
+
"filter": {"customer_id": "abc123", "status": "active"},
|
|
1226
|
+
"fields": {"_id": 1, "total": 1, "status": 1}, # projection
|
|
1227
|
+
"limit": 20,
|
|
1228
|
+
"skip": 0,
|
|
1229
|
+
})
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
**Note:** `pipeline` and `fields` are mutually exclusive — the framework rejects payloads that include both.
|
|
1233
|
+
|
|
1234
|
+
#### Querying by `_id`
|
|
1235
|
+
|
|
1236
|
+
ObjectId hex strings in `_id` filters are automatically coerced to `ObjectId` so callers can serialize queries as plain JSON:
|
|
1237
|
+
|
|
1238
|
+
```python
|
|
1239
|
+
# Plain value
|
|
1240
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1241
|
+
"session": self.session.to_dict(),
|
|
1242
|
+
"collection": "orders",
|
|
1243
|
+
"filter": {"_id": "65f1a2b3c4d5e6f7a8b9c0d1"},
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
# Operator with list
|
|
1247
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1248
|
+
"session": self.session.to_dict(),
|
|
1249
|
+
"collection": "orders",
|
|
1250
|
+
"filter": {"_id": {"$in": ["65f1...", "65f2...", "65f3..."]}},
|
|
1251
|
+
})
|
|
1252
|
+
|
|
1253
|
+
# 'id' is accepted as an alias for '_id'
|
|
1254
|
+
invoker.invoke("ServiceModelQuery-dev", payload={
|
|
1255
|
+
"session": self.session.to_dict(),
|
|
1256
|
+
"collection": "orders",
|
|
1257
|
+
"filter": {"id": "65f1a2b3c4d5e6f7a8b9c0d1"},
|
|
1258
|
+
})
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
Supported operators on `_id`: scalar (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) and list (`$in`, `$nin`). The same coercion is applied to `$match` stages inside a `pipeline`. If the string is not a valid 24-char ObjectId hex, the Lambda raises a clear validation error.
|
|
1262
|
+
|
|
1263
|
+
### Payload reference
|
|
1264
|
+
|
|
1265
|
+
| Field | Type | Required | Description |
|
|
1266
|
+
|-------|------|----------|-------------|
|
|
1267
|
+
| `session` | `dict` | Yes | Session dict from `self.session.to_dict()` — drives state-scoped DB routing |
|
|
1268
|
+
| `collection` | `str` | Yes | Collection name (must be in `allowed_collections`) |
|
|
1269
|
+
| `pipeline` | `list` | One of | MongoDB aggregation pipeline |
|
|
1270
|
+
| `filter` | `dict` | One of | MongoDB filter document (defaults to `{}`). `_id`/`id` string values are auto-coerced to `ObjectId` |
|
|
1271
|
+
| `fields` | `dict` | No | MongoDB projection (only valid with `filter` mode) |
|
|
1272
|
+
| `limit` | `int` | No | Max documents to return |
|
|
1273
|
+
| `skip` | `int` | No | Documents to skip (default `0`) |
|
|
1274
|
+
|
|
1275
|
+
### Response format
|
|
1276
|
+
|
|
1277
|
+
The Lambda always returns the standard framework envelope:
|
|
1278
|
+
|
|
1279
|
+
```json
|
|
1280
|
+
{
|
|
1281
|
+
"success": true,
|
|
1282
|
+
"data": [ ... ]
|
|
1283
|
+
}
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
On validation error (unknown collection, invalid payload):
|
|
1287
|
+
|
|
1288
|
+
```json
|
|
1289
|
+
{
|
|
1290
|
+
"success": false,
|
|
1291
|
+
"error": "Collection 'unknown' is not allowed. Allowed: orders, customers"
|
|
1292
|
+
}
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
### Calling it from another microservice
|
|
1296
|
+
|
|
1297
|
+
```python
|
|
1298
|
+
import os
|
|
1299
|
+
from aws_python_helper import LambdaInvoker, LambdaInvocationError
|
|
1300
|
+
|
|
1301
|
+
class OrdersPostAPI(API):
|
|
1302
|
+
|
|
1303
|
+
async def process(self):
|
|
1304
|
+
try:
|
|
1305
|
+
response = LambdaInvoker().invoke(
|
|
1306
|
+
os.getenv("ORDERS_MODEL_QUERY_LAMBDA_NAME"),
|
|
1307
|
+
payload={
|
|
1308
|
+
"session": self.session.to_dict(),
|
|
1309
|
+
"collection": "orders",
|
|
1310
|
+
"filter": {"status": "pending"},
|
|
1311
|
+
"limit": 100,
|
|
1312
|
+
},
|
|
1313
|
+
)
|
|
1314
|
+
except LambdaInvocationError as e:
|
|
1315
|
+
self.logger.error(f"model-query invocation failed: {e}")
|
|
1316
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1317
|
+
|
|
1318
|
+
if not response.get("success"):
|
|
1319
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1320
|
+
|
|
1321
|
+
orders = response.get("data", [])
|
|
1322
|
+
self.set_body({"orders": orders})
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Session and database routing
|
|
1326
|
+
|
|
1327
|
+
The `session` field in the payload is required. The `ModelQueryLambda` uses `session.state` to resolve the target database — the same state-scoped routing used by all framework components. This means:
|
|
1328
|
+
|
|
1329
|
+
- `{"session": {"state": "connecticut"}, ...}` → queries the `connecticut` database
|
|
1330
|
+
- `{"session": {"state": "new_jersey"}, ...}` → queries the `new_jersey` database
|
|
1331
|
+
|
|
1332
|
+
Always pass `self.session.to_dict()` when calling from an API or Lambda to ensure state propagates correctly.
|
|
1333
|
+
|
|
1334
|
+
---
|
|
1335
|
+
|
|
1167
1336
|
## 🏗️ Architecture Overview
|
|
1168
1337
|
|
|
1169
1338
|
Typical flow for event-driven architectures using this framework:
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper.egg-info/SOURCES.txt
RENAMED
|
@@ -35,6 +35,8 @@ aws_python_helper/lambda_standalone/__init__.py
|
|
|
35
35
|
aws_python_helper/lambda_standalone/base.py
|
|
36
36
|
aws_python_helper/lambda_standalone/fetcher.py
|
|
37
37
|
aws_python_helper/lambda_standalone/handler.py
|
|
38
|
+
aws_python_helper/model_query/__init__.py
|
|
39
|
+
aws_python_helper/model_query/base.py
|
|
38
40
|
aws_python_helper/repository/__init__.py
|
|
39
41
|
aws_python_helper/repository/base.py
|
|
40
42
|
aws_python_helper/sns/__init__.py
|
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/auth_middleware.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/api/auth_validators.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
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/context/state_validator.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/database/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/database/database_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/database/mongo_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/fargate/task_base.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/invoker/api_client.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/invoker/exceptions.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/invoker/lambda_invoker.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/lambda_standalone/base.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/lambda_standalone/fetcher.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/lambda_standalone/handler.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/repository/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/sqs/consumer_base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper/utils/json_encoder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper.egg-info/requires.txt
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.38.0}/aws_python_helper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|