Skip to content

Commit 367cc7d

Browse files
#378: prepare sdk-python 0.2.0 release
1 parent 456dfdd commit 367cc7d

9 files changed

Lines changed: 47 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@ All notable changes to the `durable-workflow` Python SDK are documented here.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [0.1.1]unreleased
7+
## [0.2.0]2026-04-17
88

99
### Added
1010
- Runtime server version compatibility check at worker registration. On
1111
`Worker.run()`, the SDK now calls `/api/cluster/info` and refuses to
1212
register against a server whose major version falls outside the set the
13-
SDK knows how to talk to. This prevents a 0.1.x worker from silently
13+
SDK knows how to talk to. This prevents a 0.2.x worker from silently
1414
attempting to drive a future breaking-release server. (#302)
1515
- `Client.get_cluster_info()` — fetches the server version and declared
1616
capability manifest from `/api/cluster/info`.
17-
- Avro payload codec support (optional). Install with
18-
`pip install 'durable-workflow[avro]'` to pull in `apache/avro 1.12`.
17+
- Avro payload codec support as a core runtime dependency.
1918
`serializer.encode()`, `serializer.decode()`, and
2019
`serializer.envelope()` now accept a `codec=` argument, and
2120
`decode_envelope()` honors the inner codec tag. The Worker decodes
@@ -25,6 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2524
version: int}` record), byte-compatible with the PHP
2625
`Workflow\Serializers\Avro` serializer. (#362)
2726

27+
### Changed
28+
- Avro is now the default codec for new payloads produced by the client,
29+
serializer helpers, schedules, workflow commands, and activity results.
30+
JSON payloads remain supported for compatibility with existing history.
31+
- Replayed activity results now decode using the event payload codec.
32+
2833
## [0.1.0] — 2026-04-12
2934

3035
Initial PyPI release. HTTP+JSON worker and client for the Durable

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async def main():
5656
- **Type-safe**: Full type hints, passes `mypy --strict`
5757
- **Polyglot**: Works alongside PHP workers on the same task queue
5858
- **HTTP/JSON protocol**: No gRPC, no protobuf dependencies
59-
- **Codec envelopes**: Proper `{codec: "json", blob: "..."}` serialization for cross-language workflows
59+
- **Codec envelopes**: Avro payloads by default, with JSON decode compatibility for existing history
6060

6161
## Documentation
6262

@@ -77,7 +77,7 @@ Full documentation is available at [durable-workflow.github.io/docs/2.0/sdks/pyt
7777

7878
## Compatibility
7979

80-
SDK version 0.1.x requires **Server 2.x** (versions 2.0.0+).
80+
SDK version 0.2.x is compatible with Durable Workflow server 0.x prerelease images and the Server 2.x protocol line.
8181

8282
The worker automatically checks server version at startup and raises a clear error if incompatible.
8383

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "durable-workflow"
7-
version = "0.1.1"
7+
version = "0.2.0"
88
description = "Python SDK for the Durable Workflow server (language-neutral HTTP protocol)"
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -37,7 +37,6 @@ dependencies = [
3737

3838
[project.optional-dependencies]
3939
dev = [
40-
"avro>=1.12,<2",
4140
"pytest>=8.0",
4241
"pytest-asyncio>=0.23",
4342
"mypy>=1.10",

src/durable_workflow/_avro.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,9 @@
1010
are not yet encodeable/decodeable from this SDK because typed schemas
1111
require a schema registry that is out of scope for the first Avro release.
1212
13-
The ``avro`` third-party package is an *optional* runtime dependency.
14-
Install it with::
15-
16-
pip install 'durable-workflow[avro]'
17-
18-
If the extra is not installed, calling :func:`encode` or :func:`decode`
19-
raises :class:`AvroNotInstalledError` with the install hint.
13+
The ``avro`` third-party package is a core runtime dependency. If it is
14+
missing from a broken or partial installation, calling :func:`encode` or
15+
:func:`decode` raises :class:`AvroNotInstalledError` with a reinstall hint.
2016
"""
2117
from __future__ import annotations
2218

@@ -43,7 +39,7 @@ def _load_avro_schema() -> Any:
4339
except ImportError as exc:
4440
raise AvroNotInstalledError(
4541
"The 'avro' package is required to encode/decode payloads with the 'avro' "
46-
"codec. Install with: pip install 'durable-workflow[avro]'"
42+
"codec. Reinstall durable-workflow with its runtime dependencies."
4743
) from exc
4844

4945
return avro.schema.parse(WRAPPER_SCHEMA_JSON)
@@ -59,7 +55,7 @@ def encode(value: Any) -> str:
5955
except ImportError as exc:
6056
raise AvroNotInstalledError(
6157
"The 'avro' package is required to encode payloads with the 'avro' "
62-
"codec. Install with: pip install 'durable-workflow[avro]'"
58+
"codec. Reinstall durable-workflow with its runtime dependencies."
6359
) from exc
6460

6561
schema = _load_avro_schema()
@@ -88,7 +84,7 @@ def decode(blob: str) -> Any:
8884
except ImportError as exc:
8985
raise AvroNotInstalledError(
9086
"The 'avro' package is required to decode payloads with the 'avro' "
91-
"codec. Install with: pip install 'durable-workflow[avro]'"
87+
"codec. Reinstall durable-workflow with its runtime dependencies."
9288
) from exc
9389

9490
try:

src/durable_workflow/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __init__(self, message: str, *, cause: Exception | None = None) -> None:
102102

103103

104104
class AvroNotInstalledError(DurableWorkflowError, ImportError):
105-
"""Raised when Avro codec is requested but the ``avro`` extra is not installed."""
105+
"""Raised when the core ``avro`` runtime dependency is unavailable."""
106106

107107

108108
def _raise_for_status(status: int, body: object, *, context: str = "") -> None:

src/durable_workflow/serializer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def encode(value: Any, codec: str = AVRO_CODEC) -> str:
2828
2929
Raises ``ValueError`` for unknown codecs and
3030
:class:`~durable_workflow.errors.AvroNotInstalledError` when the Avro
31-
extra is requested but not installed.
31+
runtime dependency is missing from a broken or partial installation.
3232
"""
3333
if codec == JSON_CODEC:
3434
return json.dumps(value, separators=(",", ":"), ensure_ascii=False)
@@ -61,7 +61,7 @@ def decode(blob: str | None, codec: str | None = None) -> Any:
6161
6262
Raises ``ValueError`` for unknown codecs or malformed blobs, and
6363
:class:`~durable_workflow.errors.AvroNotInstalledError` when the Avro
64-
extra is requested but not installed.
64+
runtime dependency is missing from a broken or partial installation.
6565
"""
6666
if blob is None or blob == "":
6767
return None

src/durable_workflow/worker.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ async def _register(self) -> None:
6464
log.warning("unable to parse server version %r; skipping compatibility check", server_version)
6565
major = None
6666

67-
# Require server major version 0 or 2+ (0.x is pre-release, 2.x is stable)
68-
# SDK 0.1.x is compatible with server 0.1.x and 2.x
67+
# Require server major version 0 or 2 (0.x is pre-release, 2.x is stable).
68+
# SDK 0.2.x is compatible with server 0.x prereleases and 2.x protocol releases.
6969
if major is not None and major not in (0, 2):
7070
raise RuntimeError(
71-
f"Server version {server_version} is incompatible with sdk-python 0.1.x "
71+
f"Server version {server_version} is incompatible with sdk-python 0.2.x "
7272
f"(requires server 0.x or 2.x). "
7373
f"Upgrade the server or use a compatible SDK version."
7474
)
@@ -121,15 +121,15 @@ async def _run_workflow_task(self, task: dict[str, Any]) -> list[dict[str, Any]]
121121
if decoded is not None:
122122
start_input = decoded if isinstance(decoded, list) else [decoded]
123123
except AvroNotInstalledError as e:
124-
log.exception("task %s start input Avro decode failed (extra not installed)", task_id)
124+
log.exception("task %s start input Avro decode failed (avro dependency unavailable)", task_id)
125125
try:
126126
await self.client.fail_workflow_task(
127127
task_id=task_id,
128128
lease_owner=self.worker_id,
129129
workflow_task_attempt=attempt,
130130
message=(
131131
f"cannot decode workflow start input with codec 'avro': {e}. "
132-
f"Install the avro extra: pip install 'durable-workflow[avro]'."
132+
f"Reinstall durable-workflow with its runtime dependencies."
133133
),
134134
failure_type=type(e).__name__,
135135
stack_trace=traceback.format_exc(),
@@ -174,15 +174,15 @@ async def _run_workflow_task(self, task: dict[str, Any]) -> list[dict[str, Any]]
174174
try:
175175
outcome = replay(cls, history, start_input, run_id=run_id, payload_codec=codec)
176176
except AvroNotInstalledError as e:
177-
log.exception("replay failed: Avro extra not installed")
177+
log.exception("replay failed: Avro dependency unavailable")
178178
try:
179179
await self.client.fail_workflow_task(
180180
task_id=task_id,
181181
lease_owner=self.worker_id,
182182
workflow_task_attempt=attempt,
183183
message=(
184184
f"cannot replay workflow history with codec 'avro': {e}. "
185-
f"Install the avro extra: pip install 'durable-workflow[avro]'."
185+
f"Reinstall durable-workflow with its runtime dependencies."
186186
),
187187
failure_type=type(e).__name__,
188188
stack_trace=traceback.format_exc(),
@@ -234,15 +234,15 @@ async def _run_activity_task(self, task: dict[str, Any]) -> None:
234234
try:
235235
args = serializer.decode_envelope(raw_args, codec=inbound_codec) or []
236236
except AvroNotInstalledError as e:
237-
log.exception("activity %s arguments Avro decode failed (extra not installed)", task_id)
237+
log.exception("activity %s arguments Avro decode failed (avro dependency unavailable)", task_id)
238238
try:
239239
await self.client.fail_activity_task(
240240
task_id=task_id,
241241
activity_attempt_id=attempt_id,
242242
lease_owner=self.worker_id,
243243
message=(
244244
f"cannot decode activity arguments with codec 'avro': {e}. "
245-
f"Install the avro extra: pip install 'durable-workflow[avro]'."
245+
f"Reinstall durable-workflow with its runtime dependencies."
246246
),
247247
failure_type=type(e).__name__,
248248
stack_trace=traceback.format_exc(),

tests/test_serializer.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
_AVRO_AVAILABLE = False
1212

1313
requires_avro = pytest.mark.skipif(
14-
not _AVRO_AVAILABLE, reason="avro extra not installed"
14+
not _AVRO_AVAILABLE, reason="avro package not installed"
1515
)
1616

1717

@@ -170,14 +170,14 @@ def test_empty_blob_is_none(self) -> None:
170170
class TestAvroNotInstalledError:
171171
"""Verify AvroNotInstalledError is a proper ImportError subclass.
172172
173-
This matters because callers who already catch ImportError to detect
174-
optional-dep absence (a common Python pattern) will transparently
175-
catch the SDK-specific error too.
173+
This matters because callers who already catch ImportError for broken
174+
or partial installations will transparently catch the SDK-specific
175+
error too.
176176
"""
177177

178178
def test_is_import_error_subclass(self) -> None:
179179
assert issubclass(AvroNotInstalledError, ImportError)
180180

181181
def test_carries_install_hint_in_message(self) -> None:
182-
exc = AvroNotInstalledError("install with: pip install 'durable-workflow[avro]'")
183-
assert "durable-workflow[avro]" in str(exc)
182+
exc = AvroNotInstalledError("reinstall durable-workflow with runtime dependencies")
183+
assert "runtime dependencies" in str(exc)

tests/test_worker.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ async def test_sync_activity(self, mock_client: AsyncMock) -> None:
195195

196196
@pytest.mark.asyncio
197197
async def test_activity_echoes_avro_codec(self, mock_client: AsyncMock) -> None:
198-
avro = pytest.importorskip("avro", reason="avro extra not installed")
198+
avro = pytest.importorskip("avro", reason="avro package not installed")
199199
del avro
200200
from durable_workflow import serializer as _ser
201201

@@ -317,7 +317,7 @@ async def test_activity_json_decode_failure_fails_task(self, mock_client: AsyncM
317317

318318
@pytest.mark.asyncio
319319
async def test_activity_avro_decode_failure_fails_task(self, mock_client: AsyncMock) -> None:
320-
pytest.importorskip("avro", reason="avro extra not installed")
320+
pytest.importorskip("avro", reason="avro package not installed")
321321
worker = Worker(mock_client, task_queue="q1", workflows=[], activities=[echo_activity])
322322
task = {
323323
"task_id": "at-bad-avro",
@@ -335,7 +335,7 @@ async def test_activity_avro_decode_failure_fails_task(self, mock_client: AsyncM
335335
mock_client.complete_activity_task.assert_not_called()
336336

337337
@pytest.mark.asyncio
338-
async def test_activity_avro_missing_extra_fails_task(
338+
async def test_activity_avro_missing_dependency_fails_task(
339339
self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch
340340
) -> None:
341341
from durable_workflow import _avro
@@ -344,7 +344,7 @@ async def test_activity_avro_missing_extra_fails_task(
344344
def _raise_missing(_blob: str) -> None:
345345
raise AvroNotInstalledError(
346346
"The 'avro' package is required to encode/decode payloads with the 'avro' "
347-
"codec. Install with: pip install 'durable-workflow[avro]'"
347+
"codec. Reinstall durable-workflow with its runtime dependencies."
348348
)
349349

350350
monkeypatch.setattr(_avro, "decode", _raise_missing)
@@ -360,7 +360,7 @@ def _raise_missing(_blob: str) -> None:
360360
await worker._run_activity_task(task)
361361
mock_client.fail_activity_task.assert_called_once()
362362
call_kwargs = mock_client.fail_activity_task.call_args.kwargs
363-
assert "avro extra" in call_kwargs["message"].lower() or "pip install" in call_kwargs["message"]
363+
assert "runtime dependencies" in call_kwargs["message"]
364364
assert call_kwargs["failure_type"] == "AvroNotInstalledError"
365365
assert call_kwargs["non_retryable"] is True
366366
mock_client.complete_activity_task.assert_not_called()
@@ -384,7 +384,7 @@ async def test_workflow_json_decode_failure_fails_task(self, mock_client: AsyncM
384384
mock_client.complete_workflow_task.assert_not_called()
385385

386386
@pytest.mark.asyncio
387-
async def test_workflow_avro_missing_extra_fails_task(
387+
async def test_workflow_avro_missing_dependency_fails_task(
388388
self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch
389389
) -> None:
390390
from durable_workflow import _avro
@@ -393,7 +393,7 @@ async def test_workflow_avro_missing_extra_fails_task(
393393
def _raise_missing(_blob: str) -> None:
394394
raise AvroNotInstalledError(
395395
"The 'avro' package is required to encode/decode payloads with the 'avro' "
396-
"codec. Install with: pip install 'durable-workflow[avro]'"
396+
"codec. Reinstall durable-workflow with its runtime dependencies."
397397
)
398398

399399
monkeypatch.setattr(_avro, "decode", _raise_missing)
@@ -410,23 +410,23 @@ def _raise_missing(_blob: str) -> None:
410410
await worker._run_workflow_task(task)
411411
mock_client.fail_workflow_task.assert_called_once()
412412
call_kwargs = mock_client.fail_workflow_task.call_args.kwargs
413-
assert "avro extra" in call_kwargs["message"].lower() or "pip install" in call_kwargs["message"]
413+
assert "runtime dependencies" in call_kwargs["message"]
414414
assert call_kwargs["failure_type"] == "AvroNotInstalledError"
415415
mock_client.complete_workflow_task.assert_not_called()
416416

417417
@pytest.mark.asyncio
418-
async def test_workflow_replay_avro_missing_extra_fails_task(
418+
async def test_workflow_replay_avro_missing_dependency_fails_task(
419419
self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch
420420
) -> None:
421-
"""Avro-encoded history result that cannot be decoded (extra missing)
421+
"""Avro-encoded history result that cannot be decoded (dependency missing)
422422
surfaces as fail_workflow_task, not an unhandled dispatcher exception."""
423423
from durable_workflow import _avro
424424
from durable_workflow.errors import AvroNotInstalledError
425425

426426
def _raise_missing(_blob: str) -> None:
427427
raise AvroNotInstalledError(
428428
"The 'avro' package is required to encode/decode payloads with the 'avro' "
429-
"codec. Install with: pip install 'durable-workflow[avro]'"
429+
"codec. Reinstall durable-workflow with its runtime dependencies."
430430
)
431431

432432
monkeypatch.setattr(_avro, "decode", _raise_missing)

0 commit comments

Comments
 (0)