django-lambda-tasks 0.6.0__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/steering/product.md +13 -14
  2. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/steering/structure.md +6 -6
  3. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/PKG-INFO +79 -92
  4. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/README.md +78 -91
  5. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_project/settings.py +3 -0
  6. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/admin.py +0 -8
  7. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/decorators.py +23 -124
  8. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/models.py +28 -19
  9. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/settings.py +52 -33
  10. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/tasks.py +12 -8
  11. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/pyproject.toml +1 -1
  12. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/settings.py +4 -0
  13. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_admin.py +0 -4
  14. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_batch.py +35 -29
  15. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_batch_worker.py +5 -3
  16. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_decorator.py +46 -70
  17. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_local_executor.py +1 -1
  18. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_models.py +23 -19
  19. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_settings.py +74 -48
  20. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_timeout_validation.py +68 -9
  21. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_timeouts.py +1 -0
  22. django_lambda_tasks-0.6.0/tests/test_batch_decorators.py +0 -158
  23. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.github/workflows/ci.yml +0 -0
  24. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.github/workflows/release.yml +0 -0
  25. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.gitignore +0 -0
  26. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/async-local-execution/.config.kiro +0 -0
  27. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/async-local-execution/design.md +0 -0
  28. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/async-local-execution/requirements.md +0 -0
  29. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/async-local-execution/tasks.md +0 -0
  30. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  31. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  32. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  33. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  34. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  35. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  36. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  37. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  38. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  39. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  40. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  41. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  42. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  43. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  44. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  45. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  46. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/retry-delay/.config.kiro +0 -0
  47. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/retry-delay/design.md +0 -0
  48. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/retry-delay/requirements.md +0 -0
  49. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/retry-delay/tasks.md +0 -0
  50. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  51. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks/design.md +0 -0
  52. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  53. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  54. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  55. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  56. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  57. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  58. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/singleton-task/.config.kiro +0 -0
  59. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/singleton-task/design.md +0 -0
  60. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/singleton-task/requirements.md +0 -0
  61. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/singleton-task/tasks.md +0 -0
  62. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  63. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  64. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  65. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  66. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/task-retry/.config.kiro +0 -0
  67. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/task-retry/design.md +0 -0
  68. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/task-retry/requirements.md +0 -0
  69. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/specs/task-retry/tasks.md +0 -0
  70. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.kiro/steering/tech.md +0 -0
  71. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.pre-commit-config.yaml +0 -0
  72. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/.vscode/settings.json +0 -0
  73. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/README.md +0 -0
  74. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_app/__init__.py +0 -0
  75. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_app/apps.py +0 -0
  76. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_app/tasks.py +0 -0
  77. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_app/urls.py +0 -0
  78. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_app/views.py +0 -0
  79. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_project/__init__.py +0 -0
  80. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_project/urls.py +0 -0
  81. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/example_project/wsgi.py +0 -0
  82. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/example/manage.py +0 -0
  83. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/__init__.py +0 -0
  84. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/apps.py +0 -0
  85. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/environment_loader.py +0 -0
  86. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/handler.py +0 -0
  87. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/local_executor.py +0 -0
  88. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/logging.py +0 -0
  89. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/migrations/0001_initial.py +0 -0
  90. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
  91. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/migrations/__init__.py +0 -0
  92. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/secret_loader.py +0 -0
  93. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/lambda_tasks/timeouts.py +0 -0
  94. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/conftest.py +0 -0
  95. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_decorators.py +0 -0
  96. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_deferred_enqueue.py +0 -0
  97. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_environment_loader.py +0 -0
  98. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_handler.py +0 -0
  99. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_kwargs_only.py +0 -0
  100. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_logging.py +0 -0
  101. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_memory_limit.py +0 -0
  102. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_secret_loader.py +0 -0
  103. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_serializer.py +0 -0
  104. {django_lambda_tasks-0.6.0 → django_lambda_tasks-0.6.1}/tests/test_tasks.py +0 -0
