django-lambda-tasks 0.1.6__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 (93) hide show
  1. django_lambda_tasks-0.2.1/.kiro/specs/ssm-environment-loader/.config.kiro +1 -0
  2. django_lambda_tasks-0.2.1/.kiro/specs/ssm-environment-loader/design.md +225 -0
  3. django_lambda_tasks-0.2.1/.kiro/specs/ssm-environment-loader/requirements.md +56 -0
  4. django_lambda_tasks-0.2.1/.kiro/specs/ssm-environment-loader/tasks.md +124 -0
  5. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/steering/product.md +20 -2
  6. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/steering/structure.md +4 -2
  7. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/PKG-INFO +51 -1
  8. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/README.md +50 -0
  9. django_lambda_tasks-0.2.1/lambda_tasks/environment_loader.py +155 -0
  10. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/handler.py +7 -5
  11. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/secret_loader.py +13 -1
  12. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/pyproject.toml +1 -1
  13. django_lambda_tasks-0.2.1/tests/test_environment_loader.py +696 -0
  14. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_handler.py +25 -0
  15. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_secret_loader.py +16 -5
  16. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.github/workflows/ci.yml +0 -0
  17. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.github/workflows/release.yml +0 -0
  18. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.gitignore +0 -0
  19. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  20. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  21. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  22. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  23. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  24. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  25. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  26. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  27. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  28. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  29. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  30. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  31. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  32. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  33. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  34. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  35. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/.config.kiro +0 -0
  36. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/design.md +0 -0
  37. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/requirements.md +0 -0
  38. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/tasks.md +0 -0
  39. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  40. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/design.md +0 -0
  41. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  42. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  43. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  44. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  45. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  46. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  47. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/.config.kiro +0 -0
  48. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/design.md +0 -0
  49. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/requirements.md +0 -0
  50. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/tasks.md +0 -0
  51. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/.config.kiro +0 -0
  52. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/design.md +0 -0
  53. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/requirements.md +0 -0
  54. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/tasks.md +0 -0
  55. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.kiro/steering/tech.md +0 -0
  56. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.pre-commit-config.yaml +0 -0
  57. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/.vscode/settings.json +0 -0
  58. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/README.md +0 -0
  59. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_app/__init__.py +0 -0
  60. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_app/apps.py +0 -0
  61. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_app/tasks.py +0 -0
  62. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_app/urls.py +0 -0
  63. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_app/views.py +0 -0
  64. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_project/__init__.py +0 -0
  65. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_project/settings.py +0 -0
  66. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_project/urls.py +0 -0
  67. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/example_project/wsgi.py +0 -0
  68. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/example/manage.py +0 -0
  69. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/__init__.py +0 -0
  70. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/admin.py +0 -0
  71. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/apps.py +0 -0
  72. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/decorators.py +0 -0
  73. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/logging.py +0 -0
  74. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/migrations/0001_initial.py +0 -0
  75. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/migrations/__init__.py +0 -0
  76. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/models.py +0 -0
  77. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/settings.py +0 -0
  78. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/tasks.py +0 -0
  79. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/lambda_tasks/timeouts.py +0 -0
  80. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/conftest.py +0 -0
  81. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/settings.py +0 -0
  82. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_admin.py +0 -0
  83. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_decorator.py +0 -0
  84. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_decorators.py +0 -0
  85. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_deferred_enqueue.py +0 -0
  86. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_kwargs_only.py +0 -0
  87. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_logging.py +0 -0
  88. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_models.py +0 -0
  89. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_serializer.py +0 -0
  90. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_settings.py +0 -0
  91. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_tasks.py +0 -0
  92. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_timeout_validation.py +0 -0
  93. {django_lambda_tasks-0.1.6 → django_lambda_tasks-0.2.1}/tests/test_timeouts.py +0 -0
