Skip to content

Commit 643e6ea

Browse files
committed
Merge remote-tracking branch 'origin/main' into andystaples/add-functions-support
# Conflicts: # durabletask/worker.py
2 parents 5b906bd + 6f56f6f commit 643e6ea

122 files changed

Lines changed: 6994 additions & 437 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ Examples:
3636
- Follow PEP 8 conventions.
3737
- Use `autopep8` for Python formatting.
3838

39+
## Copyright Headers
40+
41+
Every new Python (`.py`) source file MUST begin with the following copyright
42+
header as the first two lines, followed by a blank line before any code,
43+
docstring, or imports:
44+
45+
```python
46+
# Copyright (c) Microsoft Corporation.
47+
# Licensed under the MIT License.
48+
```
49+
50+
This applies to all hand-written Python files, including `__init__.py` files,
51+
tests, and examples. The only exceptions are auto-generated protobuf files
52+
(`*_pb2.py` and `*_pb2_grpc.py`), which carry their own generated header.
53+
3954
## Python Type Checking
4055

4156
Before linting, check for and fix any Pylance errors in the files you

.github/workflows/typecheck.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,18 @@ jobs:
3131
run: |
3232
python -m pip install --upgrade pip
3333
pip install -r requirements.txt
34-
pip install -e ".[azure-blob-payloads,opentelemetry]"
35-
pip install -e ./durabletask-azuremanaged
34+
# Install third-party dependencies declared by the examples so they
35+
# type-check cleanly. Each example's requirements.txt is the single
36+
# source of truth for its dependencies.
37+
for req in examples/requirements.txt examples/*/requirements.txt; do
38+
pip install -r "$req"
39+
done
40+
# Install the packages under test from local source last (non-editable
41+
# so the durabletask / durabletask.azuremanaged namespace packages are
42+
# physically merged in site-packages, which pyright can resolve). This
43+
# overrides any PyPI copy pulled in by the example requirements above.
44+
pip install --force-reinstall --no-deps . ./durabletask-azuremanaged
45+
pip install ".[azure-blob-payloads,opentelemetry]"
3646
pip install pyright
3747
3848
- name: Run pyright (strict, Python 3.10)

CHANGELOG.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,99 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
99

10+
## v1.7.0
11+
12+
ADDED
13+
14+
- Added `durabletask.scheduled`, a recurring schedule feature built on durable
15+
entities. Use `worker.configure_scheduled_tasks()` to enable it on a worker,
16+
then manage schedules from the client via `ScheduledTaskClient` (and the
17+
per-schedule `ScheduleClient`). Supports creating, describing, listing,
18+
updating, pausing, resuming, and deleting schedules with configurable
19+
`interval`, `start_at`, `end_at`, and `start_immediately_if_late` options.
20+
- Added an optional `signal_time` parameter to `EntityContext.signal_entity`
21+
and `DurableEntity.signal_entity`, allowing an entity signal to be scheduled
22+
for future delivery.
23+
- Added an optional `signal_time` parameter to `OrchestrationContext.signal_entity`
24+
and to the client `signal_entity` methods (sync and async), allowing entity
25+
signals to be scheduled for future delivery from orchestrations and clients.
26+
- Added a pluggable `DataConverter` (`durabletask.serialization`) accepted by
27+
`TaskHubGrpcWorker`, `TaskHubGrpcClient`, and `AsyncTaskHubGrpcClient` via a
28+
`data_converter` argument. Every payload boundary (inputs, outputs, events,
29+
custom status, entity state) routes through it, so one converter controls how
30+
Python values become JSON and how they are reconstructed. The default
31+
`JsonDataConverter` preserves existing behavior, so a custom converter (for
32+
example one backed by pydantic) is fully opt-in.
33+
- Custom objects can participate in serialization by exposing a `to_json()`
34+
method and a `from_json(value)` classmethod. Both are honored recursively, so
35+
nested custom objects round-trip through their own hooks.
36+
- Payloads are reconstructed into a caller-supplied type — dataclasses
37+
(including nested fields), `from_json()`-capable types, and `enum.Enum`
38+
members, recursing through `list`, `dict`, `tuple`, and `Optional`/`Union`
39+
hints. The type comes from a function's annotations, from an explicit
40+
`return_type` on `call_activity` / `call_sub_orchestrator` / `call_entity`
41+
(or `data_type` on `wait_for_external_event`), or from the typed accessors
42+
`get_input()` / `get_output()` / `get_custom_status()` on
43+
`client.OrchestrationState` and `EntityMetadata.get_typed_state(...)`. It is
44+
never inferred from the payload. Which annotated types are eligible is decided
45+
by the converter via the overridable `DataConverter.can_reconstruct(...)`; a
46+
custom converter can override it to recognize its own types (for example
47+
`pydantic.BaseModel` subclasses).
48+
49+
CHANGED
50+
51+
- Custom objects (dataclasses, `SimpleNamespace`, namedtuples) are now
52+
serialized as plain JSON. Decoding such a payload *without* a type hint now
53+
yields a plain `dict` (previously a `SimpleNamespace`; a namedtuple now
54+
round-trips as a JSON array). To get the original type back, supply a type via
55+
one of the mechanisms above. Payloads produced by older SDK versions still
56+
deserialize — including into a `SimpleNamespace` when no type is supplied — so
57+
in-flight orchestrations continue to replay across an upgrade.
58+
- JSON serialization failures now raise a `TypeError` that chains the original
59+
error (`__cause__`) and names the offending type.
60+
- `EntityContext.get_state()` / `DurableEntity.get_state()` now return a freshly
61+
reconstructed value on every call rather than a reference to a single cached
62+
object. This changes v1.6.0 behavior: mutating the returned value in place no
63+
longer affects persisted state — write it back with `set_state()`. State is
64+
also serialized eagerly at `set_state()` time, so a non-serializable value
65+
fails inside the operation (which rolls back) instead of after the batch has
66+
run.
67+
68+
FIXED
69+
70+
- Falsy entity states (`0`, `""`, `[]`, `{}`) are no longer dropped when an
71+
entity batch is persisted. Previously a falsy current state was treated as
72+
"no state" and written as `None`, effectively deleting it; only an actual
73+
`None` state now clears the persisted entity state.
74+
75+
DEPRECATED
76+
77+
- `durabletask.internal.shared.to_json` and `durabletask.internal.shared.from_json`
78+
are deprecated and now emit a `DeprecationWarning`. Use a
79+
`durabletask.serialization.DataConverter` (for example the default
80+
`JsonDataConverter`) instead. The functions continue to work for backwards
81+
compatibility.
82+
83+
BREAKING CHANGES (no runtime impact for typical users)
84+
85+
Most of these are type-level only: because the package ships `py.typed`,
86+
consumers running strict type checkers (pyright/mypy) — or subclassing the
87+
public abstract types — may need to update their code. The constructor change
88+
below also affects callers who *directly* construct the named classes, which is
89+
uncommon since they are normally handed to you by the SDK.
90+
91+
- `OrchestrationContext.call_activity`, `call_sub_orchestrator`, `call_entity`,
92+
and `wait_for_external_event` gained new keyword-only parameters
93+
(`return_type` / `data_type`). Subclasses overriding these methods should add
94+
the parameter to match the base signature.
95+
- `EntityContext` and `EntityMetadata` (and its `from_entity_metadata` /
96+
`from_entity_response` factories) now require a `data_converter` argument.
97+
These objects are normally constructed by the SDK — you receive an
98+
`EntityContext` in an entity function and an `EntityMetadata` from the client —
99+
so this only affects code that constructs them directly.
100+
101+
## v1.6.0
102+
10103
ADDED
11104

12105
- Added overridable activity-dispatch hooks `_on_activity_execution_started`

docs/supported-patterns.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ def purchase_order_workflow(ctx: task.OrchestrationContext, order: Order):
6868
yield ctx.call_activity(send_approval_request, input=order)
6969

7070
# Approvals must be received within 24 hours or they will be cancelled.
71-
approval_event = ctx.wait_for_external_event("approval_received")
71+
# Passing ``data_type`` reconstructs the event payload as an ``Approval``.
72+
approval_event = ctx.wait_for_external_event("approval_received", data_type=Approval)
7273
timeout_event = ctx.create_timer(timedelta(hours=24))
7374
winner = yield task.when_any([approval_event, timeout_event])
7475
if winner == timeout_event:
@@ -81,9 +82,11 @@ def purchase_order_workflow(ctx: task.OrchestrationContext, order: Order):
8182
```
8283

8384
As an aside, you'll also notice that the example orchestration above works with custom business
84-
objects. Support for custom business objects includes support for custom classes, custom data
85-
classes, and named tuples. Serialization and deserialization of these objects is handled
86-
automatically by the SDK.
85+
objects. Custom classes, data classes, and named tuples are serialized to plain JSON automatically.
86+
To reconstruct the original type on the receiving side, supply the type — for example via the
87+
`data_type` argument to `wait_for_external_event` (shown above), the `return_type` argument to
88+
`call_activity` / `call_sub_orchestrator` / `call_entity`, or by annotating the consuming function's
89+
input parameter. Without a type, the payload is returned as plain JSON (a `dict` or `list`).
8790

8891
See the full [human interaction sample](../examples/human_interaction.py).
8992

durabletask-azuremanaged/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
99

10+
## v1.7.0
11+
12+
- Updates base dependency to durabletask v1.7.0.
13+
14+
ADDED
15+
16+
- `DurableTaskSchedulerWorker`, `DurableTaskSchedulerClient`, and the async
17+
client now accept a `data_converter` argument and forward it to the base
18+
worker/client, so a custom `durabletask.serialization.DataConverter` (for
19+
example a pydantic-backed one) can be used with the Durable Task Scheduler.
20+
21+
## v1.6.0
22+
1023
- Updates base dependency to durabletask v1.6.0.
1124
- Added preview support for Durable Task Scheduler on-demand sandbox
1225
activities under `durabletask.azuremanaged.preview.sandboxes`. Applications
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.

durabletask-azuremanaged/durabletask/azuremanaged/client.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
)
2121
import durabletask.internal.shared as shared
2222
from durabletask.payload.store import PayloadStore
23+
from durabletask.serialization import DataConverter
2324

2425

2526
# Client class used for Durable Task Scheduler (DTS)
@@ -35,6 +36,7 @@ def __init__(self, *,
3536
resiliency_options: GrpcClientResiliencyOptions | None = None,
3637
default_version: str | None = None,
3738
payload_store: PayloadStore | None = None,
39+
data_converter: DataConverter | None = None,
3840
log_handler: logging.Handler | None = None,
3941
log_formatter: logging.Formatter | None = None):
4042

@@ -59,7 +61,8 @@ def __init__(self, *,
5961
channel_options=channel_options,
6062
resiliency_options=resiliency_options,
6163
default_version=default_version,
62-
payload_store=payload_store)
64+
payload_store=payload_store,
65+
data_converter=data_converter)
6366

6467

6568
# Async client class used for Durable Task Scheduler (DTS)
@@ -113,6 +116,7 @@ def __init__(self, *,
113116
resiliency_options: GrpcClientResiliencyOptions | None = None,
114117
default_version: str | None = None,
115118
payload_store: PayloadStore | None = None,
119+
data_converter: DataConverter | None = None,
116120
log_handler: logging.Handler | None = None,
117121
log_formatter: logging.Formatter | None = None):
118122

@@ -137,4 +141,5 @@ def __init__(self, *,
137141
channel_options=channel_options,
138142
resiliency_options=resiliency_options,
139143
default_version=default_version,
140-
payload_store=payload_store)
144+
payload_store=payload_store,
145+
data_converter=data_converter)

durabletask-azuremanaged/durabletask/azuremanaged/worker.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
import durabletask.internal.shared as shared
2020
from durabletask.payload.store import PayloadStore
21+
from durabletask.serialization import DataConverter
2122
from durabletask.worker import ConcurrencyOptions, TaskHubGrpcWorker
2223

2324

@@ -81,6 +82,7 @@ def __init__(self, *,
8182
resiliency_options: GrpcWorkerResiliencyOptions | None = None,
8283
concurrency_options: ConcurrencyOptions | None = None,
8384
payload_store: PayloadStore | None = None,
85+
data_converter: DataConverter | None = None,
8486
log_handler: logging.Handler | None = None,
8587
log_formatter: logging.Formatter | None = None):
8688

@@ -110,5 +112,6 @@ def __init__(self, *,
110112
concurrency_options=concurrency_options,
111113
# DTS natively supports long timers so chunking is unnecessary
112114
maximum_timer_interval=None,
113-
payload_store=payload_store
115+
payload_store=payload_store,
116+
data_converter=data_converter
114117
)

durabletask-azuremanaged/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
99

1010
[project]
1111
name = "durabletask.azuremanaged"
12-
version = "1.6.0"
12+
version = "1.7.0"
1313
description = "Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler"
1414
keywords = [
1515
"durable",
@@ -26,13 +26,13 @@ requires-python = ">=3.10"
2626
license = {file = "LICENSE"}
2727
readme = "README.md"
2828
dependencies = [
29-
"durabletask>=1.6.0",
29+
"durabletask>=1.7.0",
3030
"azure-identity>=1.19.0"
3131
]
3232

3333
[project.optional-dependencies]
3434
azure-blob-payloads = [
35-
"durabletask[azure-blob-payloads]>=1.6.0"
35+
"durabletask[azure-blob-payloads]>=1.7.0"
3636
]
3737

3838
[project.urls]

0 commit comments

Comments
 (0)