django-lambda-tasks 0.2.3__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/steering/product.md +2 -2
  2. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/steering/structure.md +1 -1
  3. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/PKG-INFO +3 -4
  4. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/README.md +2 -3
  5. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/admin.py +3 -3
  6. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/handler.py +17 -26
  7. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/logging.py +1 -1
  8. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/pyproject.toml +1 -1
  9. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_handler.py +82 -38
  10. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.github/workflows/ci.yml +0 -0
  11. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.github/workflows/release.yml +0 -0
  12. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.gitignore +0 -0
  13. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  14. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  15. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  16. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  17. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  18. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  19. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  20. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  21. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  22. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  23. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  24. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  25. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  26. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  27. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  28. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  29. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/retry-delay/.config.kiro +0 -0
  30. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/retry-delay/design.md +0 -0
  31. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/retry-delay/requirements.md +0 -0
  32. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/retry-delay/tasks.md +0 -0
  33. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  34. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/rse-background-tasks/design.md +0 -0
  35. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  36. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  37. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  38. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  39. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  40. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  41. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/singleton-task/.config.kiro +0 -0
  42. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/singleton-task/design.md +0 -0
  43. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/singleton-task/requirements.md +0 -0
  44. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/singleton-task/tasks.md +0 -0
  45. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  46. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  47. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  48. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  49. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/task-retry/.config.kiro +0 -0
  50. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/task-retry/design.md +0 -0
  51. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/task-retry/requirements.md +0 -0
  52. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/specs/task-retry/tasks.md +0 -0
  53. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.kiro/steering/tech.md +0 -0
  54. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.pre-commit-config.yaml +0 -0
  55. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/.vscode/settings.json +0 -0
  56. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/README.md +0 -0
  57. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_app/__init__.py +0 -0
  58. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_app/apps.py +0 -0
  59. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_app/tasks.py +0 -0
  60. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_app/urls.py +0 -0
  61. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_app/views.py +0 -0
  62. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_project/__init__.py +0 -0
  63. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_project/settings.py +0 -0
  64. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_project/urls.py +0 -0
  65. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/example_project/wsgi.py +0 -0
  66. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/example/manage.py +0 -0
  67. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/__init__.py +0 -0
  68. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/apps.py +0 -0
  69. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/decorators.py +0 -0
  70. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/environment_loader.py +0 -0
  71. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/migrations/0001_initial.py +0 -0
  72. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/migrations/__init__.py +0 -0
  73. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/models.py +0 -0
  74. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/secret_loader.py +0 -0
  75. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/settings.py +0 -0
  76. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/tasks.py +0 -0
  77. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/lambda_tasks/timeouts.py +0 -0
  78. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/conftest.py +0 -0
  79. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/settings.py +0 -0
  80. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_admin.py +0 -0
  81. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_decorator.py +0 -0
  82. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_decorators.py +0 -0
  83. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_deferred_enqueue.py +0 -0
  84. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_environment_loader.py +0 -0
  85. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_kwargs_only.py +0 -0
  86. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_logging.py +0 -0
  87. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_models.py +0 -0
  88. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_secret_loader.py +0 -0
  89. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_serializer.py +0 -0
  90. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_settings.py +0 -0
  91. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_tasks.py +0 -0
  92. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_timeout_validation.py +0 -0
  93. {django_lambda_tasks-0.2.3 → django_lambda_tasks-0.2.5}/tests/test_timeouts.py +0 -0
@@ -135,10 +135,10 @@ class SQSLambdaTaskMessage(BaseModel):
135
135
  - Returns `{"batchItemFailures": [...]}` for partial-batch failure reporting
136
136
  - Only pre-execution failures (malformed message, import error, misconfiguration) are reported as `batchItemFailures` — task logic failures are caught and recorded as `FAILED` TaskRecords without raising
137
137
  - Recommended SQS queue settings: `maxReceiveCount=1` with a DLQ configured; automatic retries are not useful since task failures are not re-driven by design
