django-lambda-tasks 0.1.4__tar.gz → 0.1.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 (87) hide show
  1. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/steering/product.md +1 -1
  2. django_lambda_tasks-0.1.4/README.md → django_lambda_tasks-0.1.5/PKG-INFO +12 -20
  3. django_lambda_tasks-0.1.4/PKG-INFO → django_lambda_tasks-0.1.5/README.md +0 -31
  4. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/decorators.py +17 -3
  5. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/handler.py +1 -2
  6. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/pyproject.toml +2 -1
  7. django_lambda_tasks-0.1.5/tests/test_timeout_validation.py +170 -0
  8. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.github/workflows/ci.yml +0 -0
  9. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.github/workflows/release.yml +0 -0
  10. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.gitignore +0 -0
  11. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  12. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  13. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  14. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  15. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  16. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  17. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  18. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  19. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  20. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  21. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  22. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  23. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  24. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  25. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  26. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  27. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/retry-delay/.config.kiro +0 -0
  28. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/retry-delay/design.md +0 -0
  29. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/retry-delay/requirements.md +0 -0
  30. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/retry-delay/tasks.md +0 -0
  31. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  32. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/design.md +0 -0
  33. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  34. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  35. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  36. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  37. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  38. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  39. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/singleton-task/.config.kiro +0 -0
  40. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/singleton-task/design.md +0 -0
  41. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/singleton-task/requirements.md +0 -0
  42. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/singleton-task/tasks.md +0 -0
  43. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/.config.kiro +0 -0
  44. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/design.md +0 -0
  45. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/requirements.md +0 -0
  46. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/tasks.md +0 -0
  47. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/steering/structure.md +0 -0
  48. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.kiro/steering/tech.md +0 -0
  49. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.pre-commit-config.yaml +0 -0
  50. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/.vscode/settings.json +0 -0
  51. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/README.md +0 -0
  52. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_app/__init__.py +0 -0
  53. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_app/apps.py +0 -0
  54. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_app/tasks.py +0 -0
  55. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_app/urls.py +0 -0
  56. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_app/views.py +0 -0
  57. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_project/__init__.py +0 -0
  58. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_project/settings.py +0 -0
  59. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_project/urls.py +0 -0
  60. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/example_project/wsgi.py +0 -0
  61. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/example/manage.py +0 -0
  62. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/__init__.py +0 -0
  63. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/admin.py +0 -0
  64. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/apps.py +0 -0
  65. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/logging.py +0 -0
  66. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/migrations/0001_initial.py +0 -0
  67. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/migrations/__init__.py +0 -0
  68. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/models.py +0 -0
  69. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/secret_loader.py +0 -0
  70. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/settings.py +0 -0
  71. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/tasks.py +0 -0
  72. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/lambda_tasks/timeouts.py +0 -0
  73. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/conftest.py +0 -0
  74. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/settings.py +0 -0
  75. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_admin.py +0 -0
  76. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_decorator.py +0 -0
  77. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_decorators.py +0 -0
  78. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_deferred_enqueue.py +0 -0
  79. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_handler.py +0 -0
  80. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_kwargs_only.py +0 -0
  81. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_logging.py +0 -0
  82. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_models.py +0 -0
  83. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_secret_loader.py +0 -0
  84. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_serializer.py +0 -0
  85. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_settings.py +0 -0
  86. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_tasks.py +0 -0
  87. {django_lambda_tasks-0.1.4 → django_lambda_tasks-0.1.5}/tests/test_timeouts.py +0 -0
@@ -178,7 +178,7 @@ Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
178
178
  | `LAMBDA_TASKS_MAX_RETRIES` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised (60 × 24 × 2) |
179
179
  | `LAMBDA_TASKS_SINGLETON_CACHE` | `"default"` | Django cache backend used for singleton task locks |
180
180
 
181
- `LAMBDA_TASKS_QUEUES` must be set and include a `"default"` key. `soft_timeout` must always be strictly less than `hard_timeout`.
181
+ `LAMBDA_TASKS_QUEUES` must be set and include a `"default"` key. Both timeout values must be greater than zero and at most `900` seconds. `soft_timeout` must always be strictly less than `hard_timeout`.
182
182
 
183
183
  ## Eager Mode
184
184
 
@@ -1,3 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-lambda-tasks
3
+ Version: 0.1.5
4
+ Summary: Run async tasks in a lambda function
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: awslambdaric
7
+ Requires-Dist: boto3
8
+ Requires-Dist: django
9
+ Requires-Dist: pydantic
10
+ Requires-Dist: redis
11
+ Description-Content-Type: text/markdown
12
+
1
13
  # Django Lambda Tasks
2
14
 
