| """ |
| FastAPI dependency functions and shared async utilities. |
| """ |
| import asyncio |
| import io |
| import logging |
| from concurrent.futures import ThreadPoolExecutor |
| from functools import partial |
| from typing import Callable, TypeVar |
|
|
| from fastapi import Depends, UploadFile, File |
| from PIL import Image |
|
|
| from app.core.config import settings |
| from app.core.exceptions import ImageDecodeError, ImageTooLargeError, InvalidMimeTypeError |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| cv_executor = ThreadPoolExecutor( |
| max_workers=settings.CV_THREAD_POOL_SIZE, |
| thread_name_prefix="cv-worker", |
| ) |
|
|
| T = TypeVar("T") |
|
|
| ALLOWED_MIME_TYPES = {"image/jpeg", "image/png", "image/webp"} |
|
|
|
|
| async def run_in_cv_executor(fn: Callable[..., T], *args) -> T: |
| """Run a blocking CV function in the dedicated thread pool.""" |
| loop = asyncio.get_event_loop() |
| return await loop.run_in_executor(cv_executor, partial(fn, *args)) |
|
|
|
|
| async def validated_image_bytes( |
| file: UploadFile = File(..., description="Selfie image (JPEG/PNG/WebP)"), |
| ) -> bytes: |
| """ |
| FastAPI dependency that validates MIME type, file size, and image dimensions. |
| Returns raw bytes ready for CV processing. |
| """ |
| content_type = file.content_type or "" |
| if content_type not in ALLOWED_MIME_TYPES: |
| raise InvalidMimeTypeError(received=content_type, allowed=ALLOWED_MIME_TYPES) |
|
|
| file_bytes = await file.read() |
|
|
| if len(file_bytes) > settings.MAX_IMAGE_BYTES: |
| raise ImageTooLargeError(max_mb=settings.MAX_IMAGE_SIZE_MB) |
|
|
| |
| try: |
| img = Image.open(io.BytesIO(file_bytes)) |
| w, h = img.size |
| if w > settings.MAX_IMAGE_DIMENSION or h > settings.MAX_IMAGE_DIMENSION: |
| raise ImageTooLargeError( |
| detail=f"Image dimensions {w}×{h} exceed maximum {settings.MAX_IMAGE_DIMENSION}px per side." |
| ) |
| except ImageTooLargeError: |
| raise |
| except Exception: |
| raise ImageDecodeError() |
|
|
| return file_bytes |
|
|