amaye15 commited on
Commit
f959360
·
1 Parent(s): a2a09f6

Intial Deployment

Browse files
Files changed (4) hide show
  1. Dockerfile +35 -0
  2. README.md +1 -0
  3. main.py +186 -0
  4. 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