Coverage for src/lexigram/web/debug/routes.py: 0%
60 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""Debug routes for development.
3Provides /debug/routes and /debug/di-graph endpoints.
4"""
6from __future__ import annotations
8from typing import Any
10from lexigram.web.exceptions import ForbiddenError
13async def _check_admin(request: Any) -> None:
14 """Ensure the current user is an admin."""
15 user = request.scope.get("extensions", {}).get("user")
16 if not user:
17 raise ForbiddenError("Authentication required")
19 role = user.get("role")
20 roles = user.get("roles", [])
21 if role != "admin" and "admin" not in roles:
22 raise ForbiddenError("Admin access required")
25async def debug_routes(request: Any) -> Any:
26 """List all registered routes in a table format."""
27 await _check_admin(request)
28 from starlette.responses import JSONResponse
30 # Get routes from the app
31 routes = []
33 if hasattr(request.app, "routes"):
34 for route in request.app.routes:
35 route_info = {
36 "path": getattr(route, "path", "/"),
37 "methods": getattr(route, "methods", []),
38 }
40 # Get handler info
41 if hasattr(route, "endpoint"):
42 endpoint = route.endpoint
43 route_info["handler"] = getattr(endpoint, "__name__", str(endpoint))
44 route_info["module"] = getattr(endpoint, "__module__", "")
46 routes.append(route_info)
48 return JSONResponse(
49 {
50 "routes": routes,
51 "count": len(routes),
52 },
53 )
56async def debug_di_graph(request: Any) -> Any:
57 """Generate a DI dependency graph in DOT/Mermaid format."""
58 await _check_admin(request)
59 from starlette.responses import JSONResponse
61 format = request.query_params.get("format", "json")
63 # Try to get container from app state
64 container = None
65 if hasattr(request.app, "state"):
66 container = getattr(request.app.state, "container", None)
68 if not container:
69 return JSONResponse(
70 {"error": "Container not available"},
71 status_code=404,
72 )
74 # Build dependency graph
75 graph: dict[str, list[Any]] = {
76 "nodes": [],
77 "edges": [],
78 }
80 if hasattr(container, "_registry"):
81 for descriptor in container._registry.all():
82 service_name = getattr(
83 descriptor.service_type,
84 "__name__",
85 str(descriptor.service_type),
86 )
87 node = {
88 "id": service_name,
89 "name": service_name,
90 "lifetime": descriptor.scope.value
91 if hasattr(descriptor.scope, "value")
92 else str(descriptor.scope),
93 }
94 graph["nodes"].append(node)
96 if format == "mermaid":
97 # Generate Mermaid graph
98 lines = ["graph LR"]
99 for node in graph["nodes"]:
100 lifetime = node.get("lifetime", "")
101 lines.append(f' {node["id"]}["{node["name"]} ({lifetime})"]')
103 output = "\n".join(lines)
104 return JSONResponse({"mermaid": output})
106 return JSONResponse(graph)
109async def debug_container(request: Any) -> Any:
110 """List all registered services in the DI container."""
111 await _check_admin(request)
112 from starlette.responses import JSONResponse
114 container = None
115 if hasattr(request.app, "state"):
116 container = getattr(request.app.state, "container", None)
118 if not container:
119 return JSONResponse(
120 {"error": "Container not available"},
121 status_code=404,
122 )
124 services = []
126 if hasattr(container, "_registry"):
127 for descriptor in container._registry.all():
128 services.append(
129 {
130 "name": getattr(
131 descriptor.service_type,
132 "__name__",
133 str(descriptor.service_type),
134 ),
135 "lifetime": descriptor.scope.value
136 if hasattr(descriptor.scope, "value")
137 else str(descriptor.scope),
138 "type": str(descriptor.implementation),
139 },
140 )
142 return JSONResponse(
143 {
144 "services": services,
145 "count": len(services),
146 },
147 )