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
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-21 04:50 -0700
1"""Default App Adapter for FastBlocks.
3Provides the main FastBlocks application instance with lifecycle management,
4startup/shutdown sequences, and adapter integration.
6Author: lesleslie <les@wedgwoodwebworks.com>
7Created: 2025-01-12
8"""
10import typing as t
11from base64 import b64encode
12from contextlib import asynccontextmanager, suppress
13from time import perf_counter
14from uuid import UUID
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
21from ._base import AppBase, AppBaseSettings
23main_start = perf_counter()
25try:
26 Cache, Storage = import_adapter()
27except Exception:
28 Cache = Storage = None
31class AppSettings(AppBaseSettings):
32 url: str = "http://localhost:8000"
33 token_id: str | None = "_fb_"
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 )
46class FastBlocksApp(FastBlocks):
47 def __init__(self, **kwargs: t.Any) -> None:
48 super().__init__(lifespan=self.lifespan, **kwargs)
50 async def init(self) -> None:
51 pass
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
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
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
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 }
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"
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 ]
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
110 async def _display_fancy_startup(self) -> None:
111 from acb.depends import depends
112 from aioconsole import aprint
113 from pyfiglet import Figlet
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)
133 def _display_simple_startup(self) -> None:
134 from contextlib import suppress
136 with suppress(Exception):
137 from acb.depends import depends
139 config = depends.get("config")
140 getattr(config.app, "name", "FastBlocks")
141 self._get_startup_time()
143 async def post_startup(self) -> None:
144 try:
145 await self._display_fancy_startup()
146 except Exception:
147 self._display_simple_startup()
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")
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
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
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
200 return logging.getLogger(self.__class__.__name__)
202 @logger.setter
203 def logger(self, value: t.Any) -> None:
204 pass
206 @logger.deleter
207 def logger(self) -> None:
208 pass
210 async def init(self) -> None:
211 import time
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
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()
241 def __call__(self, scope: Scope, receive: Receive, send: Send) -> ASGIApp:
242 return self.fastblocks_app(scope, receive, send)
244 def __getattr__(self, name: str):
245 return getattr(self.fastblocks_app, name)
247 async def post_startup(self) -> None:
248 await self.fastblocks_app.post_startup()
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())
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")
273 async def _shutdown_logger(self) -> None:
274 import asyncio
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)
284 def _cancel_remaining_tasks(self) -> None:
285 import asyncio
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()
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()
314MODULE_ID = UUID("01937d86-8f6e-7f70-c231-5678901234ef")
315MODULE_STATUS = AdapterStatus.STABLE