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

217 statements  

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

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, import_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() 

24 

25try: 

26 Cache, Storage = import_adapter() 

27except Exception: 

28 Cache = Storage = None 

29 

30 

31class AppSettings(AppBaseSettings): 

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

33 token_id: str | None = "_fb_" 

34 

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

36 super().__init__(**data) 

37 with suppress(Exception): 

38 config = depends.get("config") 

39 self.url = self.url if not config.deployed else f"https://{self.domain}" 

40 token_prefix = self.token_id or "_fb_" 

41 self.token_id = "".join( 

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

43 ) 

44 

45 

46class FastBlocksApp(FastBlocks): 

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

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

49 

50 async def init(self) -> None: 

51 pass 

52 

53 def _get_startup_time(self) -> float: 

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

55 if startup_time is None or startup_time <= 0: 

56 import time 

57 

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

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

60 return startup_time 

61 

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

63 debug_enabled = [] 

64 if hasattr(config, "debug"): 

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

66 if value and key != "production": 

67 debug_enabled.append(key) 

68 return debug_enabled 

69 

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

71 return { 

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

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

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

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

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

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

78 } 

79 

80 def _format_info_lines( 

81 self, 

82 config: t.Any, 

83 colors: dict[str, str], 

84 debug_enabled: list[str], 

85 startup_time: float, 

86 ) -> list[str]: 

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

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

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

90 

91 return [ 

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

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

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

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

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

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

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

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

100 ] 

101 

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

103 line_clean = line 

104 for color in colors.values(): 

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

106 line_width = len(line_clean) 

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

108 return " " * padding + line 

109 

110 async def _display_fancy_startup(self) -> None: 

111 from acb.depends import depends 

112 from aioconsole import aprint 

113 from pyfiglet import Figlet 

114 

115 config = depends.get("config") 

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

117 startup_time = self._get_startup_time() 

118 debug_enabled = self._get_debug_enabled(config) 

119 colors = self._get_color_constants() 

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

121 app_name.upper(), 

122 ) 

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

124 info_lines = self._format_info_lines( 

125 config, 

126 colors, 

127 debug_enabled, 

128 startup_time, 

129 ) 

130 for line in info_lines: 

131 self._clean_and_center_line(line, colors) 

132 

133 def _display_simple_startup(self) -> None: 

134 from contextlib import suppress 

135 

136 with suppress(Exception): 

137 from acb.depends import depends 

138 

139 config = depends.get("config") 

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

141 self._get_startup_time() 

142 

143 async def post_startup(self) -> None: 

144 try: 

145 await self._display_fancy_startup() 

146 except Exception: 

147 self._display_simple_startup() 

148 

149 @asynccontextmanager 

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

151 try: 

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

153 if logger: 

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

155 except Exception as e: 

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

157 if logger: 

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

159 raise 

160 yield 

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

162 if logger: 

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

164 

165 

166class App(AppBase): 

167 settings: AppSettings | None = None 

168 router: t.Any = None 

169 middleware_manager: t.Any = None 

170 templates: t.Any = None 

171 models: t.Any = None 

172 exception_handlers: t.Any = None 

173 middleware_stack: t.Any = None 

174 user_middleware: t.Any = None 

175 fastblocks_app: t.Any = None 

176 

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

178 super().__init__(**kwargs) 

179 self.settings = AppSettings() 

180 self.fastblocks_app = FastBlocksApp() 

181 self.router = None 

182 self.middleware_manager = None 

183 self.templates = None 

184 self.models = None 

185 self.exception_handlers = {} 

186 self.middleware_stack = None 

187 self.user_middleware = [] 

188 self.state = None 

189 

190 @property 

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

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

193 with suppress(Exception): 

194 return super().logger 

195 try: 

196 return depends.get("logger") 

197 except Exception: 

198 import logging 

199 

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

201 

202 @logger.setter 

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

204 pass 

205 

206 @logger.deleter 

207 def logger(self) -> None: 

208 pass 

209 

210 async def init(self) -> None: 

211 import time 

212 

213 self._init_start_time = time.time() 

214 await self.fastblocks_app.init() 

215 try: 

216 self.templates = depends.get("templates") 

217 except Exception: 

218 self.templates = None 

219 try: 

220 self.models = depends.get("models") 

221 except Exception: 

222 self.models = None 

223 try: 

224 routes_adapter = depends.get("routes") 

225 self.router = routes_adapter 

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

227 except Exception: 

228 self.router = None 

229 self.middleware_manager = None 

230 self.exception_handlers = self.fastblocks_app.exception_handlers 

231 self.middleware_stack = self.fastblocks_app.middleware_stack 

232 self.user_middleware = self.fastblocks_app.user_middleware 

233 self.state = self.fastblocks_app.state 

234 import time 

235 

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

237 self.fastblocks_app._startup_time = self._startup_time 

238 self.fastblocks_app._init_start_time = self._init_start_time 

239 await self.post_startup() 

240 

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

242 return self.fastblocks_app(scope, receive, send) 

243 

244 def __getattr__(self, name: str): 

245 return getattr(self.fastblocks_app, name) 

246 

247 async def post_startup(self) -> None: 

248 await self.fastblocks_app.post_startup() 

249 

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

251 if not get_adapter("admin"): 

252 return 

253 sql = depends.get("sql") 

254 auth = depends.get("auth") 

255 admin = depends.get("admin") 

256 admin.__init__( 

257 app, 

258 engine=sql.engine, 

259 title=self.config.admin.title, 

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

261 base_url=self.config.admin.url, 

262 logo_url=self.config.admin.logo_url, 

263 authentication_backend=auth, 

264 ) 

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

266 

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

268 self._setup_admin_adapter(app) 

269 await self.post_startup() 

270 main_start_time = perf_counter() - main_start 

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

272 

273 async def _shutdown_logger(self) -> None: 

274 import asyncio 

275 

276 completer = None 

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

278 completer = self.logger.complete() 

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

280 completer = self.logger.stop() 

281 if completer: 

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

283 

284 def _cancel_remaining_tasks(self) -> None: 

285 import asyncio 

286 

287 loop = asyncio.get_event_loop() 

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

289 if tasks: 

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

291 for task in tasks: 

292 task.cancel() 

293 

294 @asynccontextmanager 

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

296 try: 

297 await self._startup_sequence(app) 

298 except Exception as e: 

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

300 raise 

301 yield 

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

303 try: 

304 await self._shutdown_logger() 

305 except TimeoutError: 

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

307 except Exception as e: 

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

309 finally: 

310 with suppress(Exception): 

311 self._cancel_remaining_tasks() 

312 

313 

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

315MODULE_STATUS = AdapterStatus.STABLE