django-lambda-tasks 0.2.1__tar.gz → 0.2.3__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 (94) hide show
  1. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/steering/product.md +7 -5
  2. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/steering/structure.md +1 -1
  3. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/steering/tech.md +1 -1
  4. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/PKG-INFO +83 -55
  5. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/README.md +82 -54
  6. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/environment_loader.py +4 -3
  7. django_lambda_tasks-0.2.3/lambda_tasks/handler.py +99 -0
  8. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/pyproject.toml +1 -1
  9. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_environment_loader.py +60 -65
  10. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_handler.py +111 -17
  11. django_lambda_tasks-0.2.1/lambda_tasks/handler.py +0 -54
  12. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.github/workflows/ci.yml +0 -0
  13. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.github/workflows/release.yml +0 -0
  14. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.gitignore +0 -0
  15. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  16. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  17. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  18. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  19. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  20. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  21. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  22. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  23. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  24. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  25. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  26. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  27. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  28. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  29. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  30. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  31. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/retry-delay/.config.kiro +0 -0
  32. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/retry-delay/design.md +0 -0
  33. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/retry-delay/requirements.md +0 -0
  34. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/retry-delay/tasks.md +0 -0
  35. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  36. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/rse-background-tasks/design.md +0 -0
  37. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  38. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  39. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  40. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  41. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  42. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  43. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/singleton-task/.config.kiro +0 -0
  44. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/singleton-task/design.md +0 -0
  45. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/singleton-task/requirements.md +0 -0
  46. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/singleton-task/tasks.md +0 -0
  47. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  48. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  49. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  50. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  51. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/task-retry/.config.kiro +0 -0
  52. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/task-retry/design.md +0 -0
  53. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/task-retry/requirements.md +0 -0
  54. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.kiro/specs/task-retry/tasks.md +0 -0
  55. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.pre-commit-config.yaml +0 -0
  56. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/.vscode/settings.json +0 -0
  57. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/README.md +0 -0
  58. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_app/__init__.py +0 -0
  59. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_app/apps.py +0 -0
  60. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_app/tasks.py +0 -0
  61. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_app/urls.py +0 -0
  62. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_app/views.py +0 -0
  63. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_project/__init__.py +0 -0
  64. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_project/settings.py +0 -0
  65. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_project/urls.py +0 -0
  66. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/example_project/wsgi.py +0 -0
  67. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/example/manage.py +0 -0
  68. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/__init__.py +0 -0
  69. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/admin.py +0 -0
  70. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/apps.py +0 -0
  71. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/decorators.py +0 -0
  72. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/logging.py +0 -0
  73. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/migrations/0001_initial.py +0 -0
  74. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/migrations/__init__.py +0 -0
  75. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/models.py +0 -0
  76. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/secret_loader.py +0 -0
  77. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/settings.py +0 -0
  78. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/tasks.py +0 -0
  79. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/lambda_tasks/timeouts.py +0 -0
  80. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/conftest.py +0 -0
  81. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/settings.py +0 -0
  82. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_admin.py +0 -0
  83. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_decorator.py +0 -0
  84. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_decorators.py +0 -0
  85. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_deferred_enqueue.py +0 -0
  86. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_kwargs_only.py +0 -0
  87. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_logging.py +0 -0
  88. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_models.py +0 -0
  89. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_secret_loader.py +0 -0
  90. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_serializer.py +0 -0
  91. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_settings.py +0 -0
  92. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_tasks.py +0 -0
  93. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_timeout_validation.py +0 -0
  94. {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.3}/tests/test_timeouts.py +0 -0
@@ -15,7 +15,7 @@ View → @lambda_task.execute_on_commit() → SQS → Lambda handler → SQSLamb
15
15
  Key modules:
16
16
  - `decorators.py` — `@lambda_task` decorator and `LambdaTaskWrapper`
17
17
  - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish)
18
- - `handler.py` — AWS Lambda entry point with partial-batch failure reporting
18
+ - `handler.py` — AWS Lambda entry point; cold-start init runs on first invocation (not at import time) with partial-batch failure reporting
19
19
  - `logging.py` — `task_logger` for invocation-scoped log output
