django-lambda-tasks 0.2.0__tar.gz → 0.2.1__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 (94) hide show
  1. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/steering/product.md +13 -10
  2. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/steering/structure.md +3 -3
  3. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/PKG-INFO +20 -16
  4. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/README.md +19 -15
  5. django_lambda_tasks-0.2.1/lambda_tasks/environment_loader.py +155 -0
  6. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/handler.py +4 -4
  7. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/pyproject.toml +1 -1
  8. django_lambda_tasks-0.2.0/tests/test_ssm_environment_loader.py → django_lambda_tasks-0.2.1/tests/test_environment_loader.py +234 -192
  9. django_lambda_tasks-0.2.0/lambda_tasks/ssm_environment_loader.py +0 -93
  10. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.github/workflows/ci.yml +0 -0
  11. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.github/workflows/release.yml +0 -0
  12. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.gitignore +0 -0
  13. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  14. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  15. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  16. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  17. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  18. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  19. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  20. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  21. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  22. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  23. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  24. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  25. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  26. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  27. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  28. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  29. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/.config.kiro +0 -0
  30. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/design.md +0 -0
  31. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/requirements.md +0 -0
  32. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/tasks.md +0 -0
  33. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  34. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/design.md +0 -0
  35. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  36. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  37. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  38. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  39. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  40. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  41. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/.config.kiro +0 -0
  42. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/design.md +0 -0
  43. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/requirements.md +0 -0
  44. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/tasks.md +0 -0
  45. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  46. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  47. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  48. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  49. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/.config.kiro +0 -0
  50. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/design.md +0 -0
  51. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/requirements.md +0 -0
  52. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/tasks.md +0 -0
  53. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/steering/tech.md +0 -0
  54. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.pre-commit-config.yaml +0 -0
  55. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.vscode/settings.json +0 -0
  56. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/README.md +0 -0
  57. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/__init__.py +0 -0
  58. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/apps.py +0 -0
  59. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/tasks.py +0 -0
  60. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/urls.py +0 -0
  61. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/views.py +0 -0
  62. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_project/__init__.py +0 -0
  63. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_project/settings.py +0 -0
  64. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_project/urls.py +0 -0
  65. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_project/wsgi.py +0 -0
  66. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/manage.py +0 -0
  67. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/__init__.py +0 -0
  68. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/admin.py +0 -0
  69. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/apps.py +0 -0
  70. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/decorators.py +0 -0
  71. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/logging.py +0 -0
  72. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/migrations/0001_initial.py +0 -0
  73. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/migrations/__init__.py +0 -0
  74. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/models.py +0 -0
  75. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/secret_loader.py +0 -0
  76. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/settings.py +0 -0
  77. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/tasks.py +0 -0
  78. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/timeouts.py +0 -0
  79. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/conftest.py +0 -0
  80. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/settings.py +0 -0
  81. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_admin.py +0 -0
  82. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_decorator.py +0 -0
  83. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_decorators.py +0 -0
  84. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_deferred_enqueue.py +0 -0
  85. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_handler.py +0 -0
  86. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_kwargs_only.py +0 -0
  87. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_logging.py +0 -0
  88. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_models.py +0 -0
  89. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_secret_loader.py +0 -0
  90. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_serializer.py +0 -0
  91. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_settings.py +0 -0
  92. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_tasks.py +0 -0
  93. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_timeout_validation.py +0 -0
  94. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_timeouts.py +0 -0
@@ -135,26 +135,29 @@ class SQSLambdaTaskMessage(BaseModel):
135
135
  - Returns `{"batchItemFailures": [...]}` for partial-batch failure reporting
136
136
  - Only pre-execution failures (malformed message, import error, misconfiguration) are reported as `batchItemFailures` — task logic failures are caught and recorded as `FAILED` TaskRecords without raising
137
137
  - Recommended SQS queue settings: `maxReceiveCount=1` with a DLQ configured; automatic retries are not useful since task failures are not re-driven by design
