Spaces:
Sleeping
Sleeping
Added access to logs
Browse files
main.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import os, asyncio, httpx
|
|
|
|
| 2 |
from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
|
| 3 |
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -13,6 +14,8 @@ DEMO_FORWARD_URL = os.getenv("DEMO_FORWARD_URL", "").rstrip("/") # url to acc
|
|
| 13 |
DATFID_DEMO_TOKEN = os.getenv("DATFID_DEMO_TOKEN", "") # token to access demo space
|
| 14 |
# This Space's public URL (used to ping self while waiting so HF does not put this Space to sleep). Override with SELF_URL env if different.
|
| 15 |
SELF_URL = os.getenv("SELF_URL", "https://datfid-org-datfid-master.hf.space").rstrip("/")
|
|
|
|
|
|
|
| 16 |
|
| 17 |
if not HF_TOKEN:
|
| 18 |
raise RuntimeError("Missing secret 'hf_token' in public Space.")
|
|
@@ -478,6 +481,95 @@ async def secure_ping(req: Request):
|
|
| 478 |
raise HTTPException(status_code=401, detail="Missing Authorization Bearer token (dt+...)")
|
| 479 |
return await _forward("/secure-ping/", "GET", user_token=user_token)
|
| 480 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
@app.post("/modelfit/")
|
| 482 |
async def modelfit(req: Request):
|
| 483 |
user_token = _extract_user_token(req)
|
|
|
|
| 1 |
import os, asyncio, httpx
|
| 2 |
+
from urllib.parse import urlparse
|
| 3 |
from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
|
| 4 |
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 14 |
DATFID_DEMO_TOKEN = os.getenv("DATFID_DEMO_TOKEN", "") # token to access demo space
|
| 15 |
# This Space's public URL (used to ping self while waiting so HF does not put this Space to sleep). Override with SELF_URL env if different.
|
| 16 |
SELF_URL = os.getenv("SELF_URL", "https://datfid-org-datfid-master.hf.space").rstrip("/")
|
| 17 |
+
# Admin-only endpoints: require this secret (send as X-Admin-Pass header). Secret name in HF: Admin_pass
|
| 18 |
+
Admin_pass = os.getenv("Admin_pass", "")
|
| 19 |
|
| 20 |
if not HF_TOKEN:
|
| 21 |
raise RuntimeError("Missing secret 'hf_token' in public Space.")
|
|
|
|
| 481 |
raise HTTPException(status_code=401, detail="Missing Authorization Bearer token (dt+...)")
|
| 482 |
return await _forward("/secure-ping/", "GET", user_token=user_token)
|
| 483 |
|
| 484 |
+
|
| 485 |
+
def _require_admin_token(request: Request) -> None:
|
| 486 |
+
"""Raise 401 if Admin_pass is not set or X-Admin-Pass does not match."""
|
| 487 |
+
if not Admin_pass:
|
| 488 |
+
raise HTTPException(status_code=503, detail="Admin endpoints not configured (set Admin_pass in secrets).")
|
| 489 |
+
token = request.headers.get("X-Admin-Pass", "").strip()
|
| 490 |
+
if not token or token != Admin_pass:
|
| 491 |
+
raise HTTPException(status_code=401, detail="Invalid or missing X-Admin-Pass.")
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def _hf_space_url_to_repo(url: str) -> str | None:
|
| 495 |
+
"""Derive HF repo (org/space) from a Space URL (e.g. https://....hf.space). Returns None if not derivable."""
|
| 496 |
+
if not url or ".hf.space" not in url:
|
| 497 |
+
return None
|
| 498 |
+
try:
|
| 499 |
+
host = urlparse(url).netloc
|
| 500 |
+
base = host.replace(".hf.space", "").split(".")[0]
|
| 501 |
+
parts = base.split("-")
|
| 502 |
+
if len(parts) >= 4:
|
| 503 |
+
return f"{parts[0]}-{parts[1]}/{parts[2]}-{parts[3]}"
|
| 504 |
+
if len(parts) >= 2:
|
| 505 |
+
return f"{parts[0]}/{parts[1]}"
|
| 506 |
+
except Exception:
|
| 507 |
+
pass
|
| 508 |
+
return None
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
def _resolve_space_repo(space: str) -> str:
|
| 512 |
+
"""Resolve space key to HF repo (org/space). space must be 'upstream'|'demo'|'self'. Uses existing URLs, no names in code."""
|
| 513 |
+
# Optional env overrides for repo (e.g. HF_LOGS_REPO_UPSTREAM=org/space_name)
|
| 514 |
+
repo = os.getenv(f"HF_LOGS_REPO_{space.upper()}") if space else ""
|
| 515 |
+
if repo:
|
| 516 |
+
return repo.strip()
|
| 517 |
+
url_map = {"upstream": UPSTREAM_URL, "demo": DEMO_FORWARD_URL, "self": SELF_URL}
|
| 518 |
+
if not space or space not in url_map:
|
| 519 |
+
raise HTTPException(
|
| 520 |
+
status_code=400,
|
| 521 |
+
detail="Invalid or missing 'space'. Use: upstream, demo, or self.",
|
| 522 |
+
)
|
| 523 |
+
url = url_map[space]
|
| 524 |
+
if not url and space == "demo":
|
| 525 |
+
raise HTTPException(status_code=400, detail="Demo not configured (DEMO_FORWARD_URL).")
|
| 526 |
+
if not url:
|
| 527 |
+
raise HTTPException(status_code=400, detail="Missing URL for this space key.")
|
| 528 |
+
repo = _hf_space_url_to_repo(url)
|
| 529 |
+
if not repo:
|
| 530 |
+
raise HTTPException(
|
| 531 |
+
status_code=400,
|
| 532 |
+
detail=f"Could not derive repo from URL. Set HF_LOGS_REPO_{space.upper()} in env.",
|
| 533 |
+
)
|
| 534 |
+
return repo
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
async def _stream_hf_logs(log_type: str, space_repo: str):
|
| 538 |
+
"""Stream logs from HF API (run or build). space_repo e.g. DATFID-org/datfid_api."""
|
| 539 |
+
url = f"https://huggingface.co/api/spaces/{space_repo}/logs/{log_type}"
|
| 540 |
+
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
|
| 541 |
+
client = httpx.AsyncClient(timeout=60.0)
|
| 542 |
+
stream_ctx = client.stream("GET", url, headers=headers)
|
| 543 |
+
|
| 544 |
+
async def body_iter():
|
| 545 |
+
try:
|
| 546 |
+
async with stream_ctx as resp:
|
| 547 |
+
if resp.status_code != 200:
|
| 548 |
+
body = await resp.aread()
|
| 549 |
+
raise HTTPException(status_code=resp.status_code, detail=body[:500].decode(errors="replace"))
|
| 550 |
+
async for chunk in resp.aiter_bytes():
|
| 551 |
+
yield chunk
|
| 552 |
+
finally:
|
| 553 |
+
await client.aclose()
|
| 554 |
+
|
| 555 |
+
return StreamingResponse(body_iter(), media_type="text/plain; charset=utf-8")
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
@app.get("/admin/logs/run")
|
| 559 |
+
async def admin_logs_run(req: Request, space: str = "upstream"):
|
| 560 |
+
"""Admin only: stream run logs. Requires X-Admin-Pass header. Query: space=upstream|demo|self (keys for hf_url, DEMO_FORWARD_URL, SELF_URL)."""
|
| 561 |
+
_require_admin_token(req)
|
| 562 |
+
space_repo = _resolve_space_repo(space)
|
| 563 |
+
return await _stream_hf_logs("run", space_repo)
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
@app.get("/admin/logs/build")
|
| 567 |
+
async def admin_logs_build(req: Request, space: str = "upstream"):
|
| 568 |
+
"""Admin only: stream build logs. Requires X-Admin-Pass header. Query: space=upstream|demo|self."""
|
| 569 |
+
_require_admin_token(req)
|
| 570 |
+
space_repo = _resolve_space_repo(space)
|
| 571 |
+
return await _stream_hf_logs("build", space_repo)
|
| 572 |
+
|
| 573 |
@app.post("/modelfit/")
|
| 574 |
async def modelfit(req: Request):
|
| 575 |
user_token = _extract_user_token(req)
|