Coverage for fastblocks/adapters/routes/default.py: 0%
133 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 04:50 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 04:50 -0700
1"""Default Routes Adapter for FastBlocks.
3Provides dynamic route discovery and registration for FastBlocks applications.
4Includes automatic route gathering from adapters, static file serving, and HTMX endpoint support.
6Features:
7- Dynamic route discovery from adapter modules
8- HTMX-aware endpoints with template fragment rendering
9- Built-in static routes (favicon, robots.txt)
10- Automatic static file serving for storage adapters
11- Template block rendering endpoints
12- Route gathering from base routes.py files
13- Integration with FastBlocks template system
15Requirements:
16- starlette>=0.47.1
17- jinja2>=3.1.6
19Usage:
20```python
21from acb.depends import depends
22from acb.adapters import import_adapter
24routes = depends.get("routes")
26Routes = import_adapter("routes")
28app_routes = routes.routes
29```
31Author: lesleslie <les@wedgwoodwebworks.com>
32Created: 2025-01-12
33"""
35from contextlib import suppress
36from importlib import import_module
37from uuid import UUID
39from acb.adapters import (
40 AdapterStatus,
41 get_adapters,
42 get_installed_adapter,
43 import_adapter,
44 root_path,
45)
46from acb.config import Config
47from acb.debug import debug
48from acb.depends import depends
49from anyio import Path as AsyncPath
50from jinja2.exceptions import TemplateNotFound
51from starlette.endpoints import HTTPEndpoint
52from starlette.exceptions import HTTPException
53from starlette.requests import Request
54from starlette.responses import PlainTextResponse, Response
55from starlette.routing import Host, Mount, Route, Router, WebSocketRoute
56from starlette.types import Receive, Scope, Send
57from fastblocks.actions.query import create_query_context
58from fastblocks.htmx import HtmxRequest
60from ._base import RoutesBase, RoutesBaseSettings
62try:
63 Templates = import_adapter("templates")
64except Exception:
65 Templates = None
67base_routes_path = root_path / "routes.py"
70class RoutesSettings(RoutesBaseSettings): ...
73class FastBlocksEndpoint(HTTPEndpoint):
74 config: Config = depends()
76 def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
77 super().__init__(scope, receive, send)
78 self.templates = depends.get("templates")
81class Index(FastBlocksEndpoint):
82 @depends.inject
83 async def get(self, request: HtmxRequest | Request) -> Response:
84 debug(request)
85 path_params = getattr(request, "path_params", {})
86 page = path_params.get("page") or "home"
87 template = "index.html"
88 headers = {"vary": "hx-request"}
89 scope = getattr(request, "scope", {})
90 if htmx := scope.get("htmx"):
91 debug(htmx)
92 template = f"{page.lstrip('/')}.html"
93 headers["hx-push-url"] = "/" if page == "home" else page
94 debug(page, template)
95 context = create_query_context(request, base_context={"page": page.lstrip("/")})
96 query_params = getattr(request, "query_params", {})
97 if "model" in query_params:
98 model_name = query_params["model"]
99 if f"{model_name}_parser" in context:
100 parser = context[f"{model_name}_parser"]
101 context[f"{model_name}_list"] = await parser.parse_and_execute()
102 context[f"{model_name}_count"] = await parser.get_count()
103 try:
104 return await self.templates.render_template(
105 request,
106 template,
107 headers=headers,
108 context=context,
109 )
110 except TemplateNotFound:
111 raise HTTPException(status_code=404)
114class Block(FastBlocksEndpoint):
115 async def get(self, request: HtmxRequest | Request) -> Response:
116 debug(request)
117 path_params = getattr(request, "path_params", {})
118 block = f"blocks/{path_params.get('block', 'default')}.html"
119 context = create_query_context(request)
120 query_params = getattr(request, "query_params", {})
121 if "model" in query_params:
122 model_name = query_params["model"]
123 if f"{model_name}_parser" in context:
124 parser = context[f"{model_name}_parser"]
125 context[f"{model_name}_list"] = await parser.parse_and_execute()
126 context[f"{model_name}_count"] = await parser.get_count()
127 try:
128 return await self.templates.render_template(request, block, context=context)
129 except TemplateNotFound:
130 raise HTTPException(status_code=404)
133class Component(FastBlocksEndpoint):
134 @depends.inject
135 async def get(self, request: HtmxRequest | Request) -> Response:
136 debug(request)
137 component_name = getattr(request, "path_params", {}).get("component", "default")
138 query_params = getattr(request, "query_params", {})
139 context = create_query_context(request, base_context=dict(query_params))
140 if "model" in query_params:
141 model_name = query_params["model"]
142 if f"{model_name}_parser" in context:
143 parser = context[f"{model_name}_parser"]
144 context[f"{model_name}_list"] = await parser.parse_and_execute()
145 context[f"{model_name}_count"] = await parser.get_count()
146 try:
147 htmy = depends.get("htmy")
148 if htmy is None:
149 raise HTTPException(
150 status_code=500, detail="HTMY adapter not available"
151 )
152 return await htmy.render_component(request, component_name, context=context)
153 except Exception as e:
154 debug(f"Component '{component_name}' not found: {e}")
155 raise HTTPException(status_code=404)
158class Routes(RoutesBase):
159 routes: list[Route | Router | Mount | Host | WebSocketRoute] = []
161 async def gather_routes(self, path: AsyncPath) -> None:
162 depth = -2
163 if "adapters" in path.parts:
164 depth = -4
165 module_path = ".".join(path.parts[depth:]).removesuffix(".py")
166 debug(path, depth, module_path)
167 with suppress(ModuleNotFoundError):
168 module = import_module(module_path)
169 module_routes = getattr(module, "routes", None)
170 if module_routes and isinstance(module_routes, list):
171 self.routes = module.routes + self.routes
173 @staticmethod
174 async def favicon(request: Request) -> Response:
175 return PlainTextResponse("", 200)
177 @staticmethod
178 async def robots(request: Request) -> Response:
179 txt = "User-agent: *\nDisallow: /dashboard/\nDisallow: /blocks/"
180 return PlainTextResponse(txt, 200)
182 @depends.inject
183 async def init(self) -> None:
184 self.routes.extend(
185 [
186 Route("/favicon.ico", endpoint=self.favicon, methods=["GET"]),
187 Route("/robots.txt", endpoint=self.robots, methods=["GET"]),
188 Route("/", Index, methods=["GET"]),
189 Route("/{page}", Index, methods=["GET"]),
190 Route("/block/{block}", Block, methods=["GET"]),
191 Route("/component/{component}", Component, methods=["GET"]),
192 ],
193 )
194 for adapter in get_adapters():
195 routes_path = adapter.path.parent / "_routes.py"
196 if await routes_path.exists():
197 await self.gather_routes(routes_path)
198 if await base_routes_path.exists():
199 await self.gather_routes(base_routes_path)
200 if get_installed_adapter("storage") in ("file", "memory"):
201 from starlette.staticfiles import StaticFiles
203 self.routes.append(
204 Mount(
205 "/media",
206 app=StaticFiles(directory=self.config.storage.local_path / "media"),
207 name="media",
208 ),
209 )
210 if not self.config.deployed:
211 from starlette.staticfiles import StaticFiles
213 self.routes.append(
214 Mount(
215 "/static",
216 app=StaticFiles(
217 directory=self.config.storage.local_path / "static"
218 ),
219 name="media",
220 ),
221 )
222 debug(self.routes)
225MODULE_ID = UUID("01937d86-6f4c-7d5e-a01f-3456789012cd")
226MODULE_STATUS = AdapterStatus.STABLE
228with suppress(Exception):
229 depends.set(Routes)