Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import inspect | |
| import json | |
| import logging | |
| import math | |
| import secrets | |
| import tempfile | |
| import threading | |
| from collections.abc import Callable | |
| from pathlib import Path | |
| from typing import Any, get_args, get_origin | |
| from urllib.parse import unquote | |
| from starlette.applications import Starlette | |
| from starlette.requests import Request | |
| from starlette.responses import FileResponse, JSONResponse, Response, StreamingResponse | |
| from starlette.routing import Route | |
| from trackio import utils | |
| from trackio.exceptions import TrackioAPIError | |
| from trackio.remote_client import HTTP_API_VERSION | |
| logger = logging.getLogger("trackio.asgi_app") | |
| _PACKAGE_JSON_PATH = Path(__file__).parent / "package.json" | |
| _TRACKIO_PACKAGE_VERSION = json.loads(_PACKAGE_JSON_PATH.read_text())["version"] | |
| def _normalize_allowed_file_roots( | |
| allowed_file_roots: list[str | Path] | None, | |
| ) -> tuple[Path, ...]: | |
| roots = [] | |
| for root in allowed_file_roots or []: | |
| roots.append(Path(root).resolve()) | |
| return tuple(roots) | |
| def _is_allowed_file_path(path: Path, allowed_roots: tuple[Path, ...]) -> bool: | |
| resolved_path = path.resolve(strict=False) | |
| for root in allowed_roots: | |
| try: | |
| resolved_path.relative_to(root) | |
| return True | |
| except ValueError: | |
| continue | |
| return False | |
| def _json_safe(data: Any) -> Any: | |
| if data is None or isinstance(data, (str, bool, int)): | |
| return data | |
| if isinstance(data, float): | |
| return data if math.isfinite(data) else None | |
| if isinstance(data, dict): | |
| return {k: _json_safe(v) for k, v in data.items()} | |
| if isinstance(data, (list, tuple)): | |
| return [_json_safe(v) for v in data] | |
| if hasattr(data, "item"): | |
| try: | |
| return _json_safe(data.item()) | |
| except Exception: | |
| pass | |
| return str(data) | |
| def register_uploaded_temp_file(request: Request, file_path: str | Path) -> None: | |
| resolved_path = Path(file_path).resolve(strict=False) | |
| with request.app.state.uploaded_temp_files_lock: | |
| request.app.state.uploaded_temp_files.add(resolved_path) | |
| def consume_uploaded_temp_file(request: Request, file_data: Any) -> Path: | |
| file_path = file_data.get("path") if isinstance(file_data, dict) else None | |
| if not isinstance(file_path, str) or not file_path: | |
| raise TrackioAPIError("Expected uploaded file metadata with a valid path.") | |
| resolved_path = Path(file_path).resolve(strict=False) | |
| with request.app.state.uploaded_temp_files_lock: | |
| if resolved_path not in request.app.state.uploaded_temp_files: | |
| raise TrackioAPIError( | |
| "Uploaded file was not created by this Trackio server." | |
| ) | |
| request.app.state.uploaded_temp_files.remove(resolved_path) | |
| if not resolved_path.is_file(): | |
| raise TrackioAPIError("Uploaded file is missing.") | |
| return resolved_path | |
| def cleanup_uploaded_temp_file(file_path: str | Path) -> None: | |
| try: | |
| Path(file_path).unlink(missing_ok=True) | |
| except Exception: | |
| pass | |
| def _invoke_handler( | |
| fn: Any, | |
| request: Request, | |
| args: list[Any] | None = None, | |
| kwargs: dict[str, Any] | None = None, | |
| ) -> Any: | |
| sig = inspect.signature(fn) | |
| params = list(sig.parameters.values()) | |
| positional_args: list[Any] = [] | |
| keyword_args: dict[str, Any] = {} | |
| args = list(args or []) | |
| kwargs = dict(kwargs or {}) | |
| data_index = 0 | |
| for param in params: | |
| if param.name == "request": | |
| keyword_args["request"] = request | |
| elif param.kind == inspect.Parameter.VAR_POSITIONAL: | |
| positional_args.extend(args[data_index:]) | |
| data_index = len(args) | |
| elif param.kind == inspect.Parameter.VAR_KEYWORD: | |
| keyword_args.update(kwargs) | |
| kwargs.clear() | |
| elif param.name in kwargs: | |
| keyword_args[param.name] = kwargs.pop(param.name) | |
| elif param.kind in ( | |
| inspect.Parameter.POSITIONAL_ONLY, | |
| inspect.Parameter.POSITIONAL_OR_KEYWORD, | |
| ) and data_index < len(args): | |
| positional_args.append(args[data_index]) | |
| data_index += 1 | |
| elif param.default is inspect.Signature.empty and param.kind not in ( | |
| inspect.Parameter.VAR_POSITIONAL, | |
| inspect.Parameter.VAR_KEYWORD, | |
| ): | |
| raise TrackioAPIError(f"Missing required parameter: {param.name}") | |
| return fn(*positional_args, **keyword_args) | |
| async def version_handler(request: Request) -> Response: | |
| mcp_enabled = bool(getattr(request.app.state, "mcp_enabled", False)) | |
| return JSONResponse( | |
| { | |
| "version": _TRACKIO_PACKAGE_VERSION, | |
| "api_version": HTTP_API_VERSION, | |
| "api_transport": "http", | |
| "mcp_enabled": mcp_enabled, | |
| "mcp_path": "/mcp" if mcp_enabled else None, | |
| } | |
| ) | |
| def _json_schema_and_python_type(annotation: Any) -> tuple[dict[str, Any], str]: | |
| if annotation is inspect.Parameter.empty: | |
| return {"type": "object"}, "Any" | |
| origin = get_origin(annotation) | |
| args = get_args(annotation) | |
| if origin is not None and args: | |
| non_none = tuple(a for a in args if a is not type(None)) | |
| if len(non_none) == 1 and len(args) > 1: | |
| return _json_schema_and_python_type(non_none[0]) | |
| if origin is list: | |
| inner, py_inner = _json_schema_and_python_type( | |
| args[0] if args else inspect.Parameter.empty | |
| ) | |
| return {"type": "array", "items": inner}, f"list[{py_inner}]" | |
| if origin is dict: | |
| return {"type": "object"}, "dict" | |
| if annotation in (str, bytes): | |
| return {"type": "string"}, "str" | |
| if annotation is int: | |
| return {"type": "integer"}, "int" | |
| if annotation is float: | |
| return {"type": "number"}, "float" | |
| if annotation is bool: | |
| return {"type": "boolean"}, "bool" | |
| name = getattr(annotation, "__name__", None) | |
| if name: | |
| return {"type": "object"}, name | |
| return {"type": "object"}, "Any" | |
| def build_gradio_api_info(api_registry: dict[str, Any]) -> dict[str, Any]: | |
| named_endpoints: dict[str, Any] = {} | |
| for name in sorted(api_registry.keys()): | |
| fn = api_registry[name] | |
| if not callable(fn): | |
| continue | |
| sig = inspect.signature(fn) | |
| parameters: list[dict[str, Any]] = [] | |
| for pname, param in sig.parameters.items(): | |
| if pname == "request": | |
| continue | |
| jtype, pytype = _json_schema_and_python_type(param.annotation) | |
| has_default = param.default is not inspect.Parameter.empty | |
| parameters.append( | |
| { | |
| "label": pname, | |
| "parameter_name": pname, | |
| "parameter_has_default": has_default, | |
| "parameter_default": None if not has_default else param.default, | |
| "type": jtype, | |
| "python_type": {"type": pytype, "description": ""}, | |
| "component": "Api", | |
| "example_input": None, | |
| } | |
| ) | |
| ret_ann = sig.return_annotation | |
| if ret_ann is inspect.Signature.empty: | |
| ret_ann = Any | |
| rjtype, rpytype = _json_schema_and_python_type(ret_ann) | |
| returns = [ | |
| { | |
| "label": "result", | |
| "type": rjtype, | |
| "python_type": {"type": rpytype, "description": ""}, | |
| "component": "Api", | |
| } | |
| ] | |
| named_endpoints[f"/{name}"] = { | |
| "parameters": parameters, | |
| "returns": returns, | |
| "api_visibility": "public", | |
| } | |
| return {"named_endpoints": named_endpoints, "unnamed_endpoints": {}} | |
| _MAX_GRADIO_CALL_EVENTS = 256 | |
| def _hf_token_value_is_unset(value: Any) -> bool: | |
| if value is None: | |
| return True | |
| if isinstance(value, str) and value.strip() == "": | |
| return True | |
| return False | |
| def _authorization_bearer_token(request: Request) -> str | None: | |
| auth = request.headers.get("authorization") or request.headers.get("Authorization") | |
| if not auth: | |
| return None | |
| parts = auth.split() | |
| if len(parts) != 2 or parts[0].lower() != "bearer": | |
| return None | |
| tok = parts[1].strip() | |
| return tok or None | |
| def _maybe_apply_hf_token_from_authorization( | |
| request: Request, fn: Any, args: list[Any], kwargs: dict[str, Any] | |
| ) -> None: | |
| if not utils.on_spaces(): | |
| return | |
| token = _authorization_bearer_token(request) | |
| if not token: | |
| return | |
| sig = inspect.signature(fn) | |
| if "hf_token" not in sig.parameters: | |
| return | |
| params = [p for p in sig.parameters.values() if p.name != "request"] | |
| names = [p.name for p in params] | |
| if "hf_token" not in names: | |
| return | |
| idx = names.index("hf_token") | |
| if "hf_token" in kwargs: | |
| if _hf_token_value_is_unset(kwargs["hf_token"]): | |
| kwargs["hf_token"] = token | |
| return | |
| if idx < len(args): | |
| if _hf_token_value_is_unset(args[idx]): | |
| args[idx] = token | |
| return | |
| kwargs["hf_token"] = token | |
| def _store_gradio_call_result( | |
| request: Request, event_id: str, api_name: str, data: Any | |
| ) -> None: | |
| with request.app.state.gradio_call_events_lock: | |
| d = request.app.state.gradio_call_events | |
| while len(d) >= _MAX_GRADIO_CALL_EVENTS: | |
| d.pop(next(iter(d))) | |
| d[event_id] = {"api_name": api_name, "data": data} | |
| async def run_api_request(request: Request, api_name: str) -> Response: | |
| api_registry = request.app.state.api_registry | |
| fn = api_registry.get(api_name) | |
| if fn is None: | |
| return JSONResponse({"error": f"Unknown API: {api_name}"}, status_code=404) | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| body = {} | |
| args: list[Any] = [] | |
| kwargs: dict[str, Any] = {} | |
| if isinstance(body, dict): | |
| if "args" in body or "kwargs" in body: | |
| args = body.get("args") or [] | |
| kwargs = body.get("kwargs") or {} | |
| elif "data" in body and isinstance(body["data"], list): | |
| args = body["data"] | |
| else: | |
| kwargs = body | |
| elif isinstance(body, list): | |
| args = body | |
| elif body is not None: | |
| args = [body] | |
| if not isinstance(args, list): | |
| args = [args] | |
| if not isinstance(kwargs, dict): | |
| kwargs = {} | |
| _maybe_apply_hf_token_from_authorization(request, fn, args, kwargs) | |
| try: | |
| result = _invoke_handler(fn, request, args=args, kwargs=kwargs) | |
| return JSONResponse({"data": _json_safe(result)}) | |
| except TrackioAPIError as e: | |
| return JSONResponse({"error": str(e)}, status_code=400) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def api_handler(request: Request) -> Response: | |
| return await run_api_request(request, request.path_params["api_name"]) | |
| async def gradio_api_info_handler(request: Request) -> Response: | |
| return JSONResponse( | |
| build_gradio_api_info(request.app.state.api_registry), | |
| headers={"Cache-Control": "no-store"}, | |
| ) | |
| async def gradio_call_post_handler(request: Request) -> Response: | |
| api_name = request.path_params["api_name"] | |
| resp = await run_api_request(request, api_name) | |
| if resp.status_code != 200: | |
| return resp | |
| body = json.loads(bytes(resp.body).decode()) | |
| event_id = secrets.token_urlsafe(16) | |
| _store_gradio_call_result(request, event_id, api_name, body["data"]) | |
| return JSONResponse({"event_id": event_id}) | |
| async def gradio_call_poll_handler(request: Request) -> Response: | |
| api_name = request.path_params["api_name"] | |
| event_id = request.path_params["event_id"] | |
| with request.app.state.gradio_call_events_lock: | |
| event = request.app.state.gradio_call_events.pop(event_id, None) | |
| if event is None: | |
| logger.info("gradio_api poll: unknown or expired event_id") | |
| return JSONResponse({"error": "Unknown or expired event_id"}, status_code=404) | |
| if event.get("api_name") != api_name: | |
| logger.info( | |
| "gradio_api poll: api_name mismatch (path=%r, stored=%r)", | |
| api_name, | |
| event.get("api_name"), | |
| ) | |
| with request.app.state.gradio_call_events_lock: | |
| d = request.app.state.gradio_call_events | |
| while len(d) >= _MAX_GRADIO_CALL_EVENTS: | |
| d.pop(next(iter(d))) | |
| d[event_id] = event | |
| return JSONResponse({"error": "Unknown or expired event_id"}, status_code=404) | |
| data = event["data"] | |
| payload = json.dumps(_json_safe([data])) | |
| async def sse() -> Any: | |
| yield f"event: complete\ndata: {payload}\n\n" | |
| return StreamingResponse( | |
| sse(), | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-store", | |
| "X-Accel-Buffering": "no", | |
| }, | |
| ) | |
| async def upload_handler(request: Request) -> Response: | |
| upload_authorizer = getattr(request.app.state, "upload_authorizer", None) | |
| if callable(upload_authorizer): | |
| try: | |
| upload_authorizer(request) | |
| except TrackioAPIError as e: | |
| return JSONResponse({"error": str(e)}, status_code=400) | |
| form = await request.form() | |
| uploads = form.getlist("files") | |
| saved_paths = [] | |
| for upload in uploads: | |
| suffix = Path(getattr(upload, "filename", "") or "").suffix | |
| with tempfile.NamedTemporaryFile( | |
| delete=False, | |
| prefix="trackio-upload-", | |
| suffix=suffix, | |
| ) as tmp: | |
| tmp.write(await upload.read()) | |
| register_uploaded_temp_file(request, tmp.name) | |
| saved_paths.append(tmp.name) | |
| return JSONResponse({"paths": saved_paths}) | |
| async def gradio_upload_alias_handler(request: Request) -> Response: | |
| return await upload_handler(request) | |
| _DISALLOWED_FILE_SUFFIXES = frozenset( | |
| {".db", ".db-journal", ".db-wal", ".db-shm", ".sqlite", ".sqlite3"} | |
| ) | |
| async def file_handler(request: Request) -> Response: | |
| fs_path = request.query_params.get("path") | |
| if fs_path is None: | |
| return Response("Missing path", status_code=400) | |
| fp = Path(unquote(fs_path)).resolve(strict=False) | |
| if fp.suffix.lower() in _DISALLOWED_FILE_SUFFIXES: | |
| return Response("Not found", status_code=404) | |
| if _is_allowed_file_path(fp, (utils.ARTIFACTS_DIR.resolve(),)): | |
| return Response("Not found", status_code=404) | |
| allowed_roots = getattr(request.app.state, "allowed_file_roots", ()) | |
| if fp.is_file() and _is_allowed_file_path(fp, allowed_roots): | |
| return FileResponse(str(fp)) | |
| return Response("Not found", status_code=404) | |
| async def artifact_blob_handler(request: Request) -> Response: | |
| from trackio import cas # noqa: PLC0415 | |
| from trackio import server as _server # noqa: PLC0415 | |
| project = request.path_params.get("project") | |
| digest = request.path_params.get("digest") | |
| try: | |
| project = _server._validate_project_name(project) | |
| digest = _server._validate_sha256_digest(digest) | |
| except TrackioAPIError: | |
| return Response("Not found", status_code=404) | |
| blob = cas.blob_path(project, digest) | |
| if not blob.is_file(): | |
| return Response("Not found", status_code=404) | |
| return FileResponse(str(blob)) | |
| def create_trackio_starlette_app( | |
| oauth_routes: list[Route], | |
| api_registry: dict[str, Any], | |
| extra_routes: list[Any] | None = None, | |
| mcp_lifespan: Any = None, | |
| mcp_enabled: bool = False, | |
| allowed_file_roots: list[str | Path] | None = None, | |
| upload_authorizer: Callable[[Request], None] | None = None, | |
| ) -> Starlette: | |
| routes: list[Any] = list(oauth_routes) | |
| routes.extend( | |
| [ | |
| Route("/version", endpoint=version_handler, methods=["GET"]), | |
| Route("/api/upload", endpoint=upload_handler, methods=["POST"]), | |
| Route("/api/{api_name:str}", endpoint=api_handler, methods=["POST"]), | |
| Route("/file", endpoint=file_handler, methods=["GET"]), | |
| Route( | |
| "/artifact_blob/{project:str}/{digest:str}", | |
| endpoint=artifact_blob_handler, | |
| methods=["GET"], | |
| ), | |
| ] | |
| ) | |
| if utils.on_spaces(): | |
| routes.extend( | |
| [ | |
| Route( | |
| "/gradio_api/info", | |
| endpoint=gradio_api_info_handler, | |
| methods=["GET"], | |
| ), | |
| Route( | |
| "/gradio_api/info/", | |
| endpoint=gradio_api_info_handler, | |
| methods=["GET"], | |
| ), | |
| Route( | |
| "/gradio_api/upload", | |
| endpoint=gradio_upload_alias_handler, | |
| methods=["POST"], | |
| ), | |
| Route( | |
| "/gradio_api/upload/", | |
| endpoint=gradio_upload_alias_handler, | |
| methods=["POST"], | |
| ), | |
| Route( | |
| "/gradio_api/call/{api_name:str}", | |
| endpoint=gradio_call_post_handler, | |
| methods=["POST"], | |
| ), | |
| Route( | |
| "/gradio_api/call/{api_name:str}/", | |
| endpoint=gradio_call_post_handler, | |
| methods=["POST"], | |
| ), | |
| Route( | |
| "/gradio_api/call/{api_name:str}/{event_id:str}", | |
| endpoint=gradio_call_poll_handler, | |
| methods=["GET"], | |
| ), | |
| Route( | |
| "/gradio_api/call/{api_name:str}/{event_id:str}/", | |
| endpoint=gradio_call_poll_handler, | |
| methods=["GET"], | |
| ), | |
| ] | |
| ) | |
| routes.extend(extra_routes or []) | |
| app = Starlette(routes=routes, lifespan=mcp_lifespan) | |
| app.state.api_registry = api_registry | |
| app.state.mcp_enabled = mcp_enabled | |
| app.state.allowed_file_roots = _normalize_allowed_file_roots(allowed_file_roots) | |
| app.state.upload_authorizer = upload_authorizer | |
| app.state.uploaded_temp_files = set() | |
| app.state.uploaded_temp_files_lock = threading.Lock() | |
| if utils.on_spaces(): | |
| app.state.gradio_call_events = {} | |
| app.state.gradio_call_events_lock = threading.Lock() | |
| return app | |