schemarouter 0.2.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ from ._version import __version__
2
+ from .adapters.base import AdapterContext, AdapterLoadResult, AdapterRegistry, SourceAdapter
3
+ from .adapters.optimade import OPTIMADESourceAdapter
4
+ from .adapters.python import schema_tool, tool_from_callable
5
+ from .analyzers import ModelCallable, ModelQueryAnalyzer
6
+ from .errors import (
7
+ BindingDriftError,
8
+ ExecutionError,
9
+ ModelAnalysisError,
10
+ PlanningError,
11
+ PlanValidationError,
12
+ PolicyViolationError,
13
+ ProposalApprovalError,
14
+ RegistrationError,
15
+ SchemaDriftError,
16
+ SchemaRouterError,
17
+ SchemaSourceError,
18
+ SchemaValidationError,
19
+ UnsupportedSchemaSourceError,
20
+ )
21
+ from .executor import RegistryExecutor
22
+ from .models import (
23
+ EndpointSpec,
24
+ EvidenceRequirements,
25
+ ExecutionPlan,
26
+ FieldSpec,
27
+ ParameterSpec,
28
+ PlanRequest,
29
+ QueryIntent,
30
+ ToolCall,
31
+ ToolResult,
32
+ ToolSpec,
33
+ )
34
+ from .planner import KeywordAnalyzer, QueryAnalyzer, SchemaPlanner
35
+ from .policy import ExecutionPolicy
36
+ from .proposals import SchemaProposal
37
+ from .registry import InMemoryRegistry, ToolRegistry
38
+ from .runs import RetryPolicy, RunConfig, RunEvent
39
+ from .runtime import ConfiguredSchemaRouter, SchemaRouter
40
+
41
+ __all__ = [
42
+ "__version__",
43
+ "AdapterContext",
44
+ "AdapterLoadResult",
45
+ "AdapterRegistry",
46
+ "BindingDriftError",
47
+ "ConfiguredSchemaRouter",
48
+ "EndpointSpec",
49
+ "EvidenceRequirements",
50
+ "ExecutionError",
51
+ "ExecutionPlan",
52
+ "ExecutionPolicy",
53
+ "FieldSpec",
54
+ "InMemoryRegistry",
55
+ "KeywordAnalyzer",
56
+ "ModelAnalysisError",
57
+ "ModelCallable",
58
+ "ModelQueryAnalyzer",
59
+ "ParameterSpec",
60
+ "PlanRequest",
61
+ "PlanValidationError",
62
+ "PlanningError",
63
+ "PolicyViolationError",
64
+ "ProposalApprovalError",
65
+ "QueryAnalyzer",
66
+ "QueryIntent",
67
+ "RegistrationError",
68
+ "RegistryExecutor",
69
+ "RetryPolicy",
70
+ "RunConfig",
71
+ "RunEvent",
72
+ "SchemaDriftError",
73
+ "SchemaPlanner",
74
+ "SchemaProposal",
75
+ "SchemaRouter",
76
+ "SchemaRouterError",
77
+ "SchemaSourceError",
78
+ "SchemaValidationError",
79
+ "SourceAdapter",
80
+ "OPTIMADESourceAdapter",
81
+ "ToolCall",
82
+ "ToolRegistry",
83
+ "ToolResult",
84
+ "ToolSpec",
85
+ "UnsupportedSchemaSourceError",
86
+ "schema_tool",
87
+ "tool_from_callable",
88
+ ]
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("schemarouter")
7
+ except PackageNotFoundError:
8
+ __version__ = "0+unknown"
@@ -0,0 +1,29 @@
1
+ from .base import AdapterContext, AdapterLoadResult, AdapterRegistry, SourceAdapter
2
+ from .mcp import MCPRemoteInvoker, inspect_mcp_url, tool_from_mcp
3
+ from .openapi import OpenAPIRemoteInvoker, resolve_openapi_base_url, tool_from_openapi
4
+ from .optimade import OPTIMADERemoteInvoker, OPTIMADESourceAdapter
5
+ from .python import (
6
+ PythonCallableInvoker,
7
+ callable_options,
8
+ schema_tool,
9
+ tool_from_callable,
10
+ )
11
+
12
+ __all__ = [
13
+ "AdapterContext",
14
+ "AdapterLoadResult",
15
+ "AdapterRegistry",
16
+ "MCPRemoteInvoker",
17
+ "OPTIMADERemoteInvoker",
18
+ "OPTIMADESourceAdapter",
19
+ "OpenAPIRemoteInvoker",
20
+ "PythonCallableInvoker",
21
+ "SourceAdapter",
22
+ "callable_options",
23
+ "inspect_mcp_url",
24
+ "resolve_openapi_base_url",
25
+ "schema_tool",
26
+ "tool_from_callable",
27
+ "tool_from_mcp",
28
+ "tool_from_openapi",
29
+ ]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Protocol
5
+
6
+ import httpx
7
+
8
+ from ..models import ToolSpec
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class AdapterContext:
13
+ url: str
14
+ name: str | None = None
15
+ namespace: str | None = None
16
+ base_url: str | None = None
17
+ schema_headers: dict[str, str] | None = None
18
+ trusted_headers: dict[str, str] | None = None
19
+ timeout: float = 20.0
20
+ http_client: httpx.AsyncClient | None = None
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AdapterLoadResult:
25
+ tool: ToolSpec
26
+ invoker: Any | None = None
27
+
28
+
29
+ class SourceAdapter(Protocol):
30
+ kind: str
31
+ priority: int
32
+
33
+ async def load(self, context: AdapterContext) -> AdapterLoadResult | None: ...
34
+
35
+
36
+ class AdapterRegistry:
37
+ """Ordered registry for structured capability-source adapters."""
38
+
39
+ def __init__(self, adapters: list[SourceAdapter] | None = None) -> None:
40
+ self._adapters: dict[str, SourceAdapter] = {}
41
+ for adapter in adapters or []:
42
+ self.register(adapter)
43
+
44
+ def register(self, adapter: SourceAdapter, *, replace: bool = False) -> None:
45
+ kind = str(adapter.kind).strip().lower()
46
+ if not kind or kind == "auto":
47
+ raise ValueError("adapter kind must be a non-empty name other than 'auto'")
48
+ if kind in self._adapters and not replace:
49
+ raise ValueError(f"adapter kind already registered: {kind!r}")
50
+ self._adapters[kind] = adapter
51
+
52
+ def unregister(self, kind: str) -> None:
53
+ self._adapters.pop(kind.strip().lower(), None)
54
+
55
+ def get(self, kind: str) -> SourceAdapter:
56
+ normalized = kind.strip().lower()
57
+ try:
58
+ return self._adapters[normalized]
59
+ except KeyError as exc:
60
+ raise KeyError(f"unknown adapter kind: {normalized!r}") from exc
61
+
62
+ def kinds(self) -> tuple[str, ...]:
63
+ return tuple(sorted(self._adapters))
64
+
65
+ def ordered(self) -> tuple[SourceAdapter, ...]:
66
+ return tuple(
67
+ sorted(
68
+ self._adapters.values(),
69
+ key=lambda adapter: (-int(adapter.priority), str(adapter.kind)),
70
+ )
71
+ )
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..errors import SchemaSourceError
6
+ from ..models import EndpointSpec, FieldSpec, ParameterSpec, ToolSpec
7
+
8
+
9
+ def _properties(schema: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
10
+ if not isinstance(schema, dict):
11
+ return {}
12
+ props = schema.get("properties", {})
13
+ return props if isinstance(props, dict) else {}
14
+
15
+
16
+ def _as_dict(value: Any) -> dict[str, Any]:
17
+ if isinstance(value, dict):
18
+ return value
19
+ if hasattr(value, "model_dump"):
20
+ return value.model_dump(mode="json", by_alias=True, exclude_none=True)
21
+ raise TypeError(f"cannot convert MCP value {type(value)!r} to dict")
22
+
23
+
24
+ def tool_from_mcp(
25
+ server_name: str,
26
+ tools_list_result: dict[str, Any] | list[dict[str, Any]],
27
+ *,
28
+ namespace: str | None = None,
29
+ ) -> ToolSpec:
30
+ """Convert an MCP tools/list result into a single server-scoped ToolSpec.
31
+
32
+ Remote annotations are preserved as untrusted metadata only; they do not grant permissions.
33
+ """
34
+ raw_tools = (
35
+ tools_list_result.get("tools", [])
36
+ if isinstance(tools_list_result, dict)
37
+ else tools_list_result
38
+ )
39
+ endpoints: list[EndpointSpec] = []
40
+ for raw_item in raw_tools:
41
+ item = _as_dict(raw_item)
42
+ input_schema = item.get("inputSchema") or item.get("input_schema") or {}
43
+ required = (
44
+ set(input_schema.get("required", []))
45
+ if isinstance(input_schema, dict)
46
+ else set()
47
+ )
48
+ parameters = [
49
+ ParameterSpec(
50
+ name=name,
51
+ description=spec.get("description", "") if isinstance(spec, dict) else "",
52
+ required=name in required,
53
+ location="argument",
54
+ json_schema=spec if isinstance(spec, dict) else {},
55
+ )
56
+ for name, spec in _properties(input_schema).items()
57
+ ]
58
+
59
+ output_schema = item.get("outputSchema") or item.get("output_schema") or {}
60
+ output_required = (
61
+ set(output_schema.get("required", []))
62
+ if isinstance(output_schema, dict)
63
+ else set()
64
+ )
65
+ fields = [
66
+ FieldSpec(
67
+ name=name,
68
+ description=spec.get("description", "") if isinstance(spec, dict) else "",
69
+ json_schema=spec if isinstance(spec, dict) else {},
70
+ identifier=name in {"id", "uuid", "key"} or name.endswith("_id"),
71
+ aliases=[name.replace("_", " ")],
72
+ )
73
+ for name, spec in _properties(output_schema).items()
74
+ ]
75
+ metadata = {
76
+ "title": item.get("title"),
77
+ "annotations": item.get("annotations"),
78
+ "output_required": sorted(output_required),
79
+ "remote_name": item.get("name"),
80
+ }
81
+ endpoints.append(
82
+ EndpointSpec(
83
+ name=item["name"],
84
+ description=item.get("description", ""),
85
+ parameters=parameters,
86
+ output_fields=fields,
87
+ input_schema=input_schema if isinstance(input_schema, dict) else {},
88
+ output_schema=output_schema if isinstance(output_schema, dict) else {},
89
+ metadata=metadata,
90
+ )
91
+ )
92
+
93
+ return ToolSpec(
94
+ name=server_name,
95
+ namespace=namespace,
96
+ description=f"MCP server: {server_name}",
97
+ endpoints=endpoints,
98
+ metadata={"adapter": "mcp", "remote_metadata_untrusted": True},
99
+ )
100
+
101
+
102
+ async def inspect_mcp_url(
103
+ url: str,
104
+ *,
105
+ server_name: str | None = None,
106
+ namespace: str | None = None,
107
+ ) -> ToolSpec:
108
+ """Connect to a Streamable HTTP MCP URL and import all advertised tools."""
109
+ try:
110
+ from mcp import Client
111
+ except ImportError as exc:
112
+ raise SchemaSourceError(
113
+ 'MCP support requires the optional dependency: pip install "schemarouter[mcp]"'
114
+ ) from exc
115
+
116
+ raw_tools: list[dict[str, Any]] = []
117
+ try:
118
+ async with Client(url) as client:
119
+ cursor: str | None = None
120
+ while True:
121
+ page = await client.list_tools(cursor=cursor)
122
+ raw_tools.extend(_as_dict(tool) for tool in page.tools)
123
+ cursor = page.next_cursor
124
+ if cursor is None:
125
+ break
126
+
127
+ info = getattr(client, "server_info", None)
128
+ discovered_name = getattr(info, "name", None)
129
+ protocol_version = getattr(client, "protocol_version", None)
130
+ except Exception as exc: # noqa: BLE001
131
+ raise SchemaSourceError(f"failed to inspect MCP server at {url!r}") from exc
132
+
133
+ name = server_name or discovered_name or "mcp_server"
134
+ tool = tool_from_mcp(name, raw_tools, namespace=namespace)
135
+ tool.metadata.update(
136
+ {
137
+ "source_url": url,
138
+ "protocol_version": protocol_version,
139
+ }
140
+ )
141
+ return tool
142
+
143
+
144
+ class MCPRemoteInvoker:
145
+ """Trusted runtime adapter for a remote MCP server.
146
+
147
+ A fresh client lifecycle is used per invocation in v0.1. Connection pooling belongs in a
148
+ later transport layer so the core executor remains stateless and easy to reason about.
149
+ """
150
+
151
+ def __init__(self, url: str) -> None:
152
+ self.url = url
153
+
154
+ async def __call__(self, endpoint: str, arguments: dict[str, Any]) -> Any:
155
+ try:
156
+ from mcp import Client
157
+ except ImportError as exc:
158
+ raise SchemaSourceError(
159
+ 'MCP execution requires: pip install "schemarouter[mcp]"'
160
+ ) from exc
161
+
162
+ async with Client(self.url) as client:
163
+ result = await client.call_tool(endpoint, arguments)
164
+ if getattr(result, "is_error", False):
165
+ raise RuntimeError(f"MCP tool {endpoint!r} returned an error")
166
+
167
+ structured = getattr(result, "structured_content", None)
168
+ if structured is not None:
169
+ return structured
170
+
171
+ content = getattr(result, "content", None)
172
+ if content is None:
173
+ return None
174
+ return [
175
+ block.model_dump(mode="json", by_alias=True, exclude_none=True)
176
+ if hasattr(block, "model_dump")
177
+ else str(block)
178
+ for block in content
179
+ ]