aws-python-helper 1.14.0__tar.gz → 1.15.1__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-1.14.0 → aws_python_helper-1.15.1}/PKG-INFO +49 -49
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/README.md +48 -48
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/__init__.py +2 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/auth_validators.py +9 -3
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/base.py +3 -3
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/dispatcher.py +14 -2
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/context/session.py +2 -2
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/database/external_database_proxy.py +4 -4
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/database/external_mongo_manager.py +1 -1
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/fargate/task_base.py +2 -2
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/invoker/api_client.py +4 -4
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/lambda_standalone/base.py +2 -2
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/model_query/base.py +5 -5
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/permissions/catalog.py +1 -1
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/repository/audit.py +1 -1
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/repository/base.py +4 -4
- aws_python_helper-1.15.1/aws_python_helper/settings.py +118 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/sqs/consumer_base.py +2 -2
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper.egg-info/PKG-INFO +49 -49
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper.egg-info/SOURCES.txt +2 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/pyproject.toml +1 -1
- aws_python_helper-1.15.1/tests/test_settings.py +92 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/jwt_keys.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/context/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/context/state_validator.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/fargate/executor.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/invoker/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/invoker/exceptions.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/invoker/lambda_invoker.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/model_index_sync/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/model_index_sync/base.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/model_query/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/monitoring/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/monitoring/run.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/permissions/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/sns/publisher.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/setup.cfg +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/tests/test_audit_stamp.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/tests/test_jwt_keys.py +0 -0
- {aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/tests/test_sns_publisher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-python-helper
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.1
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -106,7 +106,7 @@ your-project/
|
|
|
106
106
|
│
|
|
107
107
|
├── consumer/ # SQS Consumers (direct files)
|
|
108
108
|
│ ├── user_created.py # user-created -> UserCreatedConsumer
|
|
109
|
-
│ ├──
|
|
109
|
+
│ ├── order_created.py # title-indexed -> TitleIndexedConsumer
|
|
110
110
|
│ └── order_processed.py # order-processed -> OrderProcessedConsumer
|
|
111
111
|
│
|
|
112
112
|
├── lambda/ # Standalone Lambdas (folders)
|
|
@@ -151,22 +151,22 @@ The framework uses automatic class name detection based on your folder/file stru
|
|
|
151
151
|
|
|
152
152
|
### Create an Endpoint
|
|
153
153
|
|
|
154
|
-
**1. Create your API class** in `src/api/
|
|
154
|
+
**1. Create your API class** in `src/api/orders/list.py`:
|
|
155
155
|
|
|
156
156
|
```python
|
|
157
157
|
from aws_python_helper.api.base import API
|
|
158
158
|
|
|
159
|
-
class
|
|
159
|
+
class OrdersListAPI(API):
|
|
160
160
|
async def process(self):
|
|
161
161
|
# Direct access to MongoDB
|
|
162
|
-
|
|
163
|
-
self.set_body(
|
|
162
|
+
orders = await self.db.app_db.orders.find().to_list(100)
|
|
163
|
+
self.set_body(orders)
|
|
164
164
|
```
|
|
165
165
|
|
|
166
166
|
**2. The routing is automatic:**
|
|
167
|
-
- `GET /
|
|
168
|
-
- `GET /
|
|
169
|
-
- `POST /
|
|
167
|
+
- `GET /orders` → `src/api/orders/list.py`
|
|
168
|
+
- `GET /orders/123` → `src/api/orders/get.py`
|
|
169
|
+
- `POST /orders` → `src/api/orders/post.py`
|
|
170
170
|
|
|
171
171
|
**3. Configure the generic handler** (`src/handlers/api_handler.py`):
|
|
172
172
|
|
|
@@ -177,7 +177,7 @@ handler = api_handler
|
|
|
177
177
|
|
|
178
178
|
### Create an SQS Consumer
|
|
179
179
|
|
|
180
|
-
**1. Create your consumer** in `src/consumer/
|
|
180
|
+
**1. Create your consumer** in `src/consumer/order_created.py`:
|
|
181
181
|
|
|
182
182
|
```python
|
|
183
183
|
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
@@ -186,7 +186,7 @@ class TitleIndexedConsumer(SQSConsumer):
|
|
|
186
186
|
async def process_record(self, record):
|
|
187
187
|
body = self.extract_content_message(record)
|
|
188
188
|
# Your logic here
|
|
189
|
-
await self.db.
|
|
189
|
+
await self.db.app_db.invoices.insert_one(body)
|
|
190
190
|
```
|
|
191
191
|
|
|
192
192
|
**2. Configure the handler** in `src/handlers/sqs_handler.py`:
|
|
@@ -195,9 +195,9 @@ class TitleIndexedConsumer(SQSConsumer):
|
|
|
195
195
|
from aws_python_helper.sqs.handler import sqs_handler
|
|
196
196
|
|
|
197
197
|
# Create a handler for each consumer and export it
|
|
198
|
-
|
|
198
|
+
order_created_handler = sqs_handler('title-indexed')
|
|
199
199
|
|
|
200
|
-
__all__ = ['
|
|
200
|
+
__all__ = ['order_created_handler']
|
|
201
201
|
```
|
|
202
202
|
|
|
203
203
|
### Create a Standalone Lambda
|
|
@@ -352,7 +352,7 @@ lambda_client.invoke(
|
|
|
352
352
|
|
|
353
353
|
### Publish to SNS
|
|
354
354
|
|
|
355
|
-
**1. Create your topic** in `src/topic/
|
|
355
|
+
**1. Create your topic** in `src/topic/order_created.py`:
|
|
356
356
|
|
|
357
357
|
```python
|
|
358
358
|
from aws_python_helper.sns.publisher import SNSPublisher
|
|
@@ -364,10 +364,10 @@ class TitleIndexedTopic(SNSPublisher):
|
|
|
364
364
|
topic_arn=os.getenv('TITLE_INDEXED_SNS_TOPIC_ARN')
|
|
365
365
|
)
|
|
366
366
|
|
|
367
|
-
def build_message(self,
|
|
367
|
+
def build_message(self, order_id, title, event_type='order_created'):
|
|
368
368
|
return {
|
|
369
369
|
'content': {
|
|
370
|
-
'
|
|
370
|
+
'order_id': order_id,
|
|
371
371
|
'title': title,
|
|
372
372
|
'event_type': event_type
|
|
373
373
|
},
|
|
@@ -380,18 +380,18 @@ class TitleIndexedTopic(SNSPublisher):
|
|
|
380
380
|
**2. Use the topic** from anywhere:
|
|
381
381
|
|
|
382
382
|
```python
|
|
383
|
-
from src.topic.
|
|
383
|
+
from src.topic.order_created import TitleIndexedTopic
|
|
384
384
|
|
|
385
385
|
# In a consumer, API or task
|
|
386
386
|
topic = TitleIndexedTopic()
|
|
387
387
|
|
|
388
388
|
# Publish a single message
|
|
389
|
-
await topic.publish(topic.build_message('123', '
|
|
389
|
+
await topic.publish(topic.build_message('123', 'Order 123'))
|
|
390
390
|
|
|
391
391
|
# Publish multiple messages in batch
|
|
392
392
|
messages = [
|
|
393
|
-
topic.build_message('id1', '
|
|
394
|
-
topic.build_message('id2', '
|
|
393
|
+
topic.build_message('id1', 'Order A'),
|
|
394
|
+
topic.build_message('id2', 'Order B'),
|
|
395
395
|
]
|
|
396
396
|
await topic.publish(messages)
|
|
397
397
|
```
|
|
@@ -420,7 +420,7 @@ class SearchTaxByTownTask(FargateTask):
|
|
|
420
420
|
self.logger.info(f"Processing town: {town}")
|
|
421
421
|
|
|
422
422
|
# Access to DB
|
|
423
|
-
docs = await self.db.
|
|
423
|
+
docs = await self.db.analytics.address.find({'town': town}).to_list()
|
|
424
424
|
|
|
425
425
|
# Your logic here
|
|
426
426
|
for doc in docs:
|
|
@@ -490,8 +490,8 @@ class MyAPI(API):
|
|
|
490
490
|
await self.db.analytics_db.logs.insert_one({'action': 'view'})
|
|
491
491
|
|
|
492
492
|
# Multiple collections
|
|
493
|
-
titles = await self.db.
|
|
494
|
-
articles = await self.db.
|
|
493
|
+
titles = await self.db.app_db.invoices.find().to_list(100)
|
|
494
|
+
articles = await self.db.app_db.line_items.find().to_list(100)
|
|
495
495
|
```
|
|
496
496
|
|
|
497
497
|
The pattern is always: `self.db.<database_name>.<collection_name>.<motor_operation>()`
|
|
@@ -502,7 +502,7 @@ Connect to additional MongoDB clusters using `EXTERNAL_MONGODB_CONNECTIONS`:
|
|
|
502
502
|
|
|
503
503
|
```bash
|
|
504
504
|
EXTERNAL_MONGODB_CONNECTIONS='[
|
|
505
|
-
{"name": "
|
|
505
|
+
{"name": "AnalyticsCluster", "connection_string": "mongodb+srv://cluster.mongodb.net"},
|
|
506
506
|
{"name": "ClusterAnalytics", "connection_string": "mongodb+srv://analytics.mongodb.net"}
|
|
507
507
|
]'
|
|
508
508
|
```
|
|
@@ -515,7 +515,7 @@ Access external clusters via `self.external_db`:
|
|
|
515
515
|
class AddressAPI(API):
|
|
516
516
|
async def process(self):
|
|
517
517
|
# Access external cluster: self.external_db.<ClusterName>.<database>.<collection>
|
|
518
|
-
addresses = await self.external_db.
|
|
518
|
+
addresses = await self.external_db.AnalyticsCluster.analytics.addresses.find(
|
|
519
519
|
{'town': self.data['town']}
|
|
520
520
|
).to_list(100)
|
|
521
521
|
|
|
@@ -646,7 +646,7 @@ class AddressRepository(Repository):
|
|
|
646
646
|
|
|
647
647
|
@property
|
|
648
648
|
def database_key(self):
|
|
649
|
-
return "
|
|
649
|
+
return "analytics"
|
|
650
650
|
|
|
651
651
|
@property
|
|
652
652
|
def collection_name(self):
|
|
@@ -658,7 +658,7 @@ class AddressRepository(Repository):
|
|
|
658
658
|
|
|
659
659
|
@property
|
|
660
660
|
def cluster_name(self):
|
|
661
|
-
return "
|
|
661
|
+
return "AnalyticsCluster" # Must match a name in EXTERNAL_MONGODB_CONNECTIONS
|
|
662
662
|
|
|
663
663
|
async def find_by_query(self, query, limit=None):
|
|
664
664
|
cursor = self.collection.find(query)
|
|
@@ -693,7 +693,7 @@ The framework propagates a `Session` object automatically across the entire asyn
|
|
|
693
693
|
|
|
694
694
|
| Entry point | How the session is read |
|
|
695
695
|
|-------------|-------------------------|
|
|
696
|
-
| **API Gateway** | `
|
|
696
|
+
| **API Gateway** | `STATE_HEADER` header → `session.state` (when `AUTHORIZATION` includes `state`); auth middleware → `session.user` (when includes `user`). Returns `400` if required header is missing |
|
|
697
697
|
| **Standalone Lambda** | `session` dict in the event payload — **required by default** (must include `state`), raises `ValueError` if missing. A Lambda can opt out via `requires_state = False` (e.g. `ModelIndexSyncLambda`), making the session optional |
|
|
698
698
|
| **SQS Consumer (single mode)** | Per-record: reads `session` from SNS `MessageAttributes` (Base64-encoded JSON) |
|
|
699
699
|
| **SQS Consumer (batch mode)** | Groups records by `session.state`; calls `process_batch()` once per group with the correct session in context |
|
|
@@ -706,7 +706,7 @@ The framework propagates a `Session` object automatically across the entire asyn
|
|
|
706
706
|
| **SNS Publisher** | Auto-injects the full session as a `session` `MessageAttribute` (Base64-encoded JSON) on every published message |
|
|
707
707
|
| **FargateExecutor** | Auto-injects `SESSION` as a JSON env var when launching Fargate containers |
|
|
708
708
|
|
|
709
|
-
This means that an API call with `
|
|
709
|
+
This means that an API call with `x-state: connecticut` will automatically carry the full session (state + user) through SNS → SQS → Fargate without any code changes in your consumers or tasks.
|
|
710
710
|
|
|
711
711
|
### Accessing the session in handlers
|
|
712
712
|
|
|
@@ -739,11 +739,11 @@ session.user # authenticated user dict, or None
|
|
|
739
739
|
set_session(Session(state="new_jersey")) # set manually (the framework does this automatically)
|
|
740
740
|
```
|
|
741
741
|
|
|
742
|
-
### API example —
|
|
742
|
+
### API example — the state header
|
|
743
743
|
|
|
744
744
|
```
|
|
745
|
-
GET /
|
|
746
|
-
|
|
745
|
+
GET /orders HTTP/1.1
|
|
746
|
+
x-state: connecticut
|
|
747
747
|
Authorization: Bearer <token>
|
|
748
748
|
```
|
|
749
749
|
|
|
@@ -888,10 +888,10 @@ class AdminOnlyAPI(API):
|
|
|
888
888
|
## 🎯 Complete Example
|
|
889
889
|
|
|
890
890
|
```python
|
|
891
|
-
# src/api/
|
|
891
|
+
# src/api/orders/list.py
|
|
892
892
|
from aws_python_helper.api.base import API
|
|
893
893
|
|
|
894
|
-
class
|
|
894
|
+
class OrdersListAPI(API):
|
|
895
895
|
async def validate(self):
|
|
896
896
|
if 'limit' in self.data:
|
|
897
897
|
limit = int(self.data['limit'])
|
|
@@ -906,12 +906,12 @@ class ConstitutionListAPI(API):
|
|
|
906
906
|
|
|
907
907
|
# Query MongoDB
|
|
908
908
|
limit = int(self.data.get('limit', 100))
|
|
909
|
-
results = await self.db.
|
|
909
|
+
results = await self.db.app_db.orders.find(
|
|
910
910
|
filters
|
|
911
911
|
).limit(limit).to_list(limit)
|
|
912
912
|
|
|
913
913
|
# Count total
|
|
914
|
-
total = await self.db.
|
|
914
|
+
total = await self.db.app_db.orders.count_documents(filters)
|
|
915
915
|
|
|
916
916
|
# Register in analytics
|
|
917
917
|
await self.db.analytics_db.searches.insert_one({
|
|
@@ -1142,23 +1142,23 @@ HTTP client for service-to-service calls. Resolves the target URL and auth token
|
|
|
1142
1142
|
```python
|
|
1143
1143
|
from aws_python_helper import ApiClient
|
|
1144
1144
|
|
|
1145
|
-
client = ApiClient("
|
|
1145
|
+
client = ApiClient("billing") # service name only
|
|
1146
1146
|
result = await client.post("/search", body={"keys": [...]})
|
|
1147
|
-
result = await client.get("/
|
|
1148
|
-
result = await client.list("/
|
|
1149
|
-
result = await client.put("/
|
|
1150
|
-
result = await client.patch("/
|
|
1151
|
-
result = await client.delete("/
|
|
1147
|
+
result = await client.get("/invoices/123")
|
|
1148
|
+
result = await client.list("/invoices", params={"state": "CT"})
|
|
1149
|
+
result = await client.put("/invoices/123", body={"status": "active"})
|
|
1150
|
+
result = await client.patch("/invoices/123", body={"reviewed": True})
|
|
1151
|
+
result = await client.delete("/invoices/123")
|
|
1152
1152
|
|
|
1153
1153
|
# Extra headers merged with the defaults (token is always injected automatically)
|
|
1154
|
-
client = ApiClient("
|
|
1154
|
+
client = ApiClient("billing", headers={"x-tenant": "acme"})
|
|
1155
1155
|
```
|
|
1156
1156
|
|
|
1157
1157
|
**How URL and token are resolved:**
|
|
1158
1158
|
|
|
1159
1159
|
| Env var | Purpose |
|
|
1160
1160
|
|---------|---------|
|
|
1161
|
-
| `MICROSERVICE_URLS` | JSON map: `{"
|
|
1161
|
+
| `MICROSERVICE_URLS` | JSON map: `{"billing": "https://api.example.com/", "catalog": "https://..."}` |
|
|
1162
1162
|
| `INTER_SERVICE_TOKEN` | Bearer token injected as `Authorization: Bearer <token>` |
|
|
1163
1163
|
| `AUTH_BYPASS_TOKEN` | Fallback if `INTER_SERVICE_TOKEN` is not set |
|
|
1164
1164
|
|
|
@@ -1176,7 +1176,7 @@ client = ApiClient("dockets", headers={"constitution-state": "CT"})
|
|
|
1176
1176
|
# In AWS Secrets Manager secret (JSON):
|
|
1177
1177
|
# {
|
|
1178
1178
|
# "inter_service_token": "my-secret-token",
|
|
1179
|
-
# "microservice_urls": "{\"
|
|
1179
|
+
# "microservice_urls": "{\"billing\": \"https://api.example.com/\"}",
|
|
1180
1180
|
# ...
|
|
1181
1181
|
# }
|
|
1182
1182
|
|
|
@@ -1596,7 +1596,7 @@ environment_variables = {
|
|
|
1596
1596
|
| `MICROSERVICE_URLS` | Optional | JSON map of service name → base URL used by `ApiClient` |
|
|
1597
1597
|
| `ECS_CLUSTER` | Fargate only | ECS cluster name for `FargateExecutor` |
|
|
1598
1598
|
| `ECS_SUBNETS` | Fargate only | Comma-separated subnet IDs for Fargate tasks |
|
|
1599
|
-
| `
|
|
1599
|
+
| `SESSION` | Fargate only (auto) | The whole session, injected by `FargateExecutor` — do not set manually |
|
|
1600
1600
|
| `AWS_REGION` | Fargate/SNS/SQS | AWS region |
|
|
1601
1601
|
| `AWS_ACCOUNT_ID` | SQS `get_queue_url` | AWS account ID |
|
|
1602
1602
|
| `SERVICE_NAME` | SQS `get_queue_url` | Service name prefix for queue name |
|
|
@@ -1609,9 +1609,9 @@ environment_variables = {
|
|
|
1609
1609
|
|
|
1610
1610
|
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages.
|
|
1611
1611
|
|
|
1612
|
-
**
|
|
1612
|
+
**State handling in SQS:**
|
|
1613
1613
|
- **Single mode**: the framework extracts the session from each record automatically (from SNS `MessageAttributes`, Base64-decoded) and sets it in context before calling `process_record()`. You do not need to extract it yourself.
|
|
1614
|
-
- **Batch mode**: the framework groups the incoming records by
|
|
1614
|
+
- **Batch mode**: the framework groups the incoming records by state and calls `process_batch()` once per group, with the correct state in context for each group. This ensures that state-scoped repositories resolve to the right database even when a batch contains records from different states.
|
|
1615
1615
|
|
|
1616
1616
|
```python
|
|
1617
1617
|
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
@@ -1666,7 +1666,7 @@ The `SNSPublisher` automatically injects the current session as a Base64-encoded
|
|
|
1666
1666
|
topic = TitleIndexedTopic()
|
|
1667
1667
|
|
|
1668
1668
|
# Publish multiple messages in a single call
|
|
1669
|
-
#
|
|
1669
|
+
# The state travels as a MessageAttribute on each message, injected automatically
|
|
1670
1670
|
await topic.publish([
|
|
1671
1671
|
{'content': {'id': 'id1', 'title': 'Title 1'}, 'attributes': {'type': 'created'}},
|
|
1672
1672
|
{'content': {'id': 'id2', 'title': 'Title 2'}, 'attributes': {'type': 'updated'}},
|
|
@@ -84,7 +84,7 @@ your-project/
|
|
|
84
84
|
│
|
|
85
85
|
├── consumer/ # SQS Consumers (direct files)
|
|
86
86
|
│ ├── user_created.py # user-created -> UserCreatedConsumer
|
|
87
|
-
│ ├──
|
|
87
|
+
│ ├── order_created.py # title-indexed -> TitleIndexedConsumer
|
|
88
88
|
│ └── order_processed.py # order-processed -> OrderProcessedConsumer
|
|
89
89
|
│
|
|
90
90
|
├── lambda/ # Standalone Lambdas (folders)
|
|
@@ -129,22 +129,22 @@ The framework uses automatic class name detection based on your folder/file stru
|
|
|
129
129
|
|
|
130
130
|
### Create an Endpoint
|
|
131
131
|
|
|
132
|
-
**1. Create your API class** in `src/api/
|
|
132
|
+
**1. Create your API class** in `src/api/orders/list.py`:
|
|
133
133
|
|
|
134
134
|
```python
|
|
135
135
|
from aws_python_helper.api.base import API
|
|
136
136
|
|
|
137
|
-
class
|
|
137
|
+
class OrdersListAPI(API):
|
|
138
138
|
async def process(self):
|
|
139
139
|
# Direct access to MongoDB
|
|
140
|
-
|
|
141
|
-
self.set_body(
|
|
140
|
+
orders = await self.db.app_db.orders.find().to_list(100)
|
|
141
|
+
self.set_body(orders)
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
**2. The routing is automatic:**
|
|
145
|
-
- `GET /
|
|
146
|
-
- `GET /
|
|
147
|
-
- `POST /
|
|
145
|
+
- `GET /orders` → `src/api/orders/list.py`
|
|
146
|
+
- `GET /orders/123` → `src/api/orders/get.py`
|
|
147
|
+
- `POST /orders` → `src/api/orders/post.py`
|
|
148
148
|
|
|
149
149
|
**3. Configure the generic handler** (`src/handlers/api_handler.py`):
|
|
150
150
|
|
|
@@ -155,7 +155,7 @@ handler = api_handler
|
|
|
155
155
|
|
|
156
156
|
### Create an SQS Consumer
|
|
157
157
|
|
|
158
|
-
**1. Create your consumer** in `src/consumer/
|
|
158
|
+
**1. Create your consumer** in `src/consumer/order_created.py`:
|
|
159
159
|
|
|
160
160
|
```python
|
|
161
161
|
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
@@ -164,7 +164,7 @@ class TitleIndexedConsumer(SQSConsumer):
|
|
|
164
164
|
async def process_record(self, record):
|
|
165
165
|
body = self.extract_content_message(record)
|
|
166
166
|
# Your logic here
|
|
167
|
-
await self.db.
|
|
167
|
+
await self.db.app_db.invoices.insert_one(body)
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
**2. Configure the handler** in `src/handlers/sqs_handler.py`:
|
|
@@ -173,9 +173,9 @@ class TitleIndexedConsumer(SQSConsumer):
|
|
|
173
173
|
from aws_python_helper.sqs.handler import sqs_handler
|
|
174
174
|
|
|
175
175
|
# Create a handler for each consumer and export it
|
|
176
|
-
|
|
176
|
+
order_created_handler = sqs_handler('title-indexed')
|
|
177
177
|
|
|
178
|
-
__all__ = ['
|
|
178
|
+
__all__ = ['order_created_handler']
|
|
179
179
|
```
|
|
180
180
|
|
|
181
181
|
### Create a Standalone Lambda
|
|
@@ -330,7 +330,7 @@ lambda_client.invoke(
|
|
|
330
330
|
|
|
331
331
|
### Publish to SNS
|
|
332
332
|
|
|
333
|
-
**1. Create your topic** in `src/topic/
|
|
333
|
+
**1. Create your topic** in `src/topic/order_created.py`:
|
|
334
334
|
|
|
335
335
|
```python
|
|
336
336
|
from aws_python_helper.sns.publisher import SNSPublisher
|
|
@@ -342,10 +342,10 @@ class TitleIndexedTopic(SNSPublisher):
|
|
|
342
342
|
topic_arn=os.getenv('TITLE_INDEXED_SNS_TOPIC_ARN')
|
|
343
343
|
)
|
|
344
344
|
|
|
345
|
-
def build_message(self,
|
|
345
|
+
def build_message(self, order_id, title, event_type='order_created'):
|
|
346
346
|
return {
|
|
347
347
|
'content': {
|
|
348
|
-
'
|
|
348
|
+
'order_id': order_id,
|
|
349
349
|
'title': title,
|
|
350
350
|
'event_type': event_type
|
|
351
351
|
},
|
|
@@ -358,18 +358,18 @@ class TitleIndexedTopic(SNSPublisher):
|
|
|
358
358
|
**2. Use the topic** from anywhere:
|
|
359
359
|
|
|
360
360
|
```python
|
|
361
|
-
from src.topic.
|
|
361
|
+
from src.topic.order_created import TitleIndexedTopic
|
|
362
362
|
|
|
363
363
|
# In a consumer, API or task
|
|
364
364
|
topic = TitleIndexedTopic()
|
|
365
365
|
|
|
366
366
|
# Publish a single message
|
|
367
|
-
await topic.publish(topic.build_message('123', '
|
|
367
|
+
await topic.publish(topic.build_message('123', 'Order 123'))
|
|
368
368
|
|
|
369
369
|
# Publish multiple messages in batch
|
|
370
370
|
messages = [
|
|
371
|
-
topic.build_message('id1', '
|
|
372
|
-
topic.build_message('id2', '
|
|
371
|
+
topic.build_message('id1', 'Order A'),
|
|
372
|
+
topic.build_message('id2', 'Order B'),
|
|
373
373
|
]
|
|
374
374
|
await topic.publish(messages)
|
|
375
375
|
```
|
|
@@ -398,7 +398,7 @@ class SearchTaxByTownTask(FargateTask):
|
|
|
398
398
|
self.logger.info(f"Processing town: {town}")
|
|
399
399
|
|
|
400
400
|
# Access to DB
|
|
401
|
-
docs = await self.db.
|
|
401
|
+
docs = await self.db.analytics.address.find({'town': town}).to_list()
|
|
402
402
|
|
|
403
403
|
# Your logic here
|
|
404
404
|
for doc in docs:
|
|
@@ -468,8 +468,8 @@ class MyAPI(API):
|
|
|
468
468
|
await self.db.analytics_db.logs.insert_one({'action': 'view'})
|
|
469
469
|
|
|
470
470
|
# Multiple collections
|
|
471
|
-
titles = await self.db.
|
|
472
|
-
articles = await self.db.
|
|
471
|
+
titles = await self.db.app_db.invoices.find().to_list(100)
|
|
472
|
+
articles = await self.db.app_db.line_items.find().to_list(100)
|
|
473
473
|
```
|
|
474
474
|
|
|
475
475
|
The pattern is always: `self.db.<database_name>.<collection_name>.<motor_operation>()`
|
|
@@ -480,7 +480,7 @@ Connect to additional MongoDB clusters using `EXTERNAL_MONGODB_CONNECTIONS`:
|
|
|
480
480
|
|
|
481
481
|
```bash
|
|
482
482
|
EXTERNAL_MONGODB_CONNECTIONS='[
|
|
483
|
-
{"name": "
|
|
483
|
+
{"name": "AnalyticsCluster", "connection_string": "mongodb+srv://cluster.mongodb.net"},
|
|
484
484
|
{"name": "ClusterAnalytics", "connection_string": "mongodb+srv://analytics.mongodb.net"}
|
|
485
485
|
]'
|
|
486
486
|
```
|
|
@@ -493,7 +493,7 @@ Access external clusters via `self.external_db`:
|
|
|
493
493
|
class AddressAPI(API):
|
|
494
494
|
async def process(self):
|
|
495
495
|
# Access external cluster: self.external_db.<ClusterName>.<database>.<collection>
|
|
496
|
-
addresses = await self.external_db.
|
|
496
|
+
addresses = await self.external_db.AnalyticsCluster.analytics.addresses.find(
|
|
497
497
|
{'town': self.data['town']}
|
|
498
498
|
).to_list(100)
|
|
499
499
|
|
|
@@ -624,7 +624,7 @@ class AddressRepository(Repository):
|
|
|
624
624
|
|
|
625
625
|
@property
|
|
626
626
|
def database_key(self):
|
|
627
|
-
return "
|
|
627
|
+
return "analytics"
|
|
628
628
|
|
|
629
629
|
@property
|
|
630
630
|
def collection_name(self):
|
|
@@ -636,7 +636,7 @@ class AddressRepository(Repository):
|
|
|
636
636
|
|
|
637
637
|
@property
|
|
638
638
|
def cluster_name(self):
|
|
639
|
-
return "
|
|
639
|
+
return "AnalyticsCluster" # Must match a name in EXTERNAL_MONGODB_CONNECTIONS
|
|
640
640
|
|
|
641
641
|
async def find_by_query(self, query, limit=None):
|
|
642
642
|
cursor = self.collection.find(query)
|
|
@@ -671,7 +671,7 @@ The framework propagates a `Session` object automatically across the entire asyn
|
|
|
671
671
|
|
|
672
672
|
| Entry point | How the session is read |
|
|
673
673
|
|-------------|-------------------------|
|
|
674
|
-
| **API Gateway** | `
|
|
674
|
+
| **API Gateway** | `STATE_HEADER` header → `session.state` (when `AUTHORIZATION` includes `state`); auth middleware → `session.user` (when includes `user`). Returns `400` if required header is missing |
|
|
675
675
|
| **Standalone Lambda** | `session` dict in the event payload — **required by default** (must include `state`), raises `ValueError` if missing. A Lambda can opt out via `requires_state = False` (e.g. `ModelIndexSyncLambda`), making the session optional |
|
|
676
676
|
| **SQS Consumer (single mode)** | Per-record: reads `session` from SNS `MessageAttributes` (Base64-encoded JSON) |
|
|
677
677
|
| **SQS Consumer (batch mode)** | Groups records by `session.state`; calls `process_batch()` once per group with the correct session in context |
|
|
@@ -684,7 +684,7 @@ The framework propagates a `Session` object automatically across the entire asyn
|
|
|
684
684
|
| **SNS Publisher** | Auto-injects the full session as a `session` `MessageAttribute` (Base64-encoded JSON) on every published message |
|
|
685
685
|
| **FargateExecutor** | Auto-injects `SESSION` as a JSON env var when launching Fargate containers |
|
|
686
686
|
|
|
687
|
-
This means that an API call with `
|
|
687
|
+
This means that an API call with `x-state: connecticut` will automatically carry the full session (state + user) through SNS → SQS → Fargate without any code changes in your consumers or tasks.
|
|
688
688
|
|
|
689
689
|
### Accessing the session in handlers
|
|
690
690
|
|
|
@@ -717,11 +717,11 @@ session.user # authenticated user dict, or None
|
|
|
717
717
|
set_session(Session(state="new_jersey")) # set manually (the framework does this automatically)
|
|
718
718
|
```
|
|
719
719
|
|
|
720
|
-
### API example —
|
|
720
|
+
### API example — the state header
|
|
721
721
|
|
|
722
722
|
```
|
|
723
|
-
GET /
|
|
724
|
-
|
|
723
|
+
GET /orders HTTP/1.1
|
|
724
|
+
x-state: connecticut
|
|
725
725
|
Authorization: Bearer <token>
|
|
726
726
|
```
|
|
727
727
|
|
|
@@ -866,10 +866,10 @@ class AdminOnlyAPI(API):
|
|
|
866
866
|
## 🎯 Complete Example
|
|
867
867
|
|
|
868
868
|
```python
|
|
869
|
-
# src/api/
|
|
869
|
+
# src/api/orders/list.py
|
|
870
870
|
from aws_python_helper.api.base import API
|
|
871
871
|
|
|
872
|
-
class
|
|
872
|
+
class OrdersListAPI(API):
|
|
873
873
|
async def validate(self):
|
|
874
874
|
if 'limit' in self.data:
|
|
875
875
|
limit = int(self.data['limit'])
|
|
@@ -884,12 +884,12 @@ class ConstitutionListAPI(API):
|
|
|
884
884
|
|
|
885
885
|
# Query MongoDB
|
|
886
886
|
limit = int(self.data.get('limit', 100))
|
|
887
|
-
results = await self.db.
|
|
887
|
+
results = await self.db.app_db.orders.find(
|
|
888
888
|
filters
|
|
889
889
|
).limit(limit).to_list(limit)
|
|
890
890
|
|
|
891
891
|
# Count total
|
|
892
|
-
total = await self.db.
|
|
892
|
+
total = await self.db.app_db.orders.count_documents(filters)
|
|
893
893
|
|
|
894
894
|
# Register in analytics
|
|
895
895
|
await self.db.analytics_db.searches.insert_one({
|
|
@@ -1120,23 +1120,23 @@ HTTP client for service-to-service calls. Resolves the target URL and auth token
|
|
|
1120
1120
|
```python
|
|
1121
1121
|
from aws_python_helper import ApiClient
|
|
1122
1122
|
|
|
1123
|
-
client = ApiClient("
|
|
1123
|
+
client = ApiClient("billing") # service name only
|
|
1124
1124
|
result = await client.post("/search", body={"keys": [...]})
|
|
1125
|
-
result = await client.get("/
|
|
1126
|
-
result = await client.list("/
|
|
1127
|
-
result = await client.put("/
|
|
1128
|
-
result = await client.patch("/
|
|
1129
|
-
result = await client.delete("/
|
|
1125
|
+
result = await client.get("/invoices/123")
|
|
1126
|
+
result = await client.list("/invoices", params={"state": "CT"})
|
|
1127
|
+
result = await client.put("/invoices/123", body={"status": "active"})
|
|
1128
|
+
result = await client.patch("/invoices/123", body={"reviewed": True})
|
|
1129
|
+
result = await client.delete("/invoices/123")
|
|
1130
1130
|
|
|
1131
1131
|
# Extra headers merged with the defaults (token is always injected automatically)
|
|
1132
|
-
client = ApiClient("
|
|
1132
|
+
client = ApiClient("billing", headers={"x-tenant": "acme"})
|
|
1133
1133
|
```
|
|
1134
1134
|
|
|
1135
1135
|
**How URL and token are resolved:**
|
|
1136
1136
|
|
|
1137
1137
|
| Env var | Purpose |
|
|
1138
1138
|
|---------|---------|
|
|
1139
|
-
| `MICROSERVICE_URLS` | JSON map: `{"
|
|
1139
|
+
| `MICROSERVICE_URLS` | JSON map: `{"billing": "https://api.example.com/", "catalog": "https://..."}` |
|
|
1140
1140
|
| `INTER_SERVICE_TOKEN` | Bearer token injected as `Authorization: Bearer <token>` |
|
|
1141
1141
|
| `AUTH_BYPASS_TOKEN` | Fallback if `INTER_SERVICE_TOKEN` is not set |
|
|
1142
1142
|
|
|
@@ -1154,7 +1154,7 @@ client = ApiClient("dockets", headers={"constitution-state": "CT"})
|
|
|
1154
1154
|
# In AWS Secrets Manager secret (JSON):
|
|
1155
1155
|
# {
|
|
1156
1156
|
# "inter_service_token": "my-secret-token",
|
|
1157
|
-
# "microservice_urls": "{\"
|
|
1157
|
+
# "microservice_urls": "{\"billing\": \"https://api.example.com/\"}",
|
|
1158
1158
|
# ...
|
|
1159
1159
|
# }
|
|
1160
1160
|
|
|
@@ -1574,7 +1574,7 @@ environment_variables = {
|
|
|
1574
1574
|
| `MICROSERVICE_URLS` | Optional | JSON map of service name → base URL used by `ApiClient` |
|
|
1575
1575
|
| `ECS_CLUSTER` | Fargate only | ECS cluster name for `FargateExecutor` |
|
|
1576
1576
|
| `ECS_SUBNETS` | Fargate only | Comma-separated subnet IDs for Fargate tasks |
|
|
1577
|
-
| `
|
|
1577
|
+
| `SESSION` | Fargate only (auto) | The whole session, injected by `FargateExecutor` — do not set manually |
|
|
1578
1578
|
| `AWS_REGION` | Fargate/SNS/SQS | AWS region |
|
|
1579
1579
|
| `AWS_ACCOUNT_ID` | SQS `get_queue_url` | AWS account ID |
|
|
1580
1580
|
| `SERVICE_NAME` | SQS `get_queue_url` | Service name prefix for queue name |
|
|
@@ -1587,9 +1587,9 @@ environment_variables = {
|
|
|
1587
1587
|
|
|
1588
1588
|
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages.
|
|
1589
1589
|
|
|
1590
|
-
**
|
|
1590
|
+
**State handling in SQS:**
|
|
1591
1591
|
- **Single mode**: the framework extracts the session from each record automatically (from SNS `MessageAttributes`, Base64-decoded) and sets it in context before calling `process_record()`. You do not need to extract it yourself.
|
|
1592
|
-
- **Batch mode**: the framework groups the incoming records by
|
|
1592
|
+
- **Batch mode**: the framework groups the incoming records by state and calls `process_batch()` once per group, with the correct state in context for each group. This ensures that state-scoped repositories resolve to the right database even when a batch contains records from different states.
|
|
1593
1593
|
|
|
1594
1594
|
```python
|
|
1595
1595
|
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
@@ -1644,7 +1644,7 @@ The `SNSPublisher` automatically injects the current session as a Base64-encoded
|
|
|
1644
1644
|
topic = TitleIndexedTopic()
|
|
1645
1645
|
|
|
1646
1646
|
# Publish multiple messages in a single call
|
|
1647
|
-
#
|
|
1647
|
+
# The state travels as a MessageAttribute on each message, injected automatically
|
|
1648
1648
|
await topic.publish([
|
|
1649
1649
|
{'content': {'id': 'id1', 'title': 'Title 1'}, 'attributes': {'type': 'created'}},
|
|
1650
1650
|
{'content': {'id': 'id2', 'title': 'Title 2'}, 'attributes': {'type': 'updated'}},
|
|
@@ -5,6 +5,7 @@ AWS Python Framework
|
|
|
5
5
|
__version__ = "1.0.2"
|
|
6
6
|
|
|
7
7
|
# All classes
|
|
8
|
+
from . import settings
|
|
8
9
|
from .api.base import API
|
|
9
10
|
from .sqs.consumer_base import SQSConsumer
|
|
10
11
|
from .database.mongo_manager import MongoManager
|
|
@@ -60,6 +61,7 @@ from .invoker import (
|
|
|
60
61
|
from .monitoring import process_run, resolve_code, RunReporter, ProcessRunPublisher
|
|
61
62
|
|
|
62
63
|
__all__ = [
|
|
64
|
+
"settings",
|
|
63
65
|
'API',
|
|
64
66
|
'process_run',
|
|
65
67
|
'resolve_code',
|
{aws_python_helper-1.14.0 → aws_python_helper-1.15.1}/aws_python_helper/api/auth_validators.py
RENAMED
|
@@ -7,6 +7,12 @@ from typing import Dict, Any, Optional
|
|
|
7
7
|
import os
|
|
8
8
|
import logging
|
|
9
9
|
from datetime import datetime
|
|
10
|
+
# Quién firma los tokens que este servicio verifica. Es un default histórico y
|
|
11
|
+
# no un nombre que el framework imponga: el `iss` de un token ya emitido no se
|
|
12
|
+
# puede cambiar sin invalidarlo, así que se configura con JWT_ISSUER y esto
|
|
13
|
+
# queda como el valor que venía de antes.
|
|
14
|
+
ISSUER_DEFAULT = 'constitution-core'
|
|
15
|
+
|
|
10
16
|
from . import jwt_keys
|
|
11
17
|
from .exceptions import UnauthorizedError
|
|
12
18
|
from ..database.mongo_manager import MongoManager
|
|
@@ -186,7 +192,7 @@ class TokenValidator(AuthValidator):
|
|
|
186
192
|
|
|
187
193
|
class JWTValidator(AuthValidator):
|
|
188
194
|
"""
|
|
189
|
-
Validates RS256 JWTs issued by
|
|
195
|
+
Validates RS256 JWTs issued by the identity service.
|
|
190
196
|
|
|
191
197
|
Stateless: the signature and the claims are enough, so no database is
|
|
192
198
|
touched. That is what lets any microservice authenticate a request without
|
|
@@ -198,7 +204,7 @@ class JWTValidator(AuthValidator):
|
|
|
198
204
|
Environment:
|
|
199
205
|
CORE_API_URL: where core answers. How the key is found.
|
|
200
206
|
JWT_PUBLIC_KEY: pins the key instead of reading it from core.
|
|
201
|
-
JWT_ISSUER: expected `iss`.
|
|
207
|
+
JWT_ISSUER: expected `iss`. Must match what the issuer signs.
|
|
202
208
|
JWT_AUDIENCE: expected `aud`. Only verified when set.
|
|
203
209
|
AUTH_BYPASS_TOKEN: still honoured, same as TokenValidator.
|
|
204
210
|
"""
|
|
@@ -235,7 +241,7 @@ class JWTValidator(AuthValidator):
|
|
|
235
241
|
token,
|
|
236
242
|
key,
|
|
237
243
|
algorithms=['RS256'],
|
|
238
|
-
issuer=os.getenv('JWT_ISSUER',
|
|
244
|
+
issuer=os.getenv('JWT_ISSUER', ISSUER_DEFAULT),
|
|
239
245
|
audience=audience,
|
|
240
246
|
options={'verify_aud': bool(audience)},
|
|
241
247
|
)
|