File size: 5,842 Bytes
8c290e6
 
 
a06e733
bceb9b7
a0b1478
 
 
 
 
4731ef4
058dfe9
bceb9b7
 
8c290e6
bceb9b7
 
 
058dfe9
a06e733
 
 
 
058dfe9
bceb9b7
 
 
 
 
 
 
 
8c290e6
 
 
 
 
 
 
 
bceb9b7
 
 
a0b1478
26bf52a
 
 
 
 
96de55d
26bf52a
 
 
c2219a8
a0b1478
96de55d
 
 
a0b1478
c2219a8
 
a06e733
26bf52a
 
 
 
04d1a17
c2219a8
a06e733
c2219a8
04d1a17
c2219a8
a0b1478
c2219a8
 
 
 
 
96de55d
 
 
 
 
 
 
 
 
 
c2219a8
 
 
96de55d
a0b1478
c2219a8
 
04d1a17
 
 
 
 
 
 
c2219a8
bceb9b7
 
7de90bf
 
 
 
 
 
 
 
 
 
 
 
 
 
14f478a
7de90bf
14f478a
7de90bf
 
 
 
 
bceb9b7
 
 
364dead
bceb9b7
6d90cda
 
1594f3a
14f478a
6d90cda
14f478a
 
 
bceb9b7
 
 
 
058dfe9
8c290e6
 
4731ef4
 
 
 
 
8c290e6
 
 
 
 
 
 
 
 
 
2c8258c
a06e733
 
2c8258c
a06e733
 
a0b1478
8c290e6
 
a06e733
d15e74f
058dfe9
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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)