@@ -14,10 +14,10 @@ A Django library for offloading tasks to AWS Lambda or AWS Batch outside of the
14
14
  View → @lambda_task.execute_on_commit() → SQS → Lambda handler → SQSLambdaTaskMessage.execute_immediately() → TaskRecord
15
15
  ```
16
16
 
17
- ### Batch tasks
17
+ ### Batch tasks (via queue config)
18
18
 
19
19
  ```
20
- View → @batch_task.execute_on_commit() → SQS → Lambda handler → submit_batch_job → batch.submit_job()
20
+ View → @lambda_task(queue="heavy").execute_on_commit() → SQS → Lambda handler → submit_batch_job → batch.submit_job()
21
21
  → Container → python -m lambda_tasks.handler → handler() → execute_immediately() → TaskRecord
22
22
  ```
23
23
 
@@ -27,25 +27,25 @@ Container (retry_on exception) → execute_on_commit() → SQS → Lambda → su
27
27
  ```
28
28
 
29
29
  Key modules:
30
- - `decorators.py` — `BaseTaskWrapper`, `LambdaTaskWrapper`, `BatchTaskWrapper`, `@lambda_task`, `@batch_task`
30
+ - `decorators.py` — `LambdaTaskWrapper` and `@lambda_task` decorator factory
31
31
  - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish, Batch submit, or local pool submit)
32
32
  - `local_executor.py` — `ProcessPoolExecutor`-based async local execution for development
33
33
  - `handler.py` — AWS Lambda entry point + AWS Batch container entry point (`main()`)
34
34
  - `logging.py` — `task_logger` for invocation-scoped log output
35
- - `settings.py` — `TaskBackend` enum, lazy `LambdaTasksSettings` reading from Django settings
35
+ - `settings.py` — lazy `LambdaTasksSettings` reading from Django settings, queue type helpers
36
36
 
37
37
  ## Task Definition
38
38
 
39
39
  ```python
40
- # Lambda task — max timeout 900s
40
+ # Lambda task — max timeout determined by queue type (900s for SQS, 3600s for Batch)
41
41
  @lambda_task(retry_delay=30, soft_timeout=60, hard_timeout=120, queue="default",
42
42
  retry_on=(TransientError,),
43
43
  singleton=True, retry_singleton=True)
44
44
  def my_task(*, user_id: int, action: str) -> None:
45
45
  ...
46
46
 
47
- # Batch task — max timeout 3600s, for long-running or storage-heavy workloads
48
- @batch_task(soft_timeout=1800, hard_timeout=3500, queue="default",
47
+ # Batch task — same decorator, just routed to a Batch queue
48
+ @lambda_task(queue="heavy", soft_timeout=1800, hard_timeout=3500,
49
49
  retry_on=(TransientError,))
50
50
  def heavy_task(*, file_id: int) -> None:
51
51
  ...
@@ -55,7 +55,7 @@ def heavy_task(*, file_id: int) -> None:
55
55
  - Task files should be named `tasks.py` within the Django app
56
56
  - Direct call `my_task(user_id=1, action="x")` runs synchronously
57
57
  - `my_task.execute_on_commit(user_id=1, action="x")` enqueues after transaction commit
58
- - Both decorators share `BaseTaskWrapper` — same validation, kwargs building, retry, and singleton logic
58
+ - The execution backend (Lambda vs Batch) is determined by the queue configuration, not the decorator
59
59
 
60
60
  ## Enqueuing
61
61
 
@@ -130,7 +130,7 @@ class SQSLambdaTaskMessage(BaseModel):
130
130
  `SQSLambdaTaskMessage.execute_immediately(*, message_id: str)` in `models.py`:
131
131
  1. Checks for an existing `TaskRecord` with the same `pk` (`message_id`) via `get_or_create`
132
132
  2. If a record already exists (any status), logs and returns immediately — duplicate deliveries are silently skipped
133
- 3. Resolves timeouts: decorator default → settings defaults (soft=270s, hard=300s)
133
+ 3. Resolves timeouts: decorator default → settings defaults (soft=270s, hard=300s), validated against queue max (900 for SQS, 3600 for Batch)
134
134
  4. If `wrapper.singleton` is `True`, acquires a Redis lock via `caches[SINGLETON_CACHE].lock(lock_key)` wrapping the atomic block; if `False`, no lock is acquired
135
135
  5. Runs task inside `transaction.atomic()` + `TimeoutContext`
136
136
  6. On success: updates record to `SUCCESS` with result and `end_time`
@@ -196,7 +196,7 @@ Behaviour:
196
196
 
197
197
  - `TaskRecord` — Django ORM model; persists task execution state with statuses `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
