durable-workflow 0.4.87__tar.gz → 0.4.89__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 (67) hide show
  1. {durable_workflow-0.4.87/src/durable_workflow.egg-info → durable_workflow-0.4.89}/PKG-INFO +1 -1
  2. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/pyproject.toml +1 -1
  3. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/client.py +33 -5
  4. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/worker.py +5 -0
  5. {durable_workflow-0.4.87 → durable_workflow-0.4.89/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  6. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_client.py +32 -3
  7. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_release_docs_audit_workflow.py +6 -0
  8. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_worker.py +53 -0
  9. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/LICENSE +0 -0
  10. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/README.md +0 -0
  11. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/setup.cfg +0 -0
  12. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/__init__.py +0 -0
  13. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/_avro.py +0 -0
  14. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/activity.py +0 -0
  15. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/auth_composition.py +0 -0
  16. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/errors.py +0 -0
  17. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/external_storage.py +0 -0
  18. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/external_task_input.py +0 -0
  19. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/external_task_result.py +0 -0
  20. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/history_bundle_verify.py +0 -0
  21. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/interceptors.py +0 -0
  22. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/invocable.py +0 -0
  23. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/metrics.py +0 -0
  24. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/py.typed +0 -0
  25. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/python_conformance.py +0 -0
  26. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/replay_conformance.py +0 -0
  27. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/replay_verify.py +0 -0
  28. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/retry_policy.py +0 -0
  29. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/serializer.py +0 -0
  30. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/sync.py +0 -0
  31. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/testing.py +0 -0
  32. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow/workflow.py +0 -0
  33. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  34. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  35. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  36. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow.egg-info/requires.txt +0 -0
  37. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/src/durable_workflow.egg-info/top_level.txt +0 -0
  38. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_activity_context.py +0 -0
  39. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_auth_composition.py +0 -0
  40. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_control_plane_parity_fixtures.py +0 -0
  41. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_errors.py +0 -0
  42. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_external_storage.py +0 -0
  43. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_external_task_input.py +0 -0
  44. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_external_task_result.py +0 -0
  45. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_golden_history_replay.py +0 -0
  46. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_history_bundle_verify.py +0 -0
  47. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_history_event_contract.py +0 -0
  48. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_invocable.py +0 -0
  49. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_metrics.py +0 -0
  50. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_order_processing_example.py +0 -0
  51. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_public_boundary_scanner.py +0 -0
  52. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_python_conformance.py +0 -0
  53. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_queries.py +0 -0
  54. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_readme_quickstart.py +0 -0
  55. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_replay.py +0 -0
  56. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_replay_conformance.py +0 -0
  57. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_replay_verify.py +0 -0
  58. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_retry_policy.py +0 -0
  59. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_schedules.py +0 -0
  60. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_serializer.py +0 -0
  61. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_signals.py +0 -0
  62. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_sleep.py +0 -0
  63. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_standalone_activity_client.py +0 -0
  64. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_sync.py +0 -0
  65. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_testing_harness.py +0 -0
  66. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_updates.py +0 -0
  67. {durable_workflow-0.4.87 → durable_workflow-0.4.89}/tests/test_wait_condition.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 0.4.87
3
+ Version: 0.4.89
4
4
  Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "0.4.87"
7
+ version = "0.4.89"
8
8
  description = "Python SDK for the Durable Workflow server (language-neutral HTTP protocol)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -3242,7 +3242,14 @@ class Client:
3242
3242
  return {"task": None, "poll_status": "timeout"}
3243
3243
 
3244
3244
  async def poll_workflow_task(
3245
- self, *, worker_id: str, task_queue: str, timeout: float = 35.0
3245
+ self,
3246
+ *,
3247
+ worker_id: str,
3248
+ task_queue: str,
3249
+ timeout: float = 35.0,
3250
+ build_id: str | None = None,
3251
+ history_page_size: int | None = None,
3252
+ poll_request_id: str | None = None,
3246
3253
  ) -> Any:
3247
3254
  """Long-poll for the next workflow task on ``task_queue``.
3248
3255
 
@@ -3254,6 +3261,9 @@ class Client:
3254
3261
  worker_id=worker_id,
3255
3262
  task_queue=task_queue,
3256
3263
  timeout=timeout,
3264
+ build_id=build_id,
3265
+ history_page_size=history_page_size,
3266
+ poll_request_id=poll_request_id,
3257
3267
  )
3258
3268
 
3259
3269
  return data.get("task")
@@ -3341,7 +3351,13 @@ class Client:
3341
3351
  )
3342
3352
 
3343
3353
  async def poll_query_task(
3344
- self, *, worker_id: str, task_queue: str, timeout: float = 35.0
3354
+ self,
3355
+ *,
3356
+ worker_id: str,
3357
+ task_queue: str,
3358
+ timeout: float = 35.0,
3359
+ build_id: str | None = None,
3360
+ poll_request_id: str | None = None,
3345
3361
  ) -> Any:
3346
3362
  """Long-poll for the next workflow query task on ``task_queue``.
3347
3363
 
@@ -3351,8 +3367,11 @@ class Client:
3351
3367
  body: dict[str, Any] = {
3352
3368
  "worker_id": worker_id,
3353
3369
  "task_queue": task_queue,
3354
- "poll_request_id": f"query-poll-{uuid.uuid4().hex}",
3370
+ "poll_request_id": poll_request_id or f"query-poll-{uuid.uuid4().hex}",
3355
3371
  }
3372
+ if build_id:
3373
+ body["build_id"] = build_id
3374
+
3356
3375
  for _ in range(2):
3357
3376
  try:
3358
3377
  data = await self._request(
@@ -3426,7 +3445,13 @@ class Client:
3426
3445
  )
3427
3446
 
3428
3447
  async def poll_activity_task(
3429
- self, *, worker_id: str, task_queue: str, timeout: float = 35.0
3448
+ self,
3449
+ *,
3450
+ worker_id: str,
3451
+ task_queue: str,
3452
+ timeout: float = 35.0,
3453
+ build_id: str | None = None,
3454
+ poll_request_id: str | None = None,
3430
3455
  ) -> Any:
3431
3456
  """Long-poll for the next activity task on ``task_queue``.
3432
3457
 
@@ -3436,8 +3461,11 @@ class Client:
3436
3461
  body: dict[str, Any] = {
3437
3462
  "worker_id": worker_id,
3438
3463
  "task_queue": task_queue,
3439
- "poll_request_id": f"activity-poll-{uuid.uuid4().hex}",
3464
+ "poll_request_id": poll_request_id or f"activity-poll-{uuid.uuid4().hex}",
3440
3465
  }
3466
+ if build_id:
3467
+ body["build_id"] = build_id
3468
+
3441
3469
  for _ in range(2):
3442
3470
  try:
3443
3471
  data = await self._request(
@@ -1588,6 +1588,7 @@ class Worker:
1588
1588
  worker_id=self.worker_id,
1589
1589
  task_queue=self.task_queue,
1590
1590
  timeout=self._poll_http_timeout,
1591
+ build_id=self.build_id,
1591
1592
  )
1592
1593
  except Exception as e:
1593
1594
  self._wf_semaphore.release()
@@ -1665,6 +1666,7 @@ class Worker:
1665
1666
  worker_id=self.worker_id,
1666
1667
  task_queue=self.task_queue,
1667
1668
  timeout=self._poll_http_timeout,
1669
+ build_id=self.build_id,
1668
1670
  )
1669
1671
  except Exception as e:
1670
1672
  self._act_semaphore.release()
@@ -1702,6 +1704,7 @@ class Worker:
1702
1704
  worker_id=self.worker_id,
1703
1705
  task_queue=self.task_queue,
1704
1706
  timeout=self._poll_http_timeout,
1707
+ build_id=self.build_id,
1705
1708
  )
1706
1709
  except Exception as e:
1707
1710
  self._record_poll_metrics("query", "error", time.perf_counter() - poll_start)
@@ -1960,6 +1963,7 @@ class Worker:
1960
1963
  worker_id=self.worker_id,
1961
1964
  task_queue=self.task_queue,
1962
1965
  timeout=self._poll_http_timeout,
1966
+ build_id=self.build_id,
1963
1967
  )
1964
1968
  self._record_poll_metrics(
1965
1969
  "workflow",
@@ -1990,6 +1994,7 @@ class Worker:
1990
1994
  worker_id=self.worker_id,
1991
1995
  task_queue=self.task_queue,
1992
1996
  timeout=self._poll_http_timeout,
1997
+ build_id=self.build_id,
1993
1998
  )
1994
1999
  self._record_poll_metrics(
1995
2000
  "activity",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 0.4.87
3
+ Version: 0.4.89
4
4
  Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -2302,10 +2302,15 @@ class TestFailWorkflowTask:
2302
2302
  "poll_status": "no_compatible_worker",
2303
2303
  }
2304
2304
 
2305
- with patch.object(client, "poll_workflow_task_response", new_callable=AsyncMock, return_value=response):
2306
- task = await client.poll_workflow_task(worker_id="worker-v2", task_queue="queue-1")
2305
+ with patch.object(client, "poll_workflow_task_response", new_callable=AsyncMock, return_value=response) as mock:
2306
+ task = await client.poll_workflow_task(
2307
+ worker_id="worker-v2",
2308
+ task_queue="queue-1",
2309
+ build_id="build-v2",
2310
+ )
2307
2311
 
2308
2312
  assert task is None
2313
+ assert mock.await_args.kwargs["build_id"] == "build-v2"
2309
2314
 
2310
2315
  @pytest.mark.asyncio
2311
2316
  async def test_poll_workflow_task_retries_once_with_same_poll_request_id_after_timeout(
@@ -2335,12 +2340,36 @@ class TestFailWorkflowTask:
2335
2340
  response_task = {"task": {"task_id": "activity-task-123"}}
2336
2341
 
2337
2342
  with patch.object(client, "_request", new_callable=AsyncMock, return_value=response_task) as mock:
2338
- task = await client.poll_activity_task(worker_id="worker-1", task_queue="queue-1")
2343
+ task = await client.poll_activity_task(
2344
+ worker_id="worker-1",
2345
+ task_queue="queue-1",
2346
+ build_id="build-1",
2347
+ )
2348
+
2349
+ assert task == response_task["task"]
2350
+ request_body = mock.await_args.kwargs["json"]
2351
+ assert request_body["worker_id"] == "worker-1"
2352
+ assert request_body["task_queue"] == "queue-1"
2353
+ assert request_body["build_id"] == "build-1"
2354
+ assert isinstance(request_body["poll_request_id"], str)
2355
+ assert request_body["poll_request_id"] != ""
2356
+
2357
+ @pytest.mark.asyncio
2358
+ async def test_poll_query_task_sends_build_id(self, client: Client) -> None:
2359
+ response_task = {"task": {"query_task_id": "query-task-123"}}
2360
+
2361
+ with patch.object(client, "_request", new_callable=AsyncMock, return_value=response_task) as mock:
2362
+ task = await client.poll_query_task(
2363
+ worker_id="worker-1",
2364
+ task_queue="queue-1",
2365
+ build_id="build-1",
2366
+ )
2339
2367
 
2340
2368
  assert task == response_task["task"]
2341
2369
  request_body = mock.await_args.kwargs["json"]
2342
2370
  assert request_body["worker_id"] == "worker-1"
2343
2371
  assert request_body["task_queue"] == "queue-1"
2372
+ assert request_body["build_id"] == "build-1"
2344
2373
  assert isinstance(request_body["poll_request_id"], str)
2345
2374
  assert request_body["poll_request_id"] != ""
2346
2375
 
@@ -26,9 +26,11 @@ def test_publish_workflow_runs_docs_audit_in_recoverable_job() -> None:
26
26
  "DOCS_RELEASE_AUDIT_ARTIFACT: sdk-python",
27
27
  "DOCS_RELEASE_AUDIT_VERSION: ${{ github.ref_name }}",
28
28
  "DOCS_RELEASE_AUDIT_EVIDENCE: docs-release-audit-evidence.json",
29
+ "DOCS_RELEASE_AUDIT_HANDOFF: docs-release-audit-handoff.json",
29
30
  "scripts/ci/check-docs-release-audit.sh",
30
31
  "Upload docs release audit evidence",
31
32
  "docs-release-audit-evidence.json",
33
+ "docs-release-audit-handoff.json",
32
34
  ]:
33
35
  assert expected in workflow
34
36
 
@@ -39,10 +41,14 @@ def test_publish_workflow_runs_docs_audit_in_recoverable_job() -> None:
39
41
  assert "Upload docs release audit evidence" not in publish_job
40
42
  assert "needs: publish" in docs_audit_job
41
43
  assert "durable-workflow.release.docs-release-audit-evidence" in auditor
44
+ assert "durable-workflow.release.docs-artifact-tuple-handoff" in auditor
45
+ assert "DOCS_RELEASE_AUDIT_HANDOFF" in auditor
42
46
  assert "schema: 'durable-workflow.docs.refresh-request'" in auditor
43
47
  assert "repository: 'durable-workflow.github.io'" in auditor
44
48
  assert "refresh_command: 'npm run refresh:public-artifact-versions'" in auditor
49
+ assert "refresh_files: refreshFiles" in auditor
45
50
  assert "observed_artifact_versions: versions" in auditor
51
+ assert "docs_artifact_tuple_handoff: handoff" in auditor
46
52
 
47
53
  publish_offset = workflow.index("Publish to PyPI")
48
54
  docs_audit_job_offset = workflow.index("verify-docs-release-audit:")
@@ -403,6 +403,59 @@ class TestWorkerRegistration:
403
403
  call_kwargs = mock_client.register_worker.call_args.kwargs
404
404
  assert call_kwargs["build_id"] == "release-2026.04.22-a1"
405
405
 
406
+ @pytest.mark.asyncio
407
+ async def test_poll_loops_forward_build_id_when_configured(
408
+ self, mock_client: AsyncMock
409
+ ) -> None:
410
+ workflow_called = asyncio.Event()
411
+ activity_called = asyncio.Event()
412
+ worker = Worker(
413
+ mock_client,
414
+ task_queue="q1",
415
+ workflows=[TestWorkflow],
416
+ activities=[echo_activity],
417
+ worker_id="w-build-poll",
418
+ build_id="release-2026.04.22-a1",
419
+ poll_timeout=0.01,
420
+ )
421
+
422
+ async def workflow_poll_once(**_: object) -> None:
423
+ workflow_called.set()
424
+ await worker.stop()
425
+ return None
426
+
427
+ async def activity_poll_once(**_: object) -> None:
428
+ activity_called.set()
429
+ await worker.stop()
430
+ return None
431
+
432
+ mock_client.poll_workflow_task.side_effect = workflow_poll_once
433
+ await worker._poll_workflow_tasks()
434
+ workflow_kwargs = mock_client.poll_workflow_task.call_args.kwargs
435
+ assert workflow_called.is_set()
436
+ assert workflow_kwargs["build_id"] == "release-2026.04.22-a1"
437
+
438
+ worker._stop.clear()
439
+ mock_client.poll_activity_task.side_effect = activity_poll_once
440
+ await worker._poll_activity_tasks()
441
+ activity_kwargs = mock_client.poll_activity_task.call_args.kwargs
442
+ assert activity_called.is_set()
443
+ assert activity_kwargs["build_id"] == "release-2026.04.22-a1"
444
+
445
+ worker._stop.clear()
446
+ query_called = asyncio.Event()
447
+
448
+ async def query_poll_once(**_: object) -> None:
449
+ query_called.set()
450
+ await worker.stop()
451
+ return None
452
+
453
+ mock_client.poll_query_task.side_effect = query_poll_once
454
+ await worker._poll_query_tasks()
455
+ query_kwargs = mock_client.poll_query_task.call_args.kwargs
456
+ assert query_called.is_set()
457
+ assert query_kwargs["build_id"] == "release-2026.04.22-a1"
458
+
406
459
  @pytest.mark.asyncio
407
460
  async def test_register_omits_build_id_when_not_configured(
408
461
  self, mock_client: AsyncMock