pydocket 0.26.0__tar.gz → 0.26.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.
Files changed (265) hide show
  1. {pydocket-0.26.0 → pydocket-0.26.2}/.coveragerc-core +2 -0
  2. {pydocket-0.26.0 → pydocket-0.26.2}/.gitignore +4 -0
  3. pydocket-0.26.2/.pre-commit-config.yaml +16 -0
  4. {pydocket-0.26.0 → pydocket-0.26.2}/PKG-INFO +13 -16
  5. {pydocket-0.26.0 → pydocket-0.26.2}/README.md +11 -14
  6. {pydocket-0.26.0 → pydocket-0.26.2}/pyproject.toml +12 -19
  7. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_execution_progress.py +1 -17
  8. pydocket-0.26.2/src/docket/_execution_scripts.py +97 -0
  9. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_lua.py +70 -32
  10. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_redelivery.py +1 -8
  11. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_redis.py +57 -4
  12. pydocket-0.26.2/src/docket/_redis_credentials.py +115 -0
  13. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_redis_sentinel.py +8 -2
  14. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_result_store.py +1 -0
  15. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_concurrency.py +4 -305
  16. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_debounce.py +1 -39
  17. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_ratelimit.py +1 -30
  18. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/docket.py +10 -1
  19. pydocket-0.26.2/src/docket/lua/acquire_or_park.lua +97 -0
  20. pydocket-0.26.2/src/docket/lua/cancel_cleanup.lua +20 -0
  21. pydocket-0.26.2/src/docket/lua/cancel_task.lua +56 -0
  22. pydocket-0.26.2/src/docket/lua/claim.lua +109 -0
  23. pydocket-0.26.2/src/docket/lua/debounce.lua +42 -0
  24. pydocket-0.26.2/src/docket/lua/progress_write.lua +18 -0
  25. pydocket-0.26.2/src/docket/lua/ratelimit.lua +33 -0
  26. pydocket-0.26.2/src/docket/lua/refresh_lease.lua +8 -0
  27. pydocket-0.26.2/src/docket/lua/release_and_wake.lua +111 -0
  28. pydocket-0.26.2/src/docket/lua/scavenge_and_wake.lua +105 -0
  29. pydocket-0.26.2/src/docket/lua/schedule.lua +216 -0
  30. pydocket-0.26.2/src/docket/lua/stream_due_tasks.lua +65 -0
  31. pydocket-0.26.2/src/docket/lua/terminal.lua +66 -0
  32. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/strikelist.py +5 -1
  33. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/worker.py +20 -66
  34. {pydocket-0.26.0 → pydocket-0.26.2}/tests/_container.py +14 -9
  35. pydocket-0.26.2/tests/_rotating_provider.py +42 -0
  36. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cluster/Dockerfile +1 -1
  37. pydocket-0.26.2/tests/concurrency_limits/test_parked_status.py +40 -0
  38. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/test_waiter_queue.py +11 -21
  39. pydocket-0.26.2/tests/concurrency_limits/waiters.py +21 -0
  40. {pydocket-0.26.0 → pydocket-0.26.2}/tests/conftest.py +59 -17
  41. pydocket-0.26.2/tests/lua/calc.lua +11 -0
  42. pydocket-0.26.2/tests/lua/count_argv.lua +5 -0
  43. pydocket-0.26.2/tests/lua/echo_bool.lua +4 -0
  44. pydocket-0.26.2/tests/lua/echo_described_key.lua +3 -0
  45. pydocket-0.26.2/tests/lua/echo_dict.lua +4 -0
  46. pydocket-0.26.2/tests/lua/echo_keys_and_args.lua +6 -0
  47. pydocket-0.26.2/tests/lua/echo_list.lua +4 -0
  48. pydocket-0.26.2/tests/lua/echo_numbers.lua +5 -0
  49. pydocket-0.26.2/tests/lua/echo_tuple.lua +4 -0
  50. pydocket-0.26.2/tests/lua/extra_binding.lua +4 -0
  51. pydocket-0.26.2/tests/lua/join_variadic.lua +10 -0
  52. pydocket-0.26.2/tests/lua/misdecoded_flag.lua +4 -0
  53. pydocket-0.26.2/tests/lua/noscript_echo.lua +3 -0
  54. pydocket-0.26.2/tests/test_credential_provider.py +164 -0
  55. pydocket-0.26.2/tests/test_credential_rotation.py +265 -0
  56. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_lua_decorator.py +52 -100
  57. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_pubsub_resp_version.py +2 -2
  58. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_results_storage.py +1 -1
  59. pydocket-0.26.2/tests/test_reused_keys.py +113 -0
  60. pydocket-0.26.2/tests/worker/test_strikes_at_startup.py +68 -0
  61. pydocket-0.26.0/.claude/skills/audit-docs/SKILL.md +0 -95
  62. pydocket-0.26.0/.codespellrc +0 -3
  63. pydocket-0.26.0/.dockerignore +0 -11
  64. pydocket-0.26.0/.github/codecov.yml +0 -33
  65. pydocket-0.26.0/.github/dependabot.yml +0 -23
  66. pydocket-0.26.0/.github/workflows/chaos.yml +0 -56
  67. pydocket-0.26.0/.github/workflows/ci.yml +0 -276
  68. pydocket-0.26.0/.github/workflows/docs.yml +0 -38
  69. pydocket-0.26.0/.github/workflows/publish.yml +0 -46
  70. pydocket-0.26.0/.pre-commit-config.yaml +0 -47
  71. pydocket-0.26.0/.readthedocs.yaml +0 -11
  72. pydocket-0.26.0/AGENTS.md +0 -164
  73. pydocket-0.26.0/Dockerfile +0 -15
  74. pydocket-0.26.0/SECURITY.md +0 -22
  75. pydocket-0.26.0/chaos/README.md +0 -6
  76. pydocket-0.26.0/chaos/driver.py +0 -348
  77. pydocket-0.26.0/chaos/producer.py +0 -51
  78. pydocket-0.26.0/chaos/redis.py +0 -46
  79. pydocket-0.26.0/chaos/run +0 -8
  80. pydocket-0.26.0/chaos/signals.py +0 -345
  81. pydocket-0.26.0/chaos/tasks.py +0 -42
  82. pydocket-0.26.0/docker-compose.yml +0 -17
  83. pydocket-0.26.0/docs/api-reference.md +0 -3
  84. pydocket-0.26.0/docs/dependency-injection.md +0 -607
  85. pydocket-0.26.0/docs/getting-started.md +0 -284
  86. pydocket-0.26.0/docs/index.md +0 -82
  87. pydocket-0.26.0/docs/observability.md +0 -352
  88. pydocket-0.26.0/docs/production.md +0 -577
  89. pydocket-0.26.0/docs/task-behaviors.md +0 -655
  90. pydocket-0.26.0/docs/task-patterns.md +0 -318
  91. pydocket-0.26.0/docs/testing.md +0 -422
  92. pydocket-0.26.0/examples/agenda_scatter.py +0 -128
  93. pydocket-0.26.0/examples/common.py +0 -67
  94. pydocket-0.26.0/examples/concurrency_control.py +0 -114
  95. pydocket-0.26.0/examples/fallback_arithmetic.py +0 -109
  96. pydocket-0.26.0/examples/fastapi_background_tasks.py +0 -204
  97. pydocket-0.26.0/examples/find_and_flood.py +0 -41
  98. pydocket-0.26.0/examples/local_development.py +0 -98
  99. pydocket-0.26.0/examples/self_contained_service.py +0 -62
  100. pydocket-0.26.0/examples/self_perpetuating.py +0 -36
  101. pydocket-0.26.0/examples/task_progress.py +0 -109
  102. pydocket-0.26.0/loq.toml +0 -37
  103. pydocket-0.26.0/mkdocs.yml +0 -81
  104. pydocket-0.26.0/src/docket/_execution_scripts.py +0 -481
  105. pydocket-0.26.0/telemetry/.gitignore +0 -1
  106. pydocket-0.26.0/telemetry/start +0 -9
  107. pydocket-0.26.0/telemetry/stop +0 -8
  108. pydocket-0.26.0/tests/__init__.py +0 -0
  109. pydocket-0.26.0/tests/cli/__init__.py +0 -0
  110. pydocket-0.26.0/tests/fundamentals/CLAUDE.md +0 -1
  111. pydocket-0.26.0/uv.lock +0 -2327
  112. {pydocket-0.26.0 → pydocket-0.26.2}/.coveragerc-cli +0 -0
  113. {pydocket-0.26.0 → pydocket-0.26.2}/LICENSE +0 -0
  114. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/__init__.py +0 -0
  115. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/__main__.py +0 -0
  116. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_cancellation.py +0 -0
  117. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_docket_snapshot.py +0 -0
  118. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_prometheus_exporter.py +0 -0
  119. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_redis_memory.py +0 -0
  120. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_telemetry.py +0 -0
  121. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/_uuid7.py +0 -0
  122. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/agenda.py +0 -0
  123. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/annotations.py +0 -0
  124. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/cli/__init__.py +0 -0
  125. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/cli/_support.py +0 -0
  126. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/__init__.py +0 -0
  127. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_base.py +0 -0
  128. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_contextual.py +0 -0
  129. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_cooldown.py +0 -0
  130. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_cron.py +0 -0
  131. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_functional.py +0 -0
  132. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_perpetual.py +0 -0
  133. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_progress.py +0 -0
  134. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_resolution.py +0 -0
  135. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_retry.py +0 -0
  136. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/dependencies/_timeout.py +0 -0
  137. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/execution.py +0 -0
  138. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/instrumentation.py +0 -0
  139. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/py.typed +0 -0
  140. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/tasks.py +0 -0
  141. {pydocket-0.26.0 → pydocket-0.26.2}/src/docket/testing.py +0 -0
  142. {pydocket-0.26.0/chaos → pydocket-0.26.2/tests}/__init__.py +0 -0
  143. {pydocket-0.26.0 → pydocket-0.26.2}/tests/_key_leak_checker.py +0 -0
  144. {pydocket-0.26.0/examples → pydocket-0.26.2/tests/cli}/__init__.py +0 -0
  145. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/run.py +0 -0
  146. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_clear.py +0 -0
  147. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_iterate_with_timeout.py +0 -0
  148. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_module.py +0 -0
  149. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_parsing.py +0 -0
  150. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_snapshot.py +0 -0
  151. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_striking.py +0 -0
  152. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_tasks.py +0 -0
  153. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_url_validation.py +0 -0
  154. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_version.py +0 -0
  155. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_watch.py +0 -0
  156. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_worker.py +0 -0
  157. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/test_workers.py +0 -0
  158. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cli/waiting.py +0 -0
  159. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cluster/entrypoint.sh +0 -0
  160. {pydocket-0.26.0 → pydocket-0.26.2}/tests/cluster/start-cluster.sh +0 -0
  161. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/__init__.py +0 -0
  162. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/overlap.py +0 -0
  163. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/test_annotated.py +0 -0
  164. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/test_basic.py +0 -0
  165. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/test_errors_and_resilience.py +0 -0
  166. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/test_execution_patterns.py +0 -0
  167. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/test_redelivery.py +0 -0
  168. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/test_waiter_safeguard.py +0 -0
  169. {pydocket-0.26.0 → pydocket-0.26.2}/tests/concurrency_limits/test_worker_mechanics.py +0 -0
  170. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/AGENTS.md +0 -0
  171. {pydocket-0.26.0 → pydocket-0.26.2/tests/fundamentals}/CLAUDE.md +0 -0
  172. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/conftest.py +0 -0
  173. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_async_dependencies.py +0 -0
  174. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_builtin_tasks.py +0 -0
  175. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_call_arguments.py +0 -0
  176. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_cancellation.py +0 -0
  177. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_context_injection.py +0 -0
  178. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_cron.py +0 -0
  179. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_errors.py +0 -0
  180. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_idempotency.py +0 -0
  181. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_logging.py +0 -0
  182. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_perpetual.py +0 -0
  183. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_progress_state.py +0 -0
  184. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_results.py +0 -0
  185. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_retries.py +0 -0
  186. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_scheduling.py +0 -0
  187. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_self_perpetuation.py +0 -0
  188. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_shared_dependencies.py +0 -0
  189. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_striking.py +0 -0
  190. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_sync_dependencies.py +0 -0
  191. {pydocket-0.26.0 → pydocket-0.26.2}/tests/fundamentals/test_timeouts.py +0 -0
  192. {pydocket-0.26.0 → pydocket-0.26.2}/tests/instrumentation/test_counters.py +0 -0
  193. {pydocket-0.26.0 → pydocket-0.26.2}/tests/instrumentation/test_export.py +0 -0
  194. {pydocket-0.26.0 → pydocket-0.26.2}/tests/instrumentation/test_tracing.py +0 -0
  195. {pydocket-0.26.0 → pydocket-0.26.2}/tests/sitecustomize.py +0 -0
  196. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_agenda.py +0 -0
  197. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_annotated_dependencies.py +0 -0
  198. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_automatic_perpetual_recovery.py +0 -0
  199. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_batch_scheduling.py +0 -0
  200. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_call_arguments.py +0 -0
  201. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_cancel_queued_task.py +0 -0
  202. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_cancellation.py +0 -0
  203. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_claim_supersedes.py +0 -0
  204. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_cooldown.py +0 -0
  205. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_debounce.py +0 -0
  206. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_dependencies_advanced.py +0 -0
  207. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_dependencies_core.py +0 -0
  208. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_dependency_uniqueness.py +0 -0
  209. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_depends_typing.py +0 -0
  210. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_docket_clear.py +0 -0
  211. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_docket_execution.py +0 -0
  212. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_docket_isolation.py +0 -0
  213. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_docket_keys.py +0 -0
  214. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_docket_registration.py +0 -0
  215. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_docket_snapshot.py +0 -0
  216. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_execution.py +0 -0
  217. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_execution_state.py +0 -0
  218. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_exponential_retry_overflow.py +0 -0
  219. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_fallback_task.py +0 -0
  220. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_get_result_cancellation.py +0 -0
  221. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_handler_semantics.py +0 -0
  222. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_immediate_retry_state.py +0 -0
  223. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_interrupted_tasks.py +0 -0
  224. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_key_leak_protection.py +0 -0
  225. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_lease_renewal.py +0 -0
  226. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_memory_backend.py +0 -0
  227. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_payload_escaping.py +0 -0
  228. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_perpetual_handoff.py +0 -0
  229. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_perpetual_race.py +0 -0
  230. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_perpetual_reliability.py +0 -0
  231. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_perpetual_reschedule.py +0 -0
  232. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_perpetual_state.py +0 -0
  233. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_progress_basics.py +0 -0
  234. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_progress_clobber.py +0 -0
  235. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_progress_pubsub.py +0 -0
  236. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_ratelimit.py +0 -0
  237. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_redelivery.py +0 -0
  238. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_redelivery_sweep.py +0 -0
  239. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_redis_connection.py +0 -0
  240. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_reschedule_clears_worker.py +0 -0
  241. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_results_retrieval.py +0 -0
  242. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_sentinel_urls.py +0 -0
  243. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_socket_timeout.py +0 -0
  244. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_strikelist.py +0 -0
  245. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_striking.py +0 -0
  246. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_task_cycle_round_trips.py +0 -0
  247. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_terminal_acked.py +0 -0
  248. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_testing.py +0 -0
  249. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_uncalled_for_contract.py +0 -0
  250. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_uuid7.py +0 -0
  251. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_worker_cancellation.py +0 -0
  252. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_worker_dependencies.py +0 -0
  253. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_worker_single_dependencies.py +0 -0
  254. {pydocket-0.26.0 → pydocket-0.26.2}/tests/test_worker_task_deps_interaction.py +0 -0
  255. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_bootstrap.py +0 -0
  256. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_core.py +0 -0
  257. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_crash.py +0 -0
  258. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_delivery_batch.py +0 -0
  259. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_invariants.py +0 -0
  260. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_lifecycle.py +0 -0
  261. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_liveness.py +0 -0
  262. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_reconnection.py +0 -0
  263. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_scheduling.py +0 -0
  264. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_task_factory.py +0 -0
  265. {pydocket-0.26.0 → pydocket-0.26.2}/tests/worker/test_ttl_zero.py +0 -0
