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:
copilot-swe-agent[bot]
2026-02-08 17:42:33 +00:00
parent 7827b97e06
commit d08040ac4a
73 changed files with 2200 additions and 2185 deletions
+14 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)