Changes committed
This commit is contained in:
0
docengine/app/api/__init__.py
Normal file
0
docengine/app/api/__init__.py
Normal file
15
docengine/app/api/router.py
Normal file
15
docengine/app/api/router.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.auth import router as auth_router
|
||||
from app.api.v1.documents import router as documents_router
|
||||
from app.api.v1.health import router as health_router
|
||||
from app.api.v1.templates import router as templates_router
|
||||
|
||||
api_v1_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
api_v1_router.include_router(health_router)
|
||||
api_v1_router.include_router(auth_router)
|
||||
api_v1_router.include_router(documents_router)
|
||||
api_v1_router.include_router(templates_router)
|
||||
0
docengine/app/api/v1/__init__.py
Normal file
0
docengine/app/api/v1/__init__.py
Normal file
246
docengine/app/api/v1/auth.py
Normal file
246
docengine/app/api/v1/auth.py
Normal file
@@ -0,0 +1,246 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import CurrentUser
|
||||
from app.core.security import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
hash_password,
|
||||
verify_password,
|
||||
InvalidTokenError,
|
||||
)
|
||||
from app.repositories.user_repository import RefreshTokenRepository, UserRepository
|
||||
from app.schemas.auth import (
|
||||
ChangePasswordRequest,
|
||||
LoginRequest,
|
||||
RefreshTokenRequest,
|
||||
RegisterRequest,
|
||||
TokenResponse,
|
||||
)
|
||||
from app.schemas.common import SuccessResponse
|
||||
from app.schemas.user import UserResponse
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/register",
|
||||
response_model=UserResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Register User",
|
||||
description="Register a new user account.",
|
||||
)
|
||||
def register(
|
||||
payload: RegisterRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> UserResponse:
|
||||
"""Register a new user."""
|
||||
user_repo = UserRepository(db)
|
||||
|
||||
# Check for existing user
|
||||
if user_repo.get_by_username(payload.username):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Username '{payload.username}' is already taken",
|
||||
)
|
||||
if user_repo.get_by_email(payload.email):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Email '{payload.email}' is already registered",
|
||||
)
|
||||
|
||||
hashed = hash_password(payload.password)
|
||||
user = user_repo.create_user(
|
||||
username=payload.username,
|
||||
email=payload.email,
|
||||
hashed_password=hashed,
|
||||
full_name=payload.full_name,
|
||||
role_names=["user"],
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
return UserResponse.model_validate(user)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/login",
|
||||
response_model=TokenResponse,
|
||||
summary="Login",
|
||||
description="Authenticate with username and password to obtain JWT tokens.",
|
||||
)
|
||||
def login(
|
||||
payload: LoginRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> TokenResponse:
|
||||
"""Authenticate user and return JWT tokens."""
|
||||
user_repo = UserRepository(db)
|
||||
refresh_repo = RefreshTokenRepository(db)
|
||||
|
||||
user = user_repo.get_by_username(payload.username)
|
||||
if not user or not verify_password(payload.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User account is deactivated",
|
||||
)
|
||||
|
||||
# Generate tokens
|
||||
access_token = create_access_token(data={"sub": str(user.id), "username": user.username})
|
||||
refresh_token_str = create_refresh_token(data={"sub": str(user.id)})
|
||||
|
||||
# Store refresh token
|
||||
expires_at = datetime.now(UTC) + timedelta(days=settings.jwt_refresh_token_expire_days)
|
||||
refresh_repo.create_token(
|
||||
user_id=user.id,
|
||||
token=refresh_token_str,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
|
||||
# Update last login
|
||||
user_repo.update_last_login(user)
|
||||
db.commit()
|
||||
|
||||
return TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token_str,
|
||||
token_type="bearer",
|
||||
expires_in=settings.jwt_access_token_expire_minutes * 60,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/refresh",
|
||||
response_model=TokenResponse,
|
||||
summary="Refresh Token",
|
||||
description="Obtain a new access token using a valid refresh token.",
|
||||
)
|
||||
def refresh_token(
|
||||
payload: RefreshTokenRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> TokenResponse:
|
||||
"""Refresh access token using a refresh token."""
|
||||
refresh_repo = RefreshTokenRepository(db)
|
||||
user_repo = UserRepository(db)
|
||||
|
||||
# Validate the refresh token
|
||||
try:
|
||||
token_payload = decode_token(payload.refresh_token)
|
||||
except InvalidTokenError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or expired refresh token",
|
||||
)
|
||||
|
||||
if token_payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token type",
|
||||
)
|
||||
|
||||
# Check if token exists in database and is not revoked
|
||||
stored_token = refresh_repo.get_by_token(payload.refresh_token)
|
||||
if not stored_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Refresh token not found or revoked",
|
||||
)
|
||||
|
||||
user = user_repo.get_by_id(token_payload["sub"])
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found or deactivated",
|
||||
)
|
||||
|
||||
# Revoke old refresh token
|
||||
refresh_repo.revoke_token(payload.refresh_token)
|
||||
|
||||
# Generate new tokens
|
||||
new_access_token = create_access_token(data={"sub": str(user.id), "username": user.username})
|
||||
new_refresh_token = create_refresh_token(data={"sub": str(user.id)})
|
||||
|
||||
expires_at = datetime.now(UTC) + timedelta(days=settings.jwt_refresh_token_expire_days)
|
||||
refresh_repo.create_token(
|
||||
user_id=user.id,
|
||||
token=new_refresh_token,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return TokenResponse(
|
||||
access_token=new_access_token,
|
||||
refresh_token=new_refresh_token,
|
||||
token_type="bearer",
|
||||
expires_in=settings.jwt_access_token_expire_minutes * 60,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/logout",
|
||||
response_model=SuccessResponse,
|
||||
summary="Logout",
|
||||
description="Revoke the current refresh token.",
|
||||
)
|
||||
def logout(
|
||||
payload: RefreshTokenRequest,
|
||||
current_user: CurrentUser,
|
||||
db: Session = Depends(get_db),
|
||||
) -> SuccessResponse:
|
||||
"""Logout by revoking the refresh token."""
|
||||
refresh_repo = RefreshTokenRepository(db)
|
||||
refresh_repo.revoke_token(payload.refresh_token)
|
||||
db.commit()
|
||||
return SuccessResponse(message="Successfully logged out")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/change-password",
|
||||
response_model=SuccessResponse,
|
||||
summary="Change Password",
|
||||
description="Change the current user's password.",
|
||||
)
|
||||
def change_password(
|
||||
payload: ChangePasswordRequest,
|
||||
current_user: CurrentUser,
|
||||
db: Session = Depends(get_db),
|
||||
) -> SuccessResponse:
|
||||
"""Change user password."""
|
||||
if not verify_password(payload.current_password, current_user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Current password is incorrect",
|
||||
)
|
||||
|
||||
current_user.hashed_password = hash_password(payload.new_password)
|
||||
|
||||
# Revoke all refresh tokens for security
|
||||
refresh_repo = RefreshTokenRepository(db)
|
||||
refresh_repo.revoke_all_user_tokens(current_user.id)
|
||||
db.commit()
|
||||
|
||||
return SuccessResponse(message="Password changed successfully")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me",
|
||||
response_model=UserResponse,
|
||||
summary="Get Current User",
|
||||
description="Get the currently authenticated user's profile.",
|
||||
)
|
||||
def get_me(current_user: CurrentUser) -> UserResponse:
|
||||
"""Get current authenticated user profile."""
|
||||
return UserResponse.model_validate(current_user)
|
||||
268
docengine/app/api/v1/documents.py
Normal file
268
docengine/app/api/v1/documents.py
Normal file
@@ -0,0 +1,268 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import CurrentUser
|
||||
from app.core.exceptions import FileSizeError, UnsupportedFileTypeError
|
||||
from app.core.logging_config import get_logger
|
||||
from app.models.document import Document
|
||||
from app.repositories.document_repository import DocumentRepository
|
||||
from app.schemas.common import PaginatedResponse, SuccessResponse
|
||||
from app.schemas.document import (
|
||||
DocumentListResponse,
|
||||
DocumentResponse,
|
||||
DocumentUploadResponse,
|
||||
TemplateMatchRequest,
|
||||
TemplateMatchResponse,
|
||||
)
|
||||
from app.storage.provider import LocalStorageProvider, get_storage_provider
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/documents", tags=["Documents"])
|
||||
|
||||
ALLOWED_CONTENT_TYPES = {
|
||||
"image/jpeg": "jpg",
|
||||
"image/png": "png",
|
||||
"image/tiff": "tiff",
|
||||
"application/pdf": "pdf",
|
||||
}
|
||||
|
||||
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".pdf"}
|
||||
|
||||
|
||||
def _validate_file(file: UploadFile) -> str:
|
||||
"""Validate uploaded file type and size. Returns the content type."""
|
||||
if not file.filename:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Filename is required",
|
||||
)
|
||||
|
||||
# Check extension
|
||||
from pathlib import Path
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
raise UnsupportedFileTypeError(ext)
|
||||
|
||||
# Determine content type
|
||||
content_type = file.content_type or ""
|
||||
if content_type not in ALLOWED_CONTENT_TYPES:
|
||||
# Try to infer from extension
|
||||
ext_to_ct = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".tiff": "image/tiff",
|
||||
".tif": "image/tiff",
|
||||
".pdf": "application/pdf",
|
||||
}
|
||||
content_type = ext_to_ct.get(ext, "")
|
||||
if not content_type:
|
||||
raise UnsupportedFileTypeError(file.content_type or "unknown")
|
||||
|
||||
return content_type
|
||||
|
||||
|
||||
@router.post(
|
||||
"/upload",
|
||||
response_model=DocumentUploadResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Upload Document",
|
||||
description="Upload a document (JPG, JPEG, PNG, TIFF, or PDF) for processing.",
|
||||
)
|
||||
async def upload_document(
|
||||
file: UploadFile = File(..., description="Document file to upload"),
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> DocumentUploadResponse:
|
||||
"""Upload a document for processing."""
|
||||
content_type = _validate_file(file)
|
||||
|
||||
# Read file data
|
||||
file_data = await file.read()
|
||||
|
||||
# Check file size
|
||||
if len(file_data) > settings.storage_max_file_size_bytes:
|
||||
raise FileSizeError(settings.storage_max_file_size_mb)
|
||||
|
||||
# Store file
|
||||
storage = get_storage_provider()
|
||||
checksum = storage.compute_checksum(file_data)
|
||||
safe_filename = file.filename or "unknown"
|
||||
stored_filename = storage.generate_filename(safe_filename)
|
||||
storage_path = storage.save_file(file_data, "documents", stored_filename)
|
||||
|
||||
# Create document record
|
||||
doc_repo = DocumentRepository(db)
|
||||
document = Document(
|
||||
filename=stored_filename,
|
||||
original_filename=safe_filename,
|
||||
content_type=content_type,
|
||||
file_size=len(file_data),
|
||||
checksum=checksum,
|
||||
storage_path=storage_path,
|
||||
status="pending",
|
||||
uploaded_by=current_user.id if current_user else None,
|
||||
)
|
||||
doc_repo.create(document)
|
||||
db.commit()
|
||||
db.refresh(document)
|
||||
|
||||
logger.info(
|
||||
"document_uploaded",
|
||||
document_id=str(document.id),
|
||||
filename=safe_filename,
|
||||
size=len(file_data),
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
# Trigger async processing via Celery
|
||||
try:
|
||||
from app.tasks.document_tasks import process_document_task
|
||||
process_document_task.delay(str(document.id))
|
||||
except Exception as e:
|
||||
logger.warning("celery_dispatch_failed", error=str(e), document_id=str(document.id))
|
||||
|
||||
return DocumentUploadResponse.model_validate(document)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{document_id}",
|
||||
response_model=DocumentResponse,
|
||||
summary="Get Document",
|
||||
description="Retrieve a document by its ID with all extracted content.",
|
||||
)
|
||||
def get_document(
|
||||
document_id: uuid.UUID,
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> DocumentResponse:
|
||||
"""Get a document by ID."""
|
||||
doc_repo = DocumentRepository(db)
|
||||
document = doc_repo.get_with_pages(document_id)
|
||||
if not document:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Document '{document_id}' not found",
|
||||
)
|
||||
return DocumentResponse.model_validate(document)
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=PaginatedResponse[DocumentListResponse],
|
||||
summary="List Documents",
|
||||
description="List all documents with pagination.",
|
||||
)
|
||||
def list_documents(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
status_filter: str | None = Query(default=None, alias="status"),
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> PaginatedResponse[DocumentListResponse]:
|
||||
"""List documents with pagination and optional status filter."""
|
||||
doc_repo = DocumentRepository(db)
|
||||
offset = (page - 1) * page_size
|
||||
filters = {}
|
||||
if status_filter:
|
||||
filters["status"] = status_filter
|
||||
|
||||
documents = doc_repo.get_all(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
filters=filters,
|
||||
order_by="created_at",
|
||||
order_desc=True,
|
||||
)
|
||||
total = doc_repo.count(filters=filters)
|
||||
|
||||
items = [DocumentListResponse.model_validate(doc) for doc in documents]
|
||||
return PaginatedResponse.create(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{document_id}/template",
|
||||
response_model=list[TemplateMatchResponse],
|
||||
summary="Get Document Template Matches",
|
||||
description="Get template matching results for a document.",
|
||||
)
|
||||
def get_document_template_matches(
|
||||
document_id: uuid.UUID,
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[TemplateMatchResponse]:
|
||||
"""Get template matches for a document."""
|
||||
from app.repositories.document_repository import TemplateMatchRepository
|
||||
|
||||
doc_repo = DocumentRepository(db)
|
||||
document = doc_repo.get_by_id(document_id)
|
||||
if not document:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Document '{document_id}' not found",
|
||||
)
|
||||
|
||||
match_repo = TemplateMatchRepository(db)
|
||||
matches = match_repo.get_document_matches(document_id)
|
||||
|
||||
results = []
|
||||
for match in matches:
|
||||
resp = TemplateMatchResponse(
|
||||
id=match.id,
|
||||
document_id=match.document_id,
|
||||
format_id=match.format_id,
|
||||
confidence_score=match.confidence_score,
|
||||
match_details=match.match_details,
|
||||
selected=match.selected,
|
||||
template_name=match.template.name if match.template else None,
|
||||
created_at=match.created_at,
|
||||
)
|
||||
results.append(resp)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{document_id}",
|
||||
response_model=SuccessResponse,
|
||||
summary="Delete Document",
|
||||
description="Delete a document and its associated data.",
|
||||
)
|
||||
def delete_document(
|
||||
document_id: uuid.UUID,
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> SuccessResponse:
|
||||
"""Delete a document."""
|
||||
doc_repo = DocumentRepository(db)
|
||||
document = doc_repo.get_by_id(document_id)
|
||||
if not document:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Document '{document_id}' not found",
|
||||
)
|
||||
|
||||
# Delete stored file
|
||||
try:
|
||||
storage = get_storage_provider()
|
||||
storage.delete_file(document.storage_path)
|
||||
except Exception as e:
|
||||
logger.warning("file_delete_failed", error=str(e), path=document.storage_path)
|
||||
|
||||
doc_repo.delete(document)
|
||||
db.commit()
|
||||
|
||||
return SuccessResponse(message=f"Document '{document_id}' deleted successfully")
|
||||
51
docengine/app/api/v1/health.py
Normal file
51
docengine/app/api/v1/health.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import redis
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import check_database_connection
|
||||
from app.schemas.common import HealthResponse
|
||||
|
||||
router = APIRouter(tags=["Health"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health",
|
||||
response_model=HealthResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Health Check",
|
||||
description="Check the health status of the application and its dependencies.",
|
||||
)
|
||||
async def health_check() -> HealthResponse:
|
||||
"""Perform health check on all system components."""
|
||||
# Check database
|
||||
db_status = "healthy" if check_database_connection() else "unhealthy"
|
||||
|
||||
# Check Redis
|
||||
redis_status = "healthy"
|
||||
try:
|
||||
r = redis.Redis(
|
||||
host=settings.redis_host,
|
||||
port=settings.redis_port,
|
||||
db=settings.redis_db,
|
||||
password=settings.redis_password or None,
|
||||
socket_timeout=3,
|
||||
)
|
||||
r.ping()
|
||||
r.close()
|
||||
except Exception:
|
||||
redis_status = "unhealthy"
|
||||
|
||||
overall_status = "healthy" if db_status == "healthy" and redis_status == "healthy" else "degraded"
|
||||
|
||||
return HealthResponse(
|
||||
status=overall_status,
|
||||
version=settings.app_version,
|
||||
environment=settings.app_env,
|
||||
database=db_status,
|
||||
redis=redis_status,
|
||||
timestamp=datetime.utcnow(),
|
||||
)
|
||||
223
docengine/app/api/v1/templates.py
Normal file
223
docengine/app/api/v1/templates.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import CurrentUser
|
||||
from app.core.logging_config import get_logger
|
||||
from app.repositories.document_repository import DocumentRepository, TemplateMatchRepository
|
||||
from app.repositories.template_repository import TemplateRepository
|
||||
from app.schemas.common import PaginatedResponse, SuccessResponse
|
||||
from app.schemas.document import TemplateMatchRequest, TemplateMatchResponse
|
||||
from app.schemas.template import (
|
||||
TemplateListResponse,
|
||||
TemplateRenderRequest,
|
||||
TemplateRenderResponse,
|
||||
TemplateResponse,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/templates", tags=["Templates"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=PaginatedResponse[TemplateListResponse],
|
||||
summary="List Templates",
|
||||
description="List all active templates with pagination.",
|
||||
)
|
||||
def list_templates(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> PaginatedResponse[TemplateListResponse]:
|
||||
"""List all active templates."""
|
||||
template_repo = TemplateRepository(db)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
templates = template_repo.get_active_templates(offset=offset, limit=page_size)
|
||||
total = template_repo.count_active()
|
||||
|
||||
items = [TemplateListResponse.model_validate(t) for t in templates]
|
||||
return PaginatedResponse.create(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{template_id}",
|
||||
response_model=TemplateResponse,
|
||||
summary="Get Template",
|
||||
description="Retrieve a template by ID with all its components.",
|
||||
)
|
||||
def get_template(
|
||||
template_id: uuid.UUID,
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> TemplateResponse:
|
||||
"""Get a template by ID."""
|
||||
template_repo = TemplateRepository(db)
|
||||
template = template_repo.get_by_id(template_id)
|
||||
if not template:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Template '{template_id}' not found",
|
||||
)
|
||||
return TemplateResponse.model_validate(template)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{template_id}",
|
||||
response_model=SuccessResponse,
|
||||
summary="Delete Template",
|
||||
description="Soft-delete a template by deactivating it.",
|
||||
)
|
||||
def delete_template(
|
||||
template_id: uuid.UUID,
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> SuccessResponse:
|
||||
"""Soft-delete a template."""
|
||||
template_repo = TemplateRepository(db)
|
||||
template = template_repo.get_by_id(template_id)
|
||||
if not template:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Template '{template_id}' not found",
|
||||
)
|
||||
|
||||
template_repo.deactivate_template(template_id)
|
||||
db.commit()
|
||||
|
||||
return SuccessResponse(message=f"Template '{template_id}' deactivated successfully")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/match",
|
||||
response_model=list[TemplateMatchResponse],
|
||||
summary="Match Document to Templates",
|
||||
description="Match a document against existing templates and return ranked results.",
|
||||
)
|
||||
def match_template(
|
||||
payload: TemplateMatchRequest,
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[TemplateMatchResponse]:
|
||||
"""Match a document against existing templates."""
|
||||
doc_repo = DocumentRepository(db)
|
||||
document = doc_repo.get_by_id(payload.document_id)
|
||||
if not document:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Document '{payload.document_id}' not found",
|
||||
)
|
||||
|
||||
if document.status != "completed":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Document must be in 'completed' status. Current status: '{document.status}'",
|
||||
)
|
||||
|
||||
# Perform template matching
|
||||
from app.services.matching_service import MatchingService
|
||||
matching_service = MatchingService(db)
|
||||
matches = matching_service.match_document(
|
||||
document_id=payload.document_id,
|
||||
min_confidence=payload.min_confidence,
|
||||
max_results=payload.max_results,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
results = []
|
||||
for match in matches:
|
||||
resp = TemplateMatchResponse(
|
||||
id=match.id,
|
||||
document_id=match.document_id,
|
||||
format_id=match.format_id,
|
||||
confidence_score=match.confidence_score,
|
||||
match_details=match.match_details,
|
||||
selected=match.selected,
|
||||
template_name=match.template.name if match.template else None,
|
||||
created_at=match.created_at,
|
||||
)
|
||||
results.append(resp)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@router.post(
|
||||
"/render",
|
||||
response_model=TemplateRenderResponse,
|
||||
summary="Render Template to PDF",
|
||||
description="Generate a PDF from a stored template with supplied data.",
|
||||
)
|
||||
def render_template(
|
||||
payload: TemplateRenderRequest,
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> TemplateRenderResponse:
|
||||
"""Render a template to PDF."""
|
||||
template_repo = TemplateRepository(db)
|
||||
template = template_repo.get_by_id(payload.template_id)
|
||||
if not template:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Template '{payload.template_id}' not found",
|
||||
)
|
||||
|
||||
if not template.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Template is deactivated",
|
||||
)
|
||||
|
||||
from app.services.reconstruction_service import ReconstructionService
|
||||
reconstruction_service = ReconstructionService(db)
|
||||
result = reconstruction_service.render_template(
|
||||
template=template,
|
||||
data=payload.data,
|
||||
output_filename=payload.output_filename,
|
||||
images=payload.images,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{template_id}/download",
|
||||
summary="Download Rendered PDF",
|
||||
description="Download a previously rendered PDF.",
|
||||
)
|
||||
def download_rendered_pdf(
|
||||
template_id: uuid.UUID,
|
||||
filename: str = Query(..., description="Filename of the rendered PDF"),
|
||||
current_user: CurrentUser = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> FileResponse:
|
||||
"""Download a rendered PDF."""
|
||||
from app.storage.provider import get_storage_provider
|
||||
|
||||
storage = get_storage_provider()
|
||||
storage_path = f"rendered/{filename}"
|
||||
|
||||
if not storage.file_exists(storage_path):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Rendered PDF '{filename}' not found",
|
||||
)
|
||||
|
||||
absolute_path = storage.get_absolute_path(storage_path)
|
||||
return FileResponse(
|
||||
path=absolute_path,
|
||||
media_type="application/pdf",
|
||||
filename=filename,
|
||||
)
|
||||
Reference in New Issue
Block a user