138
- - Cold-start sequence runs inside the handler on the first invocation (not at module import time) to avoid Lambda init-duration timeouts: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()` → `_configure_logging()`
138
+ - 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
139
  - A module-level `_cold_start_done` sentinel ensures the sequence runs only once; subsequent warm invocations skip it
140
140
  - Both loaders run unconditionally (outside the `DJANGO_SETTINGS_MODULE` check) — the environment secret may provide that var, and individual secrets may depend on environment-loaded vars
141
- - `_configure_logging()` sets the `lambda_tasks` logger hierarchy to `INFO` (or the level specified by `LAMBDA_TASKS_LOG_LEVEL` env var) so that `task_logger` output appears in CloudWatch
141
+ - 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
142
142
 
143
143
  ## Environment Loader
144
144
 
@@ -40,7 +40,7 @@ django-lambda-tasks/
40
40
 
41
41
  - `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
42
42
  - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` publishes to SQS or executes eagerly; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
43
- - `handler.py` — Lambda entry point; cold-start init (`resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`
43
+ - `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`
44
44
  - `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
45
45
  - `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
46
46
  - `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -320,7 +320,7 @@ def send_welcome_email(*, user_id: int, template: str) -> str:
320
320
 
321
321
  `task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `message_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
322
322
 
323
- The Lambda handler configures the `lambda_tasks` logger hierarchy to `INFO` at cold start so that `task_logger` lines appear in CloudWatch. You can override the level by setting the `LAMBDA_TASKS_LOG_LEVEL` environment variable on your Lambda function (e.g. `DEBUG`, `WARNING`).
323
+ The Lambda handler configures the `lambda_tasks` logger hierarchy to `INFO` at cold start so that `task_logger` lines appear in CloudWatch. You can control the level by configuring the `lambda_tasks` logger in your Django `LOGGING` setting (e.g. set it to `DEBUG` or `WARNING`). If your `LOGGING` dictConfig sets a root logger with a handler (as most Django projects do), `lambda_tasks` will inherit from it via propagation.
324
324
 
325
325
  Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `message_id` prefix.
326
326
 
@@ -460,14 +460,13 @@ Point your Lambda function's handler at:
460
460
  lambda_tasks.handler.handler
461
461
  ```
462
462
 
463
- The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
463
+ The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: temporary log handler attached → `resolve_environment()` → `resolve_secrets_into_env()` → log handler removed → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
464
464
 
465
465
  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).
466
466
 
467
467
  | Environment Variable | Required | Description |
468
468
  |---|---|---|
469
469
  | `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
470
- | `LAMBDA_TASKS_LOG_LEVEL` | No | Log level for the `lambda_tasks` logger hierarchy (default `INFO`). Set to `DEBUG`, `WARNING`, etc. as needed. |
471
470
  | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start (runs first). |
472
471
  | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
473
472
 
@@ -308,7 +308,7 @@ def send_welcome_email(*, user_id: int, template: str) -> str:
308
308
 
309
309
  `task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `message_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
310
310
 
311
- The Lambda handler configures the `lambda_tasks` logger hierarchy to `INFO` at cold start so that `task_logger` lines appear in CloudWatch. You can override the level by setting the `LAMBDA_TASKS_LOG_LEVEL` environment variable on your Lambda function (e.g. `DEBUG`, `WARNING`).
311
+ The Lambda handler configures the `lambda_tasks` logger hierarchy to `INFO` at cold start so that `task_logger` lines appear in CloudWatch. You can control the level by configuring the `lambda_tasks` logger in your Django `LOGGING` setting (e.g. set it to `DEBUG` or `WARNING`). If your `LOGGING` dictConfig sets a root logger with a handler (as most Django projects do), `lambda_tasks` will inherit from it via propagation.
312
312
 
313
313
  Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `message_id` prefix.
314
314
 
