Skip to content

Commit b75223c

Browse files
authored
Merge branch 'main' into andystaples-fix-management-payload-urls
2 parents e0aa562 + c595295 commit b75223c

9 files changed

Lines changed: 131 additions & 71 deletions

File tree

azure-functions-durable/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ FIXED
3535
- HTTP management payloads now preserve the host-provided management URL
3636
templates and configured HTTP base paths, include `rewindPostUri`, encode
3737
instance IDs, and use forwarded request origins when enabled by the host.
38+
- Fixed deprecated v1 status-query methods omitting orchestration output,
39+
custom status, and failure details when input display was disabled.
3840
- Fixed asynchronous durable-client construction failing after an application
3941
event loop had been closed or cleared.
4042
- Prevented Durable HTTP calls from forwarding managed identity tokens,

azure-functions-durable/azure/durable_functions/client.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,16 +334,20 @@ async def get_status(
334334
exist, a falsy status is returned rather than ``None``.
335335
336336
The ``show_history`` and ``show_history_output`` flags have no
337-
equivalent in durabletask and are ignored; ``show_input`` maps to
338-
``fetch_payloads``.
337+
equivalent in durabletask and are ignored. Payloads are fetched to
338+
preserve the v1 output, custom-status, and failure-detail fields;
339+
``show_input`` controls whether the compatibility wrapper exposes the
340+
input.
339341
"""
340-
state = await self.get_orchestration_state(instance_id, fetch_payloads=show_input)
341-
return DurableOrchestrationStatus.from_orchestration_state(state)
342+
state = await self.get_orchestration_state(instance_id, fetch_payloads=True)
343+
return DurableOrchestrationStatus.from_orchestration_state(
344+
state, include_input=show_input)
342345

343346
@deprecated("get_status_all is deprecated; use get_all_orchestration_states instead.")
344347
async def get_status_all(self) -> list[DurableOrchestrationStatus]:
345348
"""Deprecated alias for :meth:`get_all_orchestration_states`."""
346-
states = await self.get_all_orchestration_states()
349+
states = await self.get_all_orchestration_states(
350+
OrchestrationQuery(fetch_inputs_and_outputs=True))
347351
return [DurableOrchestrationStatus.from_orchestration_state(state) for state in states]
348352

349353
@deprecated("raise_event is deprecated; use raise_orchestration_event instead.")
@@ -445,7 +449,8 @@ async def get_status_by(
445449
query = OrchestrationQuery(
446450
created_time_from=created_time_from,
447451
created_time_to=created_time_to,
448-
runtime_status=to_durabletask_statuses(runtime_status))
452+
runtime_status=to_durabletask_statuses(runtime_status),
453+
fetch_inputs_and_outputs=True)
449454
states = await self.get_all_orchestration_states(query)
450455
return [DurableOrchestrationStatus.from_orchestration_state(state) for state in states]
451456

azure-functions-durable/azure/durable_functions/internal/compat/durable_orchestration_status.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@ class DurableOrchestrationStatus:
2929

3030
def __init__(self, state: Optional[OrchestrationState] = None):
3131
self._state = state
32+
self._include_input = True
3233

3334
@classmethod
3435
def from_orchestration_state(
35-
cls, state: Optional[OrchestrationState]) -> "DurableOrchestrationStatus":
36+
cls,
37+
state: Optional[OrchestrationState],
38+
*,
39+
include_input: bool = True) -> "DurableOrchestrationStatus":
3640
"""Wrap a durabletask ``OrchestrationState`` (or ``None``)."""
37-
return cls(state)
41+
status = cls(state)
42+
status._include_input = include_input
43+
return status
3844

3945
@classmethod
4046
def from_json(cls, json_obj: Any) -> "DurableOrchestrationStatus":
@@ -104,16 +110,20 @@ def last_updated_time(self) -> Optional[datetime]:
104110
@property
105111
def input_(self) -> Any:
106112
"""Get the (deserialized) input of the orchestration instance."""
107-
if self._state is None:
113+
if self._state is None or not self._include_input:
108114
return None
109115
return self._raw_payload(self._state.serialized_input)
110116

111117
@property
112118
def output(self) -> Any:
113-
"""Get the (deserialized) output of the orchestration instance."""
119+
"""Get the output or failure message of the orchestration instance."""
114120
if self._state is None:
115121
return None
116-
return self._raw_payload(self._state.serialized_output)
122+
output = self._raw_payload(self._state.serialized_output)
123+
if output is not None:
124+
return output
125+
failure = self._state.failure_details
126+
return failure.message if failure is not None else None
117127

118128
@property
119129
def runtime_status(self) -> Optional[OrchestrationRuntimeStatus]:
@@ -157,21 +167,10 @@ def to_json(self) -> dict[str, Any]:
157167
result["createdTime"] = self._format_datetime(self.created_time)
158168
if self.last_updated_time is not None:
159169
result["lastUpdatedTime"] = self._format_datetime(self.last_updated_time)
160-
output = self._raw_payload(
161-
self._state.serialized_output if self._state is not None else None)
170+
output = self.output
162171
if output is not None:
163172
result["output"] = output
164-
elif self._state is not None:
165-
# A failed orchestration carries its error in ``failure_details``
166-
# rather than ``serialized_output``; surface it under ``output``
167-
# (matching v1, where the failure message was returned through the
168-
# status output) so the error is not dropped from the payload.
169-
failure = getattr(self._state, "failure_details", None)
170-
failure_message = getattr(failure, "message", None) if failure is not None else None
171-
if failure_message:
172-
result["output"] = failure_message
173-
input_ = self._raw_payload(
174-
self._state.serialized_input if self._state is not None else None)
173+
input_ = self.input_
175174
if input_ is not None:
176175
result["input"] = input_
177176
if self.runtime_status is not None:

azure-functions-durable/azure/durable_functions/internal/compat/token_source.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(self):
1818
@abstractmethod
1919
def to_json(self) -> dict[str, str]:
2020
"""Convert this token source into a JSON-serializable dictionary."""
21-
...
21+
pass
2222

2323

2424
class ManagedIdentityTokenSource(TokenSource):

durabletask/payload/store.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class PayloadStore(abc.ABC):
4141
@abc.abstractmethod
4242
def options(self) -> LargePayloadStorageOptions:
4343
"""Return the storage options for this payload store."""
44-
...
44+
pass
4545

4646
@abc.abstractmethod
4747
def upload(self, data: bytes, *, instance_id: str | None = None) -> str:
@@ -59,12 +59,12 @@ def upload(self, data: bytes, *, instance_id: str | None = None) -> str:
5959
Returns:
6060
A token string that can be used to retrieve the payload.
6161
"""
62-
...
62+
pass
6363

6464
@abc.abstractmethod
6565
async def upload_async(self, data: bytes, *, instance_id: str | None = None) -> str:
6666
"""Async version of :meth:`upload`."""
67-
...
67+
pass
6868

6969
@abc.abstractmethod
7070
def download(self, token: str) -> bytes:
@@ -77,14 +77,14 @@ def download(self, token: str) -> bytes:
7777
Returns:
7878
The original payload bytes.
7979
"""
80-
...
80+
pass
8181

8282
@abc.abstractmethod
8383
async def download_async(self, token: str) -> bytes:
8484
"""Async version of :meth:`download`."""
85-
...
85+
pass
8686

8787
@abc.abstractmethod
8888
def is_known_token(self, value: str) -> bool:
8989
"""Return ``True`` if *value* looks like a token produced by this store."""
90-
...
90+
pass

durabletask/serialization.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class DataConverter(ABC):
6868
@abstractmethod
6969
def serialize(self, value: Any) -> str | None:
7070
"""Serialize ``value`` to a string, or ``None`` when ``value`` is ``None``."""
71-
...
71+
pass
7272

7373
@abstractmethod
7474
def deserialize(self, data: str | None, target_type: type | None = None) -> Any:
@@ -85,7 +85,7 @@ def deserialize(self, data: str | None, target_type: type | None = None) -> Any:
8585
:class:`JsonDataConverter` is best-effort and falls back; a validating
8686
converter may instead raise.
8787
"""
88-
...
88+
pass
8989

9090
@abstractmethod
9191
def coerce(self, value: Any, target_type: type | None = None) -> Any:
@@ -99,7 +99,7 @@ def coerce(self, value: Any, target_type: type | None = None) -> Any:
9999
(strict vs. best-effort) that an implementation applies in
100100
:meth:`deserialize` should apply here.
101101
"""
102-
...
102+
pass
103103

104104
def can_reconstruct(self, target_type: Any) -> bool:
105105
"""Return True if this converter can rebuild ``target_type`` from a payload.

tests/azure-functions-durable/test_client_compat.py

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -362,13 +362,13 @@ async def test_start_new_delegates_to_schedule_new_orchestration():
362362
await client.close()
363363

364364

365-
async def test_get_status_delegates_to_get_orchestration_state():
365+
async def test_get_status_always_fetches_payloads():
366366
client = _make_client()
367367
try:
368368
with patch.object(client, "get_orchestration_state",
369369
new=AsyncMock(return_value=None)) as mock:
370370
with pytest.warns(DeprecationWarning):
371-
await client.get_status("abc", show_input=True)
371+
await client.get_status("abc")
372372
mock.assert_awaited_once_with("abc", fetch_payloads=True)
373373
finally:
374374
await client.close()
@@ -381,7 +381,9 @@ async def test_get_status_all_delegates():
381381
new=AsyncMock(return_value=[])) as mock:
382382
with pytest.warns(DeprecationWarning):
383383
await client.get_status_all()
384-
mock.assert_awaited_once_with()
384+
mock.assert_awaited_once()
385+
query = mock.await_args.args[0]
386+
assert query.fetch_inputs_and_outputs is True
385387
finally:
386388
await client.close()
387389

