aws-python-helper 0.32.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.32.0 → aws_python_helper-0.33.0}/PKG-INFO +54 -37
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/README.md +52 -34
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/__init__.py +4 -3
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/api/base.py +19 -4
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/api/dispatcher.py +27 -16
- 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.32.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/executor.py +11 -5
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/task_base.py +16 -4
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/base.py +16 -6
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/repository/base.py +14 -14
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/sns/publisher.py +7 -6
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/consumer_base.py +48 -27
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/PKG-INFO +54 -37
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/SOURCES.txt +1 -1
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/pyproject.toml +2 -3
- aws_python_helper-0.32.0/aws_python_helper/context/__init__.py +0 -3
- aws_python_helper-0.32.0/aws_python_helper/context/state.py +0 -22
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-0.32.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-0.32.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,7 +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
|
|
32
|
-
- **
|
|
31
|
+
- **Session propagation**: Automatic `Session` (state + user) propagation across the entire call chain for per-state database routing
|
|
33
32
|
- **SQS Consumers**: Same pattern to process SQS messages (single or batch mode)
|
|
34
33
|
- **SNS Publishers**: Same pattern to publish messages to SNS topics
|
|
35
34
|
- **Fargate Tasks**: Same pattern to run tasks in Fargate containers
|
|
@@ -62,8 +61,9 @@ All available classes and functions:
|
|
|
62
61
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
63
62
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
64
63
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
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 |
|
|
67
67
|
| `MongoJSONEncoder` | `aws_python_helper.utils.json_encoder` | JSON encoder for MongoDB types |
|
|
68
68
|
| `mongo_json_dumps` | `aws_python_helper.utils.json_encoder` | Helper to serialize MongoDB types |
|
|
69
69
|
| `serialize_mongo_types` | `aws_python_helper.utils.serializer` | Recursively serialize MongoDB types |
|
|
@@ -272,7 +272,7 @@ response = lambda_client.invoke(
|
|
|
272
272
|
FunctionName='GenerateRouteLambda',
|
|
273
273
|
InvocationType='RequestResponse',
|
|
274
274
|
Payload=json.dumps({
|
|
275
|
-
'
|
|
275
|
+
'session': {'state': 'connecticut'}, # Required
|
|
276
276
|
'data': {
|
|
277
277
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
278
278
|
}
|
|
@@ -296,7 +296,7 @@ lambda_client.invoke(
|
|
|
296
296
|
FunctionName='GenerateRouteLambda',
|
|
297
297
|
InvocationType='Event', # Asynchronous
|
|
298
298
|
Payload=json.dumps({
|
|
299
|
-
'
|
|
299
|
+
'session': {'state': 'connecticut'}, # Required
|
|
300
300
|
'data': {
|
|
301
301
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
302
302
|
}
|
|
@@ -440,7 +440,7 @@ from aws_python_helper.fargate.executor import FargateExecutor
|
|
|
440
440
|
|
|
441
441
|
def handler(event, context):
|
|
442
442
|
executor = FargateExecutor()
|
|
443
|
-
#
|
|
443
|
+
# session is auto-propagated as SESSION env var (JSON) in the container
|
|
444
444
|
task_arn = executor.run_task(
|
|
445
445
|
'search-tax-by-town',
|
|
446
446
|
envs={'TOWN': 'Norwalk', 'ONLY_TAX': 'true'}
|
|
@@ -505,14 +505,14 @@ The framework provides a `Repository` base class that eliminates repetitive boil
|
|
|
505
505
|
| Property | Type | Default | Required |
|
|
506
506
|
|----------|------|---------|----------|
|
|
507
507
|
| `collection_name` | `str` | — | **Yes** |
|
|
508
|
-
| `database_key` | `str \| None` | `None` | No — if `None`, uses `
|
|
508
|
+
| `database_key` | `str \| None` | `None` | No — if `None`, uses `session.state` from context |
|
|
509
509
|
| `is_external` | `bool` | `False` | No |
|
|
510
510
|
| `cluster_name` | `str` | `None` | Only if `is_external=True` |
|
|
511
511
|
| `indexes` | `list` | `[]` | No |
|
|
512
512
|
|
|
513
513
|
**`database_key` controls how the database is resolved:**
|
|
514
514
|
- `database_key = "core"` (or any string) → always connects to that specific database.
|
|
515
|
-
- `database_key = None` (default) → reads `
|
|
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
516
|
|
|
517
517
|
Collections are cached per `(database_name, collection_name)` key — state-scoped repositories correctly isolate state between concurrent requests.
|
|
518
518
|
|
|
@@ -565,7 +565,7 @@ class TownsRepository(Repository):
|
|
|
565
565
|
|
|
566
566
|
### State-scoped repository (no `database_key`)
|
|
567
567
|
|
|
568
|
-
When `database_key` is not set, the repository reads
|
|
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.
|
|
569
569
|
|
|
570
570
|
```python
|
|
571
571
|
from aws_python_helper import Repository
|
|
@@ -576,9 +576,9 @@ class LandRecordsRepository(Repository):
|
|
|
576
576
|
def collection_name(self):
|
|
577
577
|
return "records"
|
|
578
578
|
|
|
579
|
-
# No database_key → uses
|
|
580
|
-
# If
|
|
581
|
-
# If
|
|
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
582
|
|
|
583
583
|
@property
|
|
584
584
|
def indexes(self):
|
|
@@ -597,7 +597,7 @@ class LandRecordsRepository(Repository):
|
|
|
597
597
|
return {"upserted": result.upserted_count, "modified": result.modified_count}
|
|
598
598
|
```
|
|
599
599
|
|
|
600
|
-
> **Note:** A `ValueError` is raised at runtime if `database_key` is `None` and `
|
|
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
601
|
|
|
602
602
|
### Repository on an external cluster
|
|
603
603
|
|
|
@@ -647,42 +647,58 @@ class MyAPI(API):
|
|
|
647
647
|
|
|
648
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.
|
|
649
649
|
|
|
650
|
-
## 🌐
|
|
650
|
+
## 🌐 Session Context
|
|
651
651
|
|
|
652
|
-
The framework
|
|
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
653
|
|
|
654
654
|
### How the framework injects it at each entry point
|
|
655
655
|
|
|
656
|
-
| Entry point | How
|
|
657
|
-
|
|
658
|
-
| **API Gateway** |
|
|
659
|
-
| **Standalone Lambda** |
|
|
660
|
-
| **SQS Consumer (single mode)** | Per-record: reads from SNS `MessageAttributes
|
|
661
|
-
| **SQS Consumer (batch mode)** | Groups records by state
|
|
662
|
-
| **Fargate Task** |
|
|
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
663
|
|
|
664
664
|
### How the framework propagates it to downstream services
|
|
665
665
|
|
|
666
666
|
| Downstream service | Propagation mechanism |
|
|
667
667
|
|--------------------|-----------------------|
|
|
668
|
-
| **SNS Publisher** | Auto-injects
|
|
669
|
-
| **FargateExecutor** | Auto-injects `
|
|
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
670
|
|
|
671
|
-
This means that an API call with `constitution-state: connecticut` will automatically carry
|
|
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`.
|
|
672
685
|
|
|
673
686
|
### State-scoped repositories
|
|
674
687
|
|
|
675
|
-
Repositories with no `database_key` (default) read `
|
|
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.
|
|
676
689
|
|
|
677
690
|
### Manual access
|
|
678
691
|
|
|
679
|
-
If you need to read or set the
|
|
692
|
+
If you need to read or set the session manually (e.g., in tests or utility code):
|
|
680
693
|
|
|
681
694
|
```python
|
|
682
|
-
from aws_python_helper import
|
|
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
|
|
683
700
|
|
|
684
|
-
state
|
|
685
|
-
set_state("new_jersey") # set manually (the framework does this automatically)
|
|
701
|
+
set_session(Session(state="new_jersey")) # set manually (the framework does this automatically)
|
|
686
702
|
```
|
|
687
703
|
|
|
688
704
|
### API example — `constitution-state` header
|
|
@@ -693,7 +709,7 @@ constitution-state: connecticut
|
|
|
693
709
|
Authorization: Bearer <token>
|
|
694
710
|
```
|
|
695
711
|
|
|
696
|
-
### Lambda invocation example — `
|
|
712
|
+
### Lambda invocation example — `session` in event
|
|
697
713
|
|
|
698
714
|
```python
|
|
699
715
|
import boto3, json
|
|
@@ -703,7 +719,7 @@ lambda_client.invoke(
|
|
|
703
719
|
FunctionName='MyLambdaFunction',
|
|
704
720
|
InvocationType='RequestResponse',
|
|
705
721
|
Payload=json.dumps({
|
|
706
|
-
'
|
|
722
|
+
'session': {'state': 'connecticut'}, # Required
|
|
707
723
|
'data': {'key': 'value'}
|
|
708
724
|
})
|
|
709
725
|
)
|
|
@@ -744,6 +760,7 @@ All properties and methods available inside an `API` subclass:
|
|
|
744
760
|
| `self.query_parameters` | `dict` | Query string parameters |
|
|
745
761
|
| `self.db` | `DatabaseProxy` | Access to main MongoDB cluster |
|
|
746
762
|
| `self.external_db` | `ExternalDatabaseProxy` | Access to external MongoDB clusters |
|
|
763
|
+
| `self.session` | `Session` | Request-scoped session (`session.state`, `session.user`) |
|
|
747
764
|
| `self.current_user` | `dict \| None` | Authenticated user document (requires `REQUIRE_AUTH=true`) |
|
|
748
765
|
| `self.is_authenticated` | `bool` | Whether the request is authenticated |
|
|
749
766
|
| `self.auth_data` | `dict \| None` | Full authentication data |
|
|
@@ -792,14 +809,14 @@ The framework includes a built-in token-based authentication middleware.
|
|
|
792
809
|
### Configuration
|
|
793
810
|
|
|
794
811
|
```bash
|
|
795
|
-
|
|
812
|
+
AUTHORIZATION=full # Authorization mode: 'user', 'state', or 'full' (default: empty/disabled)
|
|
796
813
|
AUTH_DB_NAME=my_database # MongoDB database where tokens are stored
|
|
797
814
|
AUTH_BYPASS_TOKEN=secret123 # Master token to bypass auth (for internal use)
|
|
798
815
|
```
|
|
799
816
|
|
|
800
817
|
### Using the authenticated user
|
|
801
818
|
|
|
802
|
-
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`:
|
|
803
820
|
|
|
804
821
|
```python
|
|
805
822
|
class OrderListAPI(API):
|
|
@@ -9,7 +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
|
-
- **
|
|
12
|
+
- **Session propagation**: Automatic `Session` (state + user) propagation across the entire call chain for per-state database routing
|
|
13
13
|
- **SQS Consumers**: Same pattern to process SQS messages (single or batch mode)
|
|
14
14
|
- **SNS Publishers**: Same pattern to publish messages to SNS topics
|
|
15
15
|
- **Fargate Tasks**: Same pattern to run tasks in Fargate containers
|
|
@@ -42,8 +42,9 @@ All available classes and functions:
|
|
|
42
42
|
| `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
|
|
43
43
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
44
44
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
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 |
|
|
47
48
|
| `MongoJSONEncoder` | `aws_python_helper.utils.json_encoder` | JSON encoder for MongoDB types |
|
|
48
49
|
| `mongo_json_dumps` | `aws_python_helper.utils.json_encoder` | Helper to serialize MongoDB types |
|
|
49
50
|
| `serialize_mongo_types` | `aws_python_helper.utils.serializer` | Recursively serialize MongoDB types |
|
|
@@ -252,7 +253,7 @@ response = lambda_client.invoke(
|
|
|
252
253
|
FunctionName='GenerateRouteLambda',
|
|
253
254
|
InvocationType='RequestResponse',
|
|
254
255
|
Payload=json.dumps({
|
|
255
|
-
'
|
|
256
|
+
'session': {'state': 'connecticut'}, # Required
|
|
256
257
|
'data': {
|
|
257
258
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
258
259
|
}
|
|
@@ -276,7 +277,7 @@ lambda_client.invoke(
|
|
|
276
277
|
FunctionName='GenerateRouteLambda',
|
|
277
278
|
InvocationType='Event', # Asynchronous
|
|
278
279
|
Payload=json.dumps({
|
|
279
|
-
'
|
|
280
|
+
'session': {'state': 'connecticut'}, # Required
|
|
280
281
|
'data': {
|
|
281
282
|
'shipping_id': '507f1f77bcf86cd799439011'
|
|
282
283
|
}
|
|
@@ -420,7 +421,7 @@ from aws_python_helper.fargate.executor import FargateExecutor
|
|
|
420
421
|
|
|
421
422
|
def handler(event, context):
|
|
422
423
|
executor = FargateExecutor()
|
|
423
|
-
#
|
|
424
|
+
# session is auto-propagated as SESSION env var (JSON) in the container
|
|
424
425
|
task_arn = executor.run_task(
|
|
425
426
|
'search-tax-by-town',
|
|
426
427
|
envs={'TOWN': 'Norwalk', 'ONLY_TAX': 'true'}
|
|
@@ -485,14 +486,14 @@ The framework provides a `Repository` base class that eliminates repetitive boil
|
|
|
485
486
|
| Property | Type | Default | Required |
|
|
486
487
|
|----------|------|---------|----------|
|
|
487
488
|
| `collection_name` | `str` | — | **Yes** |
|
|
488
|
-
| `database_key` | `str \| None` | `None` | No — if `None`, uses `
|
|
489
|
+
| `database_key` | `str \| None` | `None` | No — if `None`, uses `session.state` from context |
|
|
489
490
|
| `is_external` | `bool` | `False` | No |
|
|
490
491
|
| `cluster_name` | `str` | `None` | Only if `is_external=True` |
|
|
491
492
|
| `indexes` | `list` | `[]` | No |
|
|
492
493
|
|
|
493
494
|
**`database_key` controls how the database is resolved:**
|
|
494
495
|
- `database_key = "core"` (or any string) → always connects to that specific database.
|
|
495
|
-
- `database_key = None` (default) → reads `
|
|
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.
|
|
496
497
|
|
|
497
498
|
Collections are cached per `(database_name, collection_name)` key — state-scoped repositories correctly isolate state between concurrent requests.
|
|
498
499
|
|
|
@@ -545,7 +546,7 @@ class TownsRepository(Repository):
|
|
|
545
546
|
|
|
546
547
|
### State-scoped repository (no `database_key`)
|
|
547
548
|
|
|
548
|
-
When `database_key` is not set, the repository reads
|
|
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.
|
|
549
550
|
|
|
550
551
|
```python
|
|
551
552
|
from aws_python_helper import Repository
|
|
@@ -556,9 +557,9 @@ class LandRecordsRepository(Repository):
|
|
|
556
557
|
def collection_name(self):
|
|
557
558
|
return "records"
|
|
558
559
|
|
|
559
|
-
# No database_key → uses
|
|
560
|
-
# If
|
|
561
|
-
# If
|
|
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"
|
|
562
563
|
|
|
563
564
|
@property
|
|
564
565
|
def indexes(self):
|
|
@@ -577,7 +578,7 @@ class LandRecordsRepository(Repository):
|
|
|
577
578
|
return {"upserted": result.upserted_count, "modified": result.modified_count}
|
|
578
579
|
```
|
|
579
580
|
|
|
580
|
-
> **Note:** A `ValueError` is raised at runtime if `database_key` is `None` and `
|
|
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).
|
|
581
582
|
|
|
582
583
|
### Repository on an external cluster
|
|
583
584
|
|
|
@@ -627,42 +628,58 @@ class MyAPI(API):
|
|
|
627
628
|
|
|
628
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.
|
|
629
630
|
|
|
630
|
-
## 🌐
|
|
631
|
+
## 🌐 Session Context
|
|
631
632
|
|
|
632
|
-
The framework
|
|
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).
|
|
633
634
|
|
|
634
635
|
### How the framework injects it at each entry point
|
|
635
636
|
|
|
636
|
-
| Entry point | How
|
|
637
|
-
|
|
638
|
-
| **API Gateway** |
|
|
639
|
-
| **Standalone Lambda** |
|
|
640
|
-
| **SQS Consumer (single mode)** | Per-record: reads from SNS `MessageAttributes
|
|
641
|
-
| **SQS Consumer (batch mode)** | Groups records by state
|
|
642
|
-
| **Fargate Task** |
|
|
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` |
|
|
643
644
|
|
|
644
645
|
### How the framework propagates it to downstream services
|
|
645
646
|
|
|
646
647
|
| Downstream service | Propagation mechanism |
|
|
647
648
|
|--------------------|-----------------------|
|
|
648
|
-
| **SNS Publisher** | Auto-injects
|
|
649
|
-
| **FargateExecutor** | Auto-injects `
|
|
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 |
|
|
650
651
|
|
|
651
|
-
This means that an API call with `constitution-state: connecticut` will automatically carry
|
|
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`.
|
|
652
666
|
|
|
653
667
|
### State-scoped repositories
|
|
654
668
|
|
|
655
|
-
Repositories with no `database_key` (default) read `
|
|
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.
|
|
656
670
|
|
|
657
671
|
### Manual access
|
|
658
672
|
|
|
659
|
-
If you need to read or set the
|
|
673
|
+
If you need to read or set the session manually (e.g., in tests or utility code):
|
|
660
674
|
|
|
661
675
|
```python
|
|
662
|
-
from aws_python_helper import
|
|
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
|
|
663
681
|
|
|
664
|
-
state
|
|
665
|
-
set_state("new_jersey") # set manually (the framework does this automatically)
|
|
682
|
+
set_session(Session(state="new_jersey")) # set manually (the framework does this automatically)
|
|
666
683
|
```
|
|
667
684
|
|
|
668
685
|
### API example — `constitution-state` header
|
|
@@ -673,7 +690,7 @@ constitution-state: connecticut
|
|
|
673
690
|
Authorization: Bearer <token>
|
|
674
691
|
```
|
|
675
692
|
|
|
676
|
-
### Lambda invocation example — `
|
|
693
|
+
### Lambda invocation example — `session` in event
|
|
677
694
|
|
|
678
695
|
```python
|
|
679
696
|
import boto3, json
|
|
@@ -683,7 +700,7 @@ lambda_client.invoke(
|
|
|
683
700
|
FunctionName='MyLambdaFunction',
|
|
684
701
|
InvocationType='RequestResponse',
|
|
685
702
|
Payload=json.dumps({
|
|
686
|
-
'
|
|
703
|
+
'session': {'state': 'connecticut'}, # Required
|
|
687
704
|
'data': {'key': 'value'}
|
|
688
705
|
})
|
|
689
706
|
)
|
|
@@ -724,6 +741,7 @@ All properties and methods available inside an `API` subclass:
|
|
|
724
741
|
| `self.query_parameters` | `dict` | Query string parameters |
|
|
725
742
|
| `self.db` | `DatabaseProxy` | Access to main MongoDB cluster |
|
|
726
743
|
| `self.external_db` | `ExternalDatabaseProxy` | Access to external MongoDB clusters |
|
|
744
|
+
| `self.session` | `Session` | Request-scoped session (`session.state`, `session.user`) |
|
|
727
745
|
| `self.current_user` | `dict \| None` | Authenticated user document (requires `REQUIRE_AUTH=true`) |
|
|
728
746
|
| `self.is_authenticated` | `bool` | Whether the request is authenticated |
|
|
729
747
|
| `self.auth_data` | `dict \| None` | Full authentication data |
|
|
@@ -772,14 +790,14 @@ The framework includes a built-in token-based authentication middleware.
|
|
|
772
790
|
### Configuration
|
|
773
791
|
|
|
774
792
|
```bash
|
|
775
|
-
|
|
793
|
+
AUTHORIZATION=full # Authorization mode: 'user', 'state', or 'full' (default: empty/disabled)
|
|
776
794
|
AUTH_DB_NAME=my_database # MongoDB database where tokens are stored
|
|
777
795
|
AUTH_BYPASS_TOKEN=secret123 # Master token to bypass auth (for internal use)
|
|
778
796
|
```
|
|
779
797
|
|
|
780
798
|
### Using the authenticated user
|
|
781
799
|
|
|
782
|
-
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`:
|
|
783
801
|
|
|
784
802
|
```python
|
|
785
803
|
class OrderListAPI(API):
|
|
@@ -28,7 +28,7 @@ from .utils.serializer import serialize_mongo_types
|
|
|
28
28
|
from .repository.base import Repository
|
|
29
29
|
|
|
30
30
|
# Context
|
|
31
|
-
from .context.
|
|
31
|
+
from .context.session import Session, get_session, set_session
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
__all__ = [
|
|
@@ -48,7 +48,8 @@ __all__ = [
|
|
|
48
48
|
'MongoJSONEncoder',
|
|
49
49
|
'mongo_json_dumps',
|
|
50
50
|
'serialize_mongo_types',
|
|
51
|
-
'
|
|
52
|
-
'
|
|
51
|
+
'Session',
|
|
52
|
+
'get_session',
|
|
53
|
+
'set_session',
|
|
53
54
|
]
|
|
54
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,7 +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.
|
|
15
|
+
from ..context.session import get_session
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
@@ -75,23 +75,34 @@ class Dispatcher:
|
|
|
75
75
|
# 1. Prepare - Load controller and inject properties
|
|
76
76
|
api = self._prepare()
|
|
77
77
|
|
|
78
|
-
# 2.
|
|
79
|
-
|
|
80
|
-
|
|
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:
|
|
81
85
|
await self._authenticate(api)
|
|
82
86
|
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
95
106
|
|
|
96
107
|
# 3. Validate
|
|
97
108
|
await api.validate()
|