django-lambda-tasks 0.4.11__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/steering/product.md +4 -4
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/PKG-INFO +19 -19
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/README.md +18 -18
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/environment_loader.py +17 -25
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/models.py +2 -2
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/secret_loader.py +30 -25
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/pyproject.toml +1 -1
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_environment_loader.py +11 -20
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_secret_loader.py +15 -20
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.gitignore +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/steering/structure.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/README.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/manage.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/handler.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/settings.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/tests/test_timeouts.py +0 -0
|
@@ -138,12 +138,12 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
138
138
|
|
|
139
139
|
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.
|
|
140
140
|
|
|
141
|
-
Required format: `<arn>:<version-
|
|
141
|
+
Required format: `<arn>:<version-id>` (8 colon-separated segments — the ARN is 7 segments, plus the version-id). The version-id must be non-empty.
|
|
142
142
|
|
|
143
143
|
Behaviour:
|
|
144
144
|
- If `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is not set, does nothing (no AWS API calls)
|
|
145
145
|
- Validates the reference format before any AWS call — malformed references raise `ValueError` immediately
|
|
146
|
-
- Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
146
|
+
- Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
147
147
|
- Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
148
148
|
- Sets each key-value pair in `os.environ` — existing env vars are overridden (no conflict detection)
|
|
149
149
|
- Idempotent via a module-level `_loaded` sentinel — subsequent calls are free no-ops
|
|
@@ -155,12 +155,12 @@ Behaviour:
|
|
|
155
155
|
|
|
156
156
|
Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
|
|
157
157
|
|
|
158
|
-
Required format: `<arn>:<json-key>:<version-stage>:<version-id>` (10 colon-separated segments
|
|
158
|
+
Required format: `<arn>:<json-key>:<version-stage>:<version-id>` (10 colon-separated segments). The version-stage segment must be empty. The json-key and version-id must be non-empty.
|
|
159
159
|
|
|
160
160
|
Behaviour:
|
|
161
161
|
- All references are validated before any AWS call — malformed references raise `ValueError` immediately
|
|
162
162
|
- Setting both `LAMBDA_TASKS_SECRET_FOO` and `FOO` is a configuration error and raises `ValueError`
|
|
163
|
-
- Calls are batched by `(ARN, version-
|
|
163
|
+
- Calls are batched by `(ARN, version-id)` — one `GetSecretValue` per unique combination
|
|
164
164
|
- Fetched secrets are cached in-process; warm invocations pay no extra cost
|
|
165
165
|
|
|
166
166
|
## Models
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -150,7 +150,7 @@ LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
|
|
|
150
150
|
|
|
151
151
|
| Setting | Type | Description |
|
|
152
152
|
|---|---|---|
|
|
153
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-
|
|
153
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at Lambda cold start. |
|
|
154
154
|
| `LAMBDA_TASKS_SECRET_*` | env var(s) | Secrets Manager references resolved into env vars at Lambda cold start. The unprefixed name becomes the target env var. |
|
|
155
155
|
|
|
156
156
|
These are environment variables set on the Lambda function, not Django settings. See [Loading environment variables from Secrets Manager](#loading-environment-variables-from-secrets-manager) and [Resolving individual secrets from AWS Secrets Manager](#resolving-individual-secrets-from-aws-secrets-manager) for full details.
|
|
@@ -203,7 +203,6 @@ Key characteristics:
|
|
|
203
203
|
|
|
204
204
|
```python
|
|
205
205
|
@lambda_task(
|
|
206
|
-
delay=0, # seconds — SQS DelaySeconds before message becomes visible
|
|
207
206
|
soft_timeout=60, # seconds — overrides global default for this task
|
|
208
207
|
hard_timeout=90, # seconds — overrides global default for this task
|
|
209
208
|
queue="default", # named queue from LAMBDA_TASKS_QUEUES
|
|
@@ -493,20 +492,20 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
493
492
|
| Environment Variable | Required | Description |
|
|
494
493
|
|---|---|---|
|
|
495
494
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
496
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-
|
|
495
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at cold start (runs first). |
|
|
497
496
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
|
|
498
497
|
|
|
499
498
|
### Loading environment variables from Secrets Manager
|
|
500
499
|
|
|
501
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.
|
|
502
501
|
|
|
503
|
-
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version
|
|
502
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version ID:
|
|
504
503
|
|
|
505
504
|
```
|
|
506
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:
|
|
505
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:v1
|
|
507
506
|
```
|
|
508
507
|
|
|
509
|
-
The format is `<arn>:<version-
|
|
508
|
+
The format is `<arn>:<version-id>` (8 colon-separated segments — the ARN is 7 segments, plus the version-id). The version-id must be non-empty.
|
|
510
509
|
|
|
511
510
|
The secret value must be a flat JSON object where all keys and values are strings:
|
|
512
511
|
|
|
@@ -521,8 +520,8 @@ The secret value must be a flat JSON object where all keys and values are string
|
|
|
521
520
|
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
522
521
|
|
|
523
522
|
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
524
|
-
2. Parses and validates the reference format (
|
|
525
|
-
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
523
|
+
2. Parses and validates the reference format (8 segments, non-empty version-id)
|
|
524
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
526
525
|
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
527
526
|
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
528
527
|
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
@@ -533,7 +532,7 @@ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_
|
|
|
533
532
|
|
|
534
533
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
535
534
|
|
|
536
|
-
- Reference format is invalid (wrong segment count, empty version-
|
|
535
|
+
- Reference format is invalid (wrong segment count, empty version-id)
|
|
537
536
|
- Secret value is not valid JSON
|
|
538
537
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
539
538
|
- JSON contains an empty string key
|
|
@@ -547,14 +546,14 @@ The Lambda handler supports loading individual secret values from AWS Secrets Ma
|
|
|
547
546
|
Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager dynamic reference. The unprefixed name becomes the target env var:
|
|
548
547
|
|
|
549
548
|
```
|
|
550
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
549
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
551
550
|
```
|
|
552
551
|
|
|
553
552
|
At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
|
|
554
553
|
|
|
555
554
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
556
555
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
557
|
-
3. Groups references by `(ARN, version-
|
|
556
|
+
3. Groups references by `(ARN, version-id)` and makes one `GetSecretValue` call per unique combination
|
|
558
557
|
4. Extracts the named JSON key from the secret and writes it into `os.environ`
|
|
559
558
|
5. Caches fetched secrets in-process — warm invocations pay no extra cost
|
|
560
559
|
|
|
@@ -566,18 +565,18 @@ Every value must follow the full dynamic reference syntax:
|
|
|
566
565
|
<arn>:<json-key>:<version-stage>:<version-id>
|
|
567
566
|
```
|
|
568
567
|
|
|
569
|
-
|
|
568
|
+
The version-stage segment must be empty. The json-key and version-id fields are required and must be non-empty. The secret value must be a JSON object; `json-key` names the field to extract.
|
|
570
569
|
|
|
571
570
|
```
|
|
572
|
-
# arn (7 segments) : json-key : version-stage : version-id
|
|
573
|
-
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
571
|
+
# arn (7 segments) : json-key : version-stage (empty) : version-id
|
|
572
|
+
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
574
573
|
```
|
|
575
574
|
|
|
576
|
-
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-
|
|
575
|
+
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-id)` combination:
|
|
577
576
|
|
|
578
577
|
```
|
|
579
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL
|
|
580
|
-
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY
|
|
578
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL::v1
|
|
579
|
+
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY::v1
|
|
581
580
|
```
|
|
582
581
|
|
|
583
582
|
#### Validation errors
|
|
@@ -585,7 +584,8 @@ LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
|
|
|
585
584
|
The following all raise `ValueError` at cold start, preventing the Lambda container from starting with a misconfigured environment:
|
|
586
585
|
|
|
587
586
|
- Wrong number of colon-separated segments (must be exactly 10)
|
|
588
|
-
- Empty `json-key
|
|
587
|
+
- Empty `json-key` or `version-id`
|
|
588
|
+
- Non-empty `version-stage` (version-stage is not supported)
|
|
589
589
|
- Both `LAMBDA_TASKS_SECRET_FOO` and `FOO` are set — use one or the other
|
|
590
590
|
- The named JSON key does not exist in the fetched secret
|
|
591
591
|
- The secret value is not valid JSON
|
|
@@ -138,7 +138,7 @@ LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
|
|
|
138
138
|
|
|
139
139
|
| Setting | Type | Description |
|
|
140
140
|
|---|---|---|
|
|
141
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-
|
|
141
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at Lambda cold start. |
|
|
142
142
|
| `LAMBDA_TASKS_SECRET_*` | env var(s) | Secrets Manager references resolved into env vars at Lambda cold start. The unprefixed name becomes the target env var. |
|
|
143
143
|
|
|
144
144
|
These are environment variables set on the Lambda function, not Django settings. See [Loading environment variables from Secrets Manager](#loading-environment-variables-from-secrets-manager) and [Resolving individual secrets from AWS Secrets Manager](#resolving-individual-secrets-from-aws-secrets-manager) for full details.
|
|
@@ -191,7 +191,6 @@ Key characteristics:
|
|
|
191
191
|
|
|
192
192
|
```python
|
|
193
193
|
@lambda_task(
|
|
194
|
-
delay=0, # seconds — SQS DelaySeconds before message becomes visible
|
|
195
194
|
soft_timeout=60, # seconds — overrides global default for this task
|
|
196
195
|
hard_timeout=90, # seconds — overrides global default for this task
|
|
197
196
|
queue="default", # named queue from LAMBDA_TASKS_QUEUES
|
|
@@ -481,20 +480,20 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
481
480
|
| Environment Variable | Required | Description |
|
|
482
481
|
|---|---|---|
|
|
483
482
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
484
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-
|
|
483
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at cold start (runs first). |
|
|
485
484
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
|
|
486
485
|
|
|
487
486
|
### Loading environment variables from Secrets Manager
|
|
488
487
|
|
|
489
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.
|
|
490
489
|
|
|
491
|
-
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version
|
|
490
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version ID:
|
|
492
491
|
|
|
493
492
|
```
|
|
494
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:
|
|
493
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:v1
|
|
495
494
|
```
|
|
496
495
|
|
|
497
|
-
The format is `<arn>:<version-
|
|
496
|
+
The format is `<arn>:<version-id>` (8 colon-separated segments — the ARN is 7 segments, plus the version-id). The version-id must be non-empty.
|
|
498
497
|
|
|
499
498
|
The secret value must be a flat JSON object where all keys and values are strings:
|
|
500
499
|
|
|
@@ -509,8 +508,8 @@ The secret value must be a flat JSON object where all keys and values are string
|
|
|
509
508
|
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
510
509
|
|
|
511
510
|
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
512
|
-
2. Parses and validates the reference format (
|
|
513
|
-
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
511
|
+
2. Parses and validates the reference format (8 segments, non-empty version-id)
|
|
512
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
514
513
|
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
515
514
|
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
516
515
|
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
@@ -521,7 +520,7 @@ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_
|
|
|
521
520
|
|
|
522
521
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
523
522
|
|
|
524
|
-
- Reference format is invalid (wrong segment count, empty version-
|
|
523
|
+
- Reference format is invalid (wrong segment count, empty version-id)
|
|
525
524
|
- Secret value is not valid JSON
|
|
526
525
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
527
526
|
- JSON contains an empty string key
|
|
@@ -535,14 +534,14 @@ The Lambda handler supports loading individual secret values from AWS Secrets Ma
|
|
|
535
534
|
Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager dynamic reference. The unprefixed name becomes the target env var:
|
|
536
535
|
|
|
537
536
|
```
|
|
538
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
537
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
539
538
|
```
|
|
540
539
|
|
|
541
540
|
At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
|
|
542
541
|
|
|
543
542
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
544
543
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
545
|
-
3. Groups references by `(ARN, version-
|
|
544
|
+
3. Groups references by `(ARN, version-id)` and makes one `GetSecretValue` call per unique combination
|
|
546
545
|
4. Extracts the named JSON key from the secret and writes it into `os.environ`
|
|
547
546
|
5. Caches fetched secrets in-process — warm invocations pay no extra cost
|
|
548
547
|
|
|
@@ -554,18 +553,18 @@ Every value must follow the full dynamic reference syntax:
|
|
|
554
553
|
<arn>:<json-key>:<version-stage>:<version-id>
|
|
555
554
|
```
|
|
556
555
|
|
|
557
|
-
|
|
556
|
+
The version-stage segment must be empty. The json-key and version-id fields are required and must be non-empty. The secret value must be a JSON object; `json-key` names the field to extract.
|
|
558
557
|
|
|
559
558
|
```
|
|
560
|
-
# arn (7 segments) : json-key : version-stage : version-id
|
|
561
|
-
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
559
|
+
# arn (7 segments) : json-key : version-stage (empty) : version-id
|
|
560
|
+
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
562
561
|
```
|
|
563
562
|
|
|
564
|
-
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-
|
|
563
|
+
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-id)` combination:
|
|
565
564
|
|
|
566
565
|
```
|
|
567
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL
|
|
568
|
-
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY
|
|
566
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL::v1
|
|
567
|
+
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY::v1
|
|
569
568
|
```
|
|
570
569
|
|
|
571
570
|
#### Validation errors
|
|
@@ -573,7 +572,8 @@ LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
|
|
|
573
572
|
The following all raise `ValueError` at cold start, preventing the Lambda container from starting with a misconfigured environment:
|
|
574
573
|
|
|
575
574
|
- Wrong number of colon-separated segments (must be exactly 10)
|
|
576
|
-
- Empty `json-key
|
|
575
|
+
- Empty `json-key` or `version-id`
|
|
576
|
+
- Non-empty `version-stage` (version-stage is not supported)
|
|
577
577
|
- Both `LAMBDA_TASKS_SECRET_FOO` and `FOO` are set — use one or the other
|
|
578
578
|
- The named JSON key does not exist in the fetched secret
|
|
579
579
|
- The secret value is not valid JSON
|
|
@@ -8,10 +8,10 @@ object (all keys and values must be strings), and sets each key-value pair in
|
|
|
8
8
|
|
|
9
9
|
Required value format::
|
|
10
10
|
|
|
11
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123:secret:my-env:
|
|
11
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123:secret:my-env:v1
|
|
12
12
|
|
|
13
|
-
That is: ``<arn>:<version-
|
|
14
|
-
The ARN is 7 segments, plus
|
|
13
|
+
That is: ``<arn>:<version-id>`` (8 colon-separated segments).
|
|
14
|
+
The ARN is 7 segments, plus the version-id.
|
|
15
15
|
|
|
16
16
|
This is called by the handler on the first invocation (cold start) — before
|
|
17
17
|
``resolve_secrets_into_env()`` and before ``django.setup()`` — so that
|
|
@@ -36,7 +36,6 @@ _loaded: bool = False
|
|
|
36
36
|
|
|
37
37
|
class _EnvironmentSecretReference(NamedTuple):
|
|
38
38
|
arn: str
|
|
39
|
-
version_stage: str
|
|
40
39
|
version_id: str
|
|
41
40
|
|
|
42
41
|
|
|
@@ -76,40 +75,34 @@ def _parse_reference(*, value: str) -> _EnvironmentSecretReference:
|
|
|
76
75
|
|
|
77
76
|
Expected format::
|
|
78
77
|
|
|
79
|
-
<arn>:<version-
|
|
78
|
+
<arn>:<version-id>
|
|
80
79
|
|
|
81
|
-
The ARN itself is 7 colon-separated segments, plus
|
|
82
|
-
(version-
|
|
83
|
-
|
|
80
|
+
The ARN itself is 7 colon-separated segments, plus 1 suffix segment
|
|
81
|
+
(version-id) = 8 total.
|
|
82
|
+
The version-id field must be non-empty.
|
|
84
83
|
|
|
85
84
|
Raises ``ValueError`` if the format is invalid.
|
|
86
85
|
"""
|
|
87
86
|
parts = value.split(":")
|
|
88
87
|
|
|
89
|
-
if len(parts) !=
|
|
88
|
+
if len(parts) != 8:
|
|
90
89
|
raise ValueError(
|
|
91
90
|
"LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN has an invalid format. "
|
|
92
|
-
"Expected <arn>:<version-
|
|
93
|
-
f"(
|
|
91
|
+
"Expected <arn>:<version-id> "
|
|
92
|
+
f"(8 colon-separated segments), got {len(parts)}: {value!r}"
|
|
94
93
|
)
|
|
95
94
|
|
|
96
95
|
arn = ":".join(parts[:7])
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
)
|
|
96
|
+
version_id = parts[7]
|
|
97
|
+
|
|
98
|
+
if not version_id:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN is missing the "
|
|
101
|
+
f"version-id segment: {value!r}"
|
|
102
|
+
)
|
|
109
103
|
|
|
110
104
|
return _EnvironmentSecretReference(
|
|
111
105
|
arn=arn,
|
|
112
|
-
version_stage=version_stage,
|
|
113
106
|
version_id=version_id,
|
|
114
107
|
)
|
|
115
108
|
|
|
@@ -119,7 +112,6 @@ def _fetch_secret(*, ref: _EnvironmentSecretReference) -> str:
|
|
|
119
112
|
client = boto3.client("secretsmanager")
|
|
120
113
|
response = client.get_secret_value(
|
|
121
114
|
SecretId=ref.arn,
|
|
122
|
-
VersionStage=ref.version_stage,
|
|
123
115
|
VersionId=ref.version_id,
|
|
124
116
|
)
|
|
125
117
|
return response["SecretString"]
|
|
@@ -18,7 +18,7 @@ from redis.exceptions import LockError
|
|
|
18
18
|
|
|
19
19
|
from lambda_tasks.local_executor import submit_task
|
|
20
20
|
from lambda_tasks.logging import task_logger
|
|
21
|
-
from lambda_tasks.settings import LambdaTasksSettings
|
|
21
|
+
from lambda_tasks.settings import MAX_DELAY, LambdaTasksSettings
|
|
22
22
|
from lambda_tasks.timeouts import TimeoutContext
|
|
23
23
|
|
|
24
24
|
|
|
@@ -188,7 +188,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
188
188
|
)
|
|
189
189
|
|
|
190
190
|
delay = min(
|
|
191
|
-
wrapper.retry_delay + round(random.uniform(1, 5)),
|
|
191
|
+
wrapper.retry_delay + round(random.uniform(1, 5)), MAX_DELAY
|
|
192
192
|
)
|
|
193
193
|
|
|
194
194
|
retry_task = SQSLambdaTask(
|
|
@@ -8,11 +8,12 @@ Required value format
|
|
|
8
8
|
---------------------
|
|
9
9
|
Every reference must follow the full dynamic reference syntax::
|
|
10
10
|
|
|
11
|
-
LAMBDA_TASKS_SECRET_DJANGO_ADMIN_URL=arn:aws:secretsmanager:eu-west-1:123:secret:my-secret:DJANGO_ADMIN_URL
|
|
11
|
+
LAMBDA_TASKS_SECRET_DJANGO_ADMIN_URL=arn:aws:secretsmanager:eu-west-1:123:secret:my-secret:DJANGO_ADMIN_URL::v1
|
|
12
12
|
|
|
13
13
|
That is: ``<arn>:<json-key>:<version-stage>:<version-id>``
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The version-stage segment must be empty (version-stage is not supported).
|
|
16
|
+
All other suffix segments must be present and non-empty.
|
|
16
17
|
A malformed reference raises ``ValueError`` immediately so the Lambda
|
|
17
18
|
container fails at cold start rather than silently misconfiguring Django.
|
|
18
19
|
|
|
@@ -20,7 +21,7 @@ It is a configuration error to set both ``LAMBDA_TASKS_SECRET_FOO`` and
|
|
|
20
21
|
``FOO`` — use one or the other. Having both raises ``ValueError`` at cold
|
|
21
22
|
start so the misconfiguration is caught immediately.
|
|
22
23
|
|
|
23
|
-
Calls are batched by unique (ARN, version-
|
|
24
|
+
Calls are batched by unique (ARN, version-id) combination —
|
|
24
25
|
one ``GetSecretValue`` call per unique combination, regardless of how many
|
|
25
26
|
env vars reference it. Results are cached in-process so repeated calls to
|
|
26
27
|
``resolve_secrets_into_env`` are free after the first.
|
|
@@ -41,15 +42,14 @@ _PREFIX = "LAMBDA_TASKS_SECRET_"
|
|
|
41
42
|
# subsequent calls return immediately without re-scanning or re-resolving.
|
|
42
43
|
_loaded: bool = False
|
|
43
44
|
|
|
44
|
-
# Module-level cache: (arn,
|
|
45
|
+
# Module-level cache: (arn, version_id) → raw secret string.
|
|
45
46
|
# Populated on first call; reused for the lifetime of the Lambda container.
|
|
46
|
-
_secret_cache: dict[tuple[str, str
|
|
47
|
+
_secret_cache: dict[tuple[str, str], dict[str, str]] = {}
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
class _SecretReference(NamedTuple):
|
|
50
51
|
arn: str
|
|
51
52
|
json_key: str
|
|
52
|
-
version_stage: str
|
|
53
53
|
version_id: str
|
|
54
54
|
|
|
55
55
|
|
|
@@ -62,7 +62,8 @@ def _parse_reference(*, env_var: str, value: str) -> _SecretReference:
|
|
|
62
62
|
|
|
63
63
|
The ARN itself is 7 colon-separated segments, plus 3 suffix segments
|
|
64
64
|
(json-key, version-stage, version-id) = 10 total.
|
|
65
|
-
|
|
65
|
+
The version-stage segment must be empty. The json-key and version-id
|
|
66
|
+
segments must be non-empty.
|
|
66
67
|
|
|
67
68
|
Raises ``ValueError`` if the format is invalid — this is intentional so
|
|
68
69
|
the Lambda container fails at cold start rather than starting with a
|
|
@@ -83,21 +84,28 @@ def _parse_reference(*, env_var: str, value: str) -> _SecretReference:
|
|
|
83
84
|
arn = ":".join(parts[:7])
|
|
84
85
|
json_key, version_stage, version_id = parts[7], parts[8], parts[9]
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
if not json_key:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"{env_var} is missing the json-key segment in its Secrets Manager "
|
|
90
|
+
f"reference: {value!r}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if version_stage:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"{env_var} has a non-empty version-stage segment in its Secrets Manager "
|
|
96
|
+
f"reference: {value!r}. The version-stage must be empty "
|
|
97
|
+
f"(format: <arn>:<json-key>::<version-id>)."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not version_id:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"{env_var} is missing the version-id segment in its Secrets Manager "
|
|
103
|
+
f"reference: {value!r}"
|
|
104
|
+
)
|
|
96
105
|
|
|
97
106
|
return _SecretReference(
|
|
98
107
|
arn=arn,
|
|
99
108
|
json_key=json_key,
|
|
100
|
-
version_stage=version_stage,
|
|
101
109
|
version_id=version_id,
|
|
102
110
|
)
|
|
103
111
|
|
|
@@ -105,10 +113,10 @@ def _parse_reference(*, env_var: str, value: str) -> _SecretReference:
|
|
|
105
113
|
def _fetch_secret(*, client: Any, ref: _SecretReference) -> dict[str, str]:
|
|
106
114
|
"""Fetch a secret string from Secrets Manager, using the module cache.
|
|
107
115
|
|
|
108
|
-
The cache key is ``(arn,
|
|
116
|
+
The cache key is ``(arn, version_id)`` so that references
|
|
109
117
|
to different versions of the same secret are fetched independently.
|
|
110
118
|
"""
|
|
111
|
-
cache_key = (ref.arn, ref.
|
|
119
|
+
cache_key = (ref.arn, ref.version_id)
|
|
112
120
|
|
|
113
121
|
if cache_key in _secret_cache:
|
|
114
122
|
return _secret_cache[cache_key]
|
|
@@ -117,7 +125,6 @@ def _fetch_secret(*, client: Any, ref: _SecretReference) -> dict[str, str]:
|
|
|
117
125
|
|
|
118
126
|
response = client.get_secret_value(
|
|
119
127
|
SecretId=ref.arn,
|
|
120
|
-
VersionStage=ref.version_stage,
|
|
121
128
|
VersionId=ref.version_id,
|
|
122
129
|
)
|
|
123
130
|
|
|
@@ -171,10 +178,8 @@ def resolve_secrets_into_env() -> None:
|
|
|
171
178
|
"Use one or the other, not both."
|
|
172
179
|
)
|
|
173
180
|
|
|
174
|
-
# Unique (arn,
|
|
175
|
-
unique_cache_keys = {
|
|
176
|
-
(ref.arn, ref.version_stage, ref.version_id) for ref in references.values()
|
|
177
|
-
}
|
|
181
|
+
# Unique (arn, version_id) combinations to minimise API calls.
|
|
182
|
+
unique_cache_keys = {(ref.arn, ref.version_id) for ref in references.values()}
|
|
178
183
|
uncached = unique_cache_keys - _secret_cache.keys()
|
|
179
184
|
logger.info(
|
|
180
185
|
f"Resolving {len(references)} secret reference(s) from"
|
|
@@ -19,8 +19,8 @@ from lambda_tasks.environment_loader import (
|
|
|
19
19
|
resolve_environment,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
-
# A valid
|
|
23
|
-
_VALID_REF = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:my-env:
|
|
22
|
+
# A valid 8-segment reference used throughout tests
|
|
23
|
+
_VALID_REF = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:my-env:v1"
|
|
24
24
|
_VALID_ARN = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:my-env"
|
|
25
25
|
|
|
26
26
|
|
|
@@ -83,36 +83,28 @@ class TestParseReference:
|
|
|
83
83
|
"""Unit tests for _parse_reference covering format validation."""
|
|
84
84
|
|
|
85
85
|
def test_valid_reference_returns_named_tuple(self) -> None:
|
|
86
|
-
"""A valid
|
|
86
|
+
"""A valid 8-segment reference is parsed correctly."""
|
|
87
87
|
ref = _parse_reference(value=_VALID_REF)
|
|
88
88
|
assert ref.arn == _VALID_ARN
|
|
89
|
-
assert ref.version_stage == "AWSCURRENT"
|
|
90
89
|
assert ref.version_id == "v1"
|
|
91
90
|
|
|
92
91
|
def test_too_few_segments_raises_value_error(self) -> None:
|
|
93
|
-
"""Fewer than
|
|
94
|
-
with pytest.raises(ValueError, match="
|
|
92
|
+
"""Fewer than 8 segments raises ValueError."""
|
|
93
|
+
with pytest.raises(ValueError, match="8 colon-separated segments"):
|
|
95
94
|
_parse_reference(value="arn:aws:secretsmanager:eu-west-1:123:secret:my-env")
|
|
96
95
|
|
|
97
96
|
def test_too_many_segments_raises_value_error(self) -> None:
|
|
98
|
-
"""More than
|
|
99
|
-
with pytest.raises(ValueError, match="
|
|
97
|
+
"""More than 8 segments raises ValueError."""
|
|
98
|
+
with pytest.raises(ValueError, match="8 colon-separated segments"):
|
|
100
99
|
_parse_reference(
|
|
101
|
-
value="arn:aws:secretsmanager:eu-west-1:123:secret:my-env:
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
def test_empty_version_stage_raises_value_error(self) -> None:
|
|
105
|
-
"""Empty version-stage raises ValueError."""
|
|
106
|
-
with pytest.raises(ValueError, match="version-stage"):
|
|
107
|
-
_parse_reference(
|
|
108
|
-
value="arn:aws:secretsmanager:eu-west-1:123:secret:my-env::v1"
|
|
100
|
+
value="arn:aws:secretsmanager:eu-west-1:123:secret:my-env:v1:extra"
|
|
109
101
|
)
|
|
110
102
|
|
|
111
103
|
def test_empty_version_id_raises_value_error(self) -> None:
|
|
112
104
|
"""Empty version-id raises ValueError."""
|
|
113
105
|
with pytest.raises(ValueError, match="version-id"):
|
|
114
106
|
_parse_reference(
|
|
115
|
-
value="arn:aws:secretsmanager:eu-west-1:123:secret:my-env:
|
|
107
|
+
value="arn:aws:secretsmanager:eu-west-1:123:secret:my-env:"
|
|
116
108
|
)
|
|
117
109
|
|
|
118
110
|
|
|
@@ -344,7 +336,7 @@ class TestResolveHappyPath:
|
|
|
344
336
|
mock_secretsmanager_client: MagicMock,
|
|
345
337
|
monkeypatch,
|
|
346
338
|
) -> None:
|
|
347
|
-
"""_fetch_secret calls get_secret_value with ARN
|
|
339
|
+
"""_fetch_secret calls get_secret_value with ARN and VersionId only."""
|
|
348
340
|
set_env_arn(value=_VALID_REF)
|
|
349
341
|
mock_secretsmanager_client.get_secret_value.return_value = {
|
|
350
342
|
"SecretString": json.dumps({"KEY": "value"})
|
|
@@ -354,7 +346,6 @@ class TestResolveHappyPath:
|
|
|
354
346
|
|
|
355
347
|
mock_secretsmanager_client.get_secret_value.assert_called_once_with(
|
|
356
348
|
SecretId=_VALID_ARN,
|
|
357
|
-
VersionStage="AWSCURRENT",
|
|
358
349
|
VersionId="v1",
|
|
359
350
|
)
|
|
360
351
|
|
|
@@ -369,7 +360,7 @@ class TestResolveHappyPath:
|
|
|
369
360
|
"""An env var with wrong segment count raises ValueError before any API call."""
|
|
370
361
|
set_env_arn(value="arn:aws:secretsmanager:eu-west-1:123:secret:my-env")
|
|
371
362
|
|
|
372
|
-
with pytest.raises(ValueError, match="
|
|
363
|
+
with pytest.raises(ValueError, match="8 colon-separated segments"):
|
|
373
364
|
resolve_environment()
|
|
374
365
|
|
|
375
366
|
mock_secretsmanager_client.get_secret_value.assert_not_called()
|
|
@@ -23,9 +23,9 @@ ARN = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:my-secret"
|
|
|
23
23
|
ARN2 = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:other-secret"
|
|
24
24
|
|
|
25
25
|
# A valid full reference: <arn>:<json-key>:<version-stage>:<version-id>
|
|
26
|
-
# ARN = 7 segments, + 3 suffix = 10 total. version-
|
|
27
|
-
VALID_REF = f"{ARN}:MY_KEY
|
|
28
|
-
VALID_REF2 = f"{ARN2}:OTHER_KEY
|
|
26
|
+
# ARN = 7 segments, + 3 suffix = 10 total. version-stage must be empty.
|
|
27
|
+
VALID_REF = f"{ARN}:MY_KEY::v1"
|
|
28
|
+
VALID_REF2 = f"{ARN2}:OTHER_KEY::v1"
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@pytest.fixture(autouse=True)
|
|
@@ -48,15 +48,13 @@ class TestParseReferenceValid:
|
|
|
48
48
|
ref = _parse_reference(env_var="LAMBDA_TASKS_SECRET_X", value=VALID_REF)
|
|
49
49
|
assert ref.arn == ARN
|
|
50
50
|
assert ref.json_key == "MY_KEY"
|
|
51
|
-
assert ref.
|
|
52
|
-
assert ref.version_id == "AWSCURRENT"
|
|
51
|
+
assert ref.version_id == "v1"
|
|
53
52
|
|
|
54
53
|
def test_version_id_populated(self):
|
|
55
|
-
value = f"{ARN}:DJANGO_ADMIN_URL
|
|
54
|
+
value = f"{ARN}:DJANGO_ADMIN_URL::abc123"
|
|
56
55
|
ref = _parse_reference(env_var="LAMBDA_TASKS_SECRET_X", value=value)
|
|
57
56
|
assert ref.arn == ARN
|
|
58
57
|
assert ref.json_key == "DJANGO_ADMIN_URL"
|
|
59
|
-
assert ref.version_stage == "AWSCURRENT"
|
|
60
58
|
assert ref.version_id == "abc123"
|
|
61
59
|
|
|
62
60
|
|
|
@@ -72,17 +70,17 @@ class TestParseReferenceInvalid:
|
|
|
72
70
|
|
|
73
71
|
def test_empty_json_key_rejected(self):
|
|
74
72
|
# 10 segments but key is empty
|
|
75
|
-
value = f"{ARN}
|
|
73
|
+
value = f"{ARN}:::abc123"
|
|
76
74
|
with pytest.raises(ValueError, match="missing the json-key"):
|
|
77
75
|
_parse_reference(env_var="LAMBDA_TASKS_SECRET_X", value=value)
|
|
78
76
|
|
|
79
|
-
def
|
|
80
|
-
value = f"{ARN}:MY_KEY
|
|
81
|
-
with pytest.raises(ValueError, match="
|
|
77
|
+
def test_non_empty_version_stage_rejected(self):
|
|
78
|
+
value = f"{ARN}:MY_KEY:AWSCURRENT:abc123"
|
|
79
|
+
with pytest.raises(ValueError, match="version-stage must be empty"):
|
|
82
80
|
_parse_reference(env_var="LAMBDA_TASKS_SECRET_X", value=value)
|
|
83
81
|
|
|
84
82
|
def test_empty_version_id_rejected(self):
|
|
85
|
-
value = f"{ARN}:MY_KEY
|
|
83
|
+
value = f"{ARN}:MY_KEY::"
|
|
86
84
|
with pytest.raises(ValueError, match="missing the version-id"):
|
|
87
85
|
_parse_reference(env_var="LAMBDA_TASKS_SECRET_X", value=value)
|
|
88
86
|
|
|
@@ -140,7 +138,7 @@ class TestMalformedReference:
|
|
|
140
138
|
mock_boto3.client.assert_not_called()
|
|
141
139
|
|
|
142
140
|
def test_empty_key_raises_before_any_boto3_call(self, monkeypatch):
|
|
143
|
-
monkeypatch.setenv("LAMBDA_TASKS_SECRET_MY_VAR", f"{ARN}
|
|
141
|
+
monkeypatch.setenv("LAMBDA_TASKS_SECRET_MY_VAR", f"{ARN}:::abc123")
|
|
144
142
|
with patch("lambda_tasks.secret_loader.boto3") as mock_boto3:
|
|
145
143
|
with pytest.raises(ValueError, match="missing the json-key"):
|
|
146
144
|
resolve_secrets_into_env()
|
|
@@ -239,8 +237,8 @@ class TestResolution:
|
|
|
239
237
|
|
|
240
238
|
class TestBatching:
|
|
241
239
|
def test_single_api_call_for_same_arn(self, monkeypatch):
|
|
242
|
-
ref_a = f"{ARN}:KEY_A
|
|
243
|
-
ref_b = f"{ARN}:KEY_B
|
|
240
|
+
ref_a = f"{ARN}:KEY_A::v1"
|
|
241
|
+
ref_b = f"{ARN}:KEY_B::v1"
|
|
244
242
|
monkeypatch.setenv("LAMBDA_TASKS_SECRET_VAR_A", ref_a)
|
|
245
243
|
monkeypatch.setenv("LAMBDA_TASKS_SECRET_VAR_B", ref_b)
|
|
246
244
|
monkeypatch.delenv("VAR_A", raising=False)
|
|
@@ -256,8 +254,7 @@ class TestBatching:
|
|
|
256
254
|
|
|
257
255
|
client.get_secret_value.assert_called_once_with(
|
|
258
256
|
SecretId=ARN,
|
|
259
|
-
|
|
260
|
-
VersionId="AWSCURRENT",
|
|
257
|
+
VersionId="v1",
|
|
261
258
|
)
|
|
262
259
|
assert os.environ["VAR_A"] == "val-a"
|
|
263
260
|
assert os.environ["VAR_B"] == "val-b"
|
|
@@ -315,9 +312,7 @@ class TestCaching:
|
|
|
315
312
|
mock_boto3.client.assert_not_called()
|
|
316
313
|
|
|
317
314
|
def test_no_boto3_client_created_when_all_cached(self, monkeypatch):
|
|
318
|
-
secret_loader._secret_cache[(ARN, "
|
|
319
|
-
"MY_KEY": "pre-cached"
|
|
320
|
-
}
|
|
315
|
+
secret_loader._secret_cache[(ARN, "v1")] = {"MY_KEY": "pre-cached"}
|
|
321
316
|
monkeypatch.setenv("LAMBDA_TASKS_SECRET_MY_VAR", VALID_REF)
|
|
322
317
|
monkeypatch.delenv("MY_VAR", raising=False)
|
|
323
318
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/.config.kiro
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/design.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/requirements.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/tasks.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_project/__init__.py
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/example/example_project/settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.11 → django_lambda_tasks-0.5.0}/lambda_tasks/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|