Coverage for fastblocks / adapters / app / default.py: 3%

212 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-26 03:58 -0800

1"""Default App Adapter for FastBlocks. 

2 

3Provides the main FastBlocks application instance with lifecycle management, 

4startup/shutdown sequences, and adapter integration. 

5 

6Author: lesleslie <les@wedgwoodwebworks.com> 

7Created: 2025-01-12 

8""" 

9 

10import typing as t 

11from base64 import b64encode 

12from contextlib import asynccontextmanager, suppress 

13from time import perf_counter 

14from uuid import UUID 

15 

16from acb.adapters import AdapterStatus, get_adapter 

17from acb.depends import depends 

18from starlette.types import ASGIApp, Receive, Scope, Send 

19from fastblocks.applications import FastBlocks 

20 

21from ._base import AppBase, AppBaseSettings 

22 

23main_start = perf_counter() 

24Cache = Storage = None 

25 

26 

27class AppSettings(AppBaseSettings): 

28 url: str = "http://localhost:8000" 

29 token_id: str | None = "_fb_" 

30 

31 def __init__(self, **data: t.Any) -> None: 

32 super().__init__(**data) 

33 # Note: URL configuration moved to runtime initialization 

34 # to avoid coroutine access in __init__ 

35 token_prefix = self.token_id or "_fb_" 

36 self.token_id = "".join( 

37 [token_prefix, b64encode(self.name.encode()).decode().rstrip("=")], 

38 ) 

39 

40 

41class FastBlocksApp(FastBlocks): 

42 def __init__(self, **kwargs: t.Any) -> None: 

43 super().__init__(lifespan=self.lifespan, **kwargs) 

44 

45 async def init(self) -> None: 

46 pass 

47 

48 def _get_startup_time(self) -> float: 

49 startup_time = getattr(self, "_startup_time", None) 

50 if startup_time is None or startup_time <= 0: 

51 import time 

52 

53 init_start = getattr(self, "_init_start_time", None) 

54 startup_time = time.time() - init_start if init_start else 0.001 

55 return startup_time 

56 

57 def _get_debug_enabled(self, config: t.Any) -> list[str]: 

58 debug_enabled = [] 

59 if hasattr(config, "debug"): 

60 for key, value in vars(config.debug).items(): 

61 if value and key != "production": 

62 debug_enabled.append(key) 

63 return debug_enabled 

64 

65 def _get_color_constants(self) -> dict[str, str]: 

66 return { 

67 "GREEN": "\033[92m", 

68 "BLUE": "\033[94m", 

69 "YELLOW": "\033[93m", 

70 "CYAN": "\033[96m", 

71 "RESET": "\033[0m", 

72 "BOLD": "\033[1m", 

73 } 

74 

75 def _format_info_lines( 

76 self, 

77 config: t.Any, 

78 colors: dict[str, str], 

79 debug_enabled: list[str], 

80 startup_time: float, 

81 ) -> list[str]: 

82 app_title = getattr(config.app, "title", "Welcome to FastBlocks") 

83 app_domain = getattr(config.app, "domain", "localhost") 

84 debug_str = ", ".join(debug_enabled) if debug_enabled else "disabled" 

85 

86 return [ 

87 f"{colors['CYAN']}{colors['BOLD']}{app_title}{colors['RESET']}", 

88 f"{colors['BLUE']}Domain: {app_domain}{colors['RESET']}", 

89 f"{colors['YELLOW']}Debug: {debug_str}{colors['RESET']}", 

90 f"{colors['YELLOW']}══════════════════════════════════════════════════{colors['RESET']}", 

91 f"{colors['GREEN']}🚀 FastBlocks Application Ready{colors['RESET']}", 

92 f"{colors['YELLOW']}⚡ Startup time: {startup_time * 1000:.2f}ms{colors['RESET']}", 

93 f"{colors['CYAN']}🌐 Server running on http://127.0.0.1:8000{colors['RESET']}", 

94 f"{colors['YELLOW']}══════════════════════════════════════════════════{colors['RESET']}", 

95 ] 

