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.
Files changed (42) hide show
  1. aws_python_helper-0.23.0/PKG-INFO +712 -0
  2. aws_python_helper-0.23.0/README.md +692 -0
  3. aws_python_helper-0.23.0/aws_python_helper/__init__.py +45 -0
  4. aws_python_helper-0.23.0/aws_python_helper/api/__init__.py +11 -0
  5. aws_python_helper-0.23.0/aws_python_helper/api/auth_middleware.py +108 -0
  6. aws_python_helper-0.23.0/aws_python_helper/api/auth_validators.py +143 -0
  7. aws_python_helper-0.23.0/aws_python_helper/api/base.py +272 -0
  8. aws_python_helper-0.23.0/aws_python_helper/api/dispatcher.py +213 -0
  9. aws_python_helper-0.23.0/aws_python_helper/api/exceptions.py +43 -0
  10. aws_python_helper-0.23.0/aws_python_helper/api/fetcher.py +210 -0
  11. aws_python_helper-0.23.0/aws_python_helper/api/handler.py +106 -0
  12. aws_python_helper-0.23.0/aws_python_helper/database/__init__.py +11 -0
  13. aws_python_helper-0.23.0/aws_python_helper/database/database_proxy.py +50 -0
  14. aws_python_helper-0.23.0/aws_python_helper/database/external_database_proxy.py +66 -0
  15. aws_python_helper-0.23.0/aws_python_helper/database/external_mongo_manager.py +212 -0
  16. aws_python_helper-0.23.0/aws_python_helper/database/mongo_manager.py +214 -0
  17. aws_python_helper-0.23.0/aws_python_helper/fargate/__init__.py +9 -0
  18. aws_python_helper-0.23.0/aws_python_helper/fargate/executor.py +226 -0
  19. aws_python_helper-0.23.0/aws_python_helper/fargate/fetcher.py +108 -0
  20. aws_python_helper-0.23.0/aws_python_helper/fargate/handler.py +101 -0
  21. aws_python_helper-0.23.0/aws_python_helper/fargate/task_base.py +165 -0
  22. aws_python_helper-0.23.0/aws_python_helper/lambda_standalone/__init__.py +8 -0
  23. aws_python_helper-0.23.0/aws_python_helper/lambda_standalone/base.py +171 -0
  24. aws_python_helper-0.23.0/aws_python_helper/lambda_standalone/fetcher.py +122 -0
  25. aws_python_helper-0.23.0/aws_python_helper/lambda_standalone/handler.py +117 -0
  26. aws_python_helper-0.23.0/aws_python_helper/sns/__init__.py +6 -0
  27. aws_python_helper-0.23.0/aws_python_helper/sns/publisher.py +245 -0
  28. aws_python_helper-0.23.0/aws_python_helper/sqs/__init__.py +10 -0
  29. aws_python_helper-0.23.0/aws_python_helper/sqs/consumer_base.py +416 -0
  30. aws_python_helper-0.23.0/aws_python_helper/sqs/fetcher.py +111 -0
  31. aws_python_helper-0.23.0/aws_python_helper/sqs/handler.py +138 -0
  32. aws_python_helper-0.23.0/aws_python_helper/utils/__init__.py +9 -0
  33. aws_python_helper-0.23.0/aws_python_helper/utils/json_encoder.py +108 -0
  34. aws_python_helper-0.23.0/aws_python_helper/utils/response.py +145 -0
  35. aws_python_helper-0.23.0/aws_python_helper/utils/serializer.py +103 -0
  36. aws_python_helper-0.23.0/aws_python_helper.egg-info/PKG-INFO +712 -0
  37. aws_python_helper-0.23.0/aws_python_helper.egg-info/SOURCES.txt +40 -0
  38. aws_python_helper-0.23.0/aws_python_helper.egg-info/dependency_links.txt +1 -0
  39. aws_python_helper-0.23.0/aws_python_helper.egg-info/requires.txt +3 -0
  40. aws_python_helper-0.23.0/aws_python_helper.egg-info/top_level.txt +1 -0
  41. aws_python_helper-0.23.0/pyproject.toml +42 -0
  42. 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