gcharanteja commited on
Commit
fa6bb79
·
1 Parent(s): 0378642

Run FastAPI + Gradio together in one HF Space

Browse files
Files changed (5) hide show
  1. Dockerfile +3 -10
  2. README.md +49 -38
  3. app_gradio.py +130 -0
  4. pyproject.toml +2 -0
  5. 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
- # Copy application code
18
- COPY main.py README.md ./
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 ["bash", "-lc", "uv run uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860}"]
 
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: "Simple secret storage API (FastAPI + Postgres)"
9
  ---
10
 
11
- # Keyvault (FastAPI)
12
 
13
- A small **Key Vault API** for storing and retrieving secrets backed by a **PostgreSQL** database.
14
 
15
- ## What you get
 
16
 
17
- - FastAPI REST API
18
- - Stores secrets in Postgres (via SQLAlchemy)
19
- - Loads configuration from environment variables (`DATABASE_URL`)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  ## Run locally (Mac)
22
 
23
- ### 1) Create `.env`
24
 
25
- Create a file named `.env` in the project root:
26
 
27
  ```bash
28
  DATABASE_URL=postgresql+psycopg2://USER:PASSWORD@HOST:PORT/DBNAME?sslmode=require
29
  ```
30
 
31
- > Don’t commit `.env` (it’s ignored by `.gitignore`).
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
- Open:
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
- Do **not** upload your `.env` file.
 
 
 
53
 
54
- ### 2) Docker / runtime
 
55
 
56
- This Space uses Docker and starts with:
57
 
58
- - Host: `0.0.0.0`
59
- - Port: `$PORT` (Spaces provides this, usually `7860`)
60
 
61
- Once deployed, you can access:
62
 
63
- - `/docs` (Swagger UI)
64
- - `/health` (if implemented in `main.py`)
65
 
66
- ## Environment variables
67
 
68
- - `DATABASE_URL` (required): SQLAlchemy connection string for Postgres
 
 
69
 
70
- Example:
71
 
72
- ```text
73
- postgresql+psycopg2://avnadmin:password@your-host:26779/vault?sslmode=require
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