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.service_registry import ServiceRegistry
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
logger = logging.getLogger(__name__)
@@ -23,6 +32,8 @@ class BaseRecipeRoutes:
definition.handler_name for definition in ROUTE_DEFINITIONS
)
template_name: str = "recipes.html"
def __init__(self) -> None:
self.recipe_scanner = None
self.lora_scanner = None
@@ -36,6 +47,7 @@ class BaseRecipeRoutes:
self._i18n_registered = False
self._startup_hooks_registered = False
self._handler_set: RecipeHandlerSet | None = None
self._handler_mapping: dict[str, Callable] | 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."""
if self._handler_mapping is None:
owner = self.get_handler_owner()
self._handler_mapping = {
name: getattr(owner, name) for name in self._HANDLER_NAMES
}
handler_set = self._create_handler_set()
self._handler_set = handler_set
self._handler_mapping = handler_set.to_route_mapping()
return self._handler_mapping
# Internal helpers -------------------------------------------------
@@ -105,5 +116,57 @@ class BaseRecipeRoutes:
def get_handler_owner(self):
"""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
def test_to_route_mapping_uses_handler_owner(monkeypatch: pytest.MonkeyPatch):
class DummyOwner:
async def render_page(self, request):
return web.Response(text="ok")
def test_to_route_mapping_uses_handler_set():
class DummyHandlerSet:
def __init__(self):
self.calls = 0
async def list_recipes(self, request): # pragma: no cover - invoked via mapping
return web.json_response({})
def to_route_mapping(self):
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):
def get_handler_owner(self): # noqa: D401 - simple override for test
return DummyOwner()
def __init__(self):
super().__init__()
self.created = 0
monkeypatch.setattr(
base_routes_module.BaseRecipeRoutes,
"_HANDLER_NAMES",
("render_page", "list_recipes"),
)
def _create_handler_set(self): # noqa: D401 - simple override for test
self.created += 1
return DummyHandlerSet()
routes = DummyRoutes()
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"])
# Cached mapping reused on subsequent calls
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():