gcharanteja commited on
Commit ·
fa6bb79
1
Parent(s): 0378642
Run FastAPI + Gradio together in one HF Space
Browse files- Dockerfile +3 -10
- README.md +49 -38
- app_gradio.py +130 -0
- pyproject.toml +2 -0
- start.sh +25 -0
Dockerfile
CHANGED
|
@@ -5,21 +5,14 @@ WORKDIR /app
|
|
| 5 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 6 |
PYTHONUNBUFFERED=1
|
| 7 |
|
| 8 |
-
# Install uv
|
| 9 |
RUN pip install --no-cache-dir uv
|
| 10 |
|
| 11 |
-
# Copy dependency metadata first (better caching)
|
| 12 |
COPY pyproject.toml uv.lock ./
|
| 13 |
-
|
| 14 |
-
# Install dependencies into the system interpreter
|
| 15 |
RUN uv sync --frozen --no-dev
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
# If you have other files later, copy them too:
|
| 20 |
-
# COPY . .
|
| 21 |
|
| 22 |
-
# HF Spaces uses $PORT (usually 7860)
|
| 23 |
EXPOSE 7860
|
| 24 |
|
| 25 |
-
CMD ["
|
|
|
|
| 5 |
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 6 |
PYTHONUNBUFFERED=1
|
| 7 |
|
|
|
|
| 8 |
RUN pip install --no-cache-dir uv
|
| 9 |
|
|
|
|
| 10 |
COPY pyproject.toml uv.lock ./
|
|
|
|
|
|
|
| 11 |
RUN uv sync --frozen --no-dev
|
| 12 |
|
| 13 |
+
COPY main.py app_gradio.py start.sh README.md ./
|
| 14 |
+
RUN chmod +x /app/start.sh
|
|
|
|
|
|
|
| 15 |
|
|
|
|
| 16 |
EXPOSE 7860
|
| 17 |
|
| 18 |
+
CMD ["/app/start.sh"]
|
README.md
CHANGED
|
@@ -5,70 +5,81 @@ colorFrom: yellow
|
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
-
short_description: "
|
| 9 |
---
|
| 10 |
|
| 11 |
-
# Keyvault (FastAPI)
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
## Run locally (Mac)
|
| 22 |
|
| 23 |
-
### 1)
|
| 24 |
|
| 25 |
-
Create
|
| 26 |
|
| 27 |
```bash
|
| 28 |
DATABASE_URL=postgresql+psycopg2://USER:PASSWORD@HOST:PORT/DBNAME?sslmode=require
|
| 29 |
```
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
### 2) Install + run (using uv)
|
| 34 |
|
| 35 |
```bash
|
| 36 |
uv sync
|
| 37 |
-
uv run uvicorn main:app --reload --host 127.0.0.1 --port 8000
|
| 38 |
```
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
- Swagger docs: http://127.0.0.1:8000/docs
|
| 43 |
-
|
| 44 |
-
## Deploy on Hugging Face Spaces (Docker)
|
| 45 |
-
|
| 46 |
-
### 1) Add secret in Spaces
|
| 47 |
-
|
| 48 |
-
In your Space: **Settings → Repository secrets**
|
| 49 |
-
|
| 50 |
-
- `DATABASE_URL` = your Postgres connection string
|
| 51 |
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
|
|
|
| 55 |
|
| 56 |
-
|
| 57 |
|
| 58 |
-
|
| 59 |
-
- Port: `$PORT` (Spaces provides this, usually `7860`)
|
| 60 |
|
| 61 |
-
|
| 62 |
|
| 63 |
-
- `
|
| 64 |
-
- `/health` (if implemented in `main.py`)
|
| 65 |
|
| 66 |
-
##
|
| 67 |
|
| 68 |
-
-
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
```
|
|
|
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
short_description: "Secret storage with a minimal UI (Gradio) + API (FastAPI)"
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Keyvault (FastAPI + Gradio)
|
| 12 |
|
| 13 |
+
This repo runs **one Hugging Face Space** that serves:
|
| 14 |
|
| 15 |
+
- **Gradio UI** (public on port 7860 / `$PORT`)
|
| 16 |
+
- **FastAPI API** (inside the same container on `127.0.0.1:8000`)
|
| 17 |
|
| 18 |
+
The UI calls the API internally.
|
| 19 |
+
|
| 20 |
+
## Endpoints (API)
|
| 21 |
+
|
| 22 |
+
FastAPI routes (available inside the Space container, and available publicly via the Space URL):
|
| 23 |
+
|
| 24 |
+
- `GET /` (help)
|
| 25 |
+
- `GET /health`
|
| 26 |
+
- `GET /secrets`
|
| 27 |
+
- `GET /secrets/keys`
|
| 28 |
+
- `GET /secrets/{key}`
|
| 29 |
+
- `GET /secrets/{key}/full`
|
| 30 |
+
- `PUT /secrets/{key}`
|
| 31 |
+
- `POST /secrets`
|
| 32 |
+
- `DELETE /secrets/{key}`
|
| 33 |
+
- `GET /secrets/{key}/history`
|
| 34 |
+
- `GET /audit`
|
| 35 |
+
- `GET /stats`
|
| 36 |
+
|
| 37 |
+
Swagger:
|
| 38 |
+
|
| 39 |
+
- Local: `http://127.0.0.1:8000/docs`
|
| 40 |
+
- On Spaces: `https://<your-space>.hf.space/docs`
|
| 41 |
|
| 42 |
## Run locally (Mac)
|
| 43 |
|
| 44 |
+
### 1) Set DATABASE_URL
|
| 45 |
|
| 46 |
+
Create `.env`:
|
| 47 |
|
| 48 |
```bash
|
| 49 |
DATABASE_URL=postgresql+psycopg2://USER:PASSWORD@HOST:PORT/DBNAME?sslmode=require
|
| 50 |
```
|
| 51 |
|
| 52 |
+
### 2) Install
|
|
|
|
|
|
|
| 53 |
|
| 54 |
```bash
|
| 55 |
uv sync
|
|
|
|
| 56 |
```
|
| 57 |
|
| 58 |
+
### 3) Run API + UI together
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
```bash
|
| 61 |
+
chmod +x start.sh
|
| 62 |
+
./start.sh
|
| 63 |
+
```
|
| 64 |
|
| 65 |
+
- UI: http://127.0.0.1:7860
|
| 66 |
+
- API docs: http://127.0.0.1:8000/docs
|
| 67 |
|
| 68 |
+
## Deploy on Hugging Face Spaces (Docker)
|
| 69 |
|
| 70 |
+
### 1) Add Space secret
|
|
|
|
| 71 |
|
| 72 |
+
Space → **Settings → Repository secrets**:
|
| 73 |
|
| 74 |
+
- `DATABASE_URL` = your Postgres URL
|
|
|
|
| 75 |
|
| 76 |
+
### 2) URLs on Spaces
|
| 77 |
|
| 78 |
+
- UI: `https://<your-space>.hf.space/`
|
| 79 |
+
- API docs: `https://<your-space>.hf.space/docs`
|
| 80 |
+
- OpenAPI: `https://<your-space>.hf.space/openapi.json`
|
| 81 |
|
| 82 |
+
## Security note
|
| 83 |
|
| 84 |
+
This is a demo vault. Secrets are stored in plaintext in the database.
|
| 85 |
+
Do not use for real production secrets without encryption + auth.
|
|
|
app_gradio.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import gradio as gr
|
| 4 |
+
|
| 5 |
+
DEFAULT_BASE_LOCAL = "http://127.0.0.1:8000"
|
| 6 |
+
DEFAULT_BASE_SPACE_INTERNAL = "http://127.0.0.1:8000"
|
| 7 |
+
|
| 8 |
+
# If KEYVAULT_BASE_URL is not set, assume API is running in the same container
|
| 9 |
+
BASE_URL = os.getenv("KEYVAULT_BASE_URL", DEFAULT_BASE_SPACE_INTERNAL).rstrip("/")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _url(path: str) -> str:
|
| 13 |
+
return f"{BASE_URL}{path}"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def ping():
|
| 17 |
+
r = requests.get(_url("/health"), timeout=20)
|
| 18 |
+
r.raise_for_status()
|
| 19 |
+
return r.json()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def list_keys():
|
| 23 |
+
r = requests.get(_url("/secrets/keys"), timeout=20)
|
| 24 |
+
r.raise_for_status()
|
| 25 |
+
return r.json()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def get_secret(key: str):
|
| 29 |
+
if not key:
|
| 30 |
+
return {"error": "key required"}
|
| 31 |
+
r = requests.get(_url(f"/secrets/{key}"), timeout=20)
|
| 32 |
+
if r.status_code >= 400:
|
| 33 |
+
return {"error": r.text}
|
| 34 |
+
return r.json()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def put_secret(key: str, value: str, description: str):
|
| 38 |
+
if not key:
|
| 39 |
+
return {"error": "key required"}
|
| 40 |
+
payload = {"value": value, "description": (description or None)}
|
| 41 |
+
r = requests.put(_url(f"/secrets/{key}"), json=payload, timeout=20)
|
| 42 |
+
if r.status_code >= 400:
|
| 43 |
+
return {"error": r.text}
|
| 44 |
+
return r.json()
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def delete_secret(key: str):
|
| 48 |
+
if not key:
|
| 49 |
+
return {"error": "key required"}
|
| 50 |
+
r = requests.delete(_url(f"/secrets/{key}"), timeout=20)
|
| 51 |
+
if r.status_code >= 400:
|
| 52 |
+
return {"error": r.text}
|
| 53 |
+
return r.json()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def audit(limit: int, key_filter: str):
|
| 57 |
+
params = {"limit": int(limit)}
|
| 58 |
+
if key_filter:
|
| 59 |
+
params["key"] = key_filter
|
| 60 |
+
r = requests.get(_url("/audit"), params=params, timeout=20)
|
| 61 |
+
r.raise_for_status()
|
| 62 |
+
return r.json()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def stats():
|
| 66 |
+
r = requests.get(_url("/stats"), timeout=20)
|
| 67 |
+
r.raise_for_status()
|
| 68 |
+
return r.json()
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
with gr.Blocks(title="Keyvault UI") as blocks:
|
| 72 |
+
gr.Markdown("# Keyvault UI (Gradio)\nConsumes the Keyvault FastAPI endpoints.")
|
| 73 |
+
|
| 74 |
+
with gr.Row():
|
| 75 |
+
base = gr.Textbox(
|
| 76 |
+
label="API Base URL (set KEYVAULT_BASE_URL env var to override)",
|
| 77 |
+
value=BASE_URL,
|
| 78 |
+
interactive=False,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
with gr.Row():
|
| 82 |
+
btn_ping = gr.Button("Health")
|
| 83 |
+
out_ping = gr.JSON(label="Health")
|
| 84 |
+
btn_ping.click(fn=ping, outputs=out_ping)
|
| 85 |
+
|
| 86 |
+
gr.Markdown("## Secrets")
|
| 87 |
+
with gr.Row():
|
| 88 |
+
btn_keys = gr.Button("List keys")
|
| 89 |
+
out_keys = gr.JSON(label="Keys")
|
| 90 |
+
btn_keys.click(fn=list_keys, outputs=out_keys)
|
| 91 |
+
|
| 92 |
+
with gr.Row():
|
| 93 |
+
in_key = gr.Textbox(label="Key", placeholder="e.g. STRIPE_API_KEY")
|
| 94 |
+
with gr.Row():
|
| 95 |
+
btn_get = gr.Button("Get secret")
|
| 96 |
+
out_get = gr.JSON(label="Get result")
|
| 97 |
+
btn_get.click(fn=get_secret, inputs=in_key, outputs=out_get)
|
| 98 |
+
|
| 99 |
+
with gr.Row():
|
| 100 |
+
in_value = gr.Textbox(label="Value", placeholder="secret value", lines=2)
|
| 101 |
+
in_desc = gr.Textbox(label="Description (optional)", placeholder="what is this secret?")
|
| 102 |
+
with gr.Row():
|
| 103 |
+
btn_put = gr.Button("Create/Update (PUT)")
|
| 104 |
+
out_put = gr.JSON(label="PUT result")
|
| 105 |
+
btn_put.click(fn=put_secret, inputs=[in_key, in_value, in_desc], outputs=out_put)
|
| 106 |
+
|
| 107 |
+
with gr.Row():
|
| 108 |
+
btn_del = gr.Button("Delete")
|
| 109 |
+
out_del = gr.JSON(label="Delete result")
|
| 110 |
+
btn_del.click(fn=delete_secret, inputs=in_key, outputs=out_del)
|
| 111 |
+
|
| 112 |
+
gr.Markdown("## Audit + Stats")
|
| 113 |
+
with gr.Row():
|
| 114 |
+
in_limit = gr.Slider(1, 2000, value=200, step=1, label="Audit limit")
|
| 115 |
+
in_key_filter = gr.Textbox(label="Filter by key (optional)")
|
| 116 |
+
with gr.Row():
|
| 117 |
+
btn_audit = gr.Button("Get audit logs")
|
| 118 |
+
out_audit = gr.JSON(label="Audit")
|
| 119 |
+
btn_audit.click(fn=audit, inputs=[in_limit, in_key_filter], outputs=out_audit)
|
| 120 |
+
|
| 121 |
+
with gr.Row():
|
| 122 |
+
btn_stats = gr.Button("Stats")
|
| 123 |
+
out_stats = gr.JSON(label="Stats")
|
| 124 |
+
btn_stats.click(fn=stats, outputs=out_stats)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
if __name__ == "__main__":
|
| 128 |
+
# HF Spaces requires 0.0.0.0 and PORT; local will default to 7860
|
| 129 |
+
port = int(os.getenv("PORT", "7860"))
|
| 130 |
+
blocks.launch(server_name="0.0.0.0", server_port=port)
|
pyproject.toml
CHANGED
|
@@ -11,4 +11,6 @@ dependencies = [
|
|
| 11 |
"python-dotenv>=1.2.2",
|
| 12 |
"sqlalchemy>=2.0.48",
|
| 13 |
"uvicorn>=0.42.0",
|
|
|
|
|
|
|
| 14 |
]
|
|
|
|
| 11 |
"python-dotenv>=1.2.2",
|
| 12 |
"sqlalchemy>=2.0.48",
|
| 13 |
"uvicorn>=0.42.0",
|
| 14 |
+
"gradio>=5.0.0",
|
| 15 |
+
"requests>=2.31.0",
|
| 16 |
]
|
start.sh
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
cd "$(dirname "$0")"
|
| 5 |
+
|
| 6 |
+
API_HOST="${API_HOST:-127.0.0.1}"
|
| 7 |
+
API_PORT="${API_PORT:-8000}"
|
| 8 |
+
UI_HOST="${UI_HOST:-0.0.0.0}"
|
| 9 |
+
UI_PORT="${PORT:-7860}"
|
| 10 |
+
|
| 11 |
+
: "${DATABASE_URL:?DATABASE_URL is not set. Add it as a Hugging Face Space Secret (Settings → Repository secrets).}"
|
| 12 |
+
|
| 13 |
+
echo "Starting FastAPI on http://${API_HOST}:${API_PORT} ..."
|
| 14 |
+
uv run uvicorn main:app --host "${API_HOST}" --port "${API_PORT}" &
|
| 15 |
+
API_PID=$!
|
| 16 |
+
|
| 17 |
+
cleanup() {
|
| 18 |
+
echo "Stopping FastAPI..."
|
| 19 |
+
kill "${API_PID}" 2>/dev/null || true
|
| 20 |
+
}
|
| 21 |
+
trap cleanup EXIT
|
| 22 |
+
|
| 23 |
+
echo "Starting Gradio UI on http://${UI_HOST}:${UI_PORT} (API -> http://${API_HOST}:${API_PORT}) ..."
|
| 24 |
+
export KEYVAULT_BASE_URL="http://${API_HOST}:${API_PORT}"
|
| 25 |
+
uv run python app_gradio.py
|