django-lambda-tasks 0.4.1__tar.gz → 0.4.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.
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/steering/product.md +2 -1
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/steering/structure.md +1 -1
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/PKG-INFO +1 -1
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/admin.py +22 -1
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/handler.py +33 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/pyproject.toml +1 -1
- django_lambda_tasks-0.4.3/tests/test_admin.py +282 -0
- django_lambda_tasks-0.4.3/tests/test_memory_limit.py +131 -0
- django_lambda_tasks-0.4.1/tests/test_admin.py +0 -21
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.gitignore +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/README.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/README.md +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/example/manage.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/models.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/settings.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/tests/test_timeouts.py +0 -0
|
@@ -136,10 +136,11 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
136
136
|
- Returns `{"batchItemFailures": [...]}` for partial-batch failure reporting
|
|
137
137
|
- 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
|
|
138
138
|
- Recommended SQS queue settings: `maxReceiveCount=1` with a DLQ configured; automatic retries are not useful since task failures are not re-driven by design
|
|
139
|
-
- Cold-start sequence runs inside the handler on the first invocation (not at module import time) to avoid Lambda init-duration timeouts: a temporary `StreamHandler` is attached to the `lambda_tasks` logger, then `resolve_environment()` → `resolve_secrets_into_env()` (handler removed) → conditional `django.setup()`
|
|
139
|
+
- Cold-start sequence runs inside the handler on the first invocation (not at module import time) to avoid Lambda init-duration timeouts: memory limit is set, a temporary `StreamHandler` is attached to the `lambda_tasks` logger, then `resolve_environment()` → `resolve_secrets_into_env()` (handler removed) → conditional `django.setup()`
|
|
140
140
|
- A module-level `_cold_start_done` sentinel ensures the sequence runs only once; subsequent warm invocations skip it
|
|
141
141
|
- 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
|
|
142
142
|
- A temporary `StreamHandler` is attached to the `lambda_tasks` logger for the duration of the loaders so their log output is visible before Django's `LOGGING` dictConfig has run; it is removed immediately after so that Django's configuration is the sole authority on logging from that point on
|
|
143
|
+
- `resource.setrlimit(RLIMIT_DATA)` is set from `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` so that excessive allocation raises `MemoryError` instead of triggering the OOM killer
|
|
143
144
|
|
|
144
145
|
## Environment Loader
|
|
145
146
|
|
|
@@ -42,7 +42,7 @@ django-lambda-tasks/
|
|
|
42
42
|
- `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
|
|
43
43
|
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` publishes to SQS, executes eagerly, or submits 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
|
|
45
|
-
- `handler.py` — Lambda entry point; cold-start init (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`
|
|
45
|
+
- `handler.py` — Lambda entry point; 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`
|
|
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
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
from django.contrib import admin
|
|
4
4
|
from django.db.models import DurationField, ExpressionWrapper, F, QuerySet
|
|
5
5
|
from django.http import HttpRequest
|
|
6
|
+
from django.utils.module_loading import import_string
|
|
6
7
|
|
|
7
|
-
from lambda_tasks.models import
|
|
8
|
+
from lambda_tasks.models import (
|
|
9
|
+
SQSLambdaTask,
|
|
10
|
+
SQSLambdaTaskMessage,
|
|
11
|
+
TaskRecord,
|
|
12
|
+
)
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
@admin.register(TaskRecord)
|
|
@@ -33,6 +38,7 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
33
38
|
"result",
|
|
34
39
|
"traceback",
|
|
35
40
|
)
|
|
41
|
+
actions = ["replay_tasks"]
|
|
36
42
|
|
|
37
43
|
def get_queryset(self, request: HttpRequest) -> QuerySet:
|
|
38
44
|
return (
|
|
@@ -52,3 +58,18 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
52
58
|
return None
|
|
53
59
|
total_seconds = d.total_seconds()
|
|
54
60
|
return f"{total_seconds:.3f}s"
|
|
61
|
+
|
|
62
|
+
@admin.action(description="Replay selected tasks", permissions=("change",))
|
|
63
|
+
def replay_tasks(self, request: HttpRequest, queryset: QuerySet) -> None:
|
|
64
|
+
for record in queryset:
|
|
65
|
+
queue = import_string(record.task_name).queue
|
|
66
|
+
task = SQSLambdaTask(
|
|
67
|
+
message=SQSLambdaTaskMessage(
|
|
68
|
+
task_name=record.task_name,
|
|
69
|
+
kwargs=record.kwargs,
|
|
70
|
+
n_retries=0,
|
|
71
|
+
),
|
|
72
|
+
delay=0,
|
|
73
|
+
queue=queue,
|
|
74
|
+
)
|
|
75
|
+
task.execute_on_commit()
|
|
@@ -14,6 +14,7 @@ warm invocations skip it.
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
|
+
import resource
|
|
17
18
|
|
|
18
19
|
import django
|
|
19
20
|
from django.apps import apps
|
|
@@ -26,6 +27,36 @@ logger = logging.getLogger(__name__)
|
|
|
26
27
|
_cold_start_done: bool = False
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
_MEMORY_RESERVED_MB = 128
|
|
31
|
+
_MEMORY_MINIMUM_LIMIT_MB = 64
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _set_memory_limit() -> None:
|
|
35
|
+
"""Set RLIMIT_DATA from AWS_LAMBDA_FUNCTION_MEMORY_SIZE if available.
|
|
36
|
+
|
|
37
|
+
When running in Lambda, this causes Python to raise MemoryError on
|
|
38
|
+
excessive allocation instead of being killed by the OOM killer.
|
|
39
|
+
|
|
40
|
+
128 MB is reserved for the Python runtime, shared libraries, and OS
|
|
41
|
+
overhead. The limit is floored at 64 MB so that even small Lambdas
|
|
42
|
+
get some protection.
|
|
43
|
+
"""
|
|
44
|
+
memory_mb = os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")
|
|
45
|
+
|
|
46
|
+
if memory_mb is None:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
limit_bytes = (
|
|
50
|
+
max(int(memory_mb) - _MEMORY_RESERVED_MB, _MEMORY_MINIMUM_LIMIT_MB)
|
|
51
|
+
* 1024
|
|
52
|
+
* 1024
|
|
53
|
+
)
|
|
54
|
+
resource.setrlimit(resource.RLIMIT_AS, (limit_bytes, limit_bytes))
|
|
55
|
+
logger.info(
|
|
56
|
+
f"Set RLIMIT_AS to {limit_bytes} bytes ({memory_mb} MB - {_MEMORY_RESERVED_MB} MB reserved)"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
29
60
|
def _perform_cold_start() -> None:
|
|
30
61
|
"""Run one-time initialisation: env loading, secrets, Django setup.
|
|
31
62
|
|
|
@@ -49,6 +80,8 @@ def _perform_cold_start() -> None:
|
|
|
49
80
|
lambda_tasks_logger.addHandler(boot_handler)
|
|
50
81
|
lambda_tasks_logger.setLevel(logging.DEBUG)
|
|
51
82
|
|
|
83
|
+
_set_memory_limit()
|
|
84
|
+
|
|
52
85
|
try:
|
|
53
86
|
resolve_environment()
|
|
54
87
|
resolve_secrets_into_env()
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from django.contrib import admin
|
|
5
|
+
from django.contrib.auth.models import Permission, User
|
|
6
|
+
from django.contrib.contenttypes.models import ContentType
|
|
7
|
+
from django.test import RequestFactory
|
|
8
|
+
from django.utils.timezone import now
|
|
9
|
+
|
|
10
|
+
import lambda_tasks.admin # noqa: F401 — triggers @admin.register side-effect
|
|
11
|
+
from lambda_tasks.models import SQSLambdaTask, TaskRecord, TaskStatus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_task_record_registered_in_admin():
|
|
15
|
+
assert TaskRecord in admin.site._registry
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_task_record_admin_list_display():
|
|
19
|
+
assert admin.site._registry[TaskRecord].list_display == (
|
|
20
|
+
"pk",
|
|
21
|
+
"task_name",
|
|
22
|
+
"status",
|
|
23
|
+
"start_time",
|
|
24
|
+
"end_time",
|
|
25
|
+
"n_retries",
|
|
26
|
+
"duration",
|
|
27
|
+
"result",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture()
|
|
32
|
+
def admin_user(db: None) -> User:
|
|
33
|
+
return User.objects.create_superuser(
|
|
34
|
+
username="admin", password="password", email="admin@example.com"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture()
|
|
39
|
+
def task_record(db: None) -> TaskRecord:
|
|
40
|
+
return TaskRecord.objects.create(
|
|
41
|
+
id="00000000-0000-0000-0000-000000000001",
|
|
42
|
+
task_name="myapp.tasks.my_task",
|
|
43
|
+
kwargs={"user_id": 42},
|
|
44
|
+
n_retries=3,
|
|
45
|
+
status=TaskStatus.FAILED,
|
|
46
|
+
start_time=now(),
|
|
47
|
+
end_time=now(),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture()
|
|
52
|
+
def _mock_import_string():
|
|
53
|
+
class _FakeWrapper:
|
|
54
|
+
queue = "default"
|
|
55
|
+
|
|
56
|
+
with patch("lambda_tasks.admin.import_string", return_value=_FakeWrapper()):
|
|
57
|
+
yield
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.django_db()
|
|
61
|
+
class TestReplayAction:
|
|
62
|
+
def test_replay_action_is_registered(self) -> None:
|
|
63
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
64
|
+
request = RequestFactory().post("/")
|
|
65
|
+
request.user = User(is_staff=True, is_superuser=True)
|
|
66
|
+
action_names = list(model_admin.get_actions(request).keys())
|
|
67
|
+
assert "replay_tasks" in action_names
|
|
68
|
+
|
|
69
|
+
def test_replay_enqueues_task_with_original_kwargs(
|
|
70
|
+
self, admin_user: User, task_record: TaskRecord, _mock_import_string: None
|
|
71
|
+
) -> None:
|
|
72
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
73
|
+
request = RequestFactory().post("/")
|
|
74
|
+
request.user = admin_user
|
|
75
|
+
|
|
76
|
+
queryset = TaskRecord.objects.filter(pk=task_record.pk)
|
|
77
|
+
|
|
78
|
+
with patch(
|
|
79
|
+
"lambda_tasks.admin.SQSLambdaTask.execute_on_commit"
|
|
80
|
+
) as mock_execute:
|
|
81
|
+
model_admin.replay_tasks(request, queryset)
|
|
82
|
+
|
|
83
|
+
mock_execute.assert_called_once()
|
|
84
|
+
|
|
85
|
+
def test_replay_resets_n_retries_to_zero(
|
|
86
|
+
self, admin_user: User, task_record: TaskRecord, _mock_import_string: None
|
|
87
|
+
) -> None:
|
|
88
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
89
|
+
request = RequestFactory().post("/")
|
|
90
|
+
request.user = admin_user
|
|
91
|
+
|
|
92
|
+
queryset = TaskRecord.objects.filter(pk=task_record.pk)
|
|
93
|
+
|
|
94
|
+
built_tasks: list[SQSLambdaTask] = []
|
|
95
|
+
|
|
96
|
+
def capture_execute(self: SQSLambdaTask) -> None:
|
|
97
|
+
built_tasks.append(self)
|
|
98
|
+
|
|
99
|
+
with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
|
|
100
|
+
model_admin.replay_tasks(request, queryset)
|
|
101
|
+
|
|
102
|
+
assert len(built_tasks) == 1
|
|
103
|
+
assert built_tasks[0].message.n_retries == 0
|
|
104
|
+
|
|
105
|
+
def test_replay_preserves_task_name(
|
|
106
|
+
self, admin_user: User, task_record: TaskRecord, _mock_import_string: None
|
|
107
|
+
) -> None:
|
|
108
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
109
|
+
request = RequestFactory().post("/")
|
|
110
|
+
request.user = admin_user
|
|
111
|
+
|
|
112
|
+
queryset = TaskRecord.objects.filter(pk=task_record.pk)
|
|
113
|
+
|
|
114
|
+
built_tasks: list[SQSLambdaTask] = []
|
|
115
|
+
|
|
116
|
+
def capture_execute(self: SQSLambdaTask) -> None:
|
|
117
|
+
built_tasks.append(self)
|
|
118
|
+
|
|
119
|
+
with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
|
|
120
|
+
model_admin.replay_tasks(request, queryset)
|
|
121
|
+
|
|
122
|
+
assert built_tasks[0].message.task_name == "myapp.tasks.my_task"
|
|
123
|
+
assert built_tasks[0].message.kwargs == {"user_id": 42}
|
|
124
|
+
|
|
125
|
+
def test_replay_uses_delay_zero(
|
|
126
|
+
self, admin_user: User, task_record: TaskRecord, _mock_import_string: None
|
|
127
|
+
) -> None:
|
|
128
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
129
|
+
request = RequestFactory().post("/")
|
|
130
|
+
request.user = admin_user
|
|
131
|
+
|
|
132
|
+
queryset = TaskRecord.objects.filter(pk=task_record.pk)
|
|
133
|
+
|
|
134
|
+
built_tasks: list[SQSLambdaTask] = []
|
|
135
|
+
|
|
136
|
+
def capture_execute(self: SQSLambdaTask) -> None:
|
|
137
|
+
built_tasks.append(self)
|
|
138
|
+
|
|
139
|
+
with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
|
|
140
|
+
model_admin.replay_tasks(request, queryset)
|
|
141
|
+
|
|
142
|
+
assert built_tasks[0].delay == 0
|
|
143
|
+
|
|
144
|
+
def test_replay_resolves_queue_from_wrapper(
|
|
145
|
+
self, admin_user: User, db: None
|
|
146
|
+
) -> None:
|
|
147
|
+
record = TaskRecord.objects.create(
|
|
148
|
+
id="00000000-0000-0000-0000-000000000002",
|
|
149
|
+
task_name="myapp.tasks.custom_queue_task",
|
|
150
|
+
kwargs={"x": 1},
|
|
151
|
+
n_retries=0,
|
|
152
|
+
status=TaskStatus.FAILED,
|
|
153
|
+
start_time=now(),
|
|
154
|
+
end_time=now(),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
158
|
+
request = RequestFactory().post("/")
|
|
159
|
+
request.user = admin_user
|
|
160
|
+
|
|
161
|
+
queryset = TaskRecord.objects.filter(pk=record.pk)
|
|
162
|
+
|
|
163
|
+
built_tasks: list[SQSLambdaTask] = []
|
|
164
|
+
|
|
165
|
+
def capture_execute(self: SQSLambdaTask) -> None:
|
|
166
|
+
built_tasks.append(self)
|
|
167
|
+
|
|
168
|
+
class _FakeWrapper:
|
|
169
|
+
queue = "high-priority"
|
|
170
|
+
|
|
171
|
+
with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
|
|
172
|
+
with patch(
|
|
173
|
+
"lambda_tasks.admin.import_string",
|
|
174
|
+
return_value=_FakeWrapper(),
|
|
175
|
+
):
|
|
176
|
+
model_admin.replay_tasks(request, queryset)
|
|
177
|
+
|
|
178
|
+
assert built_tasks[0].queue == "high-priority"
|
|
179
|
+
|
|
180
|
+
def test_replay_raises_on_import_error(
|
|
181
|
+
self, admin_user: User, task_record: TaskRecord
|
|
182
|
+
) -> None:
|
|
183
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
184
|
+
request = RequestFactory().post("/")
|
|
185
|
+
request.user = admin_user
|
|
186
|
+
|
|
187
|
+
queryset = TaskRecord.objects.filter(pk=task_record.pk)
|
|
188
|
+
|
|
189
|
+
with patch(
|
|
190
|
+
"lambda_tasks.admin.import_string",
|
|
191
|
+
side_effect=ImportError("no such module"),
|
|
192
|
+
):
|
|
193
|
+
with pytest.raises(ImportError, match="no such module"):
|
|
194
|
+
model_admin.replay_tasks(request, queryset)
|
|
195
|
+
|
|
196
|
+
def test_replay_raises_when_not_wrapper(
|
|
197
|
+
self, admin_user: User, task_record: TaskRecord
|
|
198
|
+
) -> None:
|
|
199
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
200
|
+
request = RequestFactory().post("/")
|
|
201
|
+
request.user = admin_user
|
|
202
|
+
|
|
203
|
+
queryset = TaskRecord.objects.filter(pk=task_record.pk)
|
|
204
|
+
|
|
205
|
+
with patch(
|
|
206
|
+
"lambda_tasks.admin.import_string",
|
|
207
|
+
return_value="not a wrapper",
|
|
208
|
+
):
|
|
209
|
+
with pytest.raises(AttributeError):
|
|
210
|
+
model_admin.replay_tasks(request, queryset)
|
|
211
|
+
|
|
212
|
+
def test_replay_multiple_records(
|
|
213
|
+
self, admin_user: User, db: None, _mock_import_string: None
|
|
214
|
+
) -> None:
|
|
215
|
+
records = [
|
|
216
|
+
TaskRecord.objects.create(
|
|
217
|
+
id=f"00000000-0000-0000-0000-00000000000{i}",
|
|
218
|
+
task_name=f"myapp.tasks.task_{i}",
|
|
219
|
+
kwargs={"key": i},
|
|
220
|
+
n_retries=0,
|
|
221
|
+
status=TaskStatus.FAILED,
|
|
222
|
+
start_time=now(),
|
|
223
|
+
end_time=now(),
|
|
224
|
+
)
|
|
225
|
+
for i in range(3)
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
229
|
+
request = RequestFactory().post("/")
|
|
230
|
+
request.user = admin_user
|
|
231
|
+
|
|
232
|
+
queryset = TaskRecord.objects.filter(pk__in=[r.pk for r in records])
|
|
233
|
+
|
|
234
|
+
built_tasks: list[SQSLambdaTask] = []
|
|
235
|
+
|
|
236
|
+
def capture_execute(self: SQSLambdaTask) -> None:
|
|
237
|
+
built_tasks.append(self)
|
|
238
|
+
|
|
239
|
+
with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
|
|
240
|
+
model_admin.replay_tasks(request, queryset)
|
|
241
|
+
|
|
242
|
+
assert len(built_tasks) == 3
|
|
243
|
+
task_names = {t.message.task_name for t in built_tasks}
|
|
244
|
+
assert task_names == {
|
|
245
|
+
"myapp.tasks.task_0",
|
|
246
|
+
"myapp.tasks.task_1",
|
|
247
|
+
"myapp.tasks.task_2",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
def test_replay_action_hidden_without_change_permission(self, db: None) -> None:
|
|
251
|
+
user = User.objects.create_user(
|
|
252
|
+
username="viewer", password="password", is_staff=True
|
|
253
|
+
)
|
|
254
|
+
content_type = ContentType.objects.get_for_model(TaskRecord)
|
|
255
|
+
view_perm = Permission.objects.get(
|
|
256
|
+
codename="view_taskrecord", content_type=content_type
|
|
257
|
+
)
|
|
258
|
+
user.user_permissions.add(view_perm)
|
|
259
|
+
|
|
260
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
261
|
+
request = RequestFactory().get("/")
|
|
262
|
+
request.user = user
|
|
263
|
+
|
|
264
|
+
action_names = list(model_admin.get_actions(request).keys())
|
|
265
|
+
assert "replay_tasks" not in action_names
|
|
266
|
+
|
|
267
|
+
def test_replay_action_visible_with_change_permission(self, db: None) -> None:
|
|
268
|
+
user = User.objects.create_user(
|
|
269
|
+
username="editor", password="password", is_staff=True
|
|
270
|
+
)
|
|
271
|
+
content_type = ContentType.objects.get_for_model(TaskRecord)
|
|
272
|
+
change_perm = Permission.objects.get(
|
|
273
|
+
codename="change_taskrecord", content_type=content_type
|
|
274
|
+
)
|
|
275
|
+
user.user_permissions.add(change_perm)
|
|
276
|
+
|
|
277
|
+
model_admin = admin.site._registry[TaskRecord]
|
|
278
|
+
request = RequestFactory().get("/")
|
|
279
|
+
request.user = user
|
|
280
|
+
|
|
281
|
+
action_names = list(model_admin.get_actions(request).keys())
|
|
282
|
+
assert "replay_tasks" in action_names
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for memory limit enforcement in the Lambda handler.
|
|
3
|
+
|
|
4
|
+
The handler sets resource.RLIMIT_AS during cold start so that runaway
|
|
5
|
+
memory allocation raises MemoryError instead of triggering the Lambda OOM
|
|
6
|
+
killer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import resource
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
import lambda_tasks.handler as handler_module
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestMemoryLimitSetDuringColdStart:
|
|
16
|
+
def test_rlimit_as_set_from_env_var(self, monkeypatch):
|
|
17
|
+
"""When AWS_LAMBDA_FUNCTION_MEMORY_SIZE is set, RLIMIT_AS is configured with 128 MB reserved."""
|
|
18
|
+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
19
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "512")
|
|
20
|
+
monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
|
|
21
|
+
monkeypatch.setattr(
|
|
22
|
+
"lambda_tasks.handler.resolve_secrets_into_env", lambda: None
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
calls: list[tuple] = []
|
|
26
|
+
|
|
27
|
+
def fake_setrlimit(which: int, limits: tuple[int, int]) -> None:
|
|
28
|
+
calls.append((which, limits))
|
|
29
|
+
|
|
30
|
+
monkeypatch.setattr(resource, "setrlimit", fake_setrlimit)
|
|
31
|
+
|
|
32
|
+
handler_module._cold_start_done = False
|
|
33
|
+
handler_module.handler(event={"Records": []}, context=None)
|
|
34
|
+
|
|
35
|
+
expected = (512 - 128) * 1024 * 1024
|
|
36
|
+
assert calls == [(resource.RLIMIT_AS, (expected, expected))]
|
|
37
|
+
|
|
38
|
+
def test_rlimit_as_not_set_when_env_var_missing(self, monkeypatch):
|
|
39
|
+
"""When AWS_LAMBDA_FUNCTION_MEMORY_SIZE is not set, RLIMIT_AS is unchanged."""
|
|
40
|
+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
41
|
+
monkeypatch.delenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", raising=False)
|
|
42
|
+
monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
|
|
43
|
+
monkeypatch.setattr(
|
|
44
|
+
"lambda_tasks.handler.resolve_secrets_into_env", lambda: None
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
calls: list[tuple] = []
|
|
48
|
+
|
|
49
|
+
def fake_setrlimit(which: int, limits: tuple[int, int]) -> None:
|
|
50
|
+
calls.append((which, limits))
|
|
51
|
+
|
|
52
|
+
monkeypatch.setattr(resource, "setrlimit", fake_setrlimit)
|
|
53
|
+
|
|
54
|
+
handler_module._cold_start_done = False
|
|
55
|
+
handler_module.handler(event={"Records": []}, context=None)
|
|
56
|
+
|
|
57
|
+
assert calls == []
|
|
58
|
+
|
|
59
|
+
def test_memory_limit_set_before_loaders(self, monkeypatch):
|
|
60
|
+
"""The memory limit is set before resolve_environment runs."""
|
|
61
|
+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
62
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "256")
|
|
63
|
+
|
|
64
|
+
call_order: list[str] = []
|
|
65
|
+
|
|
66
|
+
def fake_setrlimit(which: int, limits: tuple[int, int]) -> None:
|
|
67
|
+
call_order.append("setrlimit")
|
|
68
|
+
|
|
69
|
+
def fake_resolve_environment() -> None:
|
|
70
|
+
call_order.append("resolve_environment")
|
|
71
|
+
|
|
72
|
+
def fake_resolve_secrets() -> None:
|
|
73
|
+
call_order.append("resolve_secrets_into_env")
|
|
74
|
+
|
|
75
|
+
monkeypatch.setattr(resource, "setrlimit", fake_setrlimit)
|
|
76
|
+
monkeypatch.setattr(
|
|
77
|
+
"lambda_tasks.handler.resolve_environment", fake_resolve_environment
|
|
78
|
+
)
|
|
79
|
+
monkeypatch.setattr(
|
|
80
|
+
"lambda_tasks.handler.resolve_secrets_into_env", fake_resolve_secrets
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
handler_module._cold_start_done = False
|
|
84
|
+
handler_module.handler(event={"Records": []}, context=None)
|
|
85
|
+
|
|
86
|
+
assert call_order == [
|
|
87
|
+
"setrlimit",
|
|
88
|
+
"resolve_environment",
|
|
89
|
+
"resolve_secrets_into_env",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
def test_memory_limit_is_logged(self, monkeypatch, caplog):
|
|
93
|
+
"""Setting the memory limit emits an info log."""
|
|
94
|
+
import logging
|
|
95
|
+
|
|
96
|
+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
97
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "1024")
|
|
98
|
+
monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
|
|
99
|
+
monkeypatch.setattr(
|
|
100
|
+
"lambda_tasks.handler.resolve_secrets_into_env", lambda: None
|
|
101
|
+
)
|
|
102
|
+
monkeypatch.setattr(resource, "setrlimit", lambda *args: None)
|
|
103
|
+
|
|
104
|
+
handler_module._cold_start_done = False
|
|
105
|
+
|
|
106
|
+
with caplog.at_level(logging.INFO, logger="lambda_tasks.handler"):
|
|
107
|
+
handler_module.handler(event={"Records": []}, context=None)
|
|
108
|
+
|
|
109
|
+
assert any("RLIMIT_AS" in msg for msg in caplog.messages)
|
|
110
|
+
|
|
111
|
+
def test_rlimit_floored_at_minimum_when_memory_too_small(self, monkeypatch):
|
|
112
|
+
"""When AWS_LAMBDA_FUNCTION_MEMORY_SIZE <= reserved, RLIMIT_AS is set to the 64 MB minimum."""
|
|
113
|
+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
114
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128")
|
|
115
|
+
monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
|
|
116
|
+
monkeypatch.setattr(
|
|
117
|
+
"lambda_tasks.handler.resolve_secrets_into_env", lambda: None
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
calls: list[tuple] = []
|
|
121
|
+
|
|
122
|
+
def fake_setrlimit(which: int, limits: tuple[int, int]) -> None:
|
|
123
|
+
calls.append((which, limits))
|
|
124
|
+
|
|
125
|
+
monkeypatch.setattr(resource, "setrlimit", fake_setrlimit)
|
|
126
|
+
|
|
127
|
+
handler_module._cold_start_done = False
|
|
128
|
+
handler_module.handler(event={"Records": []}, context=None)
|
|
129
|
+
|
|
130
|
+
expected = 64 * 1024 * 1024
|
|
131
|
+
assert calls == [(resource.RLIMIT_AS, (expected, expected))]
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from django.contrib import admin
|
|
2
|
-
|
|
3
|
-
import lambda_tasks.admin # noqa: F401 — triggers @admin.register side-effect
|
|
4
|
-
from lambda_tasks.models import TaskRecord
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_task_record_registered_in_admin():
|
|
8
|
-
assert TaskRecord in admin.site._registry
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_task_record_admin_list_display():
|
|
12
|
-
assert admin.site._registry[TaskRecord].list_display == (
|
|
13
|
-
"pk",
|
|
14
|
-
"task_name",
|
|
15
|
-
"status",
|
|
16
|
-
"start_time",
|
|
17
|
-
"end_time",
|
|
18
|
-
"n_retries",
|
|
19
|
-
"duration",
|
|
20
|
-
"result",
|
|
21
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/async-local-execution/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/async-local-execution/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/deferred-task-enqueue/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/eager-mode-example-app/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.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.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/retry-delay/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/retry-delay/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/rse-background-tasks/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.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.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/singleton-task/.config.kiro
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/singleton-task/design.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/singleton-task/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ssm-environment-loader/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.kiro/specs/ssm-environment-loader/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/.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
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.3}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|