3
15
  A Django library for offloading work to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler that AWS invokes with SQS message batches. Task results, status, and metadata are persisted in the Django database.
@@ -162,24 +174,6 @@ def my_task(*, arg: str) -> None:
162
174
 
163
175
  ---
164
176
 
165
- ## Per-invocation overrides
166
-
167
- Pass override kwargs prefixed with `_` to `.execute_on_commit()` to customise a single invocation:
168
-
169
- ```python
170
- send_welcome_email.execute_on_commit(
171
- user_id=42,
172
- template="welcome",
173
- _delay=30, # SQS message visibility delay in seconds
174
- )
175
- ```
176
-
177
- | Override | Type | Description |
178
- |---|---|---|
179
- | `_delay` | `int` | SQS message delay in seconds before the worker can pick it up. |
180
-
181
- ---
182
-
183
177
  ## Serializing a task invocation
184
178
 
185
179
  `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.
@@ -196,8 +190,6 @@ payload = send_welcome_email.serialize(user_id=42, template="welcome")
196
190
  # }
197
191
  ```
198
192
 
199
- Per-invocation overrides (`_delay`) are accepted the same way as in `execute_on_commit()`.
200
-
201
193
  The returned dict matches the `SQSLambdaTask` schema. To reconstruct and enqueue it later:
202
194
 
203
195
  ```python
@@ -1,14 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: django-lambda-tasks
3
- Version: 0.1.4
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
- Requires-Dist: redis
10
- Description-Content-Type: text/markdown
11
-
12
1
  # Django Lambda Tasks
13
2
 
14
3
  A Django library for offloading work to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler that AWS invokes with SQS message batches. Task results, status, and metadata are persisted in the Django database.
@@ -173,24 +162,6 @@ def my_task(*, arg: str) -> None:
173
162
 
174
163
  ---
175
164
 
176
- ## Per-invocation overrides
177
-
178
- Pass override kwargs prefixed with `_` to `.execute_on_commit()` to customise a single invocation:
179
-
180
- ```python
181
- send_welcome_email.execute_on_commit(
182
- user_id=42,
183
- template="welcome",
184
- _delay=30, # SQS message visibility delay in seconds
185
- )
186
- ```
187
-
188
- | Override | Type | Description |
189
- |---|---|---|
190
- | `_delay` | `int` | SQS message delay in seconds before the worker can pick it up. |
191
-
192
- ---
193
-
194
165
  ## Serializing a task invocation
195
166
 
196
167
  `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.
@@ -207,8 +178,6 @@ payload = send_welcome_email.serialize(user_id=42, template="welcome")
207
178
  # }
208
179
  ```
209
180
 
210
- Per-invocation overrides (`_delay`) are accepted the same way as in `execute_on_commit()`.
211
-
212
181
  The returned dict matches the `SQSLambdaTask` schema. To reconstruct and enqueue it later:
213
182
 
214
183
  ```python
@@ -101,7 +101,9 @@ class LambdaTaskWrapper:
101
101
  """Return (soft_timeout, hard_timeout) resolved against settings defaults.
102
102
 
103
103
  Merges decorator-supplied values with settings defaults, then validates
104
- the final pair. Result is cached after first access.
104
+ the final pair. Each resolved value must be greater than zero and at
105
+ most ``MAX_TIMEOUT`` (900), and ``soft_timeout`` must be strictly less
106
+ than ``hard_timeout``. Result is cached after first access.
105
107
  """
106
108
  try:
107
109
  return self._resolved_timeouts_cache
@@ -120,11 +122,15 @@ class LambdaTaskWrapper:
120
122
  else conf.DEFAULT_HARD_TIMEOUT
121
123
  )
122
124
 
123
- # Validate settings-sourced values against the cap (decorator values are checked at decoration time)
125
+ # Validate settings-sourced values (decorator values are already checked at decoration time)
124
126
  for name, value, source in (
125
127
  ("soft_timeout", soft, self._soft_timeout),
126
128
  ("hard_timeout", hard, self._hard_timeout),
127
129
  ):
130
+ if source is None and value <= 0:
131
+ raise ValueError(
132
+ f"{name} ({value}) from settings must be greater than zero."
133
+ )
128
134
  if source is None and value > MAX_TIMEOUT:
129
135
  raise ValueError(
130
136
  f"{name} ({value}) from settings exceeds the maximum allowed value of {MAX_TIMEOUT} seconds."
@@ -313,15 +319,23 @@ class LambdaTaskWrapper:
313
319
  def _validate_timeouts(
314
320
  *, soft_timeout: int | None, hard_timeout: int | None
315
321
  ) -> None:
316
- """Raise ValueError if any timeout exceeds 900s or soft_timeout >= hard_timeout."""
322
+ """Raise ValueError if any timeout is not in (0, 900] or soft_timeout >= hard_timeout.
323
+
324
+ Each timeout, when supplied, must be greater than zero and at most
325
+ ``MAX_TIMEOUT`` (900). When both are supplied, ``soft_timeout`` must
326
+ be strictly less than ``hard_timeout``.
327
+ """
317
328
  for name, value in (
318
329
  ("soft_timeout", soft_timeout),
319
330
  ("hard_timeout", hard_timeout),
320
331
  ):
332
+ if value is not None and value <= 0:
333
+ raise ValueError(f"{name} ({value}) must be greater than zero.")
321
334
  if value is not None and value > MAX_TIMEOUT:
322
335
  raise ValueError(
323
336
  f"{name} ({value}) exceeds the maximum allowed value of {MAX_TIMEOUT} seconds."
324
337
  )
338
+
325
339
  if soft_timeout is not None and hard_timeout is not None:
326
340
  if soft_timeout >= hard_timeout:
327
341
  raise ValueError(
@@ -43,8 +43,7 @@ def handler(*, event: dict, context: object) -> dict:
43
43
  ).execute_immediately(message_id=record["messageId"])
44
44
  except Exception:
45
45
  logger.error(
46
- "Failed to process SQS record %s",
47
- record.get("messageId"),
46
+ "Failed to process SQS record",
48
47
  exc_info=True,
49
48
  )
50
49
  batch_item_failures.append({"itemIdentifier": record["messageId"]})
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.1.4"
10
+ version = "0.1.5"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -16,6 +16,7 @@ dependencies = [
16
16
  "django",
17
17
  "pydantic",
18
18
  "redis",
19
+ "awslambdaric",
19
20
  ]
20
21
 
21
22
  [dependency-groups]
@@ -0,0 +1,170 @@
1
+ """
2
+ Tests for timeout > 0 validation in decorators and resolved_timeouts.
3
+
4
+ Covers:
5
+ - Decorator-time rejection of zero and negative timeouts
6
+ - Settings-sourced rejection of zero and negative timeouts via resolved_timeouts
7
+ - Property-based: any positive timeout pair with soft < hard <= 900 is accepted
8
+ """
9
+
10
+ import pytest
11
+ from hypothesis import HealthCheck, given
12
+ from hypothesis import settings as h_settings
13
+ from hypothesis import strategies as st
14
+
15
+ from lambda_tasks.decorators import LambdaTaskWrapper
16
+
17
+
18
+ def _make_func():
19
+ def _task(*, x: int) -> None:
20
+ pass
21
+
22
+ return _task
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Decorator-time: zero timeouts rejected
27
+ # ---------------------------------------------------------------------------
28
+
29
+
30
+ def test_soft_timeout_zero_raises_value_error():
31
+ """soft_timeout=0 raises ValueError at decoration time."""
32
+ with pytest.raises(ValueError, match="soft_timeout"):
33
+ LambdaTaskWrapper(_make_func(), soft_timeout=0, hard_timeout=10)
34
+
35
+
36
+ def test_hard_timeout_zero_raises_value_error():
37
+ """hard_timeout=0 raises ValueError at decoration time."""
38
+ with pytest.raises(ValueError, match="hard_timeout"):
39
+ LambdaTaskWrapper(_make_func(), soft_timeout=None, hard_timeout=0)
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Decorator-time: negative timeouts rejected
44
+ # ---------------------------------------------------------------------------
45
+
46
+
47
+ def test_soft_timeout_negative_raises_value_error():
48
+ """soft_timeout < 0 raises ValueError at decoration time."""
49
+ with pytest.raises(ValueError, match="soft_timeout"):
50
+ LambdaTaskWrapper(_make_func(), soft_timeout=-1, hard_timeout=10)
51
+
52
+
53
+ def test_hard_timeout_negative_raises_value_error():
54
+ """hard_timeout < 0 raises ValueError at decoration time."""
55
+ with pytest.raises(ValueError, match="hard_timeout"):
56
+ LambdaTaskWrapper(_make_func(), hard_timeout=-5)
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Decorator-time: boundary — timeout=1 is the minimum accepted value
61
+ # ---------------------------------------------------------------------------
62
+
63
+
64
+ def test_soft_timeout_one_is_accepted():
65
+ """soft_timeout=1 is the minimum valid value."""
66
+ wrapper = LambdaTaskWrapper(_make_func(), soft_timeout=1, hard_timeout=2)
67
+ assert wrapper._soft_timeout == 1
68
+
69
+
70
+ def test_hard_timeout_one_is_accepted():
71
+ """hard_timeout=1 is the minimum valid value (soft left to settings)."""
72
+ wrapper = LambdaTaskWrapper(_make_func(), hard_timeout=1)
73
+ assert wrapper._hard_timeout == 1
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # resolved_timeouts: settings-sourced zero rejected
78
+ # ---------------------------------------------------------------------------
79
+
80
+
81
+ def test_resolved_soft_timeout_zero_from_settings_raises_value_error(settings):
82
+ """soft_timeout=0 from settings raises ValueError on resolved_timeouts."""
83
+ settings.LAMBDA_TASKS_QUEUES = {"default": "https://sqs.example.com/default"}
84
+ settings.LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 0
85
+ settings.LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 300
86
+
87
+ wrapper = LambdaTaskWrapper(_make_func())
88
+ with pytest.raises(ValueError, match="soft_timeout"):
89
+ _ = wrapper.resolved_timeouts
90
+
91
+
92
+ def test_resolved_hard_timeout_zero_from_settings_raises_value_error(settings):
93
+ """hard_timeout=0 from settings raises ValueError on resolved_timeouts."""
94
+ settings.LAMBDA_TASKS_QUEUES = {"default": "https://sqs.example.com/default"}
95
+ settings.LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 0
96
+ settings.LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 0
97
+
98
+ wrapper = LambdaTaskWrapper(_make_func())
99
+ with pytest.raises(ValueError, match="timeout"):
100
+ _ = wrapper.resolved_timeouts
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # resolved_timeouts: settings-sourced negative rejected
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ def test_resolved_soft_timeout_negative_from_settings_raises_value_error(settings):
109
+ """soft_timeout < 0 from settings raises ValueError on resolved_timeouts."""
110
+ settings.LAMBDA_TASKS_QUEUES = {"default": "https://sqs.example.com/default"}
111
+ settings.LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = -10
112
+ settings.LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 300
113
+
114
+ wrapper = LambdaTaskWrapper(_make_func())
115
+ with pytest.raises(ValueError, match="soft_timeout"):
116
+ _ = wrapper.resolved_timeouts
117
+
118
+
119
+ def test_resolved_hard_timeout_negative_from_settings_raises_value_error(settings):
120
+ """hard_timeout < 0 from settings raises ValueError on resolved_timeouts."""
121
+ settings.LAMBDA_TASKS_QUEUES = {"default": "https://sqs.example.com/default"}
122
+ settings.LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = -10
123
+ settings.LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = -5
124
+
125
+ wrapper = LambdaTaskWrapper(_make_func())
126
+ with pytest.raises(ValueError, match="timeout"):
127
+ _ = wrapper.resolved_timeouts
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Property-based: valid timeout pairs (both > 0, soft < hard, hard <= 900)
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ _valid_timeout_pair = st.integers(min_value=1, max_value=899).flatmap(
136
+ lambda soft: st.integers(min_value=soft + 1, max_value=900).map(
137
+ lambda hard: (soft, hard)
138
+ )
139
+ )
140
+
141
+
142
+ @given(timeout_pair=_valid_timeout_pair)
143
+ @h_settings(max_examples=100, suppress_health_check=[HealthCheck.too_slow])
144
+ def test_property_valid_timeout_pair_accepted(timeout_pair):
145
+ """Any (soft, hard) with 1 <= soft < hard <= 900 is accepted at decoration time."""
146
+ soft, hard = timeout_pair
147
+ wrapper = LambdaTaskWrapper(_make_func(), soft_timeout=soft, hard_timeout=hard)
148
+ assert wrapper._soft_timeout == soft
149
+ assert wrapper._hard_timeout == hard
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # Property-based: non-positive timeouts always rejected
154
+ # ---------------------------------------------------------------------------
155
+
156
+
157
+ @given(value=st.integers(max_value=0))
158
+ @h_settings(max_examples=100, suppress_health_check=[HealthCheck.too_slow])
159
+ def test_property_non_positive_soft_timeout_rejected(value):
160
+ """Any soft_timeout <= 0 raises ValueError at decoration time."""
161
+ with pytest.raises(ValueError):
162
+ LambdaTaskWrapper(_make_func(), soft_timeout=value, hard_timeout=10)
163
+
164
+
165
+ @given(value=st.integers(max_value=0))
166
+ @h_settings(max_examples=100, suppress_health_check=[HealthCheck.too_slow])
167
+ def test_property_non_positive_hard_timeout_rejected(value):
168
+ """Any hard_timeout <= 0 raises ValueError at decoration time."""
169
+ with pytest.raises(ValueError):
170
+ LambdaTaskWrapper(_make_func(), hard_timeout=value)