@@ -0,0 +1 @@
1
+ {"specId": "293fc556-56fb-43f9-971a-68228fd351a8", "workflowType": "requirements-first", "specType": "feature"}
@@ -0,0 +1,225 @@
1
+ # Design Document: SSM Environment Loader
2
+
3
+ ## Overview
4
+
5
+ This feature adds a new module `ssm_environment_loader.py` to the `lambda_tasks` package that reads an AWS SSM Parameter Store parameter at Lambda cold start and sets its JSON content as environment variables. It follows the same pattern as the existing `secret_loader.py` — a module-level cached function called before `django.setup()` in `handler.py`.
6
+
7
+ The module exposes a single public function `resolve_ssm_environment()` that:
8
+ 1. Checks for the `LAMBDA_TASKS_SSM_ENVIRONMENT` env var
9
+ 2. If present, fetches the named SSM parameter via boto3
10
+ 3. Parses the parameter value as a flat JSON object (`dict[str, str]`)
11
+ 4. Sets each key-value pair in `os.environ`
12
+ 5. Caches the result so subsequent calls are no-ops
13
+
14
+ This runs **before** `resolve_secrets_into_env()` in the cold-start sequence, allowing SSM-loaded env vars to be referenced by the secret loader.
15
+
16
+ ## Architecture
17
+
18
+ ```mermaid
19
+ sequenceDiagram
20
+ participant Lambda as Lambda Container
21
+ participant SSM as ssm_environment_loader
22
+ participant Secret as secret_loader
23
+ participant Django as django.setup()
24
+
25
+ Lambda->>SSM: resolve_ssm_environment()
26
+ alt LAMBDA_TASKS_SSM_ENVIRONMENT is set
27
+ SSM->>SSM: Check module-level cache
28
+ alt Not cached
29
+ SSM->>AWS: ssm.get_parameter(Name=param_name, WithDecryption=True)
30
+ AWS-->>SSM: Parameter value (JSON string)
31
+ SSM->>SSM: Validate JSON (flat str→str object)
32
+ SSM->>SSM: Set os.environ for each key-value pair
33
+ SSM->>SSM: Store in module-level cache
34
+ end
35
+ end
36
+ Lambda->>Secret: resolve_secrets_into_env()
37
+ Lambda->>Lambda: Check DJANGO_SETTINGS_MODULE
38
+ alt DJANGO_SETTINGS_MODULE is set and apps not ready
39
+ Lambda->>Django: django.setup()
40
+ end
41
+ ```
42
+
43
+ Both loaders run **unconditionally** before the `DJANGO_SETTINGS_MODULE` check — they are both idempotent and cached. The SSM parameter may provide `DJANGO_SETTINGS_MODULE` itself, and secrets may reference SSM-loaded vars.
44
+
45
+ ## Components and Interfaces
46
+
47
+ ### Module: `lambda_tasks/ssm_environment_loader.py`
48
+
49
+ **Public API:**
50
+
51
+ ```python
52
+ def resolve_ssm_environment() -> None:
53
+ """Load SSM parameter content into os.environ.
54
+
55
+ Reads the parameter named by LAMBDA_TASKS_SSM_ENVIRONMENT,
56
+ parses it as a flat JSON object, and sets each key-value pair
57
+ as an environment variable. Idempotent — cached after first call.
58
+
59
+ Raises:
60
+ ValueError: If the parameter content is not valid JSON,
61
+ not a flat string→string mapping, or contains
62
+ an empty string key.
63
+ """
64
+ ```
65
+
66
+ **Internal helpers (keyword-only args, fully typed):**
67
+
68
+ ```python
69
+ def _fetch_parameter(*, parameter_name: str) -> str:
70
+ """Fetch a single SSM parameter value using boto3."""
71
+
72
+ def _validate_and_parse(*, raw_value: str, parameter_name: str) -> dict[str, str]:
73
+ """Parse JSON and validate it is a flat str→str mapping.
74
+
75
+ Raises ValueError with descriptive messages on failure.
76
+ """
77
+ ```
78
+
79
+ **Module-level state:**
80
+
81
+ ```python
82
+ _cache: dict[str, str] | None = None # None = not yet loaded; dict = loaded content
83
+ _loaded: bool = False # Sentinel to distinguish "loaded empty" from "not loaded"
84
+ ```
85
+
86
+ ### Integration point: `lambda_tasks/handler.py`
87
+
88
+ The cold-start block changes from:
89
+
90
+ ```python
91
+ if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
92
+ resolve_secrets_into_env()
93
+ django.setup()
94
+ ```
95
+
96
+ To:
97
+
98
+ ```python
99
+ from lambda_tasks.ssm_environment_loader import resolve_ssm_environment
100
+
101
+ # Both loaders are idempotent and run unconditionally before the
102
+ # DJANGO_SETTINGS_MODULE check — SSM may provide that var, and
103
+ # secrets may depend on SSM-loaded vars.
104
+ resolve_ssm_environment()
105
+ resolve_secrets_into_env()
106
+
107
+ if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
108
+ django.setup()
109
+ ```
110
+
111
+ ## Data Models
112
+
113
+ ### SSM Parameter Content Format
114
+
115
+ The SSM parameter value must be a JSON object where all keys and values are strings:
116
+
117
+ ```json
118
+ {
119
+ "DATABASE_URL": "postgres://user:pass@host:5432/db",
120
+ "REDIS_URL": "redis://host:6379/0",
121
+ "DJANGO_SECRET_KEY": "some-secret-key"
122
+ }
123
+ ```
124
+
125
+ **Validation rules:**
126
+ 1. Must be valid JSON (parseable by `json.loads`)
127
+ 2. Must be a JSON object (top-level `dict`)
128
+ 3. All values must be strings (no nested objects, arrays, numbers, booleans, or null)
129
+ 4. No empty string keys
130
+
131
+ ### Module-level Cache
132
+
133
+ ```python
134
+ _loaded: bool = False
135
+ ```
136
+
137
+ A simple boolean sentinel. Once `resolve_ssm_environment()` completes successfully, `_loaded` is set to `True`. Subsequent calls check this flag and return immediately. This is simpler than caching the parsed dict since we only need to know "did we already run?"
138
+
139
+ ## Correctness Properties
140
+
141
+ *A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
142
+
143
+ ### Property 1: Parameter content round-trip into environment
144
+
145
+ *For any* valid flat JSON object (where all keys are non-empty strings and all values are strings), when the SSM parameter returns that JSON, calling `resolve_ssm_environment()` SHALL result in every key-value pair from the JSON being present in `os.environ` with the correct value.
146
+
147
+ **Validates: Requirements 1.3**
148
+
149
+ ### Property 2: Invalid JSON rejection
150
+
151
+ *For any* string that is not valid JSON, when the SSM parameter returns that string, calling `resolve_ssm_environment()` SHALL raise a `ValueError` whose message contains the parameter name.
152
+
153
+ **Validates: Requirements 2.1**
154
+
155
+ ### Property 3: Non-flat JSON rejection with key identification
156
+
157
+ *For any* valid JSON object containing at least one non-string value (int, float, list, dict, bool, or null), calling `resolve_ssm_environment()` SHALL raise a `ValueError` whose message contains the names of all offending keys.
158
+
159
+ **Validates: Requirements 2.2**
160
+
161
+ ### Property 4: Idempotent execution
162
+
163
+ *For any* valid SSM parameter content, calling `resolve_ssm_environment()` N times (where N ≥ 1) SHALL result in exactly one SSM API call, with all subsequent calls returning immediately without contacting AWS.
164
+
165
+ **Validates: Requirements 3.1, 3.2**
166
+
167
+ ## Error Handling
168
+
169
+ | Condition | Behaviour | Rationale |
170
+ |---|---|---|
171
+ | `LAMBDA_TASKS_SSM_ENVIRONMENT` not set | Return immediately, no API call | Opt-in behaviour; no-op when not configured |
172
+ | SSM parameter not found (boto3 `ParameterNotFound`) | Let exception propagate | Fail fast at cold start; misconfiguration should crash the container |
173
+ | SSM API error (network, permissions) | Let exception propagate | Fail fast; boto3 exceptions propagate per project convention |
174
+ | Parameter value is not valid JSON | Raise `ValueError` with parameter name | Fail fast with actionable error message |
175
+ | JSON is not a flat object | Raise `ValueError` listing offending keys | Fail fast; identify exactly what's wrong |
176
+ | JSON contains empty string key | Raise `ValueError` | Empty env var names are invalid on all platforms |
177
+
178
+ **Design decision:** SSM keys override existing env vars without conflict detection. This differs from `secret_loader.py` which raises on conflicts. The rationale is that SSM parameters represent the canonical environment configuration — they are expected to override deployment-time defaults. This keeps the mental model simple: "SSM wins."
179
+
180
+ ## Testing Strategy
181
+
182
+ ### Property-Based Tests (Hypothesis)
183
+
184
+ The project already uses Hypothesis (`.hypothesis/` directory exists, `hypothesis` in dev dependencies). Each correctness property maps to a Hypothesis test with minimum 100 iterations.
185
+
186
+ | Property | Generator Strategy | Assertion |
187
+ |---|---|---|
188
+ | 1: Content round-trip | `st.dictionaries(keys=st.text(min_size=1, ...), values=st.text())` | All pairs present in `os.environ` |
189
+ | 2: Invalid JSON rejection | `st.text().filter(lambda s: not is_valid_json(s))` | `ValueError` raised, parameter name in message |
190
+ | 3: Non-flat JSON rejection | `st.dictionaries(...)` with at least one non-string value | `ValueError` raised, offending keys in message |
191
+ | 4: Idempotent execution | Valid content + `st.integers(min_value=2, max_value=10)` for call count | API called exactly once |
192
+
193
+ **Property test configuration:**
194
+ - Library: `hypothesis` (already installed)
195
+ - Minimum iterations: 100 (Hypothesis default is higher, which is fine)
196
+ - Tag format: `# Feature: ssm-environment-loader, Property {N}: {title}`
197
+
198
+ ### Unit Tests (Example-Based)
199
+
200
+ | Scenario | Type |
201
+ |---|---|
202
+ | Env var not set → no-op, no boto3 client created | Example (Req 1.2) |
203
+ | Empty string key in JSON → ValueError | Edge case (Req 2.3) |
204
+ | Handler cold-start ordering (SSM → secrets → Django) | Integration (Req 1.4, 1.5) |
205
+ | Module importable from `lambda_tasks.ssm_environment_loader` | Smoke (Req 4.2) |
206
+ | `resolve_ssm_environment` callable with no args | Smoke (Req 4.1) |
207
+ | SSM parameter override of existing env var | Example (confirms override behaviour) |
208
+
209
+ ### Test File
210
+
211
+ Tests live in `tests/test_ssm_environment_loader.py` following the project convention of one test file per source module.
212
+
213
+ ### Mocking Strategy
214
+
215
+ - boto3 SSM client is mocked at the module level (same pattern as `test_secret_loader.py`)
216
+ - `os.environ` manipulation via `monkeypatch` (pytest fixture)
217
+ - Module-level cache (`_loaded`) reset via autouse fixture between tests
218
+ - No real AWS calls in any test
219
+
220
+ ### TDD Approach
221
+
222
+ Following the project's TDD convention:
223
+ 1. Write a failing test for each property/example
224
+ 2. Implement the minimum code to make it pass
225
+ 3. Refactor while keeping tests green
@@ -0,0 +1,56 @@
1
+ # Requirements Document
2
+
3
+ ## Introduction
4
+
5
+ This feature adds SSM Parameter Store environment loading to the Lambda cold-start sequence. When the environment variable `LAMBDA_TASKS_SSM_ENVIRONMENT` is set, the Lambda handler reads the named SSM parameter, parses its JSON content as a flat key-value mapping, and sets the resulting pairs as environment variables. This happens before `django.setup()` and only on cold start, following the same pattern as the existing `resolve_secrets_into_env()` in `secret_loader.py`.
6
+
7
+ ## Glossary
8
+
9
+ - **SSM_Environment_Loader**: The module responsible for reading an SSM Parameter Store parameter and setting its JSON content as environment variables
10
+ - **SSM_Parameter**: An AWS Systems Manager Parameter Store parameter containing a JSON object whose keys and values are strings
11
+ - **Cold_Start**: The first invocation of a Lambda container, before `django.setup()` has been called
12
+ - **Handler**: The Lambda entry point in `handler.py` that orchestrates cold-start setup and SQS record processing
13
+
14
+ ## Requirements
15
+
16
+ ### Requirement 1: Load SSM parameter on cold start
17
+
18
+ **User Story:** As a developer deploying Lambda tasks, I want environment variables loaded from an SSM parameter at cold start, so that I can manage environment configuration centrally in Parameter Store without baking values into the Lambda deployment package.
19
+
20
+ #### Acceptance Criteria
21
+
22
+ 1. WHEN the environment variable `LAMBDA_TASKS_SSM_ENVIRONMENT` is set and the Lambda container is performing cold start, THE SSM_Environment_Loader SHALL retrieve the SSM parameter whose name matches the value of `LAMBDA_TASKS_SSM_ENVIRONMENT`
23
+ 2. WHEN the environment variable `LAMBDA_TASKS_SSM_ENVIRONMENT` is not set, THE SSM_Environment_Loader SHALL take no action and make no AWS API calls
24
+ 3. WHEN the SSM parameter is retrieved successfully, THE SSM_Environment_Loader SHALL parse the parameter value as a JSON object and set each key-value pair as an environment variable in `os.environ`
25
+ 4. THE SSM_Environment_Loader SHALL execute before the `os.environ.get("DJANGO_SETTINGS_MODULE")` check in the handler cold-start block
26
+ 5. THE SSM_Environment_Loader SHALL execute before `resolve_secrets_into_env()` is called during cold start
27
+ 6. THE SSM_Environment_Loader SHALL execute before `django.setup()` is called during cold start
28
+
29
+ ### Requirement 2: Validate SSM parameter content
30
+
31
+ **User Story:** As a developer, I want the loader to fail fast on invalid parameter content, so that misconfiguration is caught immediately at cold start rather than causing subtle runtime errors.
32
+
33
+ #### Acceptance Criteria
34
+
35
+ 1. IF the SSM parameter value is not valid JSON, THEN THE SSM_Environment_Loader SHALL raise a `ValueError` with a descriptive message including the parameter name
36
+ 2. IF the SSM parameter JSON is not a flat object (i.e. contains non-string values), THEN THE SSM_Environment_Loader SHALL raise a `ValueError` with a descriptive message identifying the offending keys
37
+ 3. IF the SSM parameter JSON contains an empty string as a key, THEN THE SSM_Environment_Loader SHALL raise a `ValueError` with a descriptive message
38
+
39
+ ### Requirement 3: Idempotent execution
40
+
41
+ **User Story:** As a developer, I want the loader to be safe to call multiple times, so that warm invocations pay no extra cost and the function can be called defensively.
42
+
43
+ #### Acceptance Criteria
44
+
45
+ 1. WHEN the SSM_Environment_Loader has already successfully loaded the parameter, THE SSM_Environment_Loader SHALL skip the AWS API call on subsequent invocations and return immediately
46
+ 2. THE SSM_Environment_Loader SHALL use a module-level cache to store the fetched parameter value for the lifetime of the Lambda container
47
+
48
+ ### Requirement 4: Function signature conventions
49
+
50
+ **User Story:** As a maintainer, I want the loader to follow the project's coding conventions, so that the codebase remains consistent.
51
+
52
+ #### Acceptance Criteria
53
+
54
+ 1. THE SSM_Environment_Loader SHALL expose a single public function `resolve_ssm_environment()` that takes no arguments
55
+ 2. THE SSM_Environment_Loader SHALL reside in a module named `ssm_environment_loader.py` within the `lambda_tasks` package
56
+ 3. THE SSM_Environment_Loader SHALL use keyword-only arguments for all internal helper functions with full type annotations
@@ -0,0 +1,124 @@
1
+ # Implementation Plan: SSM Environment Loader
2
+
3
+ ## Overview
4
+
5
+ Implement a new module `lambda_tasks/ssm_environment_loader.py` that reads an AWS SSM Parameter Store parameter at Lambda cold start and sets its JSON content as environment variables. Follow TDD: write failing tests first, then implement the minimum code to pass. Integrate into `handler.py` so SSM loading runs before secrets and Django setup.
6
+
7
+ ## Tasks
8
+
9
+ - [x] 1. Create module skeleton and test infrastructure
10
+ - [x] 1.1 Create `lambda_tasks/ssm_environment_loader.py` with module docstring, imports, module-level `_loaded` sentinel, and stub `resolve_ssm_environment()` that does nothing
11
+ - Define `_loaded: bool = False`
12
+ - Import `os`, `json`, `logging`, `boto3`
13
+ - Stub `resolve_ssm_environment() -> None` with `pass` body
14
+ - Stub `_fetch_parameter(*, parameter_name: str) -> str` with `pass` body
15
+ - Stub `_validate_and_parse(*, raw_value: str, parameter_name: str) -> dict[str, str]` with `pass` body
16
+ - All functions use keyword-only args and full type annotations
17
+ - _Requirements: 4.1, 4.2, 4.3_
18
+
19
+ - [x] 1.2 Create `tests/test_ssm_environment_loader.py` with test scaffolding
20
+ - Add autouse fixture to reset `_loaded` sentinel between tests
21
+ - Add fixture to patch boto3 SSM client at module level (same pattern as `test_secret_loader.py`)
22
+ - Add monkeypatch-based env var helpers
23
+ - Verify module is importable and `resolve_ssm_environment` is callable with no args
24
+ - _Requirements: 4.1, 4.2_
25
+
26
+ - [x] 2. Implement validation logic (`_validate_and_parse`)
27
+ - [x] 2.1 Write unit tests for `_validate_and_parse` covering invalid JSON, non-flat objects, and empty keys
28
+ - Test: invalid JSON string raises `ValueError` with parameter name in message
29
+ - Test: JSON with non-string values raises `ValueError` listing offending keys
30
+ - Test: JSON with empty string key raises `ValueError`
31
+ - Test: valid flat JSON returns `dict[str, str]`
32
+ - _Requirements: 2.1, 2.2, 2.3_
33
+
34
+ - [x] 2.2 Implement `_validate_and_parse` to pass all validation tests
35
+ - Parse with `json.loads`; catch `json.JSONDecodeError` and raise `ValueError` with parameter name
36
+ - Check top-level is a `dict`; raise if not
37
+ - Check all values are `str`; collect offending keys and raise `ValueError` listing them
38
+ - Check no empty string keys; raise `ValueError` if found
39
+ - Return validated `dict[str, str]`
40
+ - _Requirements: 2.1, 2.2, 2.3_
41
+
42
+ - [x] 2.3 Write property test for invalid JSON rejection
43
+ - **Property 2: Invalid JSON rejection**
44
+ - Generate arbitrary strings that are not valid JSON
45
+ - Assert `_validate_and_parse` raises `ValueError` with parameter name in message
46
+ - **Validates: Requirements 2.1**
47
+
48
+ - [x] 2.4 Write property test for non-flat JSON rejection with key identification
49
+ - **Property 3: Non-flat JSON rejection with key identification**
50
+ - Generate JSON objects with at least one non-string value (int, float, list, dict, bool, None)
51
+ - Assert `_validate_and_parse` raises `ValueError` whose message contains all offending key names
52
+ - **Validates: Requirements 2.2**
53
+
54
+ - [x] 3. Implement core loading logic (`resolve_ssm_environment`)
55
+ - [x] 3.1 Write unit tests for `resolve_ssm_environment` no-op behaviour
56
+ - Test: when `LAMBDA_TASKS_SSM_ENVIRONMENT` is not set, no boto3 client is created and no API call is made
57
+ - Test: when `LAMBDA_TASKS_SSM_ENVIRONMENT` is not set, `os.environ` is unchanged
58
+ - _Requirements: 1.2_
59
+
60
+ - [x] 3.2 Write unit tests for `resolve_ssm_environment` happy path
61
+ - Test: when SSM parameter contains valid flat JSON, all key-value pairs are set in `os.environ`
62
+ - Test: SSM keys override existing env vars (no conflict detection)
63
+ - Test: `_fetch_parameter` calls `ssm.get_parameter(Name=param_name, WithDecryption=True)`
64
+ - _Requirements: 1.1, 1.3_
65
+
66
+ - [x] 3.3 Implement `_fetch_parameter` and `resolve_ssm_environment`
67
+ - `_fetch_parameter`: create boto3 SSM client, call `get_parameter(Name=parameter_name, WithDecryption=True)`, return `Parameter.Value`
68
+ - `resolve_ssm_environment`: check `LAMBDA_TASKS_SSM_ENVIRONMENT` env var; if not set, return immediately; if `_loaded` is True, return immediately; call `_fetch_parameter`, call `_validate_and_parse`, set each key in `os.environ`, set `_loaded = True`
69
+ - _Requirements: 1.1, 1.2, 1.3_
70
+
71
+ - [x] 3.4 Write property test for parameter content round-trip into environment
72
+ - **Property 1: Parameter content round-trip into environment**
73
+ - Generate arbitrary flat `dict[str, str]` (non-empty keys, string values)
74
+ - Mock SSM to return `json.dumps(generated_dict)`
75
+ - Assert every key-value pair from the dict is present in `os.environ` after calling `resolve_ssm_environment()`
76
+ - **Validates: Requirements 1.3**
77
+
78
+ - [x] 4. Implement idempotency
79
+ - [x] 4.1 Write unit tests for idempotent execution
80
+ - Test: calling `resolve_ssm_environment()` twice results in only one boto3 API call
81
+ - Test: no boto3 client created on second call when `_loaded` is True
82
+ - _Requirements: 3.1, 3.2_
83
+
84
+ - [x] 4.2 Verify idempotency implementation passes (already implemented via `_loaded` flag in 3.3)
85
+ - Confirm `_loaded` sentinel prevents re-execution
86
+ - _Requirements: 3.1, 3.2_
87
+
88
+ - [x] 4.3 Write property test for idempotent execution
89
+ - **Property 4: Idempotent execution**
90
+ - Generate valid SSM content and a call count N (2 to 10)
91
+ - Call `resolve_ssm_environment()` N times
92
+ - Assert boto3 `get_parameter` was called exactly once
93
+ - **Validates: Requirements 3.1, 3.2**
94
+
95
+ - [x] 5. Checkpoint - Ensure all tests pass
96
+ - Ensure all tests pass, ask the user if questions arise.
97
+
98
+ - [x] 6. Integrate into handler and verify ordering
99
+ - [x] 6.1 Write integration test for handler cold-start ordering
100
+ - Test: `resolve_ssm_environment()` is called before `resolve_secrets_into_env()`
101
+ - Test: both loaders run unconditionally before the `DJANGO_SETTINGS_MODULE` check
102
+ - Test: `django.setup()` is only called when `DJANGO_SETTINGS_MODULE` is set and `apps.ready` is False
103
+ - _Requirements: 1.4, 1.5, 1.6_
104
+
105
+ - [x] 6.2 Modify `lambda_tasks/handler.py` to integrate SSM loader
106
+ - Add `from lambda_tasks.ssm_environment_loader import resolve_ssm_environment`
107
+ - Move `resolve_secrets_into_env()` outside the `if` block
108
+ - Add `resolve_ssm_environment()` call before `resolve_secrets_into_env()`
109
+ - Keep `django.setup()` inside the `if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:` conditional
110
+ - _Requirements: 1.4, 1.5, 1.6_
111
+
112
+ - [x] 7. Final checkpoint - Ensure all tests pass
113
+ - Ensure all tests pass, ask the user if questions arise.
114
+
115
+ ## Notes
116
+
117
+ - Tasks marked with `*` are optional and can be skipped for faster MVP
118
+ - Each task references specific requirements for traceability
119
+ - Checkpoints ensure incremental validation
120
+ - Property tests validate universal correctness properties from the design document
121
+ - Unit tests validate specific examples and edge cases
122
+ - Follow TDD: write failing tests first (tasks X.1, X.2), then implement (tasks X.3)
123
+ - All functions use keyword-only arguments and full type annotations per project conventions
124
+ - boto3 is mocked in all tests — no real AWS calls
@@ -135,11 +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
- - Calls `resolve_secrets_into_env()` before `django.setup()` at cold start to populate env vars from AWS Secrets Manager
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
+
141
+ ## Environment Loader
142
+
143
+ `resolve_environment()` in `environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
144
+
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.
148
+
149
+ Behaviour:
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)
154
+ - Sets each key-value pair in `os.environ` — existing env vars are overridden (no conflict detection)
155
+ - Idempotent via a module-level `_loaded` sentinel — subsequent calls are free no-ops
156
+ - Invalid reference format, invalid JSON, non-flat objects, or empty keys raise `ValueError` at cold start
139
157
 
140
158
  ## Secret Loader
141
159
 
142
- `resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, 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()`.
143
161
 
144
162
  Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
145
163
 
@@ -18,6 +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
+ │ ├── environment_loader.py # Loads env vars from Secrets Manager at cold start
21
22
  │ ├── tasks.py # Built-in maintenance tasks (cleanup_task_records)
22
23
  │ ├── timeouts.py # TimeoutContext implementation
23
24
  │ └── migrations/ # Django migrations for TaskRecord
@@ -39,8 +40,9 @@ django-lambda-tasks/
39
40
 
40
41
  - `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
