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
8 changes: 6 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
pip install -e "core[dev]"
pip install -e plugins/communication_protocols/cli[dev]
pip install -e plugins/communication_protocols/http[dev]
pip install -e plugins/communication_protocols/mcp[dev]
pip install -e plugins/communication_protocols/text[dev]

- name: Run tests with pytest
run: |
pytest tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=src/utcp --cov-report=xml --cov-report=html
pytest core/tests/ plugins/communication_protocols/cli/tests/ plugins/communication_protocols/http/tests/ plugins/communication_protocols/mcp/tests/ plugins/communication_protocols/text/tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=core/src/utcp --cov-report=xml --cov-report=html

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
Expand Down
1,009 changes: 319 additions & 690 deletions README.md

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "utcp"
version = "1.0.0"
authors = [
{ name = "UTCP Contributors" },
]
description = "Universal Tool Calling Protocol (UTCP) client library for Python"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"pydantic>=2.0",
"python-dotenv>=1.0",
"tomli>=2.0",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
license = "MPL-2.0"

[project.optional-dependencies]
dev = [
"build",
"pytest",
"pytest-asyncio",
"pytest-cov",
"coverage",
"twine",
]

[project.urls]
Homepage = "https://utcp.io"
Source = "https://github.com/universal-tool-calling-protocol/python-utcp"
Issues = "https://github.com/universal-tool-calling-protocol/python-utcp/issues"
10 changes: 10 additions & 0 deletions core/src/utcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import logging
import sys

logger = logging.getLogger("utcp")

if not logger.handlers: # Only add default handler if user didn't configure logging
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
File renamed without changes.
33 changes: 33 additions & 0 deletions core/src/utcp/data/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Authentication schemes for UTCP providers.

This module defines the authentication models supported by UTCP providers,
including API key authentication, basic authentication, and OAuth2.
"""

from abc import ABC
from pydantic import BaseModel
from utcp.interfaces.serializer import Serializer
from utcp.exceptions import UtcpSerializerValidationError
import traceback

class Auth(BaseModel, ABC):
"""Authentication details for a provider.

Attributes:
auth_type: The authentication type identifier.
"""
auth_type: str

class AuthSerializer(Serializer[Auth]):
auth_serializers: dict[str, Serializer[Auth]] = {}

def to_dict(self, obj: Auth) -> dict:
return AuthSerializer.auth_serializers[obj.auth_type].to_dict(obj)

def validate_dict(self, obj: dict) -> Auth:
try:
return AuthSerializer.auth_serializers[obj["auth_type"]].validate_dict(obj)
except KeyError:
raise ValueError(f"Invalid auth type: {obj['auth_type']}")
except Exception as e:
raise UtcpSerializerValidationError("Invalid Auth: " + traceback.format_exc()) from e
12 changes: 12 additions & 0 deletions core/src/utcp/data/auth_implementations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from utcp.data.auth_implementations.api_key_auth import ApiKeyAuth, ApiKeyAuthSerializer
from utcp.data.auth_implementations.basic_auth import BasicAuth, BasicAuthSerializer
from utcp.data.auth_implementations.oauth2_auth import OAuth2Auth, OAuth2AuthSerializer

__all__ = [
"ApiKeyAuth",
"BasicAuth",
"OAuth2Auth",
"ApiKeyAuthSerializer",
"BasicAuthSerializer",
"OAuth2AuthSerializer"
]
42 changes: 42 additions & 0 deletions core/src/utcp/data/auth_implementations/api_key_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from utcp.data.auth import Auth
from utcp.interfaces.serializer import Serializer
from pydantic import Field, ValidationError
from typing import Literal
from utcp.exceptions import UtcpSerializerValidationError

class ApiKeyAuth(Auth):
"""Authentication using an API key.

The key can be provided directly or sourced from an environment variable.
Supports placement in headers, query parameters, or cookies.

Attributes:
auth_type: The authentication type identifier, always "api_key".
api_key: The API key for authentication. Values starting with '$' or formatted as '${}' are
treated as an injected variable from environment or configuration.
var_name: The name of the header, query parameter, or cookie that
contains the API key.
location: Where to include the API key (header, query parameter, or cookie).
"""

auth_type: Literal["api_key"] = "api_key"
api_key: str = Field(..., description="The API key for authentication. Values starting with '$' or formatted as '${}' are treated as an injected variable from environment or configuration. This is the recommended way to provide API keys.")
var_name: str = Field(
"X-Api-Key", description="The name of the header, query parameter, cookie or other container for the API key."
)
location: Literal["header", "query", "cookie"] = Field(
"header", description="Where to include the API key (header, query parameter, or cookie)."
)


class ApiKeyAuthSerializer(Serializer[ApiKeyAuth]):
def to_dict(self, obj: ApiKeyAuth) -> dict:
return obj.model_dump()

def validate_dict(self, obj: dict) -> ApiKeyAuth:
try:
return ApiKeyAuth.model_validate(obj)
except ValidationError as e:
raise UtcpSerializerValidationError(f"Invalid ApiKeyAuth: {e}") from e
except Exception as e:
raise UtcpSerializerValidationError("An unexpected error occurred during ApiKeyAuth validation.") from e
34 changes: 34 additions & 0 deletions core/src/utcp/data/auth_implementations/basic_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from utcp.data.auth import Auth
from utcp.interfaces.serializer import Serializer
from pydantic import Field, ValidationError
from typing import Literal
from utcp.exceptions import UtcpSerializerValidationError

class BasicAuth(Auth):
"""Authentication using HTTP Basic Authentication.

Uses the standard HTTP Basic Authentication scheme with username and password
encoded in the Authorization header.