20
20
  - `settings.py` — lazy `LambdaTasksSettings` reading from Django settings
21
21
 
@@ -62,7 +62,7 @@ def sync_user(*, user_id: int) -> None:
62
62
 
63
63
  ## retry_on
64
64
 
65
- 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.
65
+ 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 with the same kwargs and an incremented `n_retries` counter on the `SQSLambdaTaskMessage`.
66
66
 
67
67
  - `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
68
68
  - The retry is a new invocation — the current record is terminal at `RETRYING`
@@ -125,7 +125,7 @@ class SQSLambdaTaskMessage(BaseModel):
125
125
  7. On ignored exception (type matches `ignore_errors`): rolls back task-side writes, commits record as `SUCCESS` with traceback and `end_time`
126
126
  8. On retryable exception (type matches `retry_on` or `LockError` for singleton tasks, `n_retries < MAX_RETRIES`): rolls back task-side writes, enqueues retry via `execute_on_commit` with `n_retries + 1`, commits record as `RETRYING` with traceback and `end_time`
127
127
  9. On retryable exception with `n_retries >= MAX_RETRIES`: commits record as `FAILED` with traceback, raises `MaxRetriesExceededError`
128
- 10. On any other exception: rolls back atomic block, updates record to `FAILED` with traceback
128
+ 10. On any other exception: rolls back atomic block, updates record to `FAILED` with traceback and `end_time`
129
129
 
130
130
  ## Lambda Handler
131
131
 
@@ -135,8 +135,10 @@ class SQSLambdaTaskMessage(BaseModel):
135
135
  - Returns `{"batchItemFailures": [...]}` for partial-batch failure reporting
136
136
  - Only pre-execution failures (malformed message, import error, misconfiguration) are reported as `batchItemFailures` — task logic failures are caught and recorded as `FAILED` TaskRecords without raising
137
137
  - Recommended SQS queue settings: `maxReceiveCount=1` with a DLQ configured; automatic retries are not useful since task failures are not re-driven by design
138
- - Cold-start sequence: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`
138
+ - Cold-start sequence runs inside the handler on the first invocation (not at module import time) to avoid Lambda init-duration timeouts: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()` → `_configure_logging()`
139
+ - A module-level `_cold_start_done` sentinel ensures the sequence runs only once; subsequent warm invocations skip it
139
140
  - Both loaders run unconditionally (outside the `DJANGO_SETTINGS_MODULE` check) — the environment secret may provide that var, and individual secrets may depend on environment-loaded vars
141
+ - `_configure_logging()` sets the `lambda_tasks` logger hierarchy to `INFO` (or the level specified by `LAMBDA_TASKS_LOG_LEVEL` env var) so that `task_logger` output appears in CloudWatch
140
142
 
141
143
  ## Environment Loader
142
144
 
@@ -204,7 +206,7 @@ Set `LAMBDA_TASKS_EAGER = True` to run tasks synchronously in-process (no SQS).
204
206
 
205
207
  In eager mode a random UUID4 is generated as the `message_id` passed to `execute_immediately()`.
206
208
 
207
- **Timeouts are not enforced in eager mode.** `TimeoutContext` is skipped entirely — `SIGALRM`-based timeouts require a Lambda worker process, not a Django dev server thread. Timeout values are still validated at decoration time.
209
+ **Timeouts are not enforced in eager mode.** `TimeoutContext` is still entered but becomes a no-op it checks `LAMBDA_TASKS_EAGER` internally and skips `SIGALRM` setup. `SIGALRM`-based timeouts require a Lambda worker process, not a Django dev server thread. Timeout values are still validated at decoration time.
208
210
 
209
211
  ## Logging
210
212
 
@@ -40,7 +40,7 @@ django-lambda-tasks/
40
40
 
41
41
  - `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
42
42
  - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` publishes to SQS or executes eagerly; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
43
- - `handler.py` — Lambda entry point; calls `resolve_environment()` then `resolve_secrets_into_env()` then conditionally `django.setup()` at cold start; processes SQS records independently; returns `batchItemFailures`
43
+ - `handler.py` — Lambda entry point; cold-start init (`resolve_environment()` `resolve_secrets_into_env()` conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`
44
44
  - `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
45
45
  - `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