@@ -10,11 +10,13 @@ omit =
10
10
  **/conftest.py
11
11
  tests/_key_leak_checker.py
12
12
  tests/_container.py
13
+ tests/_rotating_provider.py
13
14
  tests/sitecustomize.py
14
15
  tests/cli/*.py
15
16
  # real-Redis-only; skips on the memory and cluster legs
16
17
  tests/test_socket_timeout.py
17
18
  tests/test_pubsub_resp_version.py
19
+ tests/test_credential_rotation.py
18
20
 
19
21
  [report]
20
22
  exclude_also =
@@ -14,3 +14,7 @@ wheels/
14
14
  .coverage.*
15
15
  .loq_cache
16
16
  .claude/scheduled_tasks.lock
17
+ coverage.xml
18
+ junit.xml
19
+ target/
20
+ target-*/
@@ -0,0 +1,16 @@
1
+ # The root config's hooks, ruff among them, also cover this directory.
2
+ repos:
3
+ - repo: local
4
+ hooks:
5
+ - id: pyright
6
+ name: pyright (docket package)
7
+ entry: uv run pyright --verifytypes docket --ignoreexternal
8
+ language: system
9
+ types: [python]
10
+ pass_filenames: false
11
+ - id: pyright
12
+ name: pyright (tests and conformance agent)
13
+ entry: uv run pyright tests conformance-agent
14
+ language: system
15
+ types: [python]
16
+ pass_filenames: false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pydocket
3
- Version: 0.26.0
3
+ Version: 0.26.2
4
4
  Summary: A distributed background task system for Python functions
