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.
Files changed (46) hide show
  1. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/PKG-INFO +121 -14
  2. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/README.md +119 -11
  3. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/__init__.py +6 -0
  4. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/base.py +19 -4
  5. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/dispatcher.py +29 -4
  6. aws_python_helper-0.33.0/aws_python_helper/context/__init__.py +3 -0
  7. aws_python_helper-0.33.0/aws_python_helper/context/session.py +88 -0
  8. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/executor.py +14 -1
  9. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/task_base.py +17 -0
  10. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/base.py +17 -1
  11. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/repository/base.py +81 -30
  12. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sns/publisher.py +12 -1
  13. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/consumer_base.py +126 -49
  14. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/PKG-INFO +121 -14
  15. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/SOURCES.txt +2 -0
  16. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/pyproject.toml +2 -3
  17. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/__init__.py +0 -0
  18. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/auth_middleware.py +0 -0
  19. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/auth_validators.py +0 -0
  20. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/exceptions.py +0 -0
  21. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/fetcher.py +0 -0
  22. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/api/handler.py +0 -0
  23. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/__init__.py +0 -0
  24. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/database_proxy.py +0 -0
  25. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/external_database_proxy.py +0 -0
  26. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
  27. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/database/mongo_manager.py +0 -0
  28. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/__init__.py +0 -0
  29. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/fetcher.py +0 -0
  30. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/fargate/handler.py +0 -0
  31. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
  32. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
  33. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
  34. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/repository/__init__.py +0 -0
  35. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sns/__init__.py +0 -0
  36. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/__init__.py +0 -0
  37. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/fetcher.py +0 -0
  38. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/sqs/handler.py +0 -0
  39. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/__init__.py +0 -0
  40. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/json_encoder.py +0 -0
  41. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/response.py +0 -0
  42. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper/utils/serializer.py +0 -0
  43. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
  44. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/requires.txt +0 -0
  45. {aws_python_helper-0.31.0 → aws_python_helper-0.33.0}/aws_python_helper.egg-info/top_level.txt +0 -0
  46. {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.31.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
- | `database_name` | `str` | `"core"` | No |
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 on the main cluster (`database_name` defaults to `"core"`)
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
- ### Repository on a different database (not `"core"`)
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 database_name(self):
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
- REQUIRE_AUTH=true # Enable authentication (default: false)
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 `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`:
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
- | `database_name` | `str` | `"core"` | No |
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 on the main cluster (`database_name` defaults to `"core"`)
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
- ### Repository on a different database (not `"core"`)
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 database_name(self):
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
- REQUIRE_AUTH=true # Enable authentication (default: false)
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 `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`:
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 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,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. Authenticate (if required)
78
- require_auth = os.getenv('REQUIRE_AUTH', 'false').lower() == 'true'
79
- if require_auth:
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
 
@@ -0,0 +1,3 @@
1
+ from .session import Session, get_session, set_session
2
+
3
+ __all__ = ['Session', 'get_session', 'set_session']