41
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`)
42
- - `handler.py` — Lambda entry point; calls `resolve_secrets_into_env()` then `django.setup()` at cold start; processes SQS records independently; returns `batchItemFailures`
43
- - `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls, caches results in-process
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
+ - `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
44
46
  - `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
45
47
  - `settings.py` — `LambdaTasksSettings` instantiated fresh per use (reads live Django settings)
46
48
  - `admin.py` — Django admin registration for `TaskRecord`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.1.6
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
@@ -437,6 +437,12 @@ lambda_tasks.handler.handler
437
437
 
438
438
  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
439
 
440
+ | Environment Variable | Required | Description |
441
+ |---|---|---|
442
+ | `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
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
+ | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
445
+
440
446
  ### Resolving Django settings from AWS Secrets Manager
441
447
 
442
448
  The Lambda handler supports loading secret values from AWS Secrets Manager into the environment before Django starts. This lets your Django settings file read from `os.environ` as normal while keeping secrets out of plaintext environment variables.
@@ -487,6 +493,50 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
487
493
  - The named JSON key does not exist in the fetched secret
488
494
  - The secret value is not valid JSON
489
495
 
496
+ ### Loading environment variables from Secrets Manager
497
+
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
+
500
+ Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
501
+
502
+ ```
503
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
504
+ ```
505
+
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:
509
+
510
+ ```json
511
+ {
512
+ "DATABASE_URL": "postgres://user:pass@host:5432/db",
513
+ "REDIS_URL": "redis://host:6379/0",
514
+ "DJANGO_SETTINGS_MODULE": "myapp.settings.production"
515
+ }
516
+ ```
517
+
518
+ At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
519
+
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
526
+
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.
528
+
529
+ #### Validation errors
530
+
531
+ The following raise `ValueError` at cold start, preventing the Lambda container from starting:
532
+
533
+ - Reference format is invalid (wrong segment count, empty version-stage or version-id)
534
+ - Secret value is not valid JSON
535
+ - JSON is not a flat object (contains non-string values) — error message lists the offending keys
536
+ - JSON contains an empty string key
537
+
538
+ AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
539
+
490
540
  ---
491
541
 
492
542
  ## Built-in tasks
@@ -425,6 +425,12 @@ lambda_tasks.handler.handler
425
425
 
426
426
  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
427
 
428
+ | Environment Variable | Required | Description |
429
+ |---|---|---|
430
+ | `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
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
+ | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
433
+
428
434
  ### Resolving Django settings from AWS Secrets Manager
