django-lambda-tasks 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/steering/product.md +3 -2
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/steering/structure.md +1 -1
- django_lambda_tasks-0.2.1/README.md → django_lambda_tasks-0.2.2/PKG-INFO +16 -2
- django_lambda_tasks-0.2.1/PKG-INFO → django_lambda_tasks-0.2.2/README.md +4 -14
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/environment_loader.py +4 -3
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/handler.py +33 -11
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/pyproject.toml +1 -1
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_environment_loader.py +60 -65
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_handler.py +64 -16
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.gitignore +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/README.md +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/example/manage.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/models.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/settings.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/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
|
|
|
@@ -135,7 +135,8 @@ 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()`
|
|
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
|
|
140
141
|
|
|
141
142
|
## Environment Loader
|
|
@@ -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;
|
|
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
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-lambda-tasks
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Run async tasks in a lambda function
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: awslambdaric
|
|
7
|
+
Requires-Dist: boto3
|
|
8
|
+
Requires-Dist: django
|
|
9
|
+
Requires-Dist: pydantic
|
|
10
|
+
Requires-Dist: redis
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
1
13
|
# Django Lambda Tasks
|
|
2
14
|
|
|
3
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.
|
|
@@ -423,6 +435,8 @@ Point your Lambda function's handler at:
|
|
|
423
435
|
lambda_tasks.handler.handler
|
|
424
436
|
```
|
|
425
437
|
|
|
438
|
+
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.
|
|
439
|
+
|
|
426
440
|
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
441
|
|
|
428
442
|
| Environment Variable | Required | Description |
|
|
@@ -441,7 +455,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
|
|
|
441
455
|
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
442
456
|
```
|
|
443
457
|
|
|
444
|
-
At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
458
|
+
At cold start (on the first handler invocation), before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
445
459
|
|
|
446
460
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
447
461
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
@@ -503,7 +517,7 @@ The secret value must be a flat JSON object where all keys and values are string
|
|
|
503
517
|
}
|
|
504
518
|
```
|
|
505
519
|
|
|
506
|
-
At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
520
|
+
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
507
521
|
|
|
508
522
|
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
509
523
|
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: Run async tasks in a lambda function
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: awslambdaric
|
|
7
|
-
Requires-Dist: boto3
|
|
8
|
-
Requires-Dist: django
|
|
9
|
-
Requires-Dist: pydantic
|
|
10
|
-
Requires-Dist: redis
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
|
|
13
1
|
# Django Lambda Tasks
|
|
14
2
|
|
|
15
3
|
A Django library for offloading work to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler that AWS invokes with SQS message batches. Task results, status, and metadata are persisted in the Django database.
|
|
@@ -435,6 +423,8 @@ Point your Lambda function's handler at:
|
|
|
435
423
|
lambda_tasks.handler.handler
|
|
436
424
|
```
|
|
437
425
|
|
|
426
|
+
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.
|
|
427
|
+
|
|
438
428
|
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
429
|
|
|
440
430
|
| Environment Variable | Required | Description |
|
|
@@ -453,7 +443,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
|
|
|
453
443
|
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
454
444
|
```
|
|
455
445
|
|
|
456
|
-
At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
446
|
+
At cold start (on the first handler invocation), before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
457
447
|
|
|
458
448
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
459
449
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
@@ -515,7 +505,7 @@ The secret value must be a flat JSON object where all keys and values are string
|
|
|
515
505
|
}
|
|
516
506
|
```
|
|
517
507
|
|
|
518
|
-
At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
508
|
+
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
519
509
|
|
|
520
510
|
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
521
511
|
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
@@ -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
|
|
17
|
-
before ``django.setup()`` — so that
|
|
18
|
-
secret are available to both the
|
|
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.
|
|
@@ -4,28 +4,48 @@ AWS Lambda handler for lambda_tasks.
|
|
|
4
4
|
Processes a batch of SQS records using partial-batch failure reporting.
|
|
5
5
|
Each record is processed independently — a failure in one record does not
|
|
6
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.
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
import logging
|
|
10
16
|
import os
|
|
11
17
|
|
|
12
|
-
import django
|
|
13
|
-
from django.apps import apps
|
|
14
|
-
|
|
15
18
|
from lambda_tasks.environment_loader import resolve_environment
|
|
16
19
|
from lambda_tasks.secret_loader import resolve_secrets_into_env
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
resolve_environment()
|
|
22
|
-
resolve_secrets_into_env()
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_cold_start_done: bool = False
|
|
23
24
|
|
|
24
|
-
if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
|
|
25
|
-
django.setup()
|
|
26
25
|
|
|
26
|
+
def _perform_cold_start() -> None:
|
|
27
|
+
"""Run one-time initialisation: env loading, secrets, Django setup.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Both loaders are idempotent and run unconditionally — the environment
|
|
30
|
+
secret may provide DJANGO_SETTINGS_MODULE, and individual secrets may
|
|
31
|
+
depend on environment-loaded vars.
|
|
32
|
+
"""
|
|
33
|
+
global _cold_start_done
|
|
34
|
+
|
|
35
|
+
if _cold_start_done:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
resolve_environment()
|
|
39
|
+
resolve_secrets_into_env()
|
|
40
|
+
|
|
41
|
+
if os.environ.get("DJANGO_SETTINGS_MODULE"):
|
|
42
|
+
import django
|
|
43
|
+
from django.apps import apps
|
|
44
|
+
|
|
45
|
+
if not apps.ready:
|
|
46
|
+
django.setup()
|
|
47
|
+
|
|
48
|
+
_cold_start_done = True
|
|
29
49
|
|
|
30
50
|
|
|
31
51
|
def handler(event: dict, context: object) -> dict:
|
|
@@ -34,6 +54,8 @@ def handler(event: dict, context: object) -> dict:
|
|
|
34
54
|
Returns a partial-batch failure report so AWS only re-drives failed records.
|
|
35
55
|
Signature is fixed by AWS and uses two args only.
|
|
36
56
|
"""
|
|
57
|
+
_perform_cold_start()
|
|
58
|
+
|
|
37
59
|
# Local import due to AppRegistryNotReady
|
|
38
60
|
from lambda_tasks.models import SQSLambdaTaskMessage
|
|
39
61
|
|
|
@@ -551,34 +551,42 @@ class TestPropertyIdempotentExecution:
|
|
|
551
551
|
|
|
552
552
|
|
|
553
553
|
class TestHandlerColdStartOrdering:
|
|
554
|
-
"""Verify handler
|
|
554
|
+
"""Verify handler cold-start runs environment loader, then secrets, then conditionally Django."""
|
|
555
|
+
|
|
556
|
+
def _invoke_handler(self) -> None:
|
|
557
|
+
"""Invoke the handler with a minimal valid event to trigger cold-start."""
|
|
558
|
+
import json
|
|
559
|
+
|
|
560
|
+
from lambda_tasks.handler import handler
|
|
561
|
+
|
|
562
|
+
body = json.dumps({"task_name": "some.task", "kwargs": {}})
|
|
563
|
+
event = {"Records": [{"messageId": "msg-1", "body": body}]}
|
|
564
|
+
handler(event=event, context=None)
|
|
555
565
|
|
|
556
566
|
def test_resolve_environment_called_before_resolve_secrets_into_env(
|
|
557
567
|
self,
|
|
558
568
|
monkeypatch,
|
|
559
569
|
) -> None:
|
|
560
570
|
"""resolve_environment() runs before resolve_secrets_into_env()."""
|
|
561
|
-
import
|
|
571
|
+
import lambda_tasks.handler as handler_module
|
|
562
572
|
|
|
563
573
|
call_order: list[str] = []
|
|
564
574
|
|
|
565
575
|
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
566
576
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
),
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
sys.modules.pop("lambda_tasks.handler", None)
|
|
581
|
-
import lambda_tasks.handler # noqa: F401
|
|
577
|
+
monkeypatch.setattr(
|
|
578
|
+
handler_module,
|
|
579
|
+
"resolve_environment",
|
|
580
|
+
lambda: call_order.append("resolve_environment"),
|
|
581
|
+
)
|
|
582
|
+
monkeypatch.setattr(
|
|
583
|
+
handler_module,
|
|
584
|
+
"resolve_secrets_into_env",
|
|
585
|
+
lambda: call_order.append("resolve_secrets_into_env"),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
handler_module._cold_start_done = False
|
|
589
|
+
self._invoke_handler()
|
|
582
590
|
|
|
583
591
|
assert call_order.index("resolve_environment") < call_order.index(
|
|
584
592
|
"resolve_secrets_into_env"
|
|
@@ -592,27 +600,27 @@ class TestHandlerColdStartOrdering:
|
|
|
592
600
|
monkeypatch,
|
|
593
601
|
) -> None:
|
|
594
602
|
"""Both loaders run even when DJANGO_SETTINGS_MODULE is unset."""
|
|
595
|
-
import
|
|
603
|
+
import lambda_tasks.handler as handler_module
|
|
596
604
|
|
|
597
605
|
call_order: list[str] = []
|
|
598
606
|
|
|
599
|
-
# Ensure DJANGO_SETTINGS_MODULE is NOT set — loaders must still run
|
|
600
607
|
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
601
608
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
),
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
609
|
+
monkeypatch.setattr(
|
|
610
|
+
handler_module,
|
|
611
|
+
"resolve_environment",
|
|
612
|
+
lambda: call_order.append("resolve_environment"),
|
|
613
|
+
)
|
|
614
|
+
monkeypatch.setattr(
|
|
615
|
+
handler_module,
|
|
616
|
+
"resolve_secrets_into_env",
|
|
617
|
+
lambda: call_order.append("resolve_secrets_into_env"),
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
handler_module._cold_start_done = False
|
|
621
|
+
|
|
622
|
+
with patch("django.setup") as mock_django_setup:
|
|
623
|
+
self._invoke_handler()
|
|
616
624
|
|
|
617
625
|
assert (
|
|
618
626
|
"resolve_environment" in call_order
|
|
@@ -620,7 +628,6 @@ class TestHandlerColdStartOrdering:
|
|
|
620
628
|
assert (
|
|
621
629
|
"resolve_secrets_into_env" in call_order
|
|
622
630
|
), "resolve_secrets_into_env was not called when DJANGO_SETTINGS_MODULE is unset"
|
|
623
|
-
# django.setup() should NOT have been called since DJANGO_SETTINGS_MODULE is unset
|
|
624
631
|
mock_django_setup.assert_not_called()
|
|
625
632
|
|
|
626
633
|
def test_django_setup_called_when_settings_module_set_and_apps_not_ready(
|
|
@@ -628,22 +635,19 @@ class TestHandlerColdStartOrdering:
|
|
|
628
635
|
monkeypatch,
|
|
629
636
|
) -> None:
|
|
630
637
|
"""django.setup() called when DJANGO_SETTINGS_MODULE is set and apps.ready is False."""
|
|
631
|
-
import
|
|
638
|
+
import lambda_tasks.handler as handler_module
|
|
632
639
|
|
|
633
640
|
monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tests.settings")
|
|
641
|
+
monkeypatch.setattr(handler_module, "resolve_environment", lambda: None)
|
|
642
|
+
monkeypatch.setattr(handler_module, "resolve_secrets_into_env", lambda: None)
|
|
643
|
+
|
|
644
|
+
handler_module._cold_start_done = False
|
|
634
645
|
|
|
635
646
|
with (
|
|
636
|
-
patch(
|
|
637
|
-
"lambda_tasks.environment_loader.resolve_environment",
|
|
638
|
-
),
|
|
639
|
-
patch(
|
|
640
|
-
"lambda_tasks.secret_loader.resolve_secrets_into_env",
|
|
641
|
-
),
|
|
642
647
|
patch("django.setup") as mock_django_setup,
|
|
643
648
|
patch("django.apps.apps.ready", new=False),
|
|
644
649
|
):
|
|
645
|
-
|
|
646
|
-
import lambda_tasks.handler # noqa: F401
|
|
650
|
+
self._invoke_handler()
|
|
647
651
|
|
|
648
652
|
mock_django_setup.assert_called_once()
|
|
649
653
|
|
|
@@ -652,22 +656,19 @@ class TestHandlerColdStartOrdering:
|
|
|
652
656
|
monkeypatch,
|
|
653
657
|
) -> None:
|
|
654
658
|
"""django.setup() is NOT called when apps.ready is True."""
|
|
655
|
-
import
|
|
659
|
+
import lambda_tasks.handler as handler_module
|
|
656
660
|
|
|
657
661
|
monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tests.settings")
|
|
662
|
+
monkeypatch.setattr(handler_module, "resolve_environment", lambda: None)
|
|
663
|
+
monkeypatch.setattr(handler_module, "resolve_secrets_into_env", lambda: None)
|
|
664
|
+
|
|
665
|
+
handler_module._cold_start_done = False
|
|
658
666
|
|
|
659
667
|
with (
|
|
660
|
-
patch(
|
|
661
|
-
"lambda_tasks.environment_loader.resolve_environment",
|
|
662
|
-
),
|
|
663
|
-
patch(
|
|
664
|
-
"lambda_tasks.secret_loader.resolve_secrets_into_env",
|
|
665
|
-
),
|
|
666
668
|
patch("django.setup") as mock_django_setup,
|
|
667
669
|
patch("django.apps.apps.ready", new=True),
|
|
668
670
|
):
|
|
669
|
-
|
|
670
|
-
import lambda_tasks.handler # noqa: F401
|
|
671
|
+
self._invoke_handler()
|
|
671
672
|
|
|
672
673
|
mock_django_setup.assert_not_called()
|
|
673
674
|
|
|
@@ -676,21 +677,15 @@ class TestHandlerColdStartOrdering:
|
|
|
676
677
|
monkeypatch,
|
|
677
678
|
) -> None:
|
|
678
679
|
"""django.setup() is NOT called when DJANGO_SETTINGS_MODULE is unset."""
|
|
679
|
-
import
|
|
680
|
+
import lambda_tasks.handler as handler_module
|
|
680
681
|
|
|
681
682
|
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
683
|
+
monkeypatch.setattr(handler_module, "resolve_environment", lambda: None)
|
|
684
|
+
monkeypatch.setattr(handler_module, "resolve_secrets_into_env", lambda: None)
|
|
682
685
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
)
|
|
687
|
-
patch(
|
|
688
|
-
"lambda_tasks.secret_loader.resolve_secrets_into_env",
|
|
689
|
-
),
|
|
690
|
-
patch("django.setup") as mock_django_setup,
|
|
691
|
-
patch("django.apps.apps.ready", new=False),
|
|
692
|
-
):
|
|
693
|
-
sys.modules.pop("lambda_tasks.handler", None)
|
|
694
|
-
import lambda_tasks.handler # noqa: F401
|
|
686
|
+
handler_module._cold_start_done = False
|
|
687
|
+
|
|
688
|
+
with patch("django.setup") as mock_django_setup:
|
|
689
|
+
self._invoke_handler()
|
|
695
690
|
|
|
696
691
|
mock_django_setup.assert_not_called()
|
|
@@ -271,33 +271,44 @@ def test_property_11_batch_records_processed_independently(flags):
|
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
def test_property_4_django_setup_before_execute_task(monkeypatch):
|
|
274
|
+
"""Cold-start init (resolve_environment, resolve_secrets, django.setup) runs
|
|
275
|
+
inside the handler on first invocation, not at module import time."""
|
|
274
276
|
import importlib
|
|
275
277
|
|
|
276
278
|
import django.apps
|
|
277
279
|
|
|
278
280
|
import lambda_tasks.handler as handler_module
|
|
279
281
|
|
|
280
|
-
call_order = []
|
|
282
|
+
call_order: list[str] = []
|
|
281
283
|
|
|
282
284
|
monkeypatch.setattr(django.apps.apps, "ready", False)
|
|
283
285
|
monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tests.settings")
|
|
284
286
|
|
|
287
|
+
def spy_resolve_environment():
|
|
288
|
+
call_order.append("resolve_environment")
|
|
289
|
+
|
|
290
|
+
def spy_resolve_secrets():
|
|
291
|
+
call_order.append("resolve_secrets_into_env")
|
|
292
|
+
|
|
285
293
|
def spy_setup(*args, **kwargs):
|
|
286
294
|
call_order.append("django.setup")
|
|
287
295
|
|
|
288
|
-
monkeypatch.setattr(django, "setup", spy_setup)
|
|
289
|
-
|
|
290
296
|
def spy_model_validate(body):
|
|
291
297
|
call_order.append("execute_task")
|
|
292
298
|
return MagicMock()
|
|
293
299
|
|
|
300
|
+
monkeypatch.setattr(
|
|
301
|
+
"lambda_tasks.handler.resolve_environment", spy_resolve_environment
|
|
302
|
+
)
|
|
303
|
+
monkeypatch.setattr(
|
|
304
|
+
"lambda_tasks.handler.resolve_secrets_into_env", spy_resolve_secrets
|
|
305
|
+
)
|
|
306
|
+
monkeypatch.setattr(django, "setup", spy_setup)
|
|
294
307
|
monkeypatch.setattr(
|
|
295
308
|
"lambda_tasks.models.SQSLambdaTaskMessage.model_validate_json",
|
|
296
309
|
spy_model_validate,
|
|
297
310
|
)
|
|
298
311
|
|
|
299
|
-
importlib.reload(handler_module)
|
|
300
|
-
|
|
301
312
|
body = json.dumps(
|
|
302
313
|
{
|
|
303
314
|
"task_name": "some.task",
|
|
@@ -305,17 +316,54 @@ def test_property_4_django_setup_before_execute_task(monkeypatch):
|
|
|
305
316
|
}
|
|
306
317
|
)
|
|
307
318
|
event = {"Records": [{"messageId": "msg-1", "body": body}]}
|
|
319
|
+
|
|
320
|
+
# Reset the handler's cold-start guard so it runs init again
|
|
321
|
+
handler_module._cold_start_done = False
|
|
308
322
|
handler_module.handler(event=event, context=None)
|
|
309
323
|
|
|
310
|
-
assert
|
|
311
|
-
"
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
assert call_order == [
|
|
325
|
+
"resolve_environment",
|
|
326
|
+
"resolve_secrets_into_env",
|
|
327
|
+
"django.setup",
|
|
328
|
+
"execute_task",
|
|
329
|
+
], f"Unexpected call order: {call_order}"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_property_4_cold_start_runs_only_once(monkeypatch):
|
|
333
|
+
"""The cold-start init sequence runs only on the first invocation."""
|
|
334
|
+
import django.apps
|
|
335
|
+
|
|
336
|
+
import lambda_tasks.handler as handler_module
|
|
337
|
+
|
|
338
|
+
call_count = {"setup": 0}
|
|
339
|
+
|
|
340
|
+
monkeypatch.setattr(django.apps.apps, "ready", False)
|
|
341
|
+
monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tests.settings")
|
|
342
|
+
|
|
343
|
+
def spy_resolve_environment():
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
def spy_resolve_secrets():
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
def spy_setup(*args, **kwargs):
|
|
350
|
+
call_count["setup"] += 1
|
|
351
|
+
|
|
352
|
+
monkeypatch.setattr(
|
|
353
|
+
"lambda_tasks.handler.resolve_environment", spy_resolve_environment
|
|
318
354
|
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
)
|
|
355
|
+
monkeypatch.setattr(
|
|
356
|
+
"lambda_tasks.handler.resolve_secrets_into_env", spy_resolve_secrets
|
|
357
|
+
)
|
|
358
|
+
monkeypatch.setattr(django, "setup", spy_setup)
|
|
359
|
+
|
|
360
|
+
with _patch_model_validate(side_effect=lambda body: MagicMock()):
|
|
361
|
+
handler_module._cold_start_done = False
|
|
362
|
+
handler_module.handler(
|
|
363
|
+
event={"Records": [_make_record("m1", _valid_body())]}, context=None
|
|
364
|
+
)
|
|
365
|
+
handler_module.handler(
|
|
366
|
+
event={"Records": [_make_record("m2", _valid_body())]}, context=None
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
assert call_count["setup"] == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/.config.kiro
RENAMED
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/design.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.1 → django_lambda_tasks-0.2.2}/lambda_tasks/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|