Apply Black and isort formatting to modified files
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+39
-50
@@ -1,26 +1,29 @@
|
||||
"""
|
||||
Document processing API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from app.api.common import resolve_file_path
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
from app.api.common import resolve_file_path
|
||||
from app.tasks.process_document import process_document
|
||||
from app.tasks.upload_to_dropbox import upload_to_dropbox
|
||||
from app.tasks.upload_to_paperless import upload_to_paperless
|
||||
from app.tasks.upload_to_nextcloud import upload_to_nextcloud
|
||||
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.send_to_all import send_to_all_destinations
|
||||
from app.tasks.upload_to_dropbox import upload_to_dropbox
|
||||
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
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/process/")
|
||||
@require_login
|
||||
def process(file_path: str):
|
||||
@@ -28,101 +31,92 @@ def process(file_path: str):
|
||||
file_path = resolve_file_path(file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"File {file_path} not found."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"File {file_path} not found.")
|
||||
|
||||
task = process_document.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
@router.post("/send_to_dropbox/")
|
||||
@require_login
|
||||
def send_to_dropbox_endpoint(file_path: str):
|
||||
"""Send a document to Dropbox."""
|
||||
file_path = resolve_file_path(file_path, 'processed')
|
||||
file_path = resolve_file_path(file_path, "processed")
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"File {file_path} not found."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"File {file_path} not found.")
|
||||
task = upload_to_dropbox.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
@router.post("/send_to_paperless/")
|
||||
@require_login
|
||||
def send_to_paperless_endpoint(file_path: str):
|
||||
"""Send a document to Paperless-ngx."""
|
||||
file_path = resolve_file_path(file_path, 'processed')
|
||||
file_path = resolve_file_path(file_path, "processed")
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"File {file_path} not found."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"File {file_path} not found.")
|
||||
task = upload_to_paperless.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
@router.post("/send_to_nextcloud/")
|
||||
@require_login
|
||||
def send_to_nextcloud_endpoint(file_path: str):
|
||||
"""Send a document to NextCloud."""
|
||||
file_path = resolve_file_path(file_path, 'processed')
|
||||
file_path = resolve_file_path(file_path, "processed")
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"File {file_path} not found."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"File {file_path} not found.")
|
||||
task = upload_to_nextcloud.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
@router.post("/send_to_google_drive/")
|
||||
@require_login
|
||||
def send_to_google_drive_endpoint(file_path: str):
|
||||
"""Send a document to Google Drive."""
|
||||
file_path = resolve_file_path(file_path, 'processed')
|
||||
file_path = resolve_file_path(file_path, "processed")
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"File {file_path} not found."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"File {file_path} not found.")
|
||||
task = upload_to_google_drive.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
@router.post("/send_to_onedrive/")
|
||||
@require_login
|
||||
def send_to_onedrive_endpoint(file_path: str):
|
||||
"""Send a document to OneDrive."""
|
||||
file_path = resolve_file_path(file_path, 'processed')
|
||||
file_path = resolve_file_path(file_path, "processed")
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"File {file_path} not found."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"File {file_path} not found.")
|
||||
task = upload_to_onedrive.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
@router.post("/send_to_all_destinations/")
|
||||
@require_login
|
||||
def send_to_all_destinations_endpoint(file_path: str):
|
||||
"""Call the aggregator task that sends this file to all configured destinations."""
|
||||
file_path = resolve_file_path(file_path, 'processed')
|
||||
file_path = resolve_file_path(file_path, "processed")
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"File {file_path} not found."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"File {file_path} not found.")
|
||||
|
||||
task = send_to_all_destinations.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued", "file_path": file_path}
|
||||
|
||||
|
||||
@router.post("/processall")
|
||||
@require_login
|
||||
def process_all_pdfs_in_workdir():
|
||||
"""
|
||||
Finds all .pdf files in <workdir> and enqueues them for processing.
|
||||
|
||||
|
||||
For large batches (>processall_throttle_threshold files), tasks are staggered
|
||||
to avoid overwhelming downstream APIs.
|
||||
"""
|
||||
target_dir = settings.workdir
|
||||
if not os.path.exists(target_dir):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Directory {target_dir} does not exist."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"Directory {target_dir} does not exist.")
|
||||
|
||||
pdf_files = []
|
||||
for filename in os.listdir(target_dir):
|
||||
@@ -134,20 +128,20 @@ def process_all_pdfs_in_workdir():
|
||||
|
||||
task_ids = []
|
||||
num_files = len(pdf_files)
|
||||
|
||||
|
||||
# Apply throttling if we have more files than the threshold
|
||||
apply_throttle = num_files > settings.processall_throttle_threshold
|
||||
|
||||
|
||||
if apply_throttle:
|
||||
logger.info(
|
||||
f"Processing {num_files} files with throttling "
|
||||
f"(threshold: {settings.processall_throttle_threshold}, "
|
||||
f"delay: {settings.processall_throttle_delay}s per file)"
|
||||
)
|
||||
|
||||
|
||||
for index, pdf in enumerate(pdf_files):
|
||||
file_path = os.path.join(target_dir, pdf)
|
||||
|
||||
|
||||
if apply_throttle:
|
||||
# Stagger task submission with countdown
|
||||
# First file starts immediately (countdown=0)
|
||||
@@ -158,17 +152,12 @@ def process_all_pdfs_in_workdir():
|
||||
else:
|
||||
# No throttling - enqueue immediately
|
||||
task = process_document.delay(file_path)
|
||||
|
||||
|
||||
task_ids.append(task.id)
|
||||
|
||||
message = f"Enqueued {num_files} PDFs for processing"
|
||||
if apply_throttle:
|
||||
total_time = (num_files - 1) * settings.processall_throttle_delay
|
||||
message += f" (throttled over {total_time} seconds)"
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"pdf_files": pdf_files,
|
||||
"task_ids": task_ids,
|
||||
"throttled": apply_throttle
|
||||
}
|
||||
|
||||
return {"message": message, "pdf_files": pdf_files, "task_ids": task_ids, "throttled": apply_throttle}
|
||||
|
||||
+38
-47
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional, List, Dict, Any, Union
|
||||
from pydantic import Field, validator
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from pydantic import Field, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
database_url: str
|
||||
@@ -14,23 +16,23 @@ class Settings(BaseSettings):
|
||||
openai_model: str = "gpt-4o-mini" # Default model
|
||||
workdir: str
|
||||
debug: bool = False # Default to False
|
||||
|
||||
|
||||
# Making Dropbox optional
|
||||
dropbox_app_key: Optional[str] = None
|
||||
dropbox_app_secret: Optional[str] = None
|
||||
dropbox_folder: Optional[str] = None
|
||||
dropbox_refresh_token: Optional[str] = None
|
||||
|
||||
|
||||
# Making Nextcloud optional
|
||||
nextcloud_upload_url: Optional[str] = None
|
||||
nextcloud_username: Optional[str] = None
|
||||
nextcloud_password: Optional[str] = None
|
||||
nextcloud_folder: Optional[str] = None
|
||||
|
||||
|
||||
# Making Paperless optional
|
||||
paperless_ngx_api_token: Optional[str] = None
|
||||
paperless_host: Optional[str] = None
|
||||
|
||||
|
||||
azure_ai_key: str
|
||||
azure_region: str
|
||||
azure_endpoint: str
|
||||
@@ -71,7 +73,7 @@ class Settings(BaseSettings):
|
||||
google_drive_credentials_json: Optional[str] = ""
|
||||
google_drive_folder_id: Optional[str] = ""
|
||||
google_drive_delegate_to: Optional[str] = "" # Optional delegated user email
|
||||
|
||||
|
||||
# Google Drive OAuth settings
|
||||
google_drive_use_oauth: bool = False # Default to service account method
|
||||
google_drive_client_id: Optional[str] = ""
|
||||
@@ -137,57 +139,43 @@ class Settings(BaseSettings):
|
||||
|
||||
# Batch processing settings
|
||||
processall_throttle_threshold: int = Field(
|
||||
default=20,
|
||||
description="Number of files above which throttling is applied in /processall endpoint"
|
||||
default=20, description="Number of files above which throttling is applied in /processall endpoint"
|
||||
)
|
||||
processall_throttle_delay: int = Field(
|
||||
default=3,
|
||||
description="Delay in seconds between each task submission when throttling in /processall"
|
||||
default=3, description="Delay in seconds between each task submission when throttling in /processall"
|
||||
)
|
||||
|
||||
# Notification settings
|
||||
notification_urls: Union[List[str], str] = Field(
|
||||
default_factory=list,
|
||||
description="List of Apprise notification URLs (e.g., discord://, telegram://, etc.)"
|
||||
)
|
||||
notify_on_task_failure: bool = Field(
|
||||
default=True,
|
||||
description="Send notifications when Celery tasks fail"
|
||||
default_factory=list, description="List of Apprise notification URLs (e.g., discord://, telegram://, etc.)"
|
||||
)
|
||||
notify_on_task_failure: bool = Field(default=True, description="Send notifications when Celery tasks fail")
|
||||
notify_on_credential_failure: bool = Field(
|
||||
default=True,
|
||||
description="Send notifications when credential checks fail"
|
||||
)
|
||||
notify_on_startup: bool = Field(
|
||||
default=True,
|
||||
description="Send notifications when application starts"
|
||||
)
|
||||
notify_on_shutdown: bool = Field(
|
||||
default=False,
|
||||
description="Send notifications when application shuts down"
|
||||
default=True, description="Send notifications when credential checks fail"
|
||||
)
|
||||
notify_on_startup: bool = Field(default=True, description="Send notifications when application starts")
|
||||
notify_on_shutdown: bool = Field(default=False, description="Send notifications when application shuts down")
|
||||
notify_on_file_processed: bool = Field(
|
||||
default=True,
|
||||
description="Send notifications when files are successfully processed"
|
||||
default=True, description="Send notifications when files are successfully processed"
|
||||
)
|
||||
|
||||
@validator('notification_urls', pre=True)
|
||||
|
||||
@validator("notification_urls", pre=True)
|
||||
def parse_notification_urls(cls, v):
|
||||
"""Parse notification URLs from string or list"""
|
||||
if isinstance(v, str):
|
||||
if ',' in v:
|
||||
return [url.strip() for url in v.split(',') if url.strip()]
|
||||
if "," in v:
|
||||
return [url.strip() for url in v.split(",") if url.strip()]
|
||||
elif v.strip():
|
||||
return [v.strip()]
|
||||
return []
|
||||
return v
|
||||
|
||||
@validator('session_secret')
|
||||
@validator("session_secret")
|
||||
def validate_session_secret(cls, v, values):
|
||||
"""Validate that session_secret is set and has sufficient length when auth is enabled"""
|
||||
if values.get('auth_enabled') and not v:
|
||||
if values.get("auth_enabled") and not v:
|
||||
raise ValueError("SESSION_SECRET must be set when AUTH_ENABLED=True")
|
||||
if values.get('auth_enabled') and v and len(v) < 32:
|
||||
if values.get("auth_enabled") and v and len(v) < 32:
|
||||
raise ValueError("SESSION_SECRET must be at least 32 characters long")
|
||||
return v
|
||||
|
||||
@@ -198,13 +186,13 @@ class Settings(BaseSettings):
|
||||
env_build_date = os.environ.get("BUILD_DATE")
|
||||
if env_build_date:
|
||||
return env_build_date
|
||||
|
||||
|
||||
# Then try to get build date from BUILD_DATE file
|
||||
build_date_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "BUILD_DATE")
|
||||
if os.path.exists(build_date_file):
|
||||
with open(build_date_file, "r") as f:
|
||||
return f.read().strip()
|
||||
|
||||
|
||||
# Default to unknown if not found
|
||||
return "Unknown build date"
|
||||
|
||||
@@ -215,35 +203,38 @@ class Settings(BaseSettings):
|
||||
env_version = os.environ.get("APP_VERSION")
|
||||
if env_version:
|
||||
return env_version
|
||||
|
||||
|
||||
# Then try to get version from VERSION file
|
||||
version_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "VERSION")
|
||||
if os.path.exists(version_file):
|
||||
with open(version_file, "r") as f:
|
||||
return f.read().strip()
|
||||
|
||||
|
||||
# Default version if not found
|
||||
return "0.3.2-dev"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
# Convert string representations of booleans to actual booleans
|
||||
# and strip quotes from string values
|
||||
@classmethod
|
||||
def parse_env_var(cls, field_name: str, raw_val: str) -> Any:
|
||||
# First, strip quotes from the value if it's a string
|
||||
if isinstance(raw_val, str):
|
||||
if (raw_val.startswith('"') and raw_val.endswith('"')) or \
|
||||
(raw_val.startswith("'") and raw_val.endswith("'")):
|
||||
if (raw_val.startswith('"') and raw_val.endswith('"')) or (
|
||||
raw_val.startswith("'") and raw_val.endswith("'")
|
||||
):
|
||||
raw_val = raw_val[1:-1]
|
||||
raw_val = raw_val.strip()
|
||||
|
||||
|
||||
# Convert string representations of booleans to actual booleans
|
||||
if field_name.endswith('_enabled') or field_name == 'debug':
|
||||
if raw_val.lower() in ('false', '0', 'no', 'n', 'f'):
|
||||
if field_name.endswith("_enabled") or field_name == "debug":
|
||||
if raw_val.lower() in ("false", "0", "no", "n", "f"):
|
||||
return False
|
||||
if raw_val.lower() in ('true', '1', 'yes', 'y', 't'):
|
||||
if raw_val.lower() in ("true", "1", "yes", "y", "t"):
|
||||
return True
|
||||
return raw_val
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
Reference in New Issue
Block a user