fetch / fetch.py
pup-py's picture
support for clone
96de55d
from apscheduler.executors.asyncio import AsyncIOExecutor
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from fastapi import FastAPI, Request
from fastapi.responses import (
FileResponse,
PlainTextResponse,
Response,
)
from os import environ, getenv
from pathlib import Path
from typing import Any
import httpx
import json
import subprocess
LOGFILE = Path.home() / "a.json"
PUP_URLS = {
"Linux": "https://raw.githubusercontent.com/liquidcarbon/puppy/main/pup.sh",
"Windows": "https://raw.githubusercontent.com/liquidcarbon/puppy/main/pup.ps1",
}
class PrettyJSONResponse(Response):
media_type = "application/json"
def render(self, content: Any) -> bytes:
return json.dumps(content, indent=2).encode("utf-8")
@asynccontextmanager
async def lifespan(app: FastAPI):
scheduler.start()
yield
scheduler.shutdown()
app = FastAPI(lifespan=lifespan)
@app.get("/")
def read_root(request: Request) -> Response:
"""Main URL returning an executable installer script.
Query parameters can be used to install specific things, e.g.
curl -fsSL "https://pup-py-fetch.hf.space?python=3.11&pixi=marimo&myenv=cowsay,duckdb"
A slash ("/") in package name is interpreted as a GitHub repo.
Package specs "duckdb>=1.1" are not supported.
"""
query_params = dict(request.query_params)
# exclude internal endpoints
_ = query_params.pop("logs", "")
# python version
py_ver = query_params.pop("python", "3.12")
if "Windows" in request.headers.get("user-agent"):
pup_url = PUP_URLS.get("Windows")
script = [
f"$pup_ps1 = (iwr -useb {pup_url}).Content",
f"& ([scriptblock]::Create($pup_ps1)) {py_ver}",
]
hint = f"""iex (iwr "{request.url}").Content"""
else:
pup_url = PUP_URLS.get("Linux")
script = [f"curl -fsSL {pup_url} | bash -s {py_ver}"]
hint = f"""curl -fsSL "{request.url}" | bash"""
# pixi packages
pixi_packages = query_params.pop("pixi", "")
if pixi_packages:
for pkg in pixi_packages.split(","):
script.append(f"pixi add {pkg}")
# repos to be cloned
to_clone = query_params.pop("clone", "")
if to_clone:
for repo in to_clone.split(","):
if "/" in repo:
pkg = f"https://github.com/{repo}.git"
script.append(f"pup clone {pkg}")
else:
script.append(f"# can't clone `{repo}`: expected <username>/<reponame>")
# remaining query params are venvs
for venv, uv_packages in query_params.items():
for pkg in uv_packages.split(","):
if "/" in pkg: # slash implies GitHub repo
pkg = f"https://github.com/{pkg}.git"
script.append(f"pup add {venv} {pkg}")
script.extend(
[
"# ๐Ÿถ scripts end here; if you like what you see, copy-paste this recipe, or run like so:",
f"# {hint}",
"# to learn more, visit https://github.com/liquidcarbon/puppy\n",
]
)
return PlainTextResponse("\n".join(script))
@app.middleware("http")
async def log_request(request: Request, call_next: Any):
ts = datetime.now().strftime("%y%m%d%H%M%S%f")
data = {
# "day": int(ts[:6]),
"dt": int(ts[:-3]),
"url": request.url,
"query_params": request.query_params,
"user-agent": request.headers.get("user-agent"),
"client": request.headers.get("x-forwarded-for"),
"private_ip": request.client.host,
"method": request.method,
"headers": str(request.headers),
}
output = json.dumps(obj=data, default=str, indent=None, separators=(", ", ":"))
with open(LOGFILE, "a") as f:
f.write(output + "\n")
response = await call_next(request)
return response
@app.get("/a", response_class=PrettyJSONResponse)
def get_analytics(n: int = 5):
if n == 0:
cmd = f"tac {LOGFILE.as_posix()}"
else:
cmd = f"tail -n {n} {LOGFILE.as_posix()} | tac"
_subprocess = subprocess.run(cmd, shell=True, text=True, capture_output=True)
json_lines, stderr = _subprocess.stdout[:-1], _subprocess.stderr
try:
content = json.loads(f"[ {json_lines.replace("\n", ",")} ]")
return content
except Exception as e:
return {"error": str(e), "stderr": stderr, "json_lines": json_lines}
@app.api_route("/qa", response_class=FileResponse, methods=["GET", "HEAD"])
def query_analytics():
return LOGFILE.as_posix()
@app.api_route("/env", response_class=PrettyJSONResponse)
def show_env():
return environ
@app.get("/favicon.ico")
async def favicon():
return {"message": "woof!"}
@app.get("/ping")
async def ping():
return {"message": "woof!"}
def self_ping():
self_host1 = getenv("SPACE_HOST", "0.0.0.0:7860")
self_host2 = "https://huggingface.co/spaces/pup-py/fetch"
with httpx.Client() as client:
_ = client.get(f"http://{self_host1}/ping", follow_redirects=True)
_ = client.get(self_host2, follow_redirects=True)
scheduler = AsyncIOScheduler(executors={"default": AsyncIOExecutor()})
scheduler.add_job(self_ping, next_run_time=datetime.now() + timedelta(seconds=30))
scheduler.add_job(self_ping, "interval", minutes=720)
if __name__ == "__main__":
import uvicorn
fmt = "%(asctime)s %(levelprefix)s %(message)s"
uvicorn_logging = uvicorn.config.LOGGING_CONFIG
uvicorn_logging["formatters"]["access"]["datefmt"] = "%y%m%d @ %T"
uvicorn_logging["formatters"]["access"]["fmt"] = fmt
uvicorn_logging["formatters"]["default"]["datefmt"] = "%y%m%d @ %T"
uvicorn_logging["formatters"]["default"]["fmt"] = fmt
uvicorn.run(app, host="0.0.0.0", port=7860)