aws-python-helper 0.31.0__tar.gz → 0.32.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.31.0 → aws_python_helper-0.32.0}/PKG-INFO +100 -10
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/README.md +99 -9
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/__init__.py +5 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/api/dispatcher.py +15 -1
- aws_python_helper-0.32.0/aws_python_helper/context/__init__.py +3 -0
- aws_python_helper-0.32.0/aws_python_helper/context/state.py +22 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/fargate/executor.py +8 -1
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/fargate/task_base.py +5 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/lambda_standalone/base.py +7 -1
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/repository/base.py +81 -30
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/sns/publisher.py +11 -1
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/sqs/consumer_base.py +104 -48
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper.egg-info/PKG-INFO +100 -10
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper.egg-info/SOURCES.txt +2 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/pyproject.toml +1 -1
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/api/base.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-python-helper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
29
29
|
- **OOP structure**: Object-oriented programming for your code
|
|
30
30
|
- **Flexible MongoDB**: Direct access to multiple databases without models
|
|
31
31
|
- **External MongoDB**: Connect to multiple MongoDB clusters simultaneously
|
|
32
|
+
- **Multi-state routing**: Automatic `constitution-state` propagation across the entire call chain for per-state database routing
|
|
32
33
|
- **SQS Consumers**: Same pattern to process SQS messages (single or batch mode)
|
|
33
34
|
- **SNS Publishers**: Same pattern to publish messages to SNS topics
|
|
34
35
|
- **Fargate Tasks**: Same pattern to run tasks in Fargate containers
|
|
@@ -61,6 +62,8 @@ All available classes and functions:
|
|
|
61
62
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
62
63
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
63
64
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
65
|
+
| `get_state` | `aws_python_helper` | Read the current constitution-state from async context |
|
|
66
|
+
| `set_state` | `aws_python_helper` | Set the current constitution-state in async context |
|
|
64
67
|
| `MongoJSONEncoder` | `aws_python_helper.utils.json_encoder` | JSON encoder for MongoDB types |
|
|
65
68
|
| `mongo_json_dumps` | `aws_python_helper.utils.json_encoder` | Helper to serialize MongoDB types |
|
|
66
69
|
| `serialize_mongo_types` | `aws_python_helper.utils.serializer` | Recursively serialize MongoDB types |
|
|
@@ -269,6 +272,7 @@ response = lambda_client.invoke(
|
|
|
269
272
|
FunctionName='GenerateRouteLambda',
|
|
270
273
|
InvocationType='RequestResponse',
|
|
271
274
|
Payload=json.dumps({
|
|
275
|
+
'constitution-state': 'connecticut', # Required
|
|
272
276
|
'data': {
|
|
273
277
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
274
278
|
}
|
|
@@ -292,6 +296,7 @@ lambda_client.invoke(
|
|
|
292
296
|
FunctionName='GenerateRouteLambda',
|
|
293
297
|
InvocationType='Event', # Asynchronous
|
|
294
298
|
Payload=json.dumps({
|
|
299
|
+
'constitution-state': 'connecticut', # Required
|
|
295
300
|
'data': {
|
|
296
301
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
297
302
|
}
|
|
@@ -435,6 +440,7 @@ from aws_python_helper.fargate.executor import FargateExecutor
|
|
|
435
440
|
|
|
436
441
|
def handler(event, context):
|
|
437
442
|
executor = FargateExecutor()
|
|
443
|
+
# constitution-state is auto-propagated as CONSTITUTION_STATE env var in the container
|
|
438
444
|
task_arn = executor.run_task(
|
|
439
445
|
'search-tax-by-town',
|
|
440
446
|
envs={'TOWN': 'Norwalk', 'ONLY_TAX': 'true'}
|
|
@@ -499,11 +505,17 @@ The framework provides a `Repository` base class that eliminates repetitive boil
|
|
|
499
505
|
| Property | Type | Default | Required |
|
|
500
506
|
|----------|------|---------|----------|
|
|
501
507
|
| `collection_name` | `str` | — | **Yes** |
|
|
502
|
-
| `
|
|
508
|
+
| `database_key` | `str \| None` | `None` | No — if `None`, uses `constitution-state` from context |
|
|
503
509
|
| `is_external` | `bool` | `False` | No |
|
|
504
510
|
| `cluster_name` | `str` | `None` | Only if `is_external=True` |
|
|
505
511
|
| `indexes` | `list` | `[]` | No |
|
|
506
512
|
|
|
513
|
+
**`database_key` controls how the database is resolved:**
|
|
514
|
+
- `database_key = "core"` (or any string) → always connects to that specific database.
|
|
515
|
+
- `database_key = None` (default) → reads `constitution-state` from the async context automatically. This makes the repository **state-scoped**: it connects to `"connecticut"`, `"new_jersey"`, etc. depending on the current request.
|
|
516
|
+
|
|
517
|
+
Collections are cached per `(database_name, collection_name)` key — state-scoped repositories correctly isolate state between concurrent requests.
|
|
518
|
+
|
|
507
519
|
### Index format
|
|
508
520
|
|
|
509
521
|
```python
|
|
@@ -519,7 +531,7 @@ def indexes(self):
|
|
|
519
531
|
|
|
520
532
|
Indexes are created automatically in the background on first collection access — no need to call any initialization method.
|
|
521
533
|
|
|
522
|
-
### Repository
|
|
534
|
+
### Repository with a fixed database
|
|
523
535
|
|
|
524
536
|
```python
|
|
525
537
|
from aws_python_helper import Repository
|
|
@@ -530,6 +542,10 @@ class TownsRepository(Repository):
|
|
|
530
542
|
def collection_name(self):
|
|
531
543
|
return "towns"
|
|
532
544
|
|
|
545
|
+
@property
|
|
546
|
+
def database_key(self):
|
|
547
|
+
return "core" # always connects to the "core" database
|
|
548
|
+
|
|
533
549
|
@property
|
|
534
550
|
def indexes(self):
|
|
535
551
|
return [
|
|
@@ -547,21 +563,23 @@ class TownsRepository(Repository):
|
|
|
547
563
|
return await self.collection.find_one({"name": name})
|
|
548
564
|
```
|
|
549
565
|
|
|
550
|
-
###
|
|
566
|
+
### State-scoped repository (no `database_key`)
|
|
567
|
+
|
|
568
|
+
When `database_key` is not set, the repository reads the current `constitution-state` from context and uses it as the database name. The same repository instance connects to `"connecticut"` for one request and to `"new_jersey"` for another — automatically.
|
|
551
569
|
|
|
552
570
|
```python
|
|
553
571
|
from aws_python_helper import Repository
|
|
554
572
|
|
|
555
573
|
class LandRecordsRepository(Repository):
|
|
556
574
|
|
|
557
|
-
@property
|
|
558
|
-
def database_name(self):
|
|
559
|
-
return "land_data"
|
|
560
|
-
|
|
561
575
|
@property
|
|
562
576
|
def collection_name(self):
|
|
563
577
|
return "records"
|
|
564
578
|
|
|
579
|
+
# No database_key → uses get_state() automatically
|
|
580
|
+
# If constitution-state = "connecticut" → connects to DB "connecticut"
|
|
581
|
+
# If constitution-state = "new_jersey" → connects to DB "new_jersey"
|
|
582
|
+
|
|
565
583
|
@property
|
|
566
584
|
def indexes(self):
|
|
567
585
|
return [
|
|
@@ -579,6 +597,8 @@ class LandRecordsRepository(Repository):
|
|
|
579
597
|
return {"upserted": result.upserted_count, "modified": result.modified_count}
|
|
580
598
|
```
|
|
581
599
|
|
|
600
|
+
> **Note:** A `ValueError` is raised at runtime if `database_key` is `None` and `constitution-state` has not been set in the context. This is prevented automatically by the framework at every entry point (API, Lambda, SQS, Fargate).
|
|
601
|
+
|
|
582
602
|
### Repository on an external cluster
|
|
583
603
|
|
|
584
604
|
```python
|
|
@@ -587,7 +607,7 @@ from aws_python_helper import Repository
|
|
|
587
607
|
class AddressRepository(Repository):
|
|
588
608
|
|
|
589
609
|
@property
|
|
590
|
-
def
|
|
610
|
+
def database_key(self):
|
|
591
611
|
return "smart_data"
|
|
592
612
|
|
|
593
613
|
@property
|
|
@@ -627,6 +647,68 @@ class MyAPI(API):
|
|
|
627
647
|
|
|
628
648
|
The repository connects itself using the already-initialized `MongoManager` singleton — the same one used by `self.db`. No need to pass `self.db` or any connection object.
|
|
629
649
|
|
|
650
|
+
## 🌐 Constitution State
|
|
651
|
+
|
|
652
|
+
The framework uses a `constitution-state` value to support **multi-state database routing** — connecting each request to the correct database based on the state it belongs to (e.g., `"connecticut"`, `"new_jersey"`). This value is propagated automatically across the entire async call chain using Python's `contextvars.ContextVar`, so you never need to pass it manually between layers.
|
|
653
|
+
|
|
654
|
+
### How the framework injects it at each entry point
|
|
655
|
+
|
|
656
|
+
| Entry point | How `constitution-state` is read |
|
|
657
|
+
|-------------|----------------------------------|
|
|
658
|
+
| **API Gateway** | HTTP header `constitution-state` — **required**, returns `400` if missing |
|
|
659
|
+
| **Standalone Lambda** | Field `constitution-state` in the event payload — **required**, raises `ValueError` if missing |
|
|
660
|
+
| **SQS Consumer (single mode)** | Per-record: reads from SNS `MessageAttributes['constitution-state']`, falls back to `body.constitution_state` |
|
|
661
|
+
| **SQS Consumer (batch mode)** | Groups records by state; calls `process_batch()` once per group with the correct state in context |
|
|
662
|
+
| **Fargate Task** | Env var `CONSTITUTION_STATE` — auto-injected by `FargateExecutor` |
|
|
663
|
+
|
|
664
|
+
### How the framework propagates it to downstream services
|
|
665
|
+
|
|
666
|
+
| Downstream service | Propagation mechanism |
|
|
667
|
+
|--------------------|-----------------------|
|
|
668
|
+
| **SNS Publisher** | Auto-injects `constitution-state` as a `MessageAttribute` on every published message |
|
|
669
|
+
| **FargateExecutor** | Auto-injects `CONSTITUTION_STATE` as an env var when launching Fargate containers |
|
|
670
|
+
|
|
671
|
+
This means that an API call with `constitution-state: connecticut` will automatically carry that state through SNS → SQS → Fargate without any code changes in your consumers or tasks.
|
|
672
|
+
|
|
673
|
+
### State-scoped repositories
|
|
674
|
+
|
|
675
|
+
Repositories with no `database_key` (default) read `constitution-state` from context to resolve the target database automatically. See the [Repository Pattern](#️-repository-pattern) section for details.
|
|
676
|
+
|
|
677
|
+
### Manual access
|
|
678
|
+
|
|
679
|
+
If you need to read or set the state manually (e.g., in tests or utility code):
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
from aws_python_helper import get_state, set_state
|
|
683
|
+
|
|
684
|
+
state = get_state() # e.g. "connecticut", or None if not set
|
|
685
|
+
set_state("new_jersey") # set manually (the framework does this automatically)
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### API example — `constitution-state` header
|
|
689
|
+
|
|
690
|
+
```
|
|
691
|
+
GET /constitutions HTTP/1.1
|
|
692
|
+
constitution-state: connecticut
|
|
693
|
+
Authorization: Bearer <token>
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Lambda invocation example — `constitution-state` in event
|
|
697
|
+
|
|
698
|
+
```python
|
|
699
|
+
import boto3, json
|
|
700
|
+
|
|
701
|
+
lambda_client = boto3.client('lambda')
|
|
702
|
+
lambda_client.invoke(
|
|
703
|
+
FunctionName='MyLambdaFunction',
|
|
704
|
+
InvocationType='RequestResponse',
|
|
705
|
+
Payload=json.dumps({
|
|
706
|
+
'constitution-state': 'connecticut', # Required
|
|
707
|
+
'data': {'key': 'value'}
|
|
708
|
+
})
|
|
709
|
+
)
|
|
710
|
+
```
|
|
711
|
+
|
|
630
712
|
## 🔄 Routing Convention
|
|
631
713
|
|
|
632
714
|
The framework uses convention over configuration for the routing:
|
|
@@ -1018,6 +1100,7 @@ environment_variables = {
|
|
|
1018
1100
|
| `AUTH_BYPASS_TOKEN` | Optional | Master token to bypass authentication |
|
|
1019
1101
|
| `ECS_CLUSTER` | Fargate only | ECS cluster name for `FargateExecutor` |
|
|
1020
1102
|
| `ECS_SUBNETS` | Fargate only | Comma-separated subnet IDs for Fargate tasks |
|
|
1103
|
+
| `CONSTITUTION_STATE` | Fargate only (auto) | State injected automatically by `FargateExecutor` — do not set manually |
|
|
1021
1104
|
| `AWS_REGION` | Fargate/SNS/SQS | AWS region |
|
|
1022
1105
|
| `AWS_ACCOUNT_ID` | SQS `get_queue_url` | AWS account ID |
|
|
1023
1106
|
| `SERVICE_NAME` | SQS `get_queue_url` | Service name prefix for queue name |
|
|
@@ -1028,7 +1111,11 @@ environment_variables = {
|
|
|
1028
1111
|
|
|
1029
1112
|
### SQS Consumer - Batch Mode
|
|
1030
1113
|
|
|
1031
|
-
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages
|
|
1114
|
+
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages.
|
|
1115
|
+
|
|
1116
|
+
**Constitution-state handling in SQS:**
|
|
1117
|
+
- **Single mode**: the framework extracts `constitution-state` from each record automatically (from SNS `MessageAttributes`, then from `body.constitution_state`) and sets it in context before calling `process_record()`. You do not need to extract it yourself.
|
|
1118
|
+
- **Batch mode**: the framework groups the incoming records by `constitution-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.
|
|
1032
1119
|
|
|
1033
1120
|
```python
|
|
1034
1121
|
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
@@ -1077,10 +1164,13 @@ class OrderConsumer(SQSConsumer):
|
|
|
1077
1164
|
|
|
1078
1165
|
### SNS Publisher - Batch Publishing
|
|
1079
1166
|
|
|
1167
|
+
The `SNSPublisher` automatically injects the current `constitution-state` as a `MessageAttribute` on every published message. SQS consumers built with this framework will then extract it automatically, ensuring the state flows end-to-end through the SNS → SQS chain without any manual code.
|
|
1168
|
+
|
|
1080
1169
|
```python
|
|
1081
1170
|
topic = TitleIndexedTopic()
|
|
1082
1171
|
|
|
1083
1172
|
# Publish multiple messages in a single call
|
|
1173
|
+
# constitution-state is auto-injected as a MessageAttribute on each message
|
|
1084
1174
|
await topic.publish([
|
|
1085
1175
|
{'content': {'id': 'id1', 'title': 'Title 1'}, 'attributes': {'type': 'created'}},
|
|
1086
1176
|
{'content': {'id': 'id2', 'title': 'Title 2'}, 'attributes': {'type': 'updated'}},
|
|
@@ -9,6 +9,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
9
9
|
- **OOP structure**: Object-oriented programming for your code
|
|
10
10
|
- **Flexible MongoDB**: Direct access to multiple databases without models
|
|
11
11
|
- **External MongoDB**: Connect to multiple MongoDB clusters simultaneously
|
|
12
|
+
- **Multi-state routing**: Automatic `constitution-state` propagation across the entire call chain for per-state database routing
|
|
12
13
|
- **SQS Consumers**: Same pattern to process SQS messages (single or batch mode)
|
|
13
14
|
- **SNS Publishers**: Same pattern to publish messages to SNS topics
|
|
14
15
|
- **Fargate Tasks**: Same pattern to run tasks in Fargate containers
|
|
@@ -41,6 +42,8 @@ All available classes and functions:
|
|
|
41
42
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
42
43
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
43
44
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
45
|
+
| `get_state` | `aws_python_helper` | Read the current constitution-state from async context |
|
|
46
|
+
| `set_state` | `aws_python_helper` | Set the current constitution-state in async context |
|
|
44
47
|
| `MongoJSONEncoder` | `aws_python_helper.utils.json_encoder` | JSON encoder for MongoDB types |
|
|
45
48
|
| `mongo_json_dumps` | `aws_python_helper.utils.json_encoder` | Helper to serialize MongoDB types |
|
|
46
49
|
| `serialize_mongo_types` | `aws_python_helper.utils.serializer` | Recursively serialize MongoDB types |
|
|
@@ -249,6 +252,7 @@ response = lambda_client.invoke(
|
|
|
249
252
|
FunctionName='GenerateRouteLambda',
|
|
250
253
|
InvocationType='RequestResponse',
|
|
251
254
|
Payload=json.dumps({
|
|
255
|
+
'constitution-state': 'connecticut', # Required
|
|
252
256
|
'data': {
|
|
253
257
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
254
258
|
}
|
|
@@ -272,6 +276,7 @@ lambda_client.invoke(
|
|
|
272
276
|
FunctionName='GenerateRouteLambda',
|
|
273
277
|
InvocationType='Event', # Asynchronous
|
|
274
278
|
Payload=json.dumps({
|
|
279
|
+
'constitution-state': 'connecticut', # Required
|
|
275
280
|
'data': {
|
|
276
281
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
277
282
|
}
|
|
@@ -415,6 +420,7 @@ from aws_python_helper.fargate.executor import FargateExecutor
|
|
|
415
420
|
|
|
416
421
|
def handler(event, context):
|
|
417
422
|
executor = FargateExecutor()
|
|
423
|
+
# constitution-state is auto-propagated as CONSTITUTION_STATE env var in the container
|
|
418
424
|
task_arn = executor.run_task(
|
|
419
425
|
'search-tax-by-town',
|
|
420
426
|
envs={'TOWN': 'Norwalk', 'ONLY_TAX': 'true'}
|
|
@@ -479,11 +485,17 @@ The framework provides a `Repository` base class that eliminates repetitive boil
|
|
|
479
485
|
| Property | Type | Default | Required |
|
|
480
486
|
|----------|------|---------|----------|
|
|
481
487
|
| `collection_name` | `str` | — | **Yes** |
|
|
482
|
-
| `
|
|
488
|
+
| `database_key` | `str \| None` | `None` | No — if `None`, uses `constitution-state` from context |
|
|
483
489
|
| `is_external` | `bool` | `False` | No |
|
|
484
490
|
| `cluster_name` | `str` | `None` | Only if `is_external=True` |
|
|
485
491
|
| `indexes` | `list` | `[]` | No |
|
|
486
492
|
|
|
493
|
+
**`database_key` controls how the database is resolved:**
|
|
494
|
+
- `database_key = "core"` (or any string) → always connects to that specific database.
|
|
495
|
+
- `database_key = None` (default) → reads `constitution-state` from the async context automatically. This makes the repository **state-scoped**: it connects to `"connecticut"`, `"new_jersey"`, etc. depending on the current request.
|
|
496
|
+
|
|
497
|
+
Collections are cached per `(database_name, collection_name)` key — state-scoped repositories correctly isolate state between concurrent requests.
|
|
498
|
+
|
|
487
499
|
### Index format
|
|
488
500
|
|
|
489
501
|
```python
|
|
@@ -499,7 +511,7 @@ def indexes(self):
|
|
|
499
511
|
|
|
500
512
|
Indexes are created automatically in the background on first collection access — no need to call any initialization method.
|
|
501
513
|
|
|
502
|
-
### Repository
|
|
514
|
+
### Repository with a fixed database
|
|
503
515
|
|
|
504
516
|
```python
|
|
505
517
|
from aws_python_helper import Repository
|
|
@@ -510,6 +522,10 @@ class TownsRepository(Repository):
|
|
|
510
522
|
def collection_name(self):
|
|
511
523
|
return "towns"
|
|
512
524
|
|
|
525
|
+
@property
|
|
526
|
+
def database_key(self):
|
|
527
|
+
return "core" # always connects to the "core" database
|
|
528
|
+
|
|
513
529
|
@property
|
|
514
530
|
def indexes(self):
|
|
515
531
|
return [
|
|
@@ -527,21 +543,23 @@ class TownsRepository(Repository):
|
|
|
527
543
|
return await self.collection.find_one({"name": name})
|
|
528
544
|
```
|
|
529
545
|
|
|
530
|
-
###
|
|
546
|
+
### State-scoped repository (no `database_key`)
|
|
547
|
+
|
|
548
|
+
When `database_key` is not set, the repository reads the current `constitution-state` from context and uses it as the database name. The same repository instance connects to `"connecticut"` for one request and to `"new_jersey"` for another — automatically.
|
|
531
549
|
|
|
532
550
|
```python
|
|
533
551
|
from aws_python_helper import Repository
|
|
534
552
|
|
|
535
553
|
class LandRecordsRepository(Repository):
|
|
536
554
|
|
|
537
|
-
@property
|
|
538
|
-
def database_name(self):
|
|
539
|
-
return "land_data"
|
|
540
|
-
|
|
541
555
|
@property
|
|
542
556
|
def collection_name(self):
|
|
543
557
|
return "records"
|
|
544
558
|
|
|
559
|
+
# No database_key → uses get_state() automatically
|
|
560
|
+
# If constitution-state = "connecticut" → connects to DB "connecticut"
|
|
561
|
+
# If constitution-state = "new_jersey" → connects to DB "new_jersey"
|
|
562
|
+
|
|
545
563
|
@property
|
|
546
564
|
def indexes(self):
|
|
547
565
|
return [
|
|
@@ -559,6 +577,8 @@ class LandRecordsRepository(Repository):
|
|
|
559
577
|
return {"upserted": result.upserted_count, "modified": result.modified_count}
|
|
560
578
|
```
|
|
561
579
|
|
|
580
|
+
> **Note:** A `ValueError` is raised at runtime if `database_key` is `None` and `constitution-state` has not been set in the context. This is prevented automatically by the framework at every entry point (API, Lambda, SQS, Fargate).
|
|
581
|
+
|
|
562
582
|
### Repository on an external cluster
|
|
563
583
|
|
|
564
584
|
```python
|
|
@@ -567,7 +587,7 @@ from aws_python_helper import Repository
|
|
|
567
587
|
class AddressRepository(Repository):
|
|
568
588
|
|
|
569
589
|
@property
|
|
570
|
-
def
|
|
590
|
+
def database_key(self):
|
|
571
591
|
return "smart_data"
|
|
572
592
|
|
|
573
593
|
@property
|
|
@@ -607,6 +627,68 @@ class MyAPI(API):
|
|
|
607
627
|
|
|
608
628
|
The repository connects itself using the already-initialized `MongoManager` singleton — the same one used by `self.db`. No need to pass `self.db` or any connection object.
|
|
609
629
|
|
|
630
|
+
## 🌐 Constitution State
|
|
631
|
+
|
|
632
|
+
The framework uses a `constitution-state` value to support **multi-state database routing** — connecting each request to the correct database based on the state it belongs to (e.g., `"connecticut"`, `"new_jersey"`). This value is propagated automatically across the entire async call chain using Python's `contextvars.ContextVar`, so you never need to pass it manually between layers.
|
|
633
|
+
|
|
634
|
+
### How the framework injects it at each entry point
|
|
635
|
+
|
|
636
|
+
| Entry point | How `constitution-state` is read |
|
|
637
|
+
|-------------|----------------------------------|
|
|
638
|
+
| **API Gateway** | HTTP header `constitution-state` — **required**, returns `400` if missing |
|
|
639
|
+
| **Standalone Lambda** | Field `constitution-state` in the event payload — **required**, raises `ValueError` if missing |
|
|
640
|
+
| **SQS Consumer (single mode)** | Per-record: reads from SNS `MessageAttributes['constitution-state']`, falls back to `body.constitution_state` |
|
|
641
|
+
| **SQS Consumer (batch mode)** | Groups records by state; calls `process_batch()` once per group with the correct state in context |
|
|
642
|
+
| **Fargate Task** | Env var `CONSTITUTION_STATE` — auto-injected by `FargateExecutor` |
|
|
643
|
+
|
|
644
|
+
### How the framework propagates it to downstream services
|
|
645
|
+
|
|
646
|
+
| Downstream service | Propagation mechanism |
|
|
647
|
+
|--------------------|-----------------------|
|
|
648
|
+
| **SNS Publisher** | Auto-injects `constitution-state` as a `MessageAttribute` on every published message |
|
|
649
|
+
| **FargateExecutor** | Auto-injects `CONSTITUTION_STATE` as an env var when launching Fargate containers |
|
|
650
|
+
|
|
651
|
+
This means that an API call with `constitution-state: connecticut` will automatically carry that state through SNS → SQS → Fargate without any code changes in your consumers or tasks.
|
|
652
|
+
|
|
653
|
+
### State-scoped repositories
|
|
654
|
+
|
|
655
|
+
Repositories with no `database_key` (default) read `constitution-state` from context to resolve the target database automatically. See the [Repository Pattern](#️-repository-pattern) section for details.
|
|
656
|
+
|
|
657
|
+
### Manual access
|
|
658
|
+
|
|
659
|
+
If you need to read or set the state manually (e.g., in tests or utility code):
|
|
660
|
+
|
|
661
|
+
```python
|
|
662
|
+
from aws_python_helper import get_state, set_state
|
|
663
|
+
|
|
664
|
+
state = get_state() # e.g. "connecticut", or None if not set
|
|
665
|
+
set_state("new_jersey") # set manually (the framework does this automatically)
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### API example — `constitution-state` header
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
GET /constitutions HTTP/1.1
|
|
672
|
+
constitution-state: connecticut
|
|
673
|
+
Authorization: Bearer <token>
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Lambda invocation example — `constitution-state` in event
|
|
677
|
+
|
|
678
|
+
```python
|
|
679
|
+
import boto3, json
|
|
680
|
+
|
|
681
|
+
lambda_client = boto3.client('lambda')
|
|
682
|
+
lambda_client.invoke(
|
|
683
|
+
FunctionName='MyLambdaFunction',
|
|
684
|
+
InvocationType='RequestResponse',
|
|
685
|
+
Payload=json.dumps({
|
|
686
|
+
'constitution-state': 'connecticut', # Required
|
|
687
|
+
'data': {'key': 'value'}
|
|
688
|
+
})
|
|
689
|
+
)
|
|
690
|
+
```
|
|
691
|
+
|
|
610
692
|
## 🔄 Routing Convention
|
|
611
693
|
|
|
612
694
|
The framework uses convention over configuration for the routing:
|
|
@@ -998,6 +1080,7 @@ environment_variables = {
|
|
|
998
1080
|
| `AUTH_BYPASS_TOKEN` | Optional | Master token to bypass authentication |
|
|
999
1081
|
| `ECS_CLUSTER` | Fargate only | ECS cluster name for `FargateExecutor` |
|
|
1000
1082
|
| `ECS_SUBNETS` | Fargate only | Comma-separated subnet IDs for Fargate tasks |
|
|
1083
|
+
| `CONSTITUTION_STATE` | Fargate only (auto) | State injected automatically by `FargateExecutor` — do not set manually |
|
|
1001
1084
|
| `AWS_REGION` | Fargate/SNS/SQS | AWS region |
|
|
1002
1085
|
| `AWS_ACCOUNT_ID` | SQS `get_queue_url` | AWS account ID |
|
|
1003
1086
|
| `SERVICE_NAME` | SQS `get_queue_url` | Service name prefix for queue name |
|
|
@@ -1008,7 +1091,11 @@ environment_variables = {
|
|
|
1008
1091
|
|
|
1009
1092
|
### SQS Consumer - Batch Mode
|
|
1010
1093
|
|
|
1011
|
-
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages
|
|
1094
|
+
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages.
|
|
1095
|
+
|
|
1096
|
+
**Constitution-state handling in SQS:**
|
|
1097
|
+
- **Single mode**: the framework extracts `constitution-state` from each record automatically (from SNS `MessageAttributes`, then from `body.constitution_state`) and sets it in context before calling `process_record()`. You do not need to extract it yourself.
|
|
1098
|
+
- **Batch mode**: the framework groups the incoming records by `constitution-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.
|
|
1012
1099
|
|
|
1013
1100
|
```python
|
|
1014
1101
|
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
@@ -1057,10 +1144,13 @@ class OrderConsumer(SQSConsumer):
|
|
|
1057
1144
|
|
|
1058
1145
|
### SNS Publisher - Batch Publishing
|
|
1059
1146
|
|
|
1147
|
+
The `SNSPublisher` automatically injects the current `constitution-state` as a `MessageAttribute` on every published message. SQS consumers built with this framework will then extract it automatically, ensuring the state flows end-to-end through the SNS → SQS chain without any manual code.
|
|
1148
|
+
|
|
1060
1149
|
```python
|
|
1061
1150
|
topic = TitleIndexedTopic()
|
|
1062
1151
|
|
|
1063
1152
|
# Publish multiple messages in a single call
|
|
1153
|
+
# constitution-state is auto-injected as a MessageAttribute on each message
|
|
1064
1154
|
await topic.publish([
|
|
1065
1155
|
{'content': {'id': 'id1', 'title': 'Title 1'}, 'attributes': {'type': 'created'}},
|
|
1066
1156
|
{'content': {'id': 'id2', 'title': 'Title 2'}, 'attributes': {'type': 'updated'}},
|
|
@@ -27,6 +27,9 @@ from .utils.serializer import serialize_mongo_types
|
|
|
27
27
|
# Repository
|
|
28
28
|
from .repository.base import Repository
|
|
29
29
|
|
|
30
|
+
# Context
|
|
31
|
+
from .context.state import get_state, set_state
|
|
32
|
+
|
|
30
33
|
|
|
31
34
|
__all__ = [
|
|
32
35
|
'API',
|
|
@@ -45,5 +48,7 @@ __all__ = [
|
|
|
45
48
|
'MongoJSONEncoder',
|
|
46
49
|
'mongo_json_dumps',
|
|
47
50
|
'serialize_mongo_types',
|
|
51
|
+
'get_state',
|
|
52
|
+
'set_state',
|
|
48
53
|
]
|
|
49
54
|
|
|
@@ -12,6 +12,7 @@ from .base import API
|
|
|
12
12
|
from .exceptions import UnauthorizedError, ForbiddenError, AuthenticationError
|
|
13
13
|
from .auth_middleware import AuthMiddleware
|
|
14
14
|
from .auth_validators import TokenValidator
|
|
15
|
+
from ..context.state import set_state
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -78,7 +79,20 @@ class Dispatcher:
|
|
|
78
79
|
require_auth = os.getenv('REQUIRE_AUTH', 'false').lower() == 'true'
|
|
79
80
|
if require_auth:
|
|
80
81
|
await self._authenticate(api)
|
|
81
|
-
|
|
82
|
+
|
|
83
|
+
# 2.5 Setup constitution-state context
|
|
84
|
+
state = self.headers.get('constitution-state')
|
|
85
|
+
if not state:
|
|
86
|
+
return {
|
|
87
|
+
'code': 400,
|
|
88
|
+
'body': {
|
|
89
|
+
'error': 'Bad Request',
|
|
90
|
+
'message': "Header 'constitution-state' is required"
|
|
91
|
+
},
|
|
92
|
+
'headers': {}
|
|
93
|
+
}
|
|
94
|
+
set_state(state)
|
|
95
|
+
|
|
82
96
|
# 3. Validate
|
|
83
97
|
await api.validate()
|
|
84
98
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constitution State Context - Manages request-scoped state for multi-state database routing.
|
|
3
|
+
|
|
4
|
+
Uses Python's contextvars to propagate the current constitution-state across async call chains.
|
|
5
|
+
Set automatically by the framework at every entry point (API, Lambda, SQS Consumer, Fargate Task).
|
|
6
|
+
Read automatically by state-scoped repositories (database_key = None) to resolve the target database.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
_constitution_state: ContextVar[Optional[str]] = ContextVar('constitution_state', default=None)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_state() -> Optional[str]:
|
|
16
|
+
"""Get the current constitution state from async context."""
|
|
17
|
+
return _constitution_state.get()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_state(state: str) -> None:
|
|
21
|
+
"""Set the current constitution state in async context."""
|
|
22
|
+
_constitution_state.set(state)
|
|
@@ -10,6 +10,8 @@ import logging
|
|
|
10
10
|
import boto3
|
|
11
11
|
from typing import Dict, Any, List, Optional
|
|
12
12
|
|
|
13
|
+
from ..context.state import get_state
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
@@ -118,7 +120,12 @@ class FargateExecutor:
|
|
|
118
120
|
{'name': key.upper(), 'value': str(value)}
|
|
119
121
|
for key, value in envs.items()
|
|
120
122
|
]
|
|
121
|
-
|
|
123
|
+
|
|
124
|
+
# Auto-propagate constitution-state from current context
|
|
125
|
+
current_state = get_state()
|
|
126
|
+
if current_state and 'CONSTITUTION_STATE' not in {e['name'] for e in environment}:
|
|
127
|
+
environment.append({'name': 'CONSTITUTION_STATE', 'value': current_state})
|
|
128
|
+
|
|
122
129
|
# Add TASK_NAME for the handler to know which task to execute
|
|
123
130
|
environment.append({'name': 'TASK_NAME', 'value': task_name})
|
|
124
131
|
|
{aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/fargate/task_base.py
RENAMED
|
@@ -10,6 +10,7 @@ import logging
|
|
|
10
10
|
from abc import ABC, abstractmethod
|
|
11
11
|
from typing import Dict, Any
|
|
12
12
|
|
|
13
|
+
from ..context.state import set_state
|
|
13
14
|
from ..database.mongo_manager import MongoManager
|
|
14
15
|
from ..database.database_proxy import DatabaseProxy
|
|
15
16
|
from ..database.external_mongo_manager import ExternalMongoManager
|
|
@@ -153,6 +154,10 @@ class FargateTask(ABC):
|
|
|
153
154
|
True if executed successfully, False otherwise
|
|
154
155
|
"""
|
|
155
156
|
try:
|
|
157
|
+
# Setup constitution-state context from env var (passed by FargateExecutor)
|
|
158
|
+
state = self.require_env("CONSTITUTION_STATE")
|
|
159
|
+
set_state(state)
|
|
160
|
+
|
|
156
161
|
await self.execute()
|
|
157
162
|
return True
|
|
158
163
|
|
{aws_python_helper-0.31.0 → aws_python_helper-0.32.0}/aws_python_helper/lambda_standalone/base.py
RENAMED
|
@@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
|
|
|
6
6
|
from typing import Dict, Any
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
|
+
from ..context.state import set_state
|
|
9
10
|
from ..database.mongo_manager import MongoManager
|
|
10
11
|
from ..database.database_proxy import DatabaseProxy
|
|
11
12
|
from ..database.external_mongo_manager import ExternalMongoManager
|
|
@@ -154,7 +155,12 @@ class Lambda(ABC):
|
|
|
154
155
|
Exception: Any error during validation or processing
|
|
155
156
|
"""
|
|
156
157
|
try:
|
|
157
|
-
|
|
158
|
+
# Step 0: Setup constitution-state context
|
|
159
|
+
state = self.event.get('constitution-state')
|
|
160
|
+
if not state:
|
|
161
|
+
raise ValueError("'constitution-state' is required in the event")
|
|
162
|
+
set_state(state)
|
|
163
|
+
|
|
158
164
|
# Step 1: Validate
|
|
159
165
|
await self.validate()
|
|
160
166
|
# Step 2: Process
|