@@ -498,8 +500,10 @@ async def test_get_status_by_maps_statuses():
498500
with pytest.warns(DeprecationWarning):
499501
await client.get_status_by(
500502
runtime_status=[df.OrchestrationRuntimeStatus.Running])
503+
mock.assert_awaited_once()
501504
query = mock.await_args.args[0]
502505
assert query.runtime_status == [OrchestrationStatus.RUNNING]
506+
assert query.fetch_inputs_and_outputs is True
503507
finally:
504508
await client.close()
505509

@@ -688,16 +692,20 @@ def test_entity_class_raises_not_implemented():
688692
# Return-type shims: DurableOrchestrationStatus
689693
# ---------------------------------------------------------------------------
690694

691-
def _fake_state():
695+
def _fake_state(
696+
runtime_status=OrchestrationStatus.RUNNING,
697+
serialized_output='{"out": 2}',
698+
failure_details=None):
692699
return SimpleNamespace(
693700
name="orch",
694701
instance_id="abc",
695702
created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
696703
last_updated_at=datetime(2026, 1, 2, tzinfo=timezone.utc),
697-
runtime_status=OrchestrationStatus.RUNNING,
704+
runtime_status=runtime_status,
698705
serialized_input='{"in": 1}',
699-
serialized_output='{"out": 2}',
706+
serialized_output=serialized_output,
700707
serialized_custom_status='"cs"',
708+
failure_details=failure_details,
701709
get_input=lambda: {"in": 1},
702710
get_output=lambda: {"out": 2},
703711
get_custom_status=lambda: "cs",
@@ -710,21 +718,61 @@ def test_from_durabletask_status_reverse_mapping():
710718
OrchestrationStatus.CONTINUED_AS_NEW) == df.OrchestrationRuntimeStatus.ContinuedAsNew
711719