5
5
  Project-URL: Homepage, https://docket.lol/
6
6
  Project-URL: Documentation, https://docket.lol/en/latest/
@@ -36,7 +36,7 @@ Requires-Dist: opentelemetry-api>=1.33.0
36
36
  Requires-Dist: prometheus-client>=0.21.1
37
37
  Requires-Dist: py-key-value-aio[memory,redis]>=0.3.0
38
38
  Requires-Dist: python-json-logger>=2.0.7
39
- Requires-Dist: redis>=5
39
+ Requires-Dist: redis>=6
40
40
  Requires-Dist: rich>=13.9.4
41
41
  Requires-Dist: taskgroup>=0.2.2; python_version < '3.11'
42
42
  Requires-Dist: typer>=0.15.1
@@ -52,7 +52,7 @@ on the scheduling of future work as seamlessly and efficiently as immediate work
52
52
 
53
53
  [![PyPI - Version](https://img.shields.io/pypi/v/pydocket)](https://pypi.org/project/pydocket/)
54
54
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pydocket)](https://pypi.org/project/pydocket/)
55
- [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions/workflows/ci.yml)
55
+ [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions)
56
56
  [![Codecov](https://img.shields.io/codecov/c/github/chrisguidry/docket)](https://app.codecov.io/gh/chrisguidry/docket)
57
57
  [![PyPI - License](https://img.shields.io/pypi/l/pydocket)](https://github.com/chrisguidry/docket/blob/main/LICENSE)
58
58
  [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://docket.lol/)
@@ -130,11 +130,11 @@ With `pip`:
130
130
  pip install pydocket
131
131
  ```
132
132
 
133
- Docket requires a [Redis](http://redis.io/) server with Streams support (which was
134
- introduced in Redis 5.0.0). Docket is tested with:
133
+ Docket requires [Redis](https://redis.io/) 6.2 or later, or
134
+ [Valkey](https://valkey.io/) 8.0 or later. Docket is tested with:
135
135
 
136
- - Redis 6.2, 7.4, and 8.6 (standalone and cluster modes)
137
- - [Valkey](https://valkey.io/) 8.1
136
+ - Redis 6.2 and 8.10, and Redis 8.10 in cluster mode
137
+ - Valkey 8.0 and 9.1
138
138
  - In-memory backend via [burner-redis](https://github.com/prefectlabs/burner-redis) for testing
139
139
 
140
140
  For testing without Redis, use the in-memory backend:
@@ -151,17 +151,14 @@ See [Testing with Docket](https://docket.lol/en/latest/testing/#using-in-memory-
151
151
 
152
152
  # Hacking on `docket`
153
153
 
154
- We use [`uv`](https://docs.astral.sh/uv/) for project management, so getting set up
155
- should be as simple as cloning the repo and running:
154
+ We use [`uv`](https://docs.astral.sh/uv/) for project management. The
155
+ repository is a uv workspace, and the Python package lives in its `python/`
156
+ directory. From a clone of the repository:
156
157
 
157
158
  ```bash
158
159
  uv sync
159
- ```
160
-
161
- The to run the test suite:
162
-
163
- ```bash
164
- pytest
160
+ cd python
161
+ uv run pytest
165
162
  ```
166
163
 
167
164
  We aim to maintain 100% test coverage, which is required for all PRs to `docket`. We
@@ -170,7 +167,7 @@ begins with testing all the dusty branches and corners. This will give us the
170
167
  confidence to upgrade dependencies quickly and to adapt to new versions of Redis over
171
168
  time.
172
169
 
173
- To work on the documentation locally:
170
+ To work on the documentation locally, from the repository root:
174
171
 
175
172
  ```bash
176
173
  uv sync
@@ -3,7 +3,7 @@ on the scheduling of future work as seamlessly and efficiently as immediate work
3
3
 
4
4
  [![PyPI - Version](https://img.shields.io/pypi/v/pydocket)](https://pypi.org/project/pydocket/)
5
5
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pydocket)](https://pypi.org/project/pydocket/)
6
- [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions/workflows/ci.yml)
6
+ [![GitHub main checks](https://img.shields.io/github/check-runs/chrisguidry/docket/main)](https://github.com/chrisguidry/docket/actions)
7
7
  [![Codecov](https://img.shields.io/codecov/c/github/chrisguidry/docket)](https://app.codecov.io/gh/chrisguidry/docket)
8
8
  [![PyPI - License](https://img.shields.io/pypi/l/pydocket)](https://github.com/chrisguidry/docket/blob/main/LICENSE)
9
9
  [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://docket.lol/)
@@ -81,11 +81,11 @@ With `pip`:
81
81
  pip install pydocket
82
82
  ```
83
83
 
84
- Docket requires a [Redis](http://redis.io/) server with Streams support (which was
85
- introduced in Redis 5.0.0). Docket is tested with:
84
+ Docket requires [Redis](https://redis.io/) 6.2 or later, or
85
+ [Valkey](https://valkey.io/) 8.0 or later. Docket is tested with:
86
86
 
87
- - Redis 6.2, 7.4, and 8.6 (standalone and cluster modes)
88
- - [Valkey](https://valkey.io/) 8.1
87
+ - Redis 6.2 and 8.10, and Redis 8.10 in cluster mode
88
+ - Valkey 8.0 and 9.1
89
89
  - In-memory backend via [burner-redis](https://github.com/prefectlabs/burner-redis) for testing
90
90
 
91
91
  For testing without Redis, use the in-memory backend:
@@ -102,17 +102,14 @@ See [Testing with Docket](https://docket.lol/en/latest/testing/#using-in-memory-
102
102
 
103
103
  # Hacking on `docket`
104
104
 
105
- We use [`uv`](https://docs.astral.sh/uv/) for project management, so getting set up
106
- should be as simple as cloning the repo and running:
105
+ We use [`uv`](https://docs.astral.sh/uv/) for project management. The
106
+ repository is a uv workspace, and the Python package lives in its `python/`
107
+ directory. From a clone of the repository:
107
108
 
108
109
  ```bash
109
110
  uv sync
110
- ```
111
-
112
- The to run the test suite:
113
-
114
- ```bash
115
- pytest
111
+ cd python
112
+ uv run pytest
116
113
  ```
117
114
 
118
115
  We aim to maintain 100% test coverage, which is required for all PRs to `docket`. We
@@ -121,7 +118,7 @@ begins with testing all the dusty branches and corners. This will give us the
121
118
  confidence to upgrade dependencies quickly and to adapt to new versions of Redis over
122
119
  time.
123
120
 
124
- To work on the documentation locally:
121
+ To work on the documentation locally, from the repository root:
125
122
 
126
123
  ```bash
127
124
  uv sync
@@ -32,7 +32,7 @@ dependencies = [
32
32
  "prometheus-client>=0.21.1",
33
33
  "py-key-value-aio[memory,redis]>=0.3.0",
34
34
  "python-json-logger>=2.0.7",
35
- "redis>=5",
35
+ "redis>=6",
36
36
  "rich>=13.9.4",
37
37
  "typer>=0.15.1",
38
38
  "typing_extensions>=4.12.0",
@@ -47,12 +47,9 @@ metrics = [
47
47
 
48
48
  [dependency-groups]
49
49
  dev = [
50
- "codespell>=2.4.1",
51
50
  "docker>=7.1.0",
52
51
  # Security: urllib3 >=2.6.3 fixes decompression bomb vulnerabilities
53
52
  "urllib3>=2.6.3",
54
- "ipython>=8.0.0",
55
- "loq>=0.1.0a3",
56
53
  "opentelemetry-distro>=0.60b0",
57
54
  "opentelemetry-exporter-otlp>=1.33.0",
58
55
  "opentelemetry-exporter-prometheus>=0.60b0",
@@ -60,7 +57,6 @@ dev = [
60
57
  "opentelemetry-instrumentation-logging>=0.60b0",
61
58
  "opentelemetry-instrumentation-redis>=0.60b0",
62
59
  "opentelemetry-sdk>=1.33.0",
63
- "prek>=0.3.1",
64
60
  "pyright>=1.1.408",
65
61
  "pytest>=9.0.2",
66
62
  "pytest-asyncio>=0.24.0",
@@ -71,16 +67,6 @@ dev = [
71
67
  "pytest-timeout>=2.4.0",
72
68
  "pytest-xdist>=3.6.1",
73
69
  "ruff>=0.14.14",
74
- "zensical>=0.0.32",
75
- "mkdocstrings>=1.0.2",
76
- "mkdocstrings-python>=2.0.1",
77
- ]
78
- examples = [
79
- "fastapi>=0.120.0",
80
- "pydantic>=2.12.5",
81
- # Security: starlette >=0.49.1 fixes session middleware vulnerability
82
- "starlette>=0.49.1",
83
- "uvicorn>=0.40.0",
84
70
  ]
85
71
 
86
72
  [project.scripts]
@@ -94,6 +80,11 @@ docket = "docket.__main__:app"
94
80
 
95
81
  [tool.hatch.version]
96
82
  source = "vcs"
83
+ # Releases before the monorepo are tagged bare (0.26.0), and later ones
84
+ # python/v0.27.0. Matching only these keeps another language's tag, like
85
+ # rust/v0.1.0, out of pydocket's version.
86
+ tag-pattern = "^(?:python/v)?(?P<version>\\d+\\.\\d+\\.\\d+.*)$"
87
+ raw-options = { root = "..", scm = { git = { describe_command = "git describe --dirty --tags --long --match python/v* --match [0-9]*" } } }
97
88
 
98
89
  [tool.hatch.metadata]
99
90
  allow-direct-references = true
@@ -102,8 +93,8 @@ allow-direct-references = true
102
93
  [tool.hatch.build.targets.wheel]
103
94
  packages = ["src/docket"]
104
95
 
105
- [tool.ruff]
106
- target-version = "py310"
96
+ [tool.hatch.build.targets.sdist]
97
+ exclude = ["conformance-agent"]
107
98
 
108
99
  [tool.pytest.ini_options]
109
100
  addopts = [
@@ -160,9 +151,11 @@ parallel = true
160
151
  exclude_also = ["\\.\\.\\.$"]
161
152
 
162
153
  [tool.pyright]
163
- include = ["src", "tests", "chaos"]
154
+ include = ["src", "tests", "conformance-agent"]
155
+ # The agent runs as a directory, so its modules import each other from there.
156
+ executionEnvironments = [{ root = "conformance-agent" }]
164
157
  typeCheckingMode = "strict"
165
- venvPath = "."
158
+ venvPath = ".."
166
159
  venv = ".venv"
167
160
  reportUnnecessaryTypeIgnoreComment = "error"
168
161
  reportUnnecessaryCast = "error"
@@ -32,23 +32,7 @@ async def _progress_write(
32
32
  payload: Arg[str],
33
33
  clear_message: Arg[bool],
34
34
  fields: Args[dict[str, str]],
35
- ) -> bytes:
36
- """
37
- local hset_args = {}
38
- for i = fields_start, #ARGV, 2 do
39
- hset_args[#hset_args + 1] = ARGV[i]
40
- hset_args[#hset_args + 1] = ARGV[i + 1]
41
- end
42
- if #hset_args > 0 then
43
- redis.call('HSET', progress_key, unpack(hset_args))
44
- end
45
- if clear_message then
46
- redis.call('HDEL', progress_key, 'message')
47
- end
48
- redis.call('PUBLISH', progress_key, payload)
49
- return 'OK'
50
- """
51
- ...
35
+ ) -> bytes: ...
52
36
 
53
37
 
54
38
  class ProgressEvent(TypedDict):
@@ -0,0 +1,97 @@
1
+ """The Lua that moves one task key between lifecycle states.
2
+
3
+ Every transition in a task's life is one atomic script against the keys for
4
+ a single task: ``_schedule`` puts it on the stream or the queue, ``_claim``
5
+ takes it for a worker, ``_terminal`` records how it finished, and
6
+ ``_cancel_task`` takes it back off. The first three each check the
7
+ ``generation`` counter in the runs hash, so a stale attempt can never
8
+ overwrite a newer one.
9
+
10
+ ``Execution`` and ``Docket`` call these; the scripts hold no Python logic of
11
+ their own.
12
+ """
13
+
14
+ from typing import Any
15
+
16
+ from ._lua import Arg, Args, Key, redis_script
17
+ from ._redis import RedisClient
18
+
19
+
20
+ @redis_script
21
+ async def _schedule(
22
+ redis: RedisClient,
23
+ *,
24
+ stream_key: Key[str],
25
+ known_key: Key[str],
26
+ parked_key: Key[str],
27
+ queue_key: Key[str],
28
+ stream_id_key: Key[str],
29
+ runs_key: Key[str],
30
+ state_channel: Key[str],
31
+ task_key: Arg[str],
32
+ when_timestamp: Arg[float],
33
+ is_immediate: Arg[bool],
34
+ replace: Arg[bool],
35
+ reschedule_message_id: Arg[bytes],
36
+ expected_generation: Arg[int],
37
+ worker_group_name: Arg[str],
38
+ state_payload: Arg[str],
39
+ message: Args[dict[bytes, bytes]],
40
+ ) -> bytes | str: ...
41
+
42
+
43
+ @redis_script
44
+ async def _claim(
45
+ redis: RedisClient,
46
+ *,
47
+ runs_key: Key[str],
48
+ progress_key: Key[str],
49
+ known_key: Key[str],
50
+ stream_id_key: Key[str],
51
+ state_channel: Key[str],
52
+ stream_key: Key[str],
53
+ worker: Arg[str],
54
+ started_at: Arg[str],
55
+ generation: Arg[int],
56
+ state_payload: Arg[str],
57
+ key_json: Arg[str],
58
+ worker_group_name: Arg[str],
59
+ message_id: Arg[bytes],
60
+ ) -> list[Any]: ...
61
+
62
+
63
+ @redis_script
64
+ async def _terminal(
65
+ redis: RedisClient,
66
+ *,
67
+ runs_key: Key[str],
68
+ state_channel: Key[str],
69
+ progress_key: Key[str],
70
+ stream_key: Key[str],
71
+ generation: Arg[int],
72
+ state: Arg[str],
73
+ completed_at: Arg[str],
74
+ ttl_seconds: Arg[int],
75
+ state_payload: Arg[str],
76
+ worker_group_name: Arg[str],
77
+ message_id: Arg[bytes],
78
+ extra_fields: Args[list[str]],
79
+ ) -> bytes: ...
80
+
81
+
82
+ @redis_script
83
+ async def _cancel_task(
84
+ redis: RedisClient,
85
+ *,
86
+ stream_key: Key[str],
87
+ known_key: Key[str],
88
+ parked_key: Key[str],
89
+ queue_key: Key[str],
90
+ stream_id_key: Key[str],
91
+ runs_key: Key[str],
92
+ progress_key: Key[str],
93
+ state_channel: Key[str],
94
+ task_key: Arg[str],
95
+ completed_at: Arg[str],
96
+ state_payload: Arg[str],
97
+ ) -> bytes: ...
@@ -2,9 +2,11 @@
2
2
 
3
3
  The ``@redis_script`` decorator collapses each Lua-backed Redis operation
4
4
  into a single callable :class:`RedisScript` whose signature *is* the
5
- declared function's calling contract and whose docstring *is* the Lua
6
- source. Compared with the hand-rolled ``redis.register_script`` +
7
- lazy-singleton pattern, every script gains:
5
+ declared function's calling contract. The Lua source lives in
6
+ ``protocol/`` at the repository root, shared by every language's docket,
7
+ and a copy ships in this package's ``lua/`` directory. Compared with the
8
+ hand-rolled ``redis.register_script`` + lazy-singleton pattern, every
9
+ script gains:
8
10
 
9
11
  * SHA1 computed once at decoration time and reused forever -- no
10
12
  per-call ``register_script`` hash work.
@@ -34,25 +36,36 @@ Authoring shape:
34
36
  worker: Arg[str],
35
37
  started_at: Arg[str],
36
38
  generation: Arg[int],
37
- ) -> bytes:
38
- \"\"\"
39
- local runs_key = KEYS[1]
40
- -- ... Lua body ...
41
- return 'OK'
42
- \"\"\"
43
- ...
44
-
45
- The trailing ``...`` is the standard Python stub idiom -- pyright
46
- recognises ``docstring + Ellipsis`` as a stub body and stops asking the
47
- function to ``return`` anything, so no per-function ``# type: ignore``
48
- is needed.
39
+ ) -> bytes: ...
40
+
41
+ The stub's name picks its script: ``_claim`` runs ``lua/claim.lua``. Each
42
+ script starts with a header that binds ``KEYS`` and ``ARGV`` to local
43
+ names, then a blank line, then the body:
44
+
45
+ .. code-block:: lua
46
+
47
+ local runs_key = KEYS[1]
48
+ local progress_key = KEYS[2]
49
+ local worker = ARGV[1]
50
+ local started_at = ARGV[2]
51
+ local generation = tonumber(ARGV[3])
52
+
53
+ -- ... Lua body ...
54
+ return 'OK'
55
+
56
+ The header is the calling contract that every language follows, so the
57
+ decorator checks it against the signature when the module loads, and a
58
+ script whose header and signature disagree fails at import.
49
59
  """
50
60
 
51
61
  from __future__ import annotations
52
62
 
53
63
  import functools
54
64
  import inspect
65
+ import sys
66
+ from importlib.resources import files
55
67
  from typing import (
68
+ TYPE_CHECKING,
56
69
  Annotated,
57
70
  Any,
58
71
  Awaitable,
@@ -73,6 +86,12 @@ from redis.commands.core import AsyncScript
73
86
 
74
87
  from ._redis import Pipeline, RedisClient
75
88
 
89
+ if TYPE_CHECKING:
90
+ if sys.version_info >= (3, 11):
91
+ from importlib.resources.abc import Traversable
92
+ else:
93
+ from importlib.abc import Traversable
94
+
76
95
 
77
96
  # Marker classes used as ``Annotated`` metadata. The class objects
78
97
  # themselves go into ``__metadata__`` -- no instances needed -- and the
@@ -323,23 +342,29 @@ class RedisScript(Generic[_P, _R]):
323
342
  pipeline.eval(self.lua, len(keys), *keys, *argv)
324
343
 
325
344
 
326
- def redis_script(
327
- fn: Callable[Concatenate[RedisClient, _P], Awaitable[_R]],
328
- ) -> RedisScript[_P, _R]:
329
- """Wrap an async function declaring a Lua script as its docstring.
345
+ class ScriptDirectory:
346
+ """A directory of Lua scripts, used as the ``@redis_script`` decorator.
330
347
 
331
- Returns a :class:`RedisScript`: call it exactly like the declared
332
- function to execute immediately, or use ``.enqueue(pipeline, ...)`` to
333
- queue it on a pipeline. See the module docstring for the authoring
334
- contract and the encoding rules applied to ``Arg`` / ``Args``
335
- parameters.
348
+ Decorating a stub returns a :class:`RedisScript` that runs the file
349
+ named after the stub, without its leading underscores: ``_claim`` runs
350
+ ``claim.lua``. Call it exactly like the declared function to execute
351
+ immediately, or use ``.enqueue(pipeline, ...)`` to queue it on a
352
+ pipeline. See the module docstring for the authoring contract and the
353
+ encoding rules applied to ``Arg`` / ``Args`` parameters.
336
354
  """
337
- body = inspect.getdoc(fn)
338
- if not body:
339
- raise TypeError(
340
- f"@redis_script function {fn.__qualname__} needs a Lua body in its docstring"
341
- )
342
355
 
356
+ def __init__(self, directory: Traversable) -> None:
357
+ self._directory = directory
358
+
359
+ def __call__(
360
+ self, fn: Callable[Concatenate[RedisClient, _P], Awaitable[_R]]
361
+ ) -> RedisScript[_P, _R]:
362
+ return _wrap(fn, self._directory)
363
+
364
+
365
+ def _wrap(
366
+ fn: Callable[Concatenate[RedisClient, _P], Awaitable[_R]], directory: Traversable
367
+ ) -> RedisScript[_P, _R]:
343
368
  sig = inspect.signature(fn)
344
369
  hints = get_type_hints(fn, include_extras=True)
345
370
 
@@ -390,10 +415,22 @@ def redis_script(
390
415
  f"Args[...] must be the last parameter"
391
416
  )
392
417
 
393
- preamble = _generate_preamble(key_params, arg_params)
394
- lua = f"{preamble}\n\n{body}" if preamble else body
418
+ # Every language binds KEYS and ARGV the way this header says, so a
419
+ # header that drifts from the signature would send values to the wrong
420
+ # slots. The blank line ends the header, so an extra binding fails too.
421
+ script_name = fn.__name__.lstrip("_")
422
+ source = (directory / f"{script_name}.lua").read_text(encoding="utf-8")
423
+ header = _generate_preamble(key_params, arg_params)
424
+ if not source.startswith(f"{header}\n\n"):
425
+ raise TypeError(
426
+ f"@redis_script: the Lua header for {fn.__qualname__} must be "
427
+ f"exactly these lines, then a blank line:\n{header}"
428
+ )
429
+
430
+ return RedisScript(fn, source, key_params, arg_params)
431
+
395
432
 
396
- return RedisScript(fn, lua, key_params, arg_params)
433
+ redis_script = ScriptDirectory(files("docket") / "lua")
397
434
 
398
435
 
399
436
  __all__ = [
@@ -401,5 +438,6 @@ __all__ = [
401
438
  "Args",
402
439
  "Key",
403
440
  "RedisScript",
441
+ "ScriptDirectory",
404
442
  "redis_script",
405
443
  ]
@@ -85,14 +85,7 @@ async def _refresh_lease(
85
85
  lease_key: Key[str],
86
86
  holder: Arg[str],
87
87
  duration_ms: Arg[int],
88
- ) -> int:
89
- """
90
- if redis.call('GET', lease_key) == holder then
91
- return redis.call('PEXPIRE', lease_key, duration_ms)
92
- end
93
- return 0
94
- """
95
- ...
88
+ ) -> int: ...
96
89
 
97
90
 
98
91
  async def renew_leases(