Skip to content

Commit aa738a4

Browse files
committed
address comments, cleanup run_id, extend tests
1 parent 02aab62 commit aa738a4

8 files changed

Lines changed: 322 additions & 200 deletions

File tree

temporalio/client/_workflow.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -956,8 +956,6 @@ async def _start_update(
956956
result_type: type | None = None,
957957
rpc_metadata: Mapping[str, str | bytes] = {},
958958
rpc_timeout: timedelta | None = None,
959-
run_id: str | None = None,
960-
first_execution_run_id: str | None = None,
961959
# The following options are for Nexus Operation-backed updates. Experimental and unstable
962960
callbacks: Sequence[Callback] | None = None,
963961
links: Sequence[temporalio.api.common.v1.Link] | None = None,
@@ -970,16 +968,11 @@ async def _start_update(
970968
temporalio.workflow._UpdateDefinition.get_name_and_result_type(update)
971969
)
972970

973-
if run_id == None:
974-
run_id = self._run_id
975-
if first_execution_run_id == None:
976-
first_execution_run_id = self.first_execution_run_id
977-
978971
return await self._client._impl.start_workflow_update(
979972
StartWorkflowUpdateInput(
980973
id=self._id,
981-
run_id=run_id,
982-
first_execution_run_id=first_execution_run_id,
974+
run_id=self._run_id,
975+
first_execution_run_id=self._first_execution_run_id,
983976
update_id=id,
984977
update=update_name,
985978
args=temporalio.common._arg_or_args(arg, args),

temporalio/nexus/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
TemporalOperationHandler,
3131
)
3232
from ._temporal_client import TemporalNexusClient, TemporalOperationResult
33-
from ._token import UpdateHandle, WorkflowHandle
33+
from ._token import WorkflowHandle
3434

3535
__all__ = (
3636
"workflow_run_operation",
@@ -51,7 +51,6 @@
5151
"wait_for_worker_shutdown",
5252
"wait_for_worker_shutdown_sync",
5353
"WorkflowHandle",
54-
"UpdateHandle",
5554
"TemporalNexusClient",
5655
"TemporalOperationStartHandlerFunc",
5756
"TemporalOperationHandler",

temporalio/nexus/_operation_context.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -724,15 +724,15 @@ async def _start_nexus_backed_workflow_update( # pyright: ignore[reportUnusedFu
724724
update: str | Callable,
725725
arg: Any = temporalio.common._arg_unset,
726726
args: Sequence[Any] = [],
727-
id: str | None = None,
727+
update_id: str | None = None,
728728
result_type: type | None = None,
729729
rpc_metadata: Mapping[str, str | bytes] = {},
730730
rpc_timeout: timedelta | None = None,
731731
run_id: str | None = None,
732732
first_execution_run_id: str | None = None,
733733
) -> temporalio.client.WorkflowUpdateHandle[Any]:
734734
# Default update ID to the Nexus request ID for retry-safety (matches sdk-go).
735-
update_id = id or temporal_context.nexus_context.request_id
735+
update_id = update_id or temporal_context.nexus_context.request_id
736736
# This token is different from the actual token returned to the caller
737737
# because return token will have the run_id that is unknowable before
738738
# making the call. If run_id is passed, then it will be the same
@@ -743,7 +743,9 @@ async def _start_nexus_backed_workflow_update( # pyright: ignore[reportUnusedFu
743743
update_id=update_id,
744744
run_id=run_id,
745745
).encode()
746-
workflow_handle = temporal_context.client.get_workflow_handle(workflow_id)
746+
workflow_handle = temporal_context.client.get_workflow_handle(
747+
workflow_id, run_id=run_id, first_execution_run_id=first_execution_run_id
748+
)
747749
return await workflow_handle._start_update(
748750
update,
749751
arg,
@@ -756,6 +758,4 @@ async def _start_nexus_backed_workflow_update( # pyright: ignore[reportUnusedFu
756758
callbacks=temporal_context._get_callbacks(token),
757759
links=temporal_context._get_request_links(),
758760
request_id=temporal_context.nexus_context.request_id,
759-
run_id=run_id,
760-
first_execution_run_id=first_execution_run_id,
761761
)

temporalio/nexus/_operation_handlers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,22 +234,22 @@ async def cancel_workflow_run(
234234
)
235235
await workflow_handle.cancel()
236236

237-
# draft-review: maybe just move it inline, no need for a function just to error out
238-
# check after review in case theres some other way to override/supply custom cancels
239237
async def cancel_workflow_update(
240238
self,
241239
ctx: TemporalCancelOperationContext, # pyright: ignore[reportUnusedParameter]
242240
options: CancelUpdateWorkflowOptions, # pyright: ignore[reportUnusedParameter]
243241
) -> None:
244-
"""Cancels the workflow update backing the Nexus operation.
242+
"""Cancels the workflow update backing the Nexus operation. Cancellation is not natively supported for update-workflow Nexus operations.
243+
Inherit a TemporalOperationHandler and override this method to run cancellable workflow updates.
244+
245245
246246
.. warning::
247247
This API is experimental and unstable.
248248
"""
249249
raise HandlerError(
250250
"""
251251
Cancellation is not natively supported for update-workflow Nexus operations.
252-
Override a TemporalOperationHandler and implement this method to run cancellable workflow updates.
252+
Inherit a TemporalOperationHandler and override this method to run cancellable workflow updates.
253253
""",
254254
type=HandlerErrorType.NOT_IMPLEMENTED,
255255
)

temporalio/nexus/_temporal_client.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
_start_nexus_backing_workflow,
2626
_TemporalStartOperationContext,
2727
)
28-
from temporalio.nexus._token import UpdateHandle
2928
from temporalio.types import (
3029
MethodAsyncNoParam,
3130
MethodAsyncSingleParam,
@@ -35,6 +34,8 @@
3534
SelfType,
3635
)
3736

37+
from ._token import OperationToken, OperationTokenType
38+
3839
if TYPE_CHECKING:
3940
import temporalio.client
4041
import temporalio.workflow
@@ -289,9 +290,11 @@ async def start_workflow_update(
289290
workflow_id: str,
290291
update: temporalio.workflow.UpdateMethodMultiParam[[Any], ReturnType],
291292
*,
292-
id: str | None = None,
293+
update_id: str | None = None,
293294
rpc_metadata: Mapping[str, str | bytes] = {},
294295
rpc_timeout: timedelta | None = None,
296+
run_id: str | None = None,
297+
first_execution_run_id: str | None = None,
295298
) -> TemporalOperationResult[ReturnType]: ...
296299

297300
# Overload for single-param update
@@ -304,9 +307,11 @@ async def start_workflow_update(
304307
],
305308
arg: ParamType,
306309
*,
307-
id: str | None = None,
310+
update_id: str | None = None,
308311
rpc_metadata: Mapping[str, str | bytes] = {},
309312
rpc_timeout: timedelta | None = None,
313+
run_id: str | None = None,
314+
first_execution_run_id: str | None = None,
310315
) -> TemporalOperationResult[ReturnType]: ...
311316

312317
# Overload for multi-param update
@@ -317,9 +322,11 @@ async def start_workflow_update(
317322
update: temporalio.workflow.UpdateMethodMultiParam[MultiParamSpec, ReturnType],
318323
*,
319324
args: MultiParamSpec.args, # type: ignore
320-
id: str | None = None,
325+
update_id: str | None = None,
321326
rpc_metadata: Mapping[str, str | bytes] = {},
322327
rpc_timeout: timedelta | None = None,
328+
run_id: str | None = None,
329+
first_execution_run_id: str | None = None,
323330
) -> TemporalOperationResult[ReturnType]: ...
324331

325332
# Overload for string-name update
@@ -331,14 +338,14 @@ async def start_workflow_update(
331338
arg: Any = temporalio.common._arg_unset,
332339
*,
333340
args: Sequence[Any] = [],
334-
id: str | None = None,
341+
update_id: str | None = None,
335342
result_type: type[ReturnType] | None = None,
336343
rpc_metadata: Mapping[str, str | bytes] = {},
337344
rpc_timeout: timedelta | None = None,
345+
run_id: str | None = None,
346+
first_execution_run_id: str | None = None,
338347
) -> TemporalOperationResult[ReturnType]: ...
339348

340-
# draft-review: check why run_id and first_execution_run_id are not used
341-
# for update workflow in python sdk
342349
@abstractmethod
343350
async def start_workflow_update(
344351
self,
@@ -347,7 +354,7 @@ async def start_workflow_update(
347354
arg: Any = temporalio.common._arg_unset,
348355
*,
349356
args: Sequence[Any] = [],
350-
id: str | None = None,
357+
update_id: str | None = None,
351358
result_type: type | None = None,
352359
rpc_metadata: Mapping[str, str | bytes] = {},
353360
rpc_timeout: timedelta | None = None,
@@ -467,7 +474,7 @@ async def start_workflow_update(
467474
arg: Any = temporalio.common._arg_unset,
468475
*,
469476
args: Sequence[Any] = [],
470-
id: str | None = None,
477+
update_id: str | None = None,
471478
result_type: type | None = None,
472479
rpc_metadata: Mapping[str, str | bytes] = {},
473480
rpc_timeout: timedelta | None = None,
@@ -487,15 +494,14 @@ async def start_workflow_update(
487494
update=update,
488495
arg=arg,
489496
args=args,
490-
id=id,
497+
update_id=update_id,
491498
result_type=result_type,
492499
rpc_metadata=rpc_metadata,
493500
rpc_timeout=rpc_timeout,
494501
run_id=run_id,
495502
first_execution_run_id=first_execution_run_id,
496503
)
497504
# If the update has already completed, return the result synchronously
498-
# This is in-line with the Go implementation as well
499505
if update_handle._known_outcome is not None:
500506
try:
501507
result = await update_handle.result()
@@ -504,7 +510,11 @@ async def start_workflow_update(
504510
str(err), state=OperationErrorState.FAILED
505511
) from err
506512
return TemporalOperationResult.sync(result)
507-
nexus_handle: UpdateHandle[Any] = (
508-
UpdateHandle._unsafe_from_client_workflow_update_handle(update_handle)
509-
)
510-
return TemporalOperationResult.async_token(nexus_handle.to_token())
513+
token = OperationToken(
514+
type=OperationTokenType.UPDATE_WORKFLOW,
515+
namespace=update_handle._client.namespace,
516+
workflow_id=update_handle.workflow_id,
517+
update_id=update_handle.id,
518+
run_id=update_handle.workflow_run_id,
519+
).encode()
520+
return TemporalOperationResult.async_token(token)

temporalio/nexus/_token.py

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -211,91 +211,6 @@ def from_token(cls, token: str) -> WorkflowHandle[OutputT]:
211211
)
212212

213213

214-
@dataclass(frozen=True)
215-
class UpdateHandle(Generic[OutputT]):
216-
"""A handle to a workflow update that is backing a Nexus operation.
217-
218-
Do not instantiate this directly. Use
219-
:py:func:`temporalio.nexus.TemporalNexusClient.start_workflow_update` to create a
220-
handle.
221-
"""
222-
223-
namespace: str
224-
workflow_id: str
225-
update_id: str
226-
run_id: str | None = None
227-
# Version of the token. Treated as v1 if missing. This field is not included in the
228-
# serialized token; it's only used to reject newer token versions on load.
229-
version: int | None = None
230-
231-
def _to_client_workflow_update_handle(
232-
self,
233-
client: temporalio.client.Client,
234-
result_type: type[OutputT] | None = None,
235-
) -> temporalio.client.WorkflowUpdateHandle[Any]:
236-
"""Create a :py:class:`temporalio.client.WorkflowUpdateHandle` from the token."""
237-
if client.namespace != self.namespace:
238-
raise ValueError(
239-
f"Client namespace {client.namespace} does not match "
240-
f"operation token namespace {self.namespace}"
241-
)
242-
workflow_handle = client.get_workflow_handle(self.workflow_id)
243-
return workflow_handle.get_update_handle(
244-
self.update_id, result_type=result_type
245-
)
246-
247-
@classmethod
248-
def _unsafe_from_client_workflow_update_handle(
249-
cls, workflow_update_handle: temporalio.client.WorkflowUpdateHandle[Any]
250-
) -> UpdateHandle[OutputT]:
251-
"""Create a :py:class:`UpdateHandle` from a :py:class:`temporalio.client.WorkflowUpdateHandle`.
252-
253-
This is a private method not intended to be used by users. It does not check
254-
that the supplied client.WorkflowUpdateHandle references a workflow that has been
255-
instrumented to supply the result of a Nexus operation.
256-
"""
257-
return cls(
258-
namespace=workflow_update_handle._client.namespace,
259-
workflow_id=workflow_update_handle.workflow_id,
260-
run_id=workflow_update_handle.workflow_run_id,
261-
update_id=workflow_update_handle._id,
262-
)
263-
264-
def to_token(self) -> str:
265-
"""Convert handle to a base64url-encoded token string."""
266-
return OperationToken(
267-
type=OperationTokenType.UPDATE_WORKFLOW,
268-
namespace=self.namespace,
269-
workflow_id=self.workflow_id,
270-
run_id=self.run_id,
271-
update_id=self.update_id,
272-
).encode()
273-
274-
@classmethod
275-
def from_token(cls, token: str) -> UpdateHandle[OutputT]:
276-
"""Decodes and validates a token from its base64url-encoded string representation."""
277-
op_token = OperationToken.decode(token)
278-
if op_token.type != OperationTokenType.UPDATE_WORKFLOW:
279-
raise TypeError(
280-
f"invalid update token type: {op_token.type}, expected: {OperationTokenType.UPDATE_WORKFLOW}"
281-
)
282-
283-
if op_token.version is not None and op_token.version != 0:
284-
raise TypeError(
285-
"invalid update token: 'v' field, if present, must be 0 or null/absent"
286-
)
287-
288-
assert op_token.update_id is not None
289-
290-
return cls(
291-
namespace=op_token.namespace,
292-
workflow_id=op_token.workflow_id,
293-
run_id=op_token.run_id,
294-
update_id=op_token.update_id,
295-
version=op_token.version,
296-
)
297-
298-
299214
def _base64url_encode_no_padding(data: bytes) -> str:
300215
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
301216

tests/nexus/test_operation_token.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from temporalio.nexus._token import (
88
OperationToken,
99
OperationTokenType,
10-
UpdateHandle,
1110
WorkflowHandle,
1211
)
1312

@@ -43,13 +42,6 @@ def test_workflow_handle_to_from_token_round_trip():
4342
assert WorkflowHandle[str].from_token(handle.to_token()) == handle
4443

4544

46-
def test_update_handle_to_from_token_round_trip():
47-
handle = UpdateHandle[str](
48-
namespace="default", workflow_id="workflow-id", update_id="update-id"
49-
)
50-
assert UpdateHandle[str].from_token(handle.to_token()) == handle
51-
52-
5345
@pytest.mark.parametrize(
5446
("token", "expected"),
5547
[

0 commit comments

Comments
 (0)