138
- - Cold-start sequence: `resolve_ssm_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`
139
- - Both loaders run unconditionally (outside the `DJANGO_SETTINGS_MODULE` check) — SSM may provide that var, and secrets may depend on SSM-loaded vars
138
+ - Cold-start sequence: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`
139
+ - Both loaders run unconditionally (outside the `DJANGO_SETTINGS_MODULE` check) — the environment secret may provide that var, and individual secrets may depend on environment-loaded vars
140
140
 
141
- ## SSM Environment Loader
141
+ ## Environment Loader
142
142
 
143
- `resolve_ssm_environment()` in `ssm_environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
143
+ `resolve_environment()` in `environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
144
144
 
145
- When the environment variable `LAMBDA_TASKS_SSM_ENVIRONMENT` is set, the loader fetches the named SSM parameter, parses its JSON content as a flat key-value mapping, and sets the resulting pairs as environment variables.
145
+ When the environment variable `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is set, the loader parses the reference, fetches the named Secrets Manager secret, parses its JSON content as a flat key-value mapping, and sets the resulting pairs as environment variables.
146
+
147
+ Required format: `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
146
148
 
147
149
  Behaviour:
148
- - If `LAMBDA_TASKS_SSM_ENVIRONMENT` is not set, does nothing (no AWS API calls)
149
- - Fetches the parameter with `WithDecryption=True`
150
- - Validates the parameter value is a flat JSON object (all values must be strings, no empty keys)
150
+ - If `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is not set, does nothing (no AWS API calls)
151
+ - Validates the reference format before any AWS call — malformed references raise `ValueError` immediately
152
+ - Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
153
+ - Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
151
154
  - Sets each key-value pair in `os.environ` — existing env vars are overridden (no conflict detection)
152
155
  - Idempotent via a module-level `_loaded` sentinel — subsequent calls are free no-ops
153
- - Invalid JSON, non-flat objects, or empty keys raise `ValueError` at cold start
156
+ - Invalid reference format, invalid JSON, non-flat objects, or empty keys raise `ValueError` at cold start
154
157
 
155
158
  ## Secret Loader
156
159
 
157
- `resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, after `resolve_ssm_environment()` and before `django.setup()`.
160
+ `resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, after `resolve_environment()` and before `django.setup()`.
158
161
 
159
162
  Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
160
163
 
@@ -18,7 +18,7 @@ django-lambda-tasks/
18
18
  │ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
19
19
  │ ├── settings.py # LambdaTasksSettings (lazy Django settings reader)
20
20
  │ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
21
- │ ├── ssm_environment_loader.py # Loads env vars from SSM Parameter Store at cold start
21
+ │ ├── environment_loader.py # Loads env vars from Secrets Manager at cold start
22
22
  │ ├── tasks.py # Built-in maintenance tasks (cleanup_task_records)
23
23
  │ ├── timeouts.py # TimeoutContext implementation
24
24
  │ └── migrations/ # Django migrations for TaskRecord
@@ -40,8 +40,8 @@ django-lambda-tasks/
40
40
 
41
41
  - `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
42
42
  - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` publishes to SQS or executes eagerly; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
43
- - `handler.py` — Lambda entry point; calls `resolve_ssm_environment()` then `resolve_secrets_into_env()` then conditionally `django.setup()` at cold start; processes SQS records independently; returns `batchItemFailures`
44
- - `ssm_environment_loader.py` — loads env vars from an SSM Parameter Store parameter at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
43
+ - `handler.py` — Lambda entry point; calls `resolve_environment()` then `resolve_secrets_into_env()` then conditionally `django.setup()` at cold start; processes SQS records independently; returns `batchItemFailures`
44
+ - `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
45
45
  - `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
46
46
  - `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
47
47
  - `settings.py` — `LambdaTasksSettings` instantiated fresh per use (reads live Django settings)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -440,7 +440,7 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
440
440
  | Environment Variable | Required | Description |
441
441
  |---|---|---|
442
442
  | `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
443
- | `LAMBDA_TASKS_SSM_ENVIRONMENT` | No | SSM Parameter Store parameter name to load as environment variables at cold start. |
443
+ | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
444
444
  | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
445
445
 
446
446
  ### Resolving Django settings from AWS Secrets Manager
@@ -493,17 +493,19 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
493
493
  - The named JSON key does not exist in the fetched secret
494
494
  - The secret value is not valid JSON
495
495
 