@@ -448,14 +448,13 @@ Point your Lambda function's handler at:
448
448
  lambda_tasks.handler.handler
449
449
  ```
450
450
 
451
- The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
451
+ The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: temporary log handler attached → `resolve_environment()` → `resolve_secrets_into_env()` → log handler removed → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
452
452
 
453
453
  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).
454
454
 
455
455
  | Environment Variable | Required | Description |
456
456
  |---|---|---|
457
457
  | `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
458
- | `LAMBDA_TASKS_LOG_LEVEL` | No | Log level for the `lambda_tasks` logger hierarchy (default `INFO`). Set to `DEBUG`, `WARNING`, etc. as needed. |
459
458
  | `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start (runs first). |
460
459
  | `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
461
460
 
@@ -28,15 +28,15 @@ class TaskRecordAdmin(admin.ModelAdmin):
28
28
  super()
29
29
  .get_queryset(request)
30
30
  .annotate(
31
- duration=ExpressionWrapper(
31
+ computed_duration=ExpressionWrapper(
32
32
  F("end_time") - F("start_time"), output_field=DurationField()
33
33
  )
34
34
  )
35
35
  )
36
36
 
37
- @admin.display(description="Duration", ordering="duration")
37
+ @admin.display(description="Duration", ordering="computed_duration")
38
38
  def duration(self, obj: TaskRecord) -> str | None:
39
- d = getattr(obj, "duration", None)
39
+ d = getattr(obj, "computed_duration", None)
40
40
  if d is None:
41
41
  return None
42
42
  total_seconds = d.total_seconds()
@@ -32,45 +32,36 @@ def _perform_cold_start() -> None:
32
32
  Both loaders are idempotent and run unconditionally — the environment
33
33
  secret may provide DJANGO_SETTINGS_MODULE, and individual secrets may
34
34
  depend on environment-loaded vars.
35
+
36
+ A temporary StreamHandler is attached to the ``lambda_tasks`` logger for
37
+ the duration of the loaders so their log output is visible before Django's
38
+ LOGGING dictConfig has run. It is removed immediately after so that
39
+ Django's configuration is the sole authority on logging from that point on.
35
40
  """
36
41
  global _cold_start_done
37
42
 
38
43
  if _cold_start_done:
39
44
  return
40
45
 
41
- resolve_environment()
42
- resolve_secrets_into_env()
46
+ lambda_tasks_logger = logging.getLogger(__package__)
47
+ boot_handler = logging.StreamHandler()
48
+ boot_handler.setLevel(logging.DEBUG)
49
+ lambda_tasks_logger.addHandler(boot_handler)
50
+ lambda_tasks_logger.setLevel(logging.DEBUG)
51
+
52
+ try:
53
+ resolve_environment()
54
+ resolve_secrets_into_env()
55
+ finally:
56
+ lambda_tasks_logger.removeHandler(boot_handler)
57
+ lambda_tasks_logger.setLevel(logging.NOTSET)
43
58
 
44
59
  if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
45
60
  django.setup()
46
61
 
47
- _configure_logging()
48
-
49
62
  _cold_start_done = True
50
63
 
51
64
 
52
- def _configure_logging() -> None:
53
- """Ensure the lambda_tasks logger hierarchy emits at INFO so task log lines
54
- appear in CloudWatch.
55
-
56
- The AWS Lambda runtime pre-configures the root logger, but child loggers
57
- default to WARNING unless explicitly configured. If Django's LOGGING
58
- dictConfig has already set a level on the ``lambda_tasks`` logger (i.e. the
59
- user explicitly configured it), we leave it alone. Otherwise we default to
60
- INFO (or the value of the LAMBDA_TASKS_LOG_LEVEL env var).
61
- """
62
- lambda_tasks_logger = logging.getLogger("lambda_tasks")
63
-
64
- # level == NOTSET means nobody (neither dictConfig nor user code) has
65
- # explicitly configured this logger — safe to apply our default.
66
- if lambda_tasks_logger.level != logging.NOTSET:
67
- return
68
-
69
- log_level_name = os.environ.get("LAMBDA_TASKS_LOG_LEVEL", "INFO").upper()
70
- log_level = getattr(logging, log_level_name, logging.INFO)
71
- lambda_tasks_logger.setLevel(log_level)
72
-
73
-
74
65
  def handler(event: dict, context: object) -> dict:
75
66
  """AWS Lambda entry point. Processes a batch of SQS records.
