Skip to content

Commit 12bd085

Browse files
committed
Add async client parity coverage
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 76f2163a-0773-4889-b627-1bd4e5c7bea0
1 parent 4cce8a8 commit 12bd085

5 files changed

Lines changed: 68 additions & 18 deletions

File tree

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,9 @@ def durable_client_input(self,
386386
@self._build_function
387387
def wrap(fb: FunctionBuilder) -> FunctionBuilder:
388388
def decorator() -> FunctionBuilder:
389-
# The durableClient binding converter
390-
# (``DurableClientConverter``) constructs the rich
391-
# ``DurableFunctionsClient`` from the host-supplied configuration
392-
# string during decode, so no per-function client middleware is
393-
# needed here.
389+
# The converter returns the host configuration string. The
390+
# function wrapper below constructs and closes the requested
391+
# synchronous or asynchronous rich client per invocation.
394392
fb.add_binding(
395393
binding=DurableClient(name=client_name,
396394
task_hub=task_hub,
@@ -400,13 +398,10 @@ def decorator() -> FunctionBuilder:
400398
return decorator()
401399

402400
def attach_client_function(user_fn: Callable[..., Any]) -> FunctionBuilder:
403-
# Expose the raw client function for unit testing, mirroring the
404-
# ``.orchestrator_function`` and ``.entity_function`` attributes.
405-
# Unlike v1 there is no client middleware wrapper (the durableClient
406-
# binding converter builds the rich client during decode), so the
407-
# user function is registered directly and ``.client_function``
408-
# points back at it. Internal callers pass an already-built
409-
# ``FunctionBuilder`` (e.g. history export), which is left untouched.
401+
# Expose the original client function for unit testing, mirroring
402+
# ``.orchestrator_function`` and ``.entity_function``. The registered
403+
# wrapper resolves the client from the binding configuration and
404+
# closes it after the invocation.
410405
from ..client import DurableFunctionsClient, SyncDurableFunctionsClient
411406

412407
function = (user_fn._function._func # pyright: ignore[reportPrivateUsage]

azure-functions-durable/azure/durable_functions/internal/converters.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,9 @@ def register_durable_converters() -> None:
224224
"""Register this package's durable binding converters with azure-functions.
225225
226226
Overrides the SDK's built-in converters for the four durable binding types
227-
so the host uses the durabletask-based encodings and the durable-client
228-
binding is decoded to a :class:`DurableFunctionsClient`.
227+
so the host uses the durabletask-based encodings. The durable-client binding
228+
is decoded to its host configuration string, which the decorator converts to
229+
the requested synchronous or asynchronous rich client per invocation.
229230
"""
230231
func.register_converter(
231232
ORCHESTRATION_TRIGGER, OrchestrationTriggerConverter, overwrite=True)

durabletask/scheduled/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
This package provides a recurring schedule feature built on top of durable
77
entities and a helper orchestrator. Enable it on a worker via
88
:meth:`durabletask.worker.TaskHubGrpcWorker.configure_scheduled_tasks`, then
9-
manage schedules from the client via :class:`ScheduledTaskClient`.
9+
manage schedules from a synchronous :class:`ScheduledTaskClient` or
10+
asynchronous :class:`AsyncScheduledTaskClient`.
1011
"""
1112

1213
from durabletask.scheduled.client import (AsyncScheduleClient,

tests/durabletask/extensions/history_export/test_client.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from durabletask import client, task, worker
2121
from durabletask.extensions.history_export import (
22+
AsyncExportHistoryClient,
2223
ExportDestination,
2324
ExportFormat,
2425
ExportFormatKind,
@@ -148,6 +149,32 @@ def test_create_get_and_wait_for_job_end_to_end(
148149
assert first["kind"] == "metadata"
149150

150151

152+
async def test_async_client_create_list_wait_and_delete(
153+
writer, seeded_terminal_instances,
154+
):
155+
async with client.AsyncTaskHubGrpcClient(host_address=HOST) as dt_client:
156+
export_client = AsyncExportHistoryClient(dt_client, writer)
157+
now = datetime.now(timezone.utc)
158+
desc = await export_client.create_job(ExportJobCreationOptions(
159+
mode=ExportMode.BATCH,
160+
completed_time_from=now - timedelta(hours=1),
161+
completed_time_to=now + timedelta(hours=1),
162+
destination=ExportDestination(container="exports", prefix="async"),
163+
format=ExportFormat(kind=ExportFormatKind.JSON),
164+
))
165+
166+
final = await export_client.wait_for_job(
167+
desc.job_id, timeout=30, poll_interval=0.1)
168+
assert final.status == ExportJobStatus.COMPLETED
169+
assert (await export_client.get_job(desc.job_id)) is not None
170+
assert desc.job_id in {
171+
job.job_id async for job in export_client.list_jobs()
172+
}
173+
174+
await export_client.delete_job(desc.job_id)
175+
assert await export_client.get_job(desc.job_id) is None
176+
177+
151178
def test_get_job_returns_none_for_unknown_id(export_client):
152179
assert export_client.get_job("does-not-exist") is None
153180

tests/durabletask/scheduled/test_scheduled_e2e.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
import pytest
1111

1212
from durabletask import client, task, worker
13-
from durabletask.scheduled import (ScheduledTaskClient, ScheduleCreationOptions,
14-
ScheduleQuery, ScheduleStatus,
15-
ScheduleUpdateOptions)
13+
from durabletask.scheduled import (AsyncScheduledTaskClient,
14+
ScheduledTaskClient,
15+
ScheduleCreationOptions, ScheduleQuery,
16+
ScheduleStatus, ScheduleUpdateOptions)
1617
from durabletask.testing import create_test_backend
1718

1819
from tests.durabletask._port_utils import find_free_port
@@ -113,6 +114,31 @@ def test_get_nonexistent_returns_none():
113114
assert scheduled.get_schedule("does-not-exist") is None
114115

115116

117+
async def test_async_client_create_list_and_delete():
118+
with _make_worker() as w:
119+
w.start()
120+
async with client.AsyncTaskHubGrpcClient(host_address=HOST) as c:
121+
scheduled = AsyncScheduledTaskClient(c)
122+
schedule = await scheduled.create_schedule(ScheduleCreationOptions(
123+
schedule_id="async-schedule",
124+
orchestration_name=task.get_name(target_orchestrator),
125+
interval=timedelta(seconds=30),
126+
start_at=datetime.now(timezone.utc) + timedelta(hours=1),
127+
))
128+
129+
assert (await scheduled.get_schedule("async-schedule")) is not None
130+
listed = await scheduled.list_schedules(
131+
ScheduleQuery(schedule_id_prefix="async-"))
132+
assert [item.schedule_id for item in listed] == ["async-schedule"]
133+
134+
await schedule.pause()
135+
assert (await schedule.describe()).status == ScheduleStatus.PAUSED
136+
await schedule.resume()
137+
assert (await schedule.describe()).status == ScheduleStatus.ACTIVE
138+
await schedule.delete()
139+
assert await scheduled.get_schedule("async-schedule") is None
140+
141+
116142
def test_pause_and_resume():
117143
with _make_worker() as w:
118144
w.start()

0 commit comments

Comments
 (0)