pydocket 0.9.2__tar.gz → 0.11.0__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.

Potentially problematic release.


This version of pydocket might be problematic. Click here for more details.

Files changed (73) hide show
  1. pydocket-0.11.0/.github/workflows/claude-code-review.yml +40 -0
  2. pydocket-0.11.0/.github/workflows/claude.yml +42 -0
  3. {pydocket-0.9.2 → pydocket-0.11.0}/PKG-INFO +1 -1
  4. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/execution.py +3 -0
  5. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/instrumentation.py +6 -0
  6. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/worker.py +14 -3
  7. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_instrumentation.py +50 -0
  8. {pydocket-0.9.2 → pydocket-0.11.0}/.cursor/rules/general.mdc +0 -0
  9. {pydocket-0.9.2 → pydocket-0.11.0}/.cursor/rules/python-style.mdc +0 -0
  10. {pydocket-0.9.2 → pydocket-0.11.0}/.github/codecov.yml +0 -0
  11. {pydocket-0.9.2 → pydocket-0.11.0}/.github/workflows/chaos.yml +0 -0
  12. {pydocket-0.9.2 → pydocket-0.11.0}/.github/workflows/ci.yml +0 -0
  13. {pydocket-0.9.2 → pydocket-0.11.0}/.github/workflows/docs.yml +0 -0
  14. {pydocket-0.9.2 → pydocket-0.11.0}/.github/workflows/publish.yml +0 -0
  15. {pydocket-0.9.2 → pydocket-0.11.0}/.gitignore +0 -0
  16. {pydocket-0.9.2 → pydocket-0.11.0}/.pre-commit-config.yaml +0 -0
  17. {pydocket-0.9.2 → pydocket-0.11.0}/CLAUDE.md +0 -0
  18. {pydocket-0.9.2 → pydocket-0.11.0}/LICENSE +0 -0
  19. {pydocket-0.9.2 → pydocket-0.11.0}/README.md +0 -0
  20. {pydocket-0.9.2 → pydocket-0.11.0}/chaos/README.md +0 -0
  21. {pydocket-0.9.2 → pydocket-0.11.0}/chaos/__init__.py +0 -0
  22. {pydocket-0.9.2 → pydocket-0.11.0}/chaos/driver.py +0 -0
  23. {pydocket-0.9.2 → pydocket-0.11.0}/chaos/producer.py +0 -0
  24. {pydocket-0.9.2 → pydocket-0.11.0}/chaos/run +0 -0
  25. {pydocket-0.9.2 → pydocket-0.11.0}/chaos/tasks.py +0 -0
  26. {pydocket-0.9.2 → pydocket-0.11.0}/docs/advanced-patterns.md +0 -0
  27. {pydocket-0.9.2 → pydocket-0.11.0}/docs/api-reference.md +0 -0
  28. {pydocket-0.9.2 → pydocket-0.11.0}/docs/dependencies.md +0 -0
  29. {pydocket-0.9.2 → pydocket-0.11.0}/docs/getting-started.md +0 -0
  30. {pydocket-0.9.2 → pydocket-0.11.0}/docs/index.md +0 -0
  31. {pydocket-0.9.2 → pydocket-0.11.0}/docs/production.md +0 -0
  32. {pydocket-0.9.2 → pydocket-0.11.0}/docs/testing.md +0 -0
  33. {pydocket-0.9.2 → pydocket-0.11.0}/examples/__init__.py +0 -0
  34. {pydocket-0.9.2 → pydocket-0.11.0}/examples/common.py +0 -0
  35. {pydocket-0.9.2 → pydocket-0.11.0}/examples/concurrency_control.py +0 -0
  36. {pydocket-0.9.2 → pydocket-0.11.0}/examples/find_and_flood.py +0 -0
  37. {pydocket-0.9.2 → pydocket-0.11.0}/examples/self_perpetuating.py +0 -0
  38. {pydocket-0.9.2 → pydocket-0.11.0}/mkdocs.yml +0 -0
  39. {pydocket-0.9.2 → pydocket-0.11.0}/pyproject.toml +0 -0
  40. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/__init__.py +0 -0
  41. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/__main__.py +0 -0
  42. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/annotations.py +0 -0
  43. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/cli.py +0 -0
  44. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/dependencies.py +0 -0
  45. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/docket.py +0 -0
  46. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/py.typed +0 -0
  47. {pydocket-0.9.2 → pydocket-0.11.0}/src/docket/tasks.py +0 -0
  48. {pydocket-0.9.2 → pydocket-0.11.0}/telemetry/.gitignore +0 -0
  49. {pydocket-0.9.2 → pydocket-0.11.0}/telemetry/start +0 -0
  50. {pydocket-0.9.2 → pydocket-0.11.0}/telemetry/stop +0 -0
  51. {pydocket-0.9.2 → pydocket-0.11.0}/tests/__init__.py +0 -0
  52. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/__init__.py +0 -0
  53. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/conftest.py +0 -0
  54. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_clear.py +0 -0
  55. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_module.py +0 -0
  56. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_parsing.py +0 -0
  57. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_snapshot.py +0 -0
  58. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_striking.py +0 -0
  59. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_tasks.py +0 -0
  60. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_version.py +0 -0
  61. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_worker.py +0 -0
  62. {pydocket-0.9.2 → pydocket-0.11.0}/tests/cli/test_workers.py +0 -0
  63. {pydocket-0.9.2 → pydocket-0.11.0}/tests/conftest.py +0 -0
  64. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_concurrency_basic.py +0 -0
  65. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_concurrency_control.py +0 -0
  66. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_concurrency_refresh.py +0 -0
  67. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_dependencies.py +0 -0
  68. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_docket.py +0 -0
  69. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_execution.py +0 -0
  70. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_fundamentals.py +0 -0
  71. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_striking.py +0 -0
  72. {pydocket-0.9.2 → pydocket-0.11.0}/tests/test_worker.py +0 -0
  73. {pydocket-0.9.2 → pydocket-0.11.0}/uv.lock +0 -0