712720

713-
async def test_get_status_returns_wrapped_status():
721+
async def test_get_status_suppresses_only_input_by_default():
714722
client = _make_client()
715723
try:
724+
state = _fake_state(runtime_status=OrchestrationStatus.COMPLETED)
716725
with patch.object(client, "get_orchestration_state",
717-
new=AsyncMock(return_value=_fake_state())):
726+
new=AsyncMock(return_value=state)):
718727
with pytest.warns(DeprecationWarning):
719728
status = await client.get_status("abc")
720729
assert bool(status) is True
721730
assert status.name == "orch"
722731
assert status.instance_id == "abc"
723-
assert status.runtime_status == df.OrchestrationRuntimeStatus.Running
724-
assert status.input_ == {"in": 1}
732+
assert status.runtime_status == df.OrchestrationRuntimeStatus.Completed
733+
assert status.input_ is None
725734
assert status.output == {"out": 2}
726735
assert status.custom_status == "cs"
727-
assert status.to_json()["runtimeStatus"] == "Running"
736+
status_json = status.to_json()
737+
assert status_json["runtimeStatus"] == "Completed"
738+
assert "input" not in status_json
739+
assert status_json["output"] == {"out": 2}
740+
assert status_json["customStatus"] == "cs"
741+
finally:
742+
await client.close()
743+
744+
745+
async def test_get_status_show_input_includes_running_input():
746+
client = _make_client()
747+
try:
748+
with patch.object(client, "get_orchestration_state",
749+
new=AsyncMock(return_value=_fake_state())):
750+
with pytest.warns(DeprecationWarning):
751+
status = await client.get_status("abc", show_input=True)
752+
assert status.runtime_status == df.OrchestrationRuntimeStatus.Running
753+
assert status.input_ == {"in": 1}
754+
assert status.to_json()["input"] == {"in": 1}
755+
finally:
756+
await client.close()
757+
758+
759+
async def test_get_status_failed_preserves_failure_output():
760+
client = _make_client()
761+
try:
762+
state = _fake_state(
763+
runtime_status=OrchestrationStatus.FAILED,
764+
serialized_output=None,
765+
failure_details=SimpleNamespace(message="boom"))
766+
with patch.object(client, "get_orchestration_state",
767+
new=AsyncMock(return_value=state)):
768+
with pytest.warns(DeprecationWarning):
769+
status = await client.get_status("abc")
770+
assert status.output == "boom"
771+
status_json = status.to_json()
772+
assert status_json["runtimeStatus"] == "Failed"
773+
assert status_json["output"] == "boom"
774+
assert status_json["customStatus"] == "cs"
775+
assert "input" not in status_json
728776
finally:
729777
await client.close()
730778

