Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [main]
branches: [main, dev]
pull_request:
branches: [main]

Expand All @@ -13,15 +13,15 @@ jobs:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync --dev
- run: uv run ruff check src/metorial/ examples/ tests/
- run: uv run ruff check src/metorial/ tests/

format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync --dev
- run: uv run cblack --check src/metorial/ examples/ --exclude='src/metorial/_generated'
- run: uv run ruff format --check src/metorial/

type-check:
runs-on: ubuntu-latest
Expand Down
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ cython_debug/
.abstra/

# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/

Expand All @@ -211,5 +211,4 @@ __marimo__/

# Project-specific
playground/
tests/
pytest.ini
pytest.ini
10 changes: 1 addition & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@ repos:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
exclude: ^src/metorial/_generated/

- repo: local
hooks:
- id: cblack
name: cblack
entry: cblack
language: system
types: [python]
args: [--check]
- id: ruff-format
exclude: ^src/metorial/_generated/

- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
23 changes: 1 addition & 22 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.20.0",
"ruff>=0.4.0",
"cblack>=22.6.0",
"mypy>=1.0.0",
"types-requests>=2.32.0",
"pre-commit>=3.5.0",
Expand All @@ -64,27 +63,7 @@ packages = ["src/metorial"]
[tool.hatch.build.targets.sdist]
include = ["/src", "/README.md", "/LICENSE"]

