Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ avoiding repeated allocation of unused worker resources.

FIXED

- Fixed deprecated v1 status-query methods omitting orchestration output,
custom status, and failure details when input display was disabled.
- Fixed asynchronous durable-client construction failing after an application
event loop had been closed or cleared.
- Prevented Durable HTTP calls from forwarding managed identity tokens,
Expand Down
17 changes: 11 additions & 6 deletions azure-functions-durable/azure/durable_functions/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,16 +265,20 @@ async def get_status(
exist, a falsy status is returned rather than ``None``.

The ``show_history`` and ``show_history_output`` flags have no
equivalent in durabletask and are ignored; ``show_input`` maps to
``fetch_payloads``.
equivalent in durabletask and are ignored. Payloads are fetched to
preserve the v1 output, custom-status, and failure-detail fields;
``show_input`` controls whether the compatibility wrapper exposes the
input.
"""
state = await self.get_orchestration_state(instance_id, fetch_payloads=show_input)
return DurableOrchestrationStatus.from_orchestration_state(state)
state = await self.get_orchestration_state(instance_id, fetch_payloads=True)
return DurableOrchestrationStatus.from_orchestration_state(
state, include_input=show_input)

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

@deprecated("raise_event is deprecated; use raise_orchestration_event instead.")
Expand Down Expand Up @@ -376,7 +380,8 @@ async def get_status_by(
query = OrchestrationQuery(
created_time_from=created_time_from,
created_time_to=created_time_to,
runtime_status=to_durabletask_statuses(runtime_status))
runtime_status=to_durabletask_statuses(runtime_status),
fetch_inputs_and_outputs=True)
states = await self.get_all_orchestration_states(query)
return [DurableOrchestrationStatus.from_orchestration_state(state) for state in states]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,18 @@ class DurableOrchestrationStatus:

def __init__(self, state: Optional[OrchestrationState] = None):
self._state = state
self._include_input = True

@classmethod
def from_orchestration_state(
cls, state: Optional[OrchestrationState]) -> "DurableOrchestrationStatus":
cls,
state: Optional[OrchestrationState],
*,
include_input: bool = True) -> "DurableOrchestrationStatus":
"""Wrap a durabletask ``OrchestrationState`` (or ``None``)."""
return cls(state)
status = cls(state)
status._include_input = include_input
return status

@classmethod
def from_json(cls, json_obj: Any) -> "DurableOrchestrationStatus":
Expand Down Expand Up @@ -104,16 +110,20 @@ def last_updated_time(self) -> Optional[datetime]:
@property
def input_(self) -> Any:
"""Get the (deserialized) input of the orchestration instance."""
if self._state is None:
if self._state is None or not self._include_input:
return None
return self._raw_payload(self._state.serialized_input)

@property
def output(self) -> Any:
"""Get the (deserialized) output of the orchestration instance."""
"""Get the output or failure message of the orchestration instance."""
if self._state is None:
return None
return self._raw_payload(self._state.serialized_output)
output = self._raw_payload(self._state.serialized_output)
if output is not None:
return output
failure = self._state.failure_details
return failure.message if failure is not None else None

@property
def runtime_status(self) -> Optional[OrchestrationRuntimeStatus]:
Expand Down Expand Up @@ -157,21 +167,10 @@ def to_json(self) -> dict[str, Any]:
result["createdTime"] = self._format_datetime(self.created_time)
if self.last_updated_time is not None:
result["lastUpdatedTime"] = self._format_datetime(self.last_updated_time)
output = self._raw_payload(
self._state.serialized_output if self._state is not None else None)
output = self.output
if output is not None:
result["output"] = output
elif self._state is not None:
# A failed orchestration carries its error in ``failure_details``
# rather than ``serialized_output``; surface it under ``output``
# (matching v1, where the failure message was returned through the
# status output) so the error is not dropped from the payload.
failure = getattr(self._state, "failure_details", None)
failure_message = getattr(failure, "message", None) if failure is not None else None
if failure_message:
result["output"] = failure_message
input_ = self._raw_payload(
self._state.serialized_input if self._state is not None else None)
input_ = self.input_
if input_ is not None:
result["input"] = input_
if self.runtime_status is not None:
Expand Down
76 changes: 65 additions & 11 deletions tests/azure-functions-durable/test_client_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ async def test_start_new_delegates_to_schedule_new_orchestration():
await client.close()


async def test_get_status_delegates_to_get_orchestration_state():
async def test_get_status_always_fetches_payloads():
client = _make_client()
try:
with patch.object(client, "get_orchestration_state",
new=AsyncMock(return_value=None)) as mock:
with pytest.warns(DeprecationWarning):
await client.get_status("abc", show_input=True)
await client.get_status("abc")
mock.assert_awaited_once_with("abc", fetch_payloads=True)
finally:
await client.close()
Expand All @@ -198,7 +198,9 @@ async def test_get_status_all_delegates():
new=AsyncMock(return_value=[])) as mock:
with pytest.warns(DeprecationWarning):
await client.get_status_all()
mock.assert_awaited_once_with()
mock.assert_awaited_once()
query = mock.await_args.args[0]
assert query.fetch_inputs_and_outputs is True
finally:
await client.close()

Expand Down Expand Up @@ -315,8 +317,10 @@ async def test_get_status_by_maps_statuses():
with pytest.warns(DeprecationWarning):
await client.get_status_by(
runtime_status=[df.OrchestrationRuntimeStatus.Running])
mock.assert_awaited_once()
query = mock.await_args.args[0]
assert query.runtime_status == [OrchestrationStatus.RUNNING]
assert query.fetch_inputs_and_outputs is True
finally:
await client.close()

Expand Down Expand Up @@ -505,16 +509,20 @@ def test_entity_class_raises_not_implemented():
# Return-type shims: DurableOrchestrationStatus
# ---------------------------------------------------------------------------

def _fake_state():
def _fake_state(
runtime_status=OrchestrationStatus.RUNNING,
serialized_output='{"out": 2}',
failure_details=None):
return SimpleNamespace(
name="orch",
instance_id="abc",
created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
last_updated_at=datetime(2026, 1, 2, tzinfo=timezone.utc),
runtime_status=OrchestrationStatus.RUNNING,
runtime_status=runtime_status,
serialized_input='{"in": 1}',
serialized_output='{"out": 2}',
serialized_output=serialized_output,
serialized_custom_status='"cs"',
failure_details=failure_details,
get_input=lambda: {"in": 1},
get_output=lambda: {"out": 2},
get_custom_status=lambda: "cs",
Expand All @@ -527,21 +535,61 @@ def test_from_durabletask_status_reverse_mapping():
OrchestrationStatus.CONTINUED_AS_NEW) == df.OrchestrationRuntimeStatus.ContinuedAsNew


async def test_get_status_returns_wrapped_status():
async def test_get_status_suppresses_only_input_by_default():
client = _make_client()
try:
state = _fake_state(runtime_status=OrchestrationStatus.COMPLETED)
with patch.object(client, "get_orchestration_state",
new=AsyncMock(return_value=_fake_state())):
new=AsyncMock(return_value=state)):
with pytest.warns(DeprecationWarning):
status = await client.get_status("abc")
assert bool(status) is True
assert status.name == "orch"
assert status.instance_id == "abc"
assert status.runtime_status == df.OrchestrationRuntimeStatus.Running
assert status.input_ == {"in": 1}
assert status.runtime_status == df.OrchestrationRuntimeStatus.Completed
assert status.input_ is None
assert status.output == {"out": 2}
assert status.custom_status == "cs"
assert status.to_json()["runtimeStatus"] == "Running"
status_json = status.to_json()
assert status_json["runtimeStatus"] == "Completed"
assert "input" not in status_json
assert status_json["output"] == {"out": 2}
assert status_json["customStatus"] == "cs"
finally:
await client.close()


async def test_get_status_show_input_includes_running_input():
client = _make_client()
try:
with patch.object(client, "get_orchestration_state",
new=AsyncMock(return_value=_fake_state())):
with pytest.warns(DeprecationWarning):
status = await client.get_status("abc", show_input=True)
assert status.runtime_status == df.OrchestrationRuntimeStatus.Running
assert status.input_ == {"in": 1}
assert status.to_json()["input"] == {"in": 1}
finally:
await client.close()


async def test_get_status_failed_preserves_failure_output():
client = _make_client()
try:
state = _fake_state(
runtime_status=OrchestrationStatus.FAILED,
serialized_output=None,
failure_details=SimpleNamespace(message="boom"))
with patch.object(client, "get_orchestration_state",
new=AsyncMock(return_value=state)):
with pytest.warns(DeprecationWarning):
status = await client.get_status("abc")
assert status.output == "boom"
status_json = status.to_json()
assert status_json["runtimeStatus"] == "Failed"
assert status_json["output"] == "boom"
Comment thread
andystaples marked this conversation as resolved.
assert status_json["customStatus"] == "cs"
assert "input" not in status_json
finally:
await client.close()

Expand Down Expand Up @@ -569,6 +617,9 @@ async def test_get_status_all_returns_wrapped_list():
statuses = await client.get_status_all()
assert len(statuses) == 1
assert statuses[0].runtime_status == df.OrchestrationRuntimeStatus.Running
assert statuses[0].input_ == {"in": 1}
assert statuses[0].output == {"out": 2}
assert statuses[0].custom_status == "cs"
finally:
await client.close()

Expand All @@ -582,6 +633,9 @@ async def test_get_status_by_returns_wrapped_list():
statuses = await client.get_status_by(
runtime_status=[df.OrchestrationRuntimeStatus.Running])
assert statuses[0].instance_id == "abc"
assert statuses[0].input_ == {"in": 1}
assert statuses[0].output == {"out": 2}
assert statuses[0].custom_status == "cs"
finally:
await client.close()

Expand Down
Loading