aws-python-helper 0.35.0__tar.gz → 0.36.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.35.0 → aws_python_helper-0.36.0}/PKG-INFO +177 -19
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/README.md +174 -18
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/__init__.py +18 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/base.py +17 -1
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/dispatcher.py +11 -3
- aws_python_helper-0.36.0/aws_python_helper/invoker/__init__.py +19 -0
- aws_python_helper-0.36.0/aws_python_helper/invoker/api_client.py +153 -0
- aws_python_helper-0.36.0/aws_python_helper/invoker/exceptions.py +23 -0
- aws_python_helper-0.36.0/aws_python_helper/invoker/lambda_invoker.py +96 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/lambda_standalone/base.py +36 -6
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/PKG-INFO +177 -19
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/SOURCES.txt +4 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/requires.txt +2 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/pyproject.toml +3 -1
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/context/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/context/session.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/context/state_validator.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/executor.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/task_base.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/repository/base.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sns/publisher.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sqs/consumer_base.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-0.35.0 → aws_python_helper-0.36.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.36.0
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,6 +15,8 @@ Requires-Python: >=3.9
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
Requires-Dist: motor==3.3.2
|
|
17
17
|
Requires-Dist: pymongo==4.6.1
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
18
20
|
|
|
19
21
|
# AWS Python Framework
|
|
20
22
|
|
|
@@ -33,6 +35,9 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
33
35
|
- **Fargate Tasks**: Same pattern to run tasks in Fargate containers
|
|
34
36
|
- **Standalone Lambdas**: Create lambdas invocable directly with AWS SDK
|
|
35
37
|
- **Authentication middleware**: Built-in token-based authentication
|
|
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
|
+
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
40
|
+
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
36
41
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
37
42
|
- **Type hints**: Modern Python with type annotations
|
|
38
43
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -60,6 +65,13 @@ All available classes and functions:
|
|
|
60
65
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
61
66
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
62
67
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
68
|
+
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
69
|
+
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
70
|
+
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
71
|
+
| `LambdaResponseError` | `aws_python_helper` | Raised when a Lambda returns a FunctionError |
|
|
72
|
+
| `ServiceNotConfiguredError` | `aws_python_helper` | Raised when a service name is not in MICROSERVICE_URLS |
|
|
73
|
+
| `ApiClientError` | `aws_python_helper` | Raised when an HTTP request fails (network/timeout) |
|
|
74
|
+
| `ApiResponseError` | `aws_python_helper` | Raised when a remote API returns 4xx/5xx |
|
|
63
75
|
| `Session` | `aws_python_helper` | Request-scoped session object (state + user) |
|
|
64
76
|
| `get_session` | `aws_python_helper` | Read the current Session from async context |
|
|
65
77
|
| `set_session` | `aws_python_helper` | Set the current Session in async context |
|
|
@@ -897,19 +909,21 @@ Here's a complete example showing how an API can invoke a standalone lambda:
|
|
|
897
909
|
**1. The API endpoint** (`src/api/shippings/post.py`):
|
|
898
910
|
|
|
899
911
|
```python
|
|
900
|
-
from
|
|
901
|
-
import
|
|
902
|
-
|
|
912
|
+
from pydantic import BaseModel
|
|
913
|
+
from aws_python_helper import API, LambdaInvoker
|
|
914
|
+
|
|
915
|
+
class ShippingSchema(BaseModel):
|
|
916
|
+
customer_id: str
|
|
917
|
+
address: str
|
|
918
|
+
items: list[str]
|
|
903
919
|
|
|
904
920
|
class ShippingPostAPI(API):
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
raise ValueError(f"{field} is required")
|
|
921
|
+
|
|
922
|
+
@property
|
|
923
|
+
def schema(self):
|
|
924
|
+
return ShippingSchema # automatic validation → 400 on failure
|
|
910
925
|
|
|
911
926
|
async def process(self):
|
|
912
|
-
# Create shipping in database
|
|
913
927
|
shipping = {
|
|
914
928
|
'customer_id': self.data['customer_id'],
|
|
915
929
|
'address': self.data['address'],
|
|
@@ -921,15 +935,11 @@ class ShippingPostAPI(API):
|
|
|
921
935
|
result = await self.db.deliveries.shippings.insert_one(shipping)
|
|
922
936
|
shipping_id = str(result.inserted_id)
|
|
923
937
|
|
|
924
|
-
# Invoke standalone lambda asynchronously
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
Payload=json.dumps({
|
|
930
|
-
'data': {'shipping_id': shipping_id}
|
|
931
|
-
})
|
|
932
|
-
)
|
|
938
|
+
# Invoke standalone lambda asynchronously (fire-and-forget)
|
|
939
|
+
invoker = LambdaInvoker()
|
|
940
|
+
invoker.invoke_async('GenerateRouteLambda', payload={
|
|
941
|
+
'data': {'shipping_id': shipping_id}
|
|
942
|
+
})
|
|
933
943
|
|
|
934
944
|
self.set_code(201)
|
|
935
945
|
self.set_body({
|
|
@@ -1008,6 +1018,152 @@ __all__ = ['generate_route_handler']
|
|
|
1008
1018
|
- Can retry lambda independently if it fails
|
|
1009
1019
|
- Scalable architecture
|
|
1010
1020
|
|
|
1021
|
+
## ✅ Pydantic Schema Validation
|
|
1022
|
+
|
|
1023
|
+
The framework supports automatic request validation via [Pydantic](https://docs.pydantic.dev/) v2. Override the `schema` property on any `API` or `Lambda` subclass to declare the expected shape of `self.data`. The framework validates before calling `validate()` and replaces `self.data` with the coerced output — the property name stays the same.
|
|
1024
|
+
|
|
1025
|
+
### In an API endpoint
|
|
1026
|
+
|
|
1027
|
+
For `POST`/`PUT`, `self.data` is the request body. For `GET`/`LIST`, it is the query parameters. Both are validated the same way.
|
|
1028
|
+
|
|
1029
|
+
```python
|
|
1030
|
+
from pydantic import BaseModel
|
|
1031
|
+
from aws_python_helper import API
|
|
1032
|
+
|
|
1033
|
+
class SearchSchema(BaseModel):
|
|
1034
|
+
keys: list[str]
|
|
1035
|
+
state: str
|
|
1036
|
+
limit: int = 100
|
|
1037
|
+
|
|
1038
|
+
class SearchPostAPI(API):
|
|
1039
|
+
|
|
1040
|
+
@property
|
|
1041
|
+
def schema(self):
|
|
1042
|
+
return SearchSchema # ← that's all
|
|
1043
|
+
|
|
1044
|
+
async def process(self):
|
|
1045
|
+
keys = self.data["keys"] # self.data is validated and coerced
|
|
1046
|
+
limit = self.data["limit"] # default applied by Pydantic
|
|
1047
|
+
...
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
**On validation failure the framework automatically returns HTTP 400** — no extra code needed.
|
|
1051
|
+
|
|
1052
|
+
### In a Standalone Lambda
|
|
1053
|
+
|
|
1054
|
+
Same property, same behavior. On failure a `ValueError` is raised, which the handler returns as `{"success": False, "error": "..."}`.
|
|
1055
|
+
|
|
1056
|
+
```python
|
|
1057
|
+
from pydantic import BaseModel
|
|
1058
|
+
from aws_python_helper import Lambda
|
|
1059
|
+
|
|
1060
|
+
class SyncSchema(BaseModel):
|
|
1061
|
+
shipping_id: str
|
|
1062
|
+
carrier_id: str
|
|
1063
|
+
|
|
1064
|
+
class SyncCarrierLambda(Lambda):
|
|
1065
|
+
|
|
1066
|
+
@property
|
|
1067
|
+
def schema(self):
|
|
1068
|
+
return SyncSchema
|
|
1069
|
+
|
|
1070
|
+
async def process(self):
|
|
1071
|
+
shipping_id = self.data["shipping_id"] # validated
|
|
1072
|
+
...
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
### Without a schema
|
|
1076
|
+
|
|
1077
|
+
If `schema` is not overridden (returns `None`, the default), the behavior is identical to before — no change.
|
|
1078
|
+
|
|
1079
|
+
---
|
|
1080
|
+
|
|
1081
|
+
## 🔗 Inter-Service Communication
|
|
1082
|
+
|
|
1083
|
+
The framework provides two utilities for calling other services: `LambdaInvoker` for direct Lambda invocations and `ApiClient` for HTTP API calls.
|
|
1084
|
+
|
|
1085
|
+
### LambdaInvoker
|
|
1086
|
+
|
|
1087
|
+
Wraps `boto3` Lambda invocations with proper error handling. Never returns `None` — raises typed exceptions instead.
|
|
1088
|
+
|
|
1089
|
+
```python
|
|
1090
|
+
from aws_python_helper import LambdaInvoker, LambdaInvocationError, LambdaResponseError
|
|
1091
|
+
|
|
1092
|
+
invoker = LambdaInvoker()
|
|
1093
|
+
|
|
1094
|
+
# Synchronous — waits for result
|
|
1095
|
+
result = invoker.invoke("my-function-name", payload={"key": "value"})
|
|
1096
|
+
# result is the parsed dict returned by the Lambda
|
|
1097
|
+
|
|
1098
|
+
# Asynchronous fire-and-forget
|
|
1099
|
+
invoker.invoke_async("my-function-name", payload={"key": "value"})
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
**Exceptions:**
|
|
1103
|
+
|
|
1104
|
+
| Exception | When |
|
|
1105
|
+
|-----------|------|
|
|
1106
|
+
| `LambdaInvocationError` | boto3 could not reach the function (network, permissions, etc.) |
|
|
1107
|
+
| `LambdaResponseError` | The function itself raised an unhandled exception (`FunctionError` in the response) |
|
|
1108
|
+
|
|
1109
|
+
**Note:** `boto3` is part of the AWS Lambda Python runtime — no installation needed. For Fargate containers, add `boto3` to your `requirements.txt`.
|
|
1110
|
+
|
|
1111
|
+
### ApiClient
|
|
1112
|
+
|
|
1113
|
+
HTTP client for service-to-service calls. Resolves the target URL and auth token automatically from environment variables — no hardcoded URLs or tokens in code.
|
|
1114
|
+
|
|
1115
|
+
```python
|
|
1116
|
+
from aws_python_helper import ApiClient
|
|
1117
|
+
|
|
1118
|
+
client = ApiClient("dockets") # service name only
|
|
1119
|
+
result = await client.post("/search", body={"keys": [...]})
|
|
1120
|
+
result = await client.get("/dockets/123")
|
|
1121
|
+
result = await client.list("/dockets", params={"state": "CT"})
|
|
1122
|
+
result = await client.put("/dockets/123", body={"status": "active"})
|
|
1123
|
+
result = await client.patch("/dockets/123", body={"reviewed": True})
|
|
1124
|
+
result = await client.delete("/dockets/123")
|
|
1125
|
+
|
|
1126
|
+
# Extra headers merged with the defaults (token is always injected automatically)
|
|
1127
|
+
client = ApiClient("dockets", headers={"constitution-state": "CT"})
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
**How URL and token are resolved:**
|
|
1131
|
+
|
|
1132
|
+
| Env var | Purpose |
|
|
1133
|
+
|---------|---------|
|
|
1134
|
+
| `MICROSERVICE_URLS` | JSON map: `{"dockets": "https://api.example.com/", "title-search": "https://..."}` |
|
|
1135
|
+
| `INTER_SERVICE_TOKEN` | Bearer token injected as `Authorization: Bearer <token>` |
|
|
1136
|
+
| `AUTH_BYPASS_TOKEN` | Fallback if `INTER_SERVICE_TOKEN` is not set |
|
|
1137
|
+
|
|
1138
|
+
**Exceptions:**
|
|
1139
|
+
|
|
1140
|
+
| Exception | When |
|
|
1141
|
+
|-----------|------|
|
|
1142
|
+
| `ServiceNotConfiguredError` | Service name not found in `MICROSERVICE_URLS` |
|
|
1143
|
+
| `ApiClientError` | Network error or timeout |
|
|
1144
|
+
| `ApiResponseError` | Remote API returned 4xx or 5xx (has `.status_code` and `.response_body` attributes) |
|
|
1145
|
+
|
|
1146
|
+
**Terraform configuration** — set once per environment in Secrets Manager with keys `inter_service_token` and `microservice_urls`. The Lambda and Fargate modules pick them up automatically from infrastructure outputs; no extra variables needed in resource files beyond passing them through:
|
|
1147
|
+
|
|
1148
|
+
```hcl
|
|
1149
|
+
# In AWS Secrets Manager secret (JSON):
|
|
1150
|
+
# {
|
|
1151
|
+
# "inter_service_token": "my-secret-token",
|
|
1152
|
+
# "microservice_urls": "{\"dockets\": \"https://api-dockets.execute-api.us-east-2.amazonaws.com/\"}",
|
|
1153
|
+
# ...
|
|
1154
|
+
# }
|
|
1155
|
+
|
|
1156
|
+
# In Terraform module call (already handled by the modules):
|
|
1157
|
+
module "my_lambda" {
|
|
1158
|
+
source = "../modules/lambda"
|
|
1159
|
+
inter_service_token = data.terraform_remote_state.infrastructure.outputs.inter_service_token
|
|
1160
|
+
microservice_urls = data.terraform_remote_state.infrastructure.outputs.microservice_urls
|
|
1161
|
+
...
|
|
1162
|
+
}
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
---
|
|
1166
|
+
|
|
1011
1167
|
## 🏗️ Architecture Overview
|
|
1012
1168
|
|
|
1013
1169
|
Typical flow for event-driven architectures using this framework:
|
|
@@ -1114,6 +1270,8 @@ environment_variables = {
|
|
|
1114
1270
|
| `REQUIRE_AUTH` | Optional | Enable authentication middleware (`true`/`false`) |
|
|
1115
1271
|
| `AUTH_DB_NAME` | If `REQUIRE_AUTH=true` | MongoDB database for token validation |
|
|
1116
1272
|
| `AUTH_BYPASS_TOKEN` | Optional | Master token to bypass authentication |
|
|
1273
|
+
| `INTER_SERVICE_TOKEN` | Optional | Bearer token for `ApiClient` service-to-service calls |
|
|
1274
|
+
| `MICROSERVICE_URLS` | Optional | JSON map of service name → base URL used by `ApiClient` |
|
|
1117
1275
|
| `ECS_CLUSTER` | Fargate only | ECS cluster name for `FargateExecutor` |
|
|
1118
1276
|
| `ECS_SUBNETS` | Fargate only | Comma-separated subnet IDs for Fargate tasks |
|
|
1119
1277
|
| `CONSTITUTION_STATE` | Fargate only (auto) | State injected automatically by `FargateExecutor` — do not set manually |
|
|
@@ -15,6 +15,9 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
15
15
|
- **Fargate Tasks**: Same pattern to run tasks in Fargate containers
|
|
16
16
|
- **Standalone Lambdas**: Create lambdas invocable directly with AWS SDK
|
|
17
17
|
- **Authentication middleware**: Built-in token-based authentication
|
|
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
|
+
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
20
|
+
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
18
21
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
19
22
|
- **Type hints**: Modern Python with type annotations
|
|
20
23
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -42,6 +45,13 @@ All available classes and functions:
|
|
|
42
45
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
43
46
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
44
47
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
48
|
+
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
49
|
+
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
50
|
+
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
51
|
+
| `LambdaResponseError` | `aws_python_helper` | Raised when a Lambda returns a FunctionError |
|
|
52
|
+
| `ServiceNotConfiguredError` | `aws_python_helper` | Raised when a service name is not in MICROSERVICE_URLS |
|
|
53
|
+
| `ApiClientError` | `aws_python_helper` | Raised when an HTTP request fails (network/timeout) |
|
|
54
|
+
| `ApiResponseError` | `aws_python_helper` | Raised when a remote API returns 4xx/5xx |
|
|
45
55
|
| `Session` | `aws_python_helper` | Request-scoped session object (state + user) |
|
|
46
56
|
| `get_session` | `aws_python_helper` | Read the current Session from async context |
|
|
47
57
|
| `set_session` | `aws_python_helper` | Set the current Session in async context |
|
|
@@ -879,19 +889,21 @@ Here's a complete example showing how an API can invoke a standalone lambda:
|
|
|
879
889
|
**1. The API endpoint** (`src/api/shippings/post.py`):
|
|
880
890
|
|
|
881
891
|
```python
|
|
882
|
-
from
|
|
883
|
-
import
|
|
884
|
-
|
|
892
|
+
from pydantic import BaseModel
|
|
893
|
+
from aws_python_helper import API, LambdaInvoker
|
|
894
|
+
|
|
895
|
+
class ShippingSchema(BaseModel):
|
|
896
|
+
customer_id: str
|
|
897
|
+
address: str
|
|
898
|
+
items: list[str]
|
|
885
899
|
|
|
886
900
|
class ShippingPostAPI(API):
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
raise ValueError(f"{field} is required")
|
|
901
|
+
|
|
902
|
+
@property
|
|
903
|
+
def schema(self):
|
|
904
|
+
return ShippingSchema # automatic validation → 400 on failure
|
|
892
905
|
|
|
893
906
|
async def process(self):
|
|
894
|
-
# Create shipping in database
|
|
895
907
|
shipping = {
|
|
896
908
|
'customer_id': self.data['customer_id'],
|
|
897
909
|
'address': self.data['address'],
|
|
@@ -903,15 +915,11 @@ class ShippingPostAPI(API):
|
|
|
903
915
|
result = await self.db.deliveries.shippings.insert_one(shipping)
|
|
904
916
|
shipping_id = str(result.inserted_id)
|
|
905
917
|
|
|
906
|
-
# Invoke standalone lambda asynchronously
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
Payload=json.dumps({
|
|
912
|
-
'data': {'shipping_id': shipping_id}
|
|
913
|
-
})
|
|
914
|
-
)
|
|
918
|
+
# Invoke standalone lambda asynchronously (fire-and-forget)
|
|
919
|
+
invoker = LambdaInvoker()
|
|
920
|
+
invoker.invoke_async('GenerateRouteLambda', payload={
|
|
921
|
+
'data': {'shipping_id': shipping_id}
|
|
922
|
+
})
|
|
915
923
|
|
|
916
924
|
self.set_code(201)
|
|
917
925
|
self.set_body({
|
|
@@ -990,6 +998,152 @@ __all__ = ['generate_route_handler']
|
|
|
990
998
|
- Can retry lambda independently if it fails
|
|
991
999
|
- Scalable architecture
|
|
992
1000
|
|
|
1001
|
+
## ✅ Pydantic Schema Validation
|
|
1002
|
+
|
|
1003
|
+
The framework supports automatic request validation via [Pydantic](https://docs.pydantic.dev/) v2. Override the `schema` property on any `API` or `Lambda` subclass to declare the expected shape of `self.data`. The framework validates before calling `validate()` and replaces `self.data` with the coerced output — the property name stays the same.
|
|
1004
|
+
|
|
1005
|
+
### In an API endpoint
|
|
1006
|
+
|
|
1007
|
+
For `POST`/`PUT`, `self.data` is the request body. For `GET`/`LIST`, it is the query parameters. Both are validated the same way.
|
|
1008
|
+
|
|
1009
|
+
```python
|
|
1010
|
+
from pydantic import BaseModel
|
|
1011
|
+
from aws_python_helper import API
|
|
1012
|
+
|
|
1013
|
+
class SearchSchema(BaseModel):
|
|
1014
|
+
keys: list[str]
|
|
1015
|
+
state: str
|
|
1016
|
+
limit: int = 100
|
|
1017
|
+
|
|
1018
|
+
class SearchPostAPI(API):
|
|
1019
|
+
|
|
1020
|
+
@property
|
|
1021
|
+
def schema(self):
|
|
1022
|
+
return SearchSchema # ← that's all
|
|
1023
|
+
|
|
1024
|
+
async def process(self):
|
|
1025
|
+
keys = self.data["keys"] # self.data is validated and coerced
|
|
1026
|
+
limit = self.data["limit"] # default applied by Pydantic
|
|
1027
|
+
...
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
**On validation failure the framework automatically returns HTTP 400** — no extra code needed.
|
|
1031
|
+
|
|
1032
|
+
### In a Standalone Lambda
|
|
1033
|
+
|
|
1034
|
+
Same property, same behavior. On failure a `ValueError` is raised, which the handler returns as `{"success": False, "error": "..."}`.
|
|
1035
|
+
|
|
1036
|
+
```python
|
|
1037
|
+
from pydantic import BaseModel
|
|
1038
|
+
from aws_python_helper import Lambda
|
|
1039
|
+
|
|
1040
|
+
class SyncSchema(BaseModel):
|
|
1041
|
+
shipping_id: str
|
|
1042
|
+
carrier_id: str
|
|
1043
|
+
|
|
1044
|
+
class SyncCarrierLambda(Lambda):
|
|
1045
|
+
|
|
1046
|
+
@property
|
|
1047
|
+
def schema(self):
|
|
1048
|
+
return SyncSchema
|
|
1049
|
+
|
|
1050
|
+
async def process(self):
|
|
1051
|
+
shipping_id = self.data["shipping_id"] # validated
|
|
1052
|
+
...
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
### Without a schema
|
|
1056
|
+
|
|
1057
|
+
If `schema` is not overridden (returns `None`, the default), the behavior is identical to before — no change.
|
|
1058
|
+
|
|
1059
|
+
---
|
|
1060
|
+
|
|
1061
|
+
## 🔗 Inter-Service Communication
|
|
1062
|
+
|
|
1063
|
+
The framework provides two utilities for calling other services: `LambdaInvoker` for direct Lambda invocations and `ApiClient` for HTTP API calls.
|
|
1064
|
+
|
|
1065
|
+
### LambdaInvoker
|
|
1066
|
+
|
|
1067
|
+
Wraps `boto3` Lambda invocations with proper error handling. Never returns `None` — raises typed exceptions instead.
|
|
1068
|
+
|
|
1069
|
+
```python
|
|
1070
|
+
from aws_python_helper import LambdaInvoker, LambdaInvocationError, LambdaResponseError
|
|
1071
|
+
|
|
1072
|
+
invoker = LambdaInvoker()
|
|
1073
|
+
|
|
1074
|
+
# Synchronous — waits for result
|
|
1075
|
+
result = invoker.invoke("my-function-name", payload={"key": "value"})
|
|
1076
|
+
# result is the parsed dict returned by the Lambda
|
|
1077
|
+
|
|
1078
|
+
# Asynchronous fire-and-forget
|
|
1079
|
+
invoker.invoke_async("my-function-name", payload={"key": "value"})
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
**Exceptions:**
|
|
1083
|
+
|
|
1084
|
+
| Exception | When |
|
|
1085
|
+
|-----------|------|
|
|
1086
|
+
| `LambdaInvocationError` | boto3 could not reach the function (network, permissions, etc.) |
|
|
1087
|
+
| `LambdaResponseError` | The function itself raised an unhandled exception (`FunctionError` in the response) |
|
|
1088
|
+
|
|
1089
|
+
**Note:** `boto3` is part of the AWS Lambda Python runtime — no installation needed. For Fargate containers, add `boto3` to your `requirements.txt`.
|
|
1090
|
+
|
|
1091
|
+
### ApiClient
|
|
1092
|
+
|
|
1093
|
+
HTTP client for service-to-service calls. Resolves the target URL and auth token automatically from environment variables — no hardcoded URLs or tokens in code.
|
|
1094
|
+
|
|
1095
|
+
```python
|
|
1096
|
+
from aws_python_helper import ApiClient
|
|
1097
|
+
|
|
1098
|
+
client = ApiClient("dockets") # service name only
|
|
1099
|
+
result = await client.post("/search", body={"keys": [...]})
|
|
1100
|
+
result = await client.get("/dockets/123")
|
|
1101
|
+
result = await client.list("/dockets", params={"state": "CT"})
|
|
1102
|
+
result = await client.put("/dockets/123", body={"status": "active"})
|
|
1103
|
+
result = await client.patch("/dockets/123", body={"reviewed": True})
|
|
1104
|
+
result = await client.delete("/dockets/123")
|
|
1105
|
+
|
|
1106
|
+
# Extra headers merged with the defaults (token is always injected automatically)
|
|
1107
|
+
client = ApiClient("dockets", headers={"constitution-state": "CT"})
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
**How URL and token are resolved:**
|
|
1111
|
+
|
|
1112
|
+
| Env var | Purpose |
|
|
1113
|
+
|---------|---------|
|
|
1114
|
+
| `MICROSERVICE_URLS` | JSON map: `{"dockets": "https://api.example.com/", "title-search": "https://..."}` |
|
|
1115
|
+
| `INTER_SERVICE_TOKEN` | Bearer token injected as `Authorization: Bearer <token>` |
|
|
1116
|
+
| `AUTH_BYPASS_TOKEN` | Fallback if `INTER_SERVICE_TOKEN` is not set |
|
|
1117
|
+
|
|
1118
|
+
**Exceptions:**
|
|
1119
|
+
|
|
1120
|
+
| Exception | When |
|
|
1121
|
+
|-----------|------|
|
|
1122
|
+
| `ServiceNotConfiguredError` | Service name not found in `MICROSERVICE_URLS` |
|
|
1123
|
+
| `ApiClientError` | Network error or timeout |
|
|
1124
|
+
| `ApiResponseError` | Remote API returned 4xx or 5xx (has `.status_code` and `.response_body` attributes) |
|
|
1125
|
+
|
|
1126
|
+
**Terraform configuration** — set once per environment in Secrets Manager with keys `inter_service_token` and `microservice_urls`. The Lambda and Fargate modules pick them up automatically from infrastructure outputs; no extra variables needed in resource files beyond passing them through:
|
|
1127
|
+
|
|
1128
|
+
```hcl
|
|
1129
|
+
# In AWS Secrets Manager secret (JSON):
|
|
1130
|
+
# {
|
|
1131
|
+
# "inter_service_token": "my-secret-token",
|
|
1132
|
+
# "microservice_urls": "{\"dockets\": \"https://api-dockets.execute-api.us-east-2.amazonaws.com/\"}",
|
|
1133
|
+
# ...
|
|
1134
|
+
# }
|
|
1135
|
+
|
|
1136
|
+
# In Terraform module call (already handled by the modules):
|
|
1137
|
+
module "my_lambda" {
|
|
1138
|
+
source = "../modules/lambda"
|
|
1139
|
+
inter_service_token = data.terraform_remote_state.infrastructure.outputs.inter_service_token
|
|
1140
|
+
microservice_urls = data.terraform_remote_state.infrastructure.outputs.microservice_urls
|
|
1141
|
+
...
|
|
1142
|
+
}
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
---
|
|
1146
|
+
|
|
993
1147
|
## 🏗️ Architecture Overview
|
|
994
1148
|
|
|
995
1149
|
Typical flow for event-driven architectures using this framework:
|
|
@@ -1096,6 +1250,8 @@ environment_variables = {
|
|
|
1096
1250
|
| `REQUIRE_AUTH` | Optional | Enable authentication middleware (`true`/`false`) |
|
|
1097
1251
|
| `AUTH_DB_NAME` | If `REQUIRE_AUTH=true` | MongoDB database for token validation |
|
|
1098
1252
|
| `AUTH_BYPASS_TOKEN` | Optional | Master token to bypass authentication |
|
|
1253
|
+
| `INTER_SERVICE_TOKEN` | Optional | Bearer token for `ApiClient` service-to-service calls |
|
|
1254
|
+
| `MICROSERVICE_URLS` | Optional | JSON map of service name → base URL used by `ApiClient` |
|
|
1099
1255
|
| `ECS_CLUSTER` | Fargate only | ECS cluster name for `FargateExecutor` |
|
|
1100
1256
|
| `ECS_SUBNETS` | Fargate only | Comma-separated subnet IDs for Fargate tasks |
|
|
1101
1257
|
| `CONSTITUTION_STATE` | Fargate only (auto) | State injected automatically by `FargateExecutor` — do not set manually |
|
|
@@ -31,6 +31,17 @@ 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
|
+
# Invoker
|
|
35
|
+
from .invoker import (
|
|
36
|
+
LambdaInvoker,
|
|
37
|
+
ApiClient,
|
|
38
|
+
LambdaInvocationError,
|
|
39
|
+
LambdaResponseError,
|
|
40
|
+
ServiceNotConfiguredError,
|
|
41
|
+
ApiClientError,
|
|
42
|
+
ApiResponseError,
|
|
43
|
+
)
|
|
44
|
+
|
|
34
45
|
|
|
35
46
|
__all__ = [
|
|
36
47
|
'API',
|
|
@@ -54,5 +65,12 @@ __all__ = [
|
|
|
54
65
|
'set_session',
|
|
55
66
|
'StateValidator',
|
|
56
67
|
'InvalidStateError',
|
|
68
|
+
'LambdaInvoker',
|
|
69
|
+
'ApiClient',
|
|
70
|
+
'LambdaInvocationError',
|
|
71
|
+
'LambdaResponseError',
|
|
72
|
+
'ServiceNotConfiguredError',
|
|
73
|
+
'ApiClientError',
|
|
74
|
+
'ApiResponseError',
|
|
57
75
|
]
|
|
58
76
|
|
|
@@ -266,10 +266,26 @@ class API(ABC):
|
|
|
266
266
|
'headers': self._response_headers
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
@property
|
|
270
|
+
def schema(self):
|
|
271
|
+
"""
|
|
272
|
+
Optional Pydantic model class for automatic request validation.
|
|
273
|
+
|
|
274
|
+
Override this property to return a BaseModel subclass.
|
|
275
|
+
The dispatcher will validate self.data against it before calling validate(),
|
|
276
|
+
replace self.data with the coerced model output, and return 400 on failure.
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
@property
|
|
280
|
+
def schema(self):
|
|
281
|
+
return MyRequestSchema
|
|
282
|
+
"""
|
|
283
|
+
return None
|
|
284
|
+
|
|
269
285
|
async def validate(self):
|
|
270
286
|
"""
|
|
271
287
|
Hook for data validation
|
|
272
|
-
|
|
288
|
+
|
|
273
289
|
Override this method to implement custom validations.
|
|
274
290
|
If the validation fails, raise an exception.
|
|
275
291
|
"""
|
|
@@ -116,10 +116,18 @@ class Dispatcher:
|
|
|
116
116
|
session = get_session()
|
|
117
117
|
session.user = api._current_user
|
|
118
118
|
|
|
119
|
-
# 3.
|
|
119
|
+
# 3. Schema validation (automatic, if schema property is defined)
|
|
120
|
+
if api.schema is not None:
|
|
121
|
+
try:
|
|
122
|
+
validated = api.schema(**api.data)
|
|
123
|
+
api.data = validated.model_dump()
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise ValueError(str(e))
|
|
126
|
+
|
|
127
|
+
# 4. Validate
|
|
120
128
|
await api.validate()
|
|
121
|
-
|
|
122
|
-
#
|
|
129
|
+
|
|
130
|
+
# 5. Process
|
|
123
131
|
await api.process()
|
|
124
132
|
|
|
125
133
|
# 5. If no code was set, use 200 by default
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .lambda_invoker import LambdaInvoker
|
|
2
|
+
from .api_client import ApiClient
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
LambdaInvocationError,
|
|
5
|
+
LambdaResponseError,
|
|
6
|
+
ServiceNotConfiguredError,
|
|
7
|
+
ApiClientError,
|
|
8
|
+
ApiResponseError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"LambdaInvoker",
|
|
13
|
+
"ApiClient",
|
|
14
|
+
"LambdaInvocationError",
|
|
15
|
+
"LambdaResponseError",
|
|
16
|
+
"ServiceNotConfiguredError",
|
|
17
|
+
"ApiClientError",
|
|
18
|
+
"ApiResponseError",
|
|
19
|
+
]
|