Spaces:
Running
Running
Debug
Browse files- app/database.py +36 -80
- app/main.py +54 -8
app/database.py
CHANGED
@@ -2,8 +2,8 @@
|
|
2 |
import os
|
3 |
from databases import Database
|
4 |
from dotenv import load_dotenv
|
5 |
-
# ---
|
6 |
-
from sqlalchemy import
|
7 |
import logging
|
8 |
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
9 |
|
@@ -14,7 +14,6 @@ logger = logging.getLogger(__name__)
|
|
14 |
DEFAULT_DB_PATH = "/tmp/app.db" # Store DB in the temporary directory
|
15 |
raw_db_url = os.getenv("DATABASE_URL", f"sqlite+aiosqlite:///{DEFAULT_DB_PATH}")
|
16 |
|
17 |
-
# --- URL Parsing and Async Database setup (remains the same) ---
|
18 |
final_database_url = raw_db_url
|
19 |
if raw_db_url.startswith("sqlite+aiosqlite"):
|
20 |
parsed_url = urlparse(raw_db_url)
|
@@ -27,7 +26,10 @@ if raw_db_url.startswith("sqlite+aiosqlite"):
|
|
27 |
else:
|
28 |
logger.info(f"Using non-SQLite async DB URL: {final_database_url}")
|
29 |
|
|
|
30 |
database = Database(final_database_url)
|
|
|
|
|
31 |
metadata = MetaData()
|
32 |
users = Table(
|
33 |
"users",
|
@@ -37,92 +39,46 @@ users = Table(
|
|
37 |
Column("hashed_password", String, nullable=False),
|
38 |
)
|
39 |
|
40 |
-
# ---
|
41 |
-
sync_db_url = final_database_url.replace("+aiosqlite", "")
|
42 |
-
logger.info(f"Using synchronous DB URL for initial check/create: {sync_db_url}")
|
43 |
-
engine = create_engine(sync_db_url)
|
44 |
-
|
45 |
-
# --- Directory and Table Creation Logic ---
|
46 |
-
db_file_path = ""
|
47 |
-
if sync_db_url.startswith("sqlite"):
|
48 |
-
path_part = sync_db_url.split("sqlite:///")[-1].split("?")[0]
|
49 |
-
db_file_path = path_part # Should be /tmp/app.db
|
50 |
-
|
51 |
-
if db_file_path:
|
52 |
-
db_dir = os.path.dirname(db_file_path) # Should be /tmp
|
53 |
-
logger.info(f"Checking database directory: {db_dir}")
|
54 |
-
try:
|
55 |
-
if not os.path.exists(db_dir):
|
56 |
-
logger.error(f"CRITICAL: Directory {db_dir} does not exist!")
|
57 |
-
elif not os.access(db_dir, os.W_OK):
|
58 |
-
logger.error(f"CRITICAL: Directory {db_dir} is not writable! Cannot create database.")
|
59 |
-
else:
|
60 |
-
logger.info(f"Database directory {db_dir} appears writable.")
|
61 |
-
except OSError as e:
|
62 |
-
logger.error(f"Error accessing database directory {db_dir}: {e}")
|
63 |
-
except Exception as e:
|
64 |
-
logger.error(f"Unexpected error checking directory {db_dir}: {e}")
|
65 |
-
|
66 |
-
|
67 |
-
# --- Refined Synchronous Table Check/Creation ---
|
68 |
-
try:
|
69 |
-
logger.info("Attempting sync connection to check/create table...")
|
70 |
-
with engine.connect() as connection:
|
71 |
-
logger.info("Sync engine connection successful.")
|
72 |
-
try:
|
73 |
-
# Check if table exists
|
74 |
-
connection.execute(text("SELECT 1 FROM users LIMIT 1"))
|
75 |
-
logger.info("Users table already exists (checked via sync connection).")
|
76 |
-
# --- Catch specific SQLAlchemy error ---
|
77 |
-
except sqlalchemy_exc.OperationalError as table_missing_err:
|
78 |
-
# Check if the error message specifically indicates "no such table"
|
79 |
-
if "no such table" in str(table_missing_err).lower():
|
80 |
-
logger.warning("Users table not found (expected), attempting creation...")
|
81 |
-
try:
|
82 |
-
# Begin a transaction explicitly (optional, connect() usually does)
|
83 |
-
# with connection.begin(): # Alternative way to manage transaction
|
84 |
-
# Use the connection object for create_all
|
85 |
-
metadata.create_all(bind=connection) # <-- Bind to connection
|
86 |
-
# --- Explicitly commit ---
|
87 |
-
connection.commit()
|
88 |
-
logger.info("Users table creation attempted and committed via sync connection.")
|
89 |
-
# --- Verify creation immediately ---
|
90 |
-
try:
|
91 |
-
connection.execute(text("SELECT 1 FROM users LIMIT 1"))
|
92 |
-
logger.info("Users table successfully verified immediately after creation.")
|
93 |
-
except Exception as verify_err:
|
94 |
-
logger.error(f"Failed to verify table immediately after creation: {verify_err}")
|
95 |
-
|
96 |
-
except Exception as creation_err:
|
97 |
-
logger.exception(f"Error during table creation or commit: {creation_err}")
|
98 |
-
# Optionally rollback? connection.rollback()
|
99 |
-
|
100 |
-
else:
|
101 |
-
# Log other OperationalErrors during the check phase
|
102 |
-
logger.error(f"OperationalError during table check (but not 'no such table'): {table_missing_err}")
|
103 |
-
raise # Re-raise unexpected errors
|
104 |
-
|
105 |
-
except Exception as table_check_exc: # Catch other unexpected errors during check
|
106 |
-
logger.error(f"Unexpected error during table check: {type(table_check_exc).__name__}: {table_check_exc}")
|
107 |
-
raise # Re-raise unexpected errors
|
108 |
|
109 |
-
|
110 |
-
# Errors connecting, or unexpected errors during check/create phase
|
111 |
-
logger.exception(f"CRITICAL: Failed during sync connection or table setup: {e}")
|
112 |
-
|
113 |
-
|
114 |
-
# --- Async connect/disconnect functions (remain the same) ---
|
115 |
async def connect_db():
|
|
|
116 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
await database.connect()
|
118 |
logger.info(f"Database connection established (async): {final_database_url}")
|
|
|
119 |
except Exception as e:
|
120 |
logger.exception(f"Failed to establish async database connection: {e}")
|
121 |
-
raise
|
122 |
|
123 |
async def disconnect_db():
|
|
|
124 |
try:
|
125 |
-
|
126 |
-
|
|
|
|
|
|
|
127 |
except Exception as e:
|
128 |
logger.exception(f"Error closing async database connection: {e}")
|
|
|
2 |
import os
|
3 |
from databases import Database
|
4 |
from dotenv import load_dotenv
|
5 |
+
# --- Keep only these SQLAlchemy imports ---
|
6 |
+
from sqlalchemy import MetaData, Table, Column, Integer, String
|
7 |
import logging
|
8 |
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
9 |
|
|
|
14 |
DEFAULT_DB_PATH = "/tmp/app.db" # Store DB in the temporary directory
|
15 |
raw_db_url = os.getenv("DATABASE_URL", f"sqlite+aiosqlite:///{DEFAULT_DB_PATH}")
|
16 |
|
|
|
17 |
final_database_url = raw_db_url
|
18 |
if raw_db_url.startswith("sqlite+aiosqlite"):
|
19 |
parsed_url = urlparse(raw_db_url)
|
|
|
26 |
else:
|
27 |
logger.info(f"Using non-SQLite async DB URL: {final_database_url}")
|
28 |
|
29 |
+
# --- Async Database Instance ---
|
30 |
database = Database(final_database_url)
|
31 |
+
|
32 |
+
# --- Metadata and Table Definition (Still needed for DDL generation) ---
|
33 |
metadata = MetaData()
|
34 |
users = Table(
|
35 |
"users",
|
|
|
39 |
Column("hashed_password", String, nullable=False),
|
40 |
)
|
41 |
|
42 |
+
# --- REMOVE ALL SYNCHRONOUS ENGINE AND TABLE CREATION LOGIC ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
+
# --- Keep and refine Async connect/disconnect functions ---
|
|
|
|
|
|
|
|
|
|
|
45 |
async def connect_db():
|
46 |
+
"""Connects to the database, ensuring the parent directory exists."""
|
47 |
try:
|
48 |
+
# Ensure the directory exists just before connecting
|
49 |
+
db_file_path = final_database_url.split("sqlite:///")[-1].split("?")[0]
|
50 |
+
db_dir = os.path.dirname(db_file_path)
|
51 |
+
if db_dir: # Only proceed if a directory path was found
|
52 |
+
if not os.path.exists(db_dir):
|
53 |
+
logger.info(f"Database directory {db_dir} does not exist. Attempting creation...")
|
54 |
+
try:
|
55 |
+
os.makedirs(db_dir, exist_ok=True)
|
56 |
+
logger.info(f"Created database directory {db_dir}.")
|
57 |
+
except Exception as mkdir_err:
|
58 |
+
# Log error but proceed, connection might still work if path is valid but dir creation failed weirdly
|
59 |
+
logger.error(f"Failed to create directory {db_dir}: {mkdir_err}")
|
60 |
+
# Check writability after ensuring existence attempt
|
61 |
+
if os.path.exists(db_dir) and not os.access(db_dir, os.W_OK):
|
62 |
+
logger.error(f"CRITICAL: Directory {db_dir} exists but is not writable!")
|
63 |
+
elif not os.path.exists(db_dir):
|
64 |
+
logger.error(f"CRITICAL: Directory {db_dir} does not exist and could not be created!")
|
65 |
+
|
66 |
+
|
67 |
+
# Now attempt connection
|
68 |
await database.connect()
|
69 |
logger.info(f"Database connection established (async): {final_database_url}")
|
70 |
+
# Table creation will happen in main.py lifespan event using this connection
|
71 |
except Exception as e:
|
72 |
logger.exception(f"Failed to establish async database connection: {e}")
|
73 |
+
raise # Reraise critical error during startup
|
74 |
|
75 |
async def disconnect_db():
|
76 |
+
"""Disconnects from the database if connected."""
|
77 |
try:
|
78 |
+
if database.is_connected:
|
79 |
+
await database.disconnect()
|
80 |
+
logger.info("Database connection closed (async).")
|
81 |
+
else:
|
82 |
+
logger.info("Database already disconnected (async).")
|
83 |
except Exception as e:
|
84 |
logger.exception(f"Error closing async database connection: {e}")
|
app/main.py
CHANGED
@@ -1,6 +1,7 @@
|
|
|
|
1 |
import gradio as gr
|
2 |
-
import httpx
|
3 |
-
import websockets
|
4 |
import asyncio
|
5 |
import json
|
6 |
import os
|
@@ -8,27 +9,72 @@ import logging
|
|
8 |
from contextlib import asynccontextmanager
|
9 |
|
10 |
from fastapi import FastAPI, Depends # Import FastAPI itself
|
11 |
-
|
12 |
-
from .database import connect_db, disconnect_db
|
13 |
-
from . import
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
# Configure logging
|
16 |
logging.basicConfig(level=logging.INFO)
|
17 |
logger = logging.getLogger(__name__)
|
18 |
|
19 |
-
# Base URL for the API
|
20 |
-
# Assuming Gradio runs on 7860, FastAPI routes mounted under it
|
21 |
API_BASE_URL = "http://127.0.0.1:7860/api" # Adjust if needed
|
22 |
|
23 |
# --- FastAPI Lifespan Event ---
|
24 |
@asynccontextmanager
|
25 |
async def lifespan(app: FastAPI):
|
26 |
# Startup
|
|
|
27 |
await connect_db()
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
yield
|
30 |
# Shutdown
|
|
|
31 |
await disconnect_db()
|
|
|
32 |
|
33 |
# Create the main FastAPI app instance that Gradio will use
|
34 |
# We attach our API routes to this instance.
|
|
|
1 |
+
# app/main.py
|
2 |
import gradio as gr
|
3 |
+
import httpx
|
4 |
+
import websockets
|
5 |
import asyncio
|
6 |
import json
|
7 |
import os
|
|
|
9 |
from contextlib import asynccontextmanager
|
10 |
|
11 |
from fastapi import FastAPI, Depends # Import FastAPI itself
|
12 |
+
# --- Import necessary items from database.py ---
|
13 |
+
from .database import connect_db, disconnect_db, database, metadata, users
|
14 |
+
from .api import router as api_router
|
15 |
+
from . import schemas, auth, dependencies
|
16 |
+
|
17 |
+
# --- Import SQLAlchemy helpers for DDL generation ---
|
18 |
+
from sqlalchemy.schema import CreateTable
|
19 |
+
from sqlalchemy.dialects import sqlite # Assuming SQLite, adjust if needed
|
20 |
|
21 |
# Configure logging
|
22 |
logging.basicConfig(level=logging.INFO)
|
23 |
logger = logging.getLogger(__name__)
|
24 |
|
25 |
+
# Base URL for the API
|
|
|
26 |
API_BASE_URL = "http://127.0.0.1:7860/api" # Adjust if needed
|
27 |
|
28 |
# --- FastAPI Lifespan Event ---
|
29 |
@asynccontextmanager
|
30 |
async def lifespan(app: FastAPI):
|
31 |
# Startup
|
32 |
+
logger.info("Application startup: Connecting DB...")
|
33 |
await connect_db()
|
34 |
+
logger.info("Application startup: DB Connected. Checking/Creating tables...")
|
35 |
+
if database.is_connected: # Proceed only if connection succeeded
|
36 |
+
try:
|
37 |
+
# 1. Check if table exists using the async connection
|
38 |
+
# For SQLite, check the sqlite_master table
|
39 |
+
check_query = "SELECT name FROM sqlite_master WHERE type='table' AND name=:table_name;"
|
40 |
+
table_exists = await database.fetch_one(query=check_query, values={"table_name": users.name})
|
41 |
+
|
42 |
+
if not table_exists:
|
43 |
+
logger.info(f"Table '{users.name}' not found, attempting creation using async connection...")
|
44 |
+
|
45 |
+
# 2. Generate the CREATE TABLE statement using SQLAlchemy DDL compiler
|
46 |
+
# We need a dialect object for the compiler
|
47 |
+
# Infer dialect from the database URL if possible, default to SQLite for this example
|
48 |
+
dialect = sqlite.dialect() # Or determine dynamically based on database.url
|
49 |
+
create_table_stmt = str(CreateTable(users).compile(dialect=dialect))
|
50 |
+
logger.debug(f"Generated CREATE TABLE statement: {create_table_stmt}")
|
51 |
+
|
52 |
+
# 3. Execute the CREATE TABLE statement via the async database connection
|
53 |
+
await database.execute(query=create_table_stmt)
|
54 |
+
logger.info(f"Table '{users.name}' created successfully via async connection.")
|
55 |
+
|
56 |
+
# 4. Optional: Verify again immediately
|
57 |
+
table_exists_after = await database.fetch_one(query=check_query, values={"table_name": users.name})
|
58 |
+
if table_exists_after:
|
59 |
+
logger.info(f"Table '{users.name}' verified after creation.")
|
60 |
+
else:
|
61 |
+
logger.error(f"Table '{users.name}' verification FAILED after creation attempt!")
|
62 |
+
|
63 |
+
else:
|
64 |
+
logger.info(f"Table '{users.name}' already exists (checked via async connection).")
|
65 |
+
|
66 |
+
except Exception as db_setup_err:
|
67 |
+
logger.exception(f"CRITICAL error during async DB table setup: {db_setup_err}")
|
68 |
+
# Consider whether to halt startup here depending on severity
|
69 |
+
else:
|
70 |
+
logger.error("CRITICAL: Database connection failed, skipping table setup.")
|
71 |
+
|
72 |
+
logger.info("Application startup: DB setup phase complete.")
|
73 |
yield
|
74 |
# Shutdown
|
75 |
+
logger.info("Application shutdown: Disconnecting DB...")
|
76 |
await disconnect_db()
|
77 |
+
logger.info("Application shutdown: DB Disconnected.")
|
78 |
|
79 |
# Create the main FastAPI app instance that Gradio will use
|
80 |
# We attach our API routes to this instance.
|