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.
Files changed (51) hide show
  1. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/PKG-INFO +177 -19
  2. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/README.md +174 -18
  3. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/__init__.py +18 -0
  4. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/base.py +17 -1
  5. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/dispatcher.py +11 -3
  6. aws_python_helper-0.36.0/aws_python_helper/invoker/__init__.py +19 -0
  7. aws_python_helper-0.36.0/aws_python_helper/invoker/api_client.py +153 -0
  8. aws_python_helper-0.36.0/aws_python_helper/invoker/exceptions.py +23 -0
  9. aws_python_helper-0.36.0/aws_python_helper/invoker/lambda_invoker.py +96 -0
  10. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/lambda_standalone/base.py +36 -6
  11. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/PKG-INFO +177 -19
  12. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/SOURCES.txt +4 -0
  13. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/requires.txt +2 -0
  14. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/pyproject.toml +3 -1
  15. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/__init__.py +0 -0
  16. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/auth_middleware.py +0 -0
  17. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/auth_validators.py +0 -0
  18. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/exceptions.py +0 -0
  19. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/fetcher.py +0 -0
  20. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/api/handler.py +0 -0
  21. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/context/__init__.py +0 -0
  22. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/context/session.py +0 -0
  23. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/context/state_validator.py +0 -0
  24. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/__init__.py +0 -0
  25. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/database_proxy.py +0 -0
  26. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/external_database_proxy.py +0 -0
  27. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
  28. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/database/mongo_manager.py +0 -0
  29. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/__init__.py +0 -0
  30. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/executor.py +0 -0
  31. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/fetcher.py +0 -0
  32. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/handler.py +0 -0
  33. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/fargate/task_base.py +0 -0
  34. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
  35. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
  36. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
  37. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/repository/__init__.py +0 -0
  38. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/repository/base.py +0 -0
  39. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sns/__init__.py +0 -0
  40. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sns/publisher.py +0 -0
  41. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sqs/__init__.py +0 -0
  42. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sqs/consumer_base.py +0 -0
  43. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sqs/fetcher.py +0 -0
  44. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/sqs/handler.py +0 -0
  45. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/utils/__init__.py +0 -0
  46. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/utils/json_encoder.py +0 -0
  47. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/utils/response.py +0 -0
  48. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper/utils/serializer.py +0 -0
  49. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
  50. {aws_python_helper-0.35.0 → aws_python_helper-0.36.0}/aws_python_helper.egg-info/top_level.txt +0 -0
  51. {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.35.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 aws_python_helper.api.base import API
901
- import boto3
902
- import json
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
- async def validate(self):
906
- required_fields = ['customer_id', 'address', 'items']
907
- for field in required_fields:
908
- if field not in self.data:
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 to generate route
925
- lambda_client = boto3.client('lambda')
926
- lambda_client.invoke(
927
- FunctionName='GenerateRouteLambda',
928
- InvocationType='Event', # Asynchronous
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 aws_python_helper.api.base import API
883
- import boto3
884
- import json
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
- async def validate(self):
888
- required_fields = ['customer_id', 'address', 'items']
889
- for field in required_fields:
890
- if field not in self.data:
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 to generate route
907
- lambda_client = boto3.client('lambda')
908
- lambda_client.invoke(
909
- FunctionName='GenerateRouteLambda',
910
- InvocationType='Event', # Asynchronous
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. Validate
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
- # 4. Process
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
+ ]