198
198
  - `SQSLambdaTaskMessage` — Pydantic model; the SQS message schema; `execute_immediately()` runs the task inside `transaction.atomic()` + `TimeoutContext`
199
- - `SQSLambdaTask` — Pydantic model; holds a `SQSLambdaTaskMessage` plus routing fields (`delay`, `queue`); `_execute()` publishes to SQS or executes eagerly; `execute_on_commit()` registers `_execute` with `transaction.on_commit`
199
+ - `SQSLambdaTask` — Pydantic model; holds a `SQSLambdaTaskMessage` plus routing fields (`delay`, `queue`); `_execute()` inspects queue config to publish to SQS, submit to Batch via `submit_batch_job`, or execute eagerly/locally; `execute_on_commit()` registers `_execute` with `transaction.on_commit`
200
200
 
201
201
  ## TaskRecord Model
202
202
 
@@ -210,8 +210,7 @@ Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
210
210
 
211
211
  | Setting | Default | Description |
212
212
  |---|---|---|
213
- | `LAMBDA_TASKS_QUEUES` | — | Dict of `{queue_name: sqs_url}`, must include `"default"` |
214
- | `LAMBDA_TASKS_BATCH_QUEUES` | `{}` | Dict of `{queue_name: {"job_queue": ..., "job_definition": ...}}`, must include `"default"` when set |
213
+ | `LAMBDA_TASKS_QUEUES` | — | Dict of `{queue_name: queue_config}`. Each value is a dict with either `queue_url` (SQS) or `job_queue_arn`+`job_definition_arn` (Batch). Must include `"default"` which must be SQS. |
215
214
  | `LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` | `270` | Soft timeout in seconds |
