aws-python-helper 0.31.0__tar.gz → 0.33.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.33.0}/PKG-INFO +121 -14
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/README.md +119 -11
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/__init__.py +6 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/base.py +19 -4
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/dispatcher.py +29 -4
- aws_python_helper-0.33.0/aws_python_helper/context/__init__.py +3 -0
- aws_python_helper-0.33.0/aws_python_helper/context/session.py +88 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/executor.py +14 -1
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/task_base.py +17 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/base.py +17 -1
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/repository/base.py +81 -30
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sns/publisher.py +12 -1
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/consumer_base.py +126 -49
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/PKG-INFO +121 -14
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/SOURCES.txt +2 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/pyproject.toml +2 -3
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/setup.cfg +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-python-helper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.33.0
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/fabiae/aws-python-framework
|
|
8
8
|
Project-URL: Source Code, https://github.com/fabiae/aws-python-framework
|
|
9
9
|
Project-URL: Bug Tracker, https://github.com/fabiae/aws-python-framework/issues
|
|
10
10
|
Project-URL: Documentation, https://github.com/fabiae/aws-python-framework/blob/main/README.md
|
|
11
11
|
Keywords: aws,python,framework,helper,mongodb,sqs,sns,fargate,lambda
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Requires-Python: >=3.9
|
|
16
15
|
Description-Content-Type: text/markdown
|
|
@@ -29,6 +28,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
29
28
|
- **OOP structure**: Object-oriented programming for your code
|
|
30
29
|
- **Flexible MongoDB**: Direct access to multiple databases without models
|
|
31
30
|
- **External MongoDB**: Connect to multiple MongoDB clusters simultaneously
|
|
31
|
+
- **Session propagation**: Automatic `Session` (state + user) propagation across the entire call chain for per-state database routing
|
|
32
32
|
- **SQS Consumers**: Same pattern to process SQS messages (single or batch mode)
|
|
33
33
|
- **SNS Publishers**: Same pattern to publish messages to SNS topics
|
|
34
34
|
- **Fargate Tasks**: Same pattern to run tasks in Fargate containers
|
|
@@ -61,6 +61,9 @@ All available classes and functions:
|
|
|
61
61
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
62
62
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
63
63
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
64
|
+
| `Session` | `aws_python_helper` | Request-scoped session object (state + user) |
|
|
65
|
+
| `get_session` | `aws_python_helper` | Read the current Session from async context |
|
|
66
|
+
| `set_session` | `aws_python_helper` | Set the current Session 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
|
+
'session': {'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
|
+
'session': {'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
|
+
# session is auto-propagated as SESSION env var (JSON) 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 `session.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 `session.state` from 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 `session.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 session.state automatically
|
|
580
|
+
# If session.state = "connecticut" → connects to DB "connecticut"
|
|
581
|
+
# If session.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 `session.state` has not been set. 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,84 @@ 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
|
+
## 🌐 Session Context
|
|
651
|
+
|
|
652
|
+
The framework propagates a `Session` object automatically across the entire async call chain using Python's `contextvars.ContextVar`. The session holds `state` (for multi-state DB routing) and `user` (authenticated user from the auth middleware).
|
|
653
|
+
|
|
654
|
+
### How the framework injects it at each entry point
|
|
655
|
+
|
|
656
|
+
| Entry point | How the session is read |
|
|
657
|
+
|-------------|-------------------------|
|
|
658
|
+
| **API Gateway** | `constitution-state` header → `session.state` (when `AUTHORIZATION` includes `state`); auth middleware → `session.user` (when includes `user`). Returns `400` if required header is missing |
|
|
659
|
+
| **Standalone Lambda** | `session` dict in the event payload — **required** (must include `state`), raises `ValueError` if missing |
|
|
660
|
+
| **SQS Consumer (single mode)** | Per-record: reads `session` from SNS `MessageAttributes` (JSON), falls back to legacy `body.constitution_state` |
|
|
661
|
+
| **SQS Consumer (batch mode)** | Groups records by `session.state`; calls `process_batch()` once per group with the correct session in context |
|
|
662
|
+
| **Fargate Task** | `SESSION` env var (JSON) — 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 the full session as a `session` `MessageAttribute` (JSON string) on every published message |
|
|
669
|
+
| **FargateExecutor** | Auto-injects `SESSION` as a JSON env var when launching Fargate containers |
|
|
670
|
+
|
|
671
|
+
This means that an API call with `constitution-state: connecticut` will automatically carry the full session (state + user) through SNS → SQS → Fargate without any code changes in your consumers or tasks.
|
|
672
|
+
|
|
673
|
+
### Accessing the session in handlers
|
|
674
|
+
|
|
675
|
+
All handler base classes expose a `self.session` property:
|
|
676
|
+
|
|
677
|
+
```python
|
|
678
|
+
class MyAPI(API):
|
|
679
|
+
async def process(self):
|
|
680
|
+
state = self.session.state # e.g. "connecticut"
|
|
681
|
+
user = self.session.user # authenticated user dict, or None
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Available in `API`, `SQSConsumer`, `Lambda`, and `FargateTask`.
|
|
685
|
+
|
|
686
|
+
### State-scoped repositories
|
|
687
|
+
|
|
688
|
+
Repositories with no `database_key` (default) read `session.state` from context to resolve the target database automatically. See the [Repository Pattern](#️-repository-pattern) section for details.
|
|
689
|
+
|
|
690
|
+
### Manual access
|
|
691
|
+
|
|
692
|
+
If you need to read or set the session manually (e.g., in tests or utility code):
|
|
693
|
+
|
|
694
|
+
```python
|
|
695
|
+
from aws_python_helper import Session, get_session, set_session
|
|
696
|
+
|
|
697
|
+
session = get_session() # returns current Session (creates empty one if not set)
|
|
698
|
+
session.state # e.g. "connecticut", or None if not set
|
|
699
|
+
session.user # authenticated user dict, or None
|
|
700
|
+
|
|
701
|
+
set_session(Session(state="new_jersey")) # set manually (the framework does this automatically)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### API example — `constitution-state` header
|
|
705
|
+
|
|
706
|
+
```
|
|
707
|
+
GET /constitutions HTTP/1.1
|
|
708
|
+
constitution-state: connecticut
|
|
709
|
+
Authorization: Bearer <token>
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Lambda invocation example — `session` in event
|
|
713
|
+
|
|
714
|
+
```python
|
|
715
|
+
import boto3, json
|
|
716
|
+
|
|
717
|
+
lambda_client = boto3.client('lambda')
|
|
718
|
+
lambda_client.invoke(
|
|
719
|
+
FunctionName='MyLambdaFunction',
|
|
720
|
+
InvocationType='RequestResponse',
|
|
721
|
+
Payload=json.dumps({
|
|
722
|
+
'session': {'state': 'connecticut'}, # Required
|
|
723
|
+
'data': {'key': 'value'}
|
|
724
|
+
})
|
|
725
|
+
)
|
|
726
|
+
```
|
|
727
|
+
|
|
630
728
|
## 🔄 Routing Convention
|
|
631
729
|
|
|
632
730
|
The framework uses convention over configuration for the routing:
|
|
@@ -662,6 +760,7 @@ All properties and methods available inside an `API` subclass:
|
|
|
662
760
|
| `self.query_parameters` | `dict` | Query string parameters |
|
|
663
761
|
| `self.db` | `DatabaseProxy` | Access to main MongoDB cluster |
|
|
664
762
|
| `self.external_db` | `ExternalDatabaseProxy` | Access to external MongoDB clusters |
|
|
763
|
+
| `self.session` | `Session` | Request-scoped session (`session.state`, `session.user`) |
|
|
665
764
|
| `self.current_user` | `dict \| None` | Authenticated user document (requires `REQUIRE_AUTH=true`) |
|
|
666
765
|
| `self.is_authenticated` | `bool` | Whether the request is authenticated |
|
|
667
766
|
| `self.auth_data` | `dict \| None` | Full authentication data |
|
|
@@ -710,14 +809,14 @@ The framework includes a built-in token-based authentication middleware.
|
|
|
710
809
|
### Configuration
|
|
711
810
|
|
|
712
811
|
```bash
|
|
713
|
-
|
|
812
|
+
AUTHORIZATION=full # Authorization mode: 'user', 'state', or 'full' (default: empty/disabled)
|
|
714
813
|
AUTH_DB_NAME=my_database # MongoDB database where tokens are stored
|
|
715
814
|
AUTH_BYPASS_TOKEN=secret123 # Master token to bypass auth (for internal use)
|
|
716
815
|
```
|
|
717
816
|
|
|
718
817
|
### Using the authenticated user
|
|
719
818
|
|
|
720
|
-
When `
|
|
819
|
+
When `AUTHORIZATION` is `user` or `full`, every request must include a valid `Authorization: Bearer <token>` header. The authenticated user is available via `self.current_user`:
|
|
721
820
|
|
|
722
821
|
```python
|
|
723
822
|
class OrderListAPI(API):
|
|
@@ -1018,6 +1117,7 @@ environment_variables = {
|
|
|
1018
1117
|
| `AUTH_BYPASS_TOKEN` | Optional | Master token to bypass authentication |
|
|
1019
1118
|
| `ECS_CLUSTER` | Fargate only | ECS cluster name for `FargateExecutor` |
|
|
1020
1119
|
| `ECS_SUBNETS` | Fargate only | Comma-separated subnet IDs for Fargate tasks |
|
|
1120
|
+
| `CONSTITUTION_STATE` | Fargate only (auto) | State injected automatically by `FargateExecutor` — do not set manually |
|
|
1021
1121
|
| `AWS_REGION` | Fargate/SNS/SQS | AWS region |
|
|
1022
1122
|
| `AWS_ACCOUNT_ID` | SQS `get_queue_url` | AWS account ID |
|
|
1023
1123
|
| `SERVICE_NAME` | SQS `get_queue_url` | Service name prefix for queue name |
|
|
@@ -1028,7 +1128,11 @@ environment_variables = {
|
|
|
1028
1128
|
|
|
1029
1129
|
### SQS Consumer - Batch Mode
|
|
1030
1130
|
|
|
1031
|
-
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages
|
|
1131
|
+
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages.
|
|
1132
|
+
|
|
1133
|
+
**Constitution-state handling in SQS:**
|
|
1134
|
+
- **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.
|
|
1135
|
+
- **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
1136
|
|
|
1033
1137
|
```python
|
|
1034
1138
|
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
@@ -1077,10 +1181,13 @@ class OrderConsumer(SQSConsumer):
|
|
|
1077
1181
|
|
|
1078
1182
|
### SNS Publisher - Batch Publishing
|
|
1079
1183
|
|
|
1184
|
+
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.
|
|
1185
|
+
|
|
1080
1186
|
```python
|
|
1081
1187
|
topic = TitleIndexedTopic()
|
|
1082
1188
|
|
|
1083
1189
|
# Publish multiple messages in a single call
|
|
1190
|
+
# constitution-state is auto-injected as a MessageAttribute on each message
|
|
1084
1191
|
await topic.publish([
|
|
1085
1192
|
{'content': {'id': 'id1', 'title': 'Title 1'}, 'attributes': {'type': 'created'}},
|
|
1086
1193
|
{'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
|
+
- **Session propagation**: Automatic `Session` (state + user) 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,9 @@ 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
|
+
| `Session` | `aws_python_helper` | Request-scoped session object (state + user) |
|
|
46
|
+
| `get_session` | `aws_python_helper` | Read the current Session from async context |
|
|
47
|
+
| `set_session` | `aws_python_helper` | Set the current Session in async context |
|
|
44
48
|
| `MongoJSONEncoder` | `aws_python_helper.utils.json_encoder` | JSON encoder for MongoDB types |
|
|
45
49
|
| `mongo_json_dumps` | `aws_python_helper.utils.json_encoder` | Helper to serialize MongoDB types |
|
|
46
50
|
| `serialize_mongo_types` | `aws_python_helper.utils.serializer` | Recursively serialize MongoDB types |
|
|
@@ -249,6 +253,7 @@ response = lambda_client.invoke(
|
|
|
249
253
|
FunctionName='GenerateRouteLambda',
|
|
250
254
|
InvocationType='RequestResponse',
|
|
251
255
|
Payload=json.dumps({
|
|
256
|
+
'session': {'state': 'connecticut'}, # Required
|
|
252
257
|
'data': {
|
|
253
258
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
254
259
|
}
|
|
@@ -272,6 +277,7 @@ lambda_client.invoke(
|
|
|
272
277
|
FunctionName='GenerateRouteLambda',
|
|
273
278
|
InvocationType='Event', # Asynchronous
|
|
274
279
|
Payload=json.dumps({
|
|
280
|
+
'session': {'state': 'connecticut'}, # Required
|
|
275
281
|
'data': {
|
|
276
282
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
277
283
|
}
|
|
@@ -415,6 +421,7 @@ from aws_python_helper.fargate.executor import FargateExecutor
|
|
|
415
421
|
|
|
416
422
|
def handler(event, context):
|
|
417
423
|
executor = FargateExecutor()
|
|
424
|
+
# session is auto-propagated as SESSION env var (JSON) in the container
|
|
418
425
|
task_arn = executor.run_task(
|
|
419
426
|
'search-tax-by-town',
|
|
420
427
|
envs={'TOWN': 'Norwalk', 'ONLY_TAX': 'true'}
|
|
@@ -479,11 +486,17 @@ The framework provides a `Repository` base class that eliminates repetitive boil
|
|
|
479
486
|
| Property | Type | Default | Required |
|
|
480
487
|
|----------|------|---------|----------|
|
|
481
488
|
| `collection_name` | `str` | — | **Yes** |
|
|
482
|
-
| `
|
|
489
|
+
| `database_key` | `str \| None` | `None` | No — if `None`, uses `session.state` from context |
|
|
483
490
|
| `is_external` | `bool` | `False` | No |
|
|
484
491
|
| `cluster_name` | `str` | `None` | Only if `is_external=True` |
|
|
485
492
|
| `indexes` | `list` | `[]` | No |
|
|
486
493
|
|
|
494
|
+
**`database_key` controls how the database is resolved:**
|
|
495
|
+
- `database_key = "core"` (or any string) → always connects to that specific database.
|
|
496
|
+
- `database_key = None` (default) → reads `session.state` from context automatically. This makes the repository **state-scoped**: it connects to `"connecticut"`, `"new_jersey"`, etc. depending on the current request.
|
|
497
|
+
|
|
498
|
+
Collections are cached per `(database_name, collection_name)` key — state-scoped repositories correctly isolate state between concurrent requests.
|
|
499
|
+
|
|
487
500
|
### Index format
|
|
488
501
|
|
|
489
502
|
```python
|
|
@@ -499,7 +512,7 @@ def indexes(self):
|
|
|
499
512
|
|
|
500
513
|
Indexes are created automatically in the background on first collection access — no need to call any initialization method.
|
|
501
514
|
|
|
502
|
-
### Repository
|
|
515
|
+
### Repository with a fixed database
|
|
503
516
|
|
|
504
517
|
```python
|
|
505
518
|
from aws_python_helper import Repository
|
|
@@ -510,6 +523,10 @@ class TownsRepository(Repository):
|
|
|
510
523
|
def collection_name(self):
|
|
511
524
|
return "towns"
|
|
512
525
|
|
|
526
|
+
@property
|
|
527
|
+
def database_key(self):
|
|
528
|
+
return "core" # always connects to the "core" database
|
|
529
|
+
|
|
513
530
|
@property
|
|
514
531
|
def indexes(self):
|
|
515
532
|
return [
|
|
@@ -527,21 +544,23 @@ class TownsRepository(Repository):
|
|
|
527
544
|
return await self.collection.find_one({"name": name})
|
|
528
545
|
```
|
|
529
546
|
|
|
530
|
-
###
|
|
547
|
+
### State-scoped repository (no `database_key`)
|
|
548
|
+
|
|
549
|
+
When `database_key` is not set, the repository reads `session.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
550
|
|
|
532
551
|
```python
|
|
533
552
|
from aws_python_helper import Repository
|
|
534
553
|
|
|
535
554
|
class LandRecordsRepository(Repository):
|
|
536
555
|
|
|
537
|
-
@property
|
|
538
|
-
def database_name(self):
|
|
539
|
-
return "land_data"
|
|
540
|
-
|
|
541
556
|
@property
|
|
542
557
|
def collection_name(self):
|
|
543
558
|
return "records"
|
|
544
559
|
|
|
560
|
+
# No database_key → uses session.state automatically
|
|
561
|
+
# If session.state = "connecticut" → connects to DB "connecticut"
|
|
562
|
+
# If session.state = "new_jersey" → connects to DB "new_jersey"
|
|
563
|
+
|
|
545
564
|
@property
|
|
546
565
|
def indexes(self):
|
|
547
566
|
return [
|
|
@@ -559,6 +578,8 @@ class LandRecordsRepository(Repository):
|
|
|
559
578
|
return {"upserted": result.upserted_count, "modified": result.modified_count}
|
|
560
579
|
```
|
|
561
580
|
|
|
581
|
+
> **Note:** A `ValueError` is raised at runtime if `database_key` is `None` and `session.state` has not been set. This is prevented automatically by the framework at every entry point (API, Lambda, SQS, Fargate).
|
|
582
|
+
|
|
562
583
|
### Repository on an external cluster
|
|
563
584
|
|
|
564
585
|
```python
|
|
@@ -567,7 +588,7 @@ from aws_python_helper import Repository
|
|
|
567
588
|
class AddressRepository(Repository):
|
|
568
589
|
|
|
569
590
|
@property
|
|
570
|
-
def
|
|
591
|
+
def database_key(self):
|
|
571
592
|
return "smart_data"
|
|
572
593
|
|
|
573
594
|
@property
|
|
@@ -607,6 +628,84 @@ class MyAPI(API):
|
|
|
607
628
|
|
|
608
629
|
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
630
|
|
|
631
|
+
## 🌐 Session Context
|
|
632
|
+
|
|
633
|
+
The framework propagates a `Session` object automatically across the entire async call chain using Python's `contextvars.ContextVar`. The session holds `state` (for multi-state DB routing) and `user` (authenticated user from the auth middleware).
|
|
634
|
+
|
|
635
|
+
### How the framework injects it at each entry point
|
|
636
|
+
|
|
637
|
+
| Entry point | How the session is read |
|
|
638
|
+
|-------------|-------------------------|
|
|
639
|
+
| **API Gateway** | `constitution-state` header → `session.state` (when `AUTHORIZATION` includes `state`); auth middleware → `session.user` (when includes `user`). Returns `400` if required header is missing |
|
|
640
|
+
| **Standalone Lambda** | `session` dict in the event payload — **required** (must include `state`), raises `ValueError` if missing |
|
|
641
|
+
| **SQS Consumer (single mode)** | Per-record: reads `session` from SNS `MessageAttributes` (JSON), falls back to legacy `body.constitution_state` |
|
|
642
|
+
| **SQS Consumer (batch mode)** | Groups records by `session.state`; calls `process_batch()` once per group with the correct session in context |
|
|
643
|
+
| **Fargate Task** | `SESSION` env var (JSON) — auto-injected by `FargateExecutor` |
|
|
644
|
+
|
|
645
|
+
### How the framework propagates it to downstream services
|
|
646
|
+
|
|
647
|
+
| Downstream service | Propagation mechanism |
|
|
648
|
+
|--------------------|-----------------------|
|
|
649
|
+
| **SNS Publisher** | Auto-injects the full session as a `session` `MessageAttribute` (JSON string) on every published message |
|
|
650
|
+
| **FargateExecutor** | Auto-injects `SESSION` as a JSON env var when launching Fargate containers |
|
|
651
|
+
|
|
652
|
+
This means that an API call with `constitution-state: connecticut` will automatically carry the full session (state + user) through SNS → SQS → Fargate without any code changes in your consumers or tasks.
|
|
653
|
+
|
|
654
|
+
### Accessing the session in handlers
|
|
655
|
+
|
|
656
|
+
All handler base classes expose a `self.session` property:
|
|
657
|
+
|
|
658
|
+
```python
|
|
659
|
+
class MyAPI(API):
|
|
660
|
+
async def process(self):
|
|
661
|
+
state = self.session.state # e.g. "connecticut"
|
|
662
|
+
user = self.session.user # authenticated user dict, or None
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
Available in `API`, `SQSConsumer`, `Lambda`, and `FargateTask`.
|
|
666
|
+
|
|
667
|
+
### State-scoped repositories
|
|
668
|
+
|
|
669
|
+
Repositories with no `database_key` (default) read `session.state` from context to resolve the target database automatically. See the [Repository Pattern](#️-repository-pattern) section for details.
|
|
670
|
+
|
|
671
|
+
### Manual access
|
|
672
|
+
|
|
673
|
+
If you need to read or set the session manually (e.g., in tests or utility code):
|
|
674
|
+
|
|
675
|
+
```python
|
|
676
|
+
from aws_python_helper import Session, get_session, set_session
|
|
677
|
+
|
|
678
|
+
session = get_session() # returns current Session (creates empty one if not set)
|
|
679
|
+
session.state # e.g. "connecticut", or None if not set
|
|
680
|
+
session.user # authenticated user dict, or None
|
|
681
|
+
|
|
682
|
+
set_session(Session(state="new_jersey")) # set manually (the framework does this automatically)
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### API example — `constitution-state` header
|
|
686
|
+
|
|
687
|
+
```
|
|
688
|
+
GET /constitutions HTTP/1.1
|
|
689
|
+
constitution-state: connecticut
|
|
690
|
+
Authorization: Bearer <token>
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Lambda invocation example — `session` in event
|
|
694
|
+
|
|
695
|
+
```python
|
|
696
|
+
import boto3, json
|
|
697
|
+
|
|
698
|
+
lambda_client = boto3.client('lambda')
|
|
699
|
+
lambda_client.invoke(
|
|
700
|
+
FunctionName='MyLambdaFunction',
|
|
701
|
+
InvocationType='RequestResponse',
|
|
702
|
+
Payload=json.dumps({
|
|
703
|
+
'session': {'state': 'connecticut'}, # Required
|
|
704
|
+
'data': {'key': 'value'}
|
|
705
|
+
})
|
|
706
|
+
)
|
|
707
|
+
```
|
|
708
|
+
|
|
610
709
|
## 🔄 Routing Convention
|
|
611
710
|
|
|
612
711
|
The framework uses convention over configuration for the routing:
|
|
@@ -642,6 +741,7 @@ All properties and methods available inside an `API` subclass:
|
|
|
642
741
|
| `self.query_parameters` | `dict` | Query string parameters |
|
|
643
742
|
| `self.db` | `DatabaseProxy` | Access to main MongoDB cluster |
|
|
644
743
|
| `self.external_db` | `ExternalDatabaseProxy` | Access to external MongoDB clusters |
|
|
744
|
+
| `self.session` | `Session` | Request-scoped session (`session.state`, `session.user`) |
|
|
645
745
|
| `self.current_user` | `dict \| None` | Authenticated user document (requires `REQUIRE_AUTH=true`) |
|
|
646
746
|
| `self.is_authenticated` | `bool` | Whether the request is authenticated |
|
|
647
747
|
| `self.auth_data` | `dict \| None` | Full authentication data |
|
|
@@ -690,14 +790,14 @@ The framework includes a built-in token-based authentication middleware.
|
|
|
690
790
|
### Configuration
|
|
691
791
|
|
|
692
792
|
```bash
|
|
693
|
-
|
|
793
|
+
AUTHORIZATION=full # Authorization mode: 'user', 'state', or 'full' (default: empty/disabled)
|
|
694
794
|
AUTH_DB_NAME=my_database # MongoDB database where tokens are stored
|
|
695
795
|
AUTH_BYPASS_TOKEN=secret123 # Master token to bypass auth (for internal use)
|
|
696
796
|
```
|
|
697
797
|
|
|
698
798
|
### Using the authenticated user
|
|
699
799
|
|
|
700
|
-
When `
|
|
800
|
+
When `AUTHORIZATION` is `user` or `full`, every request must include a valid `Authorization: Bearer <token>` header. The authenticated user is available via `self.current_user`:
|
|
701
801
|
|
|
702
802
|
```python
|
|
703
803
|
class OrderListAPI(API):
|
|
@@ -998,6 +1098,7 @@ environment_variables = {
|
|
|
998
1098
|
| `AUTH_BYPASS_TOKEN` | Optional | Master token to bypass authentication |
|
|
999
1099
|
| `ECS_CLUSTER` | Fargate only | ECS cluster name for `FargateExecutor` |
|
|
1000
1100
|
| `ECS_SUBNETS` | Fargate only | Comma-separated subnet IDs for Fargate tasks |
|
|
1101
|
+
| `CONSTITUTION_STATE` | Fargate only (auto) | State injected automatically by `FargateExecutor` — do not set manually |
|
|
1001
1102
|
| `AWS_REGION` | Fargate/SNS/SQS | AWS region |
|
|
1002
1103
|
| `AWS_ACCOUNT_ID` | SQS `get_queue_url` | AWS account ID |
|
|
1003
1104
|
| `SERVICE_NAME` | SQS `get_queue_url` | Service name prefix for queue name |
|
|
@@ -1008,7 +1109,11 @@ environment_variables = {
|
|
|
1008
1109
|
|
|
1009
1110
|
### SQS Consumer - Batch Mode
|
|
1010
1111
|
|
|
1011
|
-
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages
|
|
1112
|
+
By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages.
|
|
1113
|
+
|
|
1114
|
+
**Constitution-state handling in SQS:**
|
|
1115
|
+
- **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.
|
|
1116
|
+
- **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
1117
|
|
|
1013
1118
|
```python
|
|
1014
1119
|
from aws_python_helper.sqs.consumer_base import SQSConsumer
|
|
@@ -1057,10 +1162,13 @@ class OrderConsumer(SQSConsumer):
|
|
|
1057
1162
|
|
|
1058
1163
|
### SNS Publisher - Batch Publishing
|
|
1059
1164
|
|
|
1165
|
+
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.
|
|
1166
|
+
|
|
1060
1167
|
```python
|
|
1061
1168
|
topic = TitleIndexedTopic()
|
|
1062
1169
|
|
|
1063
1170
|
# Publish multiple messages in a single call
|
|
1171
|
+
# constitution-state is auto-injected as a MessageAttribute on each message
|
|
1064
1172
|
await topic.publish([
|
|
1065
1173
|
{'content': {'id': 'id1', 'title': 'Title 1'}, 'attributes': {'type': 'created'}},
|
|
1066
1174
|
{'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.session import Session, get_session, set_session
|
|
32
|
+
|
|
30
33
|
|
|
31
34
|
__all__ = [
|
|
32
35
|
'API',
|
|
@@ -45,5 +48,8 @@ __all__ = [
|
|
|
45
48
|
'MongoJSONEncoder',
|
|
46
49
|
'mongo_json_dumps',
|
|
47
50
|
'serialize_mongo_types',
|
|
51
|
+
'Session',
|
|
52
|
+
'get_session',
|
|
53
|
+
'set_session',
|
|
48
54
|
]
|
|
49
55
|
|
|
@@ -9,6 +9,7 @@ from ..database.mongo_manager import MongoManager
|
|
|
9
9
|
from ..database.database_proxy import DatabaseProxy
|
|
10
10
|
from ..database.external_mongo_manager import ExternalMongoManager
|
|
11
11
|
from ..database.external_database_proxy import ExternalDatabaseProxy
|
|
12
|
+
from ..context.session import get_session
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class API(ABC):
|
|
@@ -136,17 +137,31 @@ class API(ABC):
|
|
|
136
137
|
self._external_db = ExternalDatabaseProxy()
|
|
137
138
|
return self._external_db
|
|
138
139
|
|
|
140
|
+
@property
|
|
141
|
+
def session(self):
|
|
142
|
+
"""
|
|
143
|
+
Request-scoped session with state, user, and extensible properties.
|
|
144
|
+
|
|
145
|
+
Populated automatically by the dispatcher from the request headers
|
|
146
|
+
and authentication middleware.
|
|
147
|
+
|
|
148
|
+
Usage:
|
|
149
|
+
state = self.session.state # constitution-state
|
|
150
|
+
user = self.session.user # authenticated user dict
|
|
151
|
+
"""
|
|
152
|
+
return get_session()
|
|
153
|
+
|
|
139
154
|
@property
|
|
140
155
|
def current_user(self) -> Optional[Dict[str, Any]]:
|
|
141
156
|
"""
|
|
142
157
|
Current authenticated user or None if not authenticated
|
|
143
|
-
|
|
158
|
+
|
|
144
159
|
This property is populated by the authentication middleware
|
|
145
|
-
when
|
|
146
|
-
|
|
160
|
+
when AUTHORIZATION is 'user' or 'full'.
|
|
161
|
+
|
|
147
162
|
Returns:
|
|
148
163
|
Dict with user data (email, role, name, etc.) or None
|
|
149
|
-
|
|
164
|
+
|
|
150
165
|
Example:
|
|
151
166
|
if self.is_authenticated:
|
|
152
167
|
user_email = self.current_user['email']
|
|
@@ -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.session import get_session
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -74,11 +75,35 @@ class Dispatcher:
|
|
|
74
75
|
# 1. Prepare - Load controller and inject properties
|
|
75
76
|
api = self._prepare()
|
|
76
77
|
|
|
77
|
-
# 2.
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
# 2. Authorization based on mode
|
|
79
|
+
authorization = os.getenv('AUTHORIZATION', '').lower()
|
|
80
|
+
requires_user = authorization in ('user', 'full')
|
|
81
|
+
requires_state = authorization in ('state', 'full')
|
|
82
|
+
|
|
83
|
+
# 2a. Authenticate (if mode requires user)
|
|
84
|
+
if requires_user:
|
|
80
85
|
await self._authenticate(api)
|
|
81
|
-
|
|
86
|
+
|
|
87
|
+
# 2b. Validate state header (if mode requires state)
|
|
88
|
+
if requires_state:
|
|
89
|
+
state = self.headers.get('constitution-state')
|
|
90
|
+
if not state:
|
|
91
|
+
return {
|
|
92
|
+
'code': 400,
|
|
93
|
+
'body': {
|
|
94
|
+
'error': 'Bad Request',
|
|
95
|
+
'message': "Header 'constitution-state' is required"
|
|
96
|
+
},
|
|
97
|
+
'headers': {}
|
|
98
|
+
}
|
|
99
|
+
session = get_session()
|
|
100
|
+
session.state = state
|
|
101
|
+
|
|
102
|
+
# 2c. Inject user into session (if authenticated)
|
|
103
|
+
if api._current_user:
|
|
104
|
+
session = get_session()
|
|
105
|
+
session.user = api._current_user
|
|
106
|
+
|
|
82
107
|
# 3. Validate
|
|
83
108
|
await api.validate()
|
|
84
109
|
|