django-lambda-tasks 0.4.0__tar.gz → 0.4.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.4.0 → django_lambda_tasks-0.4.2}/PKG-INFO +1 -1
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/admin.py +22 -1
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/local_executor.py +15 -2
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/pyproject.toml +1 -1
- django_lambda_tasks-0.4.2/tests/test_admin.py +282 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_local_executor.py +19 -2
- django_lambda_tasks-0.4.0/tests/test_admin.py +0 -21
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.gitignore +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/steering/product.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/steering/structure.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/README.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/README.md +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/example/manage.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/handler.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/models.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/settings.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/tests/test_timeouts.py +0 -0
|
@@ -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()
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Process pool executor for async local task execution."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import uuid
|
|
4
|
-
from concurrent.futures import ProcessPoolExecutor
|
|
5
|
+
from concurrent.futures import Future, ProcessPoolExecutor
|
|
5
6
|
|
|
6
7
|
from lambda_tasks.settings import LambdaTasksSettings
|
|
7
8
|
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
8
11
|
_pool: ProcessPoolExecutor | None = None
|
|
9
12
|
|
|
10
13
|
|
|
@@ -38,8 +41,18 @@ def _execute_in_worker(*, message_json: str, message_id: str) -> None:
|
|
|
38
41
|
message.execute_immediately(message_id=message_id)
|
|
39
42
|
|
|
40
43
|
|
|
44
|
+
def _log_worker_exception(future: Future) -> None: # type: ignore[type-arg]
|
|
45
|
+
"""Callback attached to each worker future. Logs unhandled exceptions."""
|
|
46
|
+
exception = future.exception()
|
|
47
|
+
if exception is not None:
|
|
48
|
+
logger.error("Worker process raised an exception", exc_info=exception)
|
|
49
|
+
|
|
50
|
+
|
|
41
51
|
def submit_task(*, message_json: str) -> None:
|
|
42
52
|
"""Submit a task to the process pool. Fire-and-forget."""
|
|
43
53
|
pool = get_pool()
|
|
44
54
|
message_id = str(uuid.uuid4())
|
|
45
|
-
pool.submit(
|
|
55
|
+
future = pool.submit(
|
|
56
|
+
_execute_in_worker, message_json=message_json, message_id=message_id
|
|
57
|
+
)
|
|
58
|
+
future.add_done_callback(_log_worker_exception)
|
|
@@ -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
|
|
@@ -344,7 +344,7 @@ class TestSubmitTask:
|
|
|
344
344
|
assert str(parsed) == message_id
|
|
345
345
|
|
|
346
346
|
def test_submit_task_does_not_wait_on_future(self, settings):
|
|
347
|
-
"""submit_task() does not call .result()
|
|
347
|
+
"""submit_task() does not call .result() on the Future.
|
|
348
348
|
|
|
349
349
|
Validates: Requirements 3.4, 5.3
|
|
350
350
|
"""
|
|
@@ -363,7 +363,24 @@ class TestSubmitTask:
|
|
|
363
363
|
)
|
|
364
364
|
|
|
365
365
|
mock_future.result.assert_not_called()
|
|
366
|
-
|
|
366
|
+
|
|
367
|
+
def test_submit_task_attaches_exception_logging_callback(self, settings):
|
|
368
|
+
"""submit_task() attaches _log_worker_exception as a done callback."""
|
|
369
|
+
from lambda_tasks.local_executor import _log_worker_exception, submit_task
|
|
370
|
+
|
|
371
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 2
|
|
372
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
373
|
+
|
|
374
|
+
mock_pool = MagicMock()
|
|
375
|
+
mock_future = MagicMock()
|
|
376
|
+
mock_pool.submit.return_value = mock_future
|
|
377
|
+
|
|
378
|
+
with patch("lambda_tasks.local_executor.get_pool", return_value=mock_pool):
|
|
379
|
+
submit_task(
|
|
380
|
+
message_json='{"task_name": "myapp.tasks.foo", "kwargs": {}, "n_retries": 0}'
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
mock_future.add_done_callback.assert_called_once_with(_log_worker_exception)
|
|
367
384
|
|
|
368
385
|
|
|
369
386
|
# ---------------------------------------------------------------------------
|
|
@@ -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.0 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.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.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.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.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/.config.kiro
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/design.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.0 → django_lambda_tasks-0.4.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
|
|
File without changes
|
|
File without changes
|