Coverage for fastblocks/adapters/templates/_base.py: 69%
91 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1import typing as t
2from abc import ABC
4from acb.adapters import get_adapters, root_path
5from acb.config import AdapterBase, Config
6from acb.depends import Inject, depends
7from anyio import Path as AsyncPath
8from starlette.requests import Request
9from starlette.responses import Response
11try:
12 # For newer versions of ACB where pkg_registry is in context
13 from acb import get_context
15 context = get_context()
16 pkg_registry = context.pkg_registry
17except (ImportError, AttributeError):
18 # Fallback for older versions or if context is not available
19 pkg_registry = None
22async def safe_await(func_or_value: t.Any) -> t.Any:
23 if callable(func_or_value):
24 try:
25 result = func_or_value()
26 if hasattr(result, "__await__") and callable(result.__await__): # type: ignore[misc]
27 return await t.cast("t.Awaitable[t.Any]", result)
28 return result
29 except Exception:
30 return True
31 return func_or_value
34TemplateContext: t.TypeAlias = dict[str, t.Any]
35TemplateResponse: t.TypeAlias = Response
36TemplateStr: t.TypeAlias = str
37TemplatePath: t.TypeAlias = str
38T = t.TypeVar("T")
41class TemplateRenderer(t.Protocol):
42 async def render_template(
43 self,
44 request: Request,
45 template: TemplatePath,
46 _: TemplateContext | None = None,
47 ) -> TemplateResponse: ...
50class TemplateLoader(t.Protocol):
51 async def get_template(self, name: TemplatePath) -> t.Any: ...
53 async def list_templates(self) -> list[TemplatePath]: ...
56class TemplatesBaseSettings(Config, ABC): # type: ignore[misc]
57 cache_timeout: int = 300
59 @depends.inject # type: ignore[misc]
60 def __init__(self, config: Inject[Config], **values: t.Any) -> None:
61 super().__init__(**values)
62 self.cache_timeout = self.cache_timeout if config.deployed else 1
65class TemplatesProtocol(t.Protocol):
66 def get_searchpath(self, adapter: t.Any, path: AsyncPath) -> None: ...
68 async def get_searchpaths(self, adapter: t.Any) -> list[AsyncPath]: ...
70 @staticmethod
71 def get_storage_path(path: AsyncPath) -> AsyncPath: ...
73 @staticmethod
74 def get_cache_key(path: AsyncPath) -> str: ...
77class TemplatesBase(AdapterBase): # type: ignore[misc]
78 app: t.Any | None = None
79 admin: t.Any | None = None
80 app_searchpaths: list[AsyncPath] | None = None
81 admin_searchpaths: list[AsyncPath] | None = None
83 def get_searchpath(self, adapter: t.Any, path: AsyncPath) -> list[AsyncPath]:
84 style = getattr(self.config.app, "style", "bulma")
85 base_path = path / "base"
86 style_path = path / style
87 style_adapter_path = path / style / adapter.name
88 theme_adapter_path = style_adapter_path / "theme"
89 return [theme_adapter_path, style_adapter_path, style_path, base_path]
91 async def get_searchpaths(self, adapter: t.Any) -> list[AsyncPath]:
92 searchpaths = []
93 base_root = self._get_base_root()
95 if adapter and hasattr(adapter, "category"):
96 searchpaths.extend(
97 self.get_searchpath(
98 adapter, base_root / "templates" / adapter.category
99 ),
100 )
102 if adapter and hasattr(adapter, "category") and adapter.category == "app":
103 searchpaths.extend(await self._get_app_searchpaths(adapter))
105 # Only use pkg_registry if it's available
106 if pkg_registry:
107 searchpaths.extend(await self._get_pkg_registry_searchpaths(adapter))
109 return searchpaths
111 def _get_base_root(self) -> AsyncPath:
112 if callable(root_path):
113 return AsyncPath(root_path())
114 return AsyncPath(root_path)
116 async def _get_app_searchpaths(self, adapter: t.Any) -> list[AsyncPath]:
117 searchpaths = []
118 for a in (
119 a
120 for a in get_adapters()
121 if a
122 and hasattr(a, "category")
123 and a.category not in ("app", "admin", "secret")
124 ):
125 exists_result = await safe_await((a.path / "_templates").exists)
126 if exists_result:
127 searchpaths.append(a.path / "_templates")
128 return searchpaths
130 async def _get_pkg_registry_searchpaths(self, adapter: t.Any) -> list[AsyncPath]:
131 searchpaths = []
132 for pkg in pkg_registry.get():
133 if (
134 pkg
135 and hasattr(pkg, "path")
136 and adapter
137 and hasattr(adapter, "category")
138 ):
139 searchpaths.extend(
140 self.get_searchpath(
141 adapter,
142 pkg.path / "adapters" / adapter.category / "_templates",
143 ),
144 )
145 return searchpaths
147 @staticmethod
148 def get_storage_path(path: AsyncPath) -> AsyncPath:
149 templates_path_name = "templates"
150 if templates_path_name not in path.parts:
151 templates_path_name = "_templates"
152 depth = path.parts.index(templates_path_name) - 1
153 _path = list(path.parts[depth:])
154 _path.insert(1, _path.pop(0))
155 return AsyncPath("/".join(_path))
156 depth = path.parts.index(templates_path_name)
157 return AsyncPath("/".join(path.parts[depth:]))
159 @staticmethod
160 def get_cache_key(path: AsyncPath) -> str:
161 return ":".join(path.parts)