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

1import typing as t 

2from abc import ABC 

3 

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 

10 

11try: 

12 # For newer versions of ACB where pkg_registry is in context 

13 from acb import get_context 

14 

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 

20 

21 

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 

32 

33 

34TemplateContext: t.TypeAlias = dict[str, t.Any] 

35TemplateResponse: t.TypeAlias = Response 

36TemplateStr: t.TypeAlias = str 

37TemplatePath: t.TypeAlias = str 

38T = t.TypeVar("T") 

39 

40 

41class TemplateRenderer(t.Protocol): 

42 async def render_template( 

43 self, 

44 request: Request, 

45 template: TemplatePath, 

46 _: TemplateContext | None = None, 

47 ) -> TemplateResponse: ... 

48 

49 

50class TemplateLoader(t.Protocol): 

51 async def get_template(self, name: TemplatePath) -> t.Any: ... 

52 

53 async def list_templates(self) -> list[TemplatePath]: ... 

54 

55 

56class TemplatesBaseSettings(Config, ABC): # type: ignore[misc] 

57 cache_timeout: int = 300 

58 

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 

63 

64 

65class TemplatesProtocol(t.Protocol): 

66 def get_searchpath(self, adapter: t.Any, path: AsyncPath) -> None: ... 

67 

68 async def get_searchpaths(self, adapter: t.Any) -> list[AsyncPath]: ... 

69 

70 @staticmethod 

71 def get_storage_path(path: AsyncPath) -> AsyncPath: ... 

72 

73 @staticmethod 

74 def get_cache_key(path: AsyncPath) -> str: ... 

75 

76 

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 

82 

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] 

90 

91 async def get_searchpaths(self, adapter: t.Any) -> list[AsyncPath]: 

92 searchpaths = [] 

93 base_root = self._get_base_root() 

94 

95 if adapter and hasattr(adapter, "category"): 

96 searchpaths.extend( 

97 self.get_searchpath( 

98 adapter, base_root / "templates" / adapter.category 

99 ), 

100 ) 

101 

102 if adapter and hasattr(adapter, "category") and adapter.category == "app": 

103 searchpaths.extend(await self._get_app_searchpaths(adapter)) 

104 

105 # Only use pkg_registry if it's available 

106 if pkg_registry: 

107 searchpaths.extend(await self._get_pkg_registry_searchpaths(adapter)) 

108 

109 return searchpaths 

110 

111 def _get_base_root(self) -> AsyncPath: 

112 if callable(root_path): 

113 return AsyncPath(root_path()) 

114 return AsyncPath(root_path) 

115 

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 

129 

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 

146 

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:])) 

158 

159 @staticmethod 

160 def get_cache_key(path: AsyncPath) -> str: 

161 return ":".join(path.parts)