django-lambda-tasks 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,462 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-lambda-tasks
3
+ Version: 0.1.0
4
+ Summary: Run async tasks in a lambda function
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: boto3
7
+ Requires-Dist: django
8
+ Requires-Dist: pydantic
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Django Lambda Tasks
12
+
13
+ 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.
14
+
15
+ > **Platform note:** Unix-based systems only (Linux, macOS). Timeout enforcement relies on `SIGALRM`.
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install django-lambda-tasks
23
+ ```
24
+
25
+ Add to `INSTALLED_APPS`:
26
+
27
+ ```python
28
+ INSTALLED_APPS = [
29
+ ...
30
+ "lambda_tasks",
31
+ ]
32
+ ```
33
+
34
+ Run migrations:
35
+
36
+ ```bash
37
+ python manage.py migrate lambda_tasks
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Quick Start
43
+
44
+ ### 1. Define a task
45
+
46
+ Create a `tasks.py` in your Django app and decorate any function with `@lambda_task`. All task arguments must be keyword-only:
47
+
48
+ ```python
49
+ # myapp/tasks.py
50
+ from lambda_tasks.decorators import lambda_task
51
+
52
+ @lambda_task
53
+ def send_welcome_email(*, user_id: int, template: str) -> str:
54
+ # ... send the email
55
+ return "sent"
56
+ ```
57
+
58
+ ### 2. Enqueue from a view
59
+
60
+ Call `.execute_on_commit()` to enqueue the task. It will be dispatched to SQS after the current database transaction commits:
61
+
62
+ ```python
63
+ # myapp/views.py
64
+ from myapp.tasks import send_welcome_email
65
+
66
+ def register(request):
67
+ user = User.objects.create(...)
68
+ send_welcome_email.execute_on_commit(user_id=user.id, template="welcome")
69
+ return HttpResponse("Registered")
70
+ ```
71
+
72
+ ### 3. Configure AWS
73
+
74
+ See [AWS Lambda and SQS setup](#aws-lambda-and-sqs-setup) for queue and Lambda configuration.
75
+
76
+ ---
77
+
78
+ ## Example app
79
+
80
+ A runnable Django project is included in the [`example/`](example/) directory. It uses `LAMBDA_TASKS_EAGER = True` so no AWS infrastructure is needed.
81
+
82
+ ```bash
83
+ cd example
84
+ uv run python manage.py migrate
85
+ uv run python manage.py createsuperuser
86
+ uv run python manage.py runserver
87
+ ```
88
+
89
+ Then visit `http://127.0.0.1:8000/trigger/?name=Alice` to trigger a task and `http://127.0.0.1:8000/admin/` to inspect the resulting `TaskRecord`.
90
+
91
+ See [`example/README.md`](example/README.md) for full details.
92
+
93
+ ---
94
+
95
+ ## Configuration
96
+
97
+ All settings are read from your Django settings module and prefixed with `LAMBDA_TASKS_`.
98
+
99
+ ### Queue settings
100
+
101
+ | Setting | Type | Description |
102
+ |---|---|---|
103
+ | `LAMBDA_TASKS_QUEUES` | `dict[str, str]` | Map of queue name → SQS queue URL. Must include a `"default"` key. |
104
+
105
+ ```python
106
+ LAMBDA_TASKS_QUEUES = {
107
+ "default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
108
+ "high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
109
+ }
110
+ ```
111
+
112
+ `LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use.
113
+
114
+ ### Timeout settings
115
+
116
+ | Setting | Type | Default | Description |
117
+ |---|---|---|---|
118
+ | `LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` | `int` | `270` | Seconds before `SoftTimeLimitExceeded` is raised inside the task. |
119
+ | `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `int` | `300` | Seconds before the task is forcibly terminated. |
120
+
121
+ Both values must be between `1` and `900` seconds (the AWS Lambda maximum runtime is 15 minutes). `soft_timeout` must always be strictly less than `hard_timeout`.
122
+
123
+ ```python
124
+ LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 240
125
+ LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
126
+ ```
127
+
128
+ ### Eager execution (development / testing)
129
+
130
+ | Setting | Type | Default | Description |
131
+ |---|---|---|---|
132
+ | `LAMBDA_TASKS_EAGER` | `bool` | `False` | When `True`, tasks run synchronously in-process instead of being sent to SQS. |
133
+
134
+ ```python
135
+ # settings/local.py
136
+ LAMBDA_TASKS_EAGER = True
137
+ ```
138
+
139
+ With eager mode enabled, `.execute_on_commit()` executes the task immediately without touching SQS. Useful for local development and test suites where you don't want to mock AWS infrastructure.
140
+
141
+ > **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext` is never entered — the task runs without any time limit. This is intentional: `SIGALRM`-based timeouts require a Lambda/Unix worker process, not a Django dev server thread.
142
+
143
+ ---
144
+
145
+ ## Decorator options
146
+
147
+ ```python
148
+ @lambda_task(
149
+ soft_timeout=60, # seconds — overrides global default for this task
150
+ hard_timeout=90, # seconds — overrides global default for this task
151
+ queue="default", # named queue from LAMBDA_TASKS_QUEUES
152
+ )
153
+ def my_task(*, arg: str) -> None:
154
+ ...
155
+ ```
156
+
157
+ | Parameter | Type | Default | Description |
158
+ |---|---|---|---|
159
+ | `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
160
+ | `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
161
+ | `queue` | `str` | `"default"` | Named queue to route this task to. |
162
+ | `ignore_errors` | `tuple[type[BaseException], ...]` | `()` | Exception types to treat as non-fatal (see [Ignored exceptions](#ignored-exceptions)). |
163
+
164
+ ---
165
+
166
+ ## Per-invocation overrides
167
+
168
+ Pass override kwargs prefixed with `_` to `.execute_on_commit()` to customise a single invocation:
169
+
170
+ ```python
171
+ send_welcome_email.execute_on_commit(
172
+ user_id=42,
173
+ template="welcome",
174
+ _delay=30, # SQS message visibility delay in seconds
175
+ )
176
+ ```
177
+
178
+ | Override | Type | Description |
179
+ |---|---|---|
180
+ | `_delay` | `int` | SQS message delay in seconds before the worker can pick it up. |
181
+
182
+ ---
183
+
184
+ ## Serializing a task invocation
185
+
186
+ `serialize()` builds and validates a task invocation the same way `execute_on_commit()` does, but returns the payload as a plain dict instead of enqueuing it. Useful when you need to inspect, store, or forward the message before deciding to send it.
187
+
188
+ ```python
189
+ payload = send_welcome_email.serialize(user_id=42, template="welcome")
190
+ # {
191
+ # "message": {
192
+ # "task_name": "myapp.tasks.send_welcome_email",
193
+ # "invocation_id": "<uuid4>",
194
+ # "kwargs": {"user_id": 42, "template": "welcome"}
195
+ # },
196
+ # "delay": 0,
197
+ # "queue": "default"
198
+ # }
199
+ ```
200
+
201
+ Per-invocation overrides (`_delay`) are accepted the same way as in `execute_on_commit()`.
202
+
203
+ The returned dict matches the `SQSLambdaTask` schema. To reconstruct and enqueue it later:
204
+
205
+ ```python
206
+ from lambda_tasks.models import SQSLambdaTask
207
+
208
+ task = SQSLambdaTask.model_validate(payload)
209
+ task.execute_on_commit()
210
+ ```
211
+
212
+ > **Note:** `serialize()` generates a fresh `invocation_id` on every call. Capture the result once if you need a stable reference to a specific invocation.
213
+
214
+ ---
215
+
216
+ ## Timeouts
217
+
218
+ Tasks support a two-phase timeout mechanism:
219
+
220
+ 1. **Soft timeout** — `SoftTimeLimitExceeded` is raised inside the running task, giving it a chance to catch the exception and clean up gracefully.
221
+ 2. **Hard timeout** — if the task is still running after the hard timeout, it is forcibly terminated and the `TaskRecord` is marked `FAILED`.
222
+
223
+ ```python
224
+ from lambda_tasks.decorators import lambda_task
225
+ from lambda_tasks.timeouts import SoftTimeLimitExceeded
226
+
227
+ @lambda_task(soft_timeout=25, hard_timeout=30)
228
+ def long_running_task(*, item_id: int) -> None:
229
+ try:
230
+ do_expensive_work(item_id)
231
+ except SoftTimeLimitExceeded:
232
+ # clean up before the hard timeout kills the process
233
+ cleanup(item_id)
234
+ raise
235
+ ```
236
+
237
+ Timeout resolution order (first non-`None` value wins):
238
+
239
+ 1. Per-task decorator default (`soft_timeout` / `hard_timeout` on `@lambda_task`)
240
+ 2. Global settings (`LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` / `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT`)
241
+
242
+ ---
243
+
244
+ ## Named queues
245
+
246
+ Route tasks to queues backed by Lambda functions with different hardware profiles (e.g. more memory or CPU):
247
+
248
+ ```python
249
+ # settings.py
250
+ LAMBDA_TASKS_QUEUES = {
251
+ "default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
252
+ "high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
253
+ }
254
+ ```
255
+
256
+ ```python
257
+ @lambda_task(queue="high_memory")
258
+ def process_large_file(*, file_id: int) -> None:
259
+ ...
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Task results
265
+
266
+ Every task invocation creates a `TaskRecord` row in the database. You can query it via the Django ORM:
267
+
268
+ ```python
269
+ from lambda_tasks.models import TaskRecord
270
+
271
+ # All recent tasks
272
+ TaskRecord.objects.all()
273
+
274
+ # Filter by status
275
+ TaskRecord.objects.filter(status=TaskRecord.TaskStatus.FAILED)
276
+
277
+ # Look up a specific invocation
278
+ TaskRecord.objects.get(invocation_id="<uuid>")
279
+ ```
280
+
281
+ ### `TaskRecord` fields
282
+
283
+ | Field | Type | Description |
284
+ |---|---|---|
285
+ | `task_name` | `str` | Fully-qualified function name (e.g. `myapp.tasks.send_welcome_email`). |
286
+ | `invocation_id` | `UUID` | Unique ID generated at enqueue time. |
287
+ | `kwargs` | `dict` | Serialized task arguments. |
288
+ | `status` | `str` | One of `RUNNING`, `SUCCESS`, `FAILED`. |
289
+ | `start_time` | `datetime \| None` | When the worker began executing the task. |
290
+ | `end_time` | `datetime \| None` | When the task completed or failed. |
291
+ | `result` | `any \| None` | Return value of the task on success. `None` when the task raised an ignored exception. |
292
+ | `traceback` | `str \| None` | Full traceback string on failure, or on success when an ignored exception was raised. `None` on clean success. |
293
+
294
+ ---
295
+
296
+ ## Logging
297
+
298
+ Import `task_logger` to emit log records that are automatically prefixed with the active `invocation_id`. This makes it straightforward to filter all logs for a specific task invocation in CloudWatch Logs Insights.
299
+
300
+ ```python
301
+ from lambda_tasks.logging import task_logger
302
+ from lambda_tasks.decorators import lambda_task
303
+
304
+ @lambda_task(soft_timeout=60, hard_timeout=90)
305
+ def send_welcome_email(*, user_id: int, template: str) -> str:
306
+ task_logger.info("sending email to user %s", user_id)
307
+ # → "[abc-123] sending email to user 42"
308
+ return "sent"
309
+ ```
310
+
311
+ `task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `invocation_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
312
+
313
+ Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `invocation_id` prefix.
314
+
315
+ To filter by invocation in CloudWatch Logs Insights:
316
+
317
+ ```
318
+ fields @timestamp, @message
319
+ | filter @message like "[your-invocation-id]"
320
+ | sort @timestamp asc
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Ignored exceptions
326
+
327
+ Pass a tuple of exception types to `ignore_errors` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor treats it as a non-fatal outcome:
328
+
329
+ - `TaskRecord.status` is set to `SUCCESS`
330
+ - The exception traceback is saved to `TaskRecord.traceback` for observability
331
+ - Task-side ORM writes inside the `transaction.atomic()` block are still rolled back
332
+ - The `TaskRecord` update is committed outside the atomic block
333
+
334
+ Exceptions not in `ignore_errors` continue to produce `FAILED` with a rollback. The default (`()`) preserves existing behaviour.
335
+
336
+ ```python
337
+ from lambda_tasks.decorators import lambda_task
338
+
339
+ class RecordNotFound(Exception):
340
+ pass
341
+
342
+ @lambda_task(ignore_errors=(RecordNotFound,))
343
+ def sync_user(*, user_id: int) -> None:
344
+ user = fetch_user(user_id) # raises RecordNotFound if already deleted
345
+ update_profile(user)
346
+ ```
347
+
348
+ If `sync_user` raises `RecordNotFound`, the `TaskRecord` will have `status=SUCCESS` and the traceback recorded in `traceback`. Any ORM writes made before the exception are rolled back.
349
+
350
+ `ignore_errors` is validated at decoration time — passing a non-exception type raises `TypeError` immediately. It is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
351
+
352
+ ---
353
+
354
+ ## Atomicity
355
+
356
+ Each task runs inside a `django.db.transaction.atomic` block. If the task raises an unhandled exception, all ORM writes made inside the task are rolled back. The `TaskRecord` failure update is always written outside the atomic block so it survives the rollback.
357
+
358
+ When an exception matches `ignore_errors`, the same rollback applies to task-side writes — the atomic block still exits via exception. Only the `TaskRecord` update (written outside the block) is committed, recording the `SUCCESS` outcome and traceback.
359
+
360
+ ---
361
+
362
+ ## Error handling
363
+
364
+ | Scenario | Exception | When |
365
+ |---|---|---|
366
+ | Task function has positional parameters | `TypeError` | At decoration time |
367
+ | `soft_timeout >= hard_timeout` | `ValueError` | At decoration time |
368
+ | Timeout value exceeds 900 seconds | `ValueError` | At decoration time or settings load |
369
+ | Unknown queue name | `ImproperlyConfigured` | At `.execute_on_commit()` |
370
+ | `LAMBDA_TASKS_QUEUES` not set | `ImproperlyConfigured` | On first settings access |
371
+ | `LAMBDA_TASKS_QUEUES` missing `"default"` | `ImproperlyConfigured` | On first settings access |
372
+ | SQS `send_message` failure | boto3 exception (propagated) | At `.execute_on_commit()` |
373
+
374
+ ---
375
+
376
+ ## AWS Lambda and SQS setup
377
+
378
+ ### SQS queue configuration
379
+
380
+ Each queue in `LAMBDA_TASKS_QUEUES` should be a standard SQS queue (not FIFO) with the following recommended settings:
381
+
382
+ - **Visibility timeout** — set this higher than your Lambda function's `hard_timeout`. If the Lambda times out before SQS receives a deletion confirmation, the message becomes visible again and will be redelivered.
383
+ - **Dead letter queue (DLQ)** — configure a DLQ to capture messages that cannot be processed. Without one, messages that exhaust retries are silently deleted.
384
+ - **`maxReceiveCount`** — controls how many times a message is retried before being moved to the DLQ. A value of `1` is recommended: task failures are recorded as `FAILED` `TaskRecord` entries and are not retried by design, so the only messages that reach the DLQ are pre-execution failures (malformed message body, import errors, misconfiguration). These are unlikely to succeed on automatic retry and are better handled by fixing the underlying issue and manually redriving from the DLQ.
385
+
386
+ ### Lambda trigger
387
+
388
+ Configure the SQS queue as an event source trigger for your Lambda function. AWS will invoke the handler with batches of messages and use the `batchItemFailures` response to determine which messages to return to the queue.
389
+
390
+ Only pre-execution failures (e.g. a malformed message or an import error) are reported as `batchItemFailures`. Task logic failures are caught by `SQSLambdaTaskMessage.execute_immediately()`, recorded in `TaskRecord`, and treated as successful from SQS's perspective — they will not be redelivered.
391
+
392
+ ### Lambda handler
393
+
394
+ Point your Lambda function's handler at:
395
+
396
+ ```
397
+ lambda_tasks.handler.handler
398
+ ```
399
+
400
+ 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).
401
+
402
+ ### Resolving Django settings from AWS Secrets Manager
403
+
404
+ The Lambda handler supports loading secret values from AWS Secrets Manager into the environment before Django starts. This lets your Django settings file read from `os.environ` as normal while keeping secrets out of plaintext environment variables.
405
+
406
+ Set any env var with the prefix `AWS_SECRETS_MANAGER_` to a full Secrets Manager dynamic reference. The unprefixed name becomes the target env var:
407
+
408
+ ```
409
+ AWS_SECRETS_MANAGER_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
410
+ ```
411
+
412
+ At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
413
+
414
+ 1. Scans all env vars for the `AWS_SECRETS_MANAGER_` prefix
415
+ 2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
416
+ 3. Groups references by `(ARN, version-stage, version-id)` and makes one `GetSecretValue` call per unique combination
417
+ 4. Extracts the named JSON key from the secret and writes it into `os.environ`
418
+ 5. Caches fetched secrets in-process — warm invocations pay no extra cost
419
+
420
+ #### Reference format
421
+
422
+ Every value must follow the full dynamic reference syntax:
423
+
424
+ ```
425
+ <arn>:<json-key>:<version-stage>:<version-id>
426
+ ```
427
+
428
+ All four fields are required and must be non-empty. The secret value must be a JSON object; `json-key` names the field to extract.
429
+
430
+ ```
431
+ # arn (7 segments) : json-key : version-stage : version-id
432
+ arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
433
+ ```
434
+
435
+ Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-stage, version-id)` combination:
436
+
437
+ ```
438
+ AWS_SECRETS_MANAGER_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL:AWSCURRENT:v1
439
+ AWS_SECRETS_MANAGER_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
440
+ ```
441
+
442
+ #### Validation errors
443
+
444
+ The following all raise `ValueError` at cold start, preventing the Lambda container from starting with a misconfigured environment:
445
+
446
+ - Wrong number of colon-separated segments (must be exactly 10)
447
+ - Empty `json-key`, `version-stage`, or `version-id`
448
+ - Both `AWS_SECRETS_MANAGER_FOO` and `FOO` are set — use one or the other
449
+ - The named JSON key does not exist in the fetched secret
450
+ - The secret value is not valid JSON
451
+
452
+ ---
453
+
454
+ ## Direct (synchronous) invocation
455
+
456
+ You can call a decorated task directly like a normal function — useful in tests or management commands where you want synchronous execution without going through SQS:
457
+
458
+ ```python
459
+ result = send_welcome_email(user_id=1, template="welcome")
460
+ ```
461
+
462
+ This bypasses the queue entirely and runs the function in the current process and transaction.
@@ -0,0 +1,15 @@
1
+ lambda_tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ lambda_tasks/admin.py,sha256=fWqKdUlg0gOzDTorDyNDN1sZSyBjshOSTxsXs1K8two,1208
3
+ lambda_tasks/apps.py,sha256=WwIpa2eQQujLgdAq1HXVRSwIppRej14O9oU3NFygN6g,186
4
+ lambda_tasks/decorators.py,sha256=euPBR6C9sbrnsGmgYtREcb9ptpoVwn4YLhQpbH6Fs00,13019
5
+ lambda_tasks/handler.py,sha256=JmPFW-vaoiFQM5TqDXMkfIiygHM9Hbxe-7HD5Jwa54g,1620
6
+ lambda_tasks/logging.py,sha256=JAp_CE7kDTYiRd-PB4zRXD1liIBHX_hzZm1mKvx2s9o,1038
7
+ lambda_tasks/models.py,sha256=t8MnIkV_wyK5JCCRShpLAjPPEcSUnzALT3rMeZELooQ,8886
8
+ lambda_tasks/secret_loader.py,sha256=bOZxT-EvoFogkKsIrt1lVQtbzzaErTKkLHtA3pjlCH0,6400
9
+ lambda_tasks/settings.py,sha256=4l_ZzP8JL1HBg4n4pv9sXyAT31BbR4hSES4lzg9-AOQ,2290
10
+ lambda_tasks/timeouts.py,sha256=vO-0-gmcRPhMbWRmshd2TM5dy_RCdI6oW_WpR50ohNI,2593
11
+ lambda_tasks/migrations/0001_initial.py,sha256=jAOVj4DtpQmfcaxElearNd603w0ltu6JmewFN20AkK8,2727
12
+ lambda_tasks/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ django_lambda_tasks-0.1.0.dist-info/METADATA,sha256=sh5G2oxZWcS3ZwZ0OBcBe_yhzz_Is-8aM_sAP6huozs,17467
14
+ django_lambda_tasks-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
15
+ django_lambda_tasks-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
File without changes
lambda_tasks/admin.py ADDED
@@ -0,0 +1,42 @@
1
+ """Admin registration for TaskRecord."""
2
+
3
+ from django.contrib import admin
4
+ from django.db.models import DurationField, ExpressionWrapper, F, QuerySet
5
+ from django.http import HttpRequest
6
+
7
+ from lambda_tasks.models import TaskRecord
8
+
9
+
10
+ @admin.register(TaskRecord)
11
+ class TaskRecordAdmin(admin.ModelAdmin):
12
+ list_display = (
13
+ "task_name",
14
+ "status",
15
+ "start_time",
16
+ "end_time",
17
+ "n_retries",
18
+ "duration",
19
+ "result",
20
+ )
21
+ list_filter = ("status", "task_name")
22
+ date_hierarchy = "start_time"
23
+ search_fields = ("invocation_id", "kwargs")
24
+
25
+ def get_queryset(self, request: HttpRequest) -> QuerySet:
26
+ return (
27
+ super()
28
+ .get_queryset(request)
29
+ .annotate(
30
+ duration=ExpressionWrapper(
31
+ F("end_time") - F("start_time"), output_field=DurationField()
32
+ )
33
+ )
34
+ )
35
+
36
+ @admin.display(description="Duration", ordering="duration")
37
+ def duration(self, obj: TaskRecord) -> str | None:
38
+ d = getattr(obj, "duration", None)
39
+ if d is None:
40
+ return None
41
+ total_seconds = d.total_seconds()
42
+ return f"{total_seconds:.3f}s"
lambda_tasks/apps.py ADDED
@@ -0,0 +1,8 @@
1
+ """Django AppConfig for the lambda_tasks library."""
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class LambdaTasksConfig(AppConfig):
7
+ name = "lambda_tasks"
8
+ verbose_name = "Lambda Tasks"