# app/modules/routes.py """ Module route discovery for FastAPI. Provides utilities to: - Discover route modules from all registered modules - Auto-register API and page routes from self-contained modules This module bridges the gap between the module system and FastAPI, allowing routes to be defined within modules and automatically discovered and registered. Route Discovery: Routes are discovered from these file locations in each module: - routes/api/admin.py -> /api/v1/admin/* - routes/api/store.py -> /api/v1/store/* - routes/api/storefront.py -> /api/v1/storefront/* - routes/pages/admin.py -> /admin/* - routes/pages/store.py -> /store/* Usage: # In app/api/v1/{admin,store,storefront}/__init__.py from app.modules.routes import get_admin_api_routes # or store/storefront for route_info in get_admin_api_routes(): router.include_router( route_info.router, prefix=route_info.custom_prefix or "", tags=route_info.tags, ) Route Configuration: Modules can export a ROUTE_CONFIG dict to customize route registration: ROUTE_CONFIG = { "prefix": "/content-pages", # Custom prefix (replaces default) "tags": ["admin-content-pages"], # Custom tags "priority": 100, # Higher = registered later (for catch-all routes) } """ import importlib import logging from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from fastapi import APIRouter logger = logging.getLogger(__name__) @dataclass class RouteInfo: """Information about a discovered route.""" router: "APIRouter" prefix: str tags: list[str] include_in_schema: bool = True module_code: str = "" route_type: str = "" # "api" or "pages" frontend: str = "" # "admin", "store", "storefront" priority: int = 0 # Higher = registered later (for catch-all routes) custom_prefix: str = "" # Custom prefix from ROUTE_CONFIG def discover_module_routes() -> list[RouteInfo]: """ Discover routes from all registered self-contained modules. Scans all modules in the registry and returns RouteInfo objects for modules that have routes directories. Route discovery looks for: - routes/api/admin.py -> admin API routes - routes/api/store.py -> store API routes - routes/api/storefront.py -> storefront API routes - routes/pages/admin.py -> admin page routes - routes/pages/store.py -> store page routes Returns: List of RouteInfo objects with router and registration info Example: >>> routes = discover_module_routes() >>> for route in routes: ... print(f"{route.module_code}: {route.route_type}/{route.frontend}") analytics: pages/store cms: api/admin cms: api/store cms: pages/admin cms: pages/store """ # Import here to avoid circular imports from app.modules.registry import MODULES routes: list[RouteInfo] = [] for module in MODULES.values(): if not module.is_self_contained: continue module_routes = _discover_module_routes(module.code) routes.extend(module_routes) logger.info(f"Discovered {len(routes)} routes from self-contained modules") return routes def _discover_module_routes(module_code: str) -> list[RouteInfo]: """ Discover routes for a specific module. Args: module_code: Module code (e.g., "analytics", "cms") Returns: List of RouteInfo for this module """ routes: list[RouteInfo] = [] dir_name = module_code.replace("-", "_") module_path = Path(__file__).parent / dir_name if not module_path.exists(): return routes routes_path = module_path / "routes" if not routes_path.exists(): return routes # Discover API routes (with fallback to routes/ for legacy modules) api_routes = _discover_routes_in_dir( module_code, dir_name, routes_path / "api", "api", fallback_dir=routes_path # Allow routes/admin.py as fallback ) routes.extend(api_routes) # Discover page routes page_routes = _discover_routes_in_dir( module_code, dir_name, routes_path / "pages", "pages" ) routes.extend(page_routes) return routes def _discover_routes_in_dir( module_code: str, dir_name: str, routes_dir: Path, route_type: str, fallback_dir: Path | None = None ) -> list[RouteInfo]: """ Discover routes in a specific directory (api/ or pages/). Args: module_code: Module code for tags dir_name: Directory name (module_code with _ instead of -) routes_dir: Path to routes/api/ or routes/pages/ route_type: "api" or "pages" fallback_dir: Optional fallback directory (e.g., routes/ for modules with routes/admin.py instead of routes/api/admin.py) Returns: List of RouteInfo for discovered routes """ routes: list[RouteInfo] = [] # Look for admin.py, store.py, storefront.py, public.py, webhooks.py frontends = { "admin": { "api_prefix": "/api/v1/admin", "pages_prefix": "/admin", "include_in_schema": True, }, "store": { "api_prefix": "/api/v1/store", "pages_prefix": "/store", "include_in_schema": route_type == "api", }, "storefront": { "api_prefix": "/api/v1/storefront", "pages_prefix": "/storefront", "include_in_schema": route_type == "api", }, "platform": { "api_prefix": "/api/v1/platform", "pages_prefix": "/platform", "include_in_schema": True, }, "webhooks": { "api_prefix": "/api/v1/webhooks", "pages_prefix": "/webhooks", "include_in_schema": True, }, "merchant": { "api_prefix": "/api/v1/merchants", "pages_prefix": "/merchants", "include_in_schema": True, }, } for frontend, config in frontends.items(): # Check primary location first, then fallback route_file = routes_dir / f"{frontend}.py" if routes_dir.exists() else None use_fallback = False if route_file is None or not route_file.exists(): if fallback_dir and fallback_dir.exists(): fallback_file = fallback_dir / f"{frontend}.py" if fallback_file.exists(): route_file = fallback_file use_fallback = True if route_file is None or not route_file.exists(): continue try: # Import the module if use_fallback: import_path = f"app.modules.{dir_name}.routes.{frontend}" else: import_path = f"app.modules.{dir_name}.routes.{route_type}.{frontend}" route_module = importlib.import_module(import_path) # Get the router (try common names) router = None for attr_name in ["router", f"{frontend}_router", "api_router", "page_router"]: if hasattr(route_module, attr_name): router = getattr(route_module, attr_name) break if router is None: logger.warning(f"No router found in {import_path}") continue # Read ROUTE_CONFIG if present route_config = getattr(route_module, "ROUTE_CONFIG", {}) custom_prefix = route_config.get("prefix", "") custom_tags = route_config.get("tags", []) priority = route_config.get("priority", 0) # Determine prefix based on route type if route_type == "api": prefix = config["api_prefix"] else: prefix = config["pages_prefix"] # Build tags - use custom tags if provided, otherwise default tags = custom_tags if custom_tags else [f"{frontend}-{module_code}"] route_info = RouteInfo( router=router, prefix=prefix, tags=tags, include_in_schema=config["include_in_schema"], module_code=module_code, route_type=route_type, frontend=frontend, priority=priority, custom_prefix=custom_prefix, ) routes.append(route_info) logger.debug(f"Discovered route: {module_code} {route_type}/{frontend}") except ImportError as e: logger.warning(f"Failed to import {import_path}: {e}") except Exception as e: logger.error(f"Error discovering routes in {route_file}: {e}") return routes def get_api_routes() -> list[RouteInfo]: """Get only API routes from modules.""" return [r for r in discover_module_routes() if r.route_type == "api"] def get_page_routes() -> list[RouteInfo]: """Get only page routes from modules.""" return [r for r in discover_module_routes() if r.route_type == "pages"] def get_store_page_routes() -> list[RouteInfo]: """ Get store page routes from modules, sorted by priority. Returns routes sorted by priority (lower first, higher last). This ensures catch-all routes (priority 100+) are registered after specific routes. """ routes = [r for r in discover_module_routes() if r.route_type == "pages" and r.frontend == "store"] return sorted(routes, key=lambda r: r.priority) def get_admin_page_routes() -> list[RouteInfo]: """Get admin page routes from modules.""" return [r for r in discover_module_routes() if r.route_type == "pages" and r.frontend == "admin"] def get_admin_api_routes() -> list[RouteInfo]: """ Get admin API routes from modules, sorted by priority. Returns routes sorted by priority (lower first, higher last). This ensures catch-all routes (priority 100+) are registered after specific routes. """ routes = [ r for r in discover_module_routes() if r.route_type == "api" and r.frontend == "admin" ] return sorted(routes, key=lambda r: r.priority) def get_store_api_routes() -> list[RouteInfo]: """ Get store API routes from modules, sorted by priority. Returns routes sorted by priority (lower first, higher last). This ensures catch-all routes (priority 100+) are registered after specific routes. """ routes = [ r for r in discover_module_routes() if r.route_type == "api" and r.frontend == "store" ] return sorted(routes, key=lambda r: r.priority) def get_storefront_api_routes() -> list[RouteInfo]: """ Get storefront API routes from modules, sorted by priority. Returns routes sorted by priority (lower first, higher last). This ensures catch-all routes (priority 100+) are registered after specific routes. """ routes = [ r for r in discover_module_routes() if r.route_type == "api" and r.frontend == "storefront" ] return sorted(routes, key=lambda r: r.priority) def get_platform_api_routes() -> list[RouteInfo]: """ Get platform API routes from modules, sorted by priority. Platform routes are unauthenticated endpoints for marketing pages, pricing info, and other public-facing features. """ routes = [ r for r in discover_module_routes() if r.route_type == "api" and r.frontend == "platform" ] return sorted(routes, key=lambda r: r.priority) def get_webhooks_api_routes() -> list[RouteInfo]: """ Get webhook API routes from modules, sorted by priority. Webhook routes handle callbacks from external services (Stripe, payment providers, etc.). """ routes = [ r for r in discover_module_routes() if r.route_type == "api" and r.frontend == "webhooks" ] return sorted(routes, key=lambda r: r.priority) def get_platform_page_routes() -> list[RouteInfo]: """ Get platform (marketing) page routes from modules, sorted by priority. Platform pages are unauthenticated marketing pages like: - Homepage (/) - Pricing (/pricing) - Signup (/signup) - Find shop (/find-shop) - CMS catch-all (/{slug}) Note: CMS routes should have priority=100 to be registered last since they have catch-all patterns. """ routes = [ r for r in discover_module_routes() if r.route_type == "pages" and r.frontend == "platform" ] return sorted(routes, key=lambda r: r.priority) def get_merchant_api_routes() -> list[RouteInfo]: """ Get merchant API routes from modules, sorted by priority. Merchant routes are authenticated endpoints for the merchant billing portal, where business owners manage subscriptions, view invoices, and see their stores. """ routes = [ r for r in discover_module_routes() if r.route_type == "api" and r.frontend == "merchant" ] return sorted(routes, key=lambda r: r.priority) def get_merchant_page_routes() -> list[RouteInfo]: """ Get merchant page routes from modules, sorted by priority. Merchant pages include: - Dashboard (overview of stores, subscriptions) - Subscriptions per platform - Billing history / invoices - Profile management """ routes = [ r for r in discover_module_routes() if r.route_type == "pages" and r.frontend == "merchant" ] return sorted(routes, key=lambda r: r.priority) def get_storefront_page_routes() -> list[RouteInfo]: """ Get storefront (customer shop) page routes from modules, sorted by priority. Storefront pages include: - Catalog pages (/, /products, /products/{id}, /categories/{slug}) - Cart and checkout (/cart, /checkout) - Account pages (/account/*) - CMS content pages (/{slug}) Note: CMS routes should have priority=100 to be registered last since they have catch-all patterns. """ routes = [ r for r in discover_module_routes() if r.route_type == "pages" and r.frontend == "storefront" ] return sorted(routes, key=lambda r: r.priority) __all__ = [ "RouteInfo", "discover_module_routes", "get_api_routes", "get_page_routes", "get_store_page_routes", "get_admin_page_routes", "get_admin_api_routes", "get_store_api_routes", "get_storefront_api_routes", "get_platform_api_routes", "get_webhooks_api_routes", "get_platform_page_routes", "get_storefront_page_routes", "get_merchant_api_routes", "get_merchant_page_routes", ]