46
46
  - `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
@@ -42,7 +42,7 @@ uv run pytest tests/test_foo.py # single file
42
42
  uv run pytest -x # stop on first failure
43
43
  ```
44
44
 
45
- - Tests live in `tests/` — one file per source module (`executor.py` → `test_executor.py`)
45
+ - Tests live in `tests/` — one file per source module (`models.py` → `test_models.py`)
46
46
  - Django settings for tests are in `tests/settings.py`
47
47
  - Use `hypothesis` for property-based tests where correctness properties can be expressed
48
48
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -127,13 +127,32 @@ LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 240
127
127
  LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
128
128
  ```
129
129
 
130
+ ### Retry settings
131
+
132
+ | Setting | Type | Default | Description |
133
+ |---|---|---|---|
134
+ | `LAMBDA_TASKS_MAX_RETRIES` | `int` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised (default is 60 × 24 × 2). |
135
+
136
+ ### Singleton settings
137
+
138
+ | Setting | Type | Default | Description |
139
+ |---|---|---|---|
140
+ | `LAMBDA_TASKS_SINGLETON_CACHE` | `str` | `"default"` | Django cache backend used for singleton task locks. |
141
+
142
+ ### Environment and secrets
143
+
144
+ | Setting | Type | Description |
145
+ |---|---|---|
146
+ | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at Lambda cold start. |
147
+ | `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. |
148
+
149
+ 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.
150
+
130
151
  ### Eager execution (development / testing)
131
152
 
132
153
  | Setting | Type | Default | Description |
133
154
  |---|---|---|---|
134
155
  | `LAMBDA_TASKS_EAGER` | `bool` | `False` | When `True`, tasks run synchronously in-process instead of being sent to SQS. |
135
- | `LAMBDA_TASKS_MAX_RETRIES` | `int` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised. |
136
- | `LAMBDA_TASKS_SINGLETON_CACHE` | `str` | `"default"` | Django cache backend used for singleton task locks. |
137
156
 
138
157
  ```python
139
158
  # settings/local.py
@@ -142,7 +161,7 @@ LAMBDA_TASKS_EAGER = True
142
161
 
143
162
  With eager mode enabled, `.execute_on_commit()` executes the task immediately without touching SQS. Useful for local development and test suites where you don't want to mock AWS infrastructure.
144
163
 
145
- > **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext` is never enteredthe task runs without any time limit. This is intentional: `SIGALRM`-based timeouts require a Lambda/Unix worker process, not a Django dev server thread.
164
+ > **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext` becomes a no-opit checks `LAMBDA_TASKS_EAGER` internally and skips `SIGALRM` setup. This is intentional: `SIGALRM`-based timeouts require a Lambda/Unix worker process, not a Django dev server thread.
146
165
 
147
166
  ---
148
167
 
@@ -150,6 +169,7 @@ With eager mode enabled, `.execute_on_commit()` executes the task immediately wi
150
169
 
151
170
  ```python
