|
24 | 24 | import time |
25 | 25 | import uuid |
26 | 26 | import warnings |
27 | | -from collections.abc import AsyncIterator, Callable |
| 27 | +from collections.abc import AsyncIterator, Callable, Mapping, Sequence |
28 | 28 | from dataclasses import dataclass |
29 | 29 | from importlib.metadata import PackageNotFoundError |
30 | 30 | from importlib.metadata import version as _pkg_version |
|
43 | 43 | ) |
44 | 44 | from .external_storage import ExternalPayloadCache, ExternalPayloadStoragePolicy, ExternalStorageDriver |
45 | 45 | from .metrics import CLIENT_REQUEST_DURATION_SECONDS, CLIENT_REQUESTS, NOOP_METRICS, MetricsRecorder |
| 46 | +from .nexus import NexusOperationResult, nexus_request_payload |
46 | 47 | from .retry_policy import TransportRetryPolicy |
47 | 48 |
|
48 | 49 | PROTOCOL_VERSION = "1.1" |
@@ -1563,6 +1564,112 @@ def get_workflow_handle( |
1563 | 1564 | """ |
1564 | 1565 | return WorkflowHandle(self, workflow_id=workflow_id, run_id=run_id, workflow_type=workflow_type) |
1565 | 1566 |
|
| 1567 | + async def execute_nexus_operation( |
| 1568 | + self, |
| 1569 | + endpoint_name: str, |
| 1570 | + service_name: str, |
| 1571 | + operation_name: str, |
| 1572 | + arguments: Sequence[Any] | None = None, |
| 1573 | + *, |
| 1574 | + mode: str | None = None, |
| 1575 | + wait_for: str | None = None, |
| 1576 | + wait_timeout_seconds: int | None = None, |
| 1577 | + idempotency_key: str | None = None, |
| 1578 | + payload_codec: str | None = None, |
| 1579 | + caller_namespace: str | None = None, |
| 1580 | + caller_workflow_instance_id: str | None = None, |
| 1581 | + caller_workflow_run_id: str | None = None, |
| 1582 | + service_sdk_language: str | None = None, |
| 1583 | + artifact_tuple: Mapping[str, Any] | None = None, |
| 1584 | + published_artifact_worker_execution: bool | None = None, |
| 1585 | + target_workflow_instance_id: str | None = None, |
| 1586 | + target_workflow_run_id: str | None = None, |
| 1587 | + connection: str | None = None, |
| 1588 | + queue: str | None = None, |
| 1589 | + business_key: str | None = None, |
| 1590 | + labels: Mapping[str, Any] | None = None, |
| 1591 | + memo: Mapping[str, Any] | None = None, |
| 1592 | + search_attributes: Mapping[str, Any] | None = None, |
| 1593 | + duplicate_start_policy: str | None = None, |
| 1594 | + raise_on_failure: bool = True, |
| 1595 | + ) -> NexusOperationResult: |
| 1596 | + """Execute a Nexus service operation through the service-catalog API. |
| 1597 | +
|
| 1598 | + Workflow code should normally yield |
| 1599 | + :meth:`durable_workflow.workflow.WorkflowContext.call_nexus_service`; |
| 1600 | + the worker uses this client method to make the single durable |
| 1601 | + execute request and records the returned service-call surface into |
| 1602 | + workflow history. |
| 1603 | + """ |
| 1604 | + request_body = nexus_request_payload( |
| 1605 | + arguments=list(arguments) if arguments is not None else [], |
| 1606 | + payload_codec=payload_codec, |
| 1607 | + mode=mode, |
| 1608 | + wait_for=wait_for, |
| 1609 | + wait_timeout_seconds=wait_timeout_seconds, |
| 1610 | + idempotency_key=idempotency_key, |
| 1611 | + caller_namespace=caller_namespace or self.namespace, |
| 1612 | + caller_workflow_instance_id=caller_workflow_instance_id, |
| 1613 | + caller_workflow_run_id=caller_workflow_run_id, |
| 1614 | + target_workflow_instance_id=target_workflow_instance_id, |
| 1615 | + target_workflow_run_id=target_workflow_run_id, |
| 1616 | + connection=connection, |
| 1617 | + queue=queue, |
| 1618 | + business_key=business_key, |
| 1619 | + labels=labels, |
| 1620 | + memo=memo, |
| 1621 | + search_attributes=search_attributes, |
| 1622 | + duplicate_start_policy=duplicate_start_policy, |
| 1623 | + ) |
| 1624 | + path = ( |
| 1625 | + f"/service-endpoints/{quote(endpoint_name, safe='')}" |
| 1626 | + f"/services/{quote(service_name, safe='')}" |
| 1627 | + f"/operations/{quote(operation_name, safe='')}/execute" |
| 1628 | + ) |
| 1629 | + context = f"{endpoint_name}/{service_name}/{operation_name}" |
| 1630 | + |
| 1631 | + try: |
| 1632 | + data = await self._request("POST", path, json=request_body, context=context) |
| 1633 | + except ServerError as exc: |
| 1634 | + if exc.status != 409 or not isinstance(exc.body, dict): |
| 1635 | + raise |
| 1636 | + result = NexusOperationResult.from_response( |
| 1637 | + exc.body, |
| 1638 | + endpoint_name=endpoint_name, |
| 1639 | + service_name=service_name, |
| 1640 | + operation_name=operation_name, |
| 1641 | + request_payload=request_body, |
| 1642 | + service_sdk_language=service_sdk_language, |
| 1643 | + artifact_tuple=artifact_tuple, |
| 1644 | + published_artifact_worker_execution=published_artifact_worker_execution, |
| 1645 | + ) |
| 1646 | + if raise_on_failure: |
| 1647 | + raise result.to_failure() from exc |
| 1648 | + return result |
| 1649 | + |
| 1650 | + if not isinstance(data, dict): |
| 1651 | + raise ServerError( |
| 1652 | + 200, |
| 1653 | + { |
| 1654 | + "reason": "invalid_nexus_operation_response", |
| 1655 | + "message": f"expected JSON object, got {type(data).__name__}", |
| 1656 | + }, |
| 1657 | + ) |
| 1658 | + |
| 1659 | + result = NexusOperationResult.from_response( |
| 1660 | + data, |
| 1661 | + endpoint_name=endpoint_name, |
| 1662 | + service_name=service_name, |
| 1663 | + operation_name=operation_name, |
| 1664 | + request_payload=request_body, |
| 1665 | + service_sdk_language=service_sdk_language, |
| 1666 | + artifact_tuple=artifact_tuple, |
| 1667 | + published_artifact_worker_execution=published_artifact_worker_execution, |
| 1668 | + ) |
| 1669 | + if raise_on_failure and result.is_failure: |
| 1670 | + raise result.to_failure() |
| 1671 | + return result |
| 1672 | + |
1566 | 1673 | # ── Health ───────────────────────────────────────────────────────── |
1567 | 1674 | async def health(self) -> dict[str, Any]: |
1568 | 1675 | """Call the server's ``/health`` endpoint and return the JSON response.""" |
|
0 commit comments