mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310, S306, S110, TID251, UP042, F401) and run black on 526 files. Co-authored-by: Hanzo Dev <[email protected]>
428 lines
14 KiB
Python
428 lines
14 KiB
Python
"""Pydantic models for hanzo-tools-api.
|
|
|
|
Provides strongly-typed, structured data objects for:
|
|
- API call results
|
|
- Operations and parameters
|
|
- Credentials and provider configs
|
|
- Tool schemas for agent integration
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
# =============================================================================
|
|
# Enums
|
|
# =============================================================================
|
|
|
|
|
|
class AuthType(str, Enum): # noqa: UP042
|
|
"""Authentication type for API providers."""
|
|
|
|
BEARER = "bearer" # Authorization: Bearer <token>
|
|
BASIC = "basic" # Basic auth (username:password)
|
|
HEADER = "header" # Custom header (e.g., x-api-key)
|
|
API_KEY = "api_key" # X-API-Key header
|
|
QUERY = "query" # Query parameter
|
|
|
|
|
|
class CredentialSource(str, Enum): # noqa: UP042
|
|
"""Source of credential resolution."""
|
|
|
|
OVERRIDE = "override" # Per-call override
|
|
MEMORY = "memory" # In-memory config
|
|
STORED = "stored" # File storage
|
|
ENVIRONMENT = "environment" # Environment variable
|
|
NONE = "none" # Not found
|
|
|
|
|
|
class ParameterLocation(str, Enum): # noqa: UP042
|
|
"""Location of API parameter."""
|
|
|
|
PATH = "path"
|
|
QUERY = "query"
|
|
HEADER = "header"
|
|
COOKIE = "cookie"
|
|
|
|
|
|
# =============================================================================
|
|
# Credential Models
|
|
# =============================================================================
|
|
|
|
|
|
class Credential(BaseModel):
|
|
"""API credential for a provider."""
|
|
|
|
model_config = ConfigDict(frozen=False)
|
|
|
|
provider: str = Field(description="Provider name")
|
|
api_key: str | None = Field(default=None, description="API key or token")
|
|
api_secret: str | None = Field(default=None, description="API secret (if needed)")
|
|
account_id: str | None = Field(default=None, description="Account/organization ID")
|
|
base_url: str | None = Field(default=None, description="Custom base URL override")
|
|
extra: dict[str, Any] = Field(
|
|
default_factory=dict, description="Provider-specific fields"
|
|
)
|
|
|
|
@property
|
|
def has_credentials(self) -> bool:
|
|
"""Check if credential has at least an API key."""
|
|
return bool(self.api_key)
|
|
|
|
|
|
class EffectiveCredential(BaseModel):
|
|
"""Resolved credential with source information."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
credential: Credential
|
|
source: CredentialSource = Field(
|
|
description="Where the credential was resolved from"
|
|
)
|
|
env_var_used: str | None = Field(
|
|
default=None, description="Environment variable used (if any)"
|
|
)
|
|
|
|
@property
|
|
def has_credentials(self) -> bool:
|
|
return self.credential.has_credentials
|
|
|
|
|
|
class ProviderConfig(BaseModel):
|
|
"""Provider-specific configuration."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
name: str = Field(description="Provider identifier")
|
|
display_name: str = Field(description="Human-readable name")
|
|
base_url: str = Field(description="Default base URL")
|
|
auth_type: AuthType = Field(
|
|
default=AuthType.BEARER, description="Authentication method"
|
|
)
|
|
auth_header: str = Field(
|
|
default="Authorization", description="Header name for auth"
|
|
)
|
|
auth_prefix: str = Field(default="Bearer", description="Prefix for auth value")
|
|
auth_query_param: str = Field(default="api_key", description="Query param for auth")
|
|
spec_url: str | None = Field(default=None, description="URL to OpenAPI spec")
|
|
env_vars: list[str] = Field(
|
|
default_factory=list, description="Environment variables to check"
|
|
)
|
|
extra_headers: dict[str, str] = Field(
|
|
default_factory=dict, description="Additional headers"
|
|
)
|
|
|
|
|
|
class ProviderStatus(BaseModel):
|
|
"""Status of a provider configuration."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
name: str
|
|
display_name: str
|
|
configured: bool = Field(description="Whether credentials are available")
|
|
source: CredentialSource | None = Field(
|
|
default=None, description="Credential source"
|
|
)
|
|
base_url: str
|
|
has_spec: bool = Field(description="Whether OpenAPI spec is available")
|
|
spec_cached: bool = Field(
|
|
default=False, description="Whether spec is cached locally"
|
|
)
|
|
spec_age_seconds: float | None = Field(
|
|
default=None, description="Age of cached spec"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# OpenAPI Operation Models
|
|
# =============================================================================
|
|
|
|
|
|
class Parameter(BaseModel):
|
|
"""API operation parameter."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
name: str = Field(description="Parameter name")
|
|
location: ParameterLocation = Field(description="Where the parameter goes")
|
|
required: bool = Field(default=False, description="Whether parameter is required")
|
|
schema_type: str = Field(default="string", description="Parameter data type")
|
|
description: str = Field(default="", description="Parameter description")
|
|
default: Any = Field(default=None, description="Default value")
|
|
enum: list[Any] = Field(default_factory=list, description="Allowed values")
|
|
json_schema: dict[str, Any] | None = Field(
|
|
default=None, description="Full JSON schema"
|
|
)
|
|
|
|
|
|
class Operation(BaseModel):
|
|
"""API operation definition."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
operation_id: str = Field(description="Unique operation identifier")
|
|
method: str = Field(description="HTTP method (GET, POST, etc.)")
|
|
path: str = Field(description="URL path pattern")
|
|
summary: str = Field(default="", description="Short description")
|
|
description: str = Field(default="", description="Detailed description")
|
|
parameters: list[Parameter] = Field(
|
|
default_factory=list, description="Operation parameters"
|
|
)
|
|
request_body_schema: dict[str, Any] | None = Field(
|
|
default=None, description="Request body JSON schema"
|
|
)
|
|
request_body_required: bool = Field(
|
|
default=False, description="Whether request body is required"
|
|
)
|
|
response_schema: dict[str, Any] | None = Field(
|
|
default=None, description="Response JSON schema"
|
|
)
|
|
tags: list[str] = Field(default_factory=list, description="Operation tags")
|
|
deprecated: bool = Field(
|
|
default=False, description="Whether operation is deprecated"
|
|
)
|
|
|
|
@property
|
|
def params_schema(self) -> dict[str, Any]:
|
|
"""Get JSON schema for parameters."""
|
|
properties = {}
|
|
required = []
|
|
|
|
for param in self.parameters:
|
|
prop = {"type": param.schema_type}
|
|
if param.description:
|
|
prop["description"] = param.description
|
|
if param.enum:
|
|
prop["enum"] = param.enum
|
|
if param.default is not None:
|
|
prop["default"] = param.default
|
|
if param.json_schema:
|
|
prop.update(param.json_schema)
|
|
|
|
properties[param.name] = prop
|
|
if param.required:
|
|
required.append(param.name)
|
|
|
|
return {
|
|
"type": "object",
|
|
"properties": properties,
|
|
"required": required,
|
|
}
|
|
|
|
def compact_repr(self) -> str:
|
|
"""Get compact representation for agent prompts."""
|
|
return f"{self.operation_id}: {self.method} {self.path} - {self.summary}"
|
|
|
|
|
|
class OperationSummary(BaseModel):
|
|
"""Compact operation summary for agent discovery."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
operation_id: str
|
|
method: str
|
|
path: str
|
|
summary: str
|
|
tags: list[str] = Field(default_factory=list)
|
|
deprecated: bool = False
|
|
|
|
@classmethod
|
|
def from_operation(cls, op: Operation) -> OperationSummary:
|
|
return cls(
|
|
operation_id=op.operation_id,
|
|
method=op.method,
|
|
path=op.path,
|
|
summary=op.summary,
|
|
tags=op.tags,
|
|
deprecated=op.deprecated,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# API Call Result Models
|
|
# =============================================================================
|
|
|
|
|
|
class APICallResult(BaseModel):
|
|
"""Structured result from an API call."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
success: bool = Field(description="Whether the call succeeded (2xx status)")
|
|
status_code: int = Field(description="HTTP status code")
|
|
headers: dict[str, str] = Field(
|
|
default_factory=dict, description="Response headers"
|
|
)
|
|
body: Any = Field(
|
|
default=None, description="Response body (parsed JSON or raw text)"
|
|
)
|
|
raw_body: str | None = Field(default=None, description="Raw response body")
|
|
elapsed_ms: float | None = Field(
|
|
default=None, description="Request duration in milliseconds"
|
|
)
|
|
request_id: str | None = Field(default=None, description="Request ID from headers")
|
|
|
|
# Metadata
|
|
provider: str = Field(description="Provider that was called")
|
|
operation_id: str | None = Field(
|
|
default=None, description="Operation ID (if using spec)"
|
|
)
|
|
method: str = Field(description="HTTP method used")
|
|
url: str = Field(description="Full URL that was called")
|
|
|
|
@property
|
|
def data(self) -> Any:
|
|
"""Alias for body for backwards compatibility."""
|
|
return self.body
|
|
|
|
@property
|
|
def is_error(self) -> bool:
|
|
"""Check if response indicates an error."""
|
|
return self.status_code >= 400
|
|
|
|
def raise_for_status(self) -> None:
|
|
"""Raise APIError if response indicates an error."""
|
|
if self.is_error:
|
|
from .errors import APIError
|
|
|
|
raise APIError(
|
|
message=f"API call failed with status {self.status_code}",
|
|
status_code=self.status_code,
|
|
body=self.body,
|
|
provider=self.provider,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Spec Cache Models
|
|
# =============================================================================
|
|
|
|
|
|
class SpecCacheEntry(BaseModel):
|
|
"""Cached OpenAPI spec entry."""
|
|
|
|
model_config = ConfigDict(frozen=False)
|
|
|
|
provider: str
|
|
spec: dict[str, Any]
|
|
fetched_at: datetime
|
|
etag: str | None = None
|
|
last_modified: str | None = None
|
|
source_url: str | None = None
|
|
|
|
@property
|
|
def age_seconds(self) -> float:
|
|
"""Get age of cache entry in seconds."""
|
|
return (datetime.now() - self.fetched_at).total_seconds()
|
|
|
|
@property
|
|
def is_stale(self) -> bool:
|
|
"""Check if cache is stale (>24 hours)."""
|
|
return self.age_seconds > 86400
|
|
|
|
|
|
# =============================================================================
|
|
# Tool Schema Models (for agent integration)
|
|
# =============================================================================
|
|
|
|
|
|
class ToolParameter(BaseModel):
|
|
"""Parameter definition for tool schema."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
name: str
|
|
type: str
|
|
description: str
|
|
required: bool = False
|
|
default: Any = None
|
|
enum: list[str] | None = None
|
|
|
|
|
|
class ToolSchema(BaseModel):
|
|
"""Machine-readable tool schema for agent integration."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
name: str = Field(description="Tool/method name")
|
|
description: str = Field(description="What the tool does")
|
|
parameters: list[ToolParameter] = Field(default_factory=list)
|
|
returns: str = Field(description="Return type description")
|
|
examples: list[str] = Field(default_factory=list, description="Example usage")
|
|
|
|
def to_json_schema(self) -> dict[str, Any]:
|
|
"""Convert to JSON schema format."""
|
|
properties = {}
|
|
required = []
|
|
|
|
for param in self.parameters:
|
|
prop = {"type": param.type, "description": param.description}
|
|
if param.enum:
|
|
prop["enum"] = param.enum
|
|
if param.default is not None:
|
|
prop["default"] = param.default
|
|
properties[param.name] = prop
|
|
if param.required:
|
|
required.append(param.name)
|
|
|
|
return {
|
|
"name": self.name,
|
|
"description": self.description,
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": properties,
|
|
"required": required,
|
|
},
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# List/Discovery Results
|
|
# =============================================================================
|
|
|
|
|
|
class ProviderListResult(BaseModel):
|
|
"""Result of listing providers."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
providers: list[ProviderStatus]
|
|
configured_count: int
|
|
total_count: int
|
|
|
|
@property
|
|
def configured(self) -> list[ProviderStatus]:
|
|
"""Get only configured providers."""
|
|
return [p for p in self.providers if p.configured]
|
|
|
|
@property
|
|
def unconfigured(self) -> list[ProviderStatus]:
|
|
"""Get only unconfigured providers."""
|
|
return [p for p in self.providers if not p.configured]
|
|
|
|
|
|
class OperationListResult(BaseModel):
|
|
"""Result of listing operations."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
provider: str
|
|
operations: list[OperationSummary]
|
|
total_count: int
|
|
filter_applied: str | None = None
|
|
|
|
def by_tag(self) -> dict[str, list[OperationSummary]]:
|
|
"""Group operations by tag."""
|
|
result: dict[str, list[OperationSummary]] = {}
|
|
for op in self.operations:
|
|
for tag in op.tags or ["untagged"]:
|
|
if tag not in result:
|
|
result[tag] = []
|
|
result[tag].append(op)
|
|
return result
|