From 613cd81152302bde5bf534f4014291a5bb02e50d Mon Sep 17 00:00:00 2001 From: pixelpaws Date: Tue, 23 Sep 2025 11:12:05 +0800 Subject: [PATCH] refactor(routes): add registrar for example images --- py/routes/example_images_route_registrar.py | 61 +++++++++++++++++++++ py/routes/example_images_routes.py | 46 ++++++++++------ tests/routes/test_example_images_routes.py | 16 +++++- 3 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 py/routes/example_images_route_registrar.py diff --git a/py/routes/example_images_route_registrar.py b/py/routes/example_images_route_registrar.py new file mode 100644 index 00000000..d0f1fab0 --- /dev/null +++ b/py/routes/example_images_route_registrar.py @@ -0,0 +1,61 @@ +"""Route registrar for example image endpoints.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Callable, Iterable, Mapping + +from aiohttp import web + + +@dataclass(frozen=True) +class RouteDefinition: + """Declarative configuration for a HTTP route.""" + + method: str + path: str + handler_name: str + + +ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = ( + RouteDefinition("POST", "/api/lm/download-example-images", "download_example_images"), + RouteDefinition("POST", "/api/lm/import-example-images", "import_example_images"), + RouteDefinition("GET", "/api/lm/example-images-status", "get_example_images_status"), + RouteDefinition("POST", "/api/lm/pause-example-images", "pause_example_images"), + RouteDefinition("POST", "/api/lm/resume-example-images", "resume_example_images"), + RouteDefinition("POST", "/api/lm/open-example-images-folder", "open_example_images_folder"), + RouteDefinition("GET", "/api/lm/example-image-files", "get_example_image_files"), + RouteDefinition("GET", "/api/lm/has-example-images", "has_example_images"), + RouteDefinition("POST", "/api/lm/delete-example-image", "delete_example_image"), + RouteDefinition("POST", "/api/lm/force-download-example-images", "force_download_example_images"), +) + + +class ExampleImagesRouteRegistrar: + """Bind declarative example image routes to an aiohttp router.""" + + _METHOD_MAP = { + "GET": "add_get", + "POST": "add_post", + "PUT": "add_put", + "DELETE": "add_delete", + } + + def __init__(self, app: web.Application) -> None: + self._app = app + + def register_routes( + self, + handler_lookup: Mapping[str, Callable[[web.Request], object]], + *, + definitions: Iterable[RouteDefinition] = ROUTE_DEFINITIONS, + ) -> None: + """Register each route definition using the supplied handlers.""" + + for definition in definitions: + handler = handler_lookup[definition.handler_name] + self._bind_route(definition.method, definition.path, handler) + + def _bind_route(self, method: str, path: str, handler: Callable[[web.Request], object]) -> None: + add_method_name = self._METHOD_MAP[method.upper()] + add_method = getattr(self._app.router, add_method_name) + add_method(path, handler) diff --git a/py/routes/example_images_routes.py b/py/routes/example_images_routes.py index 07cb0e71..193cfe1d 100644 --- a/py/routes/example_images_routes.py +++ b/py/routes/example_images_routes.py @@ -1,4 +1,9 @@ import logging +from typing import Callable + +from aiohttp import web + +from .example_images_route_registrar import ExampleImagesRouteRegistrar from ..utils.example_images_download_manager import DownloadManager from ..utils.example_images_processor import ExampleImagesProcessor from ..utils.example_images_file_manager import ExampleImagesFileManager @@ -6,22 +11,31 @@ from ..services.websocket_manager import ws_manager logger = logging.getLogger(__name__) + class ExampleImagesRoutes: """Routes for example images related functionality""" - + @staticmethod - def setup_routes(app): - """Register example images routes""" - app.router.add_post('/api/lm/download-example-images', ExampleImagesRoutes.download_example_images) - app.router.add_post('/api/lm/import-example-images', ExampleImagesRoutes.import_example_images) - app.router.add_get('/api/lm/example-images-status', ExampleImagesRoutes.get_example_images_status) - app.router.add_post('/api/lm/pause-example-images', ExampleImagesRoutes.pause_example_images) - app.router.add_post('/api/lm/resume-example-images', ExampleImagesRoutes.resume_example_images) - app.router.add_post('/api/lm/open-example-images-folder', ExampleImagesRoutes.open_example_images_folder) - app.router.add_get('/api/lm/example-image-files', ExampleImagesRoutes.get_example_image_files) - app.router.add_get('/api/lm/has-example-images', ExampleImagesRoutes.has_example_images) - app.router.add_post('/api/lm/delete-example-image', ExampleImagesRoutes.delete_example_image) - app.router.add_post('/api/lm/force-download-example-images', ExampleImagesRoutes.force_download_example_images) + def setup_routes(app: web.Application) -> None: + """Register example images routes using the registrar.""" + + registrar = ExampleImagesRouteRegistrar(app) + registrar.register_routes(ExampleImagesRoutes._route_mapping()) + + @staticmethod + def _route_mapping() -> dict[str, Callable[[web.Request], object]]: + return { + "download_example_images": ExampleImagesRoutes.download_example_images, + "import_example_images": ExampleImagesRoutes.import_example_images, + "get_example_images_status": ExampleImagesRoutes.get_example_images_status, + "pause_example_images": ExampleImagesRoutes.pause_example_images, + "resume_example_images": ExampleImagesRoutes.resume_example_images, + "open_example_images_folder": ExampleImagesRoutes.open_example_images_folder, + "get_example_image_files": ExampleImagesRoutes.get_example_image_files, + "has_example_images": ExampleImagesRoutes.has_example_images, + "delete_example_image": ExampleImagesRoutes.delete_example_image, + "force_download_example_images": ExampleImagesRoutes.force_download_example_images, + } @staticmethod async def download_example_images(request): @@ -42,7 +56,7 @@ class ExampleImagesRoutes: async def resume_example_images(request): """Resume the example images download""" return await DownloadManager.resume_download(request) - + @staticmethod async def open_example_images_folder(request): """Open the example images folder for a specific model""" @@ -57,7 +71,7 @@ class ExampleImagesRoutes: async def import_example_images(request): """Import local example images for a model""" return await ExampleImagesProcessor.import_images(request) - + @staticmethod async def has_example_images(request): """Check if example images folder exists and is not empty for a model""" @@ -71,4 +85,4 @@ class ExampleImagesRoutes: @staticmethod async def force_download_example_images(request): """Force download example images for specific models""" - return await DownloadManager.start_force_download(request) \ No newline at end of file + return await DownloadManager.start_force_download(request) diff --git a/tests/routes/test_example_images_routes.py b/tests/routes/test_example_images_routes.py index d64e1d7f..dac40ee9 100644 --- a/tests/routes/test_example_images_routes.py +++ b/tests/routes/test_example_images_routes.py @@ -1,6 +1,6 @@ from contextlib import asynccontextmanager from dataclasses import dataclass -from typing import Any, List, Tuple +from typing import Any, List, Set, Tuple from aiohttp import web from aiohttp.test_utils import TestClient, TestServer @@ -8,6 +8,7 @@ import pytest from py.routes import example_images_routes from py.routes.example_images_routes import ExampleImagesRoutes +from py.routes.example_images_route_registrar import ROUTE_DEFINITIONS @dataclass @@ -110,6 +111,19 @@ async def example_images_app(monkeypatch: pytest.MonkeyPatch) -> ExampleImagesHa await client.close() +async def test_setup_routes_registers_all_definitions(monkeypatch: pytest.MonkeyPatch): + async with example_images_app(monkeypatch) as harness: + registered: Set[tuple[str, str]] = { + (route.method, route.resource.canonical) + for route in harness.client.app.router.routes() + if route.resource.canonical + } + + expected = {(definition.method, definition.path) for definition in ROUTE_DEFINITIONS} + + assert expected <= registered + + @pytest.mark.parametrize( "endpoint, payload", [