@@ -0,0 +1,40 @@
1
+ name: Claude Code Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize]
6
+
7
+ jobs:
8
+ claude-review:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ pull-requests: read
13
+ issues: read
14
+ id-token: write
15
+
16
+ steps:
17
+ - name: Checkout repository
18
+ uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 1
21
+
22
+ - name: Run Claude Code Review
23
+ id: claude-review
24
+ uses: anthropics/claude-code-action@beta
25
+ with:
26
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
27
+ model: "claude-opus-4-1-20250805"
28
+
29
+ # Direct prompt for automated review (no @claude mention needed)
30
+ direct_prompt: |
31
+ Please review this pull request and provide feedback on:
32
+ - Code quality and best practices
33
+ - Potential bugs or issues
34
+ - Performance considerations
35
+ - Security concerns
36
+ - Test coverage, which must be maintained at 100% for this project
37
+
38
+ Be constructive and helpful in your feedback.
39
+
40
+ use_sticky_comment: true
@@ -0,0 +1,42 @@
1
+ name: Claude Code
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+ pull_request_review_comment:
7
+ types: [created]
8
+ issues:
9
+ types: [opened, assigned]
10
+ pull_request_review:
11
+ types: [submitted]
12
+
13
+ jobs:
14
+ claude:
15
+ if: |
16
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ pull-requests: read
24
+ issues: read
25
+ id-token: write
26
+ actions: read # Required for Claude to read CI results on PRs
27
+ steps:
28
+ - name: Checkout repository
29
+ uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 1
32
+
33
+ - name: Run Claude Code
34
+ id: claude
35
+ uses: anthropics/claude-code-action@beta
36
+ with:
37
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38
+
39
+ additional_permissions: |
40
+ actions: read
41
+
42
+ model: "claude-opus-4-1-20250805"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydocket
3
- Version: 0.9.2
3
+ Version: 0.11.0
4
4
  Summary: A distributed background task system for Python functions
5
5
  Project-URL: Homepage, https://github.com/chrisguidry/docket
6
6
  Project-URL: Bug Tracker, https://github.com/chrisguidry/docket/issues
@@ -51,6 +51,7 @@ class Execution:
51
51
  key: str,
52
52
  attempt: int,
53
53
  trace_context: opentelemetry.context.Context | None = None,
54
+ redelivered: bool = False,
54
55
  ) -> None:
55
56
  self.function = function
56
57
  self.args = args
@@ -59,6 +60,7 @@ class Execution:
59
60
  self.key = key
60
61
  self.attempt = attempt
61
62
  self.trace_context = trace_context
63
+ self.redelivered = redelivered
62
64
 
63
65
  def as_message(self) -> Message:
