This is an automated email from the ASF dual-hosted git repository.
yufei pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris-tools.git
The following commit(s) were added to refs/heads/main by this push:
new 34fa4e7 MCP: Add unit tests for all tools and refactors (#51)
34fa4e7 is described below
commit 34fa4e7d325f31079b92a055bcface81397e443b
Author: Yufei Gu <[email protected]>
AuthorDate: Sun Nov 23 10:15:55 2025 -0800
MCP: Add unit tests for all tools and refactors (#51)
---
mcp-server/polaris_mcp/rest.py | 4 +-
mcp-server/polaris_mcp/server.py | 47 ++++++---
mcp-server/polaris_mcp/tools/catalog.py | 21 +---
mcp-server/polaris_mcp/tools/catalog_role.py | 20 +---
mcp-server/polaris_mcp/tools/namespace.py | 21 +---
mcp-server/polaris_mcp/tools/policy.py | 21 +---
mcp-server/polaris_mcp/tools/principal.py | 21 +---
mcp-server/polaris_mcp/tools/principal_role.py | 21 +---
mcp-server/polaris_mcp/tools/table.py | 21 +---
mcp-server/tests/test_authorization.py | 140 +++++++++++++++++++++++++
mcp-server/tests/test_catalog_role_tool.py | 73 +++++++++++++
mcp-server/tests/test_catalog_tool.py | 76 ++++++++++++++
mcp-server/tests/test_namespace_tool.py | 46 ++++----
mcp-server/tests/test_policy_tool.py | 59 +++++++++++
mcp-server/tests/test_principal_role_tool.py | 62 +++++++++++
mcp-server/tests/test_principal_tool.py | 64 +++++++++++
mcp-server/tests/test_server.py | 19 ++++
mcp-server/tests/test_table_tool.py | 77 +++++++-------
mcp-server/uv.lock | 82 ++++++++++++++-
19 files changed, 702 insertions(+), 193 deletions(-)
diff --git a/mcp-server/polaris_mcp/rest.py b/mcp-server/polaris_mcp/rest.py
index e762840..530b8c6 100644
--- a/mcp-server/polaris_mcp/rest.py
+++ b/mcp-server/polaris_mcp/rest.py
@@ -27,8 +27,8 @@ from urllib.parse import urlencode, urljoin, urlsplit,
urlunsplit, quote
import urllib3
-from .authorization import AuthorizationProvider, none
-from .base import JSONDict, ToolExecutionResult
+from polaris_mcp.authorization import AuthorizationProvider, none
+from polaris_mcp.base import JSONDict, ToolExecutionResult
DEFAULT_TIMEOUT = urllib3.Timeout(connect=30.0, read=30.0)
diff --git a/mcp-server/polaris_mcp/server.py b/mcp-server/polaris_mcp/server.py
index 600806b..bdbacc1 100644
--- a/mcp-server/polaris_mcp/server.py
+++ b/mcp-server/polaris_mcp/server.py
@@ -31,14 +31,15 @@ from fastmcp.tools.tool import ToolResult as
FastMcpToolResult
from importlib import metadata
from mcp.types import TextContent
-from .authorization import (
+from polaris_mcp.authorization import (
AuthorizationProvider,
ClientCredentialsAuthorizationProvider,
StaticAuthorizationProvider,
none,
)
-from .base import ToolExecutionResult
-from .tools import (
+from polaris_mcp.base import ToolExecutionResult
+from polaris_mcp.rest import PolarisRestTool
+from polaris_mcp.tools import (
PolarisCatalogRoleTool,
PolarisCatalogTool,
PolarisNamespaceTool,
@@ -66,16 +67,38 @@ def create_server() -> FastMCP:
base_url = _resolve_base_url()
http = urllib3.PoolManager()
authorization_provider = _resolve_authorization_provider(base_url, http)
-
- table_tool = PolarisTableTool(base_url, http, authorization_provider)
- namespace_tool = PolarisNamespaceTool(base_url, http,
authorization_provider)
- principal_tool = PolarisPrincipalTool(base_url, http,
authorization_provider)
- principal_role_tool = PolarisPrincipalRoleTool(
- base_url, http, authorization_provider
+ catalog_rest = PolarisRestTool(
+ name="polaris.rest.catalog",
+ description="Shared REST delegate for catalog operations",
+ base_url=base_url,
+ default_path_prefix="api/catalog/v1/",
+ http=http,
+ authorization_provider=authorization_provider,
+ )
+ management_rest = PolarisRestTool(
+ name="polaris.rest.management",
+ description="Shared REST delegate for management operations",
+ base_url=base_url,
+ default_path_prefix="api/management/v1/",
+ http=http,
+ authorization_provider=authorization_provider,
)
- catalog_role_tool = PolarisCatalogRoleTool(base_url, http,
authorization_provider)
- policy_tool = PolarisPolicyTool(base_url, http, authorization_provider)
- catalog_tool = PolarisCatalogTool(base_url, http, authorization_provider)
+ policy_rest = PolarisRestTool(
+ name="polaris.rest.policy",
+ description="Shared REST delegate for policy operations",
+ base_url=base_url,
+ default_path_prefix="api/catalog/polaris/v1/",
+ http=http,
+ authorization_provider=authorization_provider,
+ )
+
+ table_tool = PolarisTableTool(rest_client=catalog_rest)
+ namespace_tool = PolarisNamespaceTool(rest_client=catalog_rest)
+ principal_tool = PolarisPrincipalTool(rest_client=management_rest)
+ principal_role_tool = PolarisPrincipalRoleTool(rest_client=management_rest)
+ catalog_role_tool = PolarisCatalogRoleTool(rest_client=management_rest)
+ policy_tool = PolarisPolicyTool(rest_client=policy_rest)
+ catalog_tool = PolarisCatalogTool(rest_client=management_rest)
server_version = _resolve_package_version()
mcp = FastMCP(
diff --git a/mcp-server/polaris_mcp/tools/catalog.py
b/mcp-server/polaris_mcp/tools/catalog.py
index 0f7280a..cdbd909 100644
--- a/mcp-server/polaris_mcp/tools/catalog.py
+++ b/mcp-server/polaris_mcp/tools/catalog.py
@@ -23,9 +23,6 @@ from __future__ import annotations
import copy
from typing import Any, Optional, Set
-import urllib3
-
-from polaris_mcp.authorization import AuthorizationProvider
from polaris_mcp.base import (
JSONDict,
McpTool,
@@ -50,20 +47,8 @@ class PolarisCatalogTool(McpTool):
UPDATE_ALIASES: Set[str] = {"update"}
DELETE_ALIASES: Set[str] = {"delete", "drop", "remove"}
- def __init__(
- self,
- base_url: str,
- http: urllib3.PoolManager,
- authorization_provider: AuthorizationProvider,
- ) -> None:
- self._delegate = PolarisRestTool(
- name="polaris.catalog.delegate",
- description="Internal delegate for catalog operations",
- base_url=base_url,
- default_path_prefix="api/management/v1/",
- http=http,
- authorization_provider=authorization_provider,
- )
+ def __init__(self, rest_client: PolarisRestTool) -> None:
+ self._rest_client = rest_client
@property
def name(self) -> str:
@@ -154,7 +139,7 @@ class PolarisCatalogTool(McpTool):
else: # pragma: no cover
raise ValueError(f"Unsupported operation: {operation}")
- raw = self._delegate.call(delegate_args)
+ raw = self._rest_client.call(delegate_args)
return self._maybe_augment_error(raw, normalized)
def _maybe_augment_error(
diff --git a/mcp-server/polaris_mcp/tools/catalog_role.py
b/mcp-server/polaris_mcp/tools/catalog_role.py
index 2ec9064..eeb0111 100644
--- a/mcp-server/polaris_mcp/tools/catalog_role.py
+++ b/mcp-server/polaris_mcp/tools/catalog_role.py
@@ -23,9 +23,7 @@ from __future__ import annotations
import copy
from typing import Any, Dict, Optional, Set
-import urllib3
-from polaris_mcp.authorization import AuthorizationProvider
from polaris_mcp.base import (
JSONDict,
McpTool,
@@ -55,20 +53,8 @@ class PolarisCatalogRoleTool(McpTool):
ADD_GRANT_ALIASES: Set[str] = {"add-grant", "grant"}
REVOKE_GRANT_ALIASES: Set[str] = {"revoke-grant"}
- def __init__(
- self,
- base_url: str,
- http: urllib3.PoolManager,
- authorization_provider: AuthorizationProvider,
- ) -> None:
- self._delegate = PolarisRestTool(
- name="polaris.catalogrole.delegate",
- description="Internal delegate for catalog role operations",
- base_url=base_url,
- default_path_prefix="api/management/v1/",
- http=http,
- authorization_provider=authorization_provider,
- )
+ def __init__(self, rest_client: PolarisRestTool) -> None:
+ self._rest_client = rest_client
@property
def name(self) -> str:
@@ -192,7 +178,7 @@ class PolarisCatalogRoleTool(McpTool):
else: # pragma: no cover
raise ValueError(f"Unsupported operation: {operation}")
- raw = self._delegate.call(delegate_args)
+ raw = self._rest_client.call(delegate_args)
return self._maybe_augment_error(raw, normalized)
def _catalog_role_path(self, base_path: str, arguments: Dict[str, Any]) ->
str:
diff --git a/mcp-server/polaris_mcp/tools/namespace.py
b/mcp-server/polaris_mcp/tools/namespace.py
index e1fbcf5..02f312d 100644
--- a/mcp-server/polaris_mcp/tools/namespace.py
+++ b/mcp-server/polaris_mcp/tools/namespace.py
@@ -25,9 +25,6 @@ import copy
import string
from typing import Any, Dict, List, Optional, Set
-import urllib3
-
-from polaris_mcp.authorization import AuthorizationProvider
from polaris_mcp.base import (
JSONDict,
McpTool,
@@ -57,20 +54,8 @@ class PolarisNamespaceTool(McpTool):
GET_PROPS_ALIASES: Set[str] = {"get-properties", "properties"}
DELETE_ALIASES: Set[str] = {"delete", "drop", "remove"}
- def __init__(
- self,
- base_url: str,
- http: urllib3.PoolManager,
- authorization_provider: AuthorizationProvider,
- ) -> None:
- self._delegate = PolarisRestTool(
- name="polaris.namespace.delegate",
- description="Internal delegate for namespace operations",
- base_url=base_url,
- default_path_prefix="api/catalog/v1/",
- http=http,
- authorization_provider=authorization_provider,
- )
+ def __init__(self, rest_client: PolarisRestTool) -> None:
+ self._rest_client = rest_client
@property
def name(self) -> str:
@@ -164,7 +149,7 @@ class PolarisNamespaceTool(McpTool):
else: # pragma: no cover - normalize guarantees cases
raise ValueError(f"Unsupported operation: {operation}")
- raw = self._delegate.call(delegate_args)
+ raw = self._rest_client.call(delegate_args)
return self._maybe_augment_error(raw, normalized)
def _handle_list(self, delegate_args: JSONDict, catalog: str) -> None:
diff --git a/mcp-server/polaris_mcp/tools/policy.py
b/mcp-server/polaris_mcp/tools/policy.py
index 4029e5a..5463aa8 100644
--- a/mcp-server/polaris_mcp/tools/policy.py
+++ b/mcp-server/polaris_mcp/tools/policy.py
@@ -24,9 +24,6 @@ from __future__ import annotations
import copy
from typing import Any, Dict, Optional, Set
-import urllib3
-
-from polaris_mcp.authorization import AuthorizationProvider
from polaris_mcp.base import (
JSONDict,
McpTool,
@@ -52,20 +49,8 @@ class PolarisPolicyTool(McpTool):
DETACH_ALIASES: Set[str] = {"detach", "unmap", "unattach"}
APPLICABLE_ALIASES: Set[str] = {"applicable", "applicable-policies"}
- def __init__(
- self,
- base_url: str,
- http: urllib3.PoolManager,
- authorization_provider: AuthorizationProvider,
- ) -> None:
- self._delegate = PolarisRestTool(
- name="polaris.policy.delegate",
- description="Internal delegate for policy operations",
- base_url=base_url,
- default_path_prefix="api/catalog/polaris/v1/",
- http=http,
- authorization_provider=authorization_provider,
- )
+ def __init__(self, rest_client: PolarisRestTool) -> None:
+ self._rest_client = rest_client
@property
def name(self) -> str:
@@ -184,7 +169,7 @@ class PolarisPolicyTool(McpTool):
else: # pragma: no cover
raise ValueError(f"Unsupported operation: {operation}")
- raw = self._delegate.call(delegate_args)
+ raw = self._rest_client.call(delegate_args)
return self._maybe_augment_error(raw, normalized)
def _handle_list(
diff --git a/mcp-server/polaris_mcp/tools/principal.py
b/mcp-server/polaris_mcp/tools/principal.py
index 693990b..60470fb 100644
--- a/mcp-server/polaris_mcp/tools/principal.py
+++ b/mcp-server/polaris_mcp/tools/principal.py
@@ -24,9 +24,6 @@ from __future__ import annotations
import copy
from typing import Any, Dict, Optional, Set
-import urllib3
-
-from polaris_mcp.authorization import AuthorizationProvider
from polaris_mcp.base import (
JSONDict,
McpTool,
@@ -57,20 +54,8 @@ class PolarisPrincipalTool(McpTool):
ASSIGN_ROLE_ALIASES: Set[str] = {"assign-principal-role", "assign-role"}
REVOKE_ROLE_ALIASES: Set[str] = {"revoke-principal-role", "revoke-role"}
- def __init__(
- self,
- base_url: str,
- http: urllib3.PoolManager,
- authorization_provider: AuthorizationProvider,
- ) -> None:
- self._delegate = PolarisRestTool(
- name="polaris.principal.delegate",
- description="Internal delegate for principal operations",
- base_url=base_url,
- default_path_prefix="api/management/v1/",
- http=http,
- authorization_provider=authorization_provider,
- )
+ def __init__(self, rest_client: PolarisRestTool) -> None:
+ self._rest_client = rest_client
@property
def name(self) -> str:
@@ -167,7 +152,7 @@ class PolarisPrincipalTool(McpTool):
else: # pragma: no cover
raise ValueError(f"Unsupported operation: {operation}")
- raw = self._delegate.call(delegate_args)
+ raw = self._rest_client.call(delegate_args)
return self._maybe_augment_error(raw, normalized)
def _handle_list(self, delegate_args: JSONDict) -> None:
diff --git a/mcp-server/polaris_mcp/tools/principal_role.py
b/mcp-server/polaris_mcp/tools/principal_role.py
index 8fd84f7..2941769 100644
--- a/mcp-server/polaris_mcp/tools/principal_role.py
+++ b/mcp-server/polaris_mcp/tools/principal_role.py
@@ -23,9 +23,6 @@ from __future__ import annotations
import copy
from typing import Any, Dict, Optional, Set
-import urllib3
-
-from polaris_mcp.authorization import AuthorizationProvider
from polaris_mcp.base import (
JSONDict,
McpTool,
@@ -61,20 +58,8 @@ class PolarisPrincipalRoleTool(McpTool):
"remove-catalog-role",
}
- def __init__(
- self,
- base_url: str,
- http: urllib3.PoolManager,
- authorization_provider: AuthorizationProvider,
- ) -> None:
- self._delegate = PolarisRestTool(
- name="polaris.principalrole.delegate",
- description="Internal delegate for principal role operations",
- base_url=base_url,
- default_path_prefix="api/management/v1/",
- http=http,
- authorization_provider=authorization_provider,
- )
+ def __init__(self, rest_client: PolarisRestTool) -> None:
+ self._rest_client = rest_client
@property
def name(self) -> str:
@@ -192,7 +177,7 @@ class PolarisPrincipalRoleTool(McpTool):
else: # pragma: no cover
raise ValueError(f"Unsupported operation: {operation}")
- raw = self._delegate.call(delegate_args)
+ raw = self._rest_client.call(delegate_args)
return self._maybe_augment_error(raw, normalized)
def _principal_role_path(self, arguments: Dict[str, Any]) -> str:
diff --git a/mcp-server/polaris_mcp/tools/table.py
b/mcp-server/polaris_mcp/tools/table.py
index df02fb8..a92a40b 100644
--- a/mcp-server/polaris_mcp/tools/table.py
+++ b/mcp-server/polaris_mcp/tools/table.py
@@ -25,9 +25,6 @@ import copy
import string
from typing import Any, Dict, List, Set
-import urllib3
-
-from polaris_mcp.authorization import AuthorizationProvider
from polaris_mcp.base import (
JSONDict,
McpTool,
@@ -50,20 +47,8 @@ class PolarisTableTool(McpTool):
COMMIT_ALIASES: Set[str] = {"commit", "update"}
DELETE_ALIASES: Set[str] = {"delete", "drop"}
- def __init__(
- self,
- base_url: str,
- http: urllib3.PoolManager,
- authorization_provider: AuthorizationProvider,
- ) -> None:
- self._delegate = PolarisRestTool(
- name="polaris.table.delegate",
- description="Internal delegate for table operations",
- base_url=base_url,
- default_path_prefix="api/catalog/v1/",
- http=http,
- authorization_provider=authorization_provider,
- )
+ def __init__(self, rest_client: PolarisRestTool) -> None:
+ self._rest_client = rest_client
@property
def name(self) -> str:
@@ -151,7 +136,7 @@ class PolarisTableTool(McpTool):
else: # pragma: no cover - defensive, normalize guarantees handled
cases
raise ValueError(f"Unsupported operation: {operation}")
- return self._delegate.call(delegate_args)
+ return self._rest_client.call(delegate_args)
def _handle_list(
self, delegate_args: JSONDict, catalog: str, namespace: str
diff --git a/mcp-server/tests/test_authorization.py
b/mcp-server/tests/test_authorization.py
new file mode 100644
index 0000000..4d57356
--- /dev/null
+++ b/mcp-server/tests/test_authorization.py
@@ -0,0 +1,140 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Unit tests for ``polaris_mcp.authorization``."""
+
+from __future__ import annotations
+
+import json
+import time
+from types import SimpleNamespace
+from unittest import mock
+
+import pytest
+
+from polaris_mcp.authorization import (
+ ClientCredentialsAuthorizationProvider,
+ StaticAuthorizationProvider,
+ none,
+)
+
+
+def test_static_authorization_provider_trims_and_formats() -> None:
+ provider = StaticAuthorizationProvider(" token123 ")
+ assert provider.authorization_header() == "Bearer token123"
+
+ empty = StaticAuthorizationProvider(" ")
+ assert empty.authorization_header() is None
+
+
+def test_none_authorization_provider_returns_none() -> None:
+ provider = none()
+ assert provider.authorization_header() is None
+
+
+def test_client_credentials_fetches_and_caches_tokens(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ http = mock.Mock()
+ now = time.time()
+ response = SimpleNamespace(
+ status=200,
+ data=json.dumps({"access_token": "abc", "expires_in":
120}).encode("utf-8"),
+ )
+ http.request.return_value = response
+
+ provider = ClientCredentialsAuthorizationProvider(
+ token_endpoint="https://auth/token",
+ client_id="client",
+ client_secret="secret",
+ scope=None,
+ http=http,
+ )
+
+ with mock.patch("time.time", return_value=now):
+ header1 = provider.authorization_header()
+ header2 = provider.authorization_header()
+
+ assert header1 == "Bearer abc"
+ assert header2 == "Bearer abc"
+
+ http.request.assert_called_once()
+ body = http.request.call_args.kwargs["body"]
+ assert "grant_type=client_credentials" in body
+ assert "client_id=client" in body
+ assert "client_secret=secret" in body
+
+ # Force expiry to trigger a refresh
+ http.request.reset_mock()
+ refreshed = SimpleNamespace(
+ status=200,
+ data=json.dumps({"access_token": "def", "expires_in":
3600}).encode("utf-8"),
+ )
+ http.request.return_value = refreshed
+ with mock.patch("time.time", return_value=now + 4000):
+ header3 = provider.authorization_header()
+
+ assert header3 == "Bearer def"
+ http.request.assert_called_once()
+
+
[email protected](
+ "payload,expected_message",
+ [
+ ({"access_token": ""}, "missing access_token"),
+ ({"nope": "value"}, "missing access_token"),
+ ("not-json", "invalid JSON"),
+ ],
+)
+def test_client_credentials_rejects_invalid_responses(
+ payload: object, expected_message: str
+) -> None:
+ http = mock.Mock()
+ if isinstance(payload, str):
+ data = payload.encode("utf-8")
+ else:
+ data = json.dumps(payload).encode("utf-8")
+ http.request.return_value = SimpleNamespace(status=200, data=data)
+
+ provider = ClientCredentialsAuthorizationProvider(
+ token_endpoint="https://auth/token",
+ client_id="client",
+ client_secret="secret",
+ scope=None,
+ http=http,
+ )
+
+ with pytest.raises(RuntimeError, match=expected_message):
+ provider.authorization_header()
+
+
+def test_client_credentials_errors_on_non_200_status() -> None:
+ http = mock.Mock()
+ http.request.return_value = SimpleNamespace(status=500, data=b"boom")
+
+ provider = ClientCredentialsAuthorizationProvider(
+ token_endpoint="https://auth/token",
+ client_id="client",
+ client_secret="secret",
+ scope=None,
+ http=http,
+ )
+
+ with pytest.raises(RuntimeError, match="500"):
+ provider.authorization_header()
diff --git a/mcp-server/tests/test_catalog_role_tool.py
b/mcp-server/tests/test_catalog_role_tool.py
new file mode 100644
index 0000000..ff90d2a
--- /dev/null
+++ b/mcp-server/tests/test_catalog_role_tool.py
@@ -0,0 +1,73 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Unit tests for ``polaris_mcp.tools.catalog_role``."""
+
+from __future__ import annotations
+
+import pytest
+from unittest import mock
+
+from polaris_mcp.base import ToolExecutionResult
+from polaris_mcp.tools.catalog_role import PolarisCatalogRoleTool
+
+
+def _build_tool() -> tuple[PolarisCatalogRoleTool, mock.Mock]:
+ rest_client = mock.Mock()
+ rest_client.call.return_value = ToolExecutionResult(text="ok",
is_error=False)
+ tool = PolarisCatalogRoleTool(rest_client=rest_client)
+ return tool, rest_client
+
+
+def test_list_grants_builds_expected_path() -> None:
+ tool, rest_client = _build_tool()
+
+ tool.call({"operation": "list-grants", "catalog": "prod", "catalogRole":
"analyst"})
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "GET"
+ assert payload["path"] == "catalogs/prod/catalog-roles/analyst/grants"
+
+
+def test_add_grant_requires_body() -> None:
+ tool, rest_client = _build_tool()
+ body = {"principal": "alice", "permission": "READ"}
+
+ tool.call(
+ {
+ "operation": "grant",
+ "catalog": "prod",
+ "catalogRole": "analyst",
+ "body": body,
+ }
+ )
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "PUT"
+ assert payload["path"] == "catalogs/prod/catalog-roles/analyst/grants"
+ assert payload["body"] == body
+ assert payload["body"] is not body # it's a different object
+
+
+def test_add_grant_fails_without_body() -> None:
+ tool, _ = _build_tool()
+ with pytest.raises(ValueError, match="AddGrantRequest payload"):
+ tool.call(
+ {"operation": "add-grant", "catalog": "prod", "catalogRole":
"analyst"}
+ )
diff --git a/mcp-server/tests/test_catalog_tool.py
b/mcp-server/tests/test_catalog_tool.py
new file mode 100644
index 0000000..17d7197
--- /dev/null
+++ b/mcp-server/tests/test_catalog_tool.py
@@ -0,0 +1,76 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Unit tests for ``polaris_mcp.tools.catalog``."""
+
+from __future__ import annotations
+
+import pytest
+from unittest import mock
+
+from polaris_mcp.base import ToolExecutionResult
+from polaris_mcp.tools.catalog import PolarisCatalogTool
+
+
+def _build_tool() -> tuple[PolarisCatalogTool, mock.Mock]:
+ rest_client = mock.Mock()
+ rest_client.call.return_value = ToolExecutionResult(text="ok",
is_error=False)
+ tool = PolarisCatalogTool(rest_client=rest_client)
+ return tool, rest_client
+
+
+def test_list_operation_uses_management_path() -> None:
+ tool, rest_client = _build_tool()
+
+ tool.call({"operation": "list"})
+
+ rest_client.call.assert_called_once()
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "GET"
+ assert payload["path"] == "catalogs"
+
+
+def test_get_operation_requires_catalog_and_encodes() -> None:
+ tool, rest_client = _build_tool()
+
+ tool.call({"operation": "get", "catalog": "my catalog"})
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "GET"
+ assert payload["path"] == "catalogs/my%20catalog"
+
+
+def test_create_operation_requires_body_and_copies_it() -> None:
+ tool, rest_client = _build_tool()
+ body = {"name": "c1", "properties": {"a": "b"}}
+
+ tool.call({"operation": "create", "body": body})
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "POST"
+ assert payload["path"] == "catalogs"
+ assert payload["body"] == body
+ assert payload["body"] is not body
+ assert payload["body"]["properties"] is not body["properties"]
+
+
+def test_create_operation_requires_body_present() -> None:
+ tool, _ = _build_tool()
+ with pytest.raises(ValueError, match="Create operations require"):
+ tool.call({"operation": "create"})
diff --git a/mcp-server/tests/test_namespace_tool.py
b/mcp-server/tests/test_namespace_tool.py
index 88af2b1..23f2527 100644
--- a/mcp-server/tests/test_namespace_tool.py
+++ b/mcp-server/tests/test_namespace_tool.py
@@ -1,3 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
"""Unit tests for ``polaris_mcp.tools.namespace``."""
from __future__ import annotations
@@ -8,21 +27,15 @@ from polaris_mcp.base import ToolExecutionResult
from polaris_mcp.tools.namespace import PolarisNamespaceTool
-def _build_tool(mock_rest: mock.Mock) -> tuple[PolarisNamespaceTool,
mock.Mock]:
- delegate = mock.Mock()
- delegate.call.return_value = ToolExecutionResult(text="done",
is_error=False)
- mock_rest.return_value = delegate
- tool = PolarisNamespaceTool(
- "https://polaris/", mock.sentinel.http, mock.sentinel.auth
- )
- return tool, delegate
+def _build_tool() -> tuple[PolarisNamespaceTool, mock.Mock]:
+ rest_client = mock.Mock()
+ rest_client.call.return_value = ToolExecutionResult(text="done",
is_error=False)
+ tool = PolarisNamespaceTool(rest_client=rest_client)
+ return tool, rest_client
[email protected]("polaris_mcp.tools.namespace.PolarisRestTool")
-def test_get_operation_encodes_namespace_with_unit_separator(
- mock_rest: mock.Mock,
-) -> None:
- tool, delegate = _build_tool(mock_rest)
+def test_get_operation_encodes_namespace_with_unit_separator() -> None:
+ tool, delegate = _build_tool()
tool.call(
{
@@ -38,11 +51,8 @@ def test_get_operation_encodes_namespace_with_unit_separator(
assert payload["path"] == "prod/namespaces/analytics%1Fdaily"
[email protected]("polaris_mcp.tools.namespace.PolarisRestTool")
-def test_create_operation_infers_namespace_array_from_string(
- mock_rest: mock.Mock,
-) -> None:
- tool, delegate = _build_tool(mock_rest)
+def test_create_operation_infers_namespace_array_from_string() -> None:
+ tool, delegate = _build_tool()
body = {"properties": {"owner": "analytics"}}
tool.call(
diff --git a/mcp-server/tests/test_policy_tool.py
b/mcp-server/tests/test_policy_tool.py
new file mode 100644
index 0000000..08e4aec
--- /dev/null
+++ b/mcp-server/tests/test_policy_tool.py
@@ -0,0 +1,59 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Unit tests for ``polaris_mcp.tools.policy``."""
+
+from __future__ import annotations
+
+import pytest
+from unittest import mock
+
+from polaris_mcp.base import ToolExecutionResult
+from polaris_mcp.tools.policy import PolarisPolicyTool
+
+
+def _build_tool() -> tuple[PolarisPolicyTool, mock.Mock]:
+ rest_client = mock.Mock()
+ rest_client.call.return_value = ToolExecutionResult(text="ok",
is_error=False)
+ tool = PolarisPolicyTool(rest_client=rest_client)
+ return tool, rest_client
+
+
+def test_list_operation_requires_namespace_and_builds_path() -> None:
+ tool, rest_client = _build_tool()
+
+ tool.call({"operation": "list", "catalog": "prod", "namespace":
"analytics.daily"})
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "GET"
+ assert payload["path"] == "prod/namespaces/analytics.daily/policies"
+
+
+def test_create_operation_requires_body() -> None:
+ tool, _ = _build_tool()
+ with pytest.raises(ValueError, match="Create operations require"):
+ tool.call({"operation": "create", "catalog": "prod", "namespace":
"ns"})
+
+
+def test_attach_operation_requires_policy() -> None:
+ tool, _ = _build_tool()
+ with pytest.raises(ValueError, match="Policy name is required"):
+ tool.call(
+ {"operation": "attach", "catalog": "prod", "namespace": "ns",
"body": {}}
+ )
diff --git a/mcp-server/tests/test_principal_role_tool.py
b/mcp-server/tests/test_principal_role_tool.py
new file mode 100644
index 0000000..b87bd9e
--- /dev/null
+++ b/mcp-server/tests/test_principal_role_tool.py
@@ -0,0 +1,62 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Unit tests for ``polaris_mcp.tools.principal_role``."""
+
+from __future__ import annotations
+
+import pytest
+from unittest import mock
+
+from polaris_mcp.base import ToolExecutionResult
+from polaris_mcp.tools.principal_role import PolarisPrincipalRoleTool
+
+
+def _build_tool() -> tuple[PolarisPrincipalRoleTool, mock.Mock]:
+ rest_client = mock.Mock()
+ rest_client.call.return_value = ToolExecutionResult(text="ok",
is_error=False)
+ tool = PolarisPrincipalRoleTool(rest_client=rest_client)
+ return tool, rest_client
+
+
+def test_list_operation_sets_management_path() -> None:
+ tool, rest_client = _build_tool()
+
+ tool.call({"operation": "list"})
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "GET"
+ assert payload["path"] == "principal-roles"
+
+
+def test_assign_catalog_role_requires_body() -> None:
+ tool, _ = _build_tool()
+
+ with pytest.raises(ValueError, match="Missing required field: catalog"):
+ tool.call({"operation": "assign-catalog-role", "principalRole":
"analyst"})
+
+
+def test_get_operation_encodes_principal_role() -> None:
+ tool, rest_client = _build_tool()
+
+ tool.call({"operation": "get", "principalRole": "team role"})
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "GET"
+ assert payload["path"] == "principal-roles/team%20role"
diff --git a/mcp-server/tests/test_principal_tool.py
b/mcp-server/tests/test_principal_tool.py
new file mode 100644
index 0000000..cee6287
--- /dev/null
+++ b/mcp-server/tests/test_principal_tool.py
@@ -0,0 +1,64 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Unit tests for ``polaris_mcp.tools.principal``."""
+
+from __future__ import annotations
+
+import pytest
+from unittest import mock
+
+from polaris_mcp.base import ToolExecutionResult
+from polaris_mcp.tools.principal import PolarisPrincipalTool
+
+
+def _build_tool() -> tuple[PolarisPrincipalTool, mock.Mock]:
+ rest_client = mock.Mock()
+ rest_client.call.return_value = ToolExecutionResult(text="ok",
is_error=False)
+ tool = PolarisPrincipalTool(rest_client=rest_client)
+ return tool, rest_client
+
+
+def test_list_operation_sets_management_path() -> None:
+ tool, rest_client = _build_tool()
+
+ tool.call({"operation": "list"})
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "GET"
+ assert payload["path"] == "principals"
+
+
+def test_assign_role_requires_principal_and_body() -> None:
+ tool, _ = _build_tool()
+ with pytest.raises(ValueError, match="Missing required field: principal"):
+ tool.call({"operation": "assign-role"})
+
+ with pytest.raises(ValueError, match="GrantPrincipalRoleRequest"):
+ tool.call({"operation": "assign-role", "principal": "alice"})
+
+
+def test_get_operation_encodes_principal() -> None:
+ tool, rest_client = _build_tool()
+
+ tool.call({"operation": "get", "principal": "svc user"})
+
+ payload = rest_client.call.call_args.args[0]
+ assert payload["method"] == "GET"
+ assert payload["path"] == "principals/svc%20user"
diff --git a/mcp-server/tests/test_server.py b/mcp-server/tests/test_server.py
index 0322a56..9680cc8 100644
--- a/mcp-server/tests/test_server.py
+++ b/mcp-server/tests/test_server.py
@@ -1,3 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
"""Unit tests for ``polaris_mcp.server`` helpers."""
from __future__ import annotations
diff --git a/mcp-server/tests/test_table_tool.py
b/mcp-server/tests/test_table_tool.py
index 88bd8ee..e3d5d9b 100644
--- a/mcp-server/tests/test_table_tool.py
+++ b/mcp-server/tests/test_table_tool.py
@@ -1,3 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
"""Unit tests for ``polaris_mcp.tools.table``."""
from __future__ import annotations
@@ -9,21 +28,17 @@ from polaris_mcp.base import ToolExecutionResult
from polaris_mcp.tools.table import PolarisTableTool
-def _build_tool(mock_rest: mock.Mock) -> tuple[PolarisTableTool, mock.Mock]:
- delegate = mock.Mock()
- delegate.call.return_value = ToolExecutionResult(
+def _build_tool() -> tuple[PolarisTableTool, mock.Mock]:
+ rest_client = mock.Mock()
+ rest_client.call.return_value = ToolExecutionResult(
text="ok", is_error=False, metadata={"k": "v"}
)
- mock_rest.return_value = delegate
- tool = PolarisTableTool("https://polaris/", mock.sentinel.http,
mock.sentinel.auth)
- return tool, delegate
+ tool = PolarisTableTool(rest_client=rest_client)
+ return tool, rest_client
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_list_operation_uses_get_and_copies_query_and_headers(
- mock_rest: mock.Mock,
-) -> None:
- tool, delegate = _build_tool(mock_rest)
+def test_list_operation_uses_get_and_copies_query_and_headers() -> None:
+ tool, delegate = _build_tool()
arguments = {
"operation": "LS",
"catalog": "prod west",
@@ -45,9 +60,8 @@ def test_list_operation_uses_get_and_copies_query_and_headers(
assert payload["headers"] is not arguments["headers"]
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_get_operation_accepts_alias_and_encodes_table(mock_rest: mock.Mock)
-> None:
- tool, delegate = _build_tool(mock_rest)
+def test_get_operation_accepts_alias_and_encodes_table() -> None:
+ tool, delegate = _build_tool()
arguments = {
"operation": "fetch",
"catalog": "prod",
@@ -64,17 +78,15 @@ def
test_get_operation_accepts_alias_and_encodes_table(mock_rest: mock.Mock) ->
assert "body" not in payload
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_get_operation_requires_table_argument(mock_rest: mock.Mock) -> None:
- tool, _ = _build_tool(mock_rest)
+def test_get_operation_requires_table_argument() -> None:
+ tool, _ = _build_tool()
with pytest.raises(ValueError, match="Table name is required"):
tool.call({"operation": "get", "catalog": "prod", "namespace":
"analytics"})
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_create_operation_deep_copies_request_body(mock_rest: mock.Mock) ->
None:
- tool, delegate = _build_tool(mock_rest)
+def test_create_operation_deep_copies_request_body() -> None:
+ tool, delegate = _build_tool()
body = {"table": "t1", "properties": {"schema-id": 1}}
tool.call(
{
@@ -97,17 +109,15 @@ def
test_create_operation_deep_copies_request_body(mock_rest: mock.Mock) -> None
assert payload["body"]["properties"]["schema-id"] == 1
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_create_operation_requires_body(mock_rest: mock.Mock) -> None:
- tool, _ = _build_tool(mock_rest)
+def test_create_operation_requires_body() -> None:
+ tool, _ = _build_tool()
with pytest.raises(ValueError, match="Create operations require"):
tool.call({"operation": "create", "catalog": "prod", "namespace":
"analytics"})
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_commit_operation_requires_table_and_body(mock_rest: mock.Mock) ->
None:
- tool, _ = _build_tool(mock_rest)
+def test_commit_operation_requires_table_and_body() -> None:
+ tool, _ = _build_tool()
with pytest.raises(ValueError, match="Table name is required"):
tool.call(
@@ -130,9 +140,8 @@ def
test_commit_operation_requires_table_and_body(mock_rest: mock.Mock) -> None:
)
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_commit_operation_post_request_with_body_copy(mock_rest: mock.Mock) ->
None:
- tool, delegate = _build_tool(mock_rest)
+def test_commit_operation_post_request_with_body_copy() -> None:
+ tool, delegate = _build_tool()
body = {"changes": [{"type": "append", "snapshot-id": 5}]}
tool.call(
@@ -157,9 +166,8 @@ def
test_commit_operation_post_request_with_body_copy(mock_rest: mock.Mock) -> N
assert payload["body"]["changes"][0]["snapshot-id"] == 5
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_delete_operation_uses_alias_and_encodes_table(mock_rest: mock.Mock)
-> None:
- tool, delegate = _build_tool(mock_rest)
+def test_delete_operation_uses_alias_and_encodes_table() -> None:
+ tool, delegate = _build_tool()
tool.call(
{
@@ -176,9 +184,8 @@ def
test_delete_operation_uses_alias_and_encodes_table(mock_rest: mock.Mock) ->
assert payload["path"] == "prod/namespaces/analytics/tables/fact%20daily"
[email protected]("polaris_mcp.tools.table.PolarisRestTool")
-def test_namespace_validation_rejects_blank_values(mock_rest: mock.Mock) ->
None:
- tool, _ = _build_tool(mock_rest)
+def test_namespace_validation_rejects_blank_values() -> None:
+ tool, _ = _build_tool()
with pytest.raises(ValueError, match="Namespace must be provided"):
tool.call({"operation": "list", "catalog": "prod", "namespace": None})
diff --git a/mcp-server/uv.lock b/mcp-server/uv.lock
index cd9575c..8f0f62f 100644
--- a/mcp-server/uv.lock
+++ b/mcp-server/uv.lock
@@ -165,6 +165,15 @@ wheels = [
{ url =
"https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl",
hash =
"sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size
= 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url =
"https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz",
hash =
"sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size
= 7114, upload-time = "2023-08-12T20:38:17.776Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl",
hash =
"sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size
= 7249, upload-time = "2023-08-12T20:38:16.269Z" },
+]
+
[[package]]
name = "charset-normalizer"
version = "3.4.4"
@@ -366,6 +375,15 @@ wheels = [
{ url =
"https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl",
hash =
"sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size
= 45550, upload-time = "2023-08-31T06:11:58.822Z" },
]
+[[package]]
+name = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url =
"https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz",
hash =
"sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size
= 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl",
hash =
"sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size
= 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
[[package]]
name = "dnspython"
version = "2.8.0"
@@ -443,6 +461,15 @@ wheels = [
{ url =
"https://files.pythonhosted.org/packages/bd/c6/95eacd687cfab64fec13bfb64e6c6e7da13d01ecd4cb7d7e991858a08119/fastmcp-2.13.0.2-py3-none-any.whl",
hash =
"sha256:eb381eb073a101aabbc0ac44b05e23fef0cd1619344b7703115c825c8755fa1c", size
= 367511, upload-time = "2025-10-28T13:56:18.83Z" },
]
+[[package]]
+name = "filelock"
+version = "3.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url =
"https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz",
hash =
"sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size
= 18922, upload-time = "2025-10-08T18:03:50.056Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl",
hash =
"sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size
= 16054, upload-time = "2025-10-08T18:03:48.35Z" },
+]
+
[[package]]
name = "h11"
version = "0.16.0"
@@ -489,6 +516,15 @@ wheels = [
{ url =
"https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl",
hash =
"sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size
= 8960, upload-time = "2025-10-10T21:48:21.158Z" },
]
+[[package]]
+name = "identify"
+version = "2.6.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url =
"https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz",
hash =
"sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size
= 99311, upload-time = "2025-10-02T17:43:40.631Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl",
hash =
"sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size
= 99183, upload-time = "2025-10-02T17:43:39.137Z" },
+]
+
[[package]]
name = "idna"
version = "3.11"
@@ -677,6 +713,15 @@ wheels = [
{ url =
"https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl",
hash =
"sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size
= 69667, upload-time = "2025-09-02T15:23:09.635Z" },
]
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url =
"https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz",
hash =
"sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size
= 47437, upload-time = "2024-06-04T18:44:11.171Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl",
hash =
"sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size
= 22314, upload-time = "2024-06-04T18:44:08.352Z" },
+]
+
[[package]]
name = "openapi-pydantic"
version = "0.5.1"
@@ -744,6 +789,9 @@ dependencies = [
]
[package.optional-dependencies]
+dev = [
+ { name = "pre-commit" },
+]
test = [
{ name = "pytest" },
]
@@ -751,10 +799,27 @@ test = [
[package.metadata]
requires-dist = [
{ name = "fastmcp", specifier = ">=2.13.0.2" },
+ { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8.2" },
{ name = "urllib3", specifier = ">=1.25.3,<3.0.0" },
]
-provides-extras = ["test"]
+provides-extras = ["test", "dev"]
+
+[[package]]
+name = "pre-commit"
+version = "4.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url =
"https://files.pythonhosted.org/packages/a6/49/7845c2d7bf6474efd8e27905b51b11e6ce411708c91e829b93f324de9929/pre_commit-4.4.0.tar.gz",
hash =
"sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15", size
= 197501, upload-time = "2025-11-08T21:12:11.607Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl",
hash =
"sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size
= 226049, upload-time = "2025-11-08T21:12:10.228Z" },
+]
[[package]]
name = "py-key-value-aio"
@@ -1435,6 +1500,21 @@ wheels = [
{ url =
"https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl",
hash =
"sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size
= 68109, upload-time = "2025-10-18T13:46:42.958Z" },
]
+[[package]]
+name = "virtualenv"
+version = "20.35.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url =
"https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz",
hash =
"sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size
= 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
+wheels = [
+ { url =
"https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl",
hash =
"sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size
= 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
+]
+
[[package]]
name = "websockets"
version = "15.0.1"