feat: add neutral workflow config models
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
ClientConfig,
|
||||||
|
FilesystemStoreConfig,
|
||||||
|
LocalTargetConfig,
|
||||||
|
RpcHttpTargetConfig,
|
||||||
|
RpcHttpTransportConfig,
|
||||||
|
ServerConfig,
|
||||||
|
StdlibSourceConfig,
|
||||||
|
WorkflowConfigFile,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ClientConfig",
|
||||||
|
"FilesystemStoreConfig",
|
||||||
|
"LocalTargetConfig",
|
||||||
|
"RpcHttpTargetConfig",
|
||||||
|
"RpcHttpTransportConfig",
|
||||||
|
"ServerConfig",
|
||||||
|
"StdlibSourceConfig",
|
||||||
|
"WorkflowConfigFile",
|
||||||
|
]
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowConfigModel(BaseModel):
|
||||||
|
"""Base config model: reject typos so config mistakes fail fast."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
|
||||||
|
class LocalTargetConfig(WorkflowConfigModel):
|
||||||
|
kind: Literal["local"] = "local"
|
||||||
|
|
||||||
|
|
||||||
|
class RpcHttpTargetConfig(WorkflowConfigModel):
|
||||||
|
kind: Literal["rpc_http"]
|
||||||
|
url: str
|
||||||
|
timeout_seconds: float = Field(default=30.0, gt=0)
|
||||||
|
|
||||||
|
|
||||||
|
TargetConfig = Annotated[
|
||||||
|
LocalTargetConfig | RpcHttpTargetConfig,
|
||||||
|
Field(discriminator="kind"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClientConfig(WorkflowConfigModel):
|
||||||
|
target: TargetConfig = Field(default_factory=LocalTargetConfig)
|
||||||
|
|
||||||
|
|
||||||
|
class FilesystemStoreConfig(WorkflowConfigModel):
|
||||||
|
kind: Literal["filesystem"] = "filesystem"
|
||||||
|
root: Path = Path(".wf_store")
|
||||||
|
|
||||||
|
|
||||||
|
StoreConfig = Annotated[
|
||||||
|
FilesystemStoreConfig,
|
||||||
|
Field(discriminator="kind"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RpcHttpTransportConfig(WorkflowConfigModel):
|
||||||
|
kind: Literal["rpc_http"]
|
||||||
|
host: str = "127.0.0.1"
|
||||||
|
port: int = Field(default=8765, ge=1, le=65535)
|
||||||
|
path: str = "/rpc"
|
||||||
|
|
||||||
|
@field_validator("path")
|
||||||
|
@classmethod
|
||||||
|
def validate_path(cls, value: str) -> str:
|
||||||
|
if not value.startswith("/"):
|
||||||
|
raise ValueError("transport path must start with '/'")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
ServerTransportConfig = Annotated[
|
||||||
|
RpcHttpTransportConfig,
|
||||||
|
Field(discriminator="kind"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StdlibSourceConfig(WorkflowConfigModel):
|
||||||
|
kind: Literal["stdlib"]
|
||||||
|
id: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
SourceConfig = Annotated[
|
||||||
|
StdlibSourceConfig,
|
||||||
|
Field(discriminator="kind"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ServerConfig(WorkflowConfigModel):
|
||||||
|
store: StoreConfig = Field(default_factory=FilesystemStoreConfig)
|
||||||
|
transports: list[ServerTransportConfig] = Field(default_factory=list)
|
||||||
|
sources: list[SourceConfig] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_unique_source_ids(self) -> ServerConfig:
|
||||||
|
seen: set[str] = set()
|
||||||
|
for source in self.sources:
|
||||||
|
if source.id in seen:
|
||||||
|
raise ValueError(f"duplicate source id {source.id!r}")
|
||||||
|
seen.add(source.id)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowConfigFile(WorkflowConfigModel):
|
||||||
|
version: Literal[1] = 1
|
||||||
|
client: ClientConfig = Field(default_factory=ClientConfig)
|
||||||
|
server: ServerConfig = Field(default_factory=ServerConfig)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from wf_config import (
|
||||||
|
FilesystemStoreConfig,
|
||||||
|
LocalTargetConfig,
|
||||||
|
RpcHttpTargetConfig,
|
||||||
|
RpcHttpTransportConfig,
|
||||||
|
StdlibSourceConfig,
|
||||||
|
WorkflowConfigFile,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_config_parses_local_target_and_filesystem_store() -> None:
|
||||||
|
config = WorkflowConfigFile.model_validate(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"client": {"target": {"kind": "local"}},
|
||||||
|
"server": {
|
||||||
|
"store": {"kind": "filesystem", "root": ".wf_store"},
|
||||||
|
"sources": [{"kind": "stdlib", "id": "wf.std"}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(config.client.target, LocalTargetConfig)
|
||||||
|
assert isinstance(config.server.store, FilesystemStoreConfig)
|
||||||
|
assert config.server.store.root.as_posix() == ".wf_store"
|
||||||
|
assert isinstance(config.server.sources[0], StdlibSourceConfig)
|
||||||
|
assert config.server.sources[0].id == "wf.std"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_config_parses_rpc_http_target_and_transport() -> None:
|
||||||
|
config = WorkflowConfigFile.model_validate(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"client": {
|
||||||
|
"target": {
|
||||||
|
"kind": "rpc_http",
|
||||||
|
"url": "http://127.0.0.1:8765/rpc",
|
||||||
|
"timeout_seconds": 12,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"transports": [
|
||||||
|
{
|
||||||
|
"kind": "rpc_http",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 9999,
|
||||||
|
"path": "/rpc",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(config.client.target, RpcHttpTargetConfig)
|
||||||
|
assert config.client.target.url == "http://127.0.0.1:8765/rpc"
|
||||||
|
assert config.client.target.timeout_seconds == 12
|
||||||
|
assert isinstance(config.server.transports[0], RpcHttpTransportConfig)
|
||||||
|
assert config.server.transports[0].host == "0.0.0.0"
|
||||||
|
assert config.server.transports[0].port == 9999
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_config_rejects_duplicate_source_ids() -> None:
|
||||||
|
with pytest.raises(ValidationError, match="duplicate source id"):
|
||||||
|
WorkflowConfigFile.model_validate(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"server": {
|
||||||
|
"sources": [
|
||||||
|
{"kind": "stdlib", "id": "wf.std"},
|
||||||
|
{"kind": "stdlib", "id": "wf.std"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_config_rejects_unknown_target_kind() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
WorkflowConfigFile.model_validate(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"client": {"target": {"kind": "mcp"}},
|
||||||
|
}
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user