aws-python-helper 0.23.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.23.0/PKG-INFO +712 -0
- aws_python_helper-0.23.0/README.md +692 -0
- aws_python_helper-0.23.0/aws_python_helper/__init__.py +45 -0
- aws_python_helper-0.23.0/aws_python_helper/api/__init__.py +11 -0
- aws_python_helper-0.23.0/aws_python_helper/api/auth_middleware.py +108 -0
- aws_python_helper-0.23.0/aws_python_helper/api/auth_validators.py +143 -0
- aws_python_helper-0.23.0/aws_python_helper/api/base.py +272 -0
- aws_python_helper-0.23.0/aws_python_helper/api/dispatcher.py +213 -0
- aws_python_helper-0.23.0/aws_python_helper/api/exceptions.py +43 -0
- aws_python_helper-0.23.0/aws_python_helper/api/fetcher.py +210 -0
- aws_python_helper-0.23.0/aws_python_helper/api/handler.py +106 -0
- aws_python_helper-0.23.0/aws_python_helper/database/__init__.py +11 -0
- aws_python_helper-0.23.0/aws_python_helper/database/database_proxy.py +50 -0
- aws_python_helper-0.23.0/aws_python_helper/database/external_database_proxy.py +66 -0
- aws_python_helper-0.23.0/aws_python_helper/database/external_mongo_manager.py +212 -0
- aws_python_helper-0.23.0/aws_python_helper/database/mongo_manager.py +214 -0
- aws_python_helper-0.23.0/aws_python_helper/fargate/__init__.py +9 -0
- aws_python_helper-0.23.0/aws_python_helper/fargate/executor.py +226 -0
- aws_python_helper-0.23.0/aws_python_helper/fargate/fetcher.py +108 -0
- aws_python_helper-0.23.0/aws_python_helper/fargate/handler.py +101 -0
- aws_python_helper-0.23.0/aws_python_helper/fargate/task_base.py +165 -0
- aws_python_helper-0.23.0/aws_python_helper/lambda_standalone/__init__.py +8 -0
- aws_python_helper-0.23.0/aws_python_helper/lambda_standalone/base.py +171 -0
- aws_python_helper-0.23.0/aws_python_helper/lambda_standalone/fetcher.py +122 -0
- aws_python_helper-0.23.0/aws_python_helper/lambda_standalone/handler.py +117 -0
- aws_python_helper-0.23.0/aws_python_helper/sns/__init__.py +6 -0
- aws_python_helper-0.23.0/aws_python_helper/sns/publisher.py +245 -0
- aws_python_helper-0.23.0/aws_python_helper/sqs/__init__.py +10 -0
- aws_python_helper-0.23.0/aws_python_helper/sqs/consumer_base.py +416 -0
- aws_python_helper-0.23.0/aws_python_helper/sqs/fetcher.py +111 -0
- aws_python_helper-0.23.0/aws_python_helper/sqs/handler.py +138 -0
- aws_python_helper-0.23.0/aws_python_helper/utils/__init__.py +9 -0
- aws_python_helper-0.23.0/aws_python_helper/utils/json_encoder.py +108 -0
- aws_python_helper-0.23.0/aws_python_helper/utils/response.py +145 -0
- aws_python_helper-0.23.0/aws_python_helper/utils/serializer.py +103 -0
- aws_python_helper-0.23.0/aws_python_helper.egg-info/PKG-INFO +712 -0
- aws_python_helper-0.23.0/aws_python_helper.egg-info/SOURCES.txt +40 -0
- aws_python_helper-0.23.0/aws_python_helper.egg-info/dependency_links.txt +1 -0
- aws_python_helper-0.23.0/aws_python_helper.egg-info/requires.txt +3 -0
- aws_python_helper-0.23.0/aws_python_helper.egg-info/top_level.txt +1 -0
- aws_python_helper-0.23.0/pyproject.toml +42 -0
- aws_python_helper-0.23.0/setup.cfg +4 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aws-python-helper
|
|
3
|
+
Version: 0.23.0
|
|
4
|
+
Summary: AWS Python Helper Framework
|
|
5
|
+
Author-email: Fabian Calros <neufabiae@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fabiae/aws-python-framework
|
|
8
|
+
Project-URL: Source Code, https://github.com/fabiae/aws-python-framework
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/fabiae/aws-python-framework/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/fabiae/aws-python-framework/blob/main/README.md
|
|
11
|
+
Keywords: aws,python,framework,helper,mongodb,sqs,sns,fargate,lambda
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: motor==3.3.2
|
|
18
|
+
Requires-Dist: pymongo==4.6.1
|
|
19
|
+
Requires-Dist: bcrypt>=4.0.0
|
|
20
|
+
|
|
21
|
+
# AWS Python Framework
|
|
22
|
+
|
|
23
|
+
Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks, and Standalone Lambdas with Python in AWS Lambda.
|
|
24
|
+
|
|
25
|
+
## 🚀 Features
|
|
26
|
+
|
|
27
|
+
- **Reusable single handler**: A single handler for all your API routes
|
|
28
|
+
- **Dynamic controller loading**: Routing based on convention
|
|
29
|
+
- **OOP structure**: Object-oriented programming for your code
|
|
30
|
+
- **Flexible MongoDB**: Direct access to multiple databases without models
|
|
31
|
+
- **SQS Consumers**: Same pattern to process SQS messages
|
|
32
|
+
- **SNS Publishers**: Same pattern to publish messages to SNS topics
|
|
33
|
+
- **Fargate Tasks**: Same pattern to run tasks in Fargate containers
|
|
34
|
+
- **Standalone Lambdas**: Create lambdas invocable directly with AWS SDK
|
|
35
|
+
- **Type hints**: Modern Python with type annotations
|
|
36
|
+
- **Async/await**: Full support for asynchronous operations
|
|
37
|
+
|
|
38
|
+
## 🔧 Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Install dependencies
|
|
42
|
+
pip install -r requirements.txt
|
|
43
|
+
|
|
44
|
+
# Configure MongoDB URI
|
|
45
|
+
export MONGODB_URI="mongodb://localhost:27017"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 📂 Project Structure
|
|
49
|
+
|
|
50
|
+
This framework follows a convention-based folder structure. Here's the recommended organization:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
your-project/
|
|
54
|
+
└── src/
|
|
55
|
+
├── api/ # REST APIs
|
|
56
|
+
│ └── users/ # Resource folder (kebab-case)
|
|
57
|
+
│ ├── get.py # GET /users/123 -> UserGetAPI
|
|
58
|
+
│ ├── list.py # GET /users -> UserListAPI
|
|
59
|
+
│ ├── post.py # POST /users -> UserPostAPI
|
|
60
|
+
│ ├── put.py # PUT /users/123 -> UserPutAPI
|
|
61
|
+
│ └── delete.py # DELETE /users/123 -> UserDeleteAPI
|
|
62
|
+
│
|
|
63
|
+
├── consumer/ # SQS Consumers (direct files)
|
|
64
|
+
│ ├── user_created.py # user-created -> UserCreatedConsumer
|
|
65
|
+
│ ├── title_indexed.py # title-indexed -> TitleIndexedConsumer
|
|
66
|
+
│ └── order_processed.py # order-processed -> OrderProcessedConsumer
|
|
67
|
+
│
|
|
68
|
+
├── lambda/ # Standalone Lambdas (folders)
|
|
69
|
+
│ ├── generate-route/ # generate-route -> GenerateRouteLambda
|
|
70
|
+
│ │ └── main.py
|
|
71
|
+
│ ├── sync-carrier/ # sync-carrier -> SyncCarrierLambda
|
|
72
|
+
│ │ └── main.py
|
|
73
|
+
│ └── process-payment/ # process-payment -> ProcessPaymentLambda
|
|
74
|
+
│ └── main.py
|
|
75
|
+
│
|
|
76
|
+
└── task/ # Fargate Tasks (folders)
|
|
77
|
+
├── search-tax-by-town/ # search-tax-by-town -> SearchTaxByTownTask
|
|
78
|
+
│ ├── main.py # Entry point
|
|
79
|
+
│ └── task.py # Task class
|
|
80
|
+
└── process-data/ # process-data -> ProcessDataTask
|
|
81
|
+
├── main.py
|
|
82
|
+
└── task.py
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Naming Conventions
|
|
86
|
+
|
|
87
|
+
The framework uses automatic class name detection based on your folder/file structure:
|
|
88
|
+
|
|
89
|
+
| Type | Handler Name | File Path | Class Name |
|
|
90
|
+
|------|--------------|-----------|------------|
|
|
91
|
+
| **API** | N/A | `src/api/users/list.py` | `UsersListAPI` |
|
|
92
|
+
| **Consumer** | `user-created` | `src/consumer/user_created.py` | `UserCreatedConsumer` |
|
|
93
|
+
| **Lambda** | `generate-route` | `src/lambda/generate-route/main.py` | `GenerateRouteLambda` |
|
|
94
|
+
| **Task** | `search-tax-by-town` | `src/task/search-tax-by-town/task.py` | `SearchTaxByTownTask` |
|
|
95
|
+
|
|
96
|
+
**Rules:**
|
|
97
|
+
- Handler names use **kebab-case** (e.g., `user-created`, `generate-route`)
|
|
98
|
+
- Consumer files use **snake_case** (e.g., `user_created.py`)
|
|
99
|
+
- Lambda folders use **kebab-case** (e.g., `generate-route/`)
|
|
100
|
+
- Task folders use **kebab-case** (e.g., `search-tax-by-town/`)
|
|
101
|
+
- Class names always use **PascalCase** with suffix (e.g., `UserCreatedConsumer`)
|
|
102
|
+
|
|
103
|
+
## 📝 Basic Usage
|
|
104
|
+
|
|
105
|
+
### Create an Endpoint
|
|
106
|
+
|
|
107
|
+
**1. Create your API class** in `src/api/constitutions/list.py`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from aws_python_helper.api.base import API
|
|
111
|
+
|
|
112
|
+
class ConstitutionListAPI(API):
|
|
113
|
+
async def process(self):
|
|
114
|
+
# Direct access to MongoDB
|
|
115
|
+
constitutions = await self.db.constitution_db.constitutions.find().to_list(100)
|
|
116
|
+
self.set_body(constitutions)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**2. The routing is automatic:**
|
|
120
|
+
- `GET /constitutions` → `src/api/constitutions/list.py`
|
|
121
|
+
- `GET /constitutions/123` → `src/api/constitutions/get.py`
|
|
122
|
+
- `POST /constitutions` → `src/api/constitutions/post.py`
|
|
123
|
+
|
|
124
|
+
**3. Configure the generic handler** (`src/handlers/api_handler.py`):
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from aws_python_helper.api.handler import api_handler
|
|
128
|
+
handler = api_handler
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Create an SQS Consumer
|
|
132
|
+
|
|
133
|
+
**1. Create your consumer** in `src/consumer/title_indexed.py`:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
137
|
+
|
|
138
|
+
class TitleIndexedConsumer(SQSConsumer):
|
|
139
|
+
async def process_record(self, record):
|
|
140
|
+
body = self.parse_body(record)
|
|
141
|
+
# Your logic here
|
|
142
|
+
await self.db.constitution_db.titles.insert_one(body)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**2. Configure the handler** in `src/handlers/sqs_handler.py`:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from aws_python_helper.sqs.handler import sqs_handler
|
|
149
|
+
|
|
150
|
+
# Create a handler for each consumer and export it
|
|
151
|
+
title_indexed_handler = sqs_handler('title-indexed')
|
|
152
|
+
|
|
153
|
+
__all__ = ['title_indexed_handler']
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Create a Standalone Lambda
|
|
157
|
+
|
|
158
|
+
Standalone lambdas are functions that can be invoked directly using the AWS SDK, without an HTTP endpoint. They're perfect for internal operations, integrations, and background processing tasks.
|
|
159
|
+
|
|
160
|
+
**Differences with APIs:**
|
|
161
|
+
- No API Gateway - invoked directly with AWS SDK
|
|
162
|
+
- No HTTP methods or routing
|
|
163
|
+
- Can be called from other lambdas, Step Functions, or any AWS service
|
|
164
|
+
- Perfect for internal microservices communication
|
|
165
|
+
|
|
166
|
+
**1. Create your lambda class** in `src/lambda/generate-route/main.py`:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from aws_python_helper.lambda_standalone.base import Lambda
|
|
170
|
+
from datetime import datetime
|
|
171
|
+
|
|
172
|
+
class GenerateRouteLambda(Lambda):
|
|
173
|
+
async def validate(self):
|
|
174
|
+
# Validate input data
|
|
175
|
+
if 'shipping_id' not in self.data:
|
|
176
|
+
raise ValueError("shipping_id is required")
|
|
177
|
+
|
|
178
|
+
if not isinstance(self.data['shipping_id'], str):
|
|
179
|
+
raise TypeError("shipping_id must be a string")
|
|
180
|
+
|
|
181
|
+
async def process(self):
|
|
182
|
+
# Your business logic here
|
|
183
|
+
shipping_id = self.data['shipping_id']
|
|
184
|
+
|
|
185
|
+
# Access to MongoDB
|
|
186
|
+
shipping = await self.db.deliveries.shippings.find_one(
|
|
187
|
+
{'_id': shipping_id}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if not shipping:
|
|
191
|
+
raise ValueError(f"Shipping {shipping_id} not found")
|
|
192
|
+
|
|
193
|
+
# Create route
|
|
194
|
+
route = {
|
|
195
|
+
'shipping_id': shipping_id,
|
|
196
|
+
'carrier_id': shipping.get('carrier_id'),
|
|
197
|
+
'status': 'pending',
|
|
198
|
+
'created_at': datetime.utcnow()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
result = await self.db.deliveries.routes.insert_one(route)
|
|
202
|
+
|
|
203
|
+
self.logger.info(f"Route created: {result.inserted_id}")
|
|
204
|
+
|
|
205
|
+
# Return result
|
|
206
|
+
return {
|
|
207
|
+
'route_id': str(result.inserted_id),
|
|
208
|
+
'shipping_id': shipping_id
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**2. Configure the handler** in `src/handlers/lambda_handler.py`:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from aws_python_helper.lambda_standalone.handler import lambda_handler
|
|
216
|
+
|
|
217
|
+
# Create a handler for each lambda and export it
|
|
218
|
+
generate_route_handler = lambda_handler('generate-route')
|
|
219
|
+
sync_carrier_handler = lambda_handler('sync-carrier')
|
|
220
|
+
process_payment_handler = lambda_handler('process-payment')
|
|
221
|
+
|
|
222
|
+
__all__ = [
|
|
223
|
+
'generate_route_handler',
|
|
224
|
+
'sync_carrier_handler',
|
|
225
|
+
'process_payment_handler'
|
|
226
|
+
]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Note:** The handler name `'generate-route'` (kebab-case) will automatically look for:
|
|
230
|
+
- Folder: `src/lambda/generate-route/` (kebab-case)
|
|
231
|
+
- File: `main.py`
|
|
232
|
+
- Class: `GenerateRouteLambda`
|
|
233
|
+
|
|
234
|
+
**3. Invoke from another Lambda or API** using boto3:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
import boto3
|
|
238
|
+
import json
|
|
239
|
+
|
|
240
|
+
lambda_client = boto3.client('lambda')
|
|
241
|
+
|
|
242
|
+
# Invoke synchronously (RequestResponse)
|
|
243
|
+
response = lambda_client.invoke(
|
|
244
|
+
FunctionName='GenerateRouteLambda',
|
|
245
|
+
InvocationType='RequestResponse',
|
|
246
|
+
Payload=json.dumps({
|
|
247
|
+
'data': {
|
|
248
|
+
'shipping_id': '507f1f77bcf86cd799439011'
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
result = json.loads(response['Payload'].read())
|
|
254
|
+
# {'success': True, 'data': {'route_id': '...', 'shipping_id': '...'}}
|
|
255
|
+
|
|
256
|
+
if result['success']:
|
|
257
|
+
print(f"Route created: {result['data']['route_id']}")
|
|
258
|
+
else:
|
|
259
|
+
print(f"Error: {result['error']}")
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**4. Invoke asynchronously** (fire and forget):
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# Invoke asynchronously (Event)
|
|
266
|
+
lambda_client.invoke(
|
|
267
|
+
FunctionName='GenerateRouteLambda',
|
|
268
|
+
InvocationType='Event', # Asynchronous
|
|
269
|
+
Payload=json.dumps({
|
|
270
|
+
'data': {
|
|
271
|
+
'shipping_id': '507f1f77bcf86cd799439011'
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
)
|
|
275
|
+
# Returns immediately without waiting for the result
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Naming Convention:**
|
|
279
|
+
|
|
280
|
+
| Lambda Name (kebab-case) | Folder | File | Class |
|
|
281
|
+
|--------------------------|--------|------|-------|
|
|
282
|
+
| `generate-route` | `src/lambda/generate-route/` | `main.py` | `GenerateRouteLambda` |
|
|
283
|
+
| `sync-carrier` | `src/lambda/sync-carrier/` | `main.py` | `SyncCarrierLambda` |
|
|
284
|
+
| `process-payment` | `src/lambda/process-payment/` | `main.py` | `ProcessPaymentLambda` |
|
|
285
|
+
| `send-notification` | `src/lambda/send-notification/` | `main.py` | `SendNotificationLambda` |
|
|
286
|
+
|
|
287
|
+
**Common Use Cases:**
|
|
288
|
+
- Internal microservices communication
|
|
289
|
+
- Background data processing
|
|
290
|
+
- Integration with external services
|
|
291
|
+
- Scheduled tasks (with EventBridge)
|
|
292
|
+
- Step Functions workflows
|
|
293
|
+
- Cross-service operations
|
|
294
|
+
|
|
295
|
+
### Publish to SNS
|
|
296
|
+
|
|
297
|
+
**1. Create your topic** in `src/topic/title_indexed.py`:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from aws_python_helper.sns.publisher import SNSPublisher
|
|
301
|
+
import os
|
|
302
|
+
|
|
303
|
+
class TitleIndexedTopic(SNSPublisher):
|
|
304
|
+
def __init__(self):
|
|
305
|
+
super().__init__(
|
|
306
|
+
topic_arn=os.getenv('TITLE_INDEXED_SNS_TOPIC_ARN')
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
async def publish_message(self, constitution_id, title):
|
|
310
|
+
await self.publish({
|
|
311
|
+
'constitution_id': constitution_id,
|
|
312
|
+
'title': title,
|
|
313
|
+
'event_type': 'title_indexed'
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**2. Use the topic** from anywhere:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
from src.topics.title_indexed import TitleIndexedTopic
|
|
321
|
+
|
|
322
|
+
# In a consumer, API or task
|
|
323
|
+
topic = TitleIndexedTopic()
|
|
324
|
+
await topic.publish_indexed('123', 'My Constitution')
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Run a Fargate Task
|
|
328
|
+
|
|
329
|
+
**1. Create your task** in `src/task/search-tax-by-town/task.py`:
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from aws_python_helper.fargate.task_base import FargateTask
|
|
333
|
+
|
|
334
|
+
class SearchTaxByTownTask(FargateTask):
|
|
335
|
+
|
|
336
|
+
async def execute(self):
|
|
337
|
+
town = self.require_env('TOWN')
|
|
338
|
+
self.logger.info(f"Processing town: {town}")
|
|
339
|
+
|
|
340
|
+
# Access to DB
|
|
341
|
+
docs = await self.db.smart_data.address.find({'town': town}).to_list()
|
|
342
|
+
|
|
343
|
+
# Your logic here
|
|
344
|
+
for doc in docs:
|
|
345
|
+
# Process document
|
|
346
|
+
pass
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**2. Create the entry point** in `src/task/search-tax-by-town/main.py`:
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
from aws_python_helper.fargate.handler import fargate_handler
|
|
353
|
+
import sys
|
|
354
|
+
|
|
355
|
+
if __name__ == '__main__':
|
|
356
|
+
exit_code = fargate_handler('search-tax-by-town')
|
|
357
|
+
sys.exit(exit_code)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**3. Create the Dockerfile** in `src/task/search-tax-by-town/Dockerfile`:
|
|
361
|
+
|
|
362
|
+
```dockerfile
|
|
363
|
+
FROM python:3.10.12-slim
|
|
364
|
+
WORKDIR /app
|
|
365
|
+
|
|
366
|
+
# Install dependencies
|
|
367
|
+
COPY requirements.txt /app/framework_requirements.txt
|
|
368
|
+
COPY src/task/search-tax-by-town/requirements.txt /app/task_requirements.txt
|
|
369
|
+
RUN pip install -r /app/framework_requirements.txt && \
|
|
370
|
+
pip install -r /app/task_requirements.txt
|
|
371
|
+
|
|
372
|
+
# Copy code
|
|
373
|
+
COPY aws_python_helper /app/aws_python_helper
|
|
374
|
+
COPY config.py /app/config.py
|
|
375
|
+
COPY task /app/task
|
|
376
|
+
COPY task/search-tax-by-town/main.py /app/main.py
|
|
377
|
+
|
|
378
|
+
ENV PYTHONUNBUFFERED=1
|
|
379
|
+
CMD ["python", "main.py"]
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**4. Invoke from Lambda**:
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
from aws_python_helper.fargate.executor import FargateExecutor
|
|
386
|
+
|
|
387
|
+
def handler(event, context):
|
|
388
|
+
executor = FargateExecutor()
|
|
389
|
+
task_arn = executor.run_task(
|
|
390
|
+
'search-tax-by-town',
|
|
391
|
+
envs={'town': 'Norwalk', 'only_tax': 'true'}
|
|
392
|
+
)
|
|
393
|
+
return {'taskArn': task_arn}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## 🗄️ Access to MongoDB
|
|
397
|
+
|
|
398
|
+
The framework provides flexible access to multiple databases:
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
class MyAPI(API):
|
|
402
|
+
async def process(self):
|
|
403
|
+
# Access to different databases
|
|
404
|
+
user = await self.db.users_db.users.find_one({'_id': user_id})
|
|
405
|
+
|
|
406
|
+
# Another database
|
|
407
|
+
await self.db.analytics_db.logs.insert_one({'action': 'view'})
|
|
408
|
+
|
|
409
|
+
# Multiple collections
|
|
410
|
+
titles = await self.db.constitution_db.titles.find().to_list(100)
|
|
411
|
+
articles = await self.db.constitution_db.articles.find().to_list(100)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## 🔄 Routing Convention
|
|
415
|
+
|
|
416
|
+
The framework uses convention over configuration for the routing:
|
|
417
|
+
|
|
418
|
+
| Request | Loaded file |
|
|
419
|
+
|---------|----------------|
|
|
420
|
+
| `GET /users` | `api/users/list.py` |
|
|
421
|
+
| `GET /users/123` | `api/users/get.py` |
|
|
422
|
+
| `POST /users` | `api/users/post.py` |
|
|
423
|
+
| `PUT /users/123` | `api/users/put.py` |
|
|
424
|
+
| `DELETE /users/123` | `api/users/delete.py` |
|
|
425
|
+
| `GET /users/123/posts` | `api/users/posts/list.py` |
|
|
426
|
+
| `GET /users/123/posts/456` | `api/users/posts/get.py` |
|
|
427
|
+
|
|
428
|
+
**Logic:**
|
|
429
|
+
- The parts with **even indices** (0,2,4...) are **directories**
|
|
430
|
+
- The parts with **odd indices** (1,3,5...) are **path parameters**
|
|
431
|
+
- `GET` with **odd number of parts** → **list** method
|
|
432
|
+
- `GET` with **even number of parts** → **get** method
|
|
433
|
+
- Other methods use their name directly
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
## 🎯 Complete Example
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
# src/api/constitutions/list.py
|
|
440
|
+
from aws_python_helper.api.base import API
|
|
441
|
+
|
|
442
|
+
class ConstitutionListAPI(API):
|
|
443
|
+
async def validate(self):
|
|
444
|
+
if 'limit' in self.data:
|
|
445
|
+
limit = int(self.data['limit'])
|
|
446
|
+
if limit > 1000:
|
|
447
|
+
raise ValueError("Limit cannot exceed 1000")
|
|
448
|
+
|
|
449
|
+
async def process(self):
|
|
450
|
+
# Build filters
|
|
451
|
+
filters = {}
|
|
452
|
+
if 'country' in self.data:
|
|
453
|
+
filters['country'] = self.data['country']
|
|
454
|
+
|
|
455
|
+
# Query MongoDB
|
|
456
|
+
limit = int(self.data.get('limit', 100))
|
|
457
|
+
results = await self.db.constitution_db.constitutions.find(
|
|
458
|
+
filters
|
|
459
|
+
).limit(limit).to_list(limit)
|
|
460
|
+
|
|
461
|
+
# Count total
|
|
462
|
+
total = await self.db.constitution_db.constitutions.count_documents(filters)
|
|
463
|
+
|
|
464
|
+
# Register in analytics
|
|
465
|
+
await self.db.analytics_db.searches.insert_one({
|
|
466
|
+
'filters': filters,
|
|
467
|
+
'result_count': len(results)
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
# Response
|
|
471
|
+
self.set_body({
|
|
472
|
+
'data': results,
|
|
473
|
+
'total': total
|
|
474
|
+
})
|
|
475
|
+
self.set_header('X-Total-Count', str(total))
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## 🔗 Integration Example: API + Standalone Lambda
|
|
479
|
+
|
|
480
|
+
Here's a complete example showing how an API can invoke a standalone lambda:
|
|
481
|
+
|
|
482
|
+
**Scenario:** An API endpoint that creates a shipping and then asynchronously generates its route using a standalone lambda.
|
|
483
|
+
|
|
484
|
+
**1. The API endpoint** (`src/api/shippings/post.py`):
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
from aws_python_helper.api.base import API
|
|
488
|
+
import boto3
|
|
489
|
+
import json
|
|
490
|
+
|
|
491
|
+
class ShippingPostAPI(API):
|
|
492
|
+
async def validate(self):
|
|
493
|
+
required_fields = ['customer_id', 'address', 'items']
|
|
494
|
+
for field in required_fields:
|
|
495
|
+
if field not in self.data:
|
|
496
|
+
raise ValueError(f"{field} is required")
|
|
497
|
+
|
|
498
|
+
async def process(self):
|
|
499
|
+
# Create shipping in database
|
|
500
|
+
shipping = {
|
|
501
|
+
'customer_id': self.data['customer_id'],
|
|
502
|
+
'address': self.data['address'],
|
|
503
|
+
'items': self.data['items'],
|
|
504
|
+
'status': 'pending',
|
|
505
|
+
'route_pending': True
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
result = await self.db.deliveries.shippings.insert_one(shipping)
|
|
509
|
+
shipping_id = str(result.inserted_id)
|
|
510
|
+
|
|
511
|
+
# Invoke standalone lambda asynchronously to generate route
|
|
512
|
+
lambda_client = boto3.client('lambda')
|
|
513
|
+
lambda_client.invoke(
|
|
514
|
+
FunctionName='GenerateRouteLambda',
|
|
515
|
+
InvocationType='Event', # Asynchronous
|
|
516
|
+
Payload=json.dumps({
|
|
517
|
+
'data': {'shipping_id': shipping_id}
|
|
518
|
+
})
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
self.set_code(201)
|
|
522
|
+
self.set_body({
|
|
523
|
+
'shipping_id': shipping_id,
|
|
524
|
+
'status': 'pending',
|
|
525
|
+
'message': 'Shipping created, route generation in progress'
|
|
526
|
+
})
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**2. The standalone lambda** (`src/lambda/generate-route/main.py`):
|
|
530
|
+
|
|
531
|
+
```python
|
|
532
|
+
from aws_python_helper.lambda_standalone.base import Lambda
|
|
533
|
+
|
|
534
|
+
class GenerateRouteLambda(Lambda):
|
|
535
|
+
async def validate(self):
|
|
536
|
+
if 'shipping_id' not in self.data:
|
|
537
|
+
raise ValueError("shipping_id is required")
|
|
538
|
+
|
|
539
|
+
async def process(self):
|
|
540
|
+
shipping_id = self.data['shipping_id']
|
|
541
|
+
|
|
542
|
+
# Get shipping details
|
|
543
|
+
shipping = await self.db.deliveries.shippings.find_one(
|
|
544
|
+
{'_id': shipping_id}
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if not shipping:
|
|
548
|
+
raise ValueError(f"Shipping {shipping_id} not found")
|
|
549
|
+
|
|
550
|
+
# Generate optimal route
|
|
551
|
+
route = await self.calculate_optimal_route(shipping)
|
|
552
|
+
|
|
553
|
+
# Save route
|
|
554
|
+
route_result = await self.db.deliveries.routes.insert_one(route)
|
|
555
|
+
|
|
556
|
+
# Update shipping
|
|
557
|
+
await self.db.deliveries.shippings.update_one(
|
|
558
|
+
{'_id': shipping_id},
|
|
559
|
+
{'$set': {
|
|
560
|
+
'route_id': route_result.inserted_id,
|
|
561
|
+
'route_pending': False,
|
|
562
|
+
'status': 'scheduled'
|
|
563
|
+
}}
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
'route_id': str(route_result.inserted_id),
|
|
568
|
+
'shipping_id': shipping_id
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async def calculate_optimal_route(self, shipping):
|
|
572
|
+
# Your route calculation logic here
|
|
573
|
+
return {
|
|
574
|
+
'shipping_id': shipping['_id'],
|
|
575
|
+
'carrier_id': shipping.get('carrier_id'),
|
|
576
|
+
'estimated_duration': 60,
|
|
577
|
+
'status': 'pending'
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**3. Configure handlers** (`src/handlers/lambda_handler.py`):
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
from aws_python_helper.lambda_standalone.handler import lambda_handler
|
|
585
|
+
|
|
586
|
+
generate_route_handler = lambda_handler('generate-route')
|
|
587
|
+
|
|
588
|
+
__all__ = ['generate_route_handler']
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Benefits of this pattern:**
|
|
592
|
+
- API responds immediately (better UX)
|
|
593
|
+
- Route generation happens in the background
|
|
594
|
+
- Decoupled services (easier to maintain)
|
|
595
|
+
- Can retry lambda independently if it fails
|
|
596
|
+
- Scalable architecture
|
|
597
|
+
|
|
598
|
+
## 🔐 Environment Variables
|
|
599
|
+
|
|
600
|
+
### MongoDB Configuration
|
|
601
|
+
|
|
602
|
+
El framework soporta dos formas de configurar MongoDB:
|
|
603
|
+
|
|
604
|
+
#### Opción 1: Connection String Completa
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
# URI completa con credenciales incluidas
|
|
608
|
+
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/dbname?retryWrites=true&w=majority
|
|
609
|
+
# o
|
|
610
|
+
MONGO_DB_URI=mongodb+srv://user:password@cluster.mongodb.net/dbname
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
#### Opción 2: Componentes Separados (Recomendado para Terraform)
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
# Host sin credenciales
|
|
617
|
+
MONGO_DB_HOST=mongodb+srv://cluster.mongodb.net
|
|
618
|
+
|
|
619
|
+
# Credenciales separadas (más seguro)
|
|
620
|
+
MONGO_DB_USER=admin
|
|
621
|
+
MONGO_DB_PASSWORD=my-secure-password
|
|
622
|
+
|
|
623
|
+
# Opcionales
|
|
624
|
+
MONGO_DB_NAME=my_database
|
|
625
|
+
MONGO_DB_OPTIONS=retryWrites=true&w=majority
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Ventajas de usar componentes separados:**
|
|
629
|
+
- ✅ Mejor seguridad: credenciales separadas del host
|
|
630
|
+
- ✅ Fácil integración con Terraform/AWS Secrets Manager
|
|
631
|
+
- ✅ Contraseñas con caracteres especiales se manejan automáticamente
|
|
632
|
+
- ✅ Más flexible para diferentes entornos
|
|
633
|
+
|
|
634
|
+
El framework automáticamente:
|
|
635
|
+
1. URL-encodea la contraseña (maneja `@`, `:`, `/`, etc.)
|
|
636
|
+
2. Construye la URI completa
|
|
637
|
+
3. Inicializa la conexión
|
|
638
|
+
|
|
639
|
+
### Ejemplo en Terraform
|
|
640
|
+
|
|
641
|
+
```hcl
|
|
642
|
+
environment_variables = {
|
|
643
|
+
MONGO_DB_HOST = module.mongodb.connection_string
|
|
644
|
+
MONGO_DB_USER = module.mongodb.database_user
|
|
645
|
+
MONGO_DB_PASSWORD = module.mongodb.database_password
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
## Rest Environment Variables
|
|
650
|
+
|
|
651
|
+
## 📊 Advanced Features
|
|
652
|
+
|
|
653
|
+
### SNS Publisher - Batch Publishing
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
# Publish multiple messages
|
|
657
|
+
topic = TitleIndexedTopic()
|
|
658
|
+
await topic.publish_batch_indexed([
|
|
659
|
+
{'constitution_id': 'id1', 'title': 'Title 1'},
|
|
660
|
+
{'constitution_id': 'id2', 'title': 'Title 2'},
|
|
661
|
+
{'constitution_id': 'id3', 'title': 'Title 3'}
|
|
662
|
+
])
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Fargate - Run multiple tasks
|
|
666
|
+
|
|
667
|
+
```python
|
|
668
|
+
executor = FargateExecutor()
|
|
669
|
+
task_arns = executor.run_task_batch(
|
|
670
|
+
'search-tax-by-town',
|
|
671
|
+
[
|
|
672
|
+
{'town': 'Norwalk'},
|
|
673
|
+
{'town': 'Stamford'},
|
|
674
|
+
{'town': 'Bridgeport'}
|
|
675
|
+
]
|
|
676
|
+
)
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Fargate - Check task status
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
executor = FargateExecutor()
|
|
683
|
+
task_arn = executor.run_task('my-task', {'param': 'value'})
|
|
684
|
+
|
|
685
|
+
# Check task status
|
|
686
|
+
status = executor.get_task_status(task_arn)
|
|
687
|
+
print(f"Status: {status['status']}")
|
|
688
|
+
print(f"Started at: {status['started_at']}")
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### SNS - Message Attributes
|
|
692
|
+
|
|
693
|
+
```python
|
|
694
|
+
# Publish with attributes for SNS filtering
|
|
695
|
+
topic = ConstitutionCreatedTopic()
|
|
696
|
+
await topic.publish_created(
|
|
697
|
+
constitution_id='123',
|
|
698
|
+
title='New Constitution',
|
|
699
|
+
country='Ecuador',
|
|
700
|
+
year=2023,
|
|
701
|
+
created_by='user_456',
|
|
702
|
+
attributes={'priority': 'high', 'region': 'latam'}
|
|
703
|
+
)
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
## 🤝 Contributing
|
|
707
|
+
|
|
708
|
+
If you find bugs or want to add features, please create a PR!
|
|
709
|
+
|
|
710
|
+
## 📄 License
|
|
711
|
+
|
|
712
|
+
MIT
|