refactor(routes): split recipe handlers into dedicated classes

This commit is contained in:
pixelpaws
2025-09-22 12:57:37 +08:00
parent 6aa23fe36a
commit d033a374dd
4 changed files with 1440 additions and 1576 deletions

View File

@@ -11,6 +11,15 @@ from ..config import config
from ..services.server_i18n import server_i18n from ..services.server_i18n import server_i18n
from ..services.service_registry import ServiceRegistry from ..services.service_registry import ServiceRegistry
from ..services.settings_manager import settings from ..services.settings_manager import settings
from .handlers.recipe_handlers import (
RecipeAnalysisHandler,
RecipeHandlerSet,
RecipeListingHandler,
RecipeManagementHandler,
RecipePageView,
RecipeQueryHandler,
RecipeSharingHandler,
)
from .recipe_route_registrar import ROUTE_DEFINITIONS from .recipe_route_registrar import ROUTE_DEFINITIONS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -23,6 +32,8 @@ class BaseRecipeRoutes:
definition.handler_name for definition in ROUTE_DEFINITIONS definition.handler_name for definition in ROUTE_DEFINITIONS
) )
template_name: str = "recipes.html"
def __init__(self) -> None: def __init__(self) -> None:
self.recipe_scanner = None self.recipe_scanner = None
self.lora_scanner = None self.lora_scanner = None
@@ -36,6 +47,7 @@ class BaseRecipeRoutes:
self._i18n_registered = False self._i18n_registered = False
self._startup_hooks_registered = False self._startup_hooks_registered = False
self._handler_set: RecipeHandlerSet | None = None
self._handler_mapping: dict[str, Callable] | None = None self._handler_mapping: dict[str, Callable] | None = None
async def attach_dependencies(self, app: web.Application | None = None) -> None: async def attach_dependencies(self, app: web.Application | None = None) -> None:
@@ -81,10 +93,9 @@ class BaseRecipeRoutes:
"""Return a mapping of handler name to coroutine for registrar binding.""" """Return a mapping of handler name to coroutine for registrar binding."""
if self._handler_mapping is None: if self._handler_mapping is None:
owner = self.get_handler_owner() handler_set = self._create_handler_set()
self._handler_mapping = { self._handler_set = handler_set
name: getattr(owner, name) for name in self._HANDLER_NAMES self._handler_mapping = handler_set.to_route_mapping()
}
return self._handler_mapping return self._handler_mapping
# Internal helpers ------------------------------------------------- # Internal helpers -------------------------------------------------
@@ -105,5 +116,57 @@ class BaseRecipeRoutes:
def get_handler_owner(self): def get_handler_owner(self):
"""Return the object supplying bound handler coroutines.""" """Return the object supplying bound handler coroutines."""
return self if self._handler_set is None:
self._handler_set = self._create_handler_set()
return self._handler_set
def _create_handler_set(self) -> RecipeHandlerSet:
recipe_scanner_getter = lambda: self.recipe_scanner
civitai_client_getter = lambda: self.civitai_client
page_view = RecipePageView(
ensure_dependencies_ready=self.ensure_dependencies_ready,
settings_service=self.settings,
server_i18n=self.server_i18n,
template_env=self.template_env,
template_name=self.template_name,
recipe_scanner_getter=recipe_scanner_getter,
logger=logger,
)
listing = RecipeListingHandler(
ensure_dependencies_ready=self.ensure_dependencies_ready,
recipe_scanner_getter=recipe_scanner_getter,
logger=logger,
)
query = RecipeQueryHandler(
ensure_dependencies_ready=self.ensure_dependencies_ready,
recipe_scanner_getter=recipe_scanner_getter,
format_recipe_file_url=listing.format_recipe_file_url,
logger=logger,
)
management = RecipeManagementHandler(
ensure_dependencies_ready=self.ensure_dependencies_ready,
recipe_scanner_getter=recipe_scanner_getter,
logger=logger,
)
analysis = RecipeAnalysisHandler(
ensure_dependencies_ready=self.ensure_dependencies_ready,
recipe_scanner_getter=recipe_scanner_getter,
civitai_client_getter=civitai_client_getter,
logger=logger,
)
sharing = RecipeSharingHandler(
ensure_dependencies_ready=self.ensure_dependencies_ready,
recipe_scanner_getter=recipe_scanner_getter,
logger=logger,
)
return RecipeHandlerSet(
page_view=page_view,
listing=listing,
query=query,
management=management,
analysis=analysis,
sharing=sharing,
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -128,31 +128,38 @@ def test_register_startup_hooks_appends_once():
assert len(startup_bound_to_routes) == 2 assert len(startup_bound_to_routes) == 2
def test_to_route_mapping_uses_handler_owner(monkeypatch: pytest.MonkeyPatch): def test_to_route_mapping_uses_handler_set():
class DummyOwner: class DummyHandlerSet:
async def render_page(self, request): def __init__(self):
return web.Response(text="ok") self.calls = 0
async def list_recipes(self, request): # pragma: no cover - invoked via mapping def to_route_mapping(self):
return web.json_response({}) self.calls += 1
async def render_page(request): # pragma: no cover - simple coroutine
return web.Response(text="ok")
return {"render_page": render_page}
class DummyRoutes(base_routes_module.BaseRecipeRoutes): class DummyRoutes(base_routes_module.BaseRecipeRoutes):
def get_handler_owner(self): # noqa: D401 - simple override for test def __init__(self):
return DummyOwner() super().__init__()
self.created = 0
monkeypatch.setattr( def _create_handler_set(self): # noqa: D401 - simple override for test
base_routes_module.BaseRecipeRoutes, self.created += 1
"_HANDLER_NAMES", return DummyHandlerSet()
("render_page", "list_recipes"),
)
routes = DummyRoutes() routes = DummyRoutes()
mapping = routes.to_route_mapping() mapping = routes.to_route_mapping()
assert set(mapping.keys()) == {"render_page", "list_recipes"} assert set(mapping.keys()) == {"render_page"}
assert asyncio.iscoroutinefunction(mapping["render_page"]) assert asyncio.iscoroutinefunction(mapping["render_page"])
# Cached mapping reused on subsequent calls # Cached mapping reused on subsequent calls
assert routes.to_route_mapping() is mapping assert routes.to_route_mapping() is mapping
# Handler set cached for get_handler_owner callers
assert isinstance(routes.get_handler_owner(), DummyHandlerSet)
assert routes.created == 1
def test_recipe_route_registrar_binds_every_route(): def test_recipe_route_registrar_binds_every_route():