Skip to content

Commit 079744f

Browse files
andystaplesCopilot
andcommitted
Use the native async shape for Functions orchestrators and add call_http helper
Remove AsyncDurableOrchestrationContext. Single-parameter async def orchestrators now fail with TypeError when decorated or registered, pointing to the two-parameter AsyncOrchestrationContext shape. Add azure.durable_functions.call_http for durabletask orchestration contexts. It returns a cold coroutine for AsyncOrchestrationContext and an eager task for OrchestrationContext, and shares the existing durable HTTP implementation with DurableOrchestrationContext.call_http. Restore the generator-only orchestrator_generator_wrapper testing helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 48b6c6a commit 079744f

13 files changed

Lines changed: 430 additions & 1180 deletions

File tree

‎azure-functions-durable/azure/durable_functions/__init__.py‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from .decorators.durable_app import Blueprint, DFApp
1010
from .client import DurableFunctionsClient, SyncDurableFunctionsClient
11+
from .http.call import call_http
1112
from .http.models import DurableHttpRequest, DurableHttpResponse, HttpRetryOptions
1213
from .orchestrator import Orchestrator
1314
from .internal.compat.retry_options import RetryOptions
@@ -17,10 +18,7 @@
1718
from .internal.compat.entity_state_response import EntityStateResponse
1819
from .internal.compat.entity_id import EntityId
1920
from .internal.compat.token_source import ManagedIdentityTokenSource, TokenSource
20-
from .internal.compat.orchestration_context import (
21-
AsyncDurableOrchestrationContext,
22-
DurableOrchestrationContext,
23-
)
21+
from .internal.compat.orchestration_context import DurableOrchestrationContext
2422
from .internal.compat.entity_context import DurableEntityContext
2523
from .internal.compat.compat_aliases import (
2624
DurableOrchestrationClient,
@@ -42,7 +40,6 @@
4240

4341
__all__ = [
4442
"Blueprint",
45-
"AsyncDurableOrchestrationContext",
4643
"DFApp",
4744
"DurableEntityContext",
4845
"DurableFunctionsClient",
@@ -63,4 +60,5 @@
6360
"RetryOptions",
6461
"RetryPolicy",
6562
"TokenSource",
63+
"call_http",
6664
]

‎azure-functions-durable/azure/durable_functions/decorators/durable_app.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
builtin_http_poll_orchestrator,
2222
)
2323
from ..internal.compat.activity import wrap_activity, wrap_activity_payloads
24-
from ..internal.compat.orchestration_context import FunctionsOrchestrator
24+
from ..internal.compat.orchestration_context import FunctionsOrchestrator, validate_orchestrator
2525
from ..internal.invocation import preserve_function_source, wrap_invocation
2626
from ..worker import DurableFunctionsWorker
2727

