django-lambda-tasks 0.2.0__tar.gz → 0.2.2__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 (95) hide show
  1. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/steering/product.md +15 -11
  2. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/steering/structure.md +3 -3
  3. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/PKG-INFO +23 -17
  4. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/README.md +22 -16
  5. django_lambda_tasks-0.2.2/lambda_tasks/environment_loader.py +156 -0
  6. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/handler.py +34 -12
  7. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/pyproject.toml +1 -1
  8. django_lambda_tasks-0.2.2/tests/test_environment_loader.py +691 -0
  9. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_handler.py +64 -16
  10. django_lambda_tasks-0.2.0/lambda_tasks/ssm_environment_loader.py +0 -93
  11. django_lambda_tasks-0.2.0/tests/test_ssm_environment_loader.py +0 -654
  12. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.github/workflows/ci.yml +0 -0
  13. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.github/workflows/release.yml +0 -0
  14. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.gitignore +0 -0
  15. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  16. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  17. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  18. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  19. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  20. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  21. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  22. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  23. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  24. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  25. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  26. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  27. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  28. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  29. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  30. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  31. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
  32. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/design.md +0 -0
  33. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/requirements.md +0 -0
  34. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/tasks.md +0 -0
  35. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  36. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
  37. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  38. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  39. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  40. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  41. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  42. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  43. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
  44. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/design.md +0 -0
  45. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/requirements.md +0 -0
  46. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/tasks.md +0 -0
  47. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  48. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  49. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  50. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  51. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/.config.kiro +0 -0
  52. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/design.md +0 -0
  53. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/requirements.md +0 -0
  54. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/tasks.md +0 -0
  55. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/steering/tech.md +0 -0
  56. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.pre-commit-config.yaml +0 -0
  57. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.vscode/settings.json +0 -0
  58. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/README.md +0 -0
  59. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/__init__.py +0 -0
  60. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/apps.py +0 -0
  61. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/tasks.py +0 -0
  62. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/urls.py +0 -0
  63. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/views.py +0 -0
  64. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_project/__init__.py +0 -0
  65. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_project/settings.py +0 -0
  66. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_project/urls.py +0 -0
  67. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_project/wsgi.py +0 -0
  68. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/manage.py +0 -0
  69. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/__init__.py +0 -0
  70. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/admin.py +0 -0
  71. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/apps.py +0 -0
  72. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/decorators.py +0 -0
  73. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/logging.py +0 -0
  74. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/migrations/0001_initial.py +0 -0
  75. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/migrations/__init__.py +0 -0
  76. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/models.py +0 -0
  77. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/secret_loader.py +0 -0
  78. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/settings.py +0 -0
  79. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/tasks.py +0 -0
  80. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/timeouts.py +0 -0
  81. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/conftest.py +0 -0
  82. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/settings.py +0 -0
  83. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_admin.py +0 -0
  84. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_decorator.py +0 -0
  85. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_decorators.py +0 -0
  86. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_deferred_enqueue.py +0 -0
  87. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_kwargs_only.py +0 -0
  88. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_logging.py +0 -0
  89. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_models.py +0 -0
  90. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_secret_loader.py +0 -0
  91. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_serializer.py +0 -0
  92. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_settings.py +0 -0
  93. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_tasks.py +0 -0
  94. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_timeout_validation.py +0 -0
  95. {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_timeouts.py +0 -0
@@ -15,7 +15,7 @@ View → @lambda_task.execute_on_commit() → SQS → Lambda handler → SQSLamb
15
15
  Key modules:
16
16
  - `decorators.py` — `@lambda_task` decorator and `LambdaTaskWrapper`
17
17
  - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish)
18
- - `handler.py` — AWS Lambda entry point with partial-batch failure reporting
18
+ - `handler.py` — AWS Lambda entry point; cold-start init runs on first invocation (not at import time) with partial-batch failure reporting
19
19
  - `logging.py` — `task_logger` for invocation-scoped log output
20
20
  - `settings.py` — lazy `LambdaTasksSettings` reading from Django settings
21
21
 
@@ -135,26 +135,30 @@ 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 runs inside the handler on the first invocation (not at module import time) to avoid Lambda init-duration timeouts: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`
139
+ - A module-level `_cold_start_done` sentinel ensures the sequence runs only once; subsequent warm invocations skip it
140
+ - 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
141
 
141
- ## SSM Environment Loader
142
+ ## Environment Loader
142
143
 
143
- `resolve_ssm_environment()` in `ssm_environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
144
+ `resolve_environment()` in `environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
144
145
 
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.
146
+ 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.
147
+
148
+ 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
149
 
147
150
  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)
151
+ - If `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is not set, does nothing (no AWS API calls)
152
+ - Validates the reference format before any AWS call — malformed references raise `ValueError` immediately
153
+ - Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
154
+ - Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
151
155
  - Sets each key-value pair in `os.environ` — existing env vars are overridden (no conflict detection)
