aws-python-helper 0.36.0__tar.gz → 0.37.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.37.0}/PKG-INFO +141 -1
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/README.md +140 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/__init__.py +4 -0
- aws_python_helper-0.37.0/aws_python_helper/model_query/__init__.py +3 -0
- aws_python_helper-0.37.0/aws_python_helper/model_query/base.py +120 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/PKG-INFO +141 -1
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/SOURCES.txt +2 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/pyproject.toml +1 -1
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/dispatcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/context/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/context/session.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/context/state_validator.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/executor.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/task_base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/api_client.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/exceptions.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/lambda_invoker.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/repository/base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sns/publisher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sqs/consumer_base.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-0.36.0 → aws_python_helper-0.37.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.37.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,144 @@ 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
|
+
### Payload reference
|
|
1235
|
+
|
|
1236
|
+
| Field | Type | Required | Description |
|
|
1237
|
+
|-------|------|----------|-------------|
|
|
1238
|
+
| `session` | `dict` | Yes | Session dict from `self.session.to_dict()` — drives state-scoped DB routing |
|
|
1239
|
+
| `collection` | `str` | Yes | Collection name (must be in `allowed_collections`) |
|
|
1240
|
+
| `pipeline` | `list` | One of | MongoDB aggregation pipeline |
|
|
1241
|
+
| `filter` | `dict` | One of | MongoDB filter document (defaults to `{}`) |
|
|
1242
|
+
| `fields` | `dict` | No | MongoDB projection (only valid with `filter` mode) |
|
|
1243
|
+
| `limit` | `int` | No | Max documents to return |
|
|
1244
|
+
| `skip` | `int` | No | Documents to skip (default `0`) |
|
|
1245
|
+
|
|
1246
|
+
### Response format
|
|
1247
|
+
|
|
1248
|
+
The Lambda always returns the standard framework envelope:
|
|
1249
|
+
|
|
1250
|
+
```json
|
|
1251
|
+
{
|
|
1252
|
+
"success": true,
|
|
1253
|
+
"data": [ ... ]
|
|
1254
|
+
}
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
On validation error (unknown collection, invalid payload):
|
|
1258
|
+
|
|
1259
|
+
```json
|
|
1260
|
+
{
|
|
1261
|
+
"success": false,
|
|
1262
|
+
"error": "Collection 'unknown' is not allowed. Allowed: orders, customers"
|
|
1263
|
+
}
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
### Calling it from another microservice
|
|
1267
|
+
|
|
1268
|
+
```python
|
|
1269
|
+
import os
|
|
1270
|
+
from aws_python_helper import LambdaInvoker, LambdaInvocationError
|
|
1271
|
+
|
|
1272
|
+
class OrdersPostAPI(API):
|
|
1273
|
+
|
|
1274
|
+
async def process(self):
|
|
1275
|
+
try:
|
|
1276
|
+
response = LambdaInvoker().invoke(
|
|
1277
|
+
os.getenv("ORDERS_MODEL_QUERY_LAMBDA_NAME"),
|
|
1278
|
+
payload={
|
|
1279
|
+
"session": self.session.to_dict(),
|
|
1280
|
+
"collection": "orders",
|
|
1281
|
+
"filter": {"status": "pending"},
|
|
1282
|
+
"limit": 100,
|
|
1283
|
+
},
|
|
1284
|
+
)
|
|
1285
|
+
except LambdaInvocationError as e:
|
|
1286
|
+
self.logger.error(f"model-query invocation failed: {e}")
|
|
1287
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1288
|
+
|
|
1289
|
+
if not response.get("success"):
|
|
1290
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1291
|
+
|
|
1292
|
+
orders = response.get("data", [])
|
|
1293
|
+
self.set_body({"orders": orders})
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
### Session and database routing
|
|
1297
|
+
|
|
1298
|
+
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:
|
|
1299
|
+
|
|
1300
|
+
- `{"session": {"state": "connecticut"}, ...}` → queries the `connecticut` database
|
|
1301
|
+
- `{"session": {"state": "new_jersey"}, ...}` → queries the `new_jersey` database
|
|
1302
|
+
|
|
1303
|
+
Always pass `self.session.to_dict()` when calling from an API or Lambda to ensure state propagates correctly.
|
|
1304
|
+
|
|
1305
|
+
---
|
|
1306
|
+
|
|
1167
1307
|
## 🏗️ Architecture Overview
|
|
1168
1308
|
|
|
1169
1309
|
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,144 @@ 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
|
+
### Payload reference
|
|
1215
|
+
|
|
1216
|
+
| Field | Type | Required | Description |
|
|
1217
|
+
|-------|------|----------|-------------|
|
|
1218
|
+
| `session` | `dict` | Yes | Session dict from `self.session.to_dict()` — drives state-scoped DB routing |
|
|
1219
|
+
| `collection` | `str` | Yes | Collection name (must be in `allowed_collections`) |
|
|
1220
|
+
| `pipeline` | `list` | One of | MongoDB aggregation pipeline |
|
|
1221
|
+
| `filter` | `dict` | One of | MongoDB filter document (defaults to `{}`) |
|
|
1222
|
+
| `fields` | `dict` | No | MongoDB projection (only valid with `filter` mode) |
|
|
1223
|
+
| `limit` | `int` | No | Max documents to return |
|
|
1224
|
+
| `skip` | `int` | No | Documents to skip (default `0`) |
|
|
1225
|
+
|
|
1226
|
+
### Response format
|
|
1227
|
+
|
|
1228
|
+
The Lambda always returns the standard framework envelope:
|
|
1229
|
+
|
|
1230
|
+
```json
|
|
1231
|
+
{
|
|
1232
|
+
"success": true,
|
|
1233
|
+
"data": [ ... ]
|
|
1234
|
+
}
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
On validation error (unknown collection, invalid payload):
|
|
1238
|
+
|
|
1239
|
+
```json
|
|
1240
|
+
{
|
|
1241
|
+
"success": false,
|
|
1242
|
+
"error": "Collection 'unknown' is not allowed. Allowed: orders, customers"
|
|
1243
|
+
}
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
### Calling it from another microservice
|
|
1247
|
+
|
|
1248
|
+
```python
|
|
1249
|
+
import os
|
|
1250
|
+
from aws_python_helper import LambdaInvoker, LambdaInvocationError
|
|
1251
|
+
|
|
1252
|
+
class OrdersPostAPI(API):
|
|
1253
|
+
|
|
1254
|
+
async def process(self):
|
|
1255
|
+
try:
|
|
1256
|
+
response = LambdaInvoker().invoke(
|
|
1257
|
+
os.getenv("ORDERS_MODEL_QUERY_LAMBDA_NAME"),
|
|
1258
|
+
payload={
|
|
1259
|
+
"session": self.session.to_dict(),
|
|
1260
|
+
"collection": "orders",
|
|
1261
|
+
"filter": {"status": "pending"},
|
|
1262
|
+
"limit": 100,
|
|
1263
|
+
},
|
|
1264
|
+
)
|
|
1265
|
+
except LambdaInvocationError as e:
|
|
1266
|
+
self.logger.error(f"model-query invocation failed: {e}")
|
|
1267
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1268
|
+
|
|
1269
|
+
if not response.get("success"):
|
|
1270
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1271
|
+
|
|
1272
|
+
orders = response.get("data", [])
|
|
1273
|
+
self.set_body({"orders": orders})
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
### Session and database routing
|
|
1277
|
+
|
|
1278
|
+
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:
|
|
1279
|
+
|
|
1280
|
+
- `{"session": {"state": "connecticut"}, ...}` → queries the `connecticut` database
|
|
1281
|
+
- `{"session": {"state": "new_jersey"}, ...}` → queries the `new_jersey` database
|
|
1282
|
+
|
|
1283
|
+
Always pass `self.session.to_dict()` when calling from an API or Lambda to ensure state propagates correctly.
|
|
1284
|
+
|
|
1285
|
+
---
|
|
1286
|
+
|
|
1147
1287
|
## 🏗️ Architecture Overview
|
|
1148
1288
|
|
|
1149
1289
|
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,120 @@
|
|
|
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
|
+
Response (via Lambda base run()):
|
|
29
|
+
{"success": True, "data": [...]}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from typing import Any, List, Optional
|
|
33
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
34
|
+
|
|
35
|
+
from ..lambda_standalone.base import Lambda
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _ModelQuerySchema(BaseModel):
|
|
39
|
+
model_config = ConfigDict(extra="ignore")
|
|
40
|
+
|
|
41
|
+
collection: str
|
|
42
|
+
filter: Optional[dict] = None
|
|
43
|
+
pipeline: Optional[list] = None
|
|
44
|
+
fields: Optional[dict] = None
|
|
45
|
+
limit: Optional[int] = None
|
|
46
|
+
skip: int = 0
|
|
47
|
+
|
|
48
|
+
@model_validator(mode="after")
|
|
49
|
+
def _pipeline_fields_exclusive(self):
|
|
50
|
+
if self.pipeline is not None and self.fields is not None:
|
|
51
|
+
raise ValueError("'fields' cannot be used together with 'pipeline'")
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ModelQueryLambda(Lambda):
|
|
56
|
+
"""
|
|
57
|
+
Base class for cross-service MongoDB query Lambdas.
|
|
58
|
+
|
|
59
|
+
Subclass and override `allowed_collections` to declare which collections
|
|
60
|
+
this Lambda exposes. Everything else is handled automatically.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def allowed_collections(self) -> List[str]:
|
|
65
|
+
"""
|
|
66
|
+
Whitelist of collection names this Lambda is allowed to query.
|
|
67
|
+
|
|
68
|
+
Must be overridden — an empty list rejects all requests.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
@property
|
|
72
|
+
def allowed_collections(self):
|
|
73
|
+
return ["dockets", "tax_sales"]
|
|
74
|
+
"""
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def schema(self):
|
|
79
|
+
return _ModelQuerySchema
|
|
80
|
+
|
|
81
|
+
async def validate(self):
|
|
82
|
+
collection = self.data.get("collection", "")
|
|
83
|
+
|
|
84
|
+
if not self.allowed_collections:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"allowed_collections is not configured on this ModelQueryLambda"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if collection not in self.allowed_collections:
|
|
90
|
+
allowed = ", ".join(self.allowed_collections)
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Collection '{collection}' is not allowed. Allowed: {allowed}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def process(self) -> Any:
|
|
96
|
+
collection_name: str = self.data["collection"]
|
|
97
|
+
database: str = self.data.get("database") or self.session.state
|
|
98
|
+
|
|
99
|
+
collection = getattr(getattr(self.db, database), collection_name)
|
|
100
|
+
|
|
101
|
+
pipeline: Optional[list] = self.data.get("pipeline")
|
|
102
|
+
|
|
103
|
+
if pipeline is not None:
|
|
104
|
+
results = await collection.aggregate(pipeline).to_list(length=None)
|
|
105
|
+
else:
|
|
106
|
+
mongo_filter: dict = self.data.get("filter") or {}
|
|
107
|
+
fields: Optional[dict] = self.data.get("fields")
|
|
108
|
+
limit: int = self.data.get("limit") or 0
|
|
109
|
+
skip: int = self.data.get("skip") or 0
|
|
110
|
+
|
|
111
|
+
cursor = collection.find(mongo_filter, fields or {})
|
|
112
|
+
|
|
113
|
+
if skip:
|
|
114
|
+
cursor = cursor.skip(skip)
|
|
115
|
+
if limit:
|
|
116
|
+
cursor = cursor.limit(limit)
|
|
117
|
+
|
|
118
|
+
results = await cursor.to_list(length=None)
|
|
119
|
+
|
|
120
|
+
return results
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-python-helper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.37.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,144 @@ 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
|
+
### Payload reference
|
|
1235
|
+
|
|
1236
|
+
| Field | Type | Required | Description |
|
|
1237
|
+
|-------|------|----------|-------------|
|
|
1238
|
+
| `session` | `dict` | Yes | Session dict from `self.session.to_dict()` — drives state-scoped DB routing |
|
|
1239
|
+
| `collection` | `str` | Yes | Collection name (must be in `allowed_collections`) |
|
|
1240
|
+
| `pipeline` | `list` | One of | MongoDB aggregation pipeline |
|
|
1241
|
+
| `filter` | `dict` | One of | MongoDB filter document (defaults to `{}`) |
|
|
1242
|
+
| `fields` | `dict` | No | MongoDB projection (only valid with `filter` mode) |
|
|
1243
|
+
| `limit` | `int` | No | Max documents to return |
|
|
1244
|
+
| `skip` | `int` | No | Documents to skip (default `0`) |
|
|
1245
|
+
|
|
1246
|
+
### Response format
|
|
1247
|
+
|
|
1248
|
+
The Lambda always returns the standard framework envelope:
|
|
1249
|
+
|
|
1250
|
+
```json
|
|
1251
|
+
{
|
|
1252
|
+
"success": true,
|
|
1253
|
+
"data": [ ... ]
|
|
1254
|
+
}
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
On validation error (unknown collection, invalid payload):
|
|
1258
|
+
|
|
1259
|
+
```json
|
|
1260
|
+
{
|
|
1261
|
+
"success": false,
|
|
1262
|
+
"error": "Collection 'unknown' is not allowed. Allowed: orders, customers"
|
|
1263
|
+
}
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
### Calling it from another microservice
|
|
1267
|
+
|
|
1268
|
+
```python
|
|
1269
|
+
import os
|
|
1270
|
+
from aws_python_helper import LambdaInvoker, LambdaInvocationError
|
|
1271
|
+
|
|
1272
|
+
class OrdersPostAPI(API):
|
|
1273
|
+
|
|
1274
|
+
async def process(self):
|
|
1275
|
+
try:
|
|
1276
|
+
response = LambdaInvoker().invoke(
|
|
1277
|
+
os.getenv("ORDERS_MODEL_QUERY_LAMBDA_NAME"),
|
|
1278
|
+
payload={
|
|
1279
|
+
"session": self.session.to_dict(),
|
|
1280
|
+
"collection": "orders",
|
|
1281
|
+
"filter": {"status": "pending"},
|
|
1282
|
+
"limit": 100,
|
|
1283
|
+
},
|
|
1284
|
+
)
|
|
1285
|
+
except LambdaInvocationError as e:
|
|
1286
|
+
self.logger.error(f"model-query invocation failed: {e}")
|
|
1287
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1288
|
+
|
|
1289
|
+
if not response.get("success"):
|
|
1290
|
+
raise ValueError("Could not retrieve orders. Please try again later.")
|
|
1291
|
+
|
|
1292
|
+
orders = response.get("data", [])
|
|
1293
|
+
self.set_body({"orders": orders})
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
### Session and database routing
|
|
1297
|
+
|
|
1298
|
+
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:
|
|
1299
|
+
|
|
1300
|
+
- `{"session": {"state": "connecticut"}, ...}` → queries the `connecticut` database
|
|
1301
|
+
- `{"session": {"state": "new_jersey"}, ...}` → queries the `new_jersey` database
|
|
1302
|
+
|
|
1303
|
+
Always pass `self.session.to_dict()` when calling from an API or Lambda to ensure state propagates correctly.
|
|
1304
|
+
|
|
1305
|
+
---
|
|
1306
|
+
|
|
1167
1307
|
## 🏗️ Architecture Overview
|
|
1168
1308
|
|
|
1169
1309
|
Typical flow for event-driven architectures using this framework:
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.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.37.0}/aws_python_helper/api/auth_middleware.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.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.37.0}/aws_python_helper/context/state_validator.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.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.37.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.37.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.37.0}/aws_python_helper/invoker/api_client.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/exceptions.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.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.37.0}/aws_python_helper/lambda_standalone/base.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/fetcher.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/handler.py
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.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.37.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.37.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.37.0}/aws_python_helper.egg-info/requires.txt
RENAMED
|
File without changes
|
{aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|