Attributes:
auth_type: The authentication type identifier, always "basic".
username: The username for basic authentication. Recommended to use injected variables.
password: The password for basic authentication. Recommended to use injected variables.
"""

auth_type: Literal["basic"] = "basic"
username: str = Field(..., description="The username for basic authentication.")
password: str = Field(..., description="The password for basic authentication.")


class BasicAuthSerializer(Serializer[BasicAuth]):
def to_dict(self, obj: BasicAuth) -> dict:
return obj.model_dump()

def validate_dict(self, obj: dict) -> BasicAuth:
try:
return BasicAuth.model_validate(obj)
except ValidationError as e:
raise UtcpSerializerValidationError(f"Invalid BasicAuth: {e}") from e
except Exception as e:
raise UtcpSerializerValidationError("An unexpected error occurred during BasicAuth validation.") from e
39 changes: 39 additions & 0 deletions core/src/utcp/data/auth_implementations/oauth2_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from utcp.data.auth import Auth
from utcp.interfaces.serializer import Serializer
from utcp.exceptions import UtcpSerializerValidationError
from pydantic import Field, ValidationError
from typing import Literal, Optional


class OAuth2Auth(Auth):
"""Authentication using OAuth2 client credentials flow.

Implements the OAuth2 client credentials grant type for machine-to-machine
authentication. The client automatically handles token acquisition and refresh.

Attributes:
auth_type: The authentication type identifier, always "oauth2".
token_url: The URL endpoint to fetch the OAuth2 access token from. Recommended to use injected variables.
client_id: The OAuth2 client identifier. Recommended to use injected variables.
client_secret: The OAuth2 client secret. Recommended to use injected variables.
scope: Optional scope parameter to limit the access token's permissions.
"""

auth_type: Literal["oauth2"] = "oauth2"
token_url: str = Field(..., description="The URL to fetch the OAuth2 token from.")
client_id: str = Field(..., description="The OAuth2 client ID.")
client_secret: str = Field(..., description="The OAuth2 client secret.")
scope: Optional[str] = Field(None, description="The OAuth2 scope.")


class OAuth2AuthSerializer(Serializer[OAuth2Auth]):
def to_dict(self, obj: OAuth2Auth) -> dict:
return obj.model_dump()

def validate_dict(self, obj: dict) -> OAuth2Auth:
try:
return OAuth2Auth.model_validate(obj)
except ValidationError as e:
raise UtcpSerializerValidationError(f"Invalid OAuth2Auth: {e}") from e
except Exception as e:
raise UtcpSerializerValidationError("An unexpected error occurred during OAuth2Auth validation.") from e
75 changes: 75 additions & 0 deletions core/src/utcp/data/call_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Provider configurations for UTCP tool providers.

This module defines the provider models and configurations for all supported
transport protocols in UTCP. Each provider type encapsulates the necessary
configuration to connect to and interact with tools through different
communication channels.

Supported provider types:
- HTTP: RESTful HTTP/HTTPS APIs
- SSE: Server-Sent Events for streaming
- HTTP Stream: HTTP Chunked Transfer Encoding
- CLI: Command Line Interface tools
- WebSocket: Bidirectional WebSocket connections (WIP)
- gRPC: Google Remote Procedure Call (WIP)
- GraphQL: GraphQL query language
- TCP: Raw TCP socket connections
- UDP: User Datagram Protocol
- WebRTC: Web Real-Time Communication (WIP)
- MCP: Model Context Protocol
- Text: Text file-based providers
"""

from typing import List, Optional, Union
from pydantic import BaseModel, field_serializer, field_validator, Field
import uuid
from utcp.interfaces.serializer import Serializer
from utcp.exceptions import UtcpSerializerValidationError
import traceback
from utcp.data.auth import Auth, AuthSerializer

class CallTemplate(BaseModel):
"""Base class for all UTCP tool providers.

This is the abstract base class that all specific call template implementations
inherit from. It provides the common fields that every provider must have.

Attributes:
name: Unique identifier for the provider. Defaults to a random UUID hex string.
Should be unique across all providers and recommended to be set to a human-readable name.
Can only contain letters, numbers and underscores. All special characters must be replaced with underscores.
call_template_type: The transport protocol type used by this provider.
"""

name: str = Field(default_factory=lambda: uuid.uuid4().hex)
call_template_type: str
auth: Optional[Auth] = None

@field_serializer("auth")
def serialize_auth(self, auth: Optional[Auth]):
if auth is None:
return None
return AuthSerializer().to_dict(auth)

@field_validator("auth", mode="before")
@classmethod
def validate_auth(cls, v: Optional[Union[Auth, dict]]):
if v is None:
return None
if isinstance(v, Auth):
return v
return AuthSerializer().validate_dict(v)

class CallTemplateSerializer(Serializer[CallTemplate]):
call_template_serializers: dict[str, Serializer[CallTemplate]] = {}

def to_dict(self, obj: CallTemplate) -> dict:
return CallTemplateSerializer.call_template_serializers[obj.call_template_type].to_dict(obj)

def validate_dict(self, obj: dict) -> CallTemplate:
try:
return CallTemplateSerializer.call_template_serializers[obj["call_template_type"]].validate_dict(obj)
except KeyError:
raise ValueError(f"Invalid call template type: {obj['call_template_type']}")
except Exception as e:
raise UtcpSerializerValidationError("Invalid CallTemplate: " + traceback.format_exc()) from e
10 changes: 10 additions & 0 deletions core/src/utcp/data/register_manual_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from utcp.data.call_template import CallTemplate
from utcp.data.utcp_manual import UtcpManual
from pydantic import BaseModel, Field
from typing import List

class RegisterManualResult(BaseModel):
manual_call_template: CallTemplate
manual: UtcpManual
success: bool
errors: List[str] = Field(default_factory=list)
Loading
Loading