152
156
  - 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
157
+ - Invalid reference format, invalid JSON, non-flat objects, or empty keys raise `ValueError` at cold start
154
158
 
155
159
  ## Secret Loader
156
160
 
157
- `resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, after `resolve_ssm_environment()` and before `django.setup()`.
161
+ `resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, after `resolve_environment()` and before `django.setup()`.
158
162
 
159
163
  Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
160
164
 
@@ -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; cold-start init (`resolve_environment()` `resolve_secrets_into_env()` conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; 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.2
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -435,12 +435,14 @@ Point your Lambda function's handler at:
435
435
  lambda_tasks.handler.handler
436
436
  ```
437
437
 
438
+ The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
439
+
438
440
  Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
439
441
 
440
442
  | Environment Variable | Required | Description |
441
443
  |---|---|---|
442
444
  | `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. |
445
+ | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
444
446
  | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
445
447
 
446
448
  ### Resolving Django settings from AWS Secrets Manager
@@ -453,7 +455,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
453
455
  LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
454
456
  ```
455
457
 
456
- At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
458
+ At cold start (on the first handler invocation), before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
457
459
 
458
460
  1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
459
461
  2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
@@ -493,17 +495,19 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
493
495
  - The named JSON key does not exist in the fetched secret
494
496
  - The secret value is not valid JSON
495
497
 
496
- ### Loading environment variables from SSM Parameter Store
498
+ ### Loading environment variables from Secrets Manager
497
499
 
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.
500
+ 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
501
 
500
- Set the `LAMBDA_TASKS_SSM_ENVIRONMENT` environment variable to the name of an SSM parameter:
502
+ Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
501
503
 
502
504
  ```
503
- LAMBDA_TASKS_SSM_ENVIRONMENT=/myapp/prod/environment
505
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
504
506
  ```
505
507
 
506
- The parameter value must be a flat JSON object where all keys and values are strings:
508
+ 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.
509
+
510
+ The secret value must be a flat JSON object where all keys and values are strings:
507
511
 
508
512
  ```json
509
513
  {
@@ -513,25 +517,27 @@ The parameter value must be a flat JSON object where all keys and values are str
513
517
  }
514
518
  ```
515
519
 
516
- At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_ssm_environment()` which:
520
+ At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
517
521
 
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
522
+ 1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
523
+ 2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
524
+ 3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
525
+ 4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
526
+ 5. Sets each key-value pair in `os.environ`existing env vars are overridden
527
+ 6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
523
528
 
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.
529
+ 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
530
 
526
531
  #### Validation errors
527
532
 
528
533
  The following raise `ValueError` at cold start, preventing the Lambda container from starting:
529
534
 
530
- - Parameter value is not valid JSON
535
+ - Reference format is invalid (wrong segment count, empty version-stage or version-id)
536
+ - Secret value is not valid JSON
531
537
  - JSON is not a flat object (contains non-string values) — error message lists the offending keys
532
538
  - JSON contains an empty string key
533
539
 
534
- AWS errors (parameter not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
540
+ AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
535
541
 
536
542
  ---
537
543
 
@@ -423,12 +423,14 @@ Point your Lambda function's handler at:
423
423
  lambda_tasks.handler.handler
424
424
  ```
425
425
 
426
+ The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
427
+
426
428
  Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
427
429
 
428
430
  | Environment Variable | Required | Description |
429
431
  |---|---|---|
430
432
  | `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. |
433
+ | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
432
434
  | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
433
435
 
434
436
  ### Resolving Django settings from AWS Secrets Manager
@@ -441,7 +443,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
441
443
  LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
442
444
  ```
443
445
 
444
- At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
446
+ At cold start (on the first handler invocation), before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
445
447
 
446
448
  1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
447
449
  2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
@@ -481,17 +483,19 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
481
483
  - The named JSON key does not exist in the fetched secret
482
484
  - The secret value is not valid JSON
483
485
 
484
- ### Loading environment variables from SSM Parameter Store
486
+ ### Loading environment variables from Secrets Manager
485
487
 
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.
488
+ 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
489
 
488
- Set the `LAMBDA_TASKS_SSM_ENVIRONMENT` environment variable to the name of an SSM parameter:
490
+ Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
489
491
 
490
492
  ```
491
- LAMBDA_TASKS_SSM_ENVIRONMENT=/myapp/prod/environment
493
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
492
494
  ```
493
495
 
494
- The parameter value must be a flat JSON object where all keys and values are strings:
496
+ 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.
497
+
498
+ The secret value must be a flat JSON object where all keys and values are strings:
495
499
 
496
500
  ```json
497
501
  {
@@ -501,25 +505,27 @@ The parameter value must be a flat JSON object where all keys and values are str
501
505
  }
502
506
  ```
503
507
 
504
- At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_ssm_environment()` which:
508
+ At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
505
509
 
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
510
+ 1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
511
+ 2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
512
+ 3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
513
+ 4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
514
+ 5. Sets each key-value pair in `os.environ`existing env vars are overridden
515
+ 6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
511
516
 
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.
517
+ 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
518
 
514
519
  #### Validation errors
515
520
 
516
521
  The following raise `ValueError` at cold start, preventing the Lambda container from starting:
517
522
 
518
- - Parameter value is not valid JSON
523
+ - Reference format is invalid (wrong segment count, empty version-stage or version-id)
524
+ - Secret value is not valid JSON
519
525
  - JSON is not a flat object (contains non-string values) — error message lists the offending keys
520
526
  - JSON contains an empty string key
521
527
 
522
- AWS errors (parameter not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
528
+ AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
523
529
 
524
530
  ---
525
531
 
@@ -0,0 +1,156 @@
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 is called by the handler on the first invocation (cold start) — before
17
+ ``resolve_secrets_into_env()`` and before ``django.setup()`` — so that
18
+ environment variables loaded from the secret are available to both the
19
+ secret loader and Django configuration.
20
+
21
+ The result is cached at module level via a ``_loaded`` sentinel so that
22
+ subsequent calls (warm invocations) are free no-ops.
23
+ """
24
+
25
+ import json
26
+ import logging
27
+ import os
28
+ from typing import NamedTuple
29
+
30
+ import boto3
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ _loaded: bool = False
35
+
36
+
37
+ class _EnvironmentSecretReference(NamedTuple):
38
+ arn: str
39
+ version_stage: str
40
+ version_id: str
41
+
42
+
43
+ def resolve_environment() -> None:
44
+ """Load secret content into os.environ.
45
+
46
+ Reads the secret identified by LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN,
47
+ parses it as a flat JSON object, and sets each key-value pair
48
+ as an environment variable. Idempotent — cached after first call.
49
+
50
+ Raises:
51
+ ValueError: If the env var format is invalid, the secret content
52
+ is not valid JSON, not a flat string→string mapping,
53
+ or contains an empty string key.
54
+ """
55
+ global _loaded
56
+
57
+ raw_reference = os.environ.get("LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN")
58
+ if not raw_reference:
59
+ return
60
+
61
+ if _loaded:
62
+ return
63
+
64
+ ref = _parse_reference(value=raw_reference)
65
+ raw_value = _fetch_secret(ref=ref)
66
+ parsed = _validate_and_parse(raw_value=raw_value, secret_arn=ref.arn)
67
+
68
+ for key, value in parsed.items():
69
+ os.environ[key] = value
70
+
71
+ _loaded = True
72
+
73
+
74
+ def _parse_reference(*, value: str) -> _EnvironmentSecretReference:
75
+ """Parse and validate the environment secret reference.
76
+
77
+ Expected format::
78
+
79
+ <arn>:<version-stage>:<version-id>
80
+
81
+ The ARN itself is 7 colon-separated segments, plus 2 suffix segments
82
+ (version-stage, version-id) = 9 total.
83
+ Both suffix fields must be non-empty.
84
+
85
+ Raises ``ValueError`` if the format is invalid.
86
+ """
87
+ parts = value.split(":")
88
+
89
+ if len(parts) != 9:
90
+ raise ValueError(
91
+ "LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN has an invalid format. "
92
+ "Expected <arn>:<version-stage>:<version-id> "
93
+ f"(9 colon-separated segments), got {len(parts)}: {value!r}"
94
+ )
95
+
96
+ arn = ":".join(parts[:7])
97
+ version_stage = parts[7]
98
+ version_id = parts[8]
99
+
100
+ for field, field_value in (
101
+ ("version-stage", version_stage),
102
+ ("version-id", version_id),
103
+ ):
104
+ if not field_value:
105
+ raise ValueError(
106
+ f"LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN is missing the "
107
+ f"{field} segment: {value!r}"
108
+ )
109
+
110
+ return _EnvironmentSecretReference(
111
+ arn=arn,
112
+ version_stage=version_stage,
113
+ version_id=version_id,
114
+ )
115
+
116
+
117
+ def _fetch_secret(*, ref: _EnvironmentSecretReference) -> str:
118
+ """Fetch a secret string from Secrets Manager using boto3."""
119
+ client = boto3.client("secretsmanager")
120
+ response = client.get_secret_value(
121
+ SecretId=ref.arn,
122
+ VersionStage=ref.version_stage,
123
+ VersionId=ref.version_id,
124
+ )
125
+ return response["SecretString"]
126
+
127
+
128
+ def _validate_and_parse(*, raw_value: str, secret_arn: str) -> dict[str, str]:
129
+ """Parse JSON and validate it is a flat str→str mapping.
130
+
131
+ Raises ValueError with descriptive messages on failure.
132
+ """
133
+ try:
134
+ parsed = json.loads(raw_value)
135
+ except json.JSONDecodeError as exc:
136
+ raise ValueError(
137
+ f"Secret {secret_arn} does not contain valid JSON: {exc}"
138
+ ) from exc
139
+
140
+ if not isinstance(parsed, dict):
141
+ raise ValueError(
142
+ f"Secret {secret_arn} must be a JSON object, got {type(parsed).__name__}"
143
+ )
144
+
145
+ non_string_keys = [
146
+ key for key, value in parsed.items() if not isinstance(value, str)
147
+ ]
148
+ if non_string_keys:
149
+ raise ValueError(
150
+ f"Secret {secret_arn} contains non-string values for keys: {non_string_keys}"
151
+ )
152
+
153
+ if "" in parsed:
154
+ raise ValueError(f"Secret {secret_arn} contains an empty string key")
155
+
156
+ return parsed
@@ -4,28 +4,48 @@ AWS Lambda handler for lambda_tasks.
4
4
  Processes a batch of SQS records using partial-batch failure reporting.
5
5
  Each record is processed independently — a failure in one record does not
6
6
  prevent processing of other records.
7
+
8
+ Cold-start initialisation (environment loading, secret resolution, Django
9
+ setup) runs inside the handler on the first invocation rather than at module
10
+ import time. This keeps the Lambda init phase fast and avoids init-duration
11
+ timeouts. The sequence is guarded by a module-level sentinel so subsequent
12
+ warm invocations skip it.
7
13
  """
8
14
 
9
15
  import logging
10
16
  import os
11
17
 
12
- import django
13
- from django.apps import apps
14
-
18
+ from lambda_tasks.environment_loader import resolve_environment
15
19
  from lambda_tasks.secret_loader import resolve_secrets_into_env
16
- from lambda_tasks.ssm_environment_loader import resolve_ssm_environment
17
20
 
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()
22
- resolve_secrets_into_env()
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _cold_start_done: bool = False
23
24
 
24
- if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
25
- django.setup()
26
25
 
26
+ def _perform_cold_start() -> None:
27
+ """Run one-time initialisation: env loading, secrets, Django setup.
27
28
 
28
- logger = logging.getLogger(__name__)
29
+ Both loaders are idempotent and run unconditionally — the environment
30
+ secret may provide DJANGO_SETTINGS_MODULE, and individual secrets may
31
+ depend on environment-loaded vars.
32
+ """
33
+ global _cold_start_done
34
+
35
+ if _cold_start_done:
36
+ return
37
+
38
+ resolve_environment()
39
+ resolve_secrets_into_env()
40
+
41
+ if os.environ.get("DJANGO_SETTINGS_MODULE"):
42
+ import django
43
+ from django.apps import apps
44
+
45
+ if not apps.ready:
46
+ django.setup()
47
+
48
+ _cold_start_done = True
29
49
 
30
50
 
31
51
  def handler(event: dict, context: object) -> dict:
@@ -34,6 +54,8 @@ def handler(event: dict, context: object) -> dict:
34
54
  Returns a partial-batch failure report so AWS only re-drives failed records.
35
55
  Signature is fixed by AWS and uses two args only.
36
56
  """
57
+ _perform_cold_start()
58
+
37
59
  # Local import due to AppRegistryNotReady
38
60
  from lambda_tasks.models import SQSLambdaTaskMessage
39
61
 
@@ -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.2"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"