216
215
  | `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `300` | Hard timeout in seconds |
217
216
  | `LAMBDA_TASKS_EAGER` | `False` | Run tasks synchronously in-process (no SQS) |
@@ -219,7 +218,7 @@ Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
219
218
  | `LAMBDA_TASKS_MAX_RETRIES` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised (60 × 24 × 2) |
220
219
  | `LAMBDA_TASKS_SINGLETON_CACHE` | `"default"` | Django cache backend used for singleton task locks |
221
220
 
222
- `LAMBDA_TASKS_QUEUES` must be set and include a `"default"` key. Both timeout values must be greater than zero and at most `900` seconds. `soft_timeout` must always be strictly less than `hard_timeout`.
221
+ `LAMBDA_TASKS_QUEUES` must be set and include a `"default"` key. The `"default"` queue must be an SQS queue. Both timeout values must be greater than zero and `soft_timeout` must always be strictly less than `hard_timeout`. The maximum allowed timeout depends on the queue type: 900 for SQS, 3600 for Batch.
223
222
 
224
223
  ## Eager Mode
225
224
 
@@ -289,7 +288,7 @@ def my_task(*, user_id: int) -> None:
289
288
  `lambda_tasks.tasks` provides maintenance tasks that ship with the library:
290
289
 
291
290
  - `cleanup_task_records(*, retention_days: int = 7) -> int` — deletes `TaskRecord` rows whose `start_time` is strictly older than `retention_days`. Returns the number of deleted rows. Decorated with `@lambda_task` so it can be enqueued via `cleanup_task_records.execute_on_commit()` or called directly. Users are responsible for scheduling (e.g. EventBridge rule, cron, management command).
292
- - `submit_batch_job(*, message_json: str, batch_queue: str) -> str` — submits a batch task to AWS Batch. Reads `job_queue` and `job_definition` from `LAMBDA_TASKS_BATCH_QUEUES[batch_queue]`, sanitizes the task name for use as `jobName`, and calls `batch.submit_job()` with the message passed as a `LAMBDA_TASKS_MESSAGE` environment variable override. Returns the Batch job ID. This task is enqueued automatically by `SQSLambdaTask._execute()` when `backend == TaskBackend.BATCH` — users do not call it directly.
291
+ - `submit_batch_job(*, message_json: str, batch_queue: str) -> str` — submits a batch task to AWS Batch. Reads `job_queue_arn` and `job_definition_arn` from `LAMBDA_TASKS_QUEUES[batch_queue]`, sanitizes the task name for use as `jobName`, and calls `batch.submit_job()` with the message passed as a `LAMBDA_TASKS_MESSAGE` environment variable override. Returns the Batch job ID. This task is enqueued automatically by `SQSLambdaTask._execute()` when the queue is a Batch queue — users do not call it directly.
293
292
 
294
293
  ## Conventions
295
294
 
@@ -12,11 +12,11 @@ django-lambda-tasks/
12
12
  │ ├── __init__.py
13
13
  │ ├── admin.py # Django admin for TaskRecord
14
14
  │ ├── apps.py # Django AppConfig
15
- │ ├── decorators.py # BaseTaskWrapper, LambdaTaskWrapper, BatchTaskWrapper, @lambda_task, @batch_task
15
+ │ ├── decorators.py # LambdaTaskWrapper, @lambda_task
16
16
  │ ├── handler.py # AWS Lambda entry point + AWS Batch container entry point (main)
17
17
  │ ├── logging.py # task_logger — invocation-scoped LoggerAdapter
18
18
  │ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
19
- │ ├── settings.py # LambdaTasksSettings, TaskBackend enum, constants
19
+ │ ├── settings.py # LambdaTasksSettings, constants, queue type helpers
20
20
  │ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
21
21
  │ ├── environment_loader.py # Loads env vars from Secrets Manager at cold start
22
22
  │ ├── local_executor.py # ProcessPoolExecutor for async local task execution
@@ -27,7 +27,7 @@ django-lambda-tasks/
27
27
  │ ├── conftest.py
28
28
  │ ├── settings.py # Django settings for test environment
29
29
  │ └── test_*.py # One test file per module
30
- ├── example/ # Runnable Django project (EAGER mode, no AWS needed)
30
+ ├── example/ # Runnable Django project (LOCAL_WORKERS mode, no AWS needed)
31
31
  │ ├── example_app/ # Sample app with a task definition and trigger view
32
32
  │ ├── example_project/ # Django settings, URLs, WSGI
33
33
  │ ├── manage.py
@@ -39,14 +39,14 @@ django-lambda-tasks/
39
39
 
40
40
  ## Module Responsibilities
41
41
 
42
- - `decorators.py` — defines `BaseTaskWrapper` (shared validation, kwargs building, execution logic), `LambdaTaskWrapper` and `BatchTaskWrapper` (subclasses that set `_max_timeout` and `_backend`), plus `@lambda_task` and `@batch_task` decorator factories; enforces kwargs-only at decoration time
43
- - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing + `backend` field; `_execute()` publishes to SQS, submits to Batch via `submit_batch_job`, executes eagerly, or submits to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
42
+ - `decorators.py` — defines `LambdaTaskWrapper` and `@lambda_task` decorator factory; enforces kwargs-only at decoration time; validates timeouts (> 0, soft < hard) at decoration time; upper-bound timeout validation deferred to execution time based on queue type
43
+ - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` inspects queue config to publish to SQS, submit to Batch via `submit_batch_job`, execute eagerly, or submit to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
44
44
  - `local_executor.py` — `ProcessPoolExecutor`-based async local execution; `get_pool()` lazily creates a module-level pool; `submit_task()` fire-and-forget submission; `_execute_in_worker()` deserializes and runs the task in a child process; `_pool_initializer()` calls `django.setup()` per worker (and sets `SIGINT` to `SIG_IGN` so workers ignore Ctrl+C); `_install_shutdown_handlers()` installs main-thread `SIGINT`/`SIGTERM` handlers that release the pool before chaining to the previous handler
