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

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