Coverage for src/lexigram/admin/di/mount/controllers.py: 0%

211 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 23:18 +0800

1"""Mount phase that resolves and wires every admin controller.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Any 

6 

7from lexigram.logging import get_logger 

8 

9if TYPE_CHECKING: 

10 from lexigram.admin.di.mount.context import MountContext 

11 

12_log = get_logger(__name__) 

13 

14 

15class AdminMountControllersMixin: 

16 """Resolves built-in admin controllers and wires their private services.""" 

17 

18 # Host attributes provided by AdminProvider. 

19 _config: Any 

20 _resources: list[Any] 

21 _controllers: list[Any] 

22 _mount_failures: dict[str, str] 

23 _authorizer_service: Any 

24 _get_csrf_service: Any 

25 

26 async def _resolve_audit_service(self, resolver: Any) -> Any | None: 

27 """Best-effort resolution of the optional audit-log service.""" 

28 

29 try: 

30 from lexigram.admin.auth.protocols import ( 

31 AdminAuditLogServiceProtocol, 

32 ) 

33 

34 return await resolver.resolve( 

35 AdminAuditLogServiceProtocol, 

36 bypass_visibility=True, 

37 ) 

38 except Exception: 

39 return None 

40 

41 async def _mount_controllers(self, resolver: Any, ctx: MountContext) -> None: 

42 """Resolve every built-in controller, best-effort per controller. 

43 

44 Failures are recorded in ``_mount_failures`` and only abort the mount 

45 when strict resource resolution is configured. Controllers receive 

46 runtime wiring (settings, audit, CSRF, user store) as available. 

47 

48 Args: 

49 resolver: The DI resolver for controller resolution. 

50 ctx: Mount pipeline state (``controllers`` populated in place). 