64
66
  return {
@@ -80,6 +82,7 @@ class Execution:
80
82
  key=message[b"key"].decode(),
81
83
  attempt=int(message[b"attempt"].decode()),
82
84
  trace_context=propagate.extract(message, getter=message_getter),
85
+ redelivered=False, # Default to False, will be set to True in worker if it's a redelivery
83
86
  )
84
87
 
85
88
  def general_labels(self) -> Mapping[str, str]:
@@ -40,6 +40,12 @@ TASKS_STARTED = meter.create_counter(
40
40
  unit="1",
41
41
  )
42
42
 
43
+ TASKS_REDELIVERED = meter.create_counter(
44
+ "docket_tasks_redelivered",
45
+ description="How many tasks started that were redelivered from another worker",
46
+ unit="1",
47
+ )
48
+
43
49
  TASKS_STRICKEN = meter.create_counter(
44
50
  "docket_tasks_stricken",
45
51
  description="How many tasks have been stricken from executing",
@@ -47,6 +47,7 @@ from .instrumentation import (
47
47
  TASKS_COMPLETED,
48
48
  TASKS_FAILED,
49
49
  TASKS_PERPETUATED,
50
+ TASKS_REDELIVERED,
50
51
  TASKS_RETRIED,
51
52
  TASKS_RUNNING,
52
53
  TASKS_STARTED,
@@ -286,7 +287,11 @@ class Worker:
286
287
  count=available_slots,
287
288
  )
288
289
 
289
- def start_task(message_id: RedisMessageID, message: RedisMessage) -> bool:
290
+ def start_task(
291
+ message_id: RedisMessageID,
292
+ message: RedisMessage,
293
+ is_redelivery: bool = False,
294
+ ) -> bool:
290
295
  function_name = message[b"function"].decode()
291
296
  if not (function := self.docket.tasks.get(function_name)):
292
297
  logger.warning(
@@ -297,6 +302,7 @@ class Worker:
297
302
  return False
298
303
 
299
304
  execution = Execution.from_message(function, message)
305
+ execution.redelivered = is_redelivery
300
306
 
301
307
  task = asyncio.create_task(self._execute(execution), name=execution.key)
302
308
  active_tasks[task] = message_id
@@ -342,12 +348,15 @@ class Worker:
342
348
  continue
343
349
 
344
350
  for source in [get_redeliveries, get_new_deliveries]:
345
- for _, messages in await source(redis):
351
+ for stream_key, messages in await source(redis):
352
+ is_redelivery = stream_key == b"__redelivery__"
346
353
  for message_id, message in messages:
347
354
  if not message: # pragma: no cover
348
355
  continue
349
356
 
350
- task_started = start_task(message_id, message)
357
+ task_started = start_task(
358
+ message_id, message, is_redelivery
359
+ )
351
360
  if not task_started:
352
361
  # Other errors - delete and ack
353
362
  await self._delete_known_task(redis, message)
@@ -521,6 +530,8 @@ class Worker:
521
530
  duration = 0.0
522
531
 
523
532
  TASKS_STARTED.add(1, counter_labels)
533
+ if execution.redelivered:
534
+ TASKS_REDELIVERED.add(1, counter_labels)
524
535
  TASKS_RUNNING.add(1, counter_labels)
525
536
  TASK_PUNCTUALITY.record(punctuality, counter_labels)
526
537
 
@@ -376,6 +376,14 @@ def TASKS_RETRIED(monkeypatch: pytest.MonkeyPatch) -> Mock:
376
376
  return mock
377
377
 
378
378
 
379
+ @pytest.fixture
380
+ def TASKS_REDELIVERED(monkeypatch: pytest.MonkeyPatch) -> Mock:
381
+ """Mock for the TASKS_REDELIVERED counter."""
382
+ mock = Mock(spec=Counter.add)
383
+ monkeypatch.setattr("docket.instrumentation.TASKS_REDELIVERED.add", mock)
384
+ return mock
385
+
386
+
379
387
  async def test_worker_execution_increments_task_counters(
380
388
  docket: Docket,
381
389
  worker: Worker,
@@ -386,6 +394,7 @@ async def test_worker_execution_increments_task_counters(
386
394
  TASKS_SUCCEEDED: Mock,
387
395
  TASKS_FAILED: Mock,
388
396
  TASKS_RETRIED: Mock,
397
+ TASKS_REDELIVERED: Mock,
389
398
  ):
390
399
  """Should increment the appropriate task counters when a worker executes a task."""
391
400
  await docket.add(the_task)()
@@ -397,6 +406,7 @@ async def test_worker_execution_increments_task_counters(
397
406
  TASKS_SUCCEEDED.assert_called_once_with(1, worker_labels)
398
407
  TASKS_FAILED.assert_not_called()
399
408
  TASKS_RETRIED.assert_not_called()
409
+ TASKS_REDELIVERED.assert_not_called()
400
410
 
401
411
 
402
412
  async def test_failed_task_increments_failure_counter(
@@ -409,6 +419,7 @@ async def test_failed_task_increments_failure_counter(
409
419
  TASKS_SUCCEEDED: Mock,
410
420
  TASKS_FAILED: Mock,
411
421
  TASKS_RETRIED: Mock,
422
+ TASKS_REDELIVERED: Mock,
412
423
  ):
413
424
  """Should increment the TASKS_FAILED counter when a task fails."""
414
425
  the_task.side_effect = ValueError("Womp")
@@ -422,6 +433,7 @@ async def test_failed_task_increments_failure_counter(
422
433
  TASKS_FAILED.assert_called_once_with(1, worker_labels)
423
434
  TASKS_SUCCEEDED.assert_not_called()
424
435
  TASKS_RETRIED.assert_not_called()
436
+ TASKS_REDELIVERED.assert_not_called()
425
437
 
426
438
 
427
439
  async def test_retried_task_increments_retry_counter(
@@ -433,6 +445,7 @@ async def test_retried_task_increments_retry_counter(
433
445
  TASKS_SUCCEEDED: Mock,
434
446
  TASKS_FAILED: Mock,
435
447
  TASKS_RETRIED: Mock,
448
+ TASKS_REDELIVERED: Mock,
436
449
  ):
437
450
  """Should increment the TASKS_RETRIED counter when a task is retried."""
438
451
 
@@ -448,6 +461,7 @@ async def test_retried_task_increments_retry_counter(
448
461
  assert TASKS_FAILED.call_count == 2
449
462
  assert TASKS_RETRIED.call_count == 1
450
463
  TASKS_SUCCEEDED.assert_not_called()
464
+ TASKS_REDELIVERED.assert_not_called()
451
465
 
452
466
 
453
467
  async def test_exhausted_retried_task_increments_retry_counter(
@@ -459,6 +473,7 @@ async def test_exhausted_retried_task_increments_retry_counter(
459
473
  TASKS_SUCCEEDED: Mock,
460
474
  TASKS_FAILED: Mock,
461
475
  TASKS_RETRIED: Mock,
476
+ TASKS_REDELIVERED: Mock,
462
477
  ):
463
478
  """Should increment the appropriate counters when retries are exhausted."""
464
479
 
@@ -474,6 +489,41 @@ async def test_exhausted_retried_task_increments_retry_counter(
474
489
  TASKS_FAILED.assert_called_once_with(1, worker_labels)
475
490
  TASKS_RETRIED.assert_not_called()
476
491
  TASKS_SUCCEEDED.assert_not_called()
492
+ TASKS_REDELIVERED.assert_not_called()
493
+
494
+
495
+ async def test_redelivered_tasks_increment_redelivered_counter(
496
+ docket: Docket,
497
+ worker_labels: dict[str, str],
498
+ TASKS_STARTED: Mock,
499
+ TASKS_COMPLETED: Mock,
500
+ TASKS_SUCCEEDED: Mock,
501
+ TASKS_FAILED: Mock,
502
+ TASKS_RETRIED: Mock,
503
+ TASKS_REDELIVERED: Mock,
504
+ ):
505
+ """Should increment the TASKS_REDELIVERED counter for redelivered tasks."""
506
+
507
+ async def test_task():
508
+ await asyncio.sleep(0.01)
509
+
510
+ await docket.add(test_task)()
511
+
512
+ worker = Worker(docket, redelivery_timeout=timedelta(milliseconds=50))
513
+
514
+ async with worker:
515
+ worker._execute = AsyncMock(side_effect=Exception("Simulated worker failure")) # type: ignore[assignment]
516
+
517
+ with pytest.raises(Exception, match="Simulated worker failure"):
518
+ await worker.run_until_finished()
519
+
520
+ await asyncio.sleep(0.075)
521
+
522
+ worker2 = Worker(docket, redelivery_timeout=timedelta(milliseconds=100))
523
+ async with worker2:
524
+ await worker2.run_until_finished()
525
+
526
+ assert TASKS_REDELIVERED.call_count >= 1
477
527
 
478
528
 
479
529
  @pytest.fixture
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes