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.
Files changed (76) hide show
  1. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/tasks.md +1 -1
  2. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/eager-mode-example-app/design.md +0 -3
  3. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/steering/product.md +7 -7
  4. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/steering/structure.md +3 -3
  5. django_lambda_tasks-0.1.0/README.md → django_lambda_tasks-0.1.2/PKG-INFO +20 -14
  6. django_lambda_tasks-0.1.0/PKG-INFO → django_lambda_tasks-0.1.2/README.md +10 -24
  7. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/admin.py +2 -1
  8. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/decorators.py +1 -2
  9. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/handler.py +7 -5
  10. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/logging.py +5 -5
  11. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/migrations/0001_initial.py +2 -11
  12. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/models.py +7 -8
  13. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/secret_loader.py +7 -7
  14. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/pyproject.toml +1 -1
  15. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_admin.py +1 -0
  16. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_deferred_enqueue.py +0 -9
  17. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_handler.py +6 -12
  18. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_logging.py +26 -25
  19. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_models.py +118 -121
  20. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_secret_loader.py +29 -29
  21. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_serializer.py +1 -47
  22. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_timeouts.py +3 -2
  23. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.github/workflows/ci.yml +0 -0
  24. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.github/workflows/release.yml +0 -0
  25. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.gitignore +0 -0
  26. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  27. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  28. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  29. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  30. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  31. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  32. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  33. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  34. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  35. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  36. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  37. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  38. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  39. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  40. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  41. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
  42. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  43. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  44. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  45. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  46. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  47. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  48. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/task-retry/.config.kiro +0 -0
  49. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/task-retry/design.md +0 -0
  50. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/task-retry/requirements.md +0 -0
  51. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/specs/task-retry/tasks.md +0 -0
  52. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.kiro/steering/tech.md +0 -0
  53. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.pre-commit-config.yaml +0 -0
  54. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/.vscode/settings.json +0 -0
  55. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/README.md +0 -0
  56. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/__init__.py +0 -0
  57. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/apps.py +0 -0
  58. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/tasks.py +0 -0
  59. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/urls.py +0 -0
  60. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_app/views.py +0 -0
  61. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_project/__init__.py +0 -0
  62. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_project/settings.py +0 -0
  63. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_project/urls.py +0 -0
  64. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/example_project/wsgi.py +0 -0
  65. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/example/manage.py +0 -0
  66. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/__init__.py +0 -0
  67. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/apps.py +0 -0
  68. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/migrations/__init__.py +0 -0
  69. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/settings.py +0 -0
  70. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/lambda_tasks/timeouts.py +0 -0
  71. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/conftest.py +0 -0
  72. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/settings.py +0 -0
  73. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_decorator.py +0 -0
  74. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_decorators.py +0 -0
  75. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_kwargs_only.py +0 -0
  76. {django_lambda_tasks-0.1.0 → django_lambda_tasks-0.1.2}/tests/test_settings.py +0 -0
@@ -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=..., invocation_id=str(uuid.uuid4()), kwargs=...)`
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)`
@@ -85,10 +85,7 @@ if conf.EAGER:
85
85
 
86
86
  message = SQSLambdaTaskMessage(
87
87
  task_name=task_name,
88
- invocation_id=str(uuid.uuid4()),
89
88
  kwargs=task_kwargs,
90
- soft_timeout=soft_timeout,
91
- hard_timeout=hard_timeout,
92
89
  )
93
90
  execute_task(message=message)
94
91
  return
@@ -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, a fresh `invocation_id`, and an incremented `_n_retries` counter.
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 `invocation_id` via `get_or_create`
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 `AWS_SECRETS_MANAGER_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
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 `AWS_SECRETS_MANAGER_FOO` and `FOO` is a configuration error and raises `ValueError`
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`, `invocation_id` (unique UUID), `kwargs`, `status`, `start_time`, `end_time`, `result`, `traceback`
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 `invocation_id`:
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 `invocation_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.
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 AWS_SECRETS_MANAGER_* env vars at cold start
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 `AWS_SECRETS_MANAGER_*` 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; `invocation_id` set/cleared around each task execution
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(invocation_id="<uuid>")
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 `invocation_id`. This makes it straightforward to filter all logs for a specific task invocation in CloudWatch Logs Insights.
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 `invocation_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
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 `invocation_id` prefix.
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 `AWS_SECRETS_MANAGER_` to a full Secrets Manager dynamic reference. The unprefixed name becomes the target env var:
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
- AWS_SECRETS_MANAGER_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
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 `AWS_SECRETS_MANAGER_` prefix
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
- AWS_SECRETS_MANAGER_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL:AWSCURRENT:v1
429
- AWS_SECRETS_MANAGER_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
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 `AWS_SECRETS_MANAGER_FOO` and `FOO` are set — use one or the other
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(invocation_id="<uuid>")
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 `invocation_id`. This makes it straightforward to filter all logs for a specific task invocation in CloudWatch Logs Insights.
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 `invocation_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
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 `invocation_id` prefix.
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 `AWS_SECRETS_MANAGER_` to a full Secrets Manager dynamic reference. The unprefixed name becomes the target env var:
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
- AWS_SECRETS_MANAGER_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
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 `AWS_SECRETS_MANAGER_` prefix
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
- AWS_SECRETS_MANAGER_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL:AWSCURRENT:v1
439
- AWS_SECRETS_MANAGER_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
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 `AWS_SECRETS_MANAGER_FOO` and `FOO` are set — use one or the other
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 = ("invocation_id", "kwargs")
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 with a stable invocation_id.
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 as django_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. resolve_secrets_into_env() is idempotent and caches fetched
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 django_apps.ready:
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 invocation_id on every log record while a task is executing.
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 invocation_id
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 [invocation_id] to every message."""
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.invocation_id: str | None = None
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.invocation_id}] " if self.invocation_id else ""
30
+ prefix = f"[{self.message_id}] " if self.message_id else ""
31
31
  return f"{prefix}{msg}", kwargs
32
32
 
33
33
 
@@ -1,4 +1,4 @@
1
- # Generated by Django 6.0.3 on 2026-03-26 11:44
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.BigAutoField(
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.invocation_id = self.invocation_id
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
- invocation_id=self.invocation_id,
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.invocation_id = None
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 ``AWS_SECRETS_MANAGER_`` is treated as a pointer
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
- AWS_SECRETS_MANAGER_DJANGO_ADMIN_URL=arn:aws:secretsmanager:eu-west-1:123:secret:my-secret:DJANGO_ADMIN_URL:AWSCURRENT:v1
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 ``AWS_SECRETS_MANAGER_FOO`` and
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 = "AWS_SECRETS_MANAGER_"
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 ``AWS_SECRETS_MANAGER_*`` references and resolve them.
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 ``AWS_SECRETS_MANAGER_FOO`` or
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"AWS_SECRETS_MANAGER_*: {', '.join(sorted(conflicts))}. "
160
+ f"LAMBDA_TASKS_SECRET_*: {', '.join(sorted(conflicts))}. "
161
161
  "Use one or the other, not both."
162
162
  )
163
163
 
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.1.0"
10
+ version = "0.1.2"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -10,6 +10,7 @@ def test_task_record_registered_in_admin():
10
10
 
11
11
  def test_task_record_admin_list_display():
12
12
  assert admin.site._registry[TaskRecord].list_display == (
13
+ "pk",
13
14
  "task_name",
14
15
  "status",
15
16
  "start_time",
@@ -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"