51 """ 

52 # Create shared settings_service for runtime theme overrides (best-effort) 

53 admin_settings_service = ctx.settings_service 

54 

55 for controller_cls in self._controllers: 

56 try: 

57 instance = await resolver.resolve( 

58 controller_cls, 

59 bypass_visibility=True, 

60 ) 

61 ctx.controllers.append(instance) 

62 if admin_settings_service is not None and hasattr( 

63 instance, "_settings_service" 

64 ): 

65 instance._settings_service = admin_settings_service 

66 except Exception as exc: 

67 _log.error( 

68 "admin.controller_resolution_failed", 

69 controller=controller_cls.__name__, 

70 error=str(exc), 

71 strict=self._config.strict_resource_resolution, 

72 ) 

73 self._mount_failures[f"controller:{controller_cls.__name__}"] = str(exc) 

74 if self._config.strict_resource_resolution: 

75 raise 

76 

77 # Resolve built-in WidgetController (best-effort) 

78 try: 

79 from lexigram.admin.controllers.widgets import WidgetController 

80 

81 widget_controller = await resolver.resolve( 

82 WidgetController, 

83 bypass_visibility=True, 

84 ) 

85 ctx.controllers.append(widget_controller) 

86 if admin_settings_service is not None and hasattr( 

87 widget_controller, "_settings_service" 

88 ): 

89 widget_controller._settings_service = admin_settings_service 

90 audit_service = await self._resolve_audit_service(resolver) 

91 if audit_service is not None and hasattr( 

92 widget_controller, "_audit_service" 

93 ): 

94 widget_controller._audit_service = audit_service 

95 csrf_service = await self._get_csrf_service(resolver) 

96 if hasattr(widget_controller, "_csrf_service"): 

97 widget_controller._csrf_service = csrf_service 

98 except Exception as exc: 

99 _log.error( 

100 "admin.widget_controller_resolution_failed", 

101 error=str(exc), 

102 strict=self._config.strict_resource_resolution, 

103 ) 

104 self._mount_failures["controller:WidgetController"] = str(exc) 

105 if self._config.strict_resource_resolution: 

106 raise 

107 

108 # Resolve built-in ImpersonationController (best-effort) 

109 try: 

110 from lexigram.admin.controllers.impersonation import ( 

111 ImpersonationController, 

112 ) 

113 from lexigram.admin.services.impersonation import ImpersonationService 

114 

115 impersonation_service = await resolver.resolve( 

116 ImpersonationService, 

117 bypass_visibility=True, 

118 ) 

119 if getattr(impersonation_service, "_audit", None) is None: 

120 audit_service = await self._resolve_audit_service(resolver) 

121 if audit_service is not None: 

122 impersonation_service._audit = audit_service 

123 

124 impersonation_controller = await resolver.resolve( 

125 ImpersonationController, 

126 bypass_visibility=True, 

127 ) 

128 ctx.controllers.append(impersonation_controller) 

129 except Exception as exc: 

130 _log.error( 

131 "admin.impersonation_controller_resolution_failed", 

132 error=str(exc), 

133 strict=self._config.strict_resource_resolution, 

134 ) 

135 self._mount_failures["controller:ImpersonationController"] = str(exc) 

136 if self._config.strict_resource_resolution: 

137 raise 

138 

139 # Resolve built-in TenancyController (best-effort) 

140 try: 

141 from lexigram.admin.controllers.tenancy import TenancyController 

142 from lexigram.admin.multitenancy.adapter import TenantProviderRegistry 

143 

144 tenancy_controller = await resolver.resolve( 

145 TenancyController, 

146 bypass_visibility=True, 

147 ) 

148 ctx.controllers.append(tenancy_controller) 

149 if self._config.tenancy.enabled: 

150 try: 

151 tenancy_controller._registry = await resolver.resolve( 

152 TenantProviderRegistry, 

153 bypass_visibility=True, 

154 ) 

155 except Exception: 

156 tenancy_controller._registry = None 

157 audit_service = await self._resolve_audit_service(resolver) 

158 if audit_service is not None: 

159 tenancy_controller._audit_service = audit_service 

160 except Exception as exc: 

161 _log.error( 

162 "admin.tenancy_controller_resolution_failed", 

163 error=str(exc), 

164 strict=self._config.strict_resource_resolution, 

165 ) 

166 self._mount_failures["controller:TenancyController"] = str(exc) 

167 if self._config.strict_resource_resolution: 

168 raise 

169 

170 # Resolve built-in DashboardController (best-effort) 

171 try: 

172 from lexigram.admin.controllers.dashboard import DashboardController 

173 

174 dashboard_controller = await resolver.resolve( 

175 DashboardController, 

176 bypass_visibility=True, 

177 ) 

178 ctx.controllers.append(dashboard_controller) 

179 if admin_settings_service is not None: 

180 dashboard_controller._settings_service = admin_settings_service 

181 except Exception as exc: 

182 _log.error( 

183 "admin.dashboard_controller_resolution_failed", 

184 error=str(exc), 

185 strict=self._config.strict_resource_resolution, 

186 ) 

187 self._mount_failures["controller:DashboardController"] = str(exc) 

188 if self._config.strict_resource_resolution: 

189 raise 

190 

191 # Resolve built-in AuthController (login, logout) 

192 try: 

193 from lexigram.admin.controllers.auth import AuthController 

194 

195 auth_controller = await resolver.resolve( 

196 AuthController, 

197 bypass_visibility=True, 

198 ) 

199 ctx.controllers.append(auth_controller) 

200 if admin_settings_service is not None: 

201 auth_controller._settings_service = admin_settings_service 

202 

203 # Wire self-service registration (opt-in via config). Best-effort: 

204 # without a resolvable user store, registration stays disabled. 

205 try: 

206 from lexigram.admin.auth.store.protocols import ( 

207 AdminUserStoreProtocol, 

208 ) 

209 

210 auth_controller._user_store = await resolver.resolve( 

211 AdminUserStoreProtocol, 

212 bypass_visibility=True, 

213 ) 

214 except Exception: 

215 auth_controller._user_store = None 

216 registration = getattr(self._config.auth, "registration", None) 

217 auth_controller._registration_enabled = bool( 

218 registration and registration.enabled 

219 ) 

220 auth_controller._registration_default_role = ( 

221 str(registration.default_role) if registration else "admin" 

222 ) 

223 auth_controller._registration_domains = ( 

224 list(registration.allowed_email_domains) if registration else [] 

225 ) 

226 except Exception as exc: 

227 _log.error( 

228 "admin.auth_controller_resolution_failed", 

229 error=str(exc), 

230 strict=self._config.strict_resource_resolution, 

231 ) 

232 self._mount_failures["controller:AuthController"] = str(exc) 

233 if self._config.strict_resource_resolution: 

234 raise 

235 

236 # Resolve built-in ProfileController (profile page, password change) 

237 try: 

238 from lexigram.admin.controllers.profile import ProfileController 

239 

240 profile_controller = await resolver.resolve( 

241 ProfileController, 

242 bypass_visibility=True, 

243 ) 

244 ctx.controllers.append(profile_controller) 

245 if admin_settings_service is not None: 

246 profile_controller._settings_service = admin_settings_service 

247 try: 

248 from lexigram.admin.auth.store.protocols import ( 

249 AdminUserStoreProtocol, 

250 ) 

251 

252 profile_controller._user_store = await resolver.resolve( 

253 AdminUserStoreProtocol, 

254 bypass_visibility=True, 

255 ) 

256 except Exception: 

257 profile_controller._user_store = None 

258 except Exception as exc: 

259 _log.error( 

260 "admin.profile_controller_resolution_failed", 

261 error=str(exc), 

262 strict=self._config.strict_resource_resolution, 

263 ) 

264 self._mount_failures["controller:ProfileController"] = str(exc) 

265 if self._config.strict_resource_resolution: 

266 raise 

267 

268 # Resolve built-in SetupController (first-run wizard) 

269 try: 

270 from lexigram.admin.controllers.setup import SetupController 

271 

272 setup_controller = await resolver.resolve( 

273 SetupController, 

274 bypass_visibility=True, 

275 ) 

276 ctx.controllers.append(setup_controller) 

277 if admin_settings_service is not None: 

278 setup_controller._settings_service = admin_settings_service 

279 except Exception as exc: 

280 _log.error( 

281 "admin.setup_controller_resolution_failed", 

282 error=str(exc), 

283 strict=self._config.strict_resource_resolution, 

284 ) 

285 self._mount_failures["controller:SetupController"] = str(exc) 

286 if self._config.strict_resource_resolution: 

287 raise 

288 

289 # Mount ErrorController (styled error pages) — best-effort 

290 try: 

291 from lexigram.admin.controllers.error import ErrorController 

292 

293 error_controller = await resolver.resolve( 

294 ErrorController, 

295 bypass_visibility=True, 

296 ) 

297 ctx.controllers.append(error_controller) 

298 except Exception as exc: 

299 _log.error( 

300 "admin.error_controller_resolution_failed", 

301 error=str(exc), 

302 strict=self._config.strict_resource_resolution, 

303 ) 

304 self._mount_failures["controller:ErrorController"] = str(exc) 

305 if self._config.strict_resource_resolution: 

306 raise 

307 

308 # Mount PoolHealthController (connection pool monitoring) — best-effort. 

309 # pool_manager/task_manager are optional: without them the endpoints 

310 # respond 503 instead of failing resolution. 

311 try: 

312 from lexigram.admin.controllers.pool_health import PoolHealthController 

313 

314 pool_health_controller = await resolver.resolve( 

315 PoolHealthController, 

316 bypass_visibility=True, 

317 ) 

318 ctx.controllers.append(pool_health_controller) 

319 except Exception as exc: 

320 _log.error( 

321 "admin.pool_health_controller_resolution_failed", 

322 error=str(exc), 

323 strict=self._config.strict_resource_resolution, 

324 ) 

325 self._mount_failures["controller:PoolHealthController"] = str(exc) 

326 if self._config.strict_resource_resolution: 

327 raise 

328 

329 # Mount ProgressController (SSE/status progress tracking) — best-effort. 

330 # Tries an integrator-registered tracker first; falls back to the 

331 # admin-owned LocalProgressTracker (no dependency on optional 

332 # integration packages — without one the controller mounts with the 

333 # in-process tracker instead of being skipped). 

334 try: 

335 from lexigram.admin.controllers.progress import ( 

336 LocalProgressTracker, 

337 ProgressController, 

338 ) 

339 

340 try: 

341 progress_controller = await resolver.resolve( 

342 ProgressController, 

343 bypass_visibility=True, 

344 ) 

345 except Exception: 

346 progress_controller = ProgressController(tracker=LocalProgressTracker()) 

347 ctx.controllers.append(progress_controller) 

348 except ModuleNotFoundError as exc: 

349 _log.info( 

350 "admin.progress_controller_skipped", 

351 reason="progress_controller_unavailable", 

352 error=str(exc), 

353 ) 

354 except Exception as exc: 

355 _log.error( 

356 "admin.progress_controller_resolution_failed", 

357 error=str(exc), 

358 strict=self._config.strict_resource_resolution, 

359 ) 

360 self._mount_failures["controller:ProgressController"] = str(exc) 

361 if self._config.strict_resource_resolution: 

362 raise 

363 

364 # Mount SettingsController (theme & branding settings) 

365 try: 

366 from lexigram.admin.controllers.settings import SettingsController 

367 from lexigram.admin.engine.renderer import AdminRenderer 

368 from lexigram.admin.settings.panel.registry import ConfigRegistry 

369 

370 settings_csrf = await self._get_csrf_service(resolver) 

371 

372 settings_registry: ConfigRegistry | None = None 

373 try: 

374 settings_registry = await resolver.resolve( 

375 ConfigRegistry, 

376 bypass_visibility=True, 

377 ) 

378 except Exception as exc: # noqa: BLE001 — settings registry is optional 

379 _log.warning("admin.config_registry_unavailable", reason=str(exc)) 

380 

381 settings_audit = await self._resolve_audit_service(resolver) 

382 

383 renderer = await resolver.resolve( 

384 AdminRenderer, 

385 bypass_visibility=True, 

386 ) 

387 settings_controller = SettingsController( 

388 renderer=renderer, 

389 settings_service=admin_settings_service, 

390 csrf_service=settings_csrf, 

391 audit_service=settings_audit, 

392 registry=settings_registry, 

393 rbac_config=self._config.rbac, 

394 ) 

395 ctx.controllers.append(settings_controller) 

396 except Exception as exc: 

397 _log.warning( 

398 "admin.settings_controller_skipped", 

399 error=str(exc), 

400 ) 

401 

402 # Mount InfrastructureController (cluster landing page) 

403 try: 

404 from lexigram.admin.clusters import Cluster, ClusterRegistry 

405 from lexigram.admin.controllers.clusters import ClusterCenterController 

406 from lexigram.admin.controllers.infrastructure import ( 

407 InfrastructureController, 

408 ) 

409 from lexigram.admin.engine.renderer import AdminRenderer 

410 

411 infra_renderer = await resolver.resolve( 

412 AdminRenderer, 

413 bypass_visibility=True, 

414 ) 

415 ctx.controllers.append(InfrastructureController(renderer=infra_renderer)) 

416 

417 # Build the cluster registry (built-in + config-declared extras) 

418 # and mount a generic center controller per extra cluster. 

419 cluster_registry = ClusterRegistry.with_defaults() 

420 extra_specs = getattr(self._config, "clusters", None) 

421 for spec in (extra_specs.extra if extra_specs else []) or []: 

422 cluster = Cluster( 

423 name=spec.name, 

424 label=spec.label, 

425 icon=spec.icon, 

426 order=spec.order, 

427 collapsible=spec.collapsible, 

428 collapsed_by_default=spec.collapsed_by_default, 

429 slug=spec.slug, 

430 group=spec.group, 

431 description=spec.description, 

432 ) 

433 cluster_registry.register(cluster) 

434 ctx.controllers.append( 

435 ClusterCenterController( 

436 renderer=infra_renderer, 

437 cluster=cluster, 

438 ) 

439 ) 

440 ctx.cluster_registry = cluster_registry 

441 except Exception as exc: 

442 _log.warning( 

443 "admin.infrastructure_controller_skipped", 

444 error=str(exc), 

445 ) 

446 

447 # Mount PluginsController (plugin listing & toggles) — best-effort. 

448 try: 

449 from lexigram.admin.controllers.plugins import PluginsController 

450 from lexigram.admin.engine.renderer import AdminRenderer 

451 

452 plugins_csrf_service = await self._get_csrf_service(resolver) 

453 

454 plugins_audit_service = await self._resolve_audit_service(resolver) 

455 

456 plugins_renderer = await resolver.resolve( 

457 AdminRenderer, 

458 bypass_visibility=True, 

459 ) 

460 ctx.controllers.append( 

461 PluginsController( 

462 renderer=plugins_renderer, 

463 csrf_service=plugins_csrf_service, 

464 audit_service=plugins_audit_service, 

465 rbac_config=self._config.rbac, 

466 ) 

467 ) 

468 except Exception as exc: 

469 _log.warning( 

470 "admin.plugins_controller_skipped", 

471 error=str(exc), 

472 )