45
45
  - `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`. Also provides `main()` as the AWS Batch container entry point (`python -m lambda_tasks.handler`) — reads `LAMBDA_TASKS_MESSAGE` env var, constructs a synthetic SQS event, and delegates to `handler()`
46
46
  - `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
47
47
  - `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
48
48
  - `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
49
- - `settings.py` — `TaskBackend` StrEnum (`LAMBDA`, `BATCH`); constants (`MAX_DELAY`, `MAX_TIMEOUT`, `MAX_BATCH_TIMEOUT`); `LambdaTasksSettings` instantiated fresh per use (reads live Django settings including `BATCH_QUEUES`)
49
+ - `settings.py` — constants (`MAX_DELAY`, `MAX_TIMEOUT`, `MAX_BATCH_TIMEOUT`); `_is_sqs_queue()` and `_is_batch_queue()` helpers; `LambdaTasksSettings` instantiated fresh per use (reads live Django settings); `queue_max_timeout()` method returns 900 or 3600 based on queue type
50
50
  - `admin.py` — Django admin registration for `TaskRecord`
51
51
  - `apps.py` — Django `AppConfig`; `ready()` installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
52
52
  - `tasks.py` — built-in tasks; `cleanup_task_records` deletes old `TaskRecord` rows; `submit_batch_job` receives a serialized task message and submits it to AWS Batch via `batch.submit_job()`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -12,7 +12,7 @@ Description-Content-Type: text/markdown
12
12
 
13
13
  # Django Lambda Tasks
14
14
 
15
- 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.
15
+ A Django library for offloading work to AWS Lambda or AWS Batch outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler or Batch container. Task results, status, and metadata are persisted in the Django database.
16
16
 
17
17
  > **Platform note:** Unix-based systems only (Linux, macOS). Timeout enforcement relies on `SIGALRM`.
18
18
 
@@ -86,7 +86,7 @@ See [AWS Lambda and SQS setup](#aws-lambda-and-sqs-setup) for queue and Lambda c
86
86
 
87
87
  ## Example app
88
88
 
89
- A runnable Django project is included in the [`example/`](example/) directory. It uses `LAMBDA_TASKS_EAGER = True` so no AWS infrastructure is needed.
89
+ A runnable Django project is included in the [`example/`](example/) directory. It uses `LAMBDA_TASKS_LOCAL_WORKERS = 2` so no AWS infrastructure is needed.
90
90
 
91
91
  ```bash
92
92
  cd example
@@ -109,24 +109,25 @@ All settings are read from your Django settings module and prefixed with `LAMBDA
109
109
 
110
110
  | Setting | Type | Description |
111
111
  |---|---|---|
112
- | `LAMBDA_TASKS_QUEUES` | `dict[str, str]` | Map of queue name → SQS queue URL. Must include a `"default"` key. |
113
- | `LAMBDA_TASKS_BATCH_QUEUES` | `dict[str, dict]` | Map of queue name → `{"job_queue": ..., "job_definition": ...}`. Must include a `"default"` key when set. Default `{}`. |
112
+ | `LAMBDA_TASKS_QUEUES` | `dict[str, dict]` | Map of queue name → queue configuration. Must include a `"default"` key. The `"default"` queue must be an SQS queue. |
113
+
114
+ Each queue value is a dict. The queue type is determined by the keys present:
115
+
116
+ - **SQS queue:** `{"queue_url": "https://sqs.../queue-name"}` — tasks execute on Lambda (max timeout 900s)
117
+ - **Batch queue:** `{"job_queue_arn": "arn:...", "job_definition_arn": "arn:..."}` — tasks execute on AWS Batch (max timeout 3600s)
114
118
 
115
119
  ```python
