Intial Deployment
Browse files- Dockerfile +35 -0
- README.md +1 -0
- main.py +186 -0
- requirements.txt +5 -0
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Python runtime as a parent image
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory in the container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy the requirements file into the container at /app
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
|
| 10 |
+
# Install any needed packages specified in requirements.txt
|
| 11 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 12 |
+
pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy the rest of the application code into the container at /app
|
| 15 |
+
COPY main.py .
|
| 16 |
+
|
| 17 |
+
# --- Define Volumes ---
|
| 18 |
+
# This tells Docker to manage these directories as volumes if not explicitly mounted.
|
| 19 |
+
# Data written here will persist in anonymous volumes by default.
|
| 20 |
+
VOLUME /app/data
|
| 21 |
+
VOLUME /root/.duckdb
|
| 22 |
+
# --- End Define Volumes ---
|
| 23 |
+
|
| 24 |
+
# Make API port 8000 available
|
| 25 |
+
EXPOSE 8000
|
| 26 |
+
# Make DuckDB UI port 8080 available (default)
|
| 27 |
+
EXPOSE 8080
|
| 28 |
+
|
| 29 |
+
# Define environment variables
|
| 30 |
+
ENV PYTHONUNBUFFERED=1
|
| 31 |
+
ENV UI_EXPECTED_PORT=8080
|
| 32 |
+
|
| 33 |
+
# Command to run the FastAPI application using Uvicorn
|
| 34 |
+
# The startup event in main.py will handle starting the DuckDB UI
|
| 35 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -7,6 +7,7 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
short_description: DuckDB Hosting with UI & FastAPI 4 SQL Calls & DB Downloads
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
short_description: DuckDB Hosting with UI & FastAPI 4 SQL Calls & DB Downloads
|
| 10 |
+
port:
|
| 11 |
---
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
main.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import duckdb
|
| 3 |
+
from fastapi import FastAPI, HTTPException, Body
|
| 4 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import logging
|
| 8 |
+
import time # Import time for potential startup delays
|
| 9 |
+
import asyncio
|
| 10 |
+
|
| 11 |
+
# --- Configuration ---
|
| 12 |
+
DB_DIR = Path("data")
|
| 13 |
+
DB_FILENAME = "mydatabase.db"
|
| 14 |
+
DB_FILE = DB_DIR / DB_FILENAME
|
| 15 |
+
UI_EXPECTED_PORT = 8080 # Default port DuckDB UI often tries first
|
| 16 |
+
|
| 17 |
+
# Ensure the data directory exists
|
| 18 |
+
DB_DIR.mkdir(parents=True, exist_ok=True)
|
| 19 |
+
|
| 20 |
+
# --- Logging Setup ---
|
| 21 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
# --- FastAPI App ---
|
| 25 |
+
app = FastAPI(
|
| 26 |
+
title="DuckDB API & UI Host",
|
| 27 |
+
description="Interact with DuckDB via API (/query, /download) and access the official DuckDB Web UI.",
|
| 28 |
+
version="1.0.0"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# --- Pydantic Models ---
|
| 32 |
+
class QueryRequest(BaseModel):
|
| 33 |
+
sql: str = Field(..., description="The SQL query to execute against DuckDB.")
|
| 34 |
+
|
| 35 |
+
class QueryResponse(BaseModel):
|
| 36 |
+
columns: list[str] | None = None
|
| 37 |
+
rows: list[dict] | None = None
|
| 38 |
+
message: str | None = None
|
| 39 |
+
error: str | None = None
|
| 40 |
+
|
| 41 |
+
# --- Helper Function ---
|
| 42 |
+
def execute_duckdb_query(sql_query: str, db_path: str = str(DB_FILE)):
|
| 43 |
+
"""Connects to DuckDB, executes a query, and returns results or error."""
|
| 44 |
+
con = None
|
| 45 |
+
try:
|
| 46 |
+
logger.info(f"Connecting to database: {db_path}")
|
| 47 |
+
con = duckdb.connect(database=db_path, read_only=False)
|
| 48 |
+
logger.info(f"Executing SQL: {sql_query[:200]}{'...' if len(sql_query) > 200 else ''}")
|
| 49 |
+
|
| 50 |
+
con.begin()
|
| 51 |
+
result_relation = con.execute(sql_query)
|
| 52 |
+
response_data = {"columns": None, "rows": None, "message": None, "error": None}
|
| 53 |
+
|
| 54 |
+
if result_relation.description:
|
| 55 |
+
columns = [desc[0] for desc in result_relation.description]
|
| 56 |
+
rows_raw = result_relation.fetchall()
|
| 57 |
+
rows_dict = [dict(zip(columns, row)) for row in rows_raw]
|
| 58 |
+
response_data["columns"] = columns
|
| 59 |
+
response_data["rows"] = rows_dict
|
| 60 |
+
response_data["message"] = f"Query executed successfully. Fetched {len(rows_dict)} row(s)."
|
| 61 |
+
logger.info(f"Query successful, returned {len(rows_dict)} rows.")
|
| 62 |
+
else:
|
| 63 |
+
response_data["message"] = "Query executed successfully (no data returned)."
|
| 64 |
+
logger.info("Query successful (no data returned).")
|
| 65 |
+
|
| 66 |
+
con.commit()
|
| 67 |
+
return response_data
|
| 68 |
+
|
| 69 |
+
except duckdb.Error as e:
|
| 70 |
+
logger.error(f"DuckDB Error: {e}")
|
| 71 |
+
if con: con.rollback()
|
| 72 |
+
return {"columns": None, "rows": None, "message": None, "error": str(e)}
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logger.error(f"General Error: {e}")
|
| 75 |
+
if con: con.rollback()
|
| 76 |
+
return {"columns": None, "rows": None, "message": None, "error": f"An unexpected error occurred: {e}"}
|
| 77 |
+
finally:
|
| 78 |
+
if con:
|
| 79 |
+
con.close()
|
| 80 |
+
logger.info("Database connection closed.")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# --- FastAPI Startup Event ---
|
| 84 |
+
@app.on_event("startup")
|
| 85 |
+
async def startup_event():
|
| 86 |
+
logger.info("Application startup: Initializing DuckDB UI...")
|
| 87 |
+
con = None
|
| 88 |
+
try:
|
| 89 |
+
# Connect to the main DB file to execute initialization commands
|
| 90 |
+
# Use a temporary in-memory DB for UI start if main DB doesn't exist yet?
|
| 91 |
+
# No, start_ui seems to need the target DB. Ensure DB file path exists.
|
| 92 |
+
if not DB_FILE.parent.exists():
|
| 93 |
+
DB_FILE.parent.mkdir(parents=True, exist_ok=True)
|
| 94 |
+
|
| 95 |
+
# It's crucial the UI extension can write its state.
|
| 96 |
+
# By default it uses ~/.duckdb/ which will be /root/.duckdb in the container.
|
| 97 |
+
# Ensure this is writable or mount a volume there.
|
| 98 |
+
logger.info(f"Attempting to connect to {DB_FILE} for UI setup.")
|
| 99 |
+
con = duckdb.connect(database=str(DB_FILE), read_only=False)
|
| 100 |
+
|
| 101 |
+
logger.info("Installing and loading 'ui' extension...")
|
| 102 |
+
con.execute("INSTALL ui;")
|
| 103 |
+
con.execute("LOAD ui;")
|
| 104 |
+
|
| 105 |
+
logger.info("Calling start_ui()... This will start a separate web server.")
|
| 106 |
+
# CALL start_ui() starts the server in the background (usually)
|
| 107 |
+
# It might print the URL/port it's using to stderr/stdout of the main process
|
| 108 |
+
con.execute("CALL start_ui();")
|
| 109 |
+
|
| 110 |
+
# Give the UI server a moment to start up. This is a guess.
|
| 111 |
+
# A more robust solution might involve checking if the port is listening.
|
| 112 |
+
await asyncio.sleep(2)
|
| 113 |
+
|
| 114 |
+
logger.info(f"DuckDB UI server startup initiated. It usually listens on port {UI_EXPECTED_PORT}.")
|
| 115 |
+
logger.info("Check container logs for the exact URL if it differs.")
|
| 116 |
+
logger.info("API server (FastAPI/Uvicorn) is running on port 8000.")
|
| 117 |
+
|
| 118 |
+
except duckdb.Error as e:
|
| 119 |
+
logger.error(f"CRITICAL: Failed to install/load/start DuckDB UI extension: {e}")
|
| 120 |
+
logger.error("The DuckDB UI will likely not be available.")
|
| 121 |
+
except Exception as e:
|
| 122 |
+
logger.error(f"CRITICAL: An unexpected error occurred during UI startup: {e}")
|
| 123 |
+
logger.error("The DuckDB UI will likely not be available.")
|
| 124 |
+
finally:
|
| 125 |
+
if con:
|
| 126 |
+
con.close()
|
| 127 |
+
logger.info("UI setup connection closed.")
|
| 128 |
+
|
| 129 |
+
# --- API Endpoints ---
|
| 130 |
+
@app.get("/", summary="Root Endpoint / Info", tags=["General"])
|
| 131 |
+
async def read_root():
|
| 132 |
+
"""Provides links to the API docs and the DuckDB UI."""
|
| 133 |
+
# Assumes UI is running on localhost from the container's perspective
|
| 134 |
+
# User needs to map the port correctly
|
| 135 |
+
return JSONResponse({
|
| 136 |
+
"message": "DuckDB API and UI Host",
|
| 137 |
+
"api_details": {
|
| 138 |
+
"docs": "/docs",
|
| 139 |
+
"query_endpoint": "/query (POST)",
|
| 140 |
+
"download_endpoint": "/download (GET)"
|
| 141 |
+
},
|
| 142 |
+
"duckdb_ui": {
|
| 143 |
+
"message": f"Access the official DuckDB Web UI. It should be running on port {UI_EXPECTED_PORT} inside the container.",
|
| 144 |
+
"typical_access_url": f"http://localhost:{UI_EXPECTED_PORT}",
|
| 145 |
+
"notes": f"Ensure you have mapped port {UI_EXPECTED_PORT} from the container when running `docker run` (e.g., -p {UI_EXPECTED_PORT}:{UI_EXPECTED_PORT})."
|
| 146 |
+
},
|
| 147 |
+
"database_file_container_path": str(DB_FILE)
|
| 148 |
+
})
|
| 149 |
+
|
| 150 |
+
@app.post("/query", response_model=QueryResponse, summary="Execute SQL Query", tags=["Database API"])
|
| 151 |
+
async def execute_query_endpoint(query_request: QueryRequest):
|
| 152 |
+
"""
|
| 153 |
+
Executes a given SQL query against the DuckDB database via the API.
|
| 154 |
+
Handles SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, etc.
|
| 155 |
+
"""
|
| 156 |
+
result = execute_duckdb_query(query_request.sql)
|
| 157 |
+
if result["error"]:
|
| 158 |
+
raise HTTPException(status_code=400, detail=result["error"])
|
| 159 |
+
return JSONResponse(content=result)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@app.get("/download", summary="Download Database File", tags=["Database API"])
|
| 163 |
+
async def download_database_file():
|
| 164 |
+
"""
|
| 165 |
+
Allows downloading the current DuckDB database file via the API.
|
| 166 |
+
"""
|
| 167 |
+
if not DB_FILE.is_file():
|
| 168 |
+
logger.error(f"Download request failed: Database file not found at {DB_FILE}")
|
| 169 |
+
raise HTTPException(status_code=404, detail="Database file not found.")
|
| 170 |
+
|
| 171 |
+
logger.info(f"Serving database file for download: {DB_FILE}")
|
| 172 |
+
return FileResponse(
|
| 173 |
+
path=str(DB_FILE),
|
| 174 |
+
filename=DB_FILENAME,
|
| 175 |
+
media_type='application/octet-stream'
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
# Need asyncio for sleep in startup
|
| 179 |
+
# import asyncio
|
| 180 |
+
|
| 181 |
+
# --- Run with Uvicorn (for local testing - doesn't handle UI startup well here) ---
|
| 182 |
+
# if __name__ == "__main__":
|
| 183 |
+
# # Note: Running directly with python main.py won't trigger the startup
|
| 184 |
+
# # event correctly in the same way uvicorn command does.
|
| 185 |
+
# # Use `uvicorn main:app --reload --port 8000` for local dev testing.
|
| 186 |
+
# print("Run using: uvicorn main:app --host 0.0.0.0 --port 8000")
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
duckdb>=1.0.0 # Ensure version compatibility with UI extension
|
| 4 |
+
pydantic
|
| 5 |
+
python-multipart
|