Coverage for fastblocks/adapters/templates/_base.py: 76%

74 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-21 04:50 -0700

1import typing as t 

2 

3from acb import pkg_registry 

4from acb.adapters import get_adapters, root_path 

5from acb.config import AdapterBase, Settings 

6from acb.depends import depends 

7from anyio import Path as AsyncPath 

8from starlette.requests import Request 

9from starlette.responses import Response 

10 

11 

12async def safe_await(func_or_value: t.Any) -> t.Any: 

13 if callable(func_or_value): 

14 try: 

15 result = func_or_value() 

16 if hasattr(result, "__await__") and callable(result.__await__): # type: ignore[misc] 

17 return await t.cast("t.Awaitable[t.Any]", result) 

18 return result 

19 except Exception: 

20 return True 

21 return func_or_value 

22 

23 

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

25TemplateResponse: t.TypeAlias = Response 

26TemplateStr: t.TypeAlias = str 

27TemplatePath: t.TypeAlias = str 

28T = t.TypeVar("T") 

29 

30 

31class TemplateRenderer(t.Protocol): 

32 async def render_template( 

33 self, 

34 request: Request, 

35 template: TemplatePath, 

36 _: TemplateContext | None = None, 

37 ) -> TemplateResponse: ... 

38 

39 

40class TemplateLoader(t.Protocol): 

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

42 

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

44 

45 

46class TemplatesBaseSettings(Settings): 

47 cache_timeout: int = 300 

48 

49 @depends.inject 

50 def __init__(self, config: t.Any = depends(), **values: t.Any) -> None: 

51 super().__init__(**values) 

52 self.cache_timeout = self.cache_timeout if config.deployed else 1 

53 

54 

55class TemplatesProtocol(t.Protocol): 

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

57 

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

59 

60 @staticmethod 

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

62 

63 @staticmethod 

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

65 

66 

67class TemplatesBase(AdapterBase): 

68 app: t.Any | None = None 

69 admin: t.Any | None = None 

70 app_searchpaths: list[AsyncPath] | None = None 

71 admin_searchpaths: list[AsyncPath] | None = None 

72 

73 def get_searchpath(self, adapter: t.Any, path: AsyncPath) -> list[AsyncPath]: 

74 style = getattr(self.config.app, "style", "bulma") 

75 base_path = path / "base" 

76 style_path = path / style 

77 style_adapter_path = path / style / adapter.name 

78 theme_adapter_path = style_adapter_path / "theme" 

79 return [theme_adapter_path, style_adapter_path, style_path, base_path] 

80 

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

82 searchpaths = [] 

83 if callable(root_path): 

84 base_root = AsyncPath(root_path()) 

85 else: 

86 base_root = AsyncPath(root_path) 

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

88 searchpaths.extend( 

89 self.get_searchpath( 

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

91 ), 

92 ) 

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

94 for a in [ 

95 a 

96 for a in get_adapters() 

97 if a 

98 and hasattr(a, "category") 

99 and a.category not in ("app", "admin", "secret") 

100 ]: 

101 exists_result = await safe_await((a.path / "_templates").exists) 

102 if exists_result: 

103 searchpaths.append(a.path / "_templates") 

104 for pkg in pkg_registry.get(): 

105 if ( 

106 pkg 

107 and hasattr(pkg, "path") 

108 and adapter 

109 and hasattr(adapter, "category") 

110 ): 

111 searchpaths.extend( 

112 self.get_searchpath( 

113 adapter, 

114 pkg.path / "adapters" / adapter.category / "_templates", 

115 ), 

116 ) 

117 return searchpaths 

118 

119 @staticmethod 

120 def get_storage_path(path: AsyncPath) -> AsyncPath: 

121 templates_path_name = "templates" 

122 if templates_path_name not in path.parts: 

123 templates_path_name = "_templates" 

124 depth = path.parts.index(templates_path_name) - 1 

125 _path = list(path.parts[depth:]) 

126 _path.insert(1, _path.pop(0)) 

127 return AsyncPath("/".join(_path)) 

128 depth = path.parts.index(templates_path_name) 

129 return AsyncPath("/".join(path.parts[depth:])) 

130 

131 @staticmethod 

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

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