pup-py commited on
Commit
da6aa2c
·
verified ·
1 Parent(s): 92e00a9

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. .gitignore +3 -0
  2. Dockerfile +27 -0
  3. README.md +33 -7
  4. fetch.py +186 -0
  5. pyproject.toml +16 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__
2
+ *.json
3
+ uv.lock
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM debian:latest AS puppy
2
+
3
+ # prep
4
+ RUN apt-get update \
5
+ && apt-get install -y curl \
6
+ && apt-get clean
7
+ RUN useradd -m -u 1000 user
8
+ USER user
9
+
10
+ ENV PATH=/home/user/.pixi/bin:$PATH
11
+ RUN mkdir $HOME/puppy
12
+ WORKDIR $HOME/puppy
13
+
14
+ # install puppy
15
+ RUN curl -fsSL https://raw.githubusercontent.com/liquidcarbon/puppy/main/pup.sh | bash -s 3.13
16
+ RUN pup --help
17
+
18
+ # setup app env
19
+ RUN pup new appenv
20
+ WORKDIR appenv
21
+
22
+ COPY --chown=user ./* .
23
+ RUN pixi run uv sync --project .
24
+
25
+ # HELLO WORLD
26
+ EXPOSE 7860
27
+ CMD .venv/bin/python -m fetch
README.md CHANGED
@@ -1,12 +1,38 @@
1
  ---
2
  title: Fetch
3
- emoji: 📈
4
- colorFrom: purple
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 5.47.2
8
- app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Fetch
3
+ emoji: 👀
4
+ colorFrom: green
5
+ colorTo: yellow
6
+ sdk: docker
 
 
7
  pinned: false
8
+ license: wtfpl
9
+ short_description: Puppy installer
10
  ---
11
 
12
+ # Puppy Installer
13
+
14
+ See https://github.com/liquidcarbon/puppy
15
+
16
+ ## One Installer To Rule Them All
17
+
18
+ The `pup-py-fetch` API accepts query parameters that allow specifying the exact environment recipe you want to build:
19
+ - `python`: [3.10](https://pup-py-fetch.hf.space?python=3.10) through [3.13](https://pup-py-fetch.hf.space?python=3.13)
20
+ - `pixi`: [comma-separated list of pixi/Conda dependencies](https://pup-py-fetch.hf.space?pixi=jupyter,quarto)
21
+ - `clone`: [comma-separated list of GitHub repos to clone and install](https://pup-py-fetch.hf.space?clone=marimo-team/marimo) (only GitHub at this time)
22
+ - virtual environments: [all other query parameters with comma-separated package names](https://pup-py-fetch.hf.space?env1=duckdb,pandas&env2=cowsay), including:
23
+ - regular PyPI packages (no support for version pinning at this time)
24
+ - packages from GitHub repos using <username>/<reponame>
25
+
26
+ > [!NOTE]
27
+ > As of Dec 2024, many packages still do not support python 3.13; thus, the default version in puppy is 3.12.
28
+
29
+
30
+ Visiting the URLs above returns the installation scripts. You can mix and match query parameters, unlocking single-command recipes for complex builds:
31
+
32
+ ```bash
33
+ curl -fsSL "https://pup-py-fetch.hf.space?pixi=marimo&env1=duckdb,pandas&env2=cowsay" | bash
34
+ ```
35
+
36
+ ```powershell
37
+ iex (iwr "https://pup-py-fetch.hf.space?python=3.11&pixi=marimo&tables=duckdb,polars,liquidcarbon/affinity").Content
38
+ ```
fetch.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from apscheduler.executors.asyncio import AsyncIOExecutor
2
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
3
+ from contextlib import asynccontextmanager
4
+ from datetime import datetime, timedelta
5
+ from fastapi import FastAPI, Request
6
+ from fastapi.responses import (
7
+ FileResponse,
8
+ PlainTextResponse,
9
+ Response,
10
+ )
11
+ from os import environ, getenv
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import httpx
16
+ import json
17
+ import subprocess
18
+
19
+ LOGFILE = Path.home() / "a.json"
20
+ PUP_URLS = {
21
+ "Linux": "https://raw.githubusercontent.com/liquidcarbon/puppy/main/pup.sh",
22
+ "Windows": "https://raw.githubusercontent.com/liquidcarbon/puppy/main/pup.ps1",
23
+ }
24
+
25
+
26
+ class PrettyJSONResponse(Response):
27
+ media_type = "application/json"
28
+
29
+ def render(self, content: Any) -> bytes:
30
+ return json.dumps(content, indent=2).encode("utf-8")
31
+
32
+
33
+ @asynccontextmanager
34
+ async def lifespan(app: FastAPI):
35
+ scheduler.start()
36
+ yield
37
+ scheduler.shutdown()
38
+
39
+
40
+ # app = FastAPI(lifespan=lifespan) # disable for now
41
+ app = FastAPI()
42
+
43
+
44
+ @app.get("/")
45
+ def read_root(request: Request) -> Response:
46
+ """Main URL returning an executable installer script.
47
+
48
+ Query parameters can be used to install specific things, e.g.
49
+ curl -fsSL "https://pup-py-fetch.hf.space?python=3.11&pixi=marimo&myenv=cowsay,duckdb"
50
+
51
+ A slash ("/") in package name is interpreted as a GitHub repo.
52
+ Package specs "duckdb>=1.1" are not supported.
53
+ """
54
+
55
+ query_params = dict(request.query_params)
56
+
57
+ # exclude internal endpoints
58
+ _ = query_params.pop("logs", "")
59
+
60
+ # python version
61
+ py_ver = query_params.pop("python", "3.12")
62
+ if "Windows" in request.headers.get("user-agent"):
63
+ pup_url = PUP_URLS.get("Windows")
64
+ script = [
65
+ f"$pup_ps1 = (iwr -useb {pup_url}).Content",
66
+ f"& ([scriptblock]::Create($pup_ps1)) {py_ver}",
67
+ ]
68
+ hint = f"""iex (iwr "{request.url}").Content"""
69
+ else:
70
+ pup_url = PUP_URLS.get("Linux")
71
+ script = [f"curl -fsSL {pup_url} | bash -s {py_ver}"]
72
+ hint = f"""curl -fsSL "{request.url}" | bash"""
73
+
74
+ # pixi packages
75
+ pixi_packages = query_params.pop("pixi", "")
76
+ if pixi_packages:
77
+ for pkg in pixi_packages.split(","):
78
+ script.append(f"pixi add {pkg}")
79
+
80
+ # repos to be cloned
81
+ to_clone = query_params.pop("clone", "")
82
+ if to_clone:
83
+ for repo in to_clone.split(","):
84
+ if "/" in repo:
85
+ pkg = f"https://github.com/{repo}.git"
86
+ script.append(f"pup clone {pkg}")
87
+ else:
88
+ script.append(f"# can't clone `{repo}`: expected <username>/<reponame>")
89
+
90
+ # remaining query params are venvs
91
+ for venv, uv_packages in query_params.items():
92
+ for pkg in uv_packages.split(","):
93
+ if "/" in pkg: # slash implies GitHub repo
94
+ pkg = f"https://github.com/{pkg}.git"
95
+ script.append(f"pup add {venv} {pkg}")
96
+
97
+ script.extend(
98
+ [
99
+ "# 🐶 scripts end here; if you like what you see, copy-paste this recipe, or run like so:",
100
+ f"# {hint}",
101
+ "# to learn more, visit https://github.com/liquidcarbon/puppy\n",
102
+ ]
103
+ )
104
+ return PlainTextResponse("\n".join(script))
105
+
106
+
107
+ @app.middleware("http")
108
+ async def log_request(request: Request, call_next: Any):
109
+ ts = datetime.now().strftime("%y%m%d%H%M%S%f")
110
+ data = {
111
+ # "day": int(ts[:6]),
112
+ "dt": int(ts[:-3]),
113
+ "url": request.url,
114
+ "query_params": request.query_params,
115
+ "user-agent": request.headers.get("user-agent"),
116
+ "client": request.headers.get("x-forwarded-for"),
117
+ "private_ip": request.client.host,
118
+ "method": request.method,
119
+ "headers": str(request.headers),
120
+ }
121
+ output = json.dumps(obj=data, default=str, indent=None, separators=(", ", ":"))
122
+ with open(LOGFILE, "a") as f:
123
+ f.write(output + "\n")
124
+
125
+ response = await call_next(request)
126
+ return response
127
+
128
+
129
+ @app.get("/a", response_class=PrettyJSONResponse)
130
+ def get_analytics(n: int = 5):
131
+ if n == 0:
132
+ cmd = f"tac {LOGFILE.as_posix()}"
133
+ else:
134
+ cmd = f"tail -n {n} {LOGFILE.as_posix()} | tac"
135
+ _subprocess = subprocess.run(cmd, shell=True, text=True, capture_output=True)
136
+ json_lines, stderr = _subprocess.stdout[:-1], _subprocess.stderr
137
+ try:
138
+ content = json.loads(f"[ {json_lines.replace("\n", ",")} ]")
139
+ return content
140
+ except Exception as e:
141
+ return {"error": str(e), "stderr": stderr, "json_lines": json_lines}
142
+
143
+
144
+ @app.api_route("/qa", response_class=FileResponse, methods=["GET", "HEAD"])
145
+ def query_analytics():
146
+ return LOGFILE.as_posix()
147
+
148
+
149
+ @app.api_route("/env", response_class=PrettyJSONResponse)
150
+ def show_env():
151
+ return environ
152
+
153
+
154
+ @app.get("/favicon.ico")
155
+ async def favicon():
156
+ return {"message": "woof!"}
157
+
158
+
159
+ @app.get("/ping")
160
+ async def ping():
161
+ return {"message": "woof!"}
162
+
163
+
164
+ def self_ping():
165
+ self_host1 = getenv("SPACE_HOST", "0.0.0.0:7860")
166
+ self_host2 = "https://huggingface.co/spaces/pup-py/fetch"
167
+ with httpx.Client() as client:
168
+ _ = client.get(f"http://{self_host1}/ping", follow_redirects=True)
169
+ _ = client.get(self_host2, follow_redirects=True)
170
+
171
+
172
+ scheduler = AsyncIOScheduler(executors={"default": AsyncIOExecutor()})
173
+ scheduler.add_job(self_ping, next_run_time=datetime.now() + timedelta(seconds=30))
174
+ scheduler.add_job(self_ping, "interval", minutes=720)
175
+
176
+
177
+ if __name__ == "__main__":
178
+ import uvicorn
179
+
180
+ fmt = "%(asctime)s %(levelprefix)s %(message)s"
181
+ uvicorn_logging = uvicorn.config.LOGGING_CONFIG
182
+ uvicorn_logging["formatters"]["access"]["datefmt"] = "%y%m%d @ %T"
183
+ uvicorn_logging["formatters"]["access"]["fmt"] = fmt
184
+ uvicorn_logging["formatters"]["default"]["datefmt"] = "%y%m%d @ %T"
185
+ uvicorn_logging["formatters"]["default"]["fmt"] = fmt
186
+ uvicorn.run(app, host="0.0.0.0", port=7860)
pyproject.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "fetch"
3
+ version = "0.7.0"
4
+ description = "Puppy Installer"
5
+ authors = [
6
+ { name = "Alex Kislukhin" }
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "apscheduler>=3.11.0",
12
+ "fastapi[standard]>=0.115.6",
13
+ ]
14
+
15
+ [project.urls]
16
+ homepage = "https://github.com/liquidcarbon/puppy"