76
67
 
@@ -21,7 +21,7 @@ class _TaskLogger(logging.LoggerAdapter):
21
21
  """LoggerAdapter that prepends [message_id] to every message."""
22
22
 
23
23
  def __init__(self) -> None:
24
- super().__init__(logging.getLogger("lambda_tasks.task"), extra={})
24
+ super().__init__(logging.getLogger(f"{__package__}.task"), extra={})
25
25
  self.message_id: str | None = None
26
26
 
27
27
  def process(
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.2.3"
10
+ version = "0.2.5"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -24,7 +24,7 @@ import pytest
24
24
  from hypothesis import given, settings
25
25
  from hypothesis import strategies as st
26
26
 
27
- from lambda_tasks.handler import _configure_logging, handler
27
+ from lambda_tasks.handler import handler
28
28
 
29
29
  # ---------------------------------------------------------------------------
30
30
  # Signature test
@@ -371,45 +371,89 @@ def test_property_4_cold_start_runs_only_once(monkeypatch):
371
371
 
372
372
 
373
373
  # ---------------------------------------------------------------------------
374
- # _configure_logging tests
374
+ # Cold-start logging tests
375
375
  # ---------------------------------------------------------------------------
376
376
 
377
377
 
378
- class TestConfigureLogging:
379
- def test_sets_lambda_tasks_logger_to_info_by_default(self, monkeypatch):
380
- """Without LAMBDA_TASKS_LOG_LEVEL env var, the lambda_tasks logger is set to INFO."""
381
- monkeypatch.delenv("LAMBDA_TASKS_LOG_LEVEL", raising=False)
382
- logging.getLogger("lambda_tasks").setLevel(logging.NOTSET)
383
- _configure_logging()
384
- assert logging.getLogger("lambda_tasks").level == logging.INFO
385
-
386
- def test_task_logger_effective_level_is_info(self, monkeypatch):
387
- """task_logger (lambda_tasks.task) inherits INFO from the parent."""
388
- monkeypatch.delenv("LAMBDA_TASKS_LOG_LEVEL", raising=False)
389
- logging.getLogger("lambda_tasks").setLevel(logging.NOTSET)
390
- _configure_logging()
391
- assert (
392
- logging.getLogger("lambda_tasks.task").getEffectiveLevel() == logging.INFO
378
+ class TestColdStartLogging:
379
+ def test_loader_logs_are_emitted_during_cold_start(self, monkeypatch, capfd):
380
+ """Loader log output is visible during cold start before Django setup."""
381
+ import lambda_tasks.handler as handler_module
382
+
383
+ monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
384
+
385
+ logged_messages: list[str] = []
386
+
387
+ def spy_resolve_environment():
388
+ logging.getLogger("lambda_tasks.environment_loader").info(
389
+ "env loader message"
390
+ )
391
+
392
+ def spy_resolve_secrets():
393
+ logging.getLogger("lambda_tasks.secret_loader").info(
394
+ "secret loader message"
395
+ )
396
+
397
+ monkeypatch.setattr(
398
+ "lambda_tasks.handler.resolve_environment", spy_resolve_environment
393
399
  )
400
+ monkeypatch.setattr(
401
+ "lambda_tasks.handler.resolve_secrets_into_env", spy_resolve_secrets
402
+ )
403
+
404
+ lambda_tasks_logger = logging.getLogger("lambda_tasks")
405
+ lambda_tasks_logger.handlers.clear()
406
+ lambda_tasks_logger.setLevel(logging.NOTSET)
407
+
408
+ handler_module._cold_start_done = False
409
+ handler_module.handler(event={"Records": []}, context=None)
410
+
411
+ captured = capfd.readouterr()
412
+ assert "env loader message" in captured.err
413
+ assert "secret loader message" in captured.err
414
+
415
+ def test_boot_handler_removed_after_loaders(self, monkeypatch):
416
+ """The temporary handler is removed after the loaders run, leaving
417
+ the logger clean for Django's LOGGING dictConfig."""
418
+ import lambda_tasks.handler as handler_module
419
+
420
+ monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
421
+ monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
422
+ monkeypatch.setattr(
423
+ "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
424
+ )
425
+
426
+ lambda_tasks_logger = logging.getLogger("lambda_tasks")
427
+ lambda_tasks_logger.handlers.clear()
428
+ lambda_tasks_logger.setLevel(logging.NOTSET)
429
+
430
+ handler_module._cold_start_done = False
431
+ handler_module.handler(event={"Records": []}, context=None)
432
+
433
+ assert lambda_tasks_logger.handlers == []
434
+ assert lambda_tasks_logger.level == logging.NOTSET
435
+
436
+ def test_boot_handler_removed_even_on_loader_error(self, monkeypatch):
437
+ """The temporary handler is cleaned up even if a loader raises."""
438
+ import lambda_tasks.handler as handler_module
439
+
440
+ monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
441
+ monkeypatch.setattr(
442
+ "lambda_tasks.handler.resolve_environment",
443
+ lambda: (_ for _ in ()).throw(ValueError("bad ref")),
444
+ )
445
+ monkeypatch.setattr(
446
+ "lambda_tasks.handler.resolve_secrets_into_env", lambda: None
447
+ )
448
+
449
+ lambda_tasks_logger = logging.getLogger("lambda_tasks")
450
+ lambda_tasks_logger.handlers.clear()
451
+ lambda_tasks_logger.setLevel(logging.NOTSET)
452
+
453
+ handler_module._cold_start_done = False
454
+
455
+ with pytest.raises(ValueError, match="bad ref"):
456
+ handler_module.handler(event={"Records": []}, context=None)
394
457
 
395
- def test_respects_lambda_tasks_log_level_env_var(self, monkeypatch):
396
- """LAMBDA_TASKS_LOG_LEVEL env var controls the logger level."""
397
- monkeypatch.setenv("LAMBDA_TASKS_LOG_LEVEL", "DEBUG")
398
- logging.getLogger("lambda_tasks").setLevel(logging.NOTSET)
399
- _configure_logging()
400
- assert logging.getLogger("lambda_tasks").level == logging.DEBUG
401
-
402
- def test_invalid_log_level_falls_back_to_info(self, monkeypatch):
403
- """An invalid LAMBDA_TASKS_LOG_LEVEL value falls back to INFO."""
404
- monkeypatch.setenv("LAMBDA_TASKS_LOG_LEVEL", "NONSENSE")
405
- logging.getLogger("lambda_tasks").setLevel(logging.NOTSET)
406
- _configure_logging()
407
- assert logging.getLogger("lambda_tasks").level == logging.INFO
408
-
409
- def test_does_not_override_explicit_django_logging_config(self, monkeypatch):
410
- """If Django's LOGGING dictConfig already set a level, _configure_logging leaves it alone."""
411
- monkeypatch.delenv("LAMBDA_TASKS_LOG_LEVEL", raising=False)
412
- # Simulate Django dictConfig having set the logger to WARNING
413
- logging.getLogger("lambda_tasks").setLevel(logging.WARNING)
414
- _configure_logging()
415
- assert logging.getLogger("lambda_tasks").level == logging.WARNING
458
+ assert lambda_tasks_logger.handlers == []
459
+ assert lambda_tasks_logger.level == logging.NOTSET