Files
orion/app/modules/routes.py
Samir Boulahtit f20266167d
Some checks failed
CI / ruff (push) Failing after 7s
CI / pytest (push) Failing after 1s
CI / architecture (push) Failing after 9s
CI / dependency-scanning (push) Successful in 27s
CI / audit (push) Successful in 8s
CI / docs (push) Has been skipped
fix(lint): auto-fix ruff violations and tune lint rules
- Auto-fixed 4,496 lint issues (import sorting, modern syntax, etc.)
- Added ignore rules for patterns intentional in this codebase:
  E402 (late imports), E712 (SQLAlchemy filters), B904 (raise from),
  SIM108/SIM105/SIM117 (readability preferences)
- Added per-file ignores for tests and scripts
- Excluded broken scripts/rename_terminology.py (has curly quotes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:10:42 +01:00

464 lines
14 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/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", "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/store.py -> store API routes
- routes/api/shop.py -> shop 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",
]