style: fix all flake8 linter errors across app/ directory
- Run Black formatter and isort on all app/ files - Remove unused imports (F401) across multiple files - Add # noqa: F401 for intentional re-exports in celery_worker.py, tasks/__init__.py, utils.py, frontend.py, views/base.py - Fix f-strings without placeholders (F541) in azure.py, notification.py, check_credentials.py, upload_to_onedrive.py, settings.py - Fix bare except (E722) in upload_to_sftp.py - Fix block comment format (E265) in models.py - Move imports to top of file to fix E402 in celery_app.py, celery_worker.py - Fix line-too-long (E501) by wrapping strings in multiple files - Remove unused variable (F841) in upload_to_nextcloud.py Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+14
-11
@@ -1,21 +1,24 @@
|
||||
"""
|
||||
API Router module that combines all API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.azure import router as azure_router
|
||||
from app.api.diagnostic import router as diagnostic_router
|
||||
from app.api.dropbox import router as dropbox_router
|
||||
from app.api.files import router as files_router
|
||||
from app.api.google_drive import router as google_drive_router
|
||||
from app.api.logs import router as logs_router
|
||||
from app.api.onedrive import router as onedrive_router
|
||||
from app.api.openai import router as openai_router
|
||||
from app.api.process import router as process_router
|
||||
from app.api.settings import router as settings_router
|
||||
|
||||
# Import all the individual routers
|
||||
from app.api.user import router as user_router
|
||||
from app.api.files import router as files_router
|
||||
from app.api.process import router as process_router
|
||||
from app.api.diagnostic import router as diagnostic_router
|
||||
from app.api.onedrive import router as onedrive_router
|
||||
from app.api.dropbox import router as dropbox_router
|
||||
from app.api.openai import router as openai_router
|
||||
from app.api.azure import router as azure_router
|
||||
from app.api.google_drive import router as google_drive_router
|
||||
from app.api.logs import router as logs_router
|
||||
from app.api.settings import router as settings_router
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+35
-45
@@ -1,24 +1,25 @@
|
||||
"""
|
||||
Azure AI API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Request, HTTPException, status
|
||||
import logging
|
||||
import os
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
import logging
|
||||
|
||||
import azure.core.exceptions
|
||||
from azure.ai.documentintelligence import DocumentIntelligenceAdministrationClient
|
||||
|
||||
# Import the Azure modules including the administration client
|
||||
from azure.core.credentials import AzureKeyCredential
|
||||
from azure.ai.documentintelligence import DocumentIntelligenceClient
|
||||
from azure.ai.documentintelligence import DocumentIntelligenceAdministrationClient
|
||||
import azure.core.exceptions
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/azure/test")
|
||||
@require_login
|
||||
async def test_azure_connection(request: Request):
|
||||
@@ -28,7 +29,7 @@ async def test_azure_connection(request: Request):
|
||||
"""
|
||||
try:
|
||||
logger.info("Testing Azure Document Intelligence connection")
|
||||
|
||||
|
||||
# Check if Azure configuration is present
|
||||
if not settings.azure_endpoint or not settings.azure_ai_key:
|
||||
logger.warning("Azure Document Intelligence configuration is incomplete")
|
||||
@@ -37,88 +38,77 @@ async def test_azure_connection(request: Request):
|
||||
missing.append("endpoint")
|
||||
if not settings.azure_ai_key:
|
||||
missing.append("API key")
|
||||
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Azure Document Intelligence configuration is incomplete. Missing: {', '.join(missing)}"
|
||||
"message": f"Azure Document Intelligence configuration is incomplete. Missing: {', '.join(missing)}",
|
||||
}
|
||||
|
||||
|
||||
# Try to initialize the admin client and make a request to list operations
|
||||
try:
|
||||
# Initialize the admin client with credentials
|
||||
admin_client = DocumentIntelligenceAdministrationClient(
|
||||
endpoint=settings.azure_endpoint,
|
||||
credential=AzureKeyCredential(settings.azure_ai_key)
|
||||
endpoint=settings.azure_endpoint, credential=AzureKeyCredential(settings.azure_ai_key)
|
||||
)
|
||||
|
||||
|
||||
# Test the connection by listing operations - this is a documented method in the admin client
|
||||
operations = list(admin_client.list_operations())
|
||||
|
||||
|
||||
# Successfully initialized client and made a request
|
||||
logger.info("Azure Document Intelligence Admin connection successfully tested")
|
||||
|
||||
|
||||
# Return success with available operations info
|
||||
operations_info = []
|
||||
try:
|
||||
for op in operations:
|
||||
if hasattr(op, 'operation_id') and op.operation_id:
|
||||
if hasattr(op, "operation_id") and op.operation_id:
|
||||
op_info = {
|
||||
"id": op.operation_id,
|
||||
"status": op.status if hasattr(op, 'status') else "Unknown",
|
||||
"created": str(op.created_on) if hasattr(op, 'created_on') else "Unknown",
|
||||
"kind": op.kind if hasattr(op, 'kind') else "Unknown"
|
||||
"status": op.status if hasattr(op, "status") else "Unknown",
|
||||
"created": str(op.created_on) if hasattr(op, "created_on") else "Unknown",
|
||||
"kind": op.kind if hasattr(op, "kind") else "Unknown",
|
||||
}
|
||||
operations_info.append(op_info)
|
||||
|
||||
|
||||
operation_count = len(operations_info)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Azure Document Intelligence connection is valid. Found {operation_count} operations.",
|
||||
"endpoint": settings.azure_endpoint,
|
||||
"operations_count": operation_count,
|
||||
"recent_operations": operations_info[:3] if operations_info else []
|
||||
"recent_operations": operations_info[:3] if operations_info else [],
|
||||
}
|
||||
except Exception as e:
|
||||
# If error occurs while processing operations info, still return success
|
||||
logger.warning(f"Connected to Azure but couldn't parse operations: {e}")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Azure Document Intelligence connection is valid, but couldn't retrieve operations details.",
|
||||
"endpoint": settings.azure_endpoint
|
||||
"message": "Azure Document Intelligence connection is valid, "
|
||||
"but couldn't retrieve operations details.",
|
||||
"endpoint": settings.azure_endpoint,
|
||||
}
|
||||
|
||||
|
||||
except azure.core.exceptions.ClientAuthenticationError as e:
|
||||
logger.error(f"Azure authentication error: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Authentication error: Invalid API key or credentials",
|
||||
"detail": str(e)
|
||||
"message": "Authentication error: Invalid API key or credentials",
|
||||
"detail": str(e),
|
||||
}
|
||||
except azure.core.exceptions.ServiceRequestError as e:
|
||||
logger.error(f"Azure service request error: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Service request error: Could not reach the Azure endpoint",
|
||||
"detail": str(e)
|
||||
"message": "Service request error: Could not reach the Azure endpoint",
|
||||
"detail": str(e),
|
||||
}
|
||||
except ValueError as e:
|
||||
logger.error(f"Azure configuration value error: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Configuration error: {str(e)}",
|
||||
"detail": str(e)
|
||||
}
|
||||
return {"status": "error", "message": f"Configuration error: {str(e)}", "detail": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"Azure connection test failed with unexpected error: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Connection test failed with unexpected error",
|
||||
"detail": str(e)
|
||||
}
|
||||
|
||||
return {"status": "error", "message": "Connection test failed with unexpected error", "detail": str(e)}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error testing Azure Document Intelligence connection")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected error: {str(e)}"
|
||||
}
|
||||
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
|
||||
|
||||
+2
-1
@@ -5,10 +5,11 @@ Common utilities for API routes
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.database import SessionLocal
|
||||
from app.config import settings
|
||||
from app.database import SessionLocal
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+39
-33
@@ -1,10 +1,12 @@
|
||||
"""
|
||||
Diagnostic API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
|
||||
import logging
|
||||
|
||||
from app.auth import require_login, get_current_user
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from app.auth import get_current_user, require_login
|
||||
from app.config import settings
|
||||
|
||||
# Set up logging
|
||||
@@ -12,6 +14,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/diagnostic/settings")
|
||||
@require_login
|
||||
async def diagnostic_settings(request: Request, current_user: dict = Depends(get_current_user)):
|
||||
@@ -19,82 +22,85 @@ async def diagnostic_settings(request: Request, current_user: dict = Depends(get
|
||||
API endpoint to dump settings to the log and view basic config information
|
||||
This endpoint doesn't expose sensitive information like passwords or tokens
|
||||
"""
|
||||
from app.utils.config_validator import dump_all_settings, get_settings_for_display
|
||||
from app.utils.config_validator import dump_all_settings
|
||||
|
||||
# Dump full settings to log for admin to see
|
||||
dump_all_settings()
|
||||
|
||||
|
||||
# Return safe subset of settings for API response
|
||||
safe_settings = {
|
||||
"workdir": settings.workdir,
|
||||
"external_hostname": settings.external_hostname,
|
||||
"configured_services": {
|
||||
"email": bool(getattr(settings, 'email_host', None)),
|
||||
"s3": bool(getattr(settings, 's3_bucket_name', None)),
|
||||
"dropbox": bool(getattr(settings, 'dropbox_refresh_token', None)),
|
||||
"onedrive": bool(getattr(settings, 'onedrive_refresh_token', None)),
|
||||
"nextcloud": bool(getattr(settings, 'nextcloud_upload_url', None)),
|
||||
"sftp": bool(getattr(settings, 'sftp_host', None)),
|
||||
"paperless": bool(getattr(settings, 'paperless_host', None)),
|
||||
"google_drive": bool(getattr(settings, 'google_drive_credentials_json', None)),
|
||||
"uptime_kuma": bool(getattr(settings, 'uptime_kuma_url', None)),
|
||||
"auth": bool(getattr(settings, 'authentik_config_url', None)),
|
||||
"openai": bool(getattr(settings, 'openai_api_key', None)),
|
||||
"azure": bool(getattr(settings, 'azure_api_key', None) and getattr(settings, 'azure_endpoint', None)),
|
||||
"email": bool(getattr(settings, "email_host", None)),
|
||||
"s3": bool(getattr(settings, "s3_bucket_name", None)),
|
||||
"dropbox": bool(getattr(settings, "dropbox_refresh_token", None)),
|
||||
"onedrive": bool(getattr(settings, "onedrive_refresh_token", None)),
|
||||
"nextcloud": bool(getattr(settings, "nextcloud_upload_url", None)),
|
||||
"sftp": bool(getattr(settings, "sftp_host", None)),
|
||||
"paperless": bool(getattr(settings, "paperless_host", None)),
|
||||
"google_drive": bool(getattr(settings, "google_drive_credentials_json", None)),
|
||||
"uptime_kuma": bool(getattr(settings, "uptime_kuma_url", None)),
|
||||
"auth": bool(getattr(settings, "authentik_config_url", None)),
|
||||
"openai": bool(getattr(settings, "openai_api_key", None)),
|
||||
"azure": bool(getattr(settings, "azure_api_key", None) and getattr(settings, "azure_endpoint", None)),
|
||||
},
|
||||
"imap_enabled": bool(getattr(settings, 'imap1_host', None) or getattr(settings, 'imap2_host', None)),
|
||||
"imap_enabled": bool(getattr(settings, "imap1_host", None) or getattr(settings, "imap2_host", None)),
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"settings": safe_settings,
|
||||
"message": "Full settings have been dumped to application logs"
|
||||
"message": "Full settings have been dumped to application logs",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/diagnostic/test-notification")
|
||||
@require_login
|
||||
async def test_notification(request: Request):
|
||||
# Add request_time to request.state
|
||||
import datetime
|
||||
|
||||
request.state.request_time = datetime.datetime.utcnow().isoformat()
|
||||
"""
|
||||
Send a test notification through all configured notification channels
|
||||
"""
|
||||
from app.utils.notification import send_notification
|
||||
|
||||
|
||||
try:
|
||||
notification_urls = getattr(settings, 'notification_urls', [])
|
||||
notification_urls = getattr(settings, "notification_urls", [])
|
||||
if not notification_urls:
|
||||
return {
|
||||
"status": "warning",
|
||||
"message": "No notification services configured. Add notification URLs to your configuration."
|
||||
"message": "No notification services configured. Add notification URLs to your configuration.",
|
||||
}
|
||||
|
||||
|
||||
# Send a test notification
|
||||
hostname = settings.external_hostname or "Document Processor"
|
||||
result = send_notification(
|
||||
title=f"Test Notification from {hostname}",
|
||||
message=f"This is a test notification sent at {request.state.request_time}. If you're receiving this, notifications are working!",
|
||||
message=(
|
||||
f"This is a test notification sent at {request.state.request_time}. "
|
||||
"If you're receiving this, notifications are working!"
|
||||
),
|
||||
notification_type="success",
|
||||
tags=["test", "notification", "diagnostic"]
|
||||
tags=["test", "notification", "diagnostic"],
|
||||
)
|
||||
|
||||
|
||||
if result:
|
||||
logger.info("Test notification sent successfully")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Test notification sent successfully to {len(notification_urls)} service(s)",
|
||||
"services_count": len(notification_urls)
|
||||
"services_count": len(notification_urls),
|
||||
}
|
||||
else:
|
||||
logger.warning("Test notification send attempt returned False")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Failed to send test notification. Check application logs for details."
|
||||
"message": "Failed to send test notification. Check application logs for details.",
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error sending test notification: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Error sending notification: {str(e)}"
|
||||
}
|
||||
return {"status": "error", "message": f"Error sending notification: {str(e)}"}
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@
|
||||
Dropbox API endpoints
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, status, Form
|
||||
import logging
|
||||
import os
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, Form, HTTPException, Request, status
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
|
||||
+8
-8
@@ -435,15 +435,15 @@ def retry_subtask(
|
||||
|
||||
# Map subtask names to their corresponding Celery tasks
|
||||
from app.tasks.upload_to_dropbox import upload_to_dropbox
|
||||
from app.tasks.upload_to_nextcloud import upload_to_nextcloud
|
||||
from app.tasks.upload_to_paperless import upload_to_paperless
|
||||
from app.tasks.upload_to_google_drive import upload_to_google_drive
|
||||
from app.tasks.upload_to_onedrive import upload_to_onedrive
|
||||
from app.tasks.upload_to_s3 import upload_to_s3
|
||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
||||
from app.tasks.upload_to_ftp import upload_to_ftp
|
||||
from app.tasks.upload_to_sftp import upload_to_sftp
|
||||
from app.tasks.upload_to_email import upload_to_email
|
||||
from app.tasks.upload_to_ftp import upload_to_ftp
|
||||
from app.tasks.upload_to_google_drive import upload_to_google_drive
|
||||
from app.tasks.upload_to_nextcloud import upload_to_nextcloud
|
||||
from app.tasks.upload_to_onedrive import upload_to_onedrive
|
||||
from app.tasks.upload_to_paperless import upload_to_paperless
|
||||
from app.tasks.upload_to_s3 import upload_to_s3
|
||||
from app.tasks.upload_to_sftp import upload_to_sftp
|
||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
||||
|
||||
task_map = {
|
||||
"upload_to_dropbox": upload_to_dropbox,
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
Google Drive API endpoints
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, status, Form
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, Request, status
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
|
||||
+65
-76
@@ -1,21 +1,24 @@
|
||||
"""
|
||||
Processing logs API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Request, HTTPException, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
from app.auth import require_login
|
||||
from app.models import ProcessingLog, FileRecord
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.common import get_db
|
||||
from app.auth import require_login
|
||||
from app.models import FileRecord, ProcessingLog
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/logs")
|
||||
@require_login
|
||||
def list_processing_logs(
|
||||
@@ -23,17 +26,17 @@ def list_processing_logs(
|
||||
db: Session = Depends(get_db),
|
||||
file_id: Optional[int] = Query(None, description="Filter by file ID"),
|
||||
task_id: Optional[str] = Query(None, description="Filter by task ID"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Number of logs to return")
|
||||
limit: int = Query(100, ge=1, le=1000, description="Number of logs to return"),
|
||||
):
|
||||
"""
|
||||
Returns a JSON list of ProcessingLog entries.
|
||||
Protected by `@require_login`, so only logged-in sessions can access.
|
||||
|
||||
|
||||
Query Parameters:
|
||||
- file_id: Optional filter by file ID
|
||||
- task_id: Optional filter by task ID
|
||||
- limit: Maximum number of logs to return (default 100, max 1000)
|
||||
|
||||
|
||||
Example response:
|
||||
[
|
||||
{
|
||||
@@ -49,116 +52,102 @@ def list_processing_logs(
|
||||
]
|
||||
"""
|
||||
query = db.query(ProcessingLog)
|
||||
|
||||
|
||||
# Apply filters
|
||||
if file_id is not None:
|
||||
query = query.filter(ProcessingLog.file_id == file_id)
|
||||
if task_id is not None:
|
||||
query = query.filter(ProcessingLog.task_id == task_id)
|
||||
|
||||
|
||||
# Order by timestamp descending and limit
|
||||
logs = query.order_by(desc(ProcessingLog.timestamp)).limit(limit).all()
|
||||
|
||||
|
||||
# Return a simple list of dicts
|
||||
result = []
|
||||
for log in logs:
|
||||
result.append({
|
||||
"id": log.id,
|
||||
"file_id": log.file_id,
|
||||
"task_id": log.task_id,
|
||||
"step_name": log.step_name,
|
||||
"status": log.status,
|
||||
"message": log.message,
|
||||
"timestamp": log.timestamp.isoformat() if log.timestamp else None
|
||||
})
|
||||
result.append(
|
||||
{
|
||||
"id": log.id,
|
||||
"file_id": log.file_id,
|
||||
"task_id": log.task_id,
|
||||
"step_name": log.step_name,
|
||||
"status": log.status,
|
||||
"message": log.message,
|
||||
"timestamp": log.timestamp.isoformat() if log.timestamp else None,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/logs/file/{file_id}")
|
||||
@require_login
|
||||
def get_file_processing_logs(
|
||||
request: Request,
|
||||
file_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
def get_file_processing_logs(request: Request, file_id: int, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Get all processing logs for a specific file.
|
||||
Returns logs ordered by timestamp (oldest first to show processing flow).
|
||||
|
||||
|
||||
Also includes file metadata if the file exists.
|
||||
"""
|
||||
# Check if file exists
|
||||
file_record = db.query(FileRecord).filter(FileRecord.id == file_id).first()
|
||||
if not file_record:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"File with ID {file_id} not found"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=404, detail=f"File with ID {file_id} not found")
|
||||
|
||||
# Get all logs for this file
|
||||
logs = db.query(ProcessingLog).filter(
|
||||
ProcessingLog.file_id == file_id
|
||||
).order_by(ProcessingLog.timestamp).all()
|
||||
|
||||
logs = db.query(ProcessingLog).filter(ProcessingLog.file_id == file_id).order_by(ProcessingLog.timestamp).all()
|
||||
|
||||
# Build response
|
||||
log_list = []
|
||||
for log in logs:
|
||||
log_list.append({
|
||||
"id": log.id,
|
||||
"task_id": log.task_id,
|
||||
"step_name": log.step_name,
|
||||
"status": log.status,
|
||||
"message": log.message,
|
||||
"timestamp": log.timestamp.isoformat() if log.timestamp else None
|
||||
})
|
||||
|
||||
log_list.append(
|
||||
{
|
||||
"id": log.id,
|
||||
"task_id": log.task_id,
|
||||
"step_name": log.step_name,
|
||||
"status": log.status,
|
||||
"message": log.message,
|
||||
"timestamp": log.timestamp.isoformat() if log.timestamp else None,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"file": {
|
||||
"id": file_record.id,
|
||||
"original_filename": file_record.original_filename,
|
||||
"file_size": file_record.file_size,
|
||||
"mime_type": file_record.mime_type,
|
||||
"created_at": file_record.created_at.isoformat() if file_record.created_at else None
|
||||
"created_at": file_record.created_at.isoformat() if file_record.created_at else None,
|
||||
},
|
||||
"logs": log_list,
|
||||
"total_logs": len(log_list)
|
||||
"total_logs": len(log_list),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/logs/task/{task_id}")
|
||||
@require_login
|
||||
def get_task_processing_logs(
|
||||
request: Request,
|
||||
task_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
def get_task_processing_logs(request: Request, task_id: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Get all processing logs for a specific task.
|
||||
Returns logs ordered by timestamp (oldest first to show processing flow).
|
||||
"""
|
||||
# Get all logs for this task
|
||||
logs = db.query(ProcessingLog).filter(
|
||||
ProcessingLog.task_id == task_id
|
||||
).order_by(ProcessingLog.timestamp).all()
|
||||
|
||||
logs = db.query(ProcessingLog).filter(ProcessingLog.task_id == task_id).order_by(ProcessingLog.timestamp).all()
|
||||
|
||||
if not logs:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No logs found for task {task_id}"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=404, detail=f"No logs found for task {task_id}")
|
||||
|
||||
# Build response
|
||||
log_list = []
|
||||
for log in logs:
|
||||
log_list.append({
|
||||
"id": log.id,
|
||||
"file_id": log.file_id,
|
||||
"step_name": log.step_name,
|
||||
"status": log.status,
|
||||
"message": log.message,
|
||||
"timestamp": log.timestamp.isoformat() if log.timestamp else None
|
||||
})
|
||||
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"logs": log_list,
|
||||
"total_logs": len(log_list)
|
||||
}
|
||||
log_list.append(
|
||||
{
|
||||
"id": log.id,
|
||||
"file_id": log.file_id,
|
||||
"step_name": log.step_name,
|
||||
"status": log.status,
|
||||
"message": log.message,
|
||||
"timestamp": log.timestamp.isoformat() if log.timestamp else None,
|
||||
}
|
||||
)
|
||||
|
||||
return {"task_id": task_id, "logs": log_list, "total_logs": len(log_list)}
|
||||
|
||||
+3
-2
@@ -2,12 +2,13 @@
|
||||
OneDrive API endpoints
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, status, Form
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, Form, HTTPException, Request, status
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
from app.utils.oauth_helper import exchange_oauth_token
|
||||
|
||||
+17
-25
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
OpenAI API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Request, HTTPException, status
|
||||
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
@@ -14,6 +14,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/openai/test")
|
||||
@require_login
|
||||
async def test_openai_connection(request: Request):
|
||||
@@ -22,54 +23,45 @@ async def test_openai_connection(request: Request):
|
||||
"""
|
||||
try:
|
||||
import openai
|
||||
|
||||
|
||||
logger.info("Testing OpenAI API key validity")
|
||||
|
||||
|
||||
# Check if API key is configured
|
||||
if not settings.openai_api_key:
|
||||
logger.warning("No OpenAI API key configured")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No OpenAI API key is configured"
|
||||
}
|
||||
|
||||
return {"status": "error", "message": "No OpenAI API key is configured"}
|
||||
|
||||
# Configure the client
|
||||
client = openai.OpenAI(api_key=settings.openai_api_key)
|
||||
|
||||
|
||||
# Try to make a simple request to validate the key
|
||||
try:
|
||||
# Use a models list endpoint as a simple validation
|
||||
models = client.models.list()
|
||||
|
||||
|
||||
# If we got here, the key is valid
|
||||
logger.info("OpenAI API key is valid")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "OpenAI API key is valid",
|
||||
"models_available": len(models.data) if hasattr(models, "data") else "Unknown"
|
||||
"models_available": len(models.data) if hasattr(models, "data") else "Unknown",
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"OpenAI API key test failed: {error_msg}")
|
||||
|
||||
|
||||
# Determine if this is an authentication error
|
||||
is_auth_error = "auth" in error_msg.lower() or "api key" in error_msg.lower()
|
||||
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"API key validation failed: {error_msg}",
|
||||
"is_auth_error": is_auth_error
|
||||
"is_auth_error": is_auth_error,
|
||||
}
|
||||
|
||||
|
||||
except ImportError:
|
||||
logger.exception("OpenAI package not installed")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "OpenAI package not installed"
|
||||
}
|
||||
return {"status": "error", "message": "OpenAI package not installed"}
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error testing OpenAI connection")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected error: {str(e)}"
|
||||
}
|
||||
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
|
||||
|
||||
+50
-109
@@ -3,21 +3,22 @@ API endpoints for managing application settings.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.utils.settings_service import (
|
||||
get_all_settings_from_db,
|
||||
save_setting_to_db,
|
||||
SETTING_METADATA,
|
||||
delete_setting_from_db,
|
||||
get_all_settings_from_db,
|
||||
get_setting_metadata,
|
||||
get_settings_by_category,
|
||||
save_setting_to_db,
|
||||
validate_setting_value,
|
||||
SETTING_METADATA,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -28,27 +29,26 @@ def require_admin(request: Request) -> dict:
|
||||
"""
|
||||
Dependency to ensure the user is an admin.
|
||||
Raises HTTPException if not admin.
|
||||
|
||||
|
||||
Returns:
|
||||
User dict from session
|
||||
"""
|
||||
user = request.session.get("user")
|
||||
if not user or not user.get("is_admin"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin access required"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
|
||||
return user
|
||||
|
||||
|
||||
class SettingUpdate(BaseModel):
|
||||
"""Model for updating a setting"""
|
||||
|
||||
key: str = Field(..., description="Setting key")
|
||||
value: Optional[str] = Field(None, description="Setting value (None to delete)")
|
||||
|
||||
|
||||
class SettingResponse(BaseModel):
|
||||
"""Model for setting response"""
|
||||
|
||||
key: str
|
||||
value: Optional[str]
|
||||
metadata: Dict[str, Any]
|
||||
@@ -56,17 +56,14 @@ class SettingResponse(BaseModel):
|
||||
|
||||
class SettingsListResponse(BaseModel):
|
||||
"""Model for list of settings"""
|
||||
|
||||
settings: Dict[str, Any]
|
||||
categories: Dict[str, list]
|
||||
db_settings: Dict[str, str]
|
||||
|
||||
|
||||
@router.get("/", response_model=SettingsListResponse)
|
||||
async def get_settings(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
admin: dict = Depends(require_admin)
|
||||
):
|
||||
async def get_settings(request: Request, db: Session = Depends(get_db), admin: dict = Depends(require_admin)):
|
||||
"""
|
||||
Get all application settings with metadata.
|
||||
Admin only.
|
||||
@@ -77,37 +74,22 @@ async def get_settings(
|
||||
for key in SETTING_METADATA.keys():
|
||||
if hasattr(settings, key):
|
||||
value = getattr(settings, key)
|
||||
current_settings[key] = {
|
||||
"value": value,
|
||||
"metadata": get_setting_metadata(key)
|
||||
}
|
||||
|
||||
current_settings[key] = {"value": value, "metadata": get_setting_metadata(key)}
|
||||
|
||||
# Get settings stored in database
|
||||
db_settings = get_all_settings_from_db(db)
|
||||
|
||||
|
||||
# Get settings organized by category
|
||||
categories = get_settings_by_category()
|
||||
|
||||
return SettingsListResponse(
|
||||
settings=current_settings,
|
||||
categories=categories,
|
||||
db_settings=db_settings
|
||||
)
|
||||
|
||||
return SettingsListResponse(settings=current_settings, categories=categories, db_settings=db_settings)
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving settings: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve settings"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve settings")
|
||||
|
||||
|
||||
@router.get("/{key}", response_model=SettingResponse)
|
||||
async def get_setting(
|
||||
key: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
admin: dict = Depends(require_admin)
|
||||
):
|
||||
async def get_setting(key: str, request: Request, db: Session = Depends(get_db), admin: dict = Depends(require_admin)):
|
||||
"""
|
||||
Get a specific setting by key.
|
||||
Admin only.
|
||||
@@ -115,20 +97,15 @@ async def get_setting(
|
||||
try:
|
||||
# Get current value
|
||||
value = getattr(settings, key, None)
|
||||
|
||||
|
||||
# Get metadata
|
||||
metadata = get_setting_metadata(key)
|
||||
|
||||
return SettingResponse(
|
||||
key=key,
|
||||
value=str(value) if value is not None else None,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
return SettingResponse(key=key, value=str(value) if value is not None else None, metadata=metadata)
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving setting {key}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to retrieve setting: {key}"
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to retrieve setting: {key}"
|
||||
)
|
||||
|
||||
|
||||
@@ -138,7 +115,7 @@ async def update_setting(
|
||||
setting: SettingUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
admin: dict = Depends(require_admin)
|
||||
admin: dict = Depends(require_admin),
|
||||
):
|
||||
"""
|
||||
Update a specific setting.
|
||||
@@ -149,46 +126,38 @@ async def update_setting(
|
||||
if setting.value is not None:
|
||||
is_valid, error_message = validate_setting_value(key, setting.value)
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=error_message
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_message)
|
||||
|
||||
# Save to database
|
||||
success = save_setting_to_db(db, key, setting.value)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to save setting to database"
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to save setting to database"
|
||||
)
|
||||
|
||||
|
||||
# Get metadata
|
||||
metadata = get_setting_metadata(key)
|
||||
restart_required = metadata.get("restart_required", False)
|
||||
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Setting '{key}' updated successfully",
|
||||
"restart_required": restart_required,
|
||||
"key": key,
|
||||
"value": setting.value
|
||||
"value": setting.value,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating setting {key}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update setting: {key}"
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update setting: {key}"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{key}")
|
||||
async def delete_setting(
|
||||
key: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
admin: dict = Depends(require_admin)
|
||||
key: str, request: Request, db: Session = Depends(get_db), admin: dict = Depends(require_admin)
|
||||
):
|
||||
"""
|
||||
Delete a setting from the database (reverts to environment variable or default).
|
||||
@@ -197,31 +166,24 @@ async def delete_setting(
|
||||
try:
|
||||
success = delete_setting_from_db(db, key)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Setting '{key}' not found in database"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Setting '{key}' not found in database")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Setting '{key}' deleted from database (will use environment variable or default)"
|
||||
"message": f"Setting '{key}' deleted from database (will use environment variable or default)",
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting setting {key}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete setting: {key}"
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete setting: {key}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/bulk-update")
|
||||
async def bulk_update_settings(
|
||||
updates: list[SettingUpdate],
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
admin: dict = Depends(require_admin)
|
||||
updates: list[SettingUpdate], request: Request, db: Session = Depends(get_db), admin: dict = Depends(require_admin)
|
||||
):
|
||||
"""
|
||||
Update multiple settings at once.
|
||||
@@ -229,47 +191,26 @@ async def bulk_update_settings(
|
||||
"""
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
|
||||
for update in updates:
|
||||
try:
|
||||
# Validate the setting value
|
||||
if update.value is not None:
|
||||
is_valid, error_message = validate_setting_value(update.key, update.value)
|
||||
if not is_valid:
|
||||
errors.append({
|
||||
"key": update.key,
|
||||
"error": error_message
|
||||
})
|
||||
errors.append({"key": update.key, "error": error_message})
|
||||
continue
|
||||
|
||||
|
||||
# Save to database
|
||||
success = save_setting_to_db(db, update.key, update.value)
|
||||
if success:
|
||||
results.append({
|
||||
"key": update.key,
|
||||
"value": update.value,
|
||||
"status": "success"
|
||||
})
|
||||
results.append({"key": update.key, "value": update.value, "status": "success"})
|
||||
else:
|
||||
errors.append({
|
||||
"key": update.key,
|
||||
"error": "Failed to save to database"
|
||||
})
|
||||
errors.append({"key": update.key, "error": "Failed to save to database"})
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating setting {update.key}: {e}")
|
||||
errors.append({
|
||||
"key": update.key,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
restart_required = any(
|
||||
get_setting_metadata(result["key"]).get("restart_required", False)
|
||||
for result in results
|
||||
)
|
||||
|
||||
return {
|
||||
"success": len(errors) == 0,
|
||||
"updated": results,
|
||||
"errors": errors,
|
||||
"restart_required": restart_required
|
||||
}
|
||||
errors.append({"key": update.key, "error": str(e)})
|
||||
|
||||
restart_required = any(get_setting_metadata(result["key"]).get("restart_required", False) for result in results)
|
||||
|
||||
return {"success": len(errors) == 0, "updated": results, "errors": errors, "restart_required": restart_required}
|
||||
|
||||
+9
-4
@@ -1,15 +1,18 @@
|
||||
"""
|
||||
User-related API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from hashlib import md5
|
||||
|
||||
import logging
|
||||
from hashlib import md5
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def whoami_handler(request: Request):
|
||||
"""
|
||||
Returns user info if logged in, else 401.
|
||||
@@ -26,18 +29,20 @@ async def whoami_handler(request: Request):
|
||||
# MD5 is used here for Gravatar's URL generation (not for security), so usedforsecurity=False
|
||||
email_hash = md5(email.strip().lower().encode(), usedforsecurity=False).hexdigest()
|
||||
gravatar_url = f"https://www.gravatar.com/avatar/{email_hash}?d=identicon"
|
||||
|
||||
|
||||
# Add the gravatar URL to the user object instead of creating a new response
|
||||
user_response = user.copy() # Create a copy to avoid modifying the session
|
||||
user_response["picture"] = gravatar_url
|
||||
|
||||
|
||||
return user_response
|
||||
|
||||
|
||||
# Register the same handler under two different paths
|
||||
@router.get("/whoami")
|
||||
async def whoami(request: Request):
|
||||
return await whoami_handler(request)
|
||||
|
||||
|
||||
@router.get("/auth/whoami")
|
||||
async def auth_whoami(request: Request):
|
||||
return await whoami_handler(request)
|
||||
|
||||
Reference in New Issue
Block a user