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.
Files changed (53) hide show
  1. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/PKG-INFO +141 -1
  2. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/README.md +140 -0
  3. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/__init__.py +4 -0
  4. aws_python_helper-0.37.0/aws_python_helper/model_query/__init__.py +3 -0
  5. aws_python_helper-0.37.0/aws_python_helper/model_query/base.py +120 -0
  6. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/PKG-INFO +141 -1
  7. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/SOURCES.txt +2 -0
  8. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/pyproject.toml +1 -1
  9. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/__init__.py +0 -0
  10. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/auth_middleware.py +0 -0
  11. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/auth_validators.py +0 -0
  12. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/base.py +0 -0
  13. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/dispatcher.py +0 -0
  14. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/exceptions.py +0 -0
  15. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/fetcher.py +0 -0
  16. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/api/handler.py +0 -0
  17. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/context/__init__.py +0 -0
  18. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/context/session.py +0 -0
  19. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/context/state_validator.py +0 -0
  20. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/__init__.py +0 -0
  21. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/database_proxy.py +0 -0
  22. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/external_database_proxy.py +0 -0
  23. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
  24. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/database/mongo_manager.py +0 -0
  25. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/__init__.py +0 -0
  26. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/executor.py +0 -0
  27. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/fetcher.py +0 -0
  28. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/handler.py +0 -0
  29. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/fargate/task_base.py +0 -0
  30. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/__init__.py +0 -0
  31. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/api_client.py +0 -0
  32. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/exceptions.py +0 -0
  33. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/invoker/lambda_invoker.py +0 -0
  34. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
  35. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/base.py +0 -0
  36. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
  37. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
  38. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/repository/__init__.py +0 -0
  39. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/repository/base.py +0 -0
  40. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sns/__init__.py +0 -0
  41. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sns/publisher.py +0 -0
  42. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sqs/__init__.py +0 -0
  43. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sqs/consumer_base.py +0 -0
  44. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sqs/fetcher.py +0 -0
  45. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/sqs/handler.py +0 -0
  46. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/utils/__init__.py +0 -0
  47. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/utils/json_encoder.py +0 -0
  48. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/utils/response.py +0 -0
  49. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper/utils/serializer.py +0 -0
  50. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
  51. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/requires.txt +0 -0
  52. {aws_python_helper-0.36.0 → aws_python_helper-0.37.0}/aws_python_helper.egg-info/top_level.txt +0 -0
  53. {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.36.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,3 @@
1
+ from .base import ModelQueryLambda
2
+
3
+ __all__ = ["ModelQueryLambda"]
@@ -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.36.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:
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aws-python-helper"
7
- version = "0.36.0"
7
+ version = "0.37.0"
8
8
  description = "AWS Python Helper Framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"