152
171
  @lambda_task(
172
+ delay=0, # seconds — SQS DelaySeconds before message becomes visible
153
173
  soft_timeout=60, # seconds — overrides global default for this task
154
174
  hard_timeout=90, # seconds — overrides global default for this task
155
175
  queue="default", # named queue from LAMBDA_TASKS_QUEUES
@@ -164,6 +184,7 @@ def my_task(*, arg: str) -> None:
164
184
 
165
185
  | Parameter | Type | Default | Description |
166
186
  |---|---|---|---|
187
+ | `delay` | `int` | `0` | Seconds to delay the SQS message before it becomes visible to consumers (max 900). |
167
188
  | `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
168
189
  | `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
169
190
  | `queue` | `str` | `"default"` | Named queue to route this task to. |
@@ -190,7 +211,7 @@ payload = send_welcome_email.serialize(user_id=42, template="welcome")
190
211
  # }
191
212
  ```
192
213
 
193
- The returned dict matches the `SQSLambdaTask` schema. To reconstruct and enqueue it later:
214
+ The returned dict matches the `SQSLambdaTask` schema (`message`, `delay`, `queue`). The `delay` value comes from the decorator's `delay` parameter. To reconstruct and enqueue it later:
194
215
 
195
216
  ```python
196
217
  from lambda_tasks.models import SQSLambdaTask
@@ -270,8 +291,10 @@ TaskRecord.objects.get(pk="<uuid>")
270
291
 
271
292
  | Field | Type | Description |
272
293
  |---|---|---|
294
+ | `id` | `UUID` | Primary key — set to the SQS `messageId` for deduplication. |
273
295
  | `task_name` | `str` | Fully-qualified function name (e.g. `myapp.tasks.send_welcome_email`). |
274
296
  | `kwargs` | `dict` | Serialized task arguments. |
297
+ | `n_retries` | `int` | Number of retries attempted so far (starts at 0). |
275
298
  | `status` | `str` | One of `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`. |
276
299
  | `start_time` | `datetime \| None` | When the worker began executing the task. |
277
300
  | `end_time` | `datetime \| None` | When the task completed or failed. |
@@ -297,6 +320,8 @@ def send_welcome_email(*, user_id: int, template: str) -> str:
297
320
 
298
321
  `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.
299
322
 
323
+ The Lambda handler configures the `lambda_tasks` logger hierarchy to `INFO` at cold start so that `task_logger` lines appear in CloudWatch. You can override the level by setting the `LAMBDA_TASKS_LOG_LEVEL` environment variable on your Lambda function (e.g. `DEBUG`, `WARNING`).
324
+
300
325
  Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `message_id` prefix.
301
326
 
302
327
  To filter by invocation in CloudWatch Logs Insights:
@@ -435,17 +460,64 @@ Point your Lambda function's handler at:
435
460
  lambda_tasks.handler.handler
436
461
  ```
437
462
 
463
+ The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
464
+
438
465
  Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
439
466
 
440
467
  | Environment Variable | Required | Description |
441
468
  |---|---|---|
442
469
  | `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
443
- | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
444
- | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
470
+ | `LAMBDA_TASKS_LOG_LEVEL` | No | Log level for the `lambda_tasks` logger hierarchy (default `INFO`). Set to `DEBUG`, `WARNING`, etc. as needed. |
471
+ | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start (runs first). |
472
+ | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
473
+
474
+ ### Loading environment variables from Secrets Manager
475
+
476
+ 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.
477
+
478
+ Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
479
+
480
+ ```
481
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
482
+ ```
483
+
484
+ The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
485
+
486
+ The secret value must be a flat JSON object where all keys and values are strings:
487
+
488
+ ```json
489
+ {
490
+ "DATABASE_URL": "postgres://user:pass@host:5432/db",
491
+ "REDIS_URL": "redis://host:6379/0",
492
+ "DJANGO_SETTINGS_MODULE": "myapp.settings.production"
493
+ }
494
+ ```
445
495
 
446
- ### Resolving Django settings from AWS Secrets Manager
496
+ At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
447
497
 
448
- The Lambda handler supports loading secret values from AWS Secrets Manager into the environment before Django starts. This lets your Django settings file read from `os.environ` as normal while keeping secrets out of plaintext environment variables.
498
+ 1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var if not set, does nothing
499
+ 2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
500
+ 3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
501
+ 4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
502
+ 5. Sets each key-value pair in `os.environ` — existing env vars are overridden
503
+ 6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
504
+
505
+ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
506
+
507
+ #### Validation errors
508
+
509
+ The following raise `ValueError` at cold start, preventing the Lambda container from starting:
510
+
511
+ - Reference format is invalid (wrong segment count, empty version-stage or version-id)
512
+ - Secret value is not valid JSON
513
+ - JSON is not a flat object (contains non-string values) — error message lists the offending keys
514
+ - JSON contains an empty string key
515
+
516
+ AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
517
+
518
+ ### Resolving individual secrets from AWS Secrets Manager
519
+
520
+ The Lambda handler supports loading individual 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.
449
521
 
450
522
  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:
451
523
 
@@ -453,7 +525,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
453
525
  LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
454
526
  ```
455
527
 
456
- At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
528
+ At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
457
529
 
458
530
  1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
459
531
  2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
@@ -493,50 +565,6 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
493
565
  - The named JSON key does not exist in the fetched secret
494
566
  - The secret value is not valid JSON
495
567
 
496
- ### Loading environment variables from Secrets Manager
497
-
498
- The Lambda handler supports loading environment variables from an AWS Secrets Manager secret at cold start. This lets you manage environment configuration centrally in Secrets Manager without baking values into the Lambda deployment package.
499
-
500
- Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
501
-
502
- ```
503
- LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
504
- ```
505
-
506
- The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
507
-
508
- The secret value must be a flat JSON object where all keys and values are strings:
509
-
510
- ```json
511
- {
512
- "DATABASE_URL": "postgres://user:pass@host:5432/db",
513
- "REDIS_URL": "redis://host:6379/0",
514
- "DJANGO_SETTINGS_MODULE": "myapp.settings.production"
515
- }
516
- ```
517
-
518
- At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
519
-
520
- 1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
521
- 2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
522
- 3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
523
- 4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
524
- 5. Sets each key-value pair in `os.environ` — existing env vars are overridden
525
- 6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
526
-
527
- Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
528
-
529
- #### Validation errors
530
-
531
- The following raise `ValueError` at cold start, preventing the Lambda container from starting:
532
-
533
- - Reference format is invalid (wrong segment count, empty version-stage or version-id)
534
- - Secret value is not valid JSON
535
- - JSON is not a flat object (contains non-string values) — error message lists the offending keys
536
- - JSON contains an empty string key
537
-
538
- AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
539
-
540
568
  ---
541
569
 
542
570
  ## Built-in tasks
@@ -569,4 +597,4 @@ You can call a decorated task directly like a normal function — useful in test
569
597
  result = send_welcome_email(user_id=1, template="welcome")
570
598
  ```
571
599
 
572
- This bypasses the queue entirely and runs the function in the current process and transaction.
600
+ This bypasses the queue entirely and runs the function in the current process. No `TaskRecord` is created, no `transaction.atomic()` block is used, and no timeout enforcement applies — it behaves exactly like calling the underlying function directly. Kwargs are still validated against the task's type annotations via Pydantic.
@@ -115,13 +115,32 @@ LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 240
115
115
  LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
116
116
  ```
117
117
 
118
+ ### Retry settings
119
+
120
+ | Setting | Type | Default | Description |
121
+ |---|---|---|---|
122
+ | `LAMBDA_TASKS_MAX_RETRIES` | `int` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised (default is 60 × 24 × 2). |
123
+
124
+ ### Singleton settings
125
+
126
+ | Setting | Type | Default | Description |
127
+ |---|---|---|---|
128
+ | `LAMBDA_TASKS_SINGLETON_CACHE` | `str` | `"default"` | Django cache backend used for singleton task locks. |
129
+
130
+ ### Environment and secrets
131
+
132
+ | Setting | Type | Description |
133
+ |---|---|---|
134
+ | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at Lambda cold start. |
135
+ | `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. |
136
+
137
+ 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.
138
+
118
139
  ### Eager execution (development / testing)
119
140
 
120
141
  | Setting | Type | Default | Description |
121
142
  |---|---|---|---|
122
143
  | `LAMBDA_TASKS_EAGER` | `bool` | `False` | When `True`, tasks run synchronously in-process instead of being sent to SQS. |
123
- | `LAMBDA_TASKS_MAX_RETRIES` | `int` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised. |
124
- | `LAMBDA_TASKS_SINGLETON_CACHE` | `str` | `"default"` | Django cache backend used for singleton task locks. |
125
144
 
126
145
  ```python
127
146
  # settings/local.py
@@ -130,7 +149,7 @@ LAMBDA_TASKS_EAGER = True
130
149
 
131
150
  With eager mode enabled, `.execute_on_commit()` executes the task immediately without touching SQS. Useful for local development and test suites where you don't want to mock AWS infrastructure.
132
151
 
133
- > **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext` is never enteredthe task runs without any time limit. This is intentional: `SIGALRM`-based timeouts require a Lambda/Unix worker process, not a Django dev server thread.
152
+ > **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext` becomes a no-opit checks `LAMBDA_TASKS_EAGER` internally and skips `SIGALRM` setup. This is intentional: `SIGALRM`-based timeouts require a Lambda/Unix worker process, not a Django dev server thread.
134
153
 
135
154
  ---
136
155
 
@@ -138,6 +157,7 @@ With eager mode enabled, `.execute_on_commit()` executes the task immediately wi
138
157
 
139
158
  ```python
140
159
  @lambda_task(
160
+ delay=0, # seconds — SQS DelaySeconds before message becomes visible
141
161
  soft_timeout=60, # seconds — overrides global default for this task
142
162
  hard_timeout=90, # seconds — overrides global default for this task
143
163
  queue="default", # named queue from LAMBDA_TASKS_QUEUES
@@ -152,6 +172,7 @@ def my_task(*, arg: str) -> None:
152
172
 
153
173
  | Parameter | Type | Default | Description |
154
174
  |---|---|---|---|
175
+ | `delay` | `int` | `0` | Seconds to delay the SQS message before it becomes visible to consumers (max 900). |
155
176
  | `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
156
177
  | `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
157
178
  | `queue` | `str` | `"default"` | Named queue to route this task to. |
@@ -178,7 +199,7 @@ payload = send_welcome_email.serialize(user_id=42, template="welcome")
178
199
  # }
179
200
  ```
180
201
 
181
- The returned dict matches the `SQSLambdaTask` schema. To reconstruct and enqueue it later:
202
+ The returned dict matches the `SQSLambdaTask` schema (`message`, `delay`, `queue`). The `delay` value comes from the decorator's `delay` parameter. To reconstruct and enqueue it later:
182
203
 
183
204
  ```python
184
205
  from lambda_tasks.models import SQSLambdaTask
@@ -258,8 +279,10 @@ TaskRecord.objects.get(pk="<uuid>")
258
279
 
259
280
  | Field | Type | Description |
260
281
  |---|---|---|
282
+ | `id` | `UUID` | Primary key — set to the SQS `messageId` for deduplication. |
261
283
  | `task_name` | `str` | Fully-qualified function name (e.g. `myapp.tasks.send_welcome_email`). |
262
284
  | `kwargs` | `dict` | Serialized task arguments. |
285
+ | `n_retries` | `int` | Number of retries attempted so far (starts at 0). |
263
286
  | `status` | `str` | One of `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`. |
264
287
  | `start_time` | `datetime \| None` | When the worker began executing the task. |
265
288
  | `end_time` | `datetime \| None` | When the task completed or failed. |
@@ -285,6 +308,8 @@ def send_welcome_email(*, user_id: int, template: str) -> str:
285
308
 
286
309
  `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.
287
310
 
311
+ The Lambda handler configures the `lambda_tasks` logger hierarchy to `INFO` at cold start so that `task_logger` lines appear in CloudWatch. You can override the level by setting the `LAMBDA_TASKS_LOG_LEVEL` environment variable on your Lambda function (e.g. `DEBUG`, `WARNING`).
312
+
288
313
  Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `message_id` prefix.
289
314
 
290
315
  To filter by invocation in CloudWatch Logs Insights:
@@ -423,17 +448,64 @@ Point your Lambda function's handler at:
423
448
  lambda_tasks.handler.handler
424
449
  ```
425
450
 
451
+ The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
452
+
426
453
  Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
427
454
 
428
455
  | Environment Variable | Required | Description |
429
456
  |---|---|---|
430
457
  | `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
431
- | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
432
- | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
458
+ | `LAMBDA_TASKS_LOG_LEVEL` | No | Log level for the `lambda_tasks` logger hierarchy (default `INFO`). Set to `DEBUG`, `WARNING`, etc. as needed. |
459
+ | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start (runs first). |
460
+ | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
461
+
462
+ ### Loading environment variables from Secrets Manager
463
+
464
+ 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.
465
+
466
+ Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
467
+
468
+ ```
469
+ LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
470
+ ```
471
+
472
+ The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
473
+
474
+ The secret value must be a flat JSON object where all keys and values are strings:
475
+
476
+ ```json
477
+ {
478
+ "DATABASE_URL": "postgres://user:pass@host:5432/db",
479
+ "REDIS_URL": "redis://host:6379/0",
480
+ "DJANGO_SETTINGS_MODULE": "myapp.settings.production"
481
+ }
482
+ ```
433
483
 
434
- ### Resolving Django settings from AWS Secrets Manager
484
+ At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
435
485
 
436
- The Lambda handler supports loading secret values from AWS Secrets Manager into the environment before Django starts. This lets your Django settings file read from `os.environ` as normal while keeping secrets out of plaintext environment variables.
486
+ 1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var if not set, does nothing
487
+ 2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
488
+ 3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
489
+ 4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
490
+ 5. Sets each key-value pair in `os.environ` — existing env vars are overridden
491
+ 6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
492
+
493
+ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
494
+
495
+ #### Validation errors
496
+
497
+ The following raise `ValueError` at cold start, preventing the Lambda container from starting:
498
+
499
+ - Reference format is invalid (wrong segment count, empty version-stage or version-id)
500
+ - Secret value is not valid JSON
501
+ - JSON is not a flat object (contains non-string values) — error message lists the offending keys
502
+ - JSON contains an empty string key
503
+
504
+ AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
505
+
506
+ ### Resolving individual secrets from AWS Secrets Manager
507
+
508
+ The Lambda handler supports loading individual 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.
437
509
 
438
510
  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:
439
511
 
@@ -441,7 +513,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
441
513
  LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
442
514
  ```
443
515
 
444
- At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
516
+ At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
445
517
 
446
518
  1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
447
519
  2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
@@ -481,50 +553,6 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
481
553
  - The named JSON key does not exist in the fetched secret
482
554
  - The secret value is not valid JSON
483
555
 
484
- ### Loading environment variables from Secrets Manager
485
-
486
- The Lambda handler supports loading environment variables from an AWS Secrets Manager secret at cold start. This lets you manage environment configuration centrally in Secrets Manager without baking values into the Lambda deployment package.
487
-
488
- Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
489
-
490
- ```
491
- LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
492
- ```
493
-
494
- The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
495
-
496
- The secret value must be a flat JSON object where all keys and values are strings:
497
-
498
- ```json
499
- {
500
- "DATABASE_URL": "postgres://user:pass@host:5432/db",
501
- "REDIS_URL": "redis://host:6379/0",
502
- "DJANGO_SETTINGS_MODULE": "myapp.settings.production"
503
- }
504
- ```
505
-
506
- At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
507
-
508
- 1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
509
- 2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
510
- 3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
511
- 4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
512
- 5. Sets each key-value pair in `os.environ` — existing env vars are overridden
513
- 6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
514
-
515
- Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
516
-
517
- #### Validation errors
518
-
519
- The following raise `ValueError` at cold start, preventing the Lambda container from starting:
520
-
521
- - Reference format is invalid (wrong segment count, empty version-stage or version-id)
522
- - Secret value is not valid JSON
523
- - JSON is not a flat object (contains non-string values) — error message lists the offending keys
524
- - JSON contains an empty string key
525
-
526
- AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
527
-
528
556
  ---
529
557
 
530
558
  ## Built-in tasks
@@ -557,4 +585,4 @@ You can call a decorated task directly like a normal function — useful in test
557
585
  result = send_welcome_email(user_id=1, template="welcome")
558
586
  ```
559
587
 
560
- This bypasses the queue entirely and runs the function in the current process and transaction.
588
+ This bypasses the queue entirely and runs the function in the current process. No `TaskRecord` is created, no `transaction.atomic()` block is used, and no timeout enforcement applies — it behaves exactly like calling the underlying function directly. Kwargs are still validated against the task's type annotations via Pydantic.
@@ -13,9 +13,10 @@ Required value format::
13
13
  That is: ``<arn>:<version-stage>:<version-id>`` (9 colon-separated segments).
14
14
  The ARN is 7 segments, plus version-stage and version-id.
15
15
 
16
- This runs at Lambda cold start — before ``resolve_secrets_into_env()`` and
17
- before ``django.setup()`` — so that environment variables loaded from the
18
- secret are available to both the secret loader and Django configuration.
16
+ This is called by the handler on the first invocation (cold start) — before
17
+ ``resolve_secrets_into_env()`` and before ``django.setup()`` — so that
18
+ environment variables loaded from the secret are available to both the
19
+ secret loader and Django configuration.
19
20
 
20
21
  The result is cached at module level via a ``_loaded`` sentinel so that
21
22
  subsequent calls (warm invocations) are free no-ops.
@@ -0,0 +1,99 @@
1
+ """
2
+ AWS Lambda handler for lambda_tasks.
3
+
4
+ Processes a batch of SQS records using partial-batch failure reporting.
5
+ Each record is processed independently — a failure in one record does not
6
+ prevent processing of other records.
7
+
8
+ Cold-start initialisation (environment loading, secret resolution, Django
9
+ setup) runs inside the handler on the first invocation rather than at module
10
+ import time. This keeps the Lambda init phase fast and avoids init-duration
11
+ timeouts. The sequence is guarded by a module-level sentinel so subsequent
12
+ warm invocations skip it.
13
+ """
14
+
15
+ import logging
16
+ import os
17
+
18
+ import django
19
+ from django.apps import apps
20
+
21
+ from lambda_tasks.environment_loader import resolve_environment
22
+ from lambda_tasks.secret_loader import resolve_secrets_into_env
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _cold_start_done: bool = False
27
+
28
+
29
+ def _perform_cold_start() -> None:
30
+ """Run one-time initialisation: env loading, secrets, Django setup.
31
+
32
+ Both loaders are idempotent and run unconditionally — the environment
33
+ secret may provide DJANGO_SETTINGS_MODULE, and individual secrets may
34
+ depend on environment-loaded vars.
35
+ """
36
+ global _cold_start_done
37
+
38
+ if _cold_start_done:
39
+ return
40
+
41
+ resolve_environment()
42
+ resolve_secrets_into_env()
43
+
44
+ if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
45
+ django.setup()
46
+
47
+ _configure_logging()
48
+
49
+ _cold_start_done = True
50
+
51
+
52
+ def _configure_logging() -> None:
53
+ """Ensure the lambda_tasks logger hierarchy emits at INFO so task log lines
54
+ appear in CloudWatch.
55
+
56
+ The AWS Lambda runtime pre-configures the root logger, but child loggers
57
+ default to WARNING unless explicitly configured. If Django's LOGGING
58
+ dictConfig has already set a level on the ``lambda_tasks`` logger (i.e. the
59
+ user explicitly configured it), we leave it alone. Otherwise we default to
60
+ INFO (or the value of the LAMBDA_TASKS_LOG_LEVEL env var).
61
+ """
62
+ lambda_tasks_logger = logging.getLogger("lambda_tasks")
63
+
64
+ # level == NOTSET means nobody (neither dictConfig nor user code) has
65
+ # explicitly configured this logger — safe to apply our default.
66
+ if lambda_tasks_logger.level != logging.NOTSET:
67
+ return
68
+
69
+ log_level_name = os.environ.get("LAMBDA_TASKS_LOG_LEVEL", "INFO").upper()
70
+ log_level = getattr(logging, log_level_name, logging.INFO)
71
+ lambda_tasks_logger.setLevel(log_level)
72
+
73
+
74
+ def handler(event: dict, context: object) -> dict:
75
+ """AWS Lambda entry point. Processes a batch of SQS records.
76
+
77
+ Returns a partial-batch failure report so AWS only re-drives failed records.
78
+ Signature is fixed by AWS and uses two args only.
79
+ """
80
+ _perform_cold_start()
81
+
82
+ # Local import due to AppRegistryNotReady
83
+ from lambda_tasks.models import SQSLambdaTaskMessage
84
+
85
+ batch_item_failures: list[dict] = []
86
+
87
+ for record in event["Records"]:
88
+ try:
89
+ SQSLambdaTaskMessage.model_validate_json(
90
+ record["body"]
91
+ ).execute_immediately(message_id=record["messageId"])
92
+ except Exception:
93
+ logger.error(
94
+ "Failed to process SQS record",
95
+ exc_info=True,
96
+ )
97
+ batch_item_failures.append({"itemIdentifier": record["messageId"]})
98
+
99
+ return {"batchItemFailures": batch_item_failures}
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.2.1"
10
+ version = "0.2.3"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"