116
120
  LAMBDA_TASKS_QUEUES = {
117
- "default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
118
- "high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
119
- }
120
-
121
- LAMBDA_TASKS_BATCH_QUEUES = {
122
- "default": {
123
- "job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
124
- "job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
121
+ "default": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/default"},
122
+ "high_memory": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory"},
123
+ "heavy": {
124
+ "job_queue_arn": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
125
+ "job_definition_arn": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
125
126
  },
126
127
  }
127
128
  ```
128
129
 
129
- `LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use.
130
+ `LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use. The `"default"` queue must be an SQS queue.
130
131
 
131
132
  ### Timeout settings
132
133
 
@@ -135,7 +136,7 @@ LAMBDA_TASKS_BATCH_QUEUES = {
135
136
  | `LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` | `int` | `270` | Seconds before `SoftTimeLimitExceeded` is raised inside the task. |
136
137
  | `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `int` | `300` | Seconds before the task is forcibly terminated. |
137
138
 
138
- Both values must be between `1` and `900` seconds (the AWS Lambda maximum runtime is 15 minutes). `soft_timeout` must always be strictly less than `hard_timeout`.
139
+ Both values must be greater than zero and `soft_timeout` must always be strictly less than `hard_timeout`. The maximum allowed value depends on the queue type: 900 seconds for SQS queues (Lambda's 15-minute limit), 3600 seconds for Batch queues (1 hour). Upper bound validation happens at execution time based on the task's queue.
139
140
 
140
141
  ```python
141
142
  LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 240
@@ -225,9 +226,9 @@ def my_task(*, arg: str) -> None:
225
226
 
226
227
  | Parameter | Type | Default | Description |
227
228
  |---|---|---|---|
228
- | `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
229
- | `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
230
- | `queue` | `str` | `"default"` | Named queue to route this task to. |
229
+ | `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds. Max depends on queue type (900 for SQS, 3600 for Batch). |
230
+ | `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds. Max depends on queue type (900 for SQS, 3600 for Batch). |
231
+ | `queue` | `str` | `"default"` | Named queue to route this task to. Determines both execution backend and timeout limits. |
231
232
  | `retry_on` | `tuple[type[BaseException], ...]` | `()` | Exception types that trigger an automatic retry (see [Automatic retries](#automatic-retries)). |
232
233
  | `retry_delay` | `int` | `0` | Base delay in seconds when enqueuing a retry. Jitter (1–5s) is always added; result capped at 900. Requires `retry_on` to be non-empty. |
233
234
  | `singleton` | `bool` | `False` | Prevent concurrent execution via a Redis lock (see [Singleton tasks](#singleton-tasks)). |
@@ -306,26 +307,46 @@ Timeout resolution order (first non-`None` value wins):
306
307
  1. Per-task decorator default (`soft_timeout` / `hard_timeout` on `@lambda_task`)
307
308
  2. Global settings (`LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` / `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT`)
308
309
 
310
+ The maximum allowed timeout is determined by the queue type at execution time: 900 seconds for SQS queues, 3600 seconds for Batch queues.
311
+
309
312
  ---
310
313
 
311
314
  ## Named queues
312
315
 
313
- Route tasks to queues backed by Lambda functions with different hardware profiles (e.g. more memory or CPU):
316
+ Route tasks to different execution backends by assigning them to named queues:
314
317
 
315
318
  ```python
316
319
  # settings.py
317
320
  LAMBDA_TASKS_QUEUES = {
318
- "default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
319
- "high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
321
+ "default": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/default"},
322
+ "high_memory": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory"},
323
+ "heavy": {
324
+ "job_queue_arn": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
325
+ "job_definition_arn": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
326
+ },
320
327
  }
321
328
  ```
322
329
 
323
330
  ```python
331
+ # Runs on Lambda with more memory
324
332
  @lambda_task(queue="high_memory")
325
- def process_large_file(*, file_id: int) -> None:
333
+ def process_images(*, batch_id: int) -> None:
334
+ ...
335
+
336
+ # Runs on AWS Batch (up to 1 hour, large disk)
337
+ @lambda_task(queue="heavy", soft_timeout=1800, hard_timeout=3500)
338
+ def process_large_file(*, file_id: int) -> str:
326
339
  ...
327
340
  ```
328
341
 
342
+ Tasks routed to a Batch queue are automatically submitted to AWS Batch via the built-in `submit_batch_job` task. The flow is:
343
+
344
+ 1. `execute_on_commit()` enqueues a message to the default SQS queue
345
+ 2. The Lambda handler picks up the message and runs `submit_batch_job`
346
+ 3. `submit_batch_job` calls `batch.submit_job()` with the task message as a `LAMBDA_TASKS_MESSAGE` environment variable override
347
+ 4. The Batch container runs `python -m lambda_tasks.handler`, which reads the env var, performs cold-start init, and calls `execute_immediately()`
348
+ 5. The task runs with full timeout enforcement (`SIGALRM`), retry support, and `TaskRecord` tracking
349
+
329
350
  ---
330
351
 
331
352
  ## Task results
@@ -461,10 +482,11 @@ Each task runs inside a `django.db.transaction.atomic` block. If the task raises
461
482
  |---|---|---|
462
483
  | Task function has positional parameters | `TypeError` | At decoration time |
463
484
  | `soft_timeout >= hard_timeout` | `ValueError` | At decoration time |
464
- | Timeout value exceeds 900 seconds | `ValueError` | At decoration time or settings load |
485
+ | Timeout value exceeds queue max (900 or 3600) | `ValueError` | At execution time (timeout resolution) |
465
486
  | Unknown queue name | `ImproperlyConfigured` | At `.execute_on_commit()` |
466
487
  | `LAMBDA_TASKS_QUEUES` not set | `ImproperlyConfigured` | On first settings access |
467
488
  | `LAMBDA_TASKS_QUEUES` missing `"default"` | `ImproperlyConfigured` | On first settings access |
489
+ | `"default"` queue is not SQS | `ImproperlyConfigured` | On first settings access |
468
490
  | SQS `send_message` failure | boto3 exception (propagated) | At `.execute_on_commit()` |
469
491
 
470
492
  ---
@@ -473,7 +495,7 @@ Each task runs inside a `django.db.transaction.atomic` block. If the task raises
473
495
 
474
496
  ### SQS queue configuration
475
497
 
476
- Each queue in `LAMBDA_TASKS_QUEUES` should be a standard SQS queue (not FIFO) with the following recommended settings:
498
+ Each SQS queue in `LAMBDA_TASKS_QUEUES` should be a standard SQS queue (not FIFO) with the following recommended settings:
477
499
 
478
500
  - **Visibility timeout** — set this higher than your Lambda function's `hard_timeout`. If the Lambda times out before SQS receives a deletion confirmation, the message becomes visible again and will be redelivered.
479
501
  - **Dead letter queue (DLQ)** — configure a DLQ to capture messages that cannot be processed. Without one, messages that exhaust retries are silently deleted.
@@ -600,6 +622,39 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
600
622
 
601
623
  ---
602
624
 
625
+ ## AWS Batch setup
626
+
627
+ For tasks that exceed Lambda's 15-minute timeout or 10GB ephemeral storage limit, route them to a Batch queue:
628
+
629
+ ```python
630
+ @lambda_task(queue="heavy", soft_timeout=1800, hard_timeout=3500)
631
+ def process_large_file(*, file_id: int) -> str:
632
+ # long-running work with large disk usage
633
+ return "done"
634
+ ```
635
+
636
+ ### Batch job definition setup
637
+
638
+ Your Batch job definition should:
639
+ - Use the same Docker image as your application
640
+ - Set `DJANGO_SETTINGS_MODULE` as an environment variable
641
+ - Configure secrets via the job definition's `secrets` field or via `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` / `LAMBDA_TASKS_SECRET_*` env vars (same mechanism as Lambda)
642
+ - Set the default command to `["python", "-m", "lambda_tasks.handler"]`
643
+
644
+ ### Retries
645
+
646
+ Batch tasks support `retry_on` — when a retryable exception is raised, the retry goes back through SQS → Lambda → `submit_batch_job` → new Batch job. The same `LAMBDA_TASKS_MAX_RETRIES` limit applies.
647
+
648
+ ### Container overrides limit
649
+
650
+ The total `containerOverrides` payload (including JSON formatting) is limited to 8192 characters by AWS Batch. For typical task kwargs (IDs, short strings) this is not a concern.
651
+
652
+ ### Eager and local worker modes
653
+
654
+ Tasks on Batch queues respect `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — in development, they run in-process or in the local process pool just like SQS-backed tasks, without requiring AWS infrastructure.
655
+
656
+ ---
657
+
603
658
  ## Built-in tasks
604
659
 
605
660
  ### `cleanup_task_records`
@@ -631,71 +686,3 @@ result = send_welcome_email(user_id=1, template="welcome")
631
686
  ```
632
687
 
633
688
  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.
634
-
635
- ---
636
-
637
- ## AWS Batch tasks
638
-
639
- For tasks that exceed Lambda's 15-minute timeout or 10GB ephemeral storage limit, use `@batch_task` to route execution to AWS Batch containers.
640
-
641
- ### 1. Define a batch task
642
-
643
- ```python
644
- # myapp/tasks.py
645
- from lambda_tasks.decorators import batch_task
646
-
647
- @batch_task(soft_timeout=1800, hard_timeout=3500)
648
- def process_large_file(*, file_id: int) -> str:
649
- # long-running work with large disk usage
650
- return "done"
651
- ```
652
-
653
- ### 2. Configure AWS Batch queues
654
-
655
- ```python
656
- # settings.py
657
- LAMBDA_TASKS_BATCH_QUEUES = {
658
- "default": {
659
- "job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
660
- "job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
661
- },
662
- }
663
- ```
664
-
665
- ### 3. Enqueue from a view
666
-
667
- ```python
668
- process_large_file.execute_on_commit(file_id=42)
669
- ```
670
-
671
- ### How it works
672
-
673
- 1. `execute_on_commit()` enqueues a message to SQS (same as Lambda tasks)
674
- 2. The Lambda handler picks up the message and runs the built-in `submit_batch_job` task
675
- 3. `submit_batch_job` calls `batch.submit_job()` with the task message as a `LAMBDA_TASKS_MESSAGE` environment variable override
676
- 4. The Batch container runs `python -m lambda_tasks.handler`, which reads the env var, performs cold-start init, and calls `execute_immediately()`
677
- 5. The task runs with full timeout enforcement (`SIGALRM`), retry support, and `TaskRecord` tracking
678
-
679
- ### Batch task timeouts
680
-
681
- Batch tasks support the same two-phase timeout mechanism as Lambda tasks (soft + hard), but with a maximum of **3600 seconds** (1 hour) instead of 900.
682
-
683
- ### Retries
684
-
685
- Batch tasks support `retry_on` — when a retryable exception is raised, the retry goes back through SQS → Lambda → `submit_batch_job` → new Batch job. The same `LAMBDA_TASKS_MAX_RETRIES` limit applies.
686
-
687
- ### Batch job definition setup
688
-
689
- Your Batch job definition should:
690
- - Use the same Docker image as your application
691
- - Set `DJANGO_SETTINGS_MODULE` as an environment variable
692
- - Configure secrets via the job definition's `secrets` field or via `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` / `LAMBDA_TASKS_SECRET_*` env vars (same mechanism as Lambda)
693
- - Set the default command to `["python", "-m", "lambda_tasks.handler"]`
694
-
695
- ### Container overrides limit
696
-
697
- The total `containerOverrides` payload (including JSON formatting) is limited to 8192 characters by AWS Batch. For typical task kwargs (IDs, short strings) this is not a concern.
698
-
699
- ### Eager and local worker modes
700
-
701
- Batch tasks respect `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — in development, they run in-process or in the local process pool just like Lambda tasks, without requiring AWS infrastructure.