@@ -190,6 +190,7 @@ def _configure_orchestrator_callable(
190190
"""
191191
def decorator(orchestrator_func: FunctionsOrchestrator) -> FunctionBuilder:
192192
# Construct an orchestrator based on the end-user code
193+
validate_orchestrator(orchestrator_func)
193194

194195
if input_type is not None:
195196
# Stash the decorator-declared input type so the runtime can
@@ -297,6 +298,12 @@ def orchestration_trigger(self, context_name: str,
297298
``context.get_input()`` decodes the input payload to this type. A
298299
call-site ``expected_type`` argument on ``get_input`` takes
299300
precedence over this value.
301+
302+
Raises
303+
------
304+
TypeError
305+
When applied to a single-parameter ``async def`` orchestrator.
306+
Async orchestrators take ``(ctx: task.AsyncOrchestrationContext, input)``.
300307
"""
301308
@self._build_function
302309
def wrap(fb: FunctionBuilder) -> FunctionBuilder:
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Durable HTTP calls from durabletask orchestration contexts."""
5+
6+
from __future__ import annotations
7+
8+
import json
9+
from collections.abc import Coroutine
10+
from typing import Any, overload
11+
12+
from durabletask import task
13+
14+
from ..internal.compat.token_source import TokenSource
15+
from .builtin import BUILTIN_HTTP_POLL_ORCHESTRATOR_NAME
16+
from .models import DurableHttpRequest, DurableHttpResponse, HttpRetryOptions
17+
18+
19+
def _build_request(
20+
method: str,
21+
uri: str,
22+
content: Any,
23+
headers: dict[str, str] | None,
24+
token_source: TokenSource | None,
25+
is_raw_str: bool,
26+
retry_options: HttpRetryOptions | None) -> dict[str, Any]:
27+
if is_raw_str and not isinstance(content, str):
28+
raise TypeError(
29+
"Invalid use of 'is_raw_str' parameter: 'is_raw_str' is set to "
30+
"'True' but 'content' is not an instance of type 'str'. Either "
31+
"set 'is_raw_str' to 'False', or ensure your 'content' is of "
32+
"type 'str'.")
33+
34+
json_content: str | None = None
35+
if content is not None:
36+
json_content = content if is_raw_str else json.dumps(content)
37+
38+
return DurableHttpRequest(
39+
method, uri, json_content, headers, token_source, retry_options).to_json()
40+
41+
42+
def schedule_http_request(
43+
ctx: task.OrchestrationContext,
44+
method: str,
45+
uri: str,
46+
content: Any = None,
47+
headers: dict[str, str] | None = None,
48+
token_source: TokenSource | None = None,
49+
is_raw_str: bool = False,
50+
retry_options: HttpRetryOptions | None = None) -> task.Task[DurableHttpResponse]:
51+
"""Schedule the built-in durable HTTP sub-orchestration on an eager context."""
52+
request = _build_request(method, uri, content, headers, token_source, is_raw_str, retry_options)
53+
return ctx.call_sub_orchestrator(
54+
BUILTIN_HTTP_POLL_ORCHESTRATOR_NAME, input=request, return_type=DurableHttpResponse)
55+
56+
57+
async def _call_http_async(
58+
ctx: task.AsyncOrchestrationContext,
59+
method: str,
60+
uri: str,
61+
content: Any,
62+
headers: dict[str, str] | None,
63+
token_source: TokenSource | None,
64+
is_raw_str: bool,
65+
retry_options: HttpRetryOptions | None) -> DurableHttpResponse:
66+
request = _build_request(method, uri, content, headers, token_source, is_raw_str, retry_options)
67+
return await ctx.call_sub_orchestrator(
68+
BUILTIN_HTTP_POLL_ORCHESTRATOR_NAME, input=request, return_type=DurableHttpResponse)
69+
70+
71+
@overload
72+
def call_http(
73+
ctx: task.AsyncOrchestrationContext,
74+
method: str,
75+
uri: str,
76+
*,
77+
content: Any = None,
78+
headers: dict[str, str] | None = None,
79+
token_source: TokenSource | None = None,
80+
is_raw_str: bool = False,
81+
retry_options: HttpRetryOptions | None = None,
82+
) -> Coroutine[Any, Any, DurableHttpResponse]:
83+
...
84+
85+
86+
@overload
87+
def call_http(
88+
ctx: task.OrchestrationContext,
89+
method: str,
90+
uri: str,
91+
*,
92+
content: Any = None,
93+
headers: dict[str, str] | None = None,
94+
token_source: TokenSource | None = None,
95+
is_raw_str: bool = False,
96+
retry_options: HttpRetryOptions | None = None,
97+
) -> task.Task[DurableHttpResponse]:
98+
...
99+
100+
101+
def call_http(
102+
ctx: task.AsyncOrchestrationContext | task.OrchestrationContext,
103+
method: str,
104+
uri: str,
105+
*,
106+
content: Any = None,
107+
headers: dict[str, str] | None = None,
108+
token_source: TokenSource | None = None,
109+
is_raw_str: bool = False,
110+
retry_options: HttpRetryOptions | None = None,
111+
) -> Coroutine[Any, Any, DurableHttpResponse] | task.Task[DurableHttpResponse]:
112+
"""Make a durable HTTP request from an orchestrator.
113+
114+
The request is executed by a built-in activity. If the endpoint responds
115+
with ``202 Accepted`` and a ``Location`` header, a built-in
116+
sub-orchestration polls that location until the operation completes,
117+
honoring ``Retry-After``. The result is a :class:`DurableHttpResponse`;
118+
unselected non-2xx responses are returned rather than raised.
119+
120+
With an :class:`~durabletask.task.AsyncOrchestrationContext`, this returns
121+
a single-use coroutine for ``await``. Like other async context operations,
122+
it is cold: validation, content serialization, and scheduling happen when
123+
the coroutine starts. With an
124+
:class:`~durabletask.task.OrchestrationContext`, the request is scheduled
125+
immediately and a durable task is returned for ``yield``.
126+
127+
Parameters
128+
----------
129+
ctx: AsyncOrchestrationContext | OrchestrationContext
130+
The orchestration context supplied to the orchestrator.
131+
method: str
132+
The HTTP request method.
133+
uri: str
134+
The HTTP request URI.
135+
content: Any
136+
The request content. A ``str`` is sent as JSON unless ``is_raw_str``
137+
is set; other values are serialized to JSON.
138+
headers: dict[str, str] | None
139+
The request headers.
140+
token_source: TokenSource | None
141+
The source of an OAuth token to add to the request.
142+
is_raw_str: bool
143+
When ``True``, send ``str`` content as-is instead of JSON-encoding
144+
it. Requires ``content`` to be a ``str``.
145+
retry_options: HttpRetryOptions | None
146+
Opt-in durable failure retries for the initial request only.
147+
Selected HTTP statuses and activity failures fail the call after
148+
exhaustion. Location polls do not inherit the policy. Only enable
149+
retries when repeating the request's side effects is safe.
150+
151+
Raises
152+
------
153+
TypeError
154+
If ``ctx`` is not a durabletask orchestration context, or if
155+
``is_raw_str`` is set and ``content`` is not a ``str``.
156+
"""
157+
if isinstance(ctx, task.AsyncOrchestrationContext):
158+
return _call_http_async(
159+
ctx, method, uri, content, headers, token_source, is_raw_str, retry_options)
160+
# Annotations are not enforced at runtime; reject other contexts clearly.
161+
if not isinstance(ctx, task.OrchestrationContext): # pyright: ignore[reportUnnecessaryIsInstance]
162+
raise TypeError(
163+
"call_http requires a durabletask AsyncOrchestrationContext or OrchestrationContext. "
164+
"Orchestrators that receive a DurableOrchestrationContext should use context.call_http().")
165+
return schedule_http_request(
166+
ctx, method, uri, content, headers, token_source, is_raw_str, retry_options)

0 commit comments

Comments
 (0)