Complete the public -> platform naming migration across the codebase. This aligns with the naming convention where "platform" refers to the marketing/public-facing pages of the platform itself. Changes: - Update all imports from public to platform modules - Update template references from public/ to platform/ - Update route registrations to use platform prefix - Update documentation to reflect new naming - Update test files for platform API endpoints Files affected: - app/api/main.py - router imports - app/modules/*/routes/*/platform.py - route definitions - app/modules/*/templates/*/platform/ - template files - app/modules/routes.py - route discovery - docs/* - documentation updates - tests/integration/api/v1/platform/ - test files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
422 lines
13 KiB
Python
422 lines
13 KiB
Python
# 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/vendor.py -> /api/v1/vendor/*
|
|
- routes/api/storefront.py -> /api/v1/storefront/*
|
|
- routes/pages/admin.py -> /admin/*
|
|
- routes/pages/vendor.py -> /vendor/*
|
|
|
|
Usage:
|
|
# In app/api/v1/{admin,vendor,storefront}/__init__.py
|
|
from app.modules.routes import get_admin_api_routes # or vendor/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, field
|
|
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", "vendor", "shop"
|
|
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/vendor.py -> vendor API routes
|
|
- routes/api/shop.py -> shop API routes
|
|
- routes/pages/admin.py -> admin page routes
|
|
- routes/pages/vendor.py -> vendor 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/vendor
|
|
cms: api/admin
|
|
cms: api/vendor
|
|
cms: pages/admin
|
|
cms: pages/vendor
|
|
"""
|
|
# 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, vendor.py, storefront.py, public.py, webhooks.py
|
|
frontends = {
|
|
"admin": {
|
|
"api_prefix": "/api/v1/admin",
|
|
"pages_prefix": "/admin",
|
|
"include_in_schema": True,
|
|
},
|
|
"vendor": {
|
|
"api_prefix": "/api/v1/vendor",
|
|
"pages_prefix": "/vendor",
|
|
"include_in_schema": True if route_type == "api" else False,
|
|
},
|
|
"storefront": {
|
|
"api_prefix": "/api/v1/storefront",
|
|
"pages_prefix": "/storefront",
|
|
"include_in_schema": True if route_type == "api" else False,
|
|
},
|
|
"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,
|
|
},
|
|
}
|
|
|
|
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
|
|
if custom_tags:
|
|
tags = custom_tags
|
|
else:
|
|
tags = [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_vendor_page_routes() -> list[RouteInfo]:
|
|
"""Get vendor page routes from modules."""
|
|
return [r for r in discover_module_routes() if r.route_type == "pages" and r.frontend == "vendor"]
|
|
|
|
|
|
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_vendor_api_routes() -> list[RouteInfo]:
|
|
"""
|
|
Get vendor 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 == "vendor"
|
|
]
|
|
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_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_vendor_page_routes",
|
|
"get_admin_page_routes",
|
|
"get_admin_api_routes",
|
|
"get_vendor_api_routes",
|
|
"get_storefront_api_routes",
|
|
"get_platform_api_routes",
|
|
"get_webhooks_api_routes",
|
|
"get_platform_page_routes",
|
|
"get_storefront_page_routes",
|
|
]
|