FaceCheck / app /core /dependencies.py
Mukhammadali Bakhodirov
Deploy detection fixes
9b5157d
"""
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__)
# Dedicated thread pool for blocking CV operations (MediaPipe, OpenCV, DeepFace).
# These are CPU-bound and must not run on the event loop thread.
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)
# Decompression bomb guard — check pixel dimensions without fully decoding
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