rmi-backend/rmi_sdk.py

950 lines
33 KiB
Python

"""
RMI Python SDK v2.0 — Production-Grade Client
===============================================
Rug Munch Intelligence Python SDK with:
- Async/sync dual API (httpx)
- Automatic retry with exponential backoff
- Batch tool calls (parallel execution)
- Webhook registration and management
- Developer API key support
- x402 payment handling
- Type hints throughout
- Proper error classes
Install: pip install rmi-sdk (or copy this file as rmi_sdk.py)
Usage:
from rmi_sdk import RMI
rmi = RMI(api_key="rmi_dev_...") # free tier, 100 calls/day
result = rmi.scan("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
# Async
async with AsyncRMI(api_key="rmi_dev_...") as rmi:
result = await rmi.scan("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
# Batch
results = rmi.batch([
("scan", {"address": "0x..."}),
("audit", {"address": "0x..."}),
("whale", {"address": "0x..."}),
])
Author: RMI Development
Date: 2026-06-05
"""
import contextlib
import hashlib
import os
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any, TypeVar
try:
import httpx
HTTPX_AVAILABLE = True
except ImportError:
HTTPX_AVAILABLE = False
try:
import asyncio
ASYNCIO_AVAILABLE = True
except ImportError:
ASYNCIO_AVAILABLE = False
# ── Error Classes ─────────────────────────────────────────────────
class RMIError(Exception):
"""Base RMI SDK error."""
pass
class RMIConnectionError(RMIError):
"""Failed to connect to RMI API."""
pass
class RMITimeoutError(RMIError):
"""Request timed out."""
pass
class RMIAuthenticationError(RMIError):
"""Invalid or missing API key."""
pass
class RMIRateLimitError(RMIError):
"""Rate limit exceeded."""
def __init__(self, message: str, retry_after: int = 60, remaining: int = 0):
super().__init__(message)
self.retry_after = retry_after
self.remaining = remaining
class RMIPaymentRequired(RMIError):
"""Payment required for this tool call."""
def __init__(self, message: str, pay_to: str = "", amount: str = "", chain: str = ""):
super().__init__(message)
self.pay_to = pay_to
self.amount = amount
self.chain = chain
class RMIToolError(RMIError):
"""Tool execution failed."""
def __init__(self, tool: str, message: str, status: str = "error"):
super().__init__(message)
self.tool = tool
self.status = status
class RMIDataError(RMIError):
"""No data returned for the query."""
pass
# ── Data Classes ──────────────────────────────────────────────────
@dataclass
class ToolResult:
"""Result from a tool call."""
tool: str
data: dict[str, Any]
status: str = "ok"
enrichment: dict[str, Any] | None = None
intel: dict[str, Any] | None = None
confidence: dict[str, Any] | None = None
trial_remaining: int = 0
cache_hit: bool = False
raw: dict[str, Any] = field(default_factory=dict)
@property
def is_ok(self) -> bool:
return self.status in ("ok", "success")
@property
def has_data(self) -> bool:
if not self.data:
return False
# Check for meaningful data (not just empty dicts/lists)
return any(v is not None and v != [] and v != {} for v in self.data.values())
@dataclass
class BatchResult:
"""Result from a batch tool call."""
results: list[ToolResult]
successful: int = 0
failed: int = 0
total_time_ms: float = 0
@property
def success_rate(self) -> float:
total = self.successful + self.failed
return (self.successful / total * 100) if total > 0 else 0
@dataclass
class Webhook:
"""Registered webhook."""
id: str
url: str
events: list[str]
address: str
active: bool = True
created_at: str = ""
@dataclass
class TierInfo:
"""Developer tier information."""
name: str
daily_limit: int
rate_limit_per_min: int
price: float | str
webhook_support: bool
priority_queue: bool
@dataclass
class UsageInfo:
"""API key usage information."""
tier: str
email: str
status: str
today_calls: int
daily_limit: int
remaining_today: int
total_calls: int
created_at: str
rate_limit_per_min: int
# ── Retry Logic ───────────────────────────────────────────────────
T = TypeVar("T")
def retry_with_backoff(
func: Callable[..., T],
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
exponential_base: float = 2.0,
retryable_errors: tuple[type, ...] = (RMIConnectionError, RMITimeoutError),
) -> T:
"""Execute a function with exponential backoff retry."""
last_exception = None
for attempt in range(max_retries + 1):
try:
return func()
except retryable_errors as e:
last_exception = e
if attempt == max_retries:
break
delay = min(base_delay * (exponential_base**attempt), max_delay)
# Add jitter
import random
delay *= 0.5 + random.random()
time.sleep(delay)
raise last_exception or RMIError("Retry failed")
# ── Sync SDK ──────────────────────────────────────────────────────
class RMI:
"""Rug Munch Intelligence client with automatic x402 payment handling."""
DEFAULT_BASE = "https://mcp.rugmunch.io/api/v1/x402-tools"
DEFAULT_DEV_BASE = "https://mcp.rugmunch.io/api/v1/developer"
def __init__(
self,
api_key: str | None = None,
base_url: str | None = None,
dev_base_url: str | None = None,
timeout: float = 30.0,
max_retries: int = 3,
retry_delay: float = 1.0,
fingerprint: str | None = None,
wallet_identity: str | None = None,
):
"""
Initialize RMI client.
Args:
api_key: Developer API key (from /api/v1/developer/register).
If not provided, uses trial mode (limited calls).
base_url: Base URL for x402 tools API.
dev_base_url: Base URL for developer API.
timeout: Request timeout in seconds.
max_retries: Number of retries on transient errors.
retry_delay: Base delay between retries (seconds).
fingerprint: Client fingerprint for trial tracking.
wallet_identity: Verified wallet identity (from /developer/wallet/verify).
"""
if not HTTPX_AVAILABLE:
raise ImportError(
"httpx is required: pip install httpx. Or use the fallback urllib mode (limited features)."
)
self.api_key = api_key or os.environ.get("RMI_API_KEY")
self.base = (base_url or os.environ.get("RMI_API_BASE") or self.DEFAULT_BASE).rstrip("/")
self.dev_base = (dev_base_url or os.environ.get("RMI_DEV_BASE") or self.DEFAULT_DEV_BASE).rstrip("/")
self.timeout = timeout
self.max_retries = max_retries
self.retry_delay = retry_delay
self.wallet_identity = wallet_identity
# Generate fingerprint if not provided
if fingerprint:
self._fingerprint = fingerprint
else:
self._fingerprint = hashlib.sha256(f"rmi-sdk-v2-{os.uname().nodename}-{time.time()}".encode()).hexdigest()[
:32
]
# Create client
headers = {
"Content-Type": "application/json",
"x-fingerprint": self._fingerprint,
"User-Agent": "rmi-python-sdk/2.0",
}
if self.api_key:
headers["X-RMI-Dev-Key"] = self.api_key
if self.wallet_identity:
headers["X-Wallet-Identity"] = self.wallet_identity
self._client = httpx.Client(
base_url=self.base,
headers=headers,
timeout=httpx.Timeout(timeout, connect=10.0),
http2=True, # Use HTTP/2 if available
)
def _request(
self,
method: str,
path: str,
params: dict | None = None,
json_data: dict | None = None,
headers: dict | None = None,
base_url: str | None = None,
) -> httpx.Response:
"""Make an HTTP request with retry logic."""
f"{base_url or self.base}/{path.lstrip('/')}"
def _do_request():
try:
if method.upper() == "GET":
return self._client.get(path, params=params, headers=headers)
else:
return self._client.post(path, json=json_data, headers=headers)
except httpx.ConnectTimeout as e:
raise RMITimeoutError(f"Connection timeout: {e}") from e
except httpx.ReadTimeout as e:
raise RMITimeoutError(f"Read timeout: {e}") from e
except httpx.ConnectError as e:
raise RMIConnectionError(f"Connection failed: {e}") from e
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
raise RMIAuthenticationError("Invalid API key") from e
elif e.response.status_code == 402:
body = e.response.json() if e.response.content else {}
raise RMIPaymentRequired(
message=body.get("error", "Payment required"),
pay_to=body.get("pay_to", ""),
amount=body.get("amount", ""),
chain=body.get("chain", ""),
) from e
elif e.response.status_code == 429:
retry_after = int(e.response.headers.get("Retry-After", 60))
raise RMIRateLimitError(
message="Rate limit exceeded",
retry_after=retry_after,
) from e
else:
raise RMIToolError(
tool=path.split("/")[-1],
message=f"HTTP {e.response.status_code}: {e.response.text[:200]}",
) from e
return retry_with_backoff(
_do_request,
max_retries=self.max_retries,
base_delay=self.retry_delay,
retryable_errors=(RMIConnectionError, RMITimeoutError),
)
def _parse_response(self, response: httpx.Response, tool: str) -> ToolResult:
"""Parse an API response into a ToolResult."""
try:
data = response.json()
except Exception:
raise RMIToolError(tool, f"Invalid JSON response: {response.text[:200]}")
# Check for error
if "error" in data and not data.get("result"):
raise RMIToolError(tool, data["error"], status=data.get("status", "error"))
# Check for no data
if data.get("status") == "no_data":
raise RMIDataError("No data found for query")
return ToolResult(
tool=tool,
data=data.get("result", data),
status=data.get("status", "ok"),
enrichment=data.get("_enrichment"),
intel=data.get("_intel"),
confidence=data.get("_confidence"),
trial_remaining=int(response.headers.get("X-RMI-Trial-Remaining", 0)),
cache_hit=response.headers.get("X-Cache") == "HIT",
raw=data,
)
# ── Core Tool Execution ───────────────────────────────────────
def call(self, tool: str, params: dict | None = None) -> ToolResult:
"""Call a single tool.
Args:
tool: Tool name (e.g., "scan", "audit", "whale").
params: Tool parameters as a dict.
Returns:
ToolResult with data, enrichment, and metadata.
"""
response = self._request("POST", tool, json_data=params or {})
return self._parse_response(response, tool)
# ── Convenience Methods ───────────────────────────────────────
def scan(self, address: str, chain: str = "solana") -> ToolResult:
"""Scan a wallet address for security risks."""
return self.call("scan", {"address": address, "chain": chain})
def audit(self, address: str, chain: str = "solana") -> ToolResult:
"""Audit a smart contract."""
return self.call("audit", {"address": address, "chain": chain})
def whale(self, address: str, chain: str = "solana") -> ToolResult:
"""Analyze whale wallet activity."""
return self.call("whale", {"address": address, "chain": chain})
def reputation(self, address: str) -> ToolResult:
"""Get reputation score for an address."""
return self.call("reputation_score", {"address": address})
def honeypot(self, address: str, chain: str = "solana") -> ToolResult:
"""Check if a token is a honeypot."""
return self.call("honeypot_check", {"address": address, "chain": chain})
def forensics(self, address: str, chain: str = "solana") -> ToolResult:
"""Run wallet forensics."""
return self.call("forensics", {"address": address, "chain": chain})
def sentiment(self, query: str) -> ToolResult:
"""Get market sentiment for a query."""
return self.call("sentiment", {"query": query})
def rug_probability(self, address: str, chain: str = "solana") -> ToolResult:
"""Get rug probability score for a token."""
return self.call("rug_probability", {"address": address, "chain": chain})
def composite_score(self, address: str, chain: str = "solana") -> ToolResult:
"""Get composite score for a token."""
return self.call("composite_score", {"address": address, "chain": chain})
def smart_money(self, address: str) -> ToolResult:
"""Find smart money wallets similar to this one."""
return self.call("smart_money", {"address": address})
def market_overview(self) -> ToolResult:
"""Get market overview."""
return self.call("market_overview", {})
def narrative(self) -> ToolResult:
"""Get current market narrative."""
return self.call("narrative", {})
# ── Batch Execution ───────────────────────────────────────────
def batch(self, calls: list[tuple[str, dict]], max_concurrent: int = 5) -> BatchResult:
"""Execute multiple tool calls in parallel.
Args:
calls: List of (tool_name, params) tuples.
max_concurrent: Maximum concurrent requests.
Returns:
BatchResult with all results.
"""
import concurrent.futures
import threading
results = []
successful = 0
failed = 0
start = time.monotonic()
semaphore = threading.Semaphore(max_concurrent)
def _execute(tool: str, params: dict) -> ToolResult:
with semaphore:
try:
return self.call(tool, params)
except Exception as e:
return ToolResult(
tool=tool,
data={},
status="error",
raw={"error": str(e)},
)
with concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrent) as executor:
futures = {executor.submit(_execute, tool, params): (tool, params) for tool, params in calls}
for future in concurrent.futures.as_completed(futures):
result = future.result()
results.append(result)
if result.is_ok:
successful += 1
else:
failed += 1
total_time = (time.monotonic() - start) * 1000
return BatchResult(
results=results,
successful=successful,
failed=failed,
total_time_ms=total_time,
)
# ── Developer API ─────────────────────────────────────────────
def register(self, email: str, tier: str = "free", wallet_address: str | None = None) -> dict:
"""Register for a developer API key.
Args:
email: Email address for registration.
tier: Tier name (free, trial, paid, pro).
wallet_address: Optional wallet address for enhanced identity.
Returns:
Dict with api_key and tier info.
"""
response = self._request(
"POST",
"/register",
json_data={"email": email, "tier": tier, "wallet_address": wallet_address},
base_url=self.dev_base,
)
result = response.json()
# If we got a new key, update the client
if result.get("success") and result.get("api_key"):
self.api_key = result["api_key"]
self._client.headers["X-RMI-Dev-Key"] = self.api_key
return result
def verify_key(self) -> UsageInfo:
"""Verify the current API key and get usage info."""
if not self.api_key:
raise RMIAuthenticationError("No API key set")
response = self._request(
"POST",
"/verify",
json_data={"api_key": self.api_key},
base_url=self.dev_base,
)
data = response.json()
if not data.get("valid"):
raise RMIAuthenticationError(data.get("error", "Invalid API key"))
return UsageInfo(
tier=data.get("tier", "free"),
email=data.get("email", ""),
status=data.get("status", "active"),
today_calls=data.get("today_calls", 0),
daily_limit=data.get("daily_limit", 100),
remaining_today=data.get("remaining_today", 0),
total_calls=data.get("total_calls", 0),
created_at=data.get("created_at", ""),
rate_limit_per_min=data.get("rate_limit_per_min", 10),
)
def get_tiers(self) -> list[TierInfo]:
"""Get available developer tiers."""
response = self._request("GET", "/tiers", base_url=self.dev_base)
data = response.json()
tiers = []
for name, info in data.get("tiers", {}).items():
tiers.append(
TierInfo(
name=name,
daily_limit=info.get("daily_limit", 0),
rate_limit_per_min=info.get("rate_limit_per_min", 0),
price=info.get("price", 0),
webhook_support=info.get("webhook_support", False),
priority_queue=info.get("priority_queue", False),
)
)
return tiers
def get_usage(self) -> dict:
"""Get detailed usage stats for the current API key."""
if not self.api_key:
raise RMIAuthenticationError("No API key set")
response = self._request(
"POST",
"/usage",
json_data={"api_key": self.api_key},
base_url=self.dev_base,
)
return response.json()
# ── Webhook Management ────────────────────────────────────────
def register_webhook(
self,
url: str,
events: list[str],
address: str,
) -> Webhook:
"""Register a webhook for real-time alerts.
Args:
url: Webhook URL to receive POST requests.
events: List of event types (rug_pull, whale_move, price_crash, etc.).
address: Wallet or token address to monitor.
Returns:
Webhook object with registration details.
"""
# Use the x402 webhook_register tool
result = self.call(
"webhook_register",
{
"url": url,
"events": events,
"address": address,
},
)
return Webhook(
id=result.data.get("webhook_id", ""),
url=url,
events=events,
address=address,
active=True,
)
def list_webhooks(self, address: str | None = None) -> list[Webhook]:
"""List registered webhooks."""
params = {}
if address:
params["address"] = address
response = self._request("GET", "/webhook_list", params=params)
data = response.json()
webhooks = []
for wh in data.get("webhooks", []):
webhooks.append(
Webhook(
id=wh.get("id", ""),
url=wh.get("url", ""),
events=wh.get("events", []),
address=wh.get("address", ""),
active=wh.get("active", True),
created_at=wh.get("created_at", ""),
)
)
return webhooks
def delete_webhook(self, webhook_id: str) -> bool:
"""Delete a registered webhook."""
# Webhook deletion via the x402 tools API
try:
self.call("webhook_delete", {"webhook_id": webhook_id})
return True
except Exception:
return False
# ── Tool Catalog ──────────────────────────────────────────────
def list_tools(self) -> list[dict]:
"""Get the full tool catalog."""
response = self._request("GET", "/catalog")
data = response.json()
return data.get("tools", [])
def get_tool_info(self, tool: str) -> dict | None:
"""Get info about a specific tool."""
response = self._request("GET", f"/{tool}")
return response.json()
# ── Cleanup ───────────────────────────────────────────────────
def close(self):
"""Close the HTTP client."""
self._client.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def __del__(self):
with contextlib.suppress(Exception):
self.close()
# ── Async SDK ─────────────────────────────────────────────────────
class AsyncRMI:
"""Async RMI client with httpx.
Usage:
async with AsyncRMI(api_key="...") as rmi:
result = await rmi.scan("0x...")
"""
def __init__(
self,
api_key: str | None = None,
base_url: str | None = None,
dev_base_url: str | None = None,
timeout: float = 30.0,
max_retries: int = 3,
retry_delay: float = 1.0,
fingerprint: str | None = None,
wallet_identity: str | None = None,
):
if not HTTPX_AVAILABLE:
raise ImportError("httpx is required for async mode: pip install httpx")
self.api_key = api_key or os.environ.get("RMI_API_KEY")
self.base = (base_url or os.environ.get("RMI_API_BASE") or RMI.DEFAULT_BASE).rstrip("/")
self.dev_base = (dev_base_url or os.environ.get("RMI_DEV_BASE") or RMI.DEFAULT_DEV_BASE).rstrip("/")
self.timeout = timeout
self.max_retries = max_retries
self.retry_delay = retry_delay
self.wallet_identity = wallet_identity
if fingerprint:
self._fingerprint = fingerprint
else:
self._fingerprint = hashlib.sha256(
f"rmi-sdk-v2-async-{os.uname().nodename}-{time.time()}".encode()
).hexdigest()[:32]
headers = {
"Content-Type": "application/json",
"x-fingerprint": self._fingerprint,
"User-Agent": "rmi-python-sdk/2.0-async",
}
if self.api_key:
headers["X-RMI-Dev-Key"] = self.api_key
if self.wallet_identity:
headers["X-Wallet-Identity"] = self.wallet_identity
self._client = httpx.AsyncClient(
base_url=self.base,
headers=headers,
timeout=httpx.Timeout(timeout, connect=10.0),
http2=True,
)
async def _request(
self,
method: str,
path: str,
params: dict | None = None,
json_data: dict | None = None,
headers: dict | None = None,
base_url: str | None = None,
) -> httpx.Response:
f"{base_url or self.base}/{path.lstrip('/')}"
for attempt in range(self.max_retries + 1):
try:
if method.upper() == "GET":
response = await self._client.get(path, params=params, headers=headers)
else:
response = await self._client.post(path, json=json_data, headers=headers)
# Handle error responses
if response.status_code == 401:
raise RMIAuthenticationError("Invalid API key")
elif response.status_code == 402:
body = response.json() if response.content else {}
raise RMIPaymentRequired(
message=body.get("error", "Payment required"),
pay_to=body.get("pay_to", ""),
amount=body.get("amount", ""),
chain=body.get("chain", ""),
)
elif response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
raise RMIRateLimitError("Rate limit exceeded", retry_after=retry_after)
elif response.status_code >= 400:
raise RMIToolError(
tool=path.split("/")[-1],
message=f"HTTP {response.status_code}: {response.text[:200]}",
)
return response
except (httpx.ConnectTimeout, httpx.ReadTimeout) as e:
if attempt == self.max_retries:
raise RMITimeoutError(str(e)) from e
except httpx.ConnectError as e:
if attempt == self.max_retries:
raise RMIConnectionError(str(e)) from e
# Retry with backoff
delay = min(self.retry_delay * (2**attempt), 30.0)
import random
delay *= 0.5 + random.random()
await asyncio.sleep(delay)
raise RMIError("All retries failed")
def _parse_response(self, response: httpx.Response, tool: str) -> ToolResult:
data = response.json()
if "error" in data and not data.get("result"):
raise RMIToolError(tool, data["error"], status=data.get("status", "error"))
if data.get("status") == "no_data":
raise RMIDataError("No data found")
return ToolResult(
tool=tool,
data=data.get("result", data),
status=data.get("status", "ok"),
enrichment=data.get("_enrichment"),
intel=data.get("_intel"),
confidence=data.get("_confidence"),
trial_remaining=int(response.headers.get("X-RMI-Trial-Remaining", 0)),
cache_hit=response.headers.get("X-Cache") == "HIT",
raw=data,
)
async def call(self, tool: str, params: dict | None = None) -> ToolResult:
"""Call a single tool."""
response = await self._request("POST", tool, json_data=params or {})
return self._parse_response(response, tool)
# Convenience methods (same as sync version)
async def scan(self, address: str, chain: str = "solana") -> ToolResult:
return await self.call("scan", {"address": address, "chain": chain})
async def audit(self, address: str, chain: str = "solana") -> ToolResult:
return await self.call("audit", {"address": address, "chain": chain})
async def whale(self, address: str, chain: str = "solana") -> ToolResult:
return await self.call("whale", {"address": address, "chain": chain})
async def reputation(self, address: str) -> ToolResult:
return await self.call("reputation_score", {"address": address})
async def honeypot(self, address: str, chain: str = "solana") -> ToolResult:
return await self.call("honeypot_check", {"address": address, "chain": chain})
async def forensics(self, address: str, chain: str = "solana") -> ToolResult:
return await self.call("forensics", {"address": address, "chain": chain})
async def sentiment(self, query: str) -> ToolResult:
return await self.call("sentiment", {"query": query})
async def rug_probability(self, address: str, chain: str = "solana") -> ToolResult:
return await self.call("rug_probability", {"address": address, "chain": chain})
async def composite_score(self, address: str, chain: str = "solana") -> ToolResult:
return await self.call("composite_score", {"address": address, "chain": chain})
async def smart_money(self, address: str) -> ToolResult:
return await self.call("smart_money", {"address": address})
async def market_overview(self) -> ToolResult:
return await self.call("market_overview", {})
async def narrative(self) -> ToolResult:
return await self.call("narrative", {})
# Batch execution (async)
async def batch(self, calls: list[tuple[str, dict]], max_concurrent: int = 5) -> BatchResult:
"""Execute multiple tool calls concurrently."""
semaphore = asyncio.Semaphore(max_concurrent)
results = []
successful = 0
failed = 0
start = time.monotonic()
async def _execute(tool: str, params: dict) -> ToolResult:
async with semaphore:
try:
return await self.call(tool, params)
except Exception as e:
return ToolResult(tool=tool, data={}, status="error", raw={"error": str(e)})
tasks = [_execute(tool, params) for tool, params in calls]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
failed += 1
elif result.is_ok:
successful += 1
else:
failed += 1
total_time = (time.monotonic() - start) * 1000
return BatchResult(
results=[r for r in results if isinstance(r, ToolResult)],
successful=successful,
failed=failed,
total_time_ms=total_time,
)
# Developer API (async)
async def register(self, email: str, tier: str = "free") -> dict:
response = await self._request(
"POST",
"/register",
json_data={"email": email, "tier": tier},
base_url=self.dev_base,
)
result = response.json()
if result.get("success") and result.get("api_key"):
self.api_key = result["api_key"]
self._client.headers["X-RMI-Dev-Key"] = self.api_key
return result
async def verify_key(self) -> UsageInfo:
if not self.api_key:
raise RMIAuthenticationError("No API key set")
response = await self._request(
"POST",
"/verify",
json_data={"api_key": self.api_key},
base_url=self.dev_base,
)
data = response.json()
if not data.get("valid"):
raise RMIAuthenticationError(data.get("error", "Invalid API key"))
return UsageInfo(
tier=data.get("tier", "free"),
email=data.get("email", ""),
status=data.get("status", "active"),
today_calls=data.get("today_calls", 0),
daily_limit=data.get("daily_limit", 100),
remaining_today=data.get("remaining_today", 0),
total_calls=data.get("total_calls", 0),
created_at=data.get("created_at", ""),
rate_limit_per_min=data.get("rate_limit_per_min", 10),
)
async def list_tools(self) -> list[dict]:
response = await self._request("GET", "/catalog")
return response.json().get("tools", [])
async def close(self):
await self._client.aclose()
async def __aenter__(self):
return self
async def __aexit__(self, *args):
await self.close()
def __del__(self):
try:
if hasattr(self, "_client"):
asyncio.get_event_loop().create_task(self._client.aclose())
except Exception:
pass