refactor: group rpc transport client and methods

This commit is contained in:
lda
2026-06-08 21:44:41 +07:00 Verified
parent 7582c1d6fb
commit c39d361873
25 changed files with 104 additions and 114 deletions
+17 -2
View File
@@ -113,7 +113,7 @@ def load_cli_context(
if rpc_url is not None: if rpc_url is not None:
_validate_rpc_url(rpc_url) _validate_rpc_url(rpc_url)
client = RpcWorkflowApiClient( client = rpc_client_from_target(
url=rpc_url, url=rpc_url,
timeout_seconds=_rpc_timeout_from_optional_config( timeout_seconds=_rpc_timeout_from_optional_config(
resolved_config_path, resolved_config_path,
@@ -161,7 +161,7 @@ def load_cli_context(
verbose=verbose, verbose=verbose,
) )
if isinstance(target, RpcHttpTargetConfig): if isinstance(target, RpcHttpTargetConfig):
client = RpcWorkflowApiClient( client = rpc_client_from_target(
url=str(target.url), url=str(target.url),
timeout_seconds=( timeout_seconds=(
rpc_timeout_seconds rpc_timeout_seconds
@@ -181,6 +181,21 @@ def load_cli_context(
raise ValueError(f"unsupported workflow target {target!r}") raise ValueError(f"unsupported workflow target {target!r}")
def rpc_client_from_target(
*,
url: str,
timeout_seconds: float,
) -> RpcWorkflowApiClient:
"""Build the remote workflow surface for a resolved RPC target.
CLI tests patch this project-owned seam instead of monkeypatching `httpx`
internals. The production path still keeps HTTP construction inside the
transport package.
"""
return RpcWorkflowApiClient(url=url, timeout_seconds=timeout_seconds)
def load_local_cli_context( def load_local_cli_context(
config_path: str | Path, config_path: str | Path,
*, *,
+1 -4
View File
@@ -5,9 +5,6 @@ Canonical implementation lives in `wf_sources_mcp.tool_wrappers`.
from __future__ import annotations from __future__ import annotations
from wf_sources_mcp.schema_models import model_from_schema
from wf_sources_mcp.tool_wrappers import wrap_discovered_tool from wf_sources_mcp.tool_wrappers import wrap_discovered_tool
_model_from_schema = model_from_schema # TODO: remove when callers migrate __all__ = ["wrap_discovered_tool"]
__all__ = ["_model_from_schema", "wrap_discovered_tool"]
+10 -8
View File
@@ -7,16 +7,18 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError from .errors import WorkflowRpcError
from .methods_admin import register_methods as register_admin_methods from .methods.admin import register_methods as register_admin_methods
from .methods_artifacts import register_methods as register_artifact_methods from .methods.artifacts import register_methods as register_artifact_methods
from .methods_capabilities import register_methods as register_capability_methods from .methods.capabilities import (
from .methods_deployments import register_methods as register_deployment_methods register_methods as register_capability_methods,
from .methods_drafts import register_methods as register_draft_methods )
from .methods_runs import register_methods as register_run_methods from .methods.deployments import register_methods as register_deployment_methods
from .methods_source_registry import ( from .methods.drafts import register_methods as register_draft_methods
from .methods.runs import register_methods as register_run_methods
from .methods.source_registry import (
register_methods as register_source_registry_methods, register_methods as register_source_registry_methods,
) )
from .methods_sources import register_methods as register_source_methods from .methods.sources import register_methods as register_source_methods
def create_rpc_app(server: WorkflowServer, *, rpc_path: str = "/rpc") -> jsonrpc.API: def create_rpc_app(server: WorkflowServer, *, rpc_path: str = "/rpc") -> jsonrpc.API:
@@ -2,17 +2,15 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import httpx # noqa: F401 # Backcompat for tests patching client.httpx.AsyncClient. from .admin import RpcAdminClientMixin
from .artifacts import RpcArtifactClientMixin
from .client_admin import RpcAdminClientMixin from .base import RpcClientTransport
from .client_artifacts import RpcArtifactClientMixin from .capabilities import RpcCapabilityClientMixin
from .client_base import RpcClientTransport from .deployments import RpcDeploymentClientMixin
from .client_capabilities import RpcCapabilityClientMixin from .drafts import RpcDraftClientMixin
from .client_deployments import RpcDeploymentClientMixin from .runs import RpcRunClientMixin
from .client_drafts import RpcDraftClientMixin from .source_registry import RpcSourceRegistryClientMixin
from .client_runs import RpcRunClientMixin from .sources import RpcSourceAdminClientMixin
from .client_source_registry import RpcSourceRegistryClientMixin
from .client_sources import RpcSourceAdminClientMixin
@dataclass(slots=True) @dataclass(slots=True)
@@ -6,14 +6,14 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import ( from ..models import (
AdminEmptyParams, AdminEmptyParams,
DeleteAuthParams, DeleteAuthParams,
InspectAuthParams, InspectAuthParams,
SaveAuthParams, SaveAuthParams,
) )
from .params import RpcParams from ..params import RpcParams
def register_methods( def register_methods(
@@ -6,9 +6,9 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import InspectArtifactParams, ListArtifactsParams, SaveArtifactParams from ..models import InspectArtifactParams, ListArtifactsParams, SaveArtifactParams
from .params import RpcParams from ..params import RpcParams
def register_methods( def register_methods(
@@ -6,9 +6,9 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import InspectCapabilityParams, ListCapabilitiesParams from ..models import InspectCapabilityParams, ListCapabilitiesParams
from .params import RpcParams from ..params import RpcParams
def register_methods( def register_methods(
@@ -6,15 +6,15 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import ( from ..models import (
DeleteDeploymentParams, DeleteDeploymentParams,
InspectDeploymentParams, InspectDeploymentParams,
ListDeploymentsParams, ListDeploymentsParams,
SaveDeploymentParams, SaveDeploymentParams,
ValidateDeploymentParams, ValidateDeploymentParams,
) )
from .params import RpcParams from ..params import RpcParams
def register_methods( def register_methods(
@@ -6,8 +6,8 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import ( from ..models import (
CreateArtifactFromWorkspaceParams, CreateArtifactFromWorkspaceParams,
CreateDraftFromCapabilityParams, CreateDraftFromCapabilityParams,
CreateWrapperFromWorkspaceParams, CreateWrapperFromWorkspaceParams,
@@ -18,7 +18,7 @@ from .models import (
ValidateDraftParams, ValidateDraftParams,
ValidateDraftWorkspaceParams, ValidateDraftWorkspaceParams,
) )
from .params import RpcParams from ..params import RpcParams
def register_methods( def register_methods(
@@ -6,14 +6,14 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import ( from ..models import (
InspectRunParams, InspectRunParams,
ReadRunTraceParams, ReadRunTraceParams,
ResumeRunParams, ResumeRunParams,
StartRunParams, StartRunParams,
) )
from .params import RpcParams from ..params import RpcParams
def register_methods( def register_methods(
@@ -7,8 +7,8 @@ import fastapi_jsonrpc as jsonrpc
from wf_api import WorkflowSourceRegistrySurface from wf_api import WorkflowSourceRegistrySurface
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import ( from ..models import (
AddRegistryEntryParams, AddRegistryEntryParams,
ApplyRegistryChangesParams, ApplyRegistryChangesParams,
InspectRegistryEntryParams, InspectRegistryEntryParams,
@@ -16,7 +16,7 @@ from .models import (
RegistryEntryIdParams, RegistryEntryIdParams,
UpdateRegistryEntryParams, UpdateRegistryEntryParams,
) )
from .params import RpcParams from ..params import RpcParams
def _require_source_registry_admin( def _require_source_registry_admin(
@@ -6,9 +6,9 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error from ..errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import InspectSourceParams, ListSourcesParams from ..models import InspectSourceParams, ListSourcesParams
from .params import RpcParams from ..params import RpcParams
def register_methods( def register_methods(
+29 -52
View File
@@ -8,12 +8,14 @@ from typing import Any, cast
import httpx import httpx
from typer.testing import CliRunner from typer.testing import CliRunner
import wf_cli.context as cli_context
from wf_api.models import RawWorkflowPlan from wf_api.models import RawWorkflowPlan
from wf_cli.app import app from wf_cli.app import app
from wf_cli.context import CliContext, load_cli_context, load_local_cli_context from wf_cli.context import CliContext, load_cli_context, load_local_cli_context
from wf_core import END from wf_core import END
from wf_server import build_local_static_workflow_server from wf_server import build_local_static_workflow_server
from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app
from wf_transport_rpc_http.client.sources import RpcSourceAdminClientMixin
class BrokenSourceAdmin: class BrokenSourceAdmin:
@@ -271,16 +273,29 @@ def _interrupt_plan() -> RawWorkflowPlan:
) )
def _patch_rpc_client_to_server(monkeypatch, server) -> None:
"""Route CLI-created RPC clients to an in-process ASGI test server."""
def fake_rpc_client_from_target(
*,
url: str,
timeout_seconds: float,
) -> RpcWorkflowApiClient:
return RpcWorkflowApiClient(
url=url,
timeout_seconds=timeout_seconds,
http_client=httpx.AsyncClient(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
),
)
monkeypatch.setattr(cli_context, "rpc_client_from_target", fake_rpc_client_from_target)
def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None: def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store") server = build_local_static_workflow_server(tmp_path / "store")
original_client = httpx.AsyncClient _patch_rpc_client_to_server(monkeypatch, server)
monkeypatch.setattr(
"wf_transport_rpc_http.client.httpx.AsyncClient",
lambda *args, **kwargs: original_client(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
),
)
config_path = tmp_path / "wf.json" config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8") config_path.write_text('{"version": 1}', encoding="utf-8")
@@ -325,14 +340,7 @@ def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
def test_wf_source_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None: def test_wf_source_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store") server = build_local_static_workflow_server(tmp_path / "store")
original_client = httpx.AsyncClient _patch_rpc_client_to_server(monkeypatch, server)
monkeypatch.setattr(
"wf_transport_rpc_http.client.httpx.AsyncClient",
lambda *args, **kwargs: original_client(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
),
)
config_path = tmp_path / "wf.json" config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8") config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner() runner = CliRunner()
@@ -352,14 +360,7 @@ def test_wf_remote_source_inspect_formats_expected_rpc_error(
tmp_path, tmp_path,
) -> None: ) -> None:
server = build_local_static_workflow_server(tmp_path / "store") server = build_local_static_workflow_server(tmp_path / "store")
original_client = httpx.AsyncClient _patch_rpc_client_to_server(monkeypatch, server)
monkeypatch.setattr(
"wf_transport_rpc_http.client.httpx.AsyncClient",
lambda *args, **kwargs: original_client(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
),
)
config_path = tmp_path / "wf.json" config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8") config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner() runner = CliRunner()
@@ -382,10 +383,7 @@ def test_wf_remote_source_list_formats_transport_error(monkeypatch, tmp_path) ->
request=httpx.Request("POST", "http://test/rpc"), request=httpx.Request("POST", "http://test/rpc"),
) )
monkeypatch.setattr( monkeypatch.setattr(RpcSourceAdminClientMixin, "list_sources", connection_failed)
"wf_transport_rpc_http.client_sources.RpcSourceAdminClientMixin.list_sources",
connection_failed,
)
config_path = tmp_path / "wf.json" config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8") config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner() runner = CliRunner()
@@ -463,14 +461,7 @@ def test_wf_admin_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
capability_id="workflow.demo.v1", capability_id="workflow.demo.v1",
payload={"ok": True}, payload={"ok": True},
) )
original_client = httpx.AsyncClient _patch_rpc_client_to_server(monkeypatch, server)
monkeypatch.setattr(
"wf_transport_rpc_http.client.httpx.AsyncClient",
lambda *args, **kwargs: original_client(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
),
)
config_path = tmp_path / "wf.json" config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8") config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner() runner = CliRunner()
@@ -490,14 +481,7 @@ def test_wf_admin_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
def test_wf_remote_draft_artifact_deploy_lifecycle(monkeypatch, tmp_path) -> None: def test_wf_remote_draft_artifact_deploy_lifecycle(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store") server = build_local_static_workflow_server(tmp_path / "store")
original_client = httpx.AsyncClient _patch_rpc_client_to_server(monkeypatch, server)
monkeypatch.setattr(
"wf_transport_rpc_http.client.httpx.AsyncClient",
lambda *args, **kwargs: original_client(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
),
)
config_path = tmp_path / "wf.json" config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8") config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner() runner = CliRunner()
@@ -603,14 +587,7 @@ def test_wf_remote_run_resume_interrupted_deployment(monkeypatch, tmp_path) -> N
} }
) )
) )
original_client = httpx.AsyncClient _patch_rpc_client_to_server(monkeypatch, server)
monkeypatch.setattr(
"wf_transport_rpc_http.client.httpx.AsyncClient",
lambda *args, **kwargs: original_client(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
),
)
config_path = tmp_path / "wf.json" config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8") config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner() runner = CliRunner()
@@ -8,7 +8,7 @@ import httpx
from wf_api import WorkflowSourceRegistryApi from wf_api import WorkflowSourceRegistryApi
from wf_server import build_local_static_workflow_server from wf_server import build_local_static_workflow_server
from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app
from wf_transport_rpc_http.client_source_registry import RpcSourceRegistryClientMixin from wf_transport_rpc_http.client.source_registry import RpcSourceRegistryClientMixin
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
+13 -12
View File
@@ -1,19 +1,21 @@
from __future__ import annotations from __future__ import annotations
import importlib import importlib
from pathlib import Path import inspect
from wf_transport_rpc_http.client import RpcWorkflowApiClient
def test_rpc_transport_has_domain_method_modules() -> None: def test_rpc_transport_has_domain_method_modules() -> None:
for module_name in ( for module_name in (
"wf_transport_rpc_http.methods_admin", "wf_transport_rpc_http.methods.admin",
"wf_transport_rpc_http.methods_capabilities", "wf_transport_rpc_http.methods.capabilities",
"wf_transport_rpc_http.methods_drafts", "wf_transport_rpc_http.methods.drafts",
"wf_transport_rpc_http.methods_artifacts", "wf_transport_rpc_http.methods.artifacts",
"wf_transport_rpc_http.methods_deployments", "wf_transport_rpc_http.methods.deployments",
"wf_transport_rpc_http.methods_runs", "wf_transport_rpc_http.methods.runs",
"wf_transport_rpc_http.methods_sources", "wf_transport_rpc_http.methods.sources",
"wf_transport_rpc_http.methods_source_registry", "wf_transport_rpc_http.methods.source_registry",
): ):
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
@@ -21,7 +23,6 @@ def test_rpc_transport_has_domain_method_modules() -> None:
def test_rpc_transport_client_stays_thin() -> None: def test_rpc_transport_client_stays_thin() -> None:
client_path = Path("src/wf_transport_rpc_http/client.py") line_count = len(inspect.getsource(RpcWorkflowApiClient).splitlines())
line_count = len(client_path.read_text(encoding="utf-8").splitlines())
assert line_count < 140 assert line_count < 40