|
| 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