@@ -752,6 +800,9 @@ async def test_get_status_all_returns_wrapped_list():
752800
statuses = await client.get_status_all()
753801
assert len(statuses) == 1
754802
assert statuses[0].runtime_status == df.OrchestrationRuntimeStatus.Running
803+
assert statuses[0].input_ == {"in": 1}
804+
assert statuses[0].output == {"out": 2}
805+
assert statuses[0].custom_status == "cs"
755806
finally:
756807
await client.close()
757808

@@ -765,6 +816,9 @@ async def test_get_status_by_returns_wrapped_list():
765816
statuses = await client.get_status_by(
766817
runtime_status=[df.OrchestrationRuntimeStatus.Running])
767818
assert statuses[0].instance_id == "abc"
819+
assert statuses[0].input_ == {"in": 1}
820+
assert statuses[0].output == {"out": 2}
821+
assert statuses[0].custom_status == "cs"
768822
finally:
769823
await client.close()
770824

tests/durabletask/test_orchestration_executor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ class Result:
586586
message: str
587587

588588
def annotated_activity(ctx, _) -> Result:
589-
...
589+
pass
590590

591591
captured: dict = {}
592592

@@ -626,7 +626,7 @@ class Override:
626626
value: str
627627

628628
def annotated_activity(ctx, _) -> Annotated:
629-
...
629+
pass
630630

631631
captured: dict = {}
632632

0 commit comments

Comments
 (0)