496
- ### Loading environment variables from SSM Parameter Store
496
+ ### Loading environment variables from Secrets Manager
497
497
 
498
- The Lambda handler supports loading environment variables from an AWS SSM Parameter Store parameter at cold start. This lets you manage environment configuration centrally in Parameter Store without baking values into the Lambda deployment package.
498
+ The Lambda handler supports loading environment variables from an AWS Secrets Manager secret at cold start. This lets you manage environment configuration centrally in Secrets Manager without baking values into the Lambda deployment package.
499
499
 
500
- Set the `LAMBDA_TASKS_SSM_ENVIRONMENT` environment variable to the name of an SSM parameter:
500
+ Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
501
501
 
502
502
  ```
503
- LAMBDA_TASKS_SSM_ENVIRONMENT=/myapp/prod/environment
503
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
504
504
  ```
505
505
 
506
- The parameter value must be a flat JSON object where all keys and values are strings:
506
+ The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
507
+
508
+ The secret value must be a flat JSON object where all keys and values are strings:
507
509
 
508
510
  ```json
509
511
  {
@@ -513,25 +515,27 @@ The parameter value must be a flat JSON object where all keys and values are str
513
515
  }
514
516
  ```
515
517
 
516
- At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_ssm_environment()` which:
518
+ At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
517
519
 
518
- 1. Checks for the `LAMBDA_TASKS_SSM_ENVIRONMENT` env var — if not set, does nothing
519
- 2. Fetches the named parameter via `ssm.get_parameter(Name=..., WithDecryption=True)`
520
- 3. Validates the parameter value is a flat JSON object (all values must be strings, no empty keys)
521
- 4. Sets each key-value pair in `os.environ` existing env vars are overridden
522
- 5. Caches the result via a module-level sentinel subsequent calls are free no-ops
520
+ 1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
521
+ 2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
522
+ 3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
523
+ 4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
524
+ 5. Sets each key-value pair in `os.environ`existing env vars are overridden
525
+ 6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
523
526
 
524
- Because SSM loading runs first, the parameter can provide `DJANGO_SETTINGS_MODULE` itself, and secrets loaded by `resolve_secrets_into_env()` can reference SSM-loaded values.
527
+ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
525
528
 
526
529
  #### Validation errors
527
530
 
528
531
  The following raise `ValueError` at cold start, preventing the Lambda container from starting:
529
532
 
530
- - Parameter value is not valid JSON
533
+ - Reference format is invalid (wrong segment count, empty version-stage or version-id)
534
+ - Secret value is not valid JSON
531
535
  - JSON is not a flat object (contains non-string values) — error message lists the offending keys
532
536
  - JSON contains an empty string key
533
537
 
534
- AWS errors (parameter not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
538
+ AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
535
539
 
536
540
  ---
537
541
 
@@ -428,7 +428,7 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
428
428
  | Environment Variable | Required | Description |
429
429
  |---|---|---|
430
430
  | `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
431
- | `LAMBDA_TASKS_SSM_ENVIRONMENT` | No | SSM Parameter Store parameter name to load as environment variables at cold start. |
431
+ | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
432
432
  | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
433
433
 
434
434
  ### Resolving Django settings from AWS Secrets Manager
@@ -481,17 +481,19 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
481
481
  - The named JSON key does not exist in the fetched secret
482
482
  - The secret value is not valid JSON
483
483
 
484
- ### Loading environment variables from SSM Parameter Store
484
+ ### Loading environment variables from Secrets Manager
485
485
 
486
- The Lambda handler supports loading environment variables from an AWS SSM Parameter Store parameter at cold start. This lets you manage environment configuration centrally in Parameter Store without baking values into the Lambda deployment package.
486
+ The Lambda handler supports loading environment variables from an AWS Secrets Manager secret at cold start. This lets you manage environment configuration centrally in Secrets Manager without baking values into the Lambda deployment package.
487
487
 
488
- Set the `LAMBDA_TASKS_SSM_ENVIRONMENT` environment variable to the name of an SSM parameter:
488
+ Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
489
489
 
490
490
  ```
491
- LAMBDA_TASKS_SSM_ENVIRONMENT=/myapp/prod/environment
491
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
492
492
  ```