# cblack configuration (Black with 2-space indentation)
[tool.cblack]
line-length = 88
target-version = ['py310']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| build
| dist
| src/metorial/_generated
)/
'''

# MyPy configuration (WorkOS pattern - strict mode)
# MyPy configuration
[tool.mypy]
python_version = "3.10"
strict = true
Expand Down
6 changes: 2 additions & 4 deletions src/metorial/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,10 @@ def mcp(self) -> dict[str, Any]:
}

@overload
def create_mcp_session(self, init: MetorialMcpSessionInit) -> MetorialSession:
...
def create_mcp_session(self, init: MetorialMcpSessionInit) -> MetorialSession: ...

@overload
def create_mcp_session(self, init: dict[str, Any]) -> MetorialSession:
...
def create_mcp_session(self, init: dict[str, Any]) -> MetorialSession: ...

def create_mcp_session(
self, init: MetorialMcpSessionInit | dict[str, Any]
Expand Down
2 changes: 1 addition & 1 deletion src/metorial/_client_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _normalize_server_deployments(
raise ValueError(f"Invalid deployment object format: {deployment}")
else:
raise ValueError(
f"Invalid deployment type: {type(deployment)} " "- must be string or dict"
f"Invalid deployment type: {type(deployment)} - must be string or dict"
)

return normalized
Expand Down
9 changes: 3 additions & 6 deletions src/metorial/_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@ class ToolLike(Protocol):
"""Protocol for tool-like objects that have name, description, and parameters."""

@property
def name(self) -> str:
...
def name(self) -> str: ...

@property
def description(self) -> str | None:
...
def description(self) -> str | None: ...

def get_parameters_as(self, format: str) -> dict[str, Any]:
...
def get_parameters_as(self, format: str) -> dict[str, Any]: ...


@runtime_checkable
Expand Down
2 changes: 1 addition & 1 deletion src/metorial/_sdk_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def build(
raise ValueError("api_host must be set")

def builder(
get_endpoints: Callable[[MetorialEndpointManager], dict[str, Any]]
get_endpoints: Callable[[MetorialEndpointManager], dict[str, Any]],
) -> Callable[[dict[str, Any]], dict[str, Any]]:
def sdk(config: dict[str, Any]) -> dict[str, Any]:
full_config = get_config(config)
Expand Down
12 changes: 6 additions & 6 deletions src/metorial/adapters/openai_compatible.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
call_openai_compatible_tools,
)
except ImportError:
call_openai_compatible_tools: Callable[
[Any, list[Any]], list[dict[str, Any]]
] | None = None
build_openai_compatible_tools: Callable[
[Any, bool], list[dict[str, Any]]
] | None = None
call_openai_compatible_tools: (
Callable[[Any, list[Any]], list[dict[str, Any]]] | None
) = None
build_openai_compatible_tools: (
Callable[[Any, bool], list[dict[str, Any]]] | None
) = None


class OpenAICompatibleAdapter(ProviderAdapter):
Expand Down
3 changes: 2 additions & 1 deletion src/metorial/integrations/pydantic_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ async def tool_fn(**kwargs: Any) -> str:

# Set annotations for PydanticAI to discover parameters
tool_fn.__annotations__ = {
k: v[0] for k, v in fields.items() # Get the type from (type, Field) tuple
k: v[0]
for k, v in fields.items() # Get the type from (type, Field) tuple
}
tool_fn.__annotations__["return"] = str

Expand Down
12 changes: 4 additions & 8 deletions src/metorial/mcp/mcp_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ class _ServersAPI(Protocol):
"""Protocol for servers API - only requires capabilities sub-API."""

@property
def capabilities(self) -> _CapabilitiesAPI:
...
def capabilities(self) -> _CapabilitiesAPI: ...


class MetorialCoreSDK(Protocol):
Expand All @@ -93,16 +92,13 @@ class MetorialCoreSDK(Protocol):
"""

@property
def _config(self) -> _SDKConfig:
...
def _config(self) -> _SDKConfig: ...

@property
def sessions(self) -> _SessionsAPI | None:
...
def sessions(self) -> _SessionsAPI | None: ...

@property
def servers(self) -> _ServersAPI | None:
...
def servers(self) -> _ServersAPI | None: ...


class _SessionResponse(TypedDict):
Expand Down
131 changes: 131 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Shared test fixtures and configuration for metorial tests.

Includes sync/async client parametrization pattern.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Literal
from unittest.mock import AsyncMock, MagicMock

import pytest

if TYPE_CHECKING:
from metorial import Metorial, MetorialSync


# --------------------------------------------------------------------------
# Pytest configuration
# --------------------------------------------------------------------------


def pytest_configure(config: pytest.Config) -> None:
"""Register custom markers."""
config.addinivalue_line(
"markers",
"sync_and_async: mark test to run with both sync and async clients",
)


# --------------------------------------------------------------------------
# Sync/Async client parametrization
# --------------------------------------------------------------------------


@pytest.fixture(params=["sync", "async"])
def client_type(request: pytest.FixtureRequest) -> Literal["sync", "async"]:
"""Parametrize tests to run with both sync and async clients."""
return request.param


@pytest.fixture
def metorial_client(
client_type: Literal["sync", "async"],
mock_metorial_config: dict[str, str],
) -> Metorial | MetorialSync:
"""Create a Metorial client based on client_type fixture.

This allows tests marked with @pytest.mark.sync_and_async to run
automatically with both sync and async clients.
"""
if client_type == "sync":
from metorial import MetorialSync

return MetorialSync(api_key=mock_metorial_config["apiKey"])
else:
from metorial import Metorial

return Metorial(api_key=mock_metorial_config["apiKey"])


@pytest.fixture
def async_metorial_client(mock_metorial_config: dict[str, str]) -> Metorial:
"""Create an async-only Metorial client for async-specific tests."""
from metorial import Metorial

return Metorial(api_key=mock_metorial_config["apiKey"])


@pytest.fixture
def sync_metorial_client(mock_metorial_config: dict[str, str]) -> MetorialSync:
"""Create a sync-only Metorial client for sync-specific tests."""
from metorial import MetorialSync

return MetorialSync(api_key=mock_metorial_config["apiKey"])


# --------------------------------------------------------------------------
# Mock fixtures
# --------------------------------------------------------------------------


@pytest.fixture
def mock_tool_manager() -> MagicMock:
"""Mock tool manager for testing."""
manager = MagicMock()
manager.get_tools.return_value = []
manager.call_tool = AsyncMock(return_value={"content": "result"})
manager.get_tool.return_value = None
return manager


@pytest.fixture
def mock_metorial_config() -> dict[str, str]:
"""Mock configuration for testing."""
return {
"apiKey": "test-api-key",
"apiHost": "https://api.metorial.com",
"mcpHost": "https://mcp.metorial.com",
}


@pytest.fixture
def mock_mcp_tool() -> MagicMock:
"""Mock MCP tool object."""
tool = MagicMock()
tool.name = "test_tool"
tool.description = "A test tool"
tool.parameters = {"type": "object", "properties": {"param1": {"type": "string"}}}
return tool


@pytest.fixture
def mock_mcp_session() -> MagicMock:
"""Mock MCP session for testing."""
session = MagicMock()
session.get_tool_manager = AsyncMock()
session.close = AsyncMock()
return session


@pytest.fixture
def mock_http_response() -> MagicMock:
"""Mock HTTP response for testing RawResponse."""
response = MagicMock()
response.status_code = 200
response.headers = {
"X-Request-ID": "req-test-123",
"Content-Type": "application/json",
}
return response
Loading