96 

97 def _clean_and_center_line(self, line: str, colors: dict[str, str]) -> str: 

98 line_clean = line 

99 for color in colors.values(): 

100 line_clean = line_clean.replace(color, "") 

101 line_width = len(line_clean) 

102 padding = max(0, (90 - line_width) // 2) 

103 return " " * padding + line 

104 

105 async def _display_fancy_startup(self) -> None: 

106 from acb.depends import depends 

107 from aioconsole import aprint 

108 from pyfiglet import Figlet 

109 

110 config = await depends.get("config") 

111 app_name = getattr(config.app, "name", "FastBlocks") 

112 startup_time = self._get_startup_time() 

113 debug_enabled = self._get_debug_enabled(config) 

114 colors = self._get_color_constants() 

115 banner = Figlet(font="slant", width=90, justify="center").renderText( 

116 app_name.upper(), 

117 ) 

118 await aprint(f"\n\n{banner}\n") 

119 info_lines = self._format_info_lines( 

120 config, 

121 colors, 

122 debug_enabled, 

123 startup_time, 

124 ) 

125 for line in info_lines: 

126 self._clean_and_center_line(line, colors) 

127 

128 async def _display_simple_startup(self) -> None: 

129 from contextlib import suppress 

130 

131 with suppress(Exception): 

132 from acb.depends import depends 

133 

134 config = await depends.get("config") 

135 getattr(config.app, "name", "FastBlocks") 

136 self._get_startup_time() 

137 

138 async def post_startup(self) -> None: 

139 try: 

140 await self._display_fancy_startup() 

141 except Exception: 

142 await self._display_simple_startup() 

143 

144 @asynccontextmanager 

145 async def lifespan(self, app: "FastBlocks") -> t.AsyncIterator[None]: 

146 try: 

147 logger = getattr(self, "logger", None) 

148 if logger: 

149 logger.info("FastBlocks application starting up") 

150 except Exception as e: 

151 logger = getattr(self, "logger", None) 

152 if logger: 

153 logger.exception(f"Error during startup: {e}") 

154 raise 

155 yield 

156 logger = getattr(self, "logger", None) 

157 if logger: 

158 logger.info("FastBlocks application shutting down") 

159 

160 

161class App(AppBase): 

162 settings: AppSettings | None = None 

163 router: t.Any = None 

164 middleware_manager: t.Any = None 

165 templates: t.Any = None 

166 models: t.Any = None 

167 exception_handlers: t.Any = None 

168 middleware_stack: t.Any = None 

169 user_middleware: t.Any = None 

170 fastblocks_app: t.Any = None 

171 

172 def __init__(self, **kwargs: t.Any) -> None: 

173 super().__init__(**kwargs) 

174 self.settings = AppSettings() 

175 self.fastblocks_app = FastBlocksApp() 

176 self.router = None 

177 self.middleware_manager = None 

178 self.templates = None 

179 self.models = None 

180 self.exception_handlers = {} 

181 self.middleware_stack = None 

182 self.user_middleware = [] 

183 self.state = None 

184 

185 @property 

186 def logger(self) -> t.Any: 

187 if hasattr(super(), "logger"): 

188 with suppress(Exception): 

189 return super().logger 

190 try: 

191 return depends.get("logger") 

192 except Exception: 

193 import logging 

194 

195 return logging.getLogger(self.__class__.__name__) 

196 

197 @logger.setter 

198 def logger(self, value: t.Any) -> None: 

199 pass 

200 

201 @logger.deleter 

202 def logger(self) -> None: 

203 pass 

204 

205 async def init(self) -> None: 

206 import time 

207 

208 self._init_start_time = time.time() 

209 await self.fastblocks_app.init() 

210 try: 

211 self.templates = await depends.get("templates") 

212 except Exception: 

213 self.templates = None 

214 try: 

215 self.models = await depends.get("models") 

216 except Exception: 

217 self.models = None 

218 try: 

219 routes_adapter = await depends.get("routes") 

220 self.router = routes_adapter 

221 self.fastblocks_app.routes.extend(routes_adapter.routes) 

222 except Exception: 

223 self.router = None 

224 self.middleware_manager = None 

225 self.exception_handlers = self.fastblocks_app.exception_handlers 

226 self.middleware_stack = self.fastblocks_app.middleware_stack 

227 self.user_middleware = self.fastblocks_app.user_middleware 

228 self.state = self.fastblocks_app.state 

229 import time 

230 

231 self._startup_time = time.time() - self._init_start_time 

232 self.fastblocks_app._startup_time = self._startup_time 

233 self.fastblocks_app._init_start_time = self._init_start_time 

234 await self.post_startup() 

235 

236 def __call__(self, scope: Scope, receive: Receive, send: Send) -> ASGIApp: 

237 return t.cast(ASGIApp, self.fastblocks_app(scope, receive, send)) 

238 

239 def __getattr__(self, name: str) -> t.Any: 

240 return getattr(self.fastblocks_app, name) 

241 

242 async def post_startup(self) -> None: 

243 await self.fastblocks_app.post_startup() 

244 

245 async def _setup_admin_adapter(self, app: FastBlocks) -> None: 

246 if not get_adapter("admin"): 

247 return 

248 sql = await depends.get("sql") 

249 auth = await depends.get("auth") 

250 admin = await depends.get("admin") 

251 admin.__init__( 

252 app, 

253 engine=sql.engine, 

254 title=self.config.admin.title, 

255 debug=getattr(self.config.debug, "admin", False), 

256 base_url=self.config.admin.url, 

257 logo_url=self.config.admin.logo_url, 

258 authentication_backend=auth, 

259 ) 

260 self.router.routes.insert(0, self.router.routes.pop()) 

261 

262 async def _startup_sequence(self, app: FastBlocks) -> None: 

263 await self._setup_admin_adapter(app) 

264 await self.post_startup() 

265 main_start_time = perf_counter() - main_start 

266 self.logger.warning(f"App started in {main_start_time} s") 

267 

268 async def _shutdown_logger(self) -> None: 

269 import asyncio 

270 

271 completer = None 

272 if hasattr(self.logger, "complete"): 

273 completer = self.logger.complete() 

274 elif hasattr(self.logger, "stop"): 

275 completer = self.logger.stop() 

276 if completer: 

277 await asyncio.wait_for(completer, timeout=1.0) 

278 

279 def _cancel_remaining_tasks(self) -> None: 

280 import asyncio 

281 

282 loop = asyncio.get_event_loop() 

283 tasks = [t for t in asyncio.all_tasks(loop) if not t.done()] 

284 if tasks: 

285 self.logger.debug(f"Cancelling {len(tasks)} remaining tasks") 

286 for task in tasks: 

287 task.cancel() 

288 

289 @asynccontextmanager 

290 async def lifespan(self, app: FastBlocks) -> t.AsyncIterator[None]: 

291 try: 

292 await self._startup_sequence(app) 

293 except Exception as e: 

294 self.logger.exception(f"Error during startup: {e}") 

295 raise 

296 yield 

297 self.logger.critical("Application shut down") 

298 try: 

299 await self._shutdown_logger() 

300 except TimeoutError: 

301 self.logger.warning("Logger completion timed out, forcing shutdown") 

302 except Exception as e: 

303 self.logger.exception(f"Logger completion failed: {e}") 

304 finally: 

305 with suppress(Exception): 

306 self._cancel_remaining_tasks() 

307 

308 

309MODULE_ID = UUID("01937d86-8f6e-7f70-c231-5678901234ef") 

310MODULE_STATUS = AdapterStatus.STABLE 

311 

312depends.set(App)