mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
feat: add UI registry server and tiered backend (local → registry → github)
Add FastAPI registry server that pre-fetches all component data from hanzoai/ui on GitHub, caches in memory, refreshes every 15min. Serves instant cached responses. Deploy at ui.hanzo.ai. - registry/cache.py: eager full-cache with concurrent GitHub fetching - registry/server.py: FastAPI endpoints for components, blocks, search - registry/client.py: RegistryClient matching GitHubAPIClient interface - Dockerfile for containerized deployment - UiTool now uses 3-tier fallback: local disk → registry → GitHub API - Bump hanzo-tools-ui to 0.2.0, server deps optional (fastapi, uvicorn)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
WORKDIR /build
|
||||
COPY pkg/hanzo-tools-ui/ ./pkg/hanzo-tools-ui/
|
||||
COPY pkg/hanzo-tools-core/ ./pkg/hanzo-tools-core/
|
||||
|
||||
RUN pip install --no-cache-dir \
|
||||
./pkg/hanzo-tools-core \
|
||||
"./pkg/hanzo-tools-ui[server]"
|
||||
|
||||
FROM python:3.12-slim
|
||||
|
||||
RUN useradd -m -r hanzo
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||
COPY --from=builder /usr/local/bin/hanzo-ui-registry /usr/local/bin/
|
||||
|
||||
USER hanzo
|
||||
EXPOSE 8787
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8787/api/health')"
|
||||
|
||||
ENV REGISTRY_HOST=0.0.0.0 \
|
||||
REGISTRY_PORT=8787
|
||||
|
||||
CMD ["python", "-m", "uvicorn", "hanzo_tools.ui.registry.server:app", "--host", "0.0.0.0", "--port", "8787"]
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Hanzo UI Registry — cached component server and client.
|
||||
|
||||
Server: pre-fetches all component data from hanzoai/ui on GitHub,
|
||||
serves cached responses via FastAPI. Deploy at ui.hanzo.ai.
|
||||
|
||||
Client: fetches from the registry server instead of hitting GitHub
|
||||
directly. Used by the UI tool as a middle-tier between local and GitHub.
|
||||
"""
|
||||
|
||||
from .client import RegistryClient
|
||||
|
||||
__all__ = ["RegistryClient"]
|
||||
|
||||
# Server imports are deferred to avoid requiring fastapi at import time.
|
||||
# Use: from hanzo_tools.ui.registry.server import app
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Run the Hanzo UI Registry server.
|
||||
|
||||
Usage:
|
||||
python -m hanzo_tools.ui.registry
|
||||
# or
|
||||
hanzo-ui-registry
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
import uvicorn
|
||||
|
||||
host = os.environ.get("REGISTRY_HOST", "0.0.0.0")
|
||||
port = int(os.environ.get("REGISTRY_PORT", "8787"))
|
||||
dev = os.environ.get("REGISTRY_DEV", "") == "1"
|
||||
|
||||
uvicorn.run(
|
||||
"hanzo_tools.ui.registry.server:app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=dev,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,323 @@
|
||||
"""Registry cache — eagerly fetches all component data from GitHub.
|
||||
|
||||
On startup, walks the hanzoai/ui repo and fetches every component's
|
||||
source, metadata, and demo. Stores everything in memory. Refreshes
|
||||
on a background timer. All lookups are O(1) dict reads.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from ..github_api import FRAMEWORK_CONFIGS, GitHubAPIClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_REFRESH_INTERVAL = int(os.environ.get("REFRESH_INTERVAL", "900")) # 15 min
|
||||
MAX_CONCURRENCY = 10
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentEntry:
|
||||
name: str
|
||||
type: str # "file" or "dir"
|
||||
path: str = ""
|
||||
source: str | None = None
|
||||
metadata: dict | None = None
|
||||
demo: str | None = None
|
||||
category: str = ""
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class FrameworkData:
|
||||
"""All cached data for a single framework."""
|
||||
|
||||
components: dict[str, ComponentEntry] = field(default_factory=dict)
|
||||
blocks: dict[str, ComponentEntry] = field(default_factory=dict)
|
||||
directory_cache: dict[str, dict] = field(default_factory=dict)
|
||||
|
||||
|
||||
class RegistryCache:
|
||||
"""In-memory cache of all UI component data from GitHub."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frameworks: list[str] | None = None,
|
||||
refresh_interval: int = DEFAULT_REFRESH_INTERVAL,
|
||||
):
|
||||
self._github = GitHubAPIClient()
|
||||
self._frameworks = frameworks or ["hanzo"]
|
||||
self._refresh_interval = refresh_interval
|
||||
self._data: dict[str, FrameworkData] = {}
|
||||
self._last_refresh: float = 0
|
||||
self._refresh_duration: float = 0
|
||||
self._refreshing = False
|
||||
self._ready = False
|
||||
self._task: asyncio.Task | None = None
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
return self._ready
|
||||
|
||||
@property
|
||||
def health(self) -> dict:
|
||||
total_components = sum(len(d.components) for d in self._data.values())
|
||||
total_blocks = sum(len(d.blocks) for d in self._data.values())
|
||||
age = time.time() - self._last_refresh if self._last_refresh else None
|
||||
return {
|
||||
"status": "healthy" if self._ready else "warming",
|
||||
"ready": self._ready,
|
||||
"last_refresh": self._last_refresh,
|
||||
"cache_age_seconds": round(age, 1) if age else None,
|
||||
"refresh_duration_seconds": round(self._refresh_duration, 1),
|
||||
"refreshing": self._refreshing,
|
||||
"frameworks": self._frameworks,
|
||||
"components_cached": total_components,
|
||||
"blocks_cached": total_blocks,
|
||||
}
|
||||
|
||||
async def refresh(self) -> None:
|
||||
"""Full refresh: fetch all components for all frameworks."""
|
||||
if self._refreshing:
|
||||
logger.info("Refresh already in progress, skipping")
|
||||
return
|
||||
|
||||
self._refreshing = True
|
||||
start = time.time()
|
||||
logger.info("Starting cache refresh for frameworks: %s", self._frameworks)
|
||||
|
||||
try:
|
||||
for framework in self._frameworks:
|
||||
await self._refresh_framework(framework)
|
||||
except Exception:
|
||||
logger.exception("Cache refresh failed")
|
||||
finally:
|
||||
self._refreshing = False
|
||||
self._last_refresh = time.time()
|
||||
self._refresh_duration = time.time() - start
|
||||
self._ready = True
|
||||
total = sum(len(d.components) for d in self._data.values())
|
||||
logger.info(
|
||||
"Cache refresh complete: %d components in %.1fs",
|
||||
total,
|
||||
self._refresh_duration,
|
||||
)
|
||||
|
||||
async def _refresh_framework(self, framework: str) -> None:
|
||||
"""Refresh all data for a single framework."""
|
||||
if framework not in FRAMEWORK_CONFIGS:
|
||||
logger.warning("Unknown framework: %s", framework)
|
||||
return
|
||||
|
||||
fw_data = FrameworkData()
|
||||
sem = asyncio.Semaphore(MAX_CONCURRENCY)
|
||||
|
||||
# Fetch component list
|
||||
try:
|
||||
comp_list = await self._github.list_components(framework)
|
||||
except Exception:
|
||||
logger.exception("Failed to list components for %s", framework)
|
||||
return
|
||||
|
||||
# Fetch source + metadata + demo for each component concurrently
|
||||
async def fetch_one(comp: dict) -> ComponentEntry:
|
||||
name = comp["name"]
|
||||
entry = ComponentEntry(
|
||||
name=name,
|
||||
type=comp.get("type", "file"),
|
||||
path=comp.get("path", ""),
|
||||
)
|
||||
async with sem:
|
||||
# Source
|
||||
try:
|
||||
entry.source = await self._github.fetch_component(name, framework)
|
||||
except Exception:
|
||||
logger.debug("No source for %s/%s", framework, name)
|
||||
|
||||
# Metadata
|
||||
try:
|
||||
entry.metadata = await self._github.fetch_component_metadata(
|
||||
name, framework
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Demo
|
||||
try:
|
||||
entry.demo = await self._github.fetch_component_demo(
|
||||
name, framework
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return entry
|
||||
|
||||
entries = await asyncio.gather(
|
||||
*[fetch_one(c) for c in comp_list],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
if isinstance(entry, ComponentEntry):
|
||||
fw_data.components[entry.name] = entry
|
||||
elif isinstance(entry, Exception):
|
||||
logger.debug("Component fetch error: %s", entry)
|
||||
|
||||
# Fetch blocks
|
||||
try:
|
||||
block_list = await self._github.list_blocks(framework)
|
||||
for block in block_list:
|
||||
name = block["name"]
|
||||
b_entry = ComponentEntry(name=name, type=block.get("type", "file"))
|
||||
try:
|
||||
async with sem:
|
||||
b_entry.source = await self._github.fetch_block(
|
||||
name, framework
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
fw_data.blocks[name] = b_entry
|
||||
except Exception:
|
||||
logger.debug("No blocks for %s", framework)
|
||||
|
||||
# Cache directory structure
|
||||
config = FRAMEWORK_CONFIGS[framework]
|
||||
try:
|
||||
structure = await self._github.get_directory_structure(
|
||||
config.components_path, framework
|
||||
)
|
||||
fw_data.directory_cache[config.components_path] = structure
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Atomic swap
|
||||
self._data[framework] = fw_data
|
||||
logger.info(
|
||||
"Cached %d components, %d blocks for %s",
|
||||
len(fw_data.components),
|
||||
len(fw_data.blocks),
|
||||
framework,
|
||||
)
|
||||
|
||||
async def start_background_refresh(self) -> None:
|
||||
"""Run refresh loop in the background."""
|
||||
while True:
|
||||
await asyncio.sleep(self._refresh_interval)
|
||||
try:
|
||||
await self.refresh()
|
||||
except Exception:
|
||||
logger.exception("Background refresh failed")
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the background refresh task."""
|
||||
if self._task is None:
|
||||
self._task = asyncio.create_task(self.start_background_refresh())
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the background refresh task."""
|
||||
if self._task is not None:
|
||||
self._task.cancel()
|
||||
self._task = None
|
||||
|
||||
# --- Lookup methods (all O(1)) ---
|
||||
|
||||
def _fw(self, framework: str) -> FrameworkData:
|
||||
return self._data.get(framework, FrameworkData())
|
||||
|
||||
def get_components(self, framework: str = "hanzo") -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"name": e.name,
|
||||
"type": e.type,
|
||||
"path": e.path,
|
||||
"has_source": e.source is not None,
|
||||
"has_demo": e.demo is not None,
|
||||
}
|
||||
for e in self._fw(framework).components.values()
|
||||
]
|
||||
|
||||
def get_component(self, name: str, framework: str = "hanzo") -> dict | None:
|
||||
entry = self._fw(framework).components.get(name)
|
||||
if entry is None:
|
||||
return None
|
||||
return {
|
||||
"name": entry.name,
|
||||
"type": entry.type,
|
||||
"source": entry.source,
|
||||
"metadata": entry.metadata,
|
||||
"demo": entry.demo,
|
||||
}
|
||||
|
||||
def get_component_source(self, name: str, framework: str = "hanzo") -> str | None:
|
||||
entry = self._fw(framework).components.get(name)
|
||||
return entry.source if entry else None
|
||||
|
||||
def get_component_demo(self, name: str, framework: str = "hanzo") -> str | None:
|
||||
entry = self._fw(framework).components.get(name)
|
||||
return entry.demo if entry else None
|
||||
|
||||
def get_component_metadata(
|
||||
self, name: str, framework: str = "hanzo"
|
||||
) -> dict | None:
|
||||
entry = self._fw(framework).components.get(name)
|
||||
return entry.metadata if entry else None
|
||||
|
||||
def get_blocks(self, framework: str = "hanzo") -> list[dict]:
|
||||
return [
|
||||
{"name": e.name, "type": e.type, "has_source": e.source is not None}
|
||||
for e in self._fw(framework).blocks.values()
|
||||
]
|
||||
|
||||
def get_block(self, name: str, framework: str = "hanzo") -> dict | None:
|
||||
entry = self._fw(framework).blocks.get(name)
|
||||
if entry is None:
|
||||
return None
|
||||
return {"name": entry.name, "source": entry.source}
|
||||
|
||||
def search(self, query: str, framework: str = "hanzo") -> list[dict]:
|
||||
q = query.lower()
|
||||
results = []
|
||||
for entry in self._fw(framework).components.values():
|
||||
if (
|
||||
q in entry.name.lower()
|
||||
or q in entry.description.lower()
|
||||
or q in entry.category.lower()
|
||||
):
|
||||
results.append(
|
||||
{"name": entry.name, "type": entry.type, "match": "name"}
|
||||
)
|
||||
return results
|
||||
|
||||
def get_structure(
|
||||
self, path: str, framework: str = "hanzo"
|
||||
) -> dict[str, Any] | None:
|
||||
return self._fw(framework).directory_cache.get(path)
|
||||
|
||||
def build_index(self, framework: str = "hanzo") -> dict:
|
||||
"""Build the full registry index (single-payload for client hydration)."""
|
||||
components = {}
|
||||
for name, entry in self._fw(framework).components.items():
|
||||
components[name] = {
|
||||
"name": entry.name,
|
||||
"type": entry.type,
|
||||
"source": entry.source,
|
||||
"metadata": entry.metadata,
|
||||
"demo": entry.demo,
|
||||
}
|
||||
|
||||
blocks = {}
|
||||
for name, entry in self._fw(framework).blocks.items():
|
||||
blocks[name] = {"name": entry.name, "source": entry.source}
|
||||
|
||||
return {
|
||||
"framework": framework,
|
||||
"generated_at": self._last_refresh,
|
||||
"components": components,
|
||||
"blocks": blocks,
|
||||
"total_components": len(components),
|
||||
"total_blocks": len(blocks),
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Registry client — fetches component data from the Hanzo UI registry server.
|
||||
|
||||
Mirrors the GitHubAPIClient / LocalUIClient interface so it can be used
|
||||
as a drop-in backend in UiTool. Has its own short TTL cache to avoid
|
||||
hitting the server on every tool call during a session.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
DEFAULT_REGISTRY_URL = "https://ui.hanzo.ai"
|
||||
CLIENT_CACHE_TTL = 60 # 1 minute local cache
|
||||
|
||||
|
||||
class RegistryClient:
|
||||
"""Client for the Hanzo UI registry server."""
|
||||
|
||||
def __init__(self, base_url: str | None = None):
|
||||
self._base_url = (
|
||||
base_url
|
||||
or os.environ.get("HANZO_UI_REGISTRY_URL")
|
||||
or DEFAULT_REGISTRY_URL
|
||||
).rstrip("/")
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
self._cache: dict[str, tuple[Any, float]] = {}
|
||||
self._available: bool | None = None
|
||||
|
||||
async def _get_client(self) -> httpx.AsyncClient:
|
||||
if self._client is None:
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self._base_url,
|
||||
timeout=15.0,
|
||||
headers={"User-Agent": "Hanzo-MCP-UI-Tool"},
|
||||
)
|
||||
return self._client
|
||||
|
||||
def _cache_get(self, key: str) -> Any | None:
|
||||
entry = self._cache.get(key)
|
||||
if entry is None:
|
||||
return None
|
||||
data, ts = entry
|
||||
if time.time() - ts > CLIENT_CACHE_TTL:
|
||||
del self._cache[key]
|
||||
return None
|
||||
return data
|
||||
|
||||
def _cache_set(self, key: str, data: Any) -> None:
|
||||
self._cache[key] = (data, time.time())
|
||||
|
||||
async def _get(self, path: str, params: dict | None = None) -> Any:
|
||||
cache_key = f"{path}:{params}"
|
||||
cached = self._cache_get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
client = await self._get_client()
|
||||
resp = await client.get(path, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
self._cache_set(cache_key, data)
|
||||
return data
|
||||
|
||||
async def check_available(self) -> bool:
|
||||
"""Check if the registry server is reachable."""
|
||||
if self._available is not None:
|
||||
return self._available
|
||||
try:
|
||||
client = await self._get_client()
|
||||
resp = await client.get("/api/health", timeout=5.0)
|
||||
self._available = resp.status_code == 200
|
||||
except Exception:
|
||||
self._available = False
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def available(self) -> bool | None:
|
||||
"""Cached availability check result. None if not yet checked."""
|
||||
return self._available
|
||||
|
||||
# --- Mirror of GitHubAPIClient / LocalUIClient interface ---
|
||||
|
||||
async def list_components(self, framework: str = "hanzo") -> list[dict]:
|
||||
data = await self._get("/api/components", {"framework": framework})
|
||||
return data.get("components", [])
|
||||
|
||||
async def fetch_component(self, name: str, framework: str = "hanzo") -> str:
|
||||
data = await self._get(f"/api/components/{name}", {"framework": framework})
|
||||
source = data.get("source")
|
||||
if source is None:
|
||||
raise FileNotFoundError(f"Component '{name}' source not available")
|
||||
return source
|
||||
|
||||
async def fetch_component_demo(self, name: str, framework: str = "hanzo") -> str:
|
||||
data = await self._get(
|
||||
f"/api/components/{name}/demo", {"framework": framework}
|
||||
)
|
||||
demo = data.get("demo")
|
||||
if demo is None:
|
||||
raise FileNotFoundError(f"Demo for '{name}' not available")
|
||||
return demo
|
||||
|
||||
async def fetch_component_metadata(
|
||||
self, name: str, framework: str = "hanzo"
|
||||
) -> dict:
|
||||
data = await self._get(
|
||||
f"/api/components/{name}/metadata", {"framework": framework}
|
||||
)
|
||||
return data.get("metadata", data)
|
||||
|
||||
async def list_blocks(self, framework: str = "hanzo") -> list[dict]:
|
||||
data = await self._get("/api/blocks", {"framework": framework})
|
||||
return data.get("blocks", [])
|
||||
|
||||
async def fetch_block(self, name: str, framework: str = "hanzo") -> str:
|
||||
data = await self._get(f"/api/blocks/{name}", {"framework": framework})
|
||||
source = data.get("source")
|
||||
if source is None:
|
||||
raise FileNotFoundError(f"Block '{name}' source not available")
|
||||
return source
|
||||
|
||||
async def search_components(self, query: str, framework: str = "hanzo") -> list[dict]:
|
||||
data = await self._get("/api/search", {"q": query, "framework": framework})
|
||||
return data.get("results", [])
|
||||
|
||||
async def get_directory_structure(
|
||||
self, path: str, framework: str = "hanzo"
|
||||
) -> dict:
|
||||
return await self._get("/api/structure", {"path": path, "framework": framework})
|
||||
|
||||
async def fetch_full_index(self, framework: str = "hanzo") -> dict:
|
||||
"""Fetch the full registry index — all components in one payload.
|
||||
|
||||
Use this to hydrate a local cache with a single HTTP call.
|
||||
"""
|
||||
return await self._get("/registry/index.json", {"framework": framework})
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._client is not None:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Hanzo UI Registry Server — FastAPI service serving cached component data.
|
||||
|
||||
Deploy at ui.hanzo.ai. Pre-fetches all components from hanzoai/ui on GitHub,
|
||||
serves instant cached responses. Refreshes every 15 minutes in background.
|
||||
|
||||
Run:
|
||||
python -m hanzo_tools.ui.registry
|
||||
# or
|
||||
uvicorn hanzo_tools.ui.registry.server:app --port 8787
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, ORJSONResponse
|
||||
|
||||
from .cache import RegistryCache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration
|
||||
FRAMEWORKS = os.environ.get("REGISTRY_FRAMEWORKS", "hanzo").split(",")
|
||||
REFRESH_INTERVAL = int(os.environ.get("REFRESH_INTERVAL", "900"))
|
||||
|
||||
# Global cache instance
|
||||
_cache: RegistryCache | None = None
|
||||
|
||||
|
||||
def get_cache() -> RegistryCache:
|
||||
assert _cache is not None, "Cache not initialized"
|
||||
return _cache
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global _cache
|
||||
_cache = RegistryCache(frameworks=FRAMEWORKS, refresh_interval=REFRESH_INTERVAL)
|
||||
|
||||
logger.info("Starting initial cache refresh...")
|
||||
await _cache.refresh()
|
||||
logger.info("Initial refresh complete, starting background refresh loop")
|
||||
_cache.start()
|
||||
|
||||
yield
|
||||
|
||||
_cache.stop()
|
||||
logger.info("Registry server shutting down")
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Hanzo UI Registry",
|
||||
description="Cached component registry for Hanzo UI",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["GET"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# --- API Endpoints ---
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
cache = get_cache()
|
||||
return cache.health
|
||||
|
||||
|
||||
@app.get("/api/components")
|
||||
async def list_components(framework: str = Query(default="hanzo")):
|
||||
cache = get_cache()
|
||||
components = cache.get_components(framework)
|
||||
return {"framework": framework, "total": len(components), "components": components}
|
||||
|
||||
|
||||
@app.get("/api/components/{name}")
|
||||
async def get_component(name: str, framework: str = Query(default="hanzo")):
|
||||
cache = get_cache()
|
||||
comp = cache.get_component(name, framework)
|
||||
if comp is None:
|
||||
raise HTTPException(404, detail={"error": "Component not found", "name": name})
|
||||
return comp
|
||||
|
||||
|
||||
@app.get("/api/components/{name}/source")
|
||||
async def get_component_source(name: str, framework: str = Query(default="hanzo")):
|
||||
cache = get_cache()
|
||||
source = cache.get_component_source(name, framework)
|
||||
if source is None:
|
||||
raise HTTPException(404, detail={"error": "Component not found", "name": name})
|
||||
return {"name": name, "source": source}
|
||||
|
||||
|
||||
@app.get("/api/components/{name}/demo")
|
||||
async def get_component_demo(name: str, framework: str = Query(default="hanzo")):
|
||||
cache = get_cache()
|
||||
demo = cache.get_component_demo(name, framework)
|
||||
if demo is None:
|
||||
raise HTTPException(
|
||||
404, detail={"error": "Demo not found", "name": name}
|
||||
)
|
||||
return {"name": name, "demo": demo}
|
||||
|
||||
|
||||
@app.get("/api/components/{name}/metadata")
|
||||
async def get_component_metadata(name: str, framework: str = Query(default="hanzo")):
|
||||
cache = get_cache()
|
||||
metadata = cache.get_component_metadata(name, framework)
|
||||
if metadata is None:
|
||||
raise HTTPException(404, detail={"error": "Component not found", "name": name})
|
||||
return {"name": name, "metadata": metadata}
|
||||
|
||||
|
||||
@app.get("/api/blocks")
|
||||
async def list_blocks(framework: str = Query(default="hanzo")):
|
||||
cache = get_cache()
|
||||
blocks = cache.get_blocks(framework)
|
||||
return {"framework": framework, "total": len(blocks), "blocks": blocks}
|
||||
|
||||
|
||||
@app.get("/api/blocks/{name}")
|
||||
async def get_block(name: str, framework: str = Query(default="hanzo")):
|
||||
cache = get_cache()
|
||||
block = cache.get_block(name, framework)
|
||||
if block is None:
|
||||
raise HTTPException(404, detail={"error": "Block not found", "name": name})
|
||||
return block
|
||||
|
||||
|
||||
@app.get("/api/search")
|
||||
async def search_components(
|
||||
q: str = Query(..., min_length=1), framework: str = Query(default="hanzo")
|
||||
):
|
||||
cache = get_cache()
|
||||
results = cache.search(q, framework)
|
||||
return {"query": q, "framework": framework, "results": results}
|
||||
|
||||
|
||||
@app.get("/api/structure")
|
||||
async def get_structure(
|
||||
path: str = Query(default=""), framework: str = Query(default="hanzo")
|
||||
):
|
||||
cache = get_cache()
|
||||
structure = cache.get_structure(path, framework)
|
||||
if structure is None:
|
||||
raise HTTPException(
|
||||
404, detail={"error": "Path not found in cache", "path": path}
|
||||
)
|
||||
return structure
|
||||
|
||||
|
||||
@app.get("/registry/index.json")
|
||||
async def registry_index(framework: str = Query(default="hanzo")):
|
||||
"""Full registry manifest — all components with source in one payload."""
|
||||
cache = get_cache()
|
||||
index = cache.build_index(framework)
|
||||
return JSONResponse(content=index)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"service": "Hanzo UI Registry",
|
||||
"docs": "/docs",
|
||||
"health": "/api/health",
|
||||
"components": "/api/components",
|
||||
"index": "/registry/index.json",
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
Single 'ui' tool for browsing, searching, installing, and managing
|
||||
UI components from Hanzo and other registries.
|
||||
|
||||
Local-first: when ~/work/hanzo/ui exists, reads directly from disk
|
||||
for the hanzo framework. Falls back to GitHub API otherwise.
|
||||
Backend priority: local disk → registry server → GitHub API
|
||||
|
||||
Actions:
|
||||
- list_components: List available components for a framework
|
||||
@@ -25,6 +24,7 @@ Actions:
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from typing import ClassVar
|
||||
|
||||
from mcp.server.fastmcp import Context as MCPContext
|
||||
@@ -37,6 +37,7 @@ from .github_api import (
|
||||
GitHubAPIClient,
|
||||
)
|
||||
from .local_client import LocalUIClient
|
||||
from .registry.client import RegistryClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,16 +51,20 @@ class UiTool(BaseTool):
|
||||
Browse, search, install, and manage UI components from
|
||||
Hanzo and other registries (shadcn/ui, Vue, Svelte, React Native).
|
||||
|
||||
Local-first: prefers ~/work/hanzo/ui when available for hanzo framework.
|
||||
Backend priority: local → registry → GitHub API
|
||||
"""
|
||||
|
||||
name: ClassVar[str] = "ui"
|
||||
VERSION: ClassVar[str] = "0.2.0"
|
||||
VERSION: ClassVar[str] = "0.3.0"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._github = GitHubAPIClient()
|
||||
self._local = LocalUIClient()
|
||||
self._registry = RegistryClient()
|
||||
self._registry_checked = False
|
||||
self._registry_available = False
|
||||
|
||||
if self._local.available:
|
||||
logger.info("UI tool: local hanzo/ui repo detected, using local-first mode")
|
||||
self._register_ui_actions()
|
||||
@@ -68,6 +73,21 @@ class UiTool(BaseTool):
|
||||
"""Use local client for hanzo frameworks when repo is available."""
|
||||
return framework.startswith("hanzo") and self._local.available
|
||||
|
||||
async def _use_registry(self) -> bool:
|
||||
"""Check if the registry server is available (cached after first check)."""
|
||||
if not self._registry_checked:
|
||||
self._registry_checked = True
|
||||
self._registry_available = await self._registry.check_available()
|
||||
if self._registry_available:
|
||||
logger.info("UI tool: registry server available at %s", self._registry._base_url)
|
||||
return self._registry_available
|
||||
|
||||
async def _remote_client(self):
|
||||
"""Return registry client if available, else GitHub client."""
|
||||
if await self._use_registry():
|
||||
return self._registry, "registry"
|
||||
return self._github, "github"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
local_status = " (local repo detected)" if self._local.available else ""
|
||||
@@ -113,8 +133,8 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
components = await self._local.list_components(fw)
|
||||
source = "local"
|
||||
else:
|
||||
components = await self._github.list_components(fw)
|
||||
source = "github"
|
||||
client, source = await self._remote_client()
|
||||
components = await client.list_components(fw)
|
||||
|
||||
if category:
|
||||
components = [c for c in components if c.get("category") == category]
|
||||
@@ -149,14 +169,15 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
"backend": "local",
|
||||
}
|
||||
except FileNotFoundError:
|
||||
pass # Fall through to GitHub
|
||||
pass # Fall through to remote
|
||||
|
||||
source = await self._github.fetch_component(comp_name, fw)
|
||||
client, backend = await self._remote_client()
|
||||
source = await client.fetch_component(comp_name, fw)
|
||||
return {
|
||||
"framework": self._fw_name(fw),
|
||||
"component": comp_name,
|
||||
"source": source,
|
||||
"backend": "github",
|
||||
"backend": backend,
|
||||
}
|
||||
|
||||
@self.action("get_demo", "Get component demo/example")
|
||||
@@ -184,12 +205,13 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
demo = await self._github.fetch_component_demo(comp_name, fw)
|
||||
client, backend = await self._remote_client()
|
||||
demo = await client.fetch_component_demo(comp_name, fw)
|
||||
return {
|
||||
"framework": self._fw_name(fw),
|
||||
"component": comp_name,
|
||||
"demo": demo,
|
||||
"backend": "github",
|
||||
"backend": backend,
|
||||
}
|
||||
|
||||
@self.action("get_metadata", "Get component metadata")
|
||||
@@ -213,7 +235,8 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
metadata = await self._github.fetch_component_metadata(comp_name, fw)
|
||||
client, _ = await self._remote_client()
|
||||
metadata = await client.fetch_component_metadata(comp_name, fw)
|
||||
return {
|
||||
"framework": self._fw_name(fw),
|
||||
"component": comp_name,
|
||||
@@ -232,8 +255,8 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
blocks = await self._local.list_blocks(fw)
|
||||
source = "local"
|
||||
else:
|
||||
blocks = await self._github.list_blocks(fw)
|
||||
source = "github"
|
||||
client, source = await self._remote_client()
|
||||
blocks = await client.list_blocks(fw)
|
||||
|
||||
if category:
|
||||
blocks = [b for b in blocks if b.get("category") == category]
|
||||
@@ -270,12 +293,13 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
content = await self._github.fetch_block(block_name, fw)
|
||||
client, backend = await self._remote_client()
|
||||
content = await client.fetch_block(block_name, fw)
|
||||
return {
|
||||
"framework": self._fw_name(fw),
|
||||
"block": block_name,
|
||||
"implementation": content,
|
||||
"backend": "github",
|
||||
"backend": backend,
|
||||
}
|
||||
|
||||
@self.action("search", "Search components")
|
||||
@@ -300,21 +324,12 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
"results": matches,
|
||||
}
|
||||
|
||||
components = await self._github.list_components(fw)
|
||||
q_lower = q.lower()
|
||||
|
||||
matches = [
|
||||
c
|
||||
for c in components
|
||||
if q_lower in c.get("name", "").lower()
|
||||
or q_lower in c.get("description", "").lower()
|
||||
or q_lower in c.get("category", "").lower()
|
||||
]
|
||||
|
||||
client, source = await self._remote_client()
|
||||
matches = await client.search_components(q, fw)
|
||||
return {
|
||||
"framework": self._fw_name(fw),
|
||||
"query": q,
|
||||
"source": "github",
|
||||
"source": source,
|
||||
"results": matches,
|
||||
}
|
||||
|
||||
@@ -336,11 +351,12 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
"structure": structure,
|
||||
}
|
||||
|
||||
structure = await self._github.get_directory_structure(path, fw)
|
||||
client, source = await self._remote_client()
|
||||
structure = await client.get_directory_structure(path, fw)
|
||||
return {
|
||||
"framework": self._fw_name(fw),
|
||||
"path": path or "/",
|
||||
"source": "github",
|
||||
"source": source,
|
||||
"structure": structure,
|
||||
}
|
||||
|
||||
@@ -401,10 +417,12 @@ Frameworks: hanzo (default), hanzo-native, hanzo-vue, hanzo-svelte,
|
||||
|
||||
@self.action("get_framework", "Show current and available frameworks")
|
||||
async def get_framework(ctx: MCPContext) -> dict:
|
||||
registry_up = await self._use_registry()
|
||||
return {
|
||||
"current": self._fw_name(_current_framework),
|
||||
"framework": _current_framework,
|
||||
"local_available": self._local.available,
|
||||
"registry_available": registry_up,
|
||||
"available": [
|
||||
{
|
||||
"key": key,
|
||||
|
||||
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hanzo-tools-ui"
|
||||
version = "0.1.0"
|
||||
description = "UI component registry tools for Hanzo AI (HIP-0300)"
|
||||
version = "0.2.0"
|
||||
description = "UI component registry tools and server for Hanzo AI (HIP-0300)"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.12"
|
||||
@@ -36,12 +36,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
server = [
|
||||
"fastapi>=0.115.0",
|
||||
"uvicorn[standard]>=0.34.0",
|
||||
]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
"ruff>=0.1.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
hanzo-ui-registry = "hanzo_tools.ui.registry.__main__:main"
|
||||
|
||||
[project.entry-points."hanzo.tools"]
|
||||
ui = "hanzo_tools.ui:TOOLS"
|
||||
|
||||
|
||||
@@ -1243,7 +1243,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hanzo-tools-ui"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = { editable = "pkg/hanzo-tools-ui" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -1258,18 +1258,24 @@ dev = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
server = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofiles", specifier = ">=24.0.0" },
|
||||
{ name = "fastapi", marker = "extra == 'server'", specifier = ">=0.115.0" },
|
||||
{ name = "hanzo-tools-core", specifier = ">=0.1.0" },
|
||||
{ name = "httpx", specifier = ">=0.25.0" },
|
||||
{ name = "mcp", specifier = ">=1.0.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.34.0" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
provides-extras = ["server", "dev"]
|
||||
|
||||
[[package]]
|
||||
name = "hanzoai"
|
||||
@@ -1394,6 +1400,35 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
@@ -3990,6 +4025,49 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "httptools" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
||||
{ name = "watchfiles" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
version = "0.22.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "21.2.0"
|
||||
|
||||
Reference in New Issue
Block a user