feat: type source registry results
This commit is contained in:
@@ -74,10 +74,10 @@
|
|||||||
- Typed-result slices now give `workflow.health`, all artifact, deployment,
|
- Typed-result slices now give `workflow.health`, all artifact, deployment,
|
||||||
and run operations, every persisted draft-workspace operation, and both the
|
and run operations, every persisted draft-workspace operation, and both the
|
||||||
capability and source-discovery surfaces named transport-neutral result
|
capability and source-discovery surfaces named transport-neutral result
|
||||||
schemas: 55 of 70 methods. The remaining 15 success results still collapse
|
schemas: 63 of 70 methods. The remaining seven success results still
|
||||||
to generic objects across the source-registry/admin operations. Continue
|
collapse to generic objects across connections, events, and auth admin
|
||||||
introducing operation result DTOs before adopting generated TypeScript
|
operations. Continue introducing operation result DTOs before adopting
|
||||||
contracts.
|
generated TypeScript contracts.
|
||||||
- The stock `@open-rpc/generator` TypeScript client is not suitable here. It
|
- The stock `@open-rpc/generator` TypeScript client is not suitable here. It
|
||||||
exhausted a 4 GB Node heap on the full contract and emitted invalid dotted
|
exhausted a 4 GB Node heap on the full contract and emitted invalid dotted
|
||||||
class members plus `any` results for a minimal `workflow.health` contract.
|
class members plus `any` results for a minimal `workflow.health` contract.
|
||||||
|
|||||||
@@ -79,6 +79,15 @@ from .runs import (
|
|||||||
TraceEntryPayload,
|
TraceEntryPayload,
|
||||||
WorkflowRefPayload,
|
WorkflowRefPayload,
|
||||||
)
|
)
|
||||||
|
from .source_registry import (
|
||||||
|
ApplyRegistryChangesResult,
|
||||||
|
InspectRegistryEntryResult,
|
||||||
|
ListRegistryEntriesResult,
|
||||||
|
RegistryEntryMutationResult,
|
||||||
|
RegistryEntryPayload,
|
||||||
|
RegistryEntrySummaryPayload,
|
||||||
|
RemoveRegistryEntryResult,
|
||||||
|
)
|
||||||
from .sources import (
|
from .sources import (
|
||||||
InspectSourceResult,
|
InspectSourceResult,
|
||||||
ListSourcesResult,
|
ListSourcesResult,
|
||||||
@@ -99,6 +108,7 @@ from .sources import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"ApplyRegistryChangesResult",
|
||||||
"ArtifactVersionPayload",
|
"ArtifactVersionPayload",
|
||||||
"ArtifactCatalogEntryPayload",
|
"ArtifactCatalogEntryPayload",
|
||||||
"ArtifactKindPayload",
|
"ArtifactKindPayload",
|
||||||
@@ -125,6 +135,7 @@ __all__ = [
|
|||||||
"InterruptRoutePayload",
|
"InterruptRoutePayload",
|
||||||
"InvalidDraftResult",
|
"InvalidDraftResult",
|
||||||
"InspectCapabilityResult",
|
"InspectCapabilityResult",
|
||||||
|
"InspectRegistryEntryResult",
|
||||||
"InspectSourceResult",
|
"InspectSourceResult",
|
||||||
"JsonObject",
|
"JsonObject",
|
||||||
"JsonProjector",
|
"JsonProjector",
|
||||||
@@ -134,6 +145,7 @@ __all__ = [
|
|||||||
"ListArtifactsResult",
|
"ListArtifactsResult",
|
||||||
"ListCapabilitiesResult",
|
"ListCapabilitiesResult",
|
||||||
"ListRunsResult",
|
"ListRunsResult",
|
||||||
|
"ListRegistryEntriesResult",
|
||||||
"ListSourcesResult",
|
"ListSourcesResult",
|
||||||
"NextActionPatchExamplePayload",
|
"NextActionPatchExamplePayload",
|
||||||
"NextActionsPayload",
|
"NextActionsPayload",
|
||||||
@@ -145,6 +157,9 @@ __all__ = [
|
|||||||
"PatchedDraftInvalidResult",
|
"PatchedDraftInvalidResult",
|
||||||
"PatchedDraftValidResult",
|
"PatchedDraftValidResult",
|
||||||
"RawWorkflowPlan",
|
"RawWorkflowPlan",
|
||||||
|
"RegistryEntryMutationResult",
|
||||||
|
"RegistryEntryPayload",
|
||||||
|
"RegistryEntrySummaryPayload",
|
||||||
"ReducerInventoryPayload",
|
"ReducerInventoryPayload",
|
||||||
"ResumeReadiness",
|
"ResumeReadiness",
|
||||||
"RunResult",
|
"RunResult",
|
||||||
@@ -152,6 +167,7 @@ __all__ = [
|
|||||||
"RunSummary",
|
"RunSummary",
|
||||||
"RunTraceResult",
|
"RunTraceResult",
|
||||||
"RequiredCapabilityPayload",
|
"RequiredCapabilityPayload",
|
||||||
|
"RemoveRegistryEntryResult",
|
||||||
"SaveArtifactResult",
|
"SaveArtifactResult",
|
||||||
"SavedDraftArtifactResult",
|
"SavedDraftArtifactResult",
|
||||||
"SaveDeploymentResult",
|
"SaveDeploymentResult",
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import NotRequired, TypedDict
|
||||||
|
|
||||||
|
from pydantic import ConfigDict, with_config
|
||||||
|
|
||||||
|
from .common import DependencyDiagnosticPayload, JsonObject, PageMetadataPayload
|
||||||
|
|
||||||
|
|
||||||
|
@with_config(ConfigDict(extra="allow"))
|
||||||
|
class RegistryEntryPayload(TypedDict):
|
||||||
|
"""Desired source entry with provider-specific configuration preserved."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
kind: str
|
||||||
|
enabled: bool
|
||||||
|
provider: NotRequired[str | None]
|
||||||
|
account: NotRequired[str | None]
|
||||||
|
profile: NotRequired[str | None]
|
||||||
|
transport: NotRequired[JsonObject | None]
|
||||||
|
auth_ref: NotRequired[str | None]
|
||||||
|
metadata: NotRequired[JsonObject]
|
||||||
|
|
||||||
|
|
||||||
|
class RegistryEntrySummaryPayload(TypedDict):
|
||||||
|
"""Compact desired-source row including config precedence facts."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
kind: str
|
||||||
|
enabled: bool
|
||||||
|
provider: str | None
|
||||||
|
account: str | None
|
||||||
|
profile: str | None
|
||||||
|
transport_kind: str | None
|
||||||
|
auth_ref: str | None
|
||||||
|
shadowed_by_config: bool
|
||||||
|
config_ownership: str | None
|
||||||
|
mutable: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ListRegistryEntriesResult(PageMetadataPayload):
|
||||||
|
"""Cursor-paged desired source registry entries."""
|
||||||
|
|
||||||
|
entries: list[RegistryEntrySummaryPayload]
|
||||||
|
|
||||||
|
|
||||||
|
class InspectRegistryEntryResult(TypedDict):
|
||||||
|
"""Full desired source entry with config precedence facts."""
|
||||||
|
|
||||||
|
entry: RegistryEntryPayload
|
||||||
|
shadowed_by_config: bool
|
||||||
|
config_ownership: str | None
|
||||||
|
mutable: bool
|
||||||
|
|
||||||
|
|
||||||
|
class RegistryEntryMutationResult(TypedDict):
|
||||||
|
"""Desired source entry returned after add, update, or enablement changes."""
|
||||||
|
|
||||||
|
entry: RegistryEntryPayload
|
||||||
|
shadowed_by_config: bool
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveRegistryEntryResult(TypedDict):
|
||||||
|
"""Outcome of removing one desired source registry entry."""
|
||||||
|
|
||||||
|
removed: bool
|
||||||
|
source_id: str
|
||||||
|
|
||||||
|
|
||||||
|
@with_config(ConfigDict(extra="allow"))
|
||||||
|
class ApplyRegistryChangesResult(TypedDict):
|
||||||
|
"""Summary of reconciling desired registry state into the live service."""
|
||||||
|
|
||||||
|
applied: bool
|
||||||
|
registered: list[str]
|
||||||
|
updated: list[str]
|
||||||
|
removed: list[str]
|
||||||
|
connection_count: int
|
||||||
|
registry_entry_count: int
|
||||||
|
auth_diagnostics: NotRequired[list[DependencyDiagnosticPayload]]
|
||||||
@@ -6,6 +6,21 @@ from typing import Any, Protocol, runtime_checkable
|
|||||||
|
|
||||||
from wf_platform import page_items
|
from wf_platform import page_items
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
ApplyRegistryChangesResult,
|
||||||
|
InspectRegistryEntryResult,
|
||||||
|
JsonProjector,
|
||||||
|
ListRegistryEntriesResult,
|
||||||
|
RegistryEntryMutationResult,
|
||||||
|
RemoveRegistryEntryResult,
|
||||||
|
)
|
||||||
|
|
||||||
|
_PROJECT_REGISTRY_LIST = JsonProjector(ListRegistryEntriesResult)
|
||||||
|
_PROJECT_REGISTRY_INSPECT = JsonProjector(InspectRegistryEntryResult)
|
||||||
|
_PROJECT_REGISTRY_MUTATION = JsonProjector(RegistryEntryMutationResult)
|
||||||
|
_PROJECT_REGISTRY_REMOVE = JsonProjector(RemoveRegistryEntryResult)
|
||||||
|
_PROJECT_REGISTRY_APPLY = JsonProjector(ApplyRegistryChangesResult)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowSourceRegistryProvider(Protocol):
|
class WorkflowSourceRegistryProvider(Protocol):
|
||||||
"""Provides desired source registry state for read-only admin frontends."""
|
"""Provides desired source registry state for read-only admin frontends."""
|
||||||
@@ -68,7 +83,7 @@ class WorkflowSourceRegistryApi:
|
|||||||
*,
|
*,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]:
|
) -> ListRegistryEntriesResult:
|
||||||
ownership = self._provider.config_source_ownership()
|
ownership = self._provider.config_source_ownership()
|
||||||
entries = sorted(
|
entries = sorted(
|
||||||
(
|
(
|
||||||
@@ -80,104 +95,109 @@ class WorkflowSourceRegistryApi:
|
|||||||
key=lambda item: str(item.get("id", "")),
|
key=lambda item: str(item.get("id", "")),
|
||||||
)
|
)
|
||||||
page = page_items(entries, cursor=cursor, limit=limit)
|
page = page_items(entries, cursor=cursor, limit=limit)
|
||||||
return {
|
return _PROJECT_REGISTRY_LIST(
|
||||||
"entries": list(page.items),
|
{
|
||||||
"next_cursor": page.next_cursor,
|
"entries": list(page.items),
|
||||||
"total": page.total,
|
"next_cursor": page.next_cursor,
|
||||||
}
|
"total": page.total,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def inspect_registry_entry(
|
async def inspect_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> InspectRegistryEntryResult:
|
||||||
ownership = self._provider.config_source_ownership()
|
ownership = self._provider.config_source_ownership()
|
||||||
for item in self._provider.list_registry_entries():
|
for item in self._provider.list_registry_entries():
|
||||||
entry = _payload(item)
|
entry = _payload(item)
|
||||||
if entry.get("id") == source_id:
|
if entry.get("id") == source_id:
|
||||||
return {
|
return _PROJECT_REGISTRY_INSPECT(
|
||||||
"entry": entry,
|
{
|
||||||
"shadowed_by_config": self._is_shadowed(source_id),
|
"entry": entry,
|
||||||
"config_ownership": ownership.get(source_id),
|
"shadowed_by_config": self._is_shadowed(source_id),
|
||||||
"mutable": ownership.get(source_id) != "locked",
|
"config_ownership": ownership.get(source_id),
|
||||||
}
|
"mutable": ownership.get(source_id) != "locked",
|
||||||
|
}
|
||||||
|
)
|
||||||
raise KeyError(f"unknown registry source {source_id!r}")
|
raise KeyError(f"unknown registry source {source_id!r}")
|
||||||
|
|
||||||
async def add_registry_entry(
|
async def add_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
entry: dict[str, Any],
|
entry: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
if self._mutation_provider is None:
|
if self._mutation_provider is None:
|
||||||
raise TypeError("add_registry_entry requires a mutation provider")
|
raise TypeError("add_registry_entry requires a mutation provider")
|
||||||
raw = self._mutation_provider.add_registry_entry(entry)
|
raw = self._mutation_provider.add_registry_entry(entry)
|
||||||
result = _payload(raw)
|
result = _payload(raw)
|
||||||
return {
|
return self._mutation_result(result)
|
||||||
"entry": result,
|
|
||||||
"shadowed_by_config": self._is_shadowed(result["id"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def update_registry_entry(
|
async def update_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
patch: dict[str, Any],
|
patch: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
if self._mutation_provider is None:
|
if self._mutation_provider is None:
|
||||||
raise TypeError("update_registry_entry requires a mutation provider")
|
raise TypeError("update_registry_entry requires a mutation provider")
|
||||||
raw = self._mutation_provider.update_registry_entry(source_id, patch)
|
raw = self._mutation_provider.update_registry_entry(source_id, patch)
|
||||||
result = _payload(raw)
|
result = _payload(raw)
|
||||||
return {
|
return self._mutation_result(result)
|
||||||
"entry": result,
|
|
||||||
"shadowed_by_config": self._is_shadowed(result["id"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def enable_registry_entry(
|
async def enable_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
if self._mutation_provider is None:
|
if self._mutation_provider is None:
|
||||||
raise TypeError("enable_registry_entry requires a mutation provider")
|
raise TypeError("enable_registry_entry requires a mutation provider")
|
||||||
raw = self._mutation_provider.set_registry_entry_enabled(source_id, True)
|
raw = self._mutation_provider.set_registry_entry_enabled(source_id, True)
|
||||||
result = _payload(raw)
|
result = _payload(raw)
|
||||||
return {
|
return self._mutation_result(result)
|
||||||
"entry": result,
|
|
||||||
"shadowed_by_config": self._is_shadowed(result["id"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def disable_registry_entry(
|
async def disable_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
if self._mutation_provider is None:
|
if self._mutation_provider is None:
|
||||||
raise TypeError("disable_registry_entry requires a mutation provider")
|
raise TypeError("disable_registry_entry requires a mutation provider")
|
||||||
raw = self._mutation_provider.set_registry_entry_enabled(source_id, False)
|
raw = self._mutation_provider.set_registry_entry_enabled(source_id, False)
|
||||||
result = _payload(raw)
|
result = _payload(raw)
|
||||||
return {
|
return self._mutation_result(result)
|
||||||
"entry": result,
|
|
||||||
"shadowed_by_config": self._is_shadowed(result["id"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def remove_registry_entry(
|
async def remove_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> RemoveRegistryEntryResult:
|
||||||
if self._mutation_provider is None:
|
if self._mutation_provider is None:
|
||||||
raise TypeError("remove_registry_entry requires a mutation provider")
|
raise TypeError("remove_registry_entry requires a mutation provider")
|
||||||
raw = self._mutation_provider.remove_registry_entry(source_id)
|
raw = self._mutation_provider.remove_registry_entry(source_id)
|
||||||
result = _payload(raw)
|
result = _payload(raw)
|
||||||
return {
|
return _PROJECT_REGISTRY_REMOVE(
|
||||||
"removed": bool(result.get("removed")),
|
{
|
||||||
"source_id": str(result.get("source_id", source_id)),
|
"removed": bool(result.get("removed")),
|
||||||
}
|
"source_id": str(result.get("source_id", source_id)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def apply_registry_changes(self) -> dict[str, Any]:
|
async def apply_registry_changes(self) -> ApplyRegistryChangesResult:
|
||||||
if self._apply_provider is None:
|
if self._apply_provider is None:
|
||||||
raise TypeError("apply_registry_changes requires an apply provider")
|
raise TypeError("apply_registry_changes requires an apply provider")
|
||||||
return _payload(self._apply_provider.apply_registry_changes())
|
return _PROJECT_REGISTRY_APPLY(
|
||||||
|
_payload(self._apply_provider.apply_registry_changes())
|
||||||
|
)
|
||||||
|
|
||||||
|
def _mutation_result(self, entry: dict[str, Any]) -> RegistryEntryMutationResult:
|
||||||
|
"""Attach config precedence to one provider-validated mutation entry."""
|
||||||
|
return _PROJECT_REGISTRY_MUTATION(
|
||||||
|
{
|
||||||
|
"entry": entry,
|
||||||
|
"shadowed_by_config": self._is_shadowed(entry["id"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _payload(value: Mapping[str, Any] | object) -> dict[str, Any]:
|
def _payload(value: Mapping[str, Any] | object) -> dict[str, Any]:
|
||||||
|
|||||||
+13
-8
@@ -10,6 +10,7 @@ from wf_core.models.steps import InputBinding, OutputBinding
|
|||||||
from .draft_authoring import RouteSource
|
from .draft_authoring import RouteSource
|
||||||
from .draft_updates import CapabilityStepUpdate
|
from .draft_updates import CapabilityStepUpdate
|
||||||
from .models import (
|
from .models import (
|
||||||
|
ApplyRegistryChangesResult,
|
||||||
CapabilityCallResult,
|
CapabilityCallResult,
|
||||||
CompileDraftWorkspaceResult,
|
CompileDraftWorkspaceResult,
|
||||||
CreateArtifactFromWorkspaceResult,
|
CreateArtifactFromWorkspaceResult,
|
||||||
@@ -19,14 +20,18 @@ from .models import (
|
|||||||
DeleteDraftWorkspaceResult,
|
DeleteDraftWorkspaceResult,
|
||||||
DraftWorkspaceResult,
|
DraftWorkspaceResult,
|
||||||
InspectCapabilityResult,
|
InspectCapabilityResult,
|
||||||
|
InspectRegistryEntryResult,
|
||||||
InspectSourceResult,
|
InspectSourceResult,
|
||||||
ListArtifactsResult,
|
ListArtifactsResult,
|
||||||
ListCapabilitiesResult,
|
ListCapabilitiesResult,
|
||||||
ListDeploymentsResult,
|
ListDeploymentsResult,
|
||||||
ListDraftWorkspacesResult,
|
ListDraftWorkspacesResult,
|
||||||
|
ListRegistryEntriesResult,
|
||||||
ListRunsResult,
|
ListRunsResult,
|
||||||
ListSourcesResult,
|
ListSourcesResult,
|
||||||
PatchDraftResult,
|
PatchDraftResult,
|
||||||
|
RegistryEntryMutationResult,
|
||||||
|
RemoveRegistryEntryResult,
|
||||||
RunResult,
|
RunResult,
|
||||||
RunTraceResult,
|
RunTraceResult,
|
||||||
SaveArtifactResult,
|
SaveArtifactResult,
|
||||||
@@ -562,46 +567,46 @@ class WorkflowSourceRegistrySurface(Protocol):
|
|||||||
*,
|
*,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]: ...
|
) -> ListRegistryEntriesResult: ...
|
||||||
|
|
||||||
async def inspect_registry_entry(
|
async def inspect_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]: ...
|
) -> InspectRegistryEntryResult: ...
|
||||||
|
|
||||||
async def add_registry_entry(
|
async def add_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
entry: dict[str, Any],
|
entry: dict[str, Any],
|
||||||
) -> dict[str, Any]: ...
|
) -> RegistryEntryMutationResult: ...
|
||||||
|
|
||||||
async def update_registry_entry(
|
async def update_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
patch: dict[str, Any],
|
patch: dict[str, Any],
|
||||||
) -> dict[str, Any]: ...
|
) -> RegistryEntryMutationResult: ...
|
||||||
|
|
||||||
async def enable_registry_entry(
|
async def enable_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]: ...
|
) -> RegistryEntryMutationResult: ...
|
||||||
|
|
||||||
async def disable_registry_entry(
|
async def disable_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]: ...
|
) -> RegistryEntryMutationResult: ...
|
||||||
|
|
||||||
async def remove_registry_entry(
|
async def remove_registry_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]: ...
|
) -> RemoveRegistryEntryResult: ...
|
||||||
|
|
||||||
async def apply_registry_changes(self) -> dict[str, Any]: ...
|
async def apply_registry_changes(self) -> ApplyRegistryChangesResult: ...
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from wf_api.models import (
|
||||||
|
ApplyRegistryChangesResult,
|
||||||
|
InspectRegistryEntryResult,
|
||||||
|
ListRegistryEntriesResult,
|
||||||
|
RegistryEntryMutationResult,
|
||||||
|
RemoveRegistryEntryResult,
|
||||||
|
)
|
||||||
|
|
||||||
from .base import RpcCaller
|
from .base import RpcCaller
|
||||||
|
|
||||||
@@ -13,30 +21,39 @@ class RpcSourceRegistryClientMixin:
|
|||||||
*,
|
*,
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, Any]:
|
) -> ListRegistryEntriesResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.admin.source_registry.list",
|
ListRegistryEntriesResult,
|
||||||
{"cursor": cursor, "limit": limit},
|
await self._call(
|
||||||
|
"workflow.admin.source_registry.list",
|
||||||
|
{"cursor": cursor, "limit": limit},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def inspect_registry_entry(
|
async def inspect_registry_entry(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> InspectRegistryEntryResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.admin.source_registry.inspect",
|
InspectRegistryEntryResult,
|
||||||
{"source_id": source_id},
|
await self._call(
|
||||||
|
"workflow.admin.source_registry.inspect",
|
||||||
|
{"source_id": source_id},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def add_registry_entry(
|
async def add_registry_entry(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
entry: dict[str, Any],
|
entry: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.admin.source_registry.add",
|
RegistryEntryMutationResult,
|
||||||
{"entry": entry},
|
await self._call(
|
||||||
|
"workflow.admin.source_registry.add",
|
||||||
|
{"entry": entry},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update_registry_entry(
|
async def update_registry_entry(
|
||||||
@@ -44,44 +61,59 @@ class RpcSourceRegistryClientMixin:
|
|||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
patch: dict[str, Any],
|
patch: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.admin.source_registry.update",
|
RegistryEntryMutationResult,
|
||||||
{"source_id": source_id, "patch": patch},
|
await self._call(
|
||||||
|
"workflow.admin.source_registry.update",
|
||||||
|
{"source_id": source_id, "patch": patch},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def enable_registry_entry(
|
async def enable_registry_entry(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.admin.source_registry.enable",
|
RegistryEntryMutationResult,
|
||||||
{"source_id": source_id},
|
await self._call(
|
||||||
|
"workflow.admin.source_registry.enable",
|
||||||
|
{"source_id": source_id},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def disable_registry_entry(
|
async def disable_registry_entry(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.admin.source_registry.disable",
|
RegistryEntryMutationResult,
|
||||||
{"source_id": source_id},
|
await self._call(
|
||||||
|
"workflow.admin.source_registry.disable",
|
||||||
|
{"source_id": source_id},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def remove_registry_entry(
|
async def remove_registry_entry(
|
||||||
self: RpcCaller,
|
self: RpcCaller,
|
||||||
*,
|
*,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> RemoveRegistryEntryResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.admin.source_registry.remove",
|
RemoveRegistryEntryResult,
|
||||||
{"source_id": source_id},
|
await self._call(
|
||||||
|
"workflow.admin.source_registry.remove",
|
||||||
|
{"source_id": source_id},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def apply_registry_changes(self: RpcCaller) -> dict[str, Any]:
|
async def apply_registry_changes(self: RpcCaller) -> ApplyRegistryChangesResult:
|
||||||
return await self._call(
|
return cast(
|
||||||
"workflow.admin.source_registry.apply",
|
ApplyRegistryChangesResult,
|
||||||
{},
|
await self._call(
|
||||||
|
"workflow.admin.source_registry.apply",
|
||||||
|
{},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
from __future__ import annotations
|
"""Source registry JSON-RPC method registration.
|
||||||
|
|
||||||
from typing import Any
|
Return annotations stay eagerly evaluated because fastapi-jsonrpc captures them
|
||||||
|
while registering nested handlers for response validation and OpenRPC output.
|
||||||
|
"""
|
||||||
|
|
||||||
import fastapi_jsonrpc as jsonrpc
|
import fastapi_jsonrpc as jsonrpc
|
||||||
|
|
||||||
from wf_api import WorkflowSourceRegistrySurface
|
from wf_api import WorkflowSourceRegistrySurface
|
||||||
|
from wf_api.models import (
|
||||||
|
ApplyRegistryChangesResult,
|
||||||
|
InspectRegistryEntryResult,
|
||||||
|
ListRegistryEntriesResult,
|
||||||
|
RegistryEntryMutationResult,
|
||||||
|
RemoveRegistryEntryResult,
|
||||||
|
)
|
||||||
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
|
||||||
@@ -51,7 +60,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_admin_source_registry_list(
|
async def workflow_admin_source_registry_list(
|
||||||
params: ListRegistryEntriesParams = RpcParams(),
|
params: ListRegistryEntriesParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> ListRegistryEntriesResult:
|
||||||
admin = _require_source_registry_admin(server, operation="reads")
|
admin = _require_source_registry_admin(server, operation="reads")
|
||||||
try:
|
try:
|
||||||
return await admin.list_registry_entries(
|
return await admin.list_registry_entries(
|
||||||
@@ -67,7 +76,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_admin_source_registry_inspect(
|
async def workflow_admin_source_registry_inspect(
|
||||||
params: InspectRegistryEntryParams = RpcParams(),
|
params: InspectRegistryEntryParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> InspectRegistryEntryResult:
|
||||||
admin = _require_source_registry_admin(server, operation="reads")
|
admin = _require_source_registry_admin(server, operation="reads")
|
||||||
try:
|
try:
|
||||||
return await admin.inspect_registry_entry(
|
return await admin.inspect_registry_entry(
|
||||||
@@ -82,7 +91,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_admin_source_registry_add(
|
async def workflow_admin_source_registry_add(
|
||||||
params: AddRegistryEntryParams = RpcParams(),
|
params: AddRegistryEntryParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
admin = _require_source_registry_admin(server, operation="mutations")
|
admin = _require_source_registry_admin(server, operation="mutations")
|
||||||
try:
|
try:
|
||||||
return await admin.add_registry_entry(
|
return await admin.add_registry_entry(
|
||||||
@@ -97,7 +106,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_admin_source_registry_update(
|
async def workflow_admin_source_registry_update(
|
||||||
params: UpdateRegistryEntryParams = RpcParams(),
|
params: UpdateRegistryEntryParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
admin = _require_source_registry_admin(server, operation="mutations")
|
admin = _require_source_registry_admin(server, operation="mutations")
|
||||||
try:
|
try:
|
||||||
return await admin.update_registry_entry(
|
return await admin.update_registry_entry(
|
||||||
@@ -113,7 +122,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_admin_source_registry_enable(
|
async def workflow_admin_source_registry_enable(
|
||||||
params: RegistryEntryIdParams = RpcParams(),
|
params: RegistryEntryIdParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
admin = _require_source_registry_admin(server, operation="mutations")
|
admin = _require_source_registry_admin(server, operation="mutations")
|
||||||
try:
|
try:
|
||||||
return await admin.enable_registry_entry(
|
return await admin.enable_registry_entry(
|
||||||
@@ -128,7 +137,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_admin_source_registry_disable(
|
async def workflow_admin_source_registry_disable(
|
||||||
params: RegistryEntryIdParams = RpcParams(),
|
params: RegistryEntryIdParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> RegistryEntryMutationResult:
|
||||||
admin = _require_source_registry_admin(server, operation="mutations")
|
admin = _require_source_registry_admin(server, operation="mutations")
|
||||||
try:
|
try:
|
||||||
return await admin.disable_registry_entry(
|
return await admin.disable_registry_entry(
|
||||||
@@ -143,7 +152,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_admin_source_registry_remove(
|
async def workflow_admin_source_registry_remove(
|
||||||
params: RegistryEntryIdParams = RpcParams(),
|
params: RegistryEntryIdParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> RemoveRegistryEntryResult:
|
||||||
admin = _require_source_registry_admin(server, operation="mutations")
|
admin = _require_source_registry_admin(server, operation="mutations")
|
||||||
try:
|
try:
|
||||||
return await admin.remove_registry_entry(
|
return await admin.remove_registry_entry(
|
||||||
@@ -158,7 +167,7 @@ def register_methods(
|
|||||||
)
|
)
|
||||||
async def workflow_admin_source_registry_apply(
|
async def workflow_admin_source_registry_apply(
|
||||||
params: ApplyRegistryChangesParams = RpcParams(),
|
params: ApplyRegistryChangesParams = RpcParams(),
|
||||||
) -> dict[str, Any]:
|
) -> ApplyRegistryChangesResult:
|
||||||
admin = _require_source_registry_admin(server, operation="apply")
|
admin = _require_source_registry_admin(server, operation="apply")
|
||||||
try:
|
try:
|
||||||
return await admin.apply_registry_changes()
|
return await admin.apply_registry_changes()
|
||||||
|
|||||||
@@ -169,6 +169,121 @@ def test_openrpc_exposes_typed_source_diagnosis_result(
|
|||||||
assert diagnosed["additionalProperties"] is True
|
assert diagnosed["additionalProperties"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrpc_exposes_typed_source_registry_read_results(
|
||||||
|
openrpc_document: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
_assert_result_component(
|
||||||
|
openrpc_document,
|
||||||
|
method_name="workflow.admin.source_registry.list",
|
||||||
|
component_name="ListRegistryEntriesResult",
|
||||||
|
properties={"entries", "next_cursor", "total"},
|
||||||
|
)
|
||||||
|
_assert_result_component(
|
||||||
|
openrpc_document,
|
||||||
|
method_name="workflow.admin.source_registry.inspect",
|
||||||
|
component_name="InspectRegistryEntryResult",
|
||||||
|
properties={"entry", "shadowed_by_config", "config_ownership", "mutable"},
|
||||||
|
)
|
||||||
|
schemas = openrpc_document["components"]["schemas"]
|
||||||
|
assert schemas["ListRegistryEntriesResult"]["properties"]["entries"]["items"] == {
|
||||||
|
"$ref": "#/components/schemas/RegistryEntrySummaryPayload"
|
||||||
|
}
|
||||||
|
assert schemas["InspectRegistryEntryResult"]["properties"]["entry"] == {
|
||||||
|
"$ref": "#/components/schemas/RegistryEntryPayload"
|
||||||
|
}
|
||||||
|
assert schemas["RegistryEntryPayload"]["additionalProperties"] is True
|
||||||
|
assert set(schemas["ListRegistryEntriesResult"]["required"]) == {
|
||||||
|
"entries",
|
||||||
|
"next_cursor",
|
||||||
|
"total",
|
||||||
|
}
|
||||||
|
assert set(schemas["RegistryEntrySummaryPayload"]["required"]) == {
|
||||||
|
"id",
|
||||||
|
"kind",
|
||||||
|
"enabled",
|
||||||
|
"provider",
|
||||||
|
"account",
|
||||||
|
"profile",
|
||||||
|
"transport_kind",
|
||||||
|
"auth_ref",
|
||||||
|
"shadowed_by_config",
|
||||||
|
"config_ownership",
|
||||||
|
"mutable",
|
||||||
|
}
|
||||||
|
assert set(schemas["InspectRegistryEntryResult"]["required"]) == {
|
||||||
|
"entry",
|
||||||
|
"shadowed_by_config",
|
||||||
|
"config_ownership",
|
||||||
|
"mutable",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"method_name",
|
||||||
|
[
|
||||||
|
"workflow.admin.source_registry.add",
|
||||||
|
"workflow.admin.source_registry.update",
|
||||||
|
"workflow.admin.source_registry.enable",
|
||||||
|
"workflow.admin.source_registry.disable",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_openrpc_exposes_typed_source_registry_mutation_result(
|
||||||
|
openrpc_document: dict[str, Any],
|
||||||
|
method_name: str,
|
||||||
|
) -> None:
|
||||||
|
_assert_result_component(
|
||||||
|
openrpc_document,
|
||||||
|
method_name=method_name,
|
||||||
|
component_name="RegistryEntryMutationResult",
|
||||||
|
properties={"entry", "shadowed_by_config"},
|
||||||
|
)
|
||||||
|
mutation = openrpc_document["components"]["schemas"]["RegistryEntryMutationResult"]
|
||||||
|
assert set(mutation["required"]) == {"entry", "shadowed_by_config"}
|
||||||
|
assert mutation["properties"]["entry"] == {
|
||||||
|
"$ref": "#/components/schemas/RegistryEntryPayload"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrpc_exposes_typed_source_registry_remove_and_apply_results(
|
||||||
|
openrpc_document: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
_assert_result_component(
|
||||||
|
openrpc_document,
|
||||||
|
method_name="workflow.admin.source_registry.remove",
|
||||||
|
component_name="RemoveRegistryEntryResult",
|
||||||
|
properties={"removed", "source_id"},
|
||||||
|
)
|
||||||
|
_assert_result_component(
|
||||||
|
openrpc_document,
|
||||||
|
method_name="workflow.admin.source_registry.apply",
|
||||||
|
component_name="ApplyRegistryChangesResult",
|
||||||
|
properties={
|
||||||
|
"applied",
|
||||||
|
"registered",
|
||||||
|
"updated",
|
||||||
|
"removed",
|
||||||
|
"connection_count",
|
||||||
|
"registry_entry_count",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
schemas = openrpc_document["components"]["schemas"]
|
||||||
|
assert set(schemas["RemoveRegistryEntryResult"]["required"]) == {
|
||||||
|
"removed",
|
||||||
|
"source_id",
|
||||||
|
}
|
||||||
|
assert set(schemas["ApplyRegistryChangesResult"]["required"]) == {
|
||||||
|
"applied",
|
||||||
|
"registered",
|
||||||
|
"updated",
|
||||||
|
"removed",
|
||||||
|
"connection_count",
|
||||||
|
"registry_entry_count",
|
||||||
|
}
|
||||||
|
assert schemas["ApplyRegistryChangesResult"]["properties"]["auth_diagnostics"][
|
||||||
|
"items"
|
||||||
|
] == {"$ref": "#/components/schemas/DependencyDiagnosticPayload"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("method_name", "component_name", "properties"),
|
("method_name", "component_name", "properties"),
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class FakeRegistryEntry:
|
|||||||
transport: dict[str, str] | None = None
|
transport: dict[str, str] | None = None
|
||||||
auth_ref: str | None = "github.work"
|
auth_ref: str | None = "github.work"
|
||||||
metadata: dict[str, object] | None = None
|
metadata: dict[str, object] | None = None
|
||||||
|
provider_options: dict[str, object] | None = None
|
||||||
|
|
||||||
|
|
||||||
class FakeRegistryProvider:
|
class FakeRegistryProvider:
|
||||||
@@ -31,6 +32,7 @@ class FakeRegistryProvider:
|
|||||||
id="github.work",
|
id="github.work",
|
||||||
transport={"kind": "stdio", "command": "npx"},
|
transport={"kind": "stdio", "command": "npx"},
|
||||||
metadata={},
|
metadata={},
|
||||||
|
provider_options={"region": "work"},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -152,6 +154,8 @@ async def test_rpc_source_registry_methods_return_registry_payloads(tmp_path) ->
|
|||||||
assert listed["result"]["entries"][0]["id"] == "github.work"
|
assert listed["result"]["entries"][0]["id"] == "github.work"
|
||||||
assert listed["result"]["entries"][0]["shadowed_by_config"] is True
|
assert listed["result"]["entries"][0]["shadowed_by_config"] is True
|
||||||
assert inspected["result"]["entry"]["transport"]["kind"] == "stdio"
|
assert inspected["result"]["entry"]["transport"]["kind"] == "stdio"
|
||||||
|
assert inspected["result"]["entry"]["transport"]["command"] == "npx"
|
||||||
|
assert inspected["result"]["entry"]["provider_options"] == {"region": "work"}
|
||||||
assert inspected["result"]["shadowed_by_config"] is True
|
assert inspected["result"]["shadowed_by_config"] is True
|
||||||
|
|
||||||
|
|
||||||
@@ -246,12 +250,22 @@ async def test_rpc_source_registry_add_returns_entry(tmp_path) -> None:
|
|||||||
payload = await _rpc(
|
payload = await _rpc(
|
||||||
client,
|
client,
|
||||||
"workflow.admin.source_registry.add",
|
"workflow.admin.source_registry.add",
|
||||||
{"entry": {"id": "new.mcp", "kind": "mcp", "enabled": True}},
|
{
|
||||||
|
"entry": {
|
||||||
|
"id": "new.mcp",
|
||||||
|
"kind": "mcp",
|
||||||
|
"enabled": True,
|
||||||
|
"transport": {"kind": "stdio", "command": "custom-runner"},
|
||||||
|
"provider_options": {"region": "local"},
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "result" in payload
|
assert "result" in payload
|
||||||
assert payload["result"]["entry"]["id"] == "new.mcp"
|
assert payload["result"]["entry"]["id"] == "new.mcp"
|
||||||
assert payload["result"]["entry"]["kind"] == "mcp"
|
assert payload["result"]["entry"]["kind"] == "mcp"
|
||||||
|
assert payload["result"]["entry"]["transport"]["command"] == "custom-runner"
|
||||||
|
assert payload["result"]["entry"]["provider_options"] == {"region": "local"}
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_source_registry_update_returns_entry(tmp_path) -> None:
|
async def test_rpc_source_registry_update_returns_entry(tmp_path) -> None:
|
||||||
@@ -476,6 +490,16 @@ async def test_rpc_source_registry_apply_returns_summary(tmp_path) -> None:
|
|||||||
"removed": [],
|
"removed": [],
|
||||||
"connection_count": 1,
|
"connection_count": 1,
|
||||||
"registry_entry_count": 1,
|
"registry_entry_count": 1,
|
||||||
|
"auth_diagnostics": [
|
||||||
|
{
|
||||||
|
"severity": "error",
|
||||||
|
"code": "auth_not_found",
|
||||||
|
"logical_ref": "",
|
||||||
|
"bound_source": "demo.new",
|
||||||
|
"message": "Missing auth record.",
|
||||||
|
"repair_hint": "Save the referenced auth record.",
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
server = replace(
|
server = replace(
|
||||||
build_local_static_workflow_server(tmp_path / "store"),
|
build_local_static_workflow_server(tmp_path / "store"),
|
||||||
@@ -494,6 +518,7 @@ async def test_rpc_source_registry_apply_returns_summary(tmp_path) -> None:
|
|||||||
|
|
||||||
assert payload["result"]["applied"] is True
|
assert payload["result"]["applied"] is True
|
||||||
assert payload["result"]["registered"] == ["demo.new"]
|
assert payload["result"]["registered"] == ["demo.new"]
|
||||||
|
assert payload["result"]["auth_diagnostics"][0]["code"] == "auth_not_found"
|
||||||
admin.apply_registry_changes.assert_awaited_once()
|
admin.apply_registry_changes.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user