django-lambda-tasks 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/tasks.md +1 -1
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/eager-mode-example-app/design.md +0 -3
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/steering/product.md +7 -7
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/steering/structure.md +3 -3
- django_lambda_tasks-0.1.0/README.md → django_lambda_tasks-0.1.2/PKG-INFO +20 -14
- django_lambda_tasks-0.1.0/PKG-INFO → django_lambda_tasks-0.1.2/README.md +10 -24
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/admin.py +2 -1
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/decorators.py +1 -2
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/handler.py +7 -5
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/logging.py +5 -5
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/migrations/0001_initial.py +2 -11
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/models.py +7 -8
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/secret_loader.py +7 -7
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/pyproject.toml +1 -1
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_admin.py +1 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_deferred_enqueue.py +0 -9
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_handler.py +6 -12
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_logging.py +26 -25
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_models.py +118 -121
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_secret_loader.py +29 -29
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_serializer.py +1 -47
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_timeouts.py +3 -2
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.gitignore +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/README.md +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/manage.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/settings.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_settings.py +0 -0
{django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
@@ -94,7 +94,7 @@ All new functions and methods use kwargs-only signatures (enforced by `*`).
|
|
|
94
94
|
- Add `to_json(self, **kwargs: Any) -> dict`:
|
|
95
95
|
- Pop `_delay` / `_queue` overrides (same resolution logic as `on_commit`)
|
|
96
96
|
- Validate remaining kwargs via `self._kwargs_model.model_validate(kwargs)`
|
|
97
|
-
- Build `SQSLambdaTaskMessage(task_name=...,
|
|
97
|
+
- Build `SQSLambdaTaskMessage(task_name=..., kwargs=...)`
|
|
98
98
|
- Return `SQSLambdaSQSLambdaTaskMessage(message=task_message, delay=delay, queue=queue).model_dump()`
|
|
99
99
|
- Add `enqueue_from_json(self, *, data: dict) -> None`:
|
|
100
100
|
- Validate via `SQSLambdaSQSLambdaTaskMessage.model_validate(data)`
|
|
@@ -64,7 +64,7 @@ def sync_user(*, user_id: int) -> None:
|
|
|
64
64
|
|
|
65
65
|
## retry_on
|
|
66
66
|
|
|
67
|
-
Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task raises an instance of any of those types (or a subclass), the executor automatically re-enqueues the task via `execute_on_commit` with the same kwargs,
|
|
67
|
+
Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task raises an instance of any of those types (or a subclass), the executor automatically re-enqueues the task via `execute_on_commit` with the same kwargs, and an incremented `_n_retries` counter.
|
|
68
68
|
|
|
69
69
|
- `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
|
|
70
70
|
- The retry is a new invocation — the current record is terminal at `RETRYING`
|
|
@@ -98,7 +98,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
98
98
|
## Execution
|
|
99
99
|
|
|
100
100
|
`SQSLambdaTaskMessage.execute_immediately()` in `models.py`:
|
|
101
|
-
1. Checks for an existing `TaskRecord` with the same `
|
|
101
|
+
1. Checks for an existing `TaskRecord` with the same `pk` via `get_or_create`
|
|
102
102
|
2. If a record already exists (any status), logs and returns immediately — duplicate deliveries are silently skipped
|
|
103
103
|
3. Resolves timeouts: decorator default → settings defaults (soft=270s, hard=300s)
|
|
104
104
|
4. Runs task inside `transaction.atomic()` + `TimeoutContext`
|
|
@@ -121,13 +121,13 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
121
121
|
|
|
122
122
|
`resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, before `django.setup()`.
|
|
123
123
|
|
|
124
|
-
Any env var prefixed `
|
|
124
|
+
Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
|
|
125
125
|
|
|
126
126
|
Required format: `<arn>:<json-key>:<version-stage>:<version-id>` (10 colon-separated segments, all fields non-empty).
|
|
127
127
|
|
|
128
128
|
Behaviour:
|
|
129
129
|
- All references are validated before any AWS call — malformed references raise `ValueError` immediately
|
|
130
|
-
- Setting both `
|
|
130
|
+
- Setting both `LAMBDA_TASKS_SECRET_FOO` and `FOO` is a configuration error and raises `ValueError`
|
|
131
131
|
- Calls are batched by `(ARN, version-stage, version-id)` — one `GetSecretValue` per unique combination
|
|
132
132
|
- Fetched secrets are cached in-process; warm invocations pay no extra cost
|
|
133
133
|
|
|
@@ -141,7 +141,7 @@ Behaviour:
|
|
|
141
141
|
|
|
142
142
|
## TaskRecord Model
|
|
143
143
|
|
|
144
|
-
Fields: `task_name`, `
|
|
144
|
+
Fields: `task_name`, `pk` (unique UUID), `kwargs`, `status`, `start_time`, `end_time`, `result`, `traceback`
|
|
145
145
|
|
|
146
146
|
Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
|
|
147
147
|
|
|
@@ -167,7 +167,7 @@ Set `LAMBDA_TASKS_EAGER = True` to run tasks synchronously in-process (no SQS).
|
|
|
167
167
|
|
|
168
168
|
## Logging
|
|
169
169
|
|
|
170
|
-
Import `task_logger` to emit log records that are automatically prefixed with the active `
|
|
170
|
+
Import `task_logger` to emit log records that are automatically prefixed with the active `message_id`:
|
|
171
171
|
|
|
172
172
|
```python
|
|
173
173
|
from lambda_tasks.logging import task_logger
|
|
@@ -179,7 +179,7 @@ def my_task(*, user_id: int) -> None:
|
|
|
179
179
|
# → "[abc-123] processing user 42"
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
-
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. The executor sets the `
|
|
182
|
+
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. The executor sets the `message_id` before each task runs and clears it in a `finally` block. Using your own `logging.getLogger(__name__)` is fine — those records just won't carry the prefix.
|
|
183
183
|
|
|
184
184
|
## Conventions
|
|
185
185
|
|
|
@@ -17,7 +17,7 @@ django-lambda-tasks/
|
|
|
17
17
|
│ ├── logging.py # task_logger — invocation-scoped LoggerAdapter
|
|
18
18
|
│ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
|
|
19
19
|
│ ├── settings.py # LambdaTasksSettings (lazy Django settings reader)
|
|
20
|
-
│ ├── secret_loader.py # Resolves
|
|
20
|
+
│ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
|
|
21
21
|
│ ├── timeouts.py # TimeoutContext implementation
|
|
22
22
|
│ └── migrations/ # Django migrations for TaskRecord
|
|
23
23
|
├── tests/ # pytest test suite
|
|
@@ -39,8 +39,8 @@ django-lambda-tasks/
|
|
|
39
39
|
- `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
|
|
40
40
|
- `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`)
|
|
41
41
|
- `handler.py` — Lambda entry point; calls `resolve_secrets_into_env()` then `django.setup()` at cold start; processes SQS records independently; returns `batchItemFailures`
|
|
42
|
-
- `secret_loader.py` — resolves `
|
|
43
|
-
- `logging.py` — `task_logger` singleton; `
|
|
42
|
+
- `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
|
+
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
44
44
|
- `settings.py` — `LambdaTasksSettings` instantiated fresh per use (reads live Django settings)
|
|
45
45
|
- `admin.py` — Django admin registration for `TaskRecord`
|
|
46
46
|
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-lambda-tasks
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Run async tasks in a lambda function
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: boto3
|
|
7
|
+
Requires-Dist: django
|
|
8
|
+
Requires-Dist: pydantic
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
1
11
|
# Django Lambda Tasks
|
|
2
12
|
|
|
3
13
|
A Django library for offloading work to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler that AWS invokes with SQS message batches. Task results, status, and metadata are persisted in the Django database.
|
|
@@ -180,7 +190,6 @@ payload = send_welcome_email.serialize(user_id=42, template="welcome")
|
|
|
180
190
|
# {
|
|
181
191
|
# "message": {
|
|
182
192
|
# "task_name": "myapp.tasks.send_welcome_email",
|
|
183
|
-
# "invocation_id": "<uuid4>",
|
|
184
193
|
# "kwargs": {"user_id": 42, "template": "welcome"}
|
|
185
194
|
# },
|
|
186
195
|
# "delay": 0,
|
|
@@ -199,8 +208,6 @@ task = SQSLambdaTask.model_validate(payload)
|
|
|
199
208
|
task.execute_on_commit()
|
|
200
209
|
```
|
|
201
210
|
|
|
202
|
-
> **Note:** `serialize()` generates a fresh `invocation_id` on every call. Capture the result once if you need a stable reference to a specific invocation.
|
|
203
|
-
|
|
204
211
|
---
|
|
205
212
|
|
|
206
213
|
## Timeouts
|
|
@@ -265,7 +272,7 @@ TaskRecord.objects.all()
|
|
|
265
272
|
TaskRecord.objects.filter(status=TaskRecord.TaskStatus.FAILED)
|
|
266
273
|
|
|
267
274
|
# Look up a specific invocation
|
|
268
|
-
TaskRecord.objects.get(
|
|
275
|
+
TaskRecord.objects.get(pk="<uuid>")
|
|
269
276
|
```
|
|
270
277
|
|
|
271
278
|
### `TaskRecord` fields
|
|
@@ -273,7 +280,6 @@ TaskRecord.objects.get(invocation_id="<uuid>")
|
|
|
273
280
|
| Field | Type | Description |
|
|
274
281
|
|---|---|---|
|
|
275
282
|
| `task_name` | `str` | Fully-qualified function name (e.g. `myapp.tasks.send_welcome_email`). |
|
|
276
|
-
| `invocation_id` | `UUID` | Unique ID generated at enqueue time. |
|
|
277
283
|
| `kwargs` | `dict` | Serialized task arguments. |
|
|
278
284
|
| `status` | `str` | One of `RUNNING`, `SUCCESS`, `FAILED`. |
|
|
279
285
|
| `start_time` | `datetime \| None` | When the worker began executing the task. |
|
|
@@ -285,7 +291,7 @@ TaskRecord.objects.get(invocation_id="<uuid>")
|
|
|
285
291
|
|
|
286
292
|
## Logging
|
|
287
293
|
|
|
288
|
-
Import `task_logger` to emit log records that are automatically prefixed with the active `
|
|
294
|
+
Import `task_logger` to emit log records that are automatically prefixed with the active `message_id`. This makes it straightforward to filter all logs for a specific task invocation in CloudWatch Logs Insights.
|
|
289
295
|
|
|
290
296
|
```python
|
|
291
297
|
from lambda_tasks.logging import task_logger
|
|
@@ -298,9 +304,9 @@ def send_welcome_email(*, user_id: int, template: str) -> str:
|
|
|
298
304
|
return "sent"
|
|
299
305
|
```
|
|
300
306
|
|
|
301
|
-
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `
|
|
307
|
+
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `message_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
|
|
302
308
|
|
|
303
|
-
Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `
|
|
309
|
+
Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `message_id` prefix.
|
|
304
310
|
|
|
305
311
|
To filter by invocation in CloudWatch Logs Insights:
|
|
306
312
|
|
|
@@ -393,15 +399,15 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
393
399
|
|
|
394
400
|
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.
|
|
395
401
|
|
|
396
|
-
Set any env var with the prefix `
|
|
402
|
+
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:
|
|
397
403
|
|
|
398
404
|
```
|
|
399
|
-
|
|
405
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
400
406
|
```
|
|
401
407
|
|
|
402
408
|
At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
403
409
|
|
|
404
|
-
1. Scans all env vars for the `
|
|
410
|
+
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
405
411
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
406
412
|
3. Groups references by `(ARN, version-stage, version-id)` and makes one `GetSecretValue` call per unique combination
|
|
407
413
|
4. Extracts the named JSON key from the secret and writes it into `os.environ`
|
|
@@ -425,8 +431,8 @@ arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWS
|
|
|
425
431
|
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-stage, version-id)` combination:
|
|
426
432
|
|
|
427
433
|
```
|
|
428
|
-
|
|
429
|
-
|
|
434
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
435
|
+
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
|
|
430
436
|
```
|
|
431
437
|
|
|
432
438
|
#### Validation errors
|
|
@@ -435,7 +441,7 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
435
441
|
|
|
436
442
|
- Wrong number of colon-separated segments (must be exactly 10)
|
|
437
443
|
- Empty `json-key`, `version-stage`, or `version-id`
|
|
438
|
-
- Both `
|
|
444
|
+
- Both `LAMBDA_TASKS_SECRET_FOO` and `FOO` are set — use one or the other
|
|
439
445
|
- The named JSON key does not exist in the fetched secret
|
|
440
446
|
- The secret value is not valid JSON
|
|
441
447
|
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Run async tasks in a lambda function
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: boto3
|
|
7
|
-
Requires-Dist: django
|
|
8
|
-
Requires-Dist: pydantic
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
|
|
11
1
|
# Django Lambda Tasks
|
|
12
2
|
|
|
13
3
|
A Django library for offloading work to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler that AWS invokes with SQS message batches. Task results, status, and metadata are persisted in the Django database.
|
|
@@ -190,7 +180,6 @@ payload = send_welcome_email.serialize(user_id=42, template="welcome")
|
|
|
190
180
|
# {
|
|
191
181
|
# "message": {
|
|
192
182
|
# "task_name": "myapp.tasks.send_welcome_email",
|
|
193
|
-
# "invocation_id": "<uuid4>",
|
|
194
183
|
# "kwargs": {"user_id": 42, "template": "welcome"}
|
|
195
184
|
# },
|
|
196
185
|
# "delay": 0,
|
|
@@ -209,8 +198,6 @@ task = SQSLambdaTask.model_validate(payload)
|
|
|
209
198
|
task.execute_on_commit()
|
|
210
199
|
```
|
|
211
200
|
|
|
212
|
-
> **Note:** `serialize()` generates a fresh `invocation_id` on every call. Capture the result once if you need a stable reference to a specific invocation.
|
|
213
|
-
|
|
214
201
|
---
|
|
215
202
|
|
|
216
203
|
## Timeouts
|
|
@@ -275,7 +262,7 @@ TaskRecord.objects.all()
|
|
|
275
262
|
TaskRecord.objects.filter(status=TaskRecord.TaskStatus.FAILED)
|
|
276
263
|
|
|
277
264
|
# Look up a specific invocation
|
|
278
|
-
TaskRecord.objects.get(
|
|
265
|
+
TaskRecord.objects.get(pk="<uuid>")
|
|
279
266
|
```
|
|
280
267
|
|
|
281
268
|
### `TaskRecord` fields
|
|
@@ -283,7 +270,6 @@ TaskRecord.objects.get(invocation_id="<uuid>")
|
|
|
283
270
|
| Field | Type | Description |
|
|
284
271
|
|---|---|---|
|
|
285
272
|
| `task_name` | `str` | Fully-qualified function name (e.g. `myapp.tasks.send_welcome_email`). |
|
|
286
|
-
| `invocation_id` | `UUID` | Unique ID generated at enqueue time. |
|
|
287
273
|
| `kwargs` | `dict` | Serialized task arguments. |
|
|
288
274
|
| `status` | `str` | One of `RUNNING`, `SUCCESS`, `FAILED`. |
|
|
289
275
|
| `start_time` | `datetime \| None` | When the worker began executing the task. |
|
|
@@ -295,7 +281,7 @@ TaskRecord.objects.get(invocation_id="<uuid>")
|
|
|
295
281
|
|
|
296
282
|
## Logging
|
|
297
283
|
|
|
298
|
-
Import `task_logger` to emit log records that are automatically prefixed with the active `
|
|
284
|
+
Import `task_logger` to emit log records that are automatically prefixed with the active `message_id`. This makes it straightforward to filter all logs for a specific task invocation in CloudWatch Logs Insights.
|
|
299
285
|
|
|
300
286
|
```python
|
|
301
287
|
from lambda_tasks.logging import task_logger
|
|
@@ -308,9 +294,9 @@ def send_welcome_email(*, user_id: int, template: str) -> str:
|
|
|
308
294
|
return "sent"
|
|
309
295
|
```
|
|
310
296
|
|
|
311
|
-
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `
|
|
297
|
+
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `message_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
|
|
312
298
|
|
|
313
|
-
Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `
|
|
299
|
+
Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `message_id` prefix.
|
|
314
300
|
|
|
315
301
|
To filter by invocation in CloudWatch Logs Insights:
|
|
316
302
|
|
|
@@ -403,15 +389,15 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
403
389
|
|
|
404
390
|
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.
|
|
405
391
|
|
|
406
|
-
Set any env var with the prefix `
|
|
392
|
+
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:
|
|
407
393
|
|
|
408
394
|
```
|
|
409
|
-
|
|
395
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
410
396
|
```
|
|
411
397
|
|
|
412
398
|
At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
413
399
|
|
|
414
|
-
1. Scans all env vars for the `
|
|
400
|
+
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
415
401
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
416
402
|
3. Groups references by `(ARN, version-stage, version-id)` and makes one `GetSecretValue` call per unique combination
|
|
417
403
|
4. Extracts the named JSON key from the secret and writes it into `os.environ`
|
|
@@ -435,8 +421,8 @@ arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWS
|
|
|
435
421
|
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-stage, version-id)` combination:
|
|
436
422
|
|
|
437
423
|
```
|
|
438
|
-
|
|
439
|
-
|
|
424
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
425
|
+
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
|
|
440
426
|
```
|
|
441
427
|
|
|
442
428
|
#### Validation errors
|
|
@@ -445,7 +431,7 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
445
431
|
|
|
446
432
|
- Wrong number of colon-separated segments (must be exactly 10)
|
|
447
433
|
- Empty `json-key`, `version-stage`, or `version-id`
|
|
448
|
-
- Both `
|
|
434
|
+
- Both `LAMBDA_TASKS_SECRET_FOO` and `FOO` are set — use one or the other
|
|
449
435
|
- The named JSON key does not exist in the fetched secret
|
|
450
436
|
- The secret value is not valid JSON
|
|
451
437
|
|
|
@@ -10,6 +10,7 @@ from lambda_tasks.models import TaskRecord
|
|
|
10
10
|
@admin.register(TaskRecord)
|
|
11
11
|
class TaskRecordAdmin(admin.ModelAdmin):
|
|
12
12
|
list_display = (
|
|
13
|
+
"pk",
|
|
13
14
|
"task_name",
|
|
14
15
|
"status",
|
|
15
16
|
"start_time",
|
|
@@ -20,7 +21,7 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
20
21
|
)
|
|
21
22
|
list_filter = ("status", "task_name")
|
|
22
23
|
date_hierarchy = "start_time"
|
|
23
|
-
search_fields = ("
|
|
24
|
+
search_fields = ("pk", "kwargs")
|
|
24
25
|
|
|
25
26
|
def get_queryset(self, request: HttpRequest) -> QuerySet:
|
|
26
27
|
return (
|
|
@@ -153,7 +153,6 @@ class LambdaTaskWrapper:
|
|
|
153
153
|
|
|
154
154
|
message = SQSLambdaTaskMessage(
|
|
155
155
|
task_name=f"{self._func.__module__}.{self._func.__qualname__}",
|
|
156
|
-
invocation_id=str(uuid.uuid4()),
|
|
157
156
|
kwargs=dict(kwargs),
|
|
158
157
|
n_retries=n_retries,
|
|
159
158
|
)
|
|
@@ -170,7 +169,7 @@ class LambdaTaskWrapper:
|
|
|
170
169
|
Accepts task kwargs plus reserved override kwargs:
|
|
171
170
|
_delay: int
|
|
172
171
|
|
|
173
|
-
Returns a dict matching SQSLambdaTaskMessage schema
|
|
172
|
+
Returns a dict matching SQSLambdaTaskMessage schema
|
|
174
173
|
|
|
175
174
|
Raises:
|
|
176
175
|
pydantic.ValidationError: if kwargs fail the task's declared type annotations.
|
|
@@ -10,16 +10,15 @@ import logging
|
|
|
10
10
|
import os
|
|
11
11
|
|
|
12
12
|
import django
|
|
13
|
-
from django.apps import apps
|
|
13
|
+
from django.apps import apps
|
|
14
14
|
|
|
15
|
-
from lambda_tasks.models import SQSLambdaTaskMessage
|
|
16
15
|
from lambda_tasks.secret_loader import resolve_secrets_into_env
|
|
17
16
|
|
|
18
17
|
# Cold-start Django setup — runs once per Lambda container.
|
|
19
18
|
# Secrets are resolved first so Django settings can reference the populated
|
|
20
|
-
# env vars.
|
|
19
|
+
# env vars. resolve_secrets_into_env() is idempotent and caches fetched
|
|
21
20
|
# secrets in-process, so subsequent invocations pay no extra cost.
|
|
22
|
-
if os.environ.get("DJANGO_SETTINGS_MODULE") and not
|
|
21
|
+
if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
|
|
23
22
|
resolve_secrets_into_env()
|
|
24
23
|
django.setup()
|
|
25
24
|
|
|
@@ -32,13 +31,16 @@ def handler(*, event: dict, context: object) -> dict:
|
|
|
32
31
|
|
|
33
32
|
Returns a partial-batch failure report so AWS only re-drives failed records.
|
|
34
33
|
"""
|
|
34
|
+
# Local import due to AppRegistryNotReady
|
|
35
|
+
from lambda_tasks.models import SQSLambdaTaskMessage
|
|
36
|
+
|
|
35
37
|
batch_item_failures: list[dict] = []
|
|
36
38
|
|
|
37
39
|
for record in event["Records"]:
|
|
38
40
|
try:
|
|
39
41
|
SQSLambdaTaskMessage.model_validate_json(
|
|
40
42
|
record["body"]
|
|
41
|
-
).execute_immediately()
|
|
43
|
+
).execute_immediately(message_id=record["messageId"])
|
|
42
44
|
except Exception:
|
|
43
45
|
logger.error(
|
|
44
46
|
"Failed to process SQS record %s",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Provides task_logger — a LoggerAdapter that automatically includes the
|
|
3
|
-
current
|
|
3
|
+
current message_id on every log record while a task is executing.
|
|
4
4
|
|
|
5
5
|
Usage in task functions::
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ Usage in task functions::
|
|
|
9
9
|
@lambda_task(...)
|
|
10
10
|
def my_task(*, user_id: int) -> None:
|
|
11
11
|
task_logger.info("processing user %s", user_id)
|
|
12
|
-
# → "processing user 42" tagged with the active
|
|
12
|
+
# → "processing user 42" tagged with the active message_id
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
import logging
|
|
@@ -18,16 +18,16 @@ from typing import Any
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class _TaskLogger(logging.LoggerAdapter):
|
|
21
|
-
"""LoggerAdapter that prepends [
|
|
21
|
+
"""LoggerAdapter that prepends [message_id] to every message."""
|
|
22
22
|
|
|
23
23
|
def __init__(self) -> None:
|
|
24
24
|
super().__init__(logging.getLogger("lambda_tasks.task"), extra={})
|
|
25
|
-
self.
|
|
25
|
+
self.message_id: str | None = None
|
|
26
26
|
|
|
27
27
|
def process(
|
|
28
28
|
self, msg: Any, kwargs: MutableMapping[str, Any]
|
|
29
29
|
) -> tuple[Any, MutableMapping[str, Any]]:
|
|
30
|
-
prefix = f"[{self.
|
|
30
|
+
prefix = f"[{self.message_id}] " if self.message_id else ""
|
|
31
31
|
return f"{prefix}{msg}", kwargs
|
|
32
32
|
|
|
33
33
|
|
{django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/migrations/0001_initial.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 6.0.3 on 2026-03-26
|
|
1
|
+
# Generated by Django 6.0.3 on 2026-03-26 14:06
|
|
2
2
|
|
|
3
3
|
from django.db import migrations, models
|
|
4
4
|
|
|
@@ -15,15 +15,9 @@ class Migration(migrations.Migration):
|
|
|
15
15
|
fields=[
|
|
16
16
|
(
|
|
17
17
|
"id",
|
|
18
|
-
models.
|
|
19
|
-
auto_created=True,
|
|
20
|
-
primary_key=True,
|
|
21
|
-
serialize=False,
|
|
22
|
-
verbose_name="ID",
|
|
23
|
-
),
|
|
18
|
+
models.UUIDField(editable=False, primary_key=True, serialize=False),
|
|
24
19
|
),
|
|
25
20
|
("task_name", models.CharField(editable=False, max_length=255)),
|
|
26
|
-
("invocation_id", models.UUIDField(editable=False, unique=True)),
|
|
27
21
|
("kwargs", models.JSONField(editable=False)),
|
|
28
22
|
("n_retries", models.PositiveSmallIntegerField(editable=False)),
|
|
29
23
|
(
|
|
@@ -50,9 +44,6 @@ class Migration(migrations.Migration):
|
|
|
50
44
|
models.Index(
|
|
51
45
|
fields=["task_name"], name="lambda_task_task_na_f00cb7_idx"
|
|
52
46
|
),
|
|
53
|
-
models.Index(
|
|
54
|
-
fields=["invocation_id"], name="lambda_task_invocat_3d5a22_idx"
|
|
55
|
-
),
|
|
56
47
|
models.Index(
|
|
57
48
|
fields=["status"], name="lambda_task_status_6901ee_idx"
|
|
58
49
|
),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import random
|
|
4
4
|
import traceback
|
|
5
|
+
import uuid
|
|
5
6
|
|
|
6
7
|
import boto3
|
|
7
8
|
from django.core.exceptions import ImproperlyConfigured
|
|
@@ -33,8 +34,8 @@ class TaskStatus(models.TextChoices):
|
|
|
33
34
|
class TaskRecord(models.Model):
|
|
34
35
|
TaskStatus = TaskStatus
|
|
35
36
|
|
|
37
|
+
id = models.UUIDField(primary_key=True, editable=False)
|
|
36
38
|
task_name = models.CharField(max_length=255, editable=False)
|
|
37
|
-
invocation_id = models.UUIDField(unique=True, editable=False)
|
|
38
39
|
kwargs = models.JSONField(editable=False)
|
|
39
40
|
n_retries = models.PositiveSmallIntegerField(editable=False)
|
|
40
41
|
status = models.CharField(
|
|
@@ -55,7 +56,6 @@ class TaskRecord(models.Model):
|
|
|
55
56
|
ordering = ["-start_time"]
|
|
56
57
|
indexes = [
|
|
57
58
|
models.Index(fields=["task_name"]),
|
|
58
|
-
models.Index(fields=["invocation_id"]),
|
|
59
59
|
models.Index(fields=["status"]),
|
|
60
60
|
models.Index(fields=["-start_time"]),
|
|
61
61
|
]
|
|
@@ -71,18 +71,17 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
71
71
|
model_config = ConfigDict(extra="forbid")
|
|
72
72
|
|
|
73
73
|
task_name: str
|
|
74
|
-
invocation_id: str
|
|
75
74
|
kwargs: dict
|
|
76
75
|
n_retries: int = Field(default=0, ge=0)
|
|
77
76
|
|
|
78
|
-
def execute_immediately(self) -> None:
|
|
77
|
+
def execute_immediately(self, *, message_id: str) -> None:
|
|
79
78
|
"""Execute a background task described.
|
|
80
79
|
|
|
81
80
|
Creates a TaskRecord, resolves timeouts, validates configuration,
|
|
82
81
|
runs the task inside an atomic block with timeout enforcement, and
|
|
83
82
|
persists the outcome.
|
|
84
83
|
"""
|
|
85
|
-
task_logger.
|
|
84
|
+
task_logger.message_id = message_id
|
|
86
85
|
|
|
87
86
|
try:
|
|
88
87
|
task_logger.info(f"Received {self.task_name}")
|
|
@@ -104,7 +103,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
104
103
|
|
|
105
104
|
record, created = (
|
|
106
105
|
TaskRecord.objects.get_or_create( # ty: ignore[unresolved-attribute]
|
|
107
|
-
|
|
106
|
+
pk=message_id,
|
|
108
107
|
defaults={
|
|
109
108
|
"task_name": self.task_name,
|
|
110
109
|
"kwargs": self.kwargs,
|
|
@@ -200,7 +199,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
200
199
|
)
|
|
201
200
|
|
|
202
201
|
finally:
|
|
203
|
-
task_logger.
|
|
202
|
+
task_logger.message_id = None
|
|
204
203
|
|
|
205
204
|
|
|
206
205
|
class SQSLambdaTask(BaseModel):
|
|
@@ -224,7 +223,7 @@ class SQSLambdaTask(BaseModel):
|
|
|
224
223
|
conf = LambdaTasksSettings()
|
|
225
224
|
|
|
226
225
|
if conf.EAGER:
|
|
227
|
-
self.message.execute_immediately()
|
|
226
|
+
self.message.execute_immediately(message_id=str(uuid.uuid4()))
|
|
228
227
|
else:
|
|
229
228
|
try:
|
|
230
229
|
queue_url = conf.QUEUES[self.queue]
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Resolves environment variables that reference AWS Secrets Manager ARNs.
|
|
3
3
|
|
|
4
|
-
Any env var prefixed with ``
|
|
4
|
+
Any env var prefixed with ``LAMBDA_TASKS_SECRET_`` is treated as a pointer
|
|
5
5
|
to a secret value. The unprefixed name is the target env var to populate.
|
|
6
6
|
|
|
7
7
|
Required value format
|
|
8
8
|
---------------------
|
|
9
9
|
Every reference must follow the full dynamic reference syntax::
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
LAMBDA_TASKS_SECRET_DJANGO_ADMIN_URL=arn:aws:secretsmanager:eu-west-1:123:secret:my-secret:DJANGO_ADMIN_URL:AWSCURRENT:v1
|
|
12
12
|
|
|
13
13
|
That is: ``<arn>:<json-key>:<version-stage>:<version-id>``
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ All four suffix segments must be present and non-empty.
|
|
|
16
16
|
A malformed reference raises ``ValueError`` immediately so the Lambda
|
|
17
17
|
container fails at cold start rather than silently misconfiguring Django.
|
|
18
18
|
|
|
19
|
-
It is a configuration error to set both ``
|
|
19
|
+
It is a configuration error to set both ``LAMBDA_TASKS_SECRET_FOO`` and
|
|
20
20
|
``FOO`` — use one or the other. Having both raises ``ValueError`` at cold
|
|
21
21
|
start so the misconfiguration is caught immediately.
|
|
22
22
|
|
|
@@ -35,7 +35,7 @@ import boto3
|
|
|
35
35
|
|
|
36
36
|
logger = logging.getLogger(__name__)
|
|
37
37
|
|
|
38
|
-
_PREFIX = "
|
|
38
|
+
_PREFIX = "LAMBDA_TASKS_SECRET_"
|
|
39
39
|
|
|
40
40
|
# Module-level cache: (arn, version_stage, version_id) → raw secret string.
|
|
41
41
|
# Populated on first call; reused for the lifetime of the Lambda container.
|
|
@@ -128,14 +128,14 @@ def _fetch_secret(*, client: object, ref: _SecretReference) -> dict[str, str]:
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
def resolve_secrets_into_env() -> None:
|
|
131
|
-
"""Scan env vars for ``
|
|
131
|
+
"""Scan env vars for ``LAMBDA_TASKS_SECRET_*`` references and resolve them.
|
|
132
132
|
|
|
133
133
|
For each matching env var the resolved value is written back into
|
|
134
134
|
``os.environ`` under the unprefixed name.
|
|
135
135
|
|
|
136
136
|
Raises ``ValueError`` at cold start if:
|
|
137
137
|
- A reference is malformed (wrong segment count, any empty field)
|
|
138
|
-
- The target env var is already set — use ``
|
|
138
|
+
- The target env var is already set — use ``LAMBDA_TASKS_SECRET_FOO`` or
|
|
139
139
|
``FOO``, not both
|
|
140
140
|
|
|
141
141
|
This function is idempotent — calling it multiple times is safe and cheap
|
|
@@ -157,7 +157,7 @@ def resolve_secrets_into_env() -> None:
|
|
|
157
157
|
if conflicts:
|
|
158
158
|
raise ValueError(
|
|
159
159
|
"The following environment variables are set both directly and via "
|
|
160
|
-
f"
|
|
160
|
+
f"LAMBDA_TASKS_SECRET_*: {', '.join(sorted(conflicts))}. "
|
|
161
161
|
"Use one or the other, not both."
|
|
162
162
|
)
|
|
163
163
|
|
|
@@ -58,13 +58,6 @@ def test_to_json_task_name_matches_module_qualname():
|
|
|
58
58
|
assert result["message"]["task_name"] == expected
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def test_to_json_invocation_id_is_valid_uuid4():
|
|
62
|
-
"""result['message']['invocation_id'] is a valid UUID4 string."""
|
|
63
|
-
result = _wrapper.serialize(x=1)
|
|
64
|
-
parsed = uuid.UUID(result["message"]["invocation_id"])
|
|
65
|
-
assert parsed.version == 4
|
|
66
|
-
|
|
67
|
-
|
|
68
61
|
def test_to_json_raises_validation_error_for_wrong_type_kwargs():
|
|
69
62
|
"""to_json(x='not_an_int') raises ValidationError when x is annotated as int."""
|
|
70
63
|
with pytest.raises(ValidationError):
|
|
@@ -95,7 +88,6 @@ _valid_deferred_dict_st = st.fixed_dictionaries(
|
|
|
95
88
|
"message": st.fixed_dictionaries(
|
|
96
89
|
{
|
|
97
90
|
"task_name": st.just("tests.test_deferred_enqueue._task"),
|
|
98
|
-
"invocation_id": st.uuids().map(str),
|
|
99
91
|
"kwargs": st.fixed_dictionaries({"x": st.integers()}),
|
|
100
92
|
}
|
|
101
93
|
),
|
|
@@ -120,7 +112,6 @@ def test_p1_to_json_structural_invariant(x: int) -> None:
|
|
|
120
112
|
result = _wrapper.serialize(x=x)
|
|
121
113
|
SQSLambdaTask.model_validate(result)
|
|
122
114
|
assert result["message"]["task_name"] == f"{_task.__module__}.{_task.__qualname__}"
|
|
123
|
-
assert uuid.UUID(result["message"]["invocation_id"]).version == 4
|
|
124
115
|
assert result["message"]["kwargs"] == {"x": x}
|
|
125
116
|
assert result["delay"] == 0
|
|
126
117
|
assert result["queue"] == "default"
|