pydocket 0.9.1__tar.gz → 0.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (71) hide show
  1. {pydocket-0.9.1 → pydocket-0.9.2}/PKG-INFO +1 -1
  2. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/docket.py +17 -8
  3. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/worker.py +2 -1
  4. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_worker.py +25 -0
  5. {pydocket-0.9.1 → pydocket-0.9.2}/.cursor/rules/general.mdc +0 -0
  6. {pydocket-0.9.1 → pydocket-0.9.2}/.cursor/rules/python-style.mdc +0 -0
  7. {pydocket-0.9.1 → pydocket-0.9.2}/.github/codecov.yml +0 -0
  8. {pydocket-0.9.1 → pydocket-0.9.2}/.github/workflows/chaos.yml +0 -0
  9. {pydocket-0.9.1 → pydocket-0.9.2}/.github/workflows/ci.yml +0 -0
  10. {pydocket-0.9.1 → pydocket-0.9.2}/.github/workflows/docs.yml +0 -0
  11. {pydocket-0.9.1 → pydocket-0.9.2}/.github/workflows/publish.yml +0 -0
  12. {pydocket-0.9.1 → pydocket-0.9.2}/.gitignore +0 -0
  13. {pydocket-0.9.1 → pydocket-0.9.2}/.pre-commit-config.yaml +0 -0
  14. {pydocket-0.9.1 → pydocket-0.9.2}/CLAUDE.md +0 -0
  15. {pydocket-0.9.1 → pydocket-0.9.2}/LICENSE +0 -0
  16. {pydocket-0.9.1 → pydocket-0.9.2}/README.md +0 -0
  17. {pydocket-0.9.1 → pydocket-0.9.2}/chaos/README.md +0 -0
  18. {pydocket-0.9.1 → pydocket-0.9.2}/chaos/__init__.py +0 -0
  19. {pydocket-0.9.1 → pydocket-0.9.2}/chaos/driver.py +0 -0
  20. {pydocket-0.9.1 → pydocket-0.9.2}/chaos/producer.py +0 -0
  21. {pydocket-0.9.1 → pydocket-0.9.2}/chaos/run +0 -0
  22. {pydocket-0.9.1 → pydocket-0.9.2}/chaos/tasks.py +0 -0
  23. {pydocket-0.9.1 → pydocket-0.9.2}/docs/advanced-patterns.md +0 -0
  24. {pydocket-0.9.1 → pydocket-0.9.2}/docs/api-reference.md +0 -0
  25. {pydocket-0.9.1 → pydocket-0.9.2}/docs/dependencies.md +0 -0
  26. {pydocket-0.9.1 → pydocket-0.9.2}/docs/getting-started.md +0 -0
  27. {pydocket-0.9.1 → pydocket-0.9.2}/docs/index.md +0 -0
  28. {pydocket-0.9.1 → pydocket-0.9.2}/docs/production.md +0 -0
  29. {pydocket-0.9.1 → pydocket-0.9.2}/docs/testing.md +0 -0
  30. {pydocket-0.9.1 → pydocket-0.9.2}/examples/__init__.py +0 -0
  31. {pydocket-0.9.1 → pydocket-0.9.2}/examples/common.py +0 -0
  32. {pydocket-0.9.1 → pydocket-0.9.2}/examples/concurrency_control.py +0 -0
  33. {pydocket-0.9.1 → pydocket-0.9.2}/examples/find_and_flood.py +0 -0
  34. {pydocket-0.9.1 → pydocket-0.9.2}/examples/self_perpetuating.py +0 -0
  35. {pydocket-0.9.1 → pydocket-0.9.2}/mkdocs.yml +0 -0
  36. {pydocket-0.9.1 → pydocket-0.9.2}/pyproject.toml +0 -0
  37. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/__init__.py +0 -0
  38. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/__main__.py +0 -0
  39. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/annotations.py +0 -0
  40. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/cli.py +0 -0
  41. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/dependencies.py +0 -0
  42. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/execution.py +0 -0
  43. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/instrumentation.py +0 -0
  44. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/py.typed +0 -0
  45. {pydocket-0.9.1 → pydocket-0.9.2}/src/docket/tasks.py +0 -0
  46. {pydocket-0.9.1 → pydocket-0.9.2}/telemetry/.gitignore +0 -0
  47. {pydocket-0.9.1 → pydocket-0.9.2}/telemetry/start +0 -0
  48. {pydocket-0.9.1 → pydocket-0.9.2}/telemetry/stop +0 -0
  49. {pydocket-0.9.1 → pydocket-0.9.2}/tests/__init__.py +0 -0
  50. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/__init__.py +0 -0
  51. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/conftest.py +0 -0
  52. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_clear.py +0 -0
  53. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_module.py +0 -0
  54. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_parsing.py +0 -0
  55. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_snapshot.py +0 -0
  56. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_striking.py +0 -0
  57. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_tasks.py +0 -0
  58. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_version.py +0 -0
  59. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_worker.py +0 -0
  60. {pydocket-0.9.1 → pydocket-0.9.2}/tests/cli/test_workers.py +0 -0
  61. {pydocket-0.9.1 → pydocket-0.9.2}/tests/conftest.py +0 -0
  62. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_concurrency_basic.py +0 -0
  63. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_concurrency_control.py +0 -0
  64. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_concurrency_refresh.py +0 -0
  65. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_dependencies.py +0 -0
  66. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_docket.py +0 -0
  67. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_execution.py +0 -0
  68. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_fundamentals.py +0 -0
  69. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_instrumentation.py +0 -0
  70. {pydocket-0.9.1 → pydocket-0.9.2}/tests/test_striking.py +0 -0
  71. {pydocket-0.9.1 → pydocket-0.9.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydocket
3
- Version: 0.9.1
3
+ Version: 0.9.2
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
@@ -428,6 +428,9 @@ class Docket:
428
428
  def parked_task_key(self, key: str) -> str:
429
429
  return f"{self.name}:{key}"
430
430
 
431
+ def stream_id_key(self, key: str) -> str:
432
+ return f"{self.name}:stream-id:{key}"
433
+
431
434
  async def _schedule(
432
435
  self,
433
436
  redis: Redis,
@@ -472,13 +475,14 @@ class Docket:
472
475
  self._schedule_task_script = cast(
473
476
  _schedule_task,
474
477
  redis.register_script(
475
- # KEYS: stream_key, known_key, parked_key, queue_key
478
+ # KEYS: stream_key, known_key, parked_key, queue_key, stream_id_key
476
479
  # ARGV: task_key, when_timestamp, is_immediate, replace, ...message_fields
477
480
  """
478
481
  local stream_key = KEYS[1]
479
482
  local known_key = KEYS[2]
480
483
  local parked_key = KEYS[3]
481
484
  local queue_key = KEYS[4]
485
+ local stream_id_key = KEYS[5]
482
486
 
483
487
  local task_key = ARGV[1]
484
488
  local when_timestamp = ARGV[2]
@@ -494,11 +498,11 @@ class Docket:
494
498
 
495
499
  -- Handle replacement: cancel existing task if needed
496
500
  if replace then
497
- local existing_message_id = redis.call('HGET', known_key, 'stream_message_id')
501
+ local existing_message_id = redis.call('GET', stream_id_key)
498
502
  if existing_message_id then
499
503
  redis.call('XDEL', stream_key, existing_message_id)
500
504
  end
501
- redis.call('DEL', known_key, parked_key)
505
+ redis.call('DEL', known_key, parked_key, stream_id_key)
502
506
  redis.call('ZREM', queue_key, task_key)
503
507
  else
504
508
  -- Check if task already exists
@@ -510,11 +514,12 @@ class Docket:
510
514
  if is_immediate then
511
515
  -- Add to stream and store message ID for later cancellation
512
516
  local message_id = redis.call('XADD', stream_key, '*', unpack(message))
513
- redis.call('HSET', known_key, 'when', when_timestamp, 'stream_message_id', message_id)
517
+ redis.call('SET', known_key, when_timestamp)
518
+ redis.call('SET', stream_id_key, message_id)
514
519
  return message_id
515
520
  else
516
521
  -- Add to queue with task data in parked hash
517
- redis.call('HSET', known_key, 'when', when_timestamp)
522
+ redis.call('SET', known_key, when_timestamp)
518
523
  redis.call('HSET', parked_key, unpack(message))
519
524
  redis.call('ZADD', queue_key, when_timestamp, task_key)
520
525
  return 'QUEUED'
@@ -530,6 +535,7 @@ class Docket:
530
535
  known_task_key,
531
536
  self.parked_task_key(key),
532
537
  self.queue_key,
538
+ self.stream_id_key(key),
533
539
  ],
534
540
  args=[
535
541
  key,
@@ -556,23 +562,24 @@ class Docket:
556
562
  self._cancel_task_script = cast(
557
563
  _cancel_task,
558
564
  redis.register_script(
559
- # KEYS: stream_key, known_key, parked_key, queue_key
565
+ # KEYS: stream_key, known_key, parked_key, queue_key, stream_id_key
560
566
  # ARGV: task_key
561
567
  """
562
568
  local stream_key = KEYS[1]
563
569
  local known_key = KEYS[2]
564
570
  local parked_key = KEYS[3]
565
571
  local queue_key = KEYS[4]
572
+ local stream_id_key = KEYS[5]
566
573
  local task_key = ARGV[1]
567
574
 
568
575
  -- Delete from stream if message ID exists
569
- local message_id = redis.call('HGET', known_key, 'stream_message_id')
576
+ local message_id = redis.call('GET', stream_id_key)
570
577
  if message_id then
571
578
  redis.call('XDEL', stream_key, message_id)
572
579
  end
573
580
 
574
581
  -- Clean up all task-related keys
575
- redis.call('DEL', known_key, parked_key)
582
+ redis.call('DEL', known_key, parked_key, stream_id_key)
576
583
  redis.call('ZREM', queue_key, task_key)
577
584
 
578
585
  return 'OK'
@@ -588,6 +595,7 @@ class Docket:
588
595
  self.known_task_key(key),
589
596
  self.parked_task_key(key),
590
597
  self.queue_key,
598
+ self.stream_id_key(key),
591
599
  ],
592
600
  args=[key],
593
601
  )
@@ -897,6 +905,7 @@ class Docket:
897
905
  key = key_bytes.decode()
898
906
  pipeline.delete(self.parked_task_key(key))
899
907
  pipeline.delete(self.known_task_key(key))
908
+ pipeline.delete(self.stream_id_key(key))
900
909
 
901
910
  await pipeline.execute()
902
911
 
@@ -495,7 +495,8 @@ class Worker:
495
495
 
496
496
  logger.debug("Deleting known task", extra=self._log_context())
497
497
  known_task_key = self.docket.known_task_key(key)
498
- await redis.delete(known_task_key)
498
+ stream_id_key = self.docket.stream_id_key(key)
499
+ await redis.delete(known_task_key, stream_id_key)
499
500
 
500
501
  async def _execute(self, execution: Execution) -> None:
501
502
  log_context = {**self._log_context(), **execution.specific_labels()}
@@ -1731,3 +1731,28 @@ async def test_redis_key_cleanup_cancelled_task(docket: Docket, worker: Worker)
1731
1731
 
1732
1732
  # Verify cleanup after cancellation
1733
1733
  await checker.verify_keys_returned_to_baseline("task cancellation")
1734
+
1735
+
1736
+ async def test_replace_task_with_legacy_known_key(
1737
+ docket: Docket, worker: Worker, the_task: AsyncMock, now: Callable[[], datetime]
1738
+ ):
1739
+ """Test that replace() works with legacy string known_keys.
1740
+
1741
+ This reproduces the exact production scenario where replace() would get
1742
+ WRONGTYPE errors when trying to HGET on legacy string known_keys.
1743
+ The main goal is to verify no WRONGTYPE error occurs.
1744
+ """
1745
+ key = f"legacy-replace-task:{uuid4()}"
1746
+
1747
+ # Simulate legacy state: create known_key as string (old format)
1748
+ async with docket.redis() as redis:
1749
+ known_task_key = docket.known_task_key(key)
1750
+ when = now()
1751
+
1752
+ # Create legacy known_key as STRING (what old code did)
1753
+ await redis.set(known_task_key, str(when.timestamp()))
1754
+
1755
+ # Now try to replace - this should work without WRONGTYPE error
1756
+ # The key point is that this call succeeds without throwing WRONGTYPE
1757
+ replacement_time = now() + timedelta(seconds=1)
1758
+ await docket.replace("trace", replacement_time, key=key)("replacement message")
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
File without changes