Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- database.py +91 -0
- main.py +36 -28
- upload_space.py +1 -1
database.py
CHANGED
|
@@ -84,6 +84,21 @@ def init_db():
|
|
| 84 |
FOREIGN KEY(owner_username) REFERENCES users(username)
|
| 85 |
)
|
| 86 |
''')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
conn.commit()
|
| 89 |
conn.close()
|
|
@@ -349,6 +364,7 @@ def update_patient(username: str, patient_db_id: int, updates: Dict[str, Any]) -
|
|
| 349 |
values.extend([patient_db_id, username])
|
| 350 |
query = f"UPDATE patients SET {', '.join(fields)} WHERE id = ? AND owner_username = ?"
|
| 351 |
|
|
|
|
| 352 |
c.execute(query, values)
|
| 353 |
count = c.rowcount
|
| 354 |
conn.commit()
|
|
@@ -358,4 +374,79 @@ def update_patient(username: str, patient_db_id: int, updates: Dict[str, Any]) -
|
|
| 358 |
logging.error(f"Error updating patient: {e}")
|
| 359 |
return False
|
| 360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
|
|
|
| 84 |
FOREIGN KEY(owner_username) REFERENCES users(username)
|
| 85 |
)
|
| 86 |
''')
|
| 87 |
+
|
| 88 |
+
# Create Jobs Table (PERSISTENCE)
|
| 89 |
+
c.execute('''
|
| 90 |
+
CREATE TABLE IF NOT EXISTS jobs (
|
| 91 |
+
id TEXT PRIMARY KEY,
|
| 92 |
+
status TEXT NOT NULL,
|
| 93 |
+
result TEXT, -- JSON serialized
|
| 94 |
+
error TEXT,
|
| 95 |
+
created_at REAL,
|
| 96 |
+
storage_path TEXT,
|
| 97 |
+
username TEXT,
|
| 98 |
+
file_type TEXT,
|
| 99 |
+
FOREIGN KEY(username) REFERENCES users(username)
|
| 100 |
+
)
|
| 101 |
+
''')
|
| 102 |
|
| 103 |
conn.commit()
|
| 104 |
conn.close()
|
|
|
|
| 364 |
values.extend([patient_db_id, username])
|
| 365 |
query = f"UPDATE patients SET {', '.join(fields)} WHERE id = ? AND owner_username = ?"
|
| 366 |
|
| 367 |
+
|
| 368 |
c.execute(query, values)
|
| 369 |
count = c.rowcount
|
| 370 |
conn.commit()
|
|
|
|
| 374 |
logging.error(f"Error updating patient: {e}")
|
| 375 |
return False
|
| 376 |
|
| 377 |
+
# --- Job Operations (Persistence) ---
|
| 378 |
+
|
| 379 |
+
import json
|
| 380 |
+
|
| 381 |
+
def create_job(job_data: Dict[str, Any]):
|
| 382 |
+
"""Create a new job record."""
|
| 383 |
+
try:
|
| 384 |
+
conn = get_db_connection()
|
| 385 |
+
c = conn.cursor()
|
| 386 |
+
c.execute('''
|
| 387 |
+
INSERT INTO jobs (id, status, result, error, created_at, storage_path, username, file_type)
|
| 388 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 389 |
+
''', (
|
| 390 |
+
job_data['id'],
|
| 391 |
+
job_data.get('status', 'pending'),
|
| 392 |
+
json.dumps(job_data.get('result')) if job_data.get('result') else None,
|
| 393 |
+
job_data.get('error'),
|
| 394 |
+
job_data['created_at'],
|
| 395 |
+
job_data.get('storage_path'),
|
| 396 |
+
job_data.get('username'),
|
| 397 |
+
job_data.get('file_type')
|
| 398 |
+
))
|
| 399 |
+
conn.commit()
|
| 400 |
+
conn.close()
|
| 401 |
+
return True
|
| 402 |
+
except Exception as e:
|
| 403 |
+
logging.error(f"Error creating job: {e}")
|
| 404 |
+
return False
|
| 405 |
+
|
| 406 |
+
def get_job(job_id: str) -> Optional[Dict[str, Any]]:
|
| 407 |
+
"""Retrieve job by ID."""
|
| 408 |
+
conn = get_db_connection()
|
| 409 |
+
c = conn.cursor()
|
| 410 |
+
c.execute('SELECT * FROM jobs WHERE id = ?', (job_id,))
|
| 411 |
+
row = c.fetchone()
|
| 412 |
+
conn.close()
|
| 413 |
+
|
| 414 |
+
if row:
|
| 415 |
+
job = dict(row)
|
| 416 |
+
if job['result']:
|
| 417 |
+
try:
|
| 418 |
+
job['result'] = json.loads(job['result'])
|
| 419 |
+
except:
|
| 420 |
+
job['result'] = None
|
| 421 |
+
return job
|
| 422 |
+
return None
|
| 423 |
+
|
| 424 |
+
def update_job_status(job_id: str, status: str, result: Optional[Dict] = None, error: Optional[str] = None):
|
| 425 |
+
"""Update job status and result."""
|
| 426 |
+
try:
|
| 427 |
+
conn = get_db_connection()
|
| 428 |
+
c = conn.cursor()
|
| 429 |
+
|
| 430 |
+
updates = ["status = ?"]
|
| 431 |
+
params = [status]
|
| 432 |
+
|
| 433 |
+
if result is not None:
|
| 434 |
+
updates.append("result = ?")
|
| 435 |
+
params.append(json.dumps(result))
|
| 436 |
+
|
| 437 |
+
if error is not None:
|
| 438 |
+
updates.append("error = ?")
|
| 439 |
+
params.append(error)
|
| 440 |
+
|
| 441 |
+
params.append(job_id)
|
| 442 |
+
|
| 443 |
+
query = f"UPDATE jobs SET {', '.join(updates)} WHERE id = ?"
|
| 444 |
+
c.execute(query, params)
|
| 445 |
+
conn.commit()
|
| 446 |
+
conn.close()
|
| 447 |
+
return True
|
| 448 |
+
except Exception as e:
|
| 449 |
+
logging.error(f"Error updating job: {e}")
|
| 450 |
+
return False
|
| 451 |
+
|
| 452 |
|
main.py
CHANGED
|
@@ -699,7 +699,7 @@ class FeedbackModel(BaseModel):
|
|
| 699 |
# =========================================================================
|
| 700 |
# GLOBAL STATE
|
| 701 |
# =========================================================================
|
| 702 |
-
jobs: Dict[str, Job] = {}
|
| 703 |
storage_provider = get_storage_provider(os.getenv("STORAGE_MODE", "LOCAL"))
|
| 704 |
|
| 705 |
# Initialize Database
|
|
@@ -1232,12 +1232,20 @@ async def limit_concurrency(request: Request, call_next):
|
|
| 1232 |
# =========================================================================
|
| 1233 |
async def process_analysis(job_id: str, image_bytes: bytes):
|
| 1234 |
"""Background task to run inference and log to registry."""
|
| 1235 |
-
job
|
|
|
|
| 1236 |
if not job:
|
|
|
|
| 1237 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1238 |
|
| 1239 |
logger.info(f"Processing Job {job_id}")
|
| 1240 |
-
|
|
|
|
| 1241 |
start_time = time.time()
|
| 1242 |
|
| 1243 |
try:
|
|
@@ -1247,27 +1255,27 @@ async def process_analysis(job_id: str, image_bytes: bytes):
|
|
| 1247 |
loop = asyncio.get_event_loop()
|
| 1248 |
result = await loop.run_in_executor(None, model_wrapper.predict, image_bytes)
|
| 1249 |
|
| 1250 |
-
job.result = result
|
| 1251 |
-
job.status = JobStatus.COMPLETED
|
| 1252 |
-
|
| 1253 |
# Calculate computation time
|
| 1254 |
computation_time_ms = int((time.time() - start_time) * 1000)
|
| 1255 |
|
|
|
|
|
|
|
|
|
|
| 1256 |
# Log to registry (REAL DATA)
|
| 1257 |
-
if
|
| 1258 |
domain = result.get('domain', {}).get('label', 'Unknown')
|
| 1259 |
top_diag = result.get('specific', [{}])[0].get('label', 'Unknown') if result.get('specific') else 'Unknown'
|
| 1260 |
confidence = result.get('specific', [{}])[0].get('probability', 0) if result.get('specific') else 0
|
| 1261 |
priority = result.get('priority', 'Normale')
|
| 1262 |
|
| 1263 |
database.log_analysis(
|
| 1264 |
-
username=
|
| 1265 |
domain=domain,
|
| 1266 |
top_diagnosis=top_diag,
|
| 1267 |
confidence=confidence,
|
| 1268 |
priority=priority,
|
| 1269 |
computation_time_ms=computation_time_ms,
|
| 1270 |
-
file_type=
|
| 1271 |
)
|
| 1272 |
logger.info(f"β
Job {job_id} logged to registry")
|
| 1273 |
|
|
@@ -1275,8 +1283,7 @@ async def process_analysis(job_id: str, image_bytes: bytes):
|
|
| 1275 |
|
| 1276 |
except Exception as e:
|
| 1277 |
logger.error(f"β Job {job_id} failed: {str(e)}")
|
| 1278 |
-
|
| 1279 |
-
job.status = JobStatus.FAILED
|
| 1280 |
|
| 1281 |
# =========================================================================
|
| 1282 |
# API ENDPOINTS
|
|
@@ -1403,15 +1410,16 @@ async def analyze_image(
|
|
| 1403 |
else:
|
| 1404 |
file_type = 'OTHER'
|
| 1405 |
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
| 1411 |
-
storage_path
|
| 1412 |
-
username
|
| 1413 |
-
file_type
|
| 1414 |
-
|
|
|
|
| 1415 |
|
| 1416 |
background_tasks.add_task(process_analysis, job_id, image_bytes)
|
| 1417 |
|
|
@@ -1425,19 +1433,19 @@ async def get_result(task_id: str, current_user: User = Depends(get_current_user
|
|
| 1425 |
- **Requires authentication**
|
| 1426 |
- Returns job status and results when complete
|
| 1427 |
"""
|
| 1428 |
-
job =
|
| 1429 |
if not job:
|
| 1430 |
logger.warning(f"Job not found: {task_id}")
|
|
|
|
|
|
|
| 1431 |
raise HTTPException(status_code=404, detail="Job not found")
|
| 1432 |
|
| 1433 |
-
# Verify ownership
|
| 1434 |
-
if job.
|
| 1435 |
-
|
| 1436 |
-
|
| 1437 |
-
logger.warning(f"Unauthorized access attempt to job {task_id} by {current_user.username}")
|
| 1438 |
-
raise HTTPException(status_code=403, detail="Access denied")
|
| 1439 |
|
| 1440 |
-
logger.info(f"Polling Job {task_id}: Status={job.status}")
|
| 1441 |
return job
|
| 1442 |
|
| 1443 |
@app.get("/health")
|
|
|
|
| 699 |
# =========================================================================
|
| 700 |
# GLOBAL STATE
|
| 701 |
# =========================================================================
|
| 702 |
+
jobs: Dict[str, Job] = {} # REMOVED: Now using SQLite persistence
|
| 703 |
storage_provider = get_storage_provider(os.getenv("STORAGE_MODE", "LOCAL"))
|
| 704 |
|
| 705 |
# Initialize Database
|
|
|
|
| 1232 |
# =========================================================================
|
| 1233 |
async def process_analysis(job_id: str, image_bytes: bytes):
|
| 1234 |
"""Background task to run inference and log to registry."""
|
| 1235 |
+
# RESILIENCE: Retrieve job from DB
|
| 1236 |
+
job = database.get_job(job_id)
|
| 1237 |
if not job:
|
| 1238 |
+
logger.error(f"β Job {job_id} not found in DB during background processing")
|
| 1239 |
return
|
| 1240 |
+
|
| 1241 |
+
# We must construct a Job object or just work with the dict
|
| 1242 |
+
# Let's work with the dict for consistency, or simple variables
|
| 1243 |
+
username = job.get('username')
|
| 1244 |
+
file_type = job.get('file_type')
|
| 1245 |
|
| 1246 |
logger.info(f"Processing Job {job_id}")
|
| 1247 |
+
database.update_job_status(job_id, JobStatus.PROCESSING.value)
|
| 1248 |
+
|
| 1249 |
start_time = time.time()
|
| 1250 |
|
| 1251 |
try:
|
|
|
|
| 1255 |
loop = asyncio.get_event_loop()
|
| 1256 |
result = await loop.run_in_executor(None, model_wrapper.predict, image_bytes)
|
| 1257 |
|
|
|
|
|
|
|
|
|
|
| 1258 |
# Calculate computation time
|
| 1259 |
computation_time_ms = int((time.time() - start_time) * 1000)
|
| 1260 |
|
| 1261 |
+
# Update Job in DB
|
| 1262 |
+
database.update_job_status(job_id, JobStatus.COMPLETED.value, result=result)
|
| 1263 |
+
|
| 1264 |
# Log to registry (REAL DATA)
|
| 1265 |
+
if username and result:
|
| 1266 |
domain = result.get('domain', {}).get('label', 'Unknown')
|
| 1267 |
top_diag = result.get('specific', [{}])[0].get('label', 'Unknown') if result.get('specific') else 'Unknown'
|
| 1268 |
confidence = result.get('specific', [{}])[0].get('probability', 0) if result.get('specific') else 0
|
| 1269 |
priority = result.get('priority', 'Normale')
|
| 1270 |
|
| 1271 |
database.log_analysis(
|
| 1272 |
+
username=username,
|
| 1273 |
domain=domain,
|
| 1274 |
top_diagnosis=top_diag,
|
| 1275 |
confidence=confidence,
|
| 1276 |
priority=priority,
|
| 1277 |
computation_time_ms=computation_time_ms,
|
| 1278 |
+
file_type=file_type or 'Unknown'
|
| 1279 |
)
|
| 1280 |
logger.info(f"β
Job {job_id} logged to registry")
|
| 1281 |
|
|
|
|
| 1283 |
|
| 1284 |
except Exception as e:
|
| 1285 |
logger.error(f"β Job {job_id} failed: {str(e)}")
|
| 1286 |
+
database.update_job_status(job_id, JobStatus.FAILED.value, error=str(e))
|
|
|
|
| 1287 |
|
| 1288 |
# =========================================================================
|
| 1289 |
# API ENDPOINTS
|
|
|
|
| 1410 |
else:
|
| 1411 |
file_type = 'OTHER'
|
| 1412 |
|
| 1413 |
+
# Persist Job to DB
|
| 1414 |
+
job_data = {
|
| 1415 |
+
"id": job_id,
|
| 1416 |
+
"status": JobStatus.PENDING.value,
|
| 1417 |
+
"created_at": time.time(),
|
| 1418 |
+
"storage_path": storage_path,
|
| 1419 |
+
"username": current_user.username,
|
| 1420 |
+
"file_type": file_type
|
| 1421 |
+
}
|
| 1422 |
+
database.create_job(job_data)
|
| 1423 |
|
| 1424 |
background_tasks.add_task(process_analysis, job_id, image_bytes)
|
| 1425 |
|
|
|
|
| 1433 |
- **Requires authentication**
|
| 1434 |
- Returns job status and results when complete
|
| 1435 |
"""
|
| 1436 |
+
job = database.get_job(task_id)
|
| 1437 |
if not job:
|
| 1438 |
logger.warning(f"Job not found: {task_id}")
|
| 1439 |
+
# If job is lost (server restart before persistence, or bad ID), return 404
|
| 1440 |
+
# Frontend should handle this by stopping polling
|
| 1441 |
raise HTTPException(status_code=404, detail="Job not found")
|
| 1442 |
|
| 1443 |
+
# Verify ownership
|
| 1444 |
+
if job.get('username') != current_user.username:
|
| 1445 |
+
logger.warning(f"Unauthorized access attempt to job {task_id} by {current_user.username}")
|
| 1446 |
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
|
|
|
| 1447 |
|
| 1448 |
+
logger.info(f"Polling Job {task_id}: Status={job.get('status')}")
|
| 1449 |
return job
|
| 1450 |
|
| 1451 |
@app.get("/health")
|
upload_space.py
CHANGED
|
@@ -17,5 +17,5 @@ upload_folder(
|
|
| 17 |
ignore_patterns=["models/*", "*.pyc", "__pycache__", "*.db", "storage/*", "data_storage/*", ".env", "venv", ".git", ".idea"]
|
| 18 |
)
|
| 19 |
|
| 20 |
-
print("
|
| 21 |
print("Your Space should start building at: https://huggingface.co/spaces/issoufzousko07/elephmind-api")
|
|
|
|
| 17 |
ignore_patterns=["models/*", "*.pyc", "__pycache__", "*.db", "storage/*", "data_storage/*", ".env", "venv", ".git", ".idea"]
|
| 18 |
)
|
| 19 |
|
| 20 |
+
print("[OK] Upload complete!")
|
| 21 |
print("Your Space should start building at: https://huggingface.co/spaces/issoufzousko07/elephmind-api")
|