429
435
 
430
436
  The Lambda handler supports loading secret values from AWS Secrets Manager into the environment before Django starts. This lets your Django settings file read from `os.environ` as normal while keeping secrets out of plaintext environment variables.
@@ -475,6 +481,50 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
475
481
  - The named JSON key does not exist in the fetched secret
476
482
  - The secret value is not valid JSON
477
483
 
484
+ ### Loading environment variables from Secrets Manager
485
+
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
+
488
+ Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
489
+
490
+ ```
491
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
492
+ ```
493
+
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:
497
+
498
+ ```json
499
+ {
500
+ "DATABASE_URL": "postgres://user:pass@host:5432/db",
501
+ "REDIS_URL": "redis://host:6379/0",
502
+ "DJANGO_SETTINGS_MODULE": "myapp.settings.production"
503
+ }
504
+ ```
505
+
506
+ At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
507
+
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
514
+
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.
516
+
517
+ #### Validation errors
518
+
519
+ The following raise `ValueError` at cold start, preventing the Lambda container from starting:
520
+
521
+ - Reference format is invalid (wrong segment count, empty version-stage or version-id)
522
+ - Secret value is not valid JSON
523
+ - JSON is not a flat object (contains non-string values) — error message lists the offending keys
524
+ - JSON contains an empty string key
525
+
526
+ AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
527
+
478
528
  ---
479
529
 
480
530
  ## Built-in tasks