From 0f47cd6c96c6ddfb1a01138499fcfe3b82a96e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=2E=20M=2E=20P=C4=83duraru?= Date: Sat, 19 Apr 2025 10:44:16 +0200 Subject: [PATCH] feat: Add bulk add with event generator --- app/main.py | 3 +- app/services/saver/manager.py | 28 ++++++++++++++ app/services/saver/router.py | 36 +++++++++++------- tests/services/saver/__init__.py | 0 .../saver/test_multiple_images/1-crown.jpg | Bin 0 -> 5508 bytes .../saver/test_multiple_images/2-galicia.jpg | Bin 0 -> 7630 bytes .../saver/test_multiple_images/3-salitos.jpg | Bin 0 -> 8219 bytes .../test_multiple_images/4-crown-red.jpg | Bin 0 -> 6501 bytes .../saver/test_multiple_images/5-star.jpg | Bin 0 -> 6009 bytes tests/services/saver/test_saver_bulk.py | 28 ++++++++++++++ 10 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 tests/services/saver/__init__.py create mode 100644 tests/services/saver/test_multiple_images/1-crown.jpg create mode 100644 tests/services/saver/test_multiple_images/2-galicia.jpg create mode 100644 tests/services/saver/test_multiple_images/3-salitos.jpg create mode 100644 tests/services/saver/test_multiple_images/4-crown-red.jpg create mode 100644 tests/services/saver/test_multiple_images/5-star.jpg create mode 100644 tests/services/saver/test_saver_bulk.py diff --git a/app/main.py b/app/main.py index dfd28acd..35c925e4 100644 --- a/app/main.py +++ b/app/main.py @@ -2,14 +2,13 @@ import sentry_sdk import uvicorn -from fastapi import Depends, FastAPI +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from pyinstrument import Profiler from starlette import status from starlette.requests import Request from app.config import LIMIT_PERIOD, Settings -from app.services.auth import validate_api_key from app.services.detect.router import detect_router from app.services.identify.router import identify_router from app.services.limiter import request_limiter diff --git a/app/services/saver/manager.py b/app/services/saver/manager.py index b18e2c5f..a610a00e 100644 --- a/app/services/saver/manager.py +++ b/app/services/saver/manager.py @@ -1,3 +1,4 @@ +import asyncio import uuid from typing import TYPE_CHECKING @@ -12,6 +13,33 @@ import numpy as np +async def save_image_progress( + file: UploadFile, + name: str, + user_id: str, + progress_callback: asyncio.Event, + vector: list[float] | None = None, +): + """Asynchronously saves an image and triggers a progress callback once completed. + + Args: + ---- + file (UploadFile): The image file to be uploaded. + name (str): The name for the image. + user_id (str): Unique user identifier. + progress_callback (asyncio.Event): Event to signal when the upload is complete. + vector (list[float] | None, optional): Optional vector data associated with the image. + + Returns: + ------- + str: URL of the uploaded image. + + """ + url_upload = await save_image(file=file, name=name, user_id=user_id, vector=vector) + progress_callback.set() + return url_upload + + async def save_image( file: UploadFile, name: str, diff --git a/app/services/saver/router.py b/app/services/saver/router.py index 3cbe044b..1c7acbfe 100644 --- a/app/services/saver/router.py +++ b/app/services/saver/router.py @@ -1,12 +1,14 @@ import asyncio +import json from fastapi import APIRouter, Depends, UploadFile from starlette.requests import Request +from starlette.responses import StreamingResponse from app.config import LIMIT_PERIOD from app.services.auth import validate_api_key from app.services.limiter import request_limiter -from app.services.saver.manager import remove_image, save_image +from app.services.saver.manager import remove_image, save_image, save_image_progress saver_router: APIRouter = APIRouter(dependencies=[Depends(validate_api_key)], tags=["Saver"]) @@ -39,29 +41,37 @@ async def post_save_image( @saver_router.post("/saver/bulk") -@request_limiter.limit(LIMIT_PERIOD) async def post_save_images( files: list[UploadFile], user_id: str, request: Request, -) -> None: - """Save multiple images. +) -> StreamingResponse: + """Save multiple images and send progress updates via SSE.""" - Args: - ---- - files (list[UploadFile]): The files you want to update. - user_id (str): The user id that is uploading the files. - request (Request): Needed for the limiter + async def event_generator(progress: list[asyncio.Event], total_images: int) -> str: + for processed, event in enumerate(progress): + await event.wait() + event.clear() + yield f"data: {json.dumps({'processed': processed, + 'total': total_images, + 'percentage': processed / total_images})}\n\n" - """ - tasks = [] + progress_events = [asyncio.Event() for _ in files] - for file in files: + tasks = [] + for idx, file in enumerate(files): name = file.filename - task = save_image(file, name, user_id) + task = save_image_progress( + file=file, name=name, user_id=user_id, progress_callback=progress_events[idx] + ) tasks.append(task) + await asyncio.gather(*tasks) + return StreamingResponse( + event_generator(progress_events, len(files)), media_type="text/event-stream" + ) + @saver_router.delete("/delete") @request_limiter.limit(LIMIT_PERIOD) diff --git a/tests/services/saver/__init__.py b/tests/services/saver/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/services/saver/test_multiple_images/1-crown.jpg b/tests/services/saver/test_multiple_images/1-crown.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ec4afd79b8e1597865f6e5abe888491131380161 GIT binary patch literal 5508 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 F_#e5wk^TSx literal 0 HcmV?d00001 diff --git a/tests/services/saver/test_multiple_images/5-star.jpg b/tests/services/saver/test_multiple_images/5-star.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd9cbf028549a77024e342a4827090413840c266 GIT binary patch literal 6009 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 literal 0 HcmV?d00001 diff --git a/tests/services/saver/test_saver_bulk.py b/tests/services/saver/test_saver_bulk.py new file mode 100644 index 00000000..e15ee6be --- /dev/null +++ b/tests/services/saver/test_saver_bulk.py @@ -0,0 +1,28 @@ +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +from dotenv import load_dotenv +from starlette.requests import Request + +from app.services.saver.router import post_save_images +from app.shared.utils import upload_file + +load_dotenv() + +TEST_USER: str = "test_user" + + +@pytest.mark.asyncio +async def test_saver_bulk_ok(): + """Manual test to check if the API endpoint is working. + + This test will return always true, but it's useful for me to see if + the files are being uploaded. + """ + directory: Path = Path("tests/services/saver/test_multiple_images") + paths: list = list(directory.glob("*.jpg")) + uploaded_files = [await upload_file(path) for path in paths] + + fake_request = MagicMock(spec=Request) + await post_save_images(files=uploaded_files, user_id="test_user", request=fake_request)