durable-workflow 0.2.0__tar.gz → 0.3.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.
Files changed (46) hide show
  1. durable_workflow-0.3.0/PKG-INFO +275 -0
  2. durable_workflow-0.3.0/README.md +239 -0
  3. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/pyproject.toml +12 -5
  4. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/__init__.py +18 -2
  5. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/activity.py +21 -0
  6. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/client.py +395 -24
  7. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/errors.py +81 -5
  8. durable_workflow-0.3.0/src/durable_workflow/metrics.py +140 -0
  9. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/retry_policy.py +20 -2
  10. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/sync.py +26 -2
  11. durable_workflow-0.3.0/src/durable_workflow/testing.py +267 -0
  12. durable_workflow-0.3.0/src/durable_workflow/worker.py +850 -0
  13. durable_workflow-0.3.0/src/durable_workflow/workflow.py +1154 -0
  14. durable_workflow-0.3.0/src/durable_workflow.egg-info/PKG-INFO +275 -0
  15. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow.egg-info/SOURCES.txt +10 -0
  16. durable_workflow-0.3.0/src/durable_workflow.egg-info/requires.txt +15 -0
  17. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_client.py +265 -0
  18. durable_workflow-0.3.0/tests/test_metrics.py +123 -0
  19. durable_workflow-0.3.0/tests/test_order_processing_example.py +71 -0
  20. durable_workflow-0.3.0/tests/test_queries.py +131 -0
  21. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_replay.py +138 -3
  22. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_retry_policy.py +4 -1
  23. durable_workflow-0.3.0/tests/test_signals.py +158 -0
  24. durable_workflow-0.3.0/tests/test_sleep.py +64 -0
  25. durable_workflow-0.3.0/tests/test_testing_harness.py +221 -0
  26. durable_workflow-0.3.0/tests/test_updates.py +225 -0
  27. durable_workflow-0.3.0/tests/test_wait_condition.py +235 -0
  28. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_worker.py +260 -22
  29. durable_workflow-0.2.0/PKG-INFO +0 -137
  30. durable_workflow-0.2.0/README.md +0 -105
  31. durable_workflow-0.2.0/src/durable_workflow/worker.py +0 -445
  32. durable_workflow-0.2.0/src/durable_workflow/workflow.py +0 -425
  33. durable_workflow-0.2.0/src/durable_workflow.egg-info/PKG-INFO +0 -137
  34. durable_workflow-0.2.0/src/durable_workflow.egg-info/requires.txt +0 -8
  35. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/LICENSE +0 -0
  36. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/setup.cfg +0 -0
  37. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/_avro.py +0 -0
  38. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/py.typed +0 -0
  39. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow/serializer.py +0 -0
  40. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  41. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
  42. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_activity_context.py +0 -0
  43. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_errors.py +0 -0
  44. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_schedules.py +0 -0
  45. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_serializer.py +0 -0
  46. {durable_workflow-0.2.0 → durable_workflow-0.3.0}/tests/test_sync.py +0 -0
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.4
2
+ Name: durable-workflow
3
+ Version: 0.3.0
4
+ Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
5
+ Author: Durable Workflow Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/durable-workflow/sdk-python
8
+ Project-URL: Documentation, https://durable-workflow.github.io/docs/2.0/polyglot/python
9
+ Project-URL: Repository, https://github.com/durable-workflow/sdk-python
10
+ Project-URL: Issues, https://github.com/zorporation/durable-workflow/issues
11
+ Keywords: workflow,durable,orchestration,temporal,saga
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: avro<2,>=1.12
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
28
+ Requires-Dist: mypy>=1.10; extra == "dev"
29
+ Requires-Dist: ruff>=0.4; extra == "dev"
30
+ Provides-Extra: prometheus
31
+ Requires-Dist: prometheus-client>=0.20; extra == "prometheus"
32
+ Provides-Extra: docs
33
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
34
+ Requires-Dist: mkdocstrings[python]>=0.25; extra == "docs"
35
+ Dynamic: license-file
36
+
37
+ # Durable Workflow (Python SDK)
38
+
39
+ A Python SDK for the [Durable Workflow server](https://github.com/durable-workflow/server). Speaks the server's language-neutral HTTP/JSON worker protocol — no PHP runtime required.
40
+
41
+ Status: **Alpha**. Core features implemented: workflows, activities, schedules, signals, timers, child workflows, continue-as-new, side effects, version markers, and worker-applied accepted updates. Client calls for queries and updates exist; Python workflow-side query receiver metadata is available, while server-routed Python query execution and pre-accept update validator routing are still in progress. Full language-neutral protocol support for cross-PHP/Python orchestration is the release goal.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install durable-workflow
47
+ ```
48
+
49
+ Or for development:
50
+
51
+ ```bash
52
+ pip install -e '.[dev]'
53
+ ```
54
+
55
+ ## Quickstart
56
+
57
+ ```python
58
+ from durable_workflow import Client, Worker, workflow, activity
59
+
60
+ @activity.defn(name="greet")
61
+ def greet(name: str) -> str:
62
+ return f"hello, {name}"
63
+
64
+ @workflow.defn(name="greeter")
65
+ class GreeterWorkflow:
66
+ def run(self, ctx, name):
67
+ result = yield ctx.schedule_activity("greet", [name])
68
+ return result
69
+
70
+ async def main():
71
+ client = Client("http://server:8080", token="dev-token-123", namespace="default")
72
+ worker = Worker(
73
+ client,
74
+ task_queue="python-workers",
75
+ workflows=[GreeterWorkflow],
76
+ activities=[greet],
77
+ )
78
+ handle = await client.start_workflow(
79
+ workflow_type="greeter",
80
+ workflow_id="greet-1",
81
+ task_queue="python-workers",
82
+ input=["world"],
83
+ )
84
+ await worker.run_until(workflow_id="greet-1", timeout=30.0)
85
+ result = await client.get_result(handle)
86
+ print(result) # "hello, world"
87
+ ```
88
+
89
+ For a fuller deployable example, see
90
+ [`examples/order_processing`](examples/order_processing), which runs a
91
+ multi-activity order workflow against a local server with Docker Compose.
92
+
93
+ ## Activity retries and timeouts
94
+
95
+ Configure per-call activity retries and deadlines from workflow code:
96
+
97
+ ```python
98
+ from durable_workflow import ActivityRetryPolicy
99
+
100
+ result = yield ctx.schedule_activity(
101
+ "charge-card",
102
+ [order],
103
+ retry_policy=ActivityRetryPolicy(
104
+ max_attempts=4,
105
+ initial_interval_seconds=1,
106
+ backoff_coefficient=2,
107
+ maximum_interval_seconds=30,
108
+ non_retryable_error_types=["ValidationError"],
109
+ ),
110
+ start_to_close_timeout=120,
111
+ schedule_to_close_timeout=300,
112
+ heartbeat_timeout=15,
113
+ )
114
+ ```
115
+
116
+ Child workflow starts use the same retry policy shape and workflow-level
117
+ execution/run timeout names:
118
+
119
+ ```python
120
+ from durable_workflow import ChildWorkflowRetryPolicy
121
+
122
+ receipt = yield ctx.start_child_workflow(
123
+ "payment.child",
124
+ [order],
125
+ retry_policy=ChildWorkflowRetryPolicy(
126
+ max_attempts=3,
127
+ initial_interval_seconds=2,
128
+ backoff_coefficient=2,
129
+ non_retryable_error_types=["ValidationError"],
130
+ ),
131
+ execution_timeout_seconds=600,
132
+ run_timeout_seconds=120,
133
+ )
134
+ ```
135
+
136
+ ## Workflow signals, queries, and updates
137
+
138
+ Signals mutate workflow state during replay:
139
+
140
+ ```python
141
+ @workflow.defn(name="approval")
142
+ class ApprovalWorkflow:
143
+ def __init__(self) -> None:
144
+ self.approved = False
145
+
146
+ @workflow.signal("approve")
147
+ def approve(self, by: str) -> None:
148
+ self.approved = True
149
+
150
+ @workflow.query("status")
151
+ def status(self) -> dict:
152
+ return {"approved": self.approved}
153
+
154
+ @workflow.update("set_approval")
155
+ def set_approval(self, approved: bool) -> dict:
156
+ self.approved = approved
157
+ return {"approved": self.approved}
158
+
159
+ @set_approval.validator
160
+ def validate_set_approval(self, approved: bool) -> None:
161
+ if not isinstance(approved, bool):
162
+ raise ValueError("approved must be boolean")
163
+ ```
164
+
165
+ The Python SDK records query and update receiver metadata on workflow classes,
166
+ exposes a query-state replay helper, and applies accepted updates on Python
167
+ workflow tasks by emitting `complete_update` or `fail_update` back to the
168
+ server. Query routing and synchronous pre-accept update validator execution are
169
+ still server-side follow-ups; use those paths only with deployments that
170
+ advertise support for the target workflow type.
171
+
172
+ ## Features
173
+
174
+ - **Async-first**: Built on `httpx` and `asyncio`
175
+ - **Type-safe**: Full type hints, passes `mypy --strict`
176
+ - **Polyglot**: Works alongside PHP workers on the same task queue
177
+ - **HTTP/JSON protocol**: No gRPC, no protobuf dependencies
178
+ - **Codec envelopes**: Avro payloads by default, with JSON decode compatibility for existing history
179
+ - **Metrics hooks**: Pluggable counters and histograms, with an optional Prometheus adapter
180
+
181
+ ## Authentication
182
+
183
+ For local servers that use one shared bearer token, pass `token=`:
184
+
185
+ ```python
186
+ client = Client("http://server:8080", token="shared-token", namespace="default")
187
+ ```
188
+
189
+ For production servers with role-scoped tokens, pass separate credentials for
190
+ control-plane calls and worker-plane polling:
191
+
192
+ ```python
193
+ client = Client(
194
+ "https://workflow.example.internal",
195
+ control_token="operator-token",
196
+ worker_token="worker-token",
197
+ namespace="orders",
198
+ )
199
+ ```
200
+
201
+ Create one client per namespace when your deployment issues namespace-scoped
202
+ tokens. The SDK sends the configured token as `Authorization: Bearer ...` and
203
+ the namespace as `X-Namespace` on every request.
204
+
205
+ ## Metrics
206
+
207
+ Pass a recorder to `Client(metrics=...)` or `Worker(metrics=...)` to collect request, poll, and task metrics. The SDK ships a no-op default, an `InMemoryMetrics` recorder for tests or custom exporter loops, and `PrometheusMetrics` for deployments that install the optional extra:
208
+
209
+ ```bash
210
+ pip install 'durable-workflow[prometheus]'
211
+ ```
212
+
213
+ ```python
214
+ from durable_workflow import Client, PrometheusMetrics
215
+
216
+ metrics = PrometheusMetrics()
217
+ client = Client("http://server:8080", token="dev-token-123", metrics=metrics)
218
+ ```
219
+
220
+ Custom recorders implement `increment(name, value=1.0, tags=None)` and `record(name, value, tags=None)`.
221
+
222
+ ## Documentation
223
+
224
+ Full documentation is available at
225
+ [durable-workflow.github.io/docs/2.0/polyglot/python](https://durable-workflow.github.io/docs/2.0/polyglot/python):
226
+
227
+ - [Python SDK guide](https://durable-workflow.com/docs/2.0/polyglot/python)
228
+ - [API reference](https://python.durable-workflow.com/)
229
+
230
+ ## Requirements
231
+
232
+ - Python ≥ 3.10
233
+ - A running [Durable Workflow server](https://github.com/durable-workflow/server)
234
+
235
+ ## Compatibility
236
+
237
+ SDK version 0.2.x is compatible with servers that advertise these protocol
238
+ manifests from `GET /api/cluster/info`:
239
+
240
+ - `control_plane.version: "2"`
241
+ - `control_plane.request_contract.schema: durable-workflow.v2.control-plane-request.contract` version `1`
242
+ - `worker_protocol.version: "1.0"`
243
+
244
+ The top-level server `version` is build identity only. The worker checks these
245
+ protocol manifests at startup and fails closed when compatibility is missing,
246
+ unknown, or undiscoverable.
247
+
248
+ ## Development
249
+
250
+ ```bash
251
+ # Install dev dependencies
252
+ pip install -e '.[dev]'
253
+
254
+ # Run tests
255
+ pytest
256
+
257
+ # Run integration tests (requires Docker)
258
+ pytest -m integration
259
+
260
+ # Type check
261
+ mypy src/durable_workflow/
262
+
263
+ # Lint
264
+ ruff check src/ tests/
265
+
266
+ # Preview the API reference site locally
267
+ pip install -e '.[docs]'
268
+ mkdocs serve
269
+ ```
270
+
271
+ The API reference is published to [python.durable-workflow.com](https://python.durable-workflow.com/) and rebuilt automatically on push to `main`.
272
+
273
+ ## License
274
+
275
+ MIT
@@ -0,0 +1,239 @@
1
+ # Durable Workflow (Python SDK)
2
+
3
+ A Python SDK for the [Durable Workflow server](https://github.com/durable-workflow/server). Speaks the server's language-neutral HTTP/JSON worker protocol — no PHP runtime required.
4
+
5
+ Status: **Alpha**. Core features implemented: workflows, activities, schedules, signals, timers, child workflows, continue-as-new, side effects, version markers, and worker-applied accepted updates. Client calls for queries and updates exist; Python workflow-side query receiver metadata is available, while server-routed Python query execution and pre-accept update validator routing are still in progress. Full language-neutral protocol support for cross-PHP/Python orchestration is the release goal.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install durable-workflow
11
+ ```
12
+
13
+ Or for development:
14
+
15
+ ```bash
16
+ pip install -e '.[dev]'
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ ```python
22
+ from durable_workflow import Client, Worker, workflow, activity
23
+
24
+ @activity.defn(name="greet")
25
+ def greet(name: str) -> str:
26
+ return f"hello, {name}"
27
+
28
+ @workflow.defn(name="greeter")
29
+ class GreeterWorkflow:
30
+ def run(self, ctx, name):
31
+ result = yield ctx.schedule_activity("greet", [name])
32
+ return result
33
+
34
+ async def main():
35
+ client = Client("http://server:8080", token="dev-token-123", namespace="default")
36
+ worker = Worker(
37
+ client,
38
+ task_queue="python-workers",
39
+ workflows=[GreeterWorkflow],
40
+ activities=[greet],
41
+ )
42
+ handle = await client.start_workflow(
43
+ workflow_type="greeter",
44
+ workflow_id="greet-1",
45
+ task_queue="python-workers",
46
+ input=["world"],
47
+ )
48
+ await worker.run_until(workflow_id="greet-1", timeout=30.0)
49
+ result = await client.get_result(handle)
50
+ print(result) # "hello, world"
51
+ ```
52
+
53
+ For a fuller deployable example, see
54
+ [`examples/order_processing`](examples/order_processing), which runs a
55
+ multi-activity order workflow against a local server with Docker Compose.
56
+
57
+ ## Activity retries and timeouts
58
+
59
+ Configure per-call activity retries and deadlines from workflow code:
60
+
61
+ ```python
62
+ from durable_workflow import ActivityRetryPolicy
63
+
64
+ result = yield ctx.schedule_activity(
65
+ "charge-card",
66
+ [order],
67
+ retry_policy=ActivityRetryPolicy(
68
+ max_attempts=4,
69
+ initial_interval_seconds=1,
70
+ backoff_coefficient=2,
71
+ maximum_interval_seconds=30,
72
+ non_retryable_error_types=["ValidationError"],
73
+ ),
74
+ start_to_close_timeout=120,
75
+ schedule_to_close_timeout=300,
76
+ heartbeat_timeout=15,
77
+ )
78
+ ```
79
+
80
+ Child workflow starts use the same retry policy shape and workflow-level
81
+ execution/run timeout names:
82
+
83
+ ```python
84
+ from durable_workflow import ChildWorkflowRetryPolicy
85
+
86
+ receipt = yield ctx.start_child_workflow(
87
+ "payment.child",
88
+ [order],
89
+ retry_policy=ChildWorkflowRetryPolicy(
90
+ max_attempts=3,
91
+ initial_interval_seconds=2,
92
+ backoff_coefficient=2,
93
+ non_retryable_error_types=["ValidationError"],
94
+ ),
95
+ execution_timeout_seconds=600,
96
+ run_timeout_seconds=120,
97
+ )
98
+ ```
99
+
100
+ ## Workflow signals, queries, and updates
101
+
102
+ Signals mutate workflow state during replay:
103
+
104
+ ```python
105
+ @workflow.defn(name="approval")
106
+ class ApprovalWorkflow:
107
+ def __init__(self) -> None:
108
+ self.approved = False
109
+
110
+ @workflow.signal("approve")
111
+ def approve(self, by: str) -> None:
112
+ self.approved = True
113
+
114
+ @workflow.query("status")
115
+ def status(self) -> dict:
116
+ return {"approved": self.approved}
117
+
118
+ @workflow.update("set_approval")
119
+ def set_approval(self, approved: bool) -> dict:
120
+ self.approved = approved
121
+ return {"approved": self.approved}
122
+
123
+ @set_approval.validator
124
+ def validate_set_approval(self, approved: bool) -> None:
125
+ if not isinstance(approved, bool):
126
+ raise ValueError("approved must be boolean")
127
+ ```
128
+
129
+ The Python SDK records query and update receiver metadata on workflow classes,
130
+ exposes a query-state replay helper, and applies accepted updates on Python
131
+ workflow tasks by emitting `complete_update` or `fail_update` back to the
132
+ server. Query routing and synchronous pre-accept update validator execution are
133
+ still server-side follow-ups; use those paths only with deployments that
134
+ advertise support for the target workflow type.
135
+
136
+ ## Features
137
+
138
+ - **Async-first**: Built on `httpx` and `asyncio`
139
+ - **Type-safe**: Full type hints, passes `mypy --strict`
140
+ - **Polyglot**: Works alongside PHP workers on the same task queue
141
+ - **HTTP/JSON protocol**: No gRPC, no protobuf dependencies
142
+ - **Codec envelopes**: Avro payloads by default, with JSON decode compatibility for existing history
143
+ - **Metrics hooks**: Pluggable counters and histograms, with an optional Prometheus adapter
144
+
145
+ ## Authentication
146
+
147
+ For local servers that use one shared bearer token, pass `token=`:
148
+
149
+ ```python
150
+ client = Client("http://server:8080", token="shared-token", namespace="default")
151
+ ```
152
+
153
+ For production servers with role-scoped tokens, pass separate credentials for
154
+ control-plane calls and worker-plane polling:
155
+
156
+ ```python
157
+ client = Client(
158
+ "https://workflow.example.internal",
159
+ control_token="operator-token",
160
+ worker_token="worker-token",
161
+ namespace="orders",
162
+ )
163
+ ```
164
+
165
+ Create one client per namespace when your deployment issues namespace-scoped
166
+ tokens. The SDK sends the configured token as `Authorization: Bearer ...` and
167
+ the namespace as `X-Namespace` on every request.
168
+
169
+ ## Metrics
170
+
171
+ Pass a recorder to `Client(metrics=...)` or `Worker(metrics=...)` to collect request, poll, and task metrics. The SDK ships a no-op default, an `InMemoryMetrics` recorder for tests or custom exporter loops, and `PrometheusMetrics` for deployments that install the optional extra:
172
+
173
+ ```bash
174
+ pip install 'durable-workflow[prometheus]'
175
+ ```
176
+
177
+ ```python
178
+ from durable_workflow import Client, PrometheusMetrics
179
+
180
+ metrics = PrometheusMetrics()
181
+ client = Client("http://server:8080", token="dev-token-123", metrics=metrics)
182
+ ```
183
+
184
+ Custom recorders implement `increment(name, value=1.0, tags=None)` and `record(name, value, tags=None)`.
185
+
186
+ ## Documentation
187
+
188
+ Full documentation is available at
189
+ [durable-workflow.github.io/docs/2.0/polyglot/python](https://durable-workflow.github.io/docs/2.0/polyglot/python):
190
+
191
+ - [Python SDK guide](https://durable-workflow.com/docs/2.0/polyglot/python)
192
+ - [API reference](https://python.durable-workflow.com/)
193
+
194
+ ## Requirements
195
+
196
+ - Python ≥ 3.10
197
+ - A running [Durable Workflow server](https://github.com/durable-workflow/server)
198
+
199
+ ## Compatibility
200
+
201
+ SDK version 0.2.x is compatible with servers that advertise these protocol
202
+ manifests from `GET /api/cluster/info`:
203
+
204
+ - `control_plane.version: "2"`
205
+ - `control_plane.request_contract.schema: durable-workflow.v2.control-plane-request.contract` version `1`
206
+ - `worker_protocol.version: "1.0"`
207
+
208
+ The top-level server `version` is build identity only. The worker checks these
209
+ protocol manifests at startup and fails closed when compatibility is missing,
210
+ unknown, or undiscoverable.
211
+
212
+ ## Development
213
+
214
+ ```bash
215
+ # Install dev dependencies
216
+ pip install -e '.[dev]'
217
+
218
+ # Run tests
219
+ pytest
220
+
221
+ # Run integration tests (requires Docker)
222
+ pytest -m integration
223
+
224
+ # Type check
225
+ mypy src/durable_workflow/
226
+
227
+ # Lint
228
+ ruff check src/ tests/
229
+
230
+ # Preview the API reference site locally
231
+ pip install -e '.[docs]'
232
+ mkdocs serve
233
+ ```
234
+
235
+ The API reference is published to [python.durable-workflow.com](https://python.durable-workflow.com/) and rebuilt automatically on push to `main`.
236
+
237
+ ## License
238
+
239
+ MIT
@@ -1,14 +1,15 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61"]
2
+ requires = ["setuptools>=77"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "0.2.0"
7
+ version = "0.3.0"
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"
11
- license = { text = "MIT" }
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
12
13
  authors = [
13
14
  { name = "Durable Workflow Contributors" },
14
15
  ]
@@ -22,7 +23,6 @@ keywords = [
22
23
  classifiers = [
23
24
  "Development Status :: 3 - Alpha",
24
25
  "Intended Audience :: Developers",
25
- "License :: OSI Approved :: MIT License",
26
26
  "Programming Language :: Python :: 3",
27
27
  "Programming Language :: Python :: 3.10",
28
28
  "Programming Language :: Python :: 3.11",
@@ -42,10 +42,17 @@ dev = [
42
42
  "mypy>=1.10",
43
43
  "ruff>=0.4",
44
44
  ]
45
+ prometheus = [
46
+ "prometheus-client>=0.20",
47
+ ]
48
+ docs = [
49
+ "mkdocs-material>=9.5",
50
+ "mkdocstrings[python]>=0.25",
51
+ ]
45
52
 
46
53
  [project.urls]
47
54
  Homepage = "https://github.com/durable-workflow/sdk-python"
48
- Documentation = "https://durable-workflow.github.io/docs/2.0/sdks/python/"
55
+ Documentation = "https://durable-workflow.github.io/docs/2.0/polyglot/python"
49
56
  Repository = "https://github.com/durable-workflow/sdk-python"
50
57
  Issues = "https://github.com/zorporation/durable-workflow/issues"
51
58
 
@@ -6,7 +6,7 @@ try:
6
6
  except PackageNotFoundError: # source checkout without installed metadata
7
7
  __version__ = "0.0.0+unknown"
8
8
 
9
- from . import activity, sync, workflow
9
+ from . import activity, sync, testing, workflow
10
10
  from .activity import ActivityContext, ActivityInfo
11
11
  from .client import (
12
12
  Client,
@@ -40,14 +40,23 @@ from .errors import (
40
40
  WorkflowNotFound,
41
41
  WorkflowTerminated,
42
42
  )
43
+ from .metrics import (
44
+ InMemoryMetrics,
45
+ MetricsRecorder,
46
+ NoopMetrics,
47
+ PrometheusMetrics,
48
+ )
49
+ from .retry_policy import RetryPolicy, TransportRetryPolicy
43
50
  from .worker import Worker
44
- from .workflow import ContinueAsNew, StartChildWorkflow
51
+ from .workflow import ActivityRetryPolicy, ChildWorkflowRetryPolicy, ContinueAsNew, StartChildWorkflow
45
52
 
46
53
  __all__ = [
47
54
  "__version__",
48
55
  "ActivityCancelled",
49
56
  "ActivityContext",
50
57
  "ActivityInfo",
58
+ "ActivityRetryPolicy",
59
+ "ChildWorkflowRetryPolicy",
51
60
  "ChildWorkflowFailed",
52
61
  "Client",
53
62
  "ContinueAsNew",
@@ -68,12 +77,19 @@ __all__ = [
68
77
  "WorkflowList",
69
78
  "activity",
70
79
  "sync",
80
+ "testing",
71
81
  "workflow",
72
82
  "DurableWorkflowError",
73
83
  "InvalidArgument",
84
+ "InMemoryMetrics",
85
+ "MetricsRecorder",
74
86
  "NamespaceNotFound",
87
+ "NoopMetrics",
75
88
  "QueryFailed",
89
+ "PrometheusMetrics",
90
+ "RetryPolicy",
76
91
  "ServerError",
92
+ "TransportRetryPolicy",
77
93
  "Unauthorized",
78
94
  "UpdateRejected",
79
95
  "WorkflowAlreadyStarted",
@@ -20,6 +20,8 @@ _current_context: contextvars.ContextVar[ActivityContext | None] = contextvars.C
20
20
 
21
21
  @dataclass(frozen=True)
22
22
  class ActivityInfo:
23
+ """Metadata attached to the currently running activity attempt."""
24
+
23
25
  task_id: str
24
26
  activity_type: str
25
27
  activity_attempt_id: str
@@ -29,6 +31,8 @@ class ActivityInfo:
29
31
 
30
32
 
31
33
  class ActivityContext:
34
+ """Per-attempt activity context exposed by :func:`durable_workflow.activity.context`."""
35
+
32
36
  def __init__(
33
37
  self,
34
38
  *,
@@ -41,13 +45,26 @@ class ActivityContext:
41
45
 
42
46
  @property
43
47
  def info(self) -> ActivityInfo:
48
+ """Metadata for the currently running activity attempt."""
44
49
  return self._info
45
50
 
46
51
  @property
47
52
  def is_cancelled(self) -> bool:
53
+ """``True`` once the server has signalled that the owning workflow cancelled this activity."""
48
54
  return self._cancel_requested
49
55
 
50
56
  async def heartbeat(self, details: dict[str, Any] | None = None) -> None:
57
+ """Report liveness to the server and check for a cancellation request.
58
+
59
+ Long-running activities should call ``heartbeat()`` periodically so the
60
+ server can distinguish a slow-but-alive attempt from a dead worker.
61
+ Optional ``details`` are attached to the heartbeat and surface as the
62
+ activity's last-known progress on failure.
63
+
64
+ Raises :class:`~durable_workflow.errors.ActivityCancelled` when the
65
+ owning workflow has requested cancellation, so the activity can exit
66
+ cleanly at its next natural break point.
67
+ """
51
68
  resp = await self._client.heartbeat_activity_task(
52
69
  task_id=self._info.task_id,
53
70
  activity_attempt_id=self._info.activity_attempt_id,
@@ -60,6 +77,7 @@ class ActivityContext:
60
77
 
61
78
 
62
79
  def context() -> ActivityContext:
80
+ """Return the current activity attempt context."""
63
81
  ctx = _current_context.get()
64
82
  if ctx is None:
65
83
  raise RuntimeError("activity.context() called outside of an activity execution")
@@ -71,6 +89,8 @@ def _set_context(ctx: ActivityContext | None) -> contextvars.Token[ActivityConte
71
89
 
72
90
 
73
91
  def defn(*, name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
92
+ """Register a callable as an activity type under a language-neutral name."""
93
+
74
94
  def wrap(fn: Callable[..., Any]) -> Callable[..., Any]:
75
95
  fn.__activity_name__ = name # type: ignore[attr-defined]
76
96
  _REGISTRY[name] = fn
@@ -80,4 +100,5 @@ def defn(*, name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
80
100
 
81
101
 
82
102
  def registry() -> dict[str, Callable[..., Any]]:
103
+ """Return a copy of activity callables registered in this process."""
83
104
  return dict(_REGISTRY)