aws-python-helper 0.32.0__tar.gz → 0.34.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/PKG-INFO +56 -39
  2. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/README.md +54 -36
  3. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/__init__.py +7 -3
  4. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/api/base.py +19 -4
  5. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/api/dispatcher.py +39 -16
  6. aws_python_helper-0.34.0/aws_python_helper/context/__init__.py +4 -0
  7. aws_python_helper-0.34.0/aws_python_helper/context/session.py +88 -0
  8. aws_python_helper-0.34.0/aws_python_helper/context/state_validator.py +54 -0
  9. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/fargate/executor.py +11 -5
  10. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/fargate/task_base.py +16 -4
  11. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/lambda_standalone/base.py +18 -6
  12. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/repository/base.py +14 -14
  13. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/sns/publisher.py +27 -10
  14. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/sqs/consumer_base.py +45 -31
  15. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper.egg-info/PKG-INFO +56 -39
  16. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper.egg-info/SOURCES.txt +2 -1
  17. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/pyproject.toml +2 -3
  18. aws_python_helper-0.32.0/aws_python_helper/context/__init__.py +0 -3
  19. aws_python_helper-0.32.0/aws_python_helper/context/state.py +0 -22
  20. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/api/__init__.py +0 -0
  21. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/api/auth_middleware.py +0 -0
  22. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/api/auth_validators.py +0 -0
  23. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/api/exceptions.py +0 -0
  24. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/api/fetcher.py +0 -0
  25. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/api/handler.py +0 -0
  26. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/database/__init__.py +0 -0
  27. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/database/database_proxy.py +0 -0
  28. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/database/external_database_proxy.py +0 -0
  29. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
  30. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/database/mongo_manager.py +0 -0
  31. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/fargate/__init__.py +0 -0
  32. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/fargate/fetcher.py +0 -0
  33. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/fargate/handler.py +0 -0
  34. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
  35. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
  36. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
  37. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/repository/__init__.py +0 -0
  38. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/sns/__init__.py +0 -0
  39. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/sqs/__init__.py +0 -0
  40. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/sqs/fetcher.py +0 -0
  41. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/sqs/handler.py +0 -0
  42. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/utils/__init__.py +0 -0
  43. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/utils/json_encoder.py +0 -0
  44. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/utils/response.py +0 -0
  45. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper/utils/serializer.py +0 -0
  46. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
  47. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper.egg-info/requires.txt +0 -0
  48. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/aws_python_helper.egg-info/top_level.txt +0 -0
  49. {aws_python_helper-0.32.0 → aws_python_helper-0.34.0}/setup.cfg +0 -0
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-python-helper
3
- Version: 0.32.0
3
+ Version: 0.34.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
- - **Multi-state routing**: Automatic `constitution-state` propagation across the entire call chain for per-state database routing
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
- | `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
+ | `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
- 'constitution-state': 'connecticut', # Required
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
- 'constitution-state': 'connecticut', # Required
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
- # constitution-state is auto-propagated as CONSTITUTION_STATE env var in the container
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 `constitution-state` from context |
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 `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.
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 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.
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 get_state() automatically
580
- # If constitution-state = "connecticut" → connects to DB "connecticut"
581
- # If constitution-state = "new_jersey" → connects to DB "new_jersey"
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 `constitution-state` has not been set in the context. This is prevented automatically by the framework at every entry point (API, Lambda, SQS, Fargate).
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
- ## 🌐 Constitution State
650
+ ## 🌐 Session Context
651
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.
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 `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` |
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` (Base64-encoded JSON) |
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 `constitution-state` as a `MessageAttribute` on every published message |
669
- | **FargateExecutor** | Auto-injects `CONSTITUTION_STATE` as an env var when launching Fargate containers |
668
+ | **SNS Publisher** | Auto-injects the full session as a `session` `MessageAttribute` (Base64-encoded JSON) 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 that state through SNS → SQS → Fargate without any code changes in your consumers or tasks.
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 `constitution-state` from context to resolve the target database automatically. See the [Repository Pattern](#️-repository-pattern) section for details.
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 state manually (e.g., in tests or utility code):
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 get_state, set_state
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 = get_state() # e.g. "connecticut", or None if not set
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 — `constitution-state` in event
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
- 'constitution-state': 'connecticut', # Required
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
- REQUIRE_AUTH=true # Enable authentication (default: false)
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 `REQUIRE_AUTH=true`, every request must include a valid `Authorization: Bearer <token>` header. The authenticated user is available via `self.current_user`:
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):
@@ -1114,7 +1131,7 @@ environment_variables = {
1114
1131
  By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages.
1115
1132
 
1116
1133
  **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.
1134
+ - **Single mode**: the framework extracts the session from each record automatically (from SNS `MessageAttributes`, Base64-decoded) and sets it in context before calling `process_record()`. You do not need to extract it yourself.
1118
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.
1119
1136
 
1120
1137
  ```python
@@ -1164,7 +1181,7 @@ class OrderConsumer(SQSConsumer):
1164
1181
 
1165
1182
  ### SNS Publisher - Batch Publishing
1166
1183
 
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.
1184
+ The `SNSPublisher` automatically injects the current session as a Base64-encoded `MessageAttribute` on every published message. Base64 encoding is used to avoid SNS filter policy issues with raw JSON string values in attributes. SQS consumers built with this framework will decode it automatically, ensuring the session flows end-to-end through the SNS → SQS chain without any manual code.
1168
1185
 
1169
1186
  ```python
1170
1187
  topic = TitleIndexedTopic()
@@ -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
- - **Multi-state routing**: Automatic `constitution-state` propagation across the entire call chain for per-state database routing
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
- | `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 |
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
- 'constitution-state': 'connecticut', # Required
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
- 'constitution-state': 'connecticut', # Required
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
- # constitution-state is auto-propagated as CONSTITUTION_STATE env var in the container
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 `constitution-state` from context |
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 `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
+ - `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 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.
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 get_state() automatically
560
- # If constitution-state = "connecticut" → connects to DB "connecticut"
561
- # If constitution-state = "new_jersey" → connects to DB "new_jersey"
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 `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
+ > **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
- ## 🌐 Constitution State
631
+ ## 🌐 Session Context
631
632
 
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
+ 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 `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` |
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` (Base64-encoded JSON) |
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 `constitution-state` as a `MessageAttribute` on every published message |
649
- | **FargateExecutor** | Auto-injects `CONSTITUTION_STATE` as an env var when launching Fargate containers |
649
+ | **SNS Publisher** | Auto-injects the full session as a `session` `MessageAttribute` (Base64-encoded JSON) 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 that state through SNS → SQS → Fargate without any code changes in your consumers or tasks.
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 `constitution-state` from context to resolve the target database automatically. See the [Repository Pattern](#️-repository-pattern) section for details.
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 state manually (e.g., in tests or utility code):
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 get_state, set_state
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 = get_state() # e.g. "connecticut", or None if not set
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 — `constitution-state` in event
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
- 'constitution-state': 'connecticut', # Required
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
- REQUIRE_AUTH=true # Enable authentication (default: false)
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 `REQUIRE_AUTH=true`, every request must include a valid `Authorization: Bearer <token>` header. The authenticated user is available via `self.current_user`:
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):
@@ -1094,7 +1112,7 @@ environment_variables = {
1094
1112
  By default, consumers process messages one by one (`"single"` mode). Use `"batch"` mode when you need to group or bulk-process messages.
1095
1113
 
1096
1114
  **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.
1115
+ - **Single mode**: the framework extracts the session from each record automatically (from SNS `MessageAttributes`, Base64-decoded) and sets it in context before calling `process_record()`. You do not need to extract it yourself.
1098
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.
1099
1117
 
1100
1118
  ```python
@@ -1144,7 +1162,7 @@ class OrderConsumer(SQSConsumer):
1144
1162
 
1145
1163
  ### SNS Publisher - Batch Publishing
1146
1164
 
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.
1165
+ The `SNSPublisher` automatically injects the current session as a Base64-encoded `MessageAttribute` on every published message. Base64 encoding is used to avoid SNS filter policy issues with raw JSON string values in attributes. SQS consumers built with this framework will decode it automatically, ensuring the session flows end-to-end through the SNS → SQS chain without any manual code.
1148
1166
 
1149
1167
  ```python
1150
1168
  topic = TitleIndexedTopic()
@@ -28,7 +28,8 @@ from .utils.serializer import serialize_mongo_types
28
28
  from .repository.base import Repository
29
29
 
30
30
  # Context
31
- from .context.state import get_state, set_state
31
+ from .context.session import Session, get_session, set_session
32
+ from .context.state_validator import StateValidator, InvalidStateError
32
33
 
33
34
 
34
35
  __all__ = [
@@ -48,7 +49,10 @@ __all__ = [
48
49
  'MongoJSONEncoder',
49
50
  'mongo_json_dumps',
50
51
  'serialize_mongo_types',
51
- 'get_state',
52
- 'set_state',
52
+ 'Session',
53
+ 'get_session',
54
+ 'set_session',
55
+ 'StateValidator',
56
+ 'InvalidStateError',
53
57
  ]
54
58
 
@@ -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 REQUIRE_AUTH=true.
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,8 @@ 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
+ from ..context.session import get_session
16
+ from ..context.state_validator import StateValidator, InvalidStateError
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
@@ -75,23 +76,45 @@ class Dispatcher:
75
76
  # 1. Prepare - Load controller and inject properties
76
77
  api = self._prepare()
77
78
 
78
- # 2. Authenticate (if required)
79
- require_auth = os.getenv('REQUIRE_AUTH', 'false').lower() == 'true'
80
- if require_auth:
79
+ # 2. Authorization based on mode
80
+ authorization = os.getenv('AUTHORIZATION', '').lower()
81
+ requires_user = authorization in ('user', 'full')
82
+ requires_state = authorization in ('state', 'full')
83
+
84
+ # 2a. Authenticate (if mode requires user)
85
+ if requires_user:
81
86
  await self._authenticate(api)
82
87
 
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)
88
+ # 2b. Validate state header (if mode requires state)
89
+ if requires_state:
90
+ state = self.headers.get('constitution-state')
91
+ if not state:
92
+ return {
93
+ 'code': 400,
94
+ 'body': {
95
+ 'error': 'Bad Request',
96
+ 'message': "Header 'constitution-state' is required"
97
+ },
98
+ 'headers': {}
99
+ }
100
+ try:
101
+ await StateValidator.validate(state)
102
+ except InvalidStateError as e:
103
+ return {
104
+ 'code': 403,
105
+ 'body': {
106
+ 'error': 'Forbidden',
107
+ 'message': str(e)
108
+ },
109
+ 'headers': {}
110
+ }
111
+ session = get_session()
112
+ session.state = state
113
+
114
+ # 2c. Inject user into session (if authenticated)
115
+ if api._current_user:
116
+ session = get_session()
117
+ session.user = api._current_user
95
118
 
96
119
  # 3. Validate
97
120
  await api.validate()
@@ -0,0 +1,4 @@
1
+ from .session import Session, get_session, set_session
2
+ from .state_validator import StateValidator, InvalidStateError
3
+
4
+ __all__ = ['Session', 'get_session', 'set_session', 'StateValidator', 'InvalidStateError']