493
493
 
494
- The parameter value must be a flat JSON object where all keys and values are strings:
494
+ The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
495
+
496
+ The secret value must be a flat JSON object where all keys and values are strings:
495
497
 
496
498
  ```json
497
499
  {
@@ -501,25 +503,27 @@ The parameter value must be a flat JSON object where all keys and values are str
501
503
  }
502
504
  ```
503
505
 
504
- At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_ssm_environment()` which:
506
+ At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
505
507
 
506
- 1. Checks for the `LAMBDA_TASKS_SSM_ENVIRONMENT` env var — if not set, does nothing
507
- 2. Fetches the named parameter via `ssm.get_parameter(Name=..., WithDecryption=True)`
508
- 3. Validates the parameter value is a flat JSON object (all values must be strings, no empty keys)
509
- 4. Sets each key-value pair in `os.environ` existing env vars are overridden
510
- 5. Caches the result via a module-level sentinel subsequent calls are free no-ops
508
+ 1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
509
+ 2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
510
+ 3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
511
+ 4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
512
+ 5. Sets each key-value pair in `os.environ`existing env vars are overridden
513
+ 6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
511
514
 
512
- Because SSM loading runs first, the parameter can provide `DJANGO_SETTINGS_MODULE` itself, and secrets loaded by `resolve_secrets_into_env()` can reference SSM-loaded values.
515
+ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
513
516
 
514
517
  #### Validation errors
515
518
 
516
519
  The following raise `ValueError` at cold start, preventing the Lambda container from starting:
517
520
 
518
- - Parameter value is not valid JSON
521
+ - Reference format is invalid (wrong segment count, empty version-stage or version-id)
522
+ - Secret value is not valid JSON
519
523
  - JSON is not a flat object (contains non-string values) — error message lists the offending keys
520
524
  - JSON contains an empty string key
521
525
 
522
- AWS errors (parameter not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
526
+ AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
523
527
 
524
528
  ---
525
529
 
@@ -0,0 +1,155 @@
1
+ """
2
+ Resolves environment variables from an AWS Secrets Manager secret.
3
+
4
+ When the environment variable ``LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN``
5
+ is set, this module fetches the named secret, parses its value as a flat JSON
6
+ object (all keys and values must be strings), and sets each key-value pair in
7
+ ``os.environ``.
8
+
9
+ Required value format::
10
+
11
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123:secret:my-env:AWSCURRENT:v1
12
+
13
+ That is: ``<arn>:<version-stage>:<version-id>`` (9 colon-separated segments).
14
+ The ARN is 7 segments, plus version-stage and version-id.
15
+
16
+ This runs at Lambda cold start — before ``resolve_secrets_into_env()`` and
17
+ before ``django.setup()`` — so that environment variables loaded from the
18
+ secret are available to both the secret loader and Django configuration.
19
+
20
+ The result is cached at module level via a ``_loaded`` sentinel so that
21
+ subsequent calls (warm invocations) are free no-ops.
22
+ """
23
+
24
+ import json
25
+ import logging
26
+ import os
27
+ from typing import NamedTuple
28
+
29
+ import boto3
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ _loaded: bool = False
34
+
35
+
36
+ class _EnvironmentSecretReference(NamedTuple):
37
+ arn: str
38
+ version_stage: str
39
+ version_id: str
40
+
41
+
42
+ def resolve_environment() -> None:
43
+ """Load secret content into os.environ.
44
+
45
+ Reads the secret identified by LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN,
46
+ parses it as a flat JSON object, and sets each key-value pair
47
+ as an environment variable. Idempotent — cached after first call.
48
+
49
+ Raises:
50
+ ValueError: If the env var format is invalid, the secret content
51
+ is not valid JSON, not a flat string→string mapping,
52
+ or contains an empty string key.
53
+ """
54
+ global _loaded
55
+
56
+ raw_reference = os.environ.get("LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN")
57
+ if not raw_reference:
58
+ return
59
+
60
+ if _loaded:
61
+ return
62
+
63
+ ref = _parse_reference(value=raw_reference)
64
+ raw_value = _fetch_secret(ref=ref)
65
+ parsed = _validate_and_parse(raw_value=raw_value, secret_arn=ref.arn)
66
+
67
+ for key, value in parsed.items():
68
+ os.environ[key] = value
69
+
70
+ _loaded = True
71
+
72
+
73
+ def _parse_reference(*, value: str) -> _EnvironmentSecretReference:
74
+ """Parse and validate the environment secret reference.
75
+
76
+ Expected format::
77
+
78
+ <arn>:<version-stage>:<version-id>
79
+
80
+ The ARN itself is 7 colon-separated segments, plus 2 suffix segments
81
+ (version-stage, version-id) = 9 total.
82
+ Both suffix fields must be non-empty.
83
+
84
+ Raises ``ValueError`` if the format is invalid.
85
+ """
86
+ parts = value.split(":")
87
+
88
+ if len(parts) != 9:
89
+ raise ValueError(
90
+ "LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN has an invalid format. "
91
+ "Expected <arn>:<version-stage>:<version-id> "
92
+ f"(9 colon-separated segments), got {len(parts)}: {value!r}"
93
+ )
94
+
95
+ arn = ":".join(parts[:7])
96
+ version_stage = parts[7]
97
+ version_id = parts[8]
98
+
99
+ for field, field_value in (
100
+ ("version-stage", version_stage),
101
+ ("version-id", version_id),
102
+ ):
103
+ if not field_value:
104
+ raise ValueError(
105
+ f"LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN is missing the "
106
+ f"{field} segment: {value!r}"
107
+ )
108
+
109
+ return _EnvironmentSecretReference(
110
+ arn=arn,
111
+ version_stage=version_stage,
112
+ version_id=version_id,
113
+ )
114
+
115
+
116
+ def _fetch_secret(*, ref: _EnvironmentSecretReference) -> str:
117
+ """Fetch a secret string from Secrets Manager using boto3."""
118
+ client = boto3.client("secretsmanager")
119
+ response = client.get_secret_value(
120
+ SecretId=ref.arn,
121
+ VersionStage=ref.version_stage,
122
+ VersionId=ref.version_id,
123
+ )
124
+ return response["SecretString"]
125
+
126
+
127
+ def _validate_and_parse(*, raw_value: str, secret_arn: str) -> dict[str, str]:
128
+ """Parse JSON and validate it is a flat str→str mapping.
129
+
130
+ Raises ValueError with descriptive messages on failure.
131
+ """
132
+ try:
133
+ parsed = json.loads(raw_value)
134
+ except json.JSONDecodeError as exc:
135
+ raise ValueError(
136
+ f"Secret {secret_arn} does not contain valid JSON: {exc}"
137
+ ) from exc
138
+
139
+ if not isinstance(parsed, dict):
140
+ raise ValueError(
141
+ f"Secret {secret_arn} must be a JSON object, got {type(parsed).__name__}"
142
+ )
143
+
144
+ non_string_keys = [
145
+ key for key, value in parsed.items() if not isinstance(value, str)
146
+ ]
147
+ if non_string_keys:
148
+ raise ValueError(
149
+ f"Secret {secret_arn} contains non-string values for keys: {non_string_keys}"
150
+ )
151
+
152
+ if "" in parsed:
153
+ raise ValueError(f"Secret {secret_arn} contains an empty string key")
154
+
155
+ return parsed
@@ -12,13 +12,13 @@ import os
12
12
  import django
13
13
  from django.apps import apps
14
14
 
15
+ from lambda_tasks.environment_loader import resolve_environment
15
16
  from lambda_tasks.secret_loader import resolve_secrets_into_env
16
- from lambda_tasks.ssm_environment_loader import resolve_ssm_environment
17
17
 
18
18
  # Both loaders are idempotent and run unconditionally before the
19
- # DJANGO_SETTINGS_MODULE check — SSM may provide that var, and
20
- # secrets may depend on SSM-loaded vars.
21
- resolve_ssm_environment()
19
+ # DJANGO_SETTINGS_MODULE check — the environment secret may provide
20
+ # that var, and individual secrets may depend on environment-loaded vars.
21
+ resolve_environment()
22
22
  resolve_secrets_into_env()
23
23
 
24
24
  if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.2.0"
10
+ version = "0.2.1"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"