Coverage for fastblocks/adapters/templates/integration.py: 29%

147 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-09 00:47 -0700

1"""Advanced Template Management Integration Module. 

2 

3This module integrates all advanced template management components: 

4- Advanced Template Manager with validation and autocomplete 

5- Async Template Renderer with performance optimization 

6- Block Renderer for HTMX fragments and partials 

7- Enhanced filters for secondary adapters 

8- Template CLI tools and utilities 

9 

10This provides a unified interface for FastBlocks Week 7-8 template features. 

11 

12Requirements: 

13- jinja2>=3.1.6 

14- jinja2-async-environment>=0.14.3 

15- starlette-async-jinja>=1.12.4 

16 

17Author: lesleslie <les@wedgwoodwebworks.com> 

18Created: 2025-01-12 

19""" 

20 

21import typing as t 

22from contextlib import suppress 

23from uuid import UUID 

24 

25from acb.adapters import AdapterStatus 

26from acb.depends import depends 

27from starlette.requests import Request 

28from starlette.responses import Response 

29 

30from ._advanced_manager import AdvancedTemplateManager, AdvancedTemplatesSettings 

31from ._async_filters import FASTBLOCKS_ASYNC_FILTERS 

32from ._async_renderer import AsyncTemplateRenderer, RenderContext, RenderMode 

33from ._block_renderer import BlockRenderer, BlockRenderRequest, BlockUpdateMode 

34from ._enhanced_filters import ENHANCED_ASYNC_FILTERS, ENHANCED_FILTERS 

35from ._filters import FASTBLOCKS_FILTERS 

36from .jinja2 import Templates 

37 

38 

39class AdvancedTemplatesIntegration: 

40 """Unified interface for advanced template management features.""" 

41 

42 def __init__(self) -> None: 

43 self.settings = AdvancedTemplatesSettings() 

44 self.base_templates: Templates | None = None 

45 self.advanced_manager: AdvancedTemplateManager | None = None 

46 self.async_renderer: AsyncTemplateRenderer | None = None 

47 self.block_renderer: BlockRenderer | None = None 

48 self._initialized = False 

49 

50 async def initialize(self) -> None: 

51 """Initialize all components.""" 

52 if self._initialized: 

53 return 

54 

55 # Get or create base templates 

56 try: 

57 self.base_templates = depends.get("templates") 

58 except Exception: 

59 self.base_templates = Templates() 

60 await self.base_templates.init() 

61 

62 # Initialize advanced manager 

63 self.advanced_manager = AdvancedTemplateManager(self.settings) 

64 await self.advanced_manager.initialize() 

65 

66 # Initialize async renderer 

67 self.async_renderer = AsyncTemplateRenderer( 

68 base_templates=self.base_templates, advanced_manager=self.advanced_manager 

69 ) 

70 await self.async_renderer.initialize() 

71 

72 # Initialize block renderer 

73 self.block_renderer = BlockRenderer( 

74 async_renderer=self.async_renderer, advanced_manager=self.advanced_manager 

75 ) 

76 await self.block_renderer.initialize() 

77 

78 # Register all filters 

79 await self._register_filters() 

80 

81 self._initialized = True 

82 

83 async def _register_filters(self) -> None: 

84 """Register all template filters with Jinja2 environments.""" 

85 if not self.base_templates: 

86 return 

87 

88 all_filters = FASTBLOCKS_FILTERS | ENHANCED_FILTERS 

89 

90 all_async_filters = FASTBLOCKS_ASYNC_FILTERS | ENHANCED_ASYNC_FILTERS 

91 

92 # Register with main app environment 

93 if self.base_templates.app and hasattr(self.base_templates.app.env, "filters"): 

94 for name, filter_func in all_filters.items(): 

95 self.base_templates.app.env.filters[name] = filter_func 

96 

97 for name, async_filter_func in all_async_filters.items(): 

98 self.base_templates.app.env.filters[name] = async_filter_func 

99 

100 # Register with admin environment if available 

101 if self.base_templates.admin and hasattr( 

102 self.base_templates.admin.env, "filters" 

103 ): 

104 for name, filter_func in all_filters.items(): 

105 self.base_templates.admin.env.filters[name] = filter_func 

106 

107 for name, async_filter_func in all_async_filters.items(): 

108 self.base_templates.admin.env.filters[name] = async_filter_func 

109 

110 # Template Validation API 

111 async def validate_template( 

112 self, 

113 template_source: str, 

114 template_name: str = "unknown", 

115 context: dict[str, t.Any] | None = None, 

116 ) -> dict[str, t.Any]: 

117 """Validate template and return results.""" 

118 if not self.advanced_manager: 

119 await self.initialize() 

120 

121 result = await self.advanced_manager.validate_template( # type: ignore[union-attr] 

122 template_source, template_name, context 

123 ) 

124 

125 return { 

126 "is_valid": result.is_valid, 

127 "errors": [ 

128 { 

129 "message": error.message, 

130 "line_number": error.line_number, 

131 "column_number": error.column_number, 

132 "error_type": error.error_type, 

133 "severity": error.severity, 

134 "context": error.context, 

135 } 

136 for error in result.errors 

137 ], 

138 "warnings": [ 

139 { 

140 "message": warning.message, 

141 "line_number": warning.line_number, 

142 "severity": warning.severity, 

143 } 

144 for warning in result.warnings 

145 ], 

146 "suggestions": result.suggestions, 

147 "used_variables": list(result.used_variables), 

148 "undefined_variables": list(result.undefined_variables), 

149 "available_filters": list(result.available_filters), 

150 "available_functions": list(result.available_functions), 

151 } 

152 

153 # Autocomplete API 

154 async def get_autocomplete_suggestions( 

155 self, context: str, cursor_position: int = 0, template_name: str = "unknown" 

156 ) -> list[dict[str, t.Any]]: 

157 """Get autocomplete suggestions for template editing.""" 

158 if not self.advanced_manager: 

159 await self.initialize() 

160 

161 suggestions = await self.advanced_manager.get_autocomplete_suggestions( # type: ignore[union-attr] 

162 context, cursor_position, template_name 

163 ) 

164 

165 return [ 

166 { 

167 "name": item.name, 

168 "type": item.type, 

169 "description": item.description, 

170 "signature": item.signature, 

171 "adapter_source": item.adapter_source, 

172 "example": item.example, 

173 } 

174 for item in suggestions 

175 ] 

176 

177 # Fragment Management API 

178 async def get_fragments_for_template( 

179 self, template_name: str 

180 ) -> list[dict[str, t.Any]]: 

181 """Get available fragments for a template.""" 

182 if not self.advanced_manager: 

183 await self.initialize() 

184 

185 fragments = await self.advanced_manager.get_fragments_for_template( # type: ignore[union-attr] 

186 template_name 

187 ) 

188 

189 return [ 

190 { 

191 "name": fragment.name, 

192 "template_path": fragment.template_path, 

193 "block_name": fragment.block_name, 

194 "start_line": fragment.start_line, 

195 "end_line": fragment.end_line, 

196 "variables": list(fragment.variables), 

197 "dependencies": list(fragment.dependencies), 

198 } 

199 for fragment in fragments 

200 ] 

201 

202 async def render_fragment( 

203 self, 

204 fragment_name: str, 

205 context: dict[str, t.Any] | None = None, 

206 template_name: str | None = None, 

207 secure: bool = False, 

208 ) -> str: 

209 """Render a template fragment.""" 

210 if not self.advanced_manager: 

211 await self.initialize() 

212 

213 return await self.advanced_manager.render_fragment( # type: ignore[union-attr] 

214 fragment_name, context, template_name, secure 

215 ) 

216 

217 # Enhanced Rendering API 

218 async def render_template( 

219 self, 

220 request: Request, 

221 template_name: str, 

222 context: dict[str, t.Any] | None = None, 

223 mode: str = "standard", 

224 fragment_name: str | None = None, 

225 block_name: str | None = None, 

226 validate: bool = False, 

227 secure: bool = False, 

228 **kwargs: t.Any, 

229 ) -> Response: 

230 """Render template with advanced features.""" 

231 if not self.async_renderer: 

232 await self.initialize() 

233 

234 # Map mode string to enum 

235 mode_mapping = { 

236 "standard": RenderMode.STANDARD, 

237 "fragment": RenderMode.FRAGMENT, 

238 "block": RenderMode.BLOCK, 

239 "streaming": RenderMode.STREAMING, 

240 "htmx": RenderMode.HTMX, 

241 } 

242 

243 RenderContext( 

244 template_name=template_name, 

245 context=context or {}, 

246 request=request, 

247 mode=mode_mapping.get(mode, RenderMode.STANDARD), 

248 fragment_name=fragment_name, 

249 block_name=block_name, 

250 validate_template=validate, 

251 secure_render=secure, 

252 **kwargs, 

253 ) 

254 

255 return await self.async_renderer.render_response( # type: ignore[union-attr] 

256 request, template_name, context, **kwargs 

257 ) 

258 

259 # Block Rendering API 

260 async def render_block( 

261 self, 

262 request: Request, 

263 block_id: str, 

264 context: dict[str, t.Any] | None = None, 

265 update_mode: str = "replace", 

266 target_selector: str | None = None, 

267 validate: bool = False, 

268 ) -> Response: 

269 """Render a specific template block.""" 

270 if not self.block_renderer: 

271 await self.initialize() 

272 

273 # Map update mode string to enum 

274 update_mode_mapping = { 

275 "replace": BlockUpdateMode.REPLACE, 

276 "append": BlockUpdateMode.APPEND, 

277 "prepend": BlockUpdateMode.PREPEND, 

278 "inner": BlockUpdateMode.INNER, 

279 "outer": BlockUpdateMode.OUTER, 

280 "delete": BlockUpdateMode.DELETE, 

281 } 

282 

283 block_request = BlockRenderRequest( 

284 block_id=block_id, 

285 context=context or {}, 

286 request=request, 

287 target_selector=target_selector, 

288 update_mode=update_mode_mapping.get(update_mode, BlockUpdateMode.REPLACE), 

289 validate=validate, 

290 ) 

291 

292 result = await self.block_renderer.render_block(block_request) # type: ignore[union-attr] 

293 

294 from starlette.responses import HTMLResponse 

295 

296 return HTMLResponse(content=result.content, headers=result.htmx_headers) 

297 

298 async def render_htmx_fragment( 

299 self, 

300 request: Request, 

301 fragment_name: str, 

302 context: dict[str, t.Any] | None = None, 

303 template_name: str | None = None, 

304 **kwargs: t.Any, 

305 ) -> Response: 

306 """Render HTMX fragment with appropriate headers.""" 

307 if not self.async_renderer: 

308 await self.initialize() 

309 

310 return await self.async_renderer.render_htmx_fragment( # type: ignore[union-attr] 

311 request, fragment_name, context, template_name, **kwargs 

312 ) 

313 

314 # Block Management API 

315 def register_htmx_block( 

316 self, 

317 name: str, 

318 template_name: str, 

319 block_name: str | None = None, 

320 htmx_endpoint: str | None = None, 

321 update_mode: str = "replace", 

322 trigger: str = "manual", 

323 auto_refresh: int | None = None, 

324 **kwargs: t.Any, 

325 ) -> dict[str, t.Any]: 

326 """Register a block optimized for HTMX interactions.""" 

327 if not self.block_renderer: 

328 raise RuntimeError("Block renderer not initialized") 

329 

330 from .block_renderer import BlockTrigger, BlockUpdateMode 

331 

332 # Map string values to enums 

333 update_mode_mapping = { 

334 "replace": BlockUpdateMode.REPLACE, 

335 "append": BlockUpdateMode.APPEND, 

336 "prepend": BlockUpdateMode.PREPEND, 

337 "inner": BlockUpdateMode.INNER, 

338 "outer": BlockUpdateMode.OUTER, 

339 "delete": BlockUpdateMode.DELETE, 

340 } 

341 

342 trigger_mapping = { 

343 "manual": BlockTrigger.MANUAL, 

344 "auto": BlockTrigger.AUTO, 

345 "lazy": BlockTrigger.LAZY, 

346 "polling": BlockTrigger.POLLING, 

347 "websocket": BlockTrigger.WEBSOCKET, 

348 } 

349 

350 block_def = self.block_renderer.register_htmx_block( 

351 name=name, 

352 template_name=template_name, 

353 block_name=block_name, 

354 htmx_endpoint=htmx_endpoint, 

355 update_mode=update_mode_mapping.get(update_mode, BlockUpdateMode.REPLACE), 

356 trigger=trigger_mapping.get(trigger, BlockTrigger.MANUAL), 

357 auto_refresh=auto_refresh, 

358 **kwargs, 

359 ) 

360 

361 return { 

362 "name": block_def.name, 

363 "template_name": block_def.template_name, 

364 "block_name": block_def.block_name, 

365 "css_selector": block_def.css_selector, 

366 "htmx_attrs": block_def.htmx_attrs, 

367 "update_mode": block_def.update_mode.value, 

368 "trigger": block_def.trigger.value, 

369 } 

370 

371 async def get_block_info(self, block_id: str) -> dict[str, t.Any]: 

372 """Get information about a registered block.""" 

373 if not self.block_renderer: 

374 await self.initialize() 

375 

376 return await self.block_renderer.get_block_info(block_id) # type: ignore[union-attr] 

377 

378 def get_htmx_attributes_for_block(self, block_id: str) -> str: 

379 """Get HTMX attributes string for a block.""" 

380 if not self.block_renderer: 

381 return "" 

382 

383 return self.block_renderer.get_htmx_attributes_for_block(block_id) 

384 

385 # Performance and Monitoring API 

386 async def get_performance_metrics( 

387 self, template_name: str | None = None 

388 ) -> dict[str, t.Any]: 

389 """Get template rendering performance metrics.""" 

390 if not self.async_renderer: 

391 await self.initialize() 

392 

393 return await self.async_renderer.get_performance_metrics(template_name) # type: ignore[union-attr] 

394 

395 def clear_caches(self) -> None: 

396 """Clear all template caches.""" 

397 if self.advanced_manager: 

398 self.advanced_manager.clear_caches() 

399 

400 if self.async_renderer: 

401 self.async_renderer.clear_cache() 

402 

403 # Utility API 

404 async def precompile_templates(self) -> dict[str, t.Any]: 

405 """Precompile templates for performance optimization.""" 

406 if not self.advanced_manager: 

407 await self.initialize() 

408 

409 compiled = await self.advanced_manager.precompile_templates() # type: ignore[union-attr] 

410 return {name: True for name in compiled.keys()} 

411 

412 async def get_template_dependencies(self, template_name: str) -> list[str]: 

413 """Get dependencies for a template.""" 

414 if not self.advanced_manager: 

415 await self.initialize() 

416 

417 deps = await self.advanced_manager.get_template_dependencies(template_name) # type: ignore[union-attr] 

418 return list(deps) 

419 

420 

421# Global integration instance 

422_integration_instance: AdvancedTemplatesIntegration | None = None 

423 

424 

425async def get_advanced_templates() -> AdvancedTemplatesIntegration: 

426 """Get or create the global advanced templates integration instance.""" 

427 global _integration_instance 

428 

429 if _integration_instance is None: 

430 _integration_instance = AdvancedTemplatesIntegration() 

431 await _integration_instance.initialize() 

432 

433 return _integration_instance 

434 

435 

436# Convenience functions for common operations 

437async def validate_template_source( 

438 template_source: str, 

439 template_name: str = "unknown", 

440 context: dict[str, t.Any] | None = None, 

441) -> dict[str, t.Any]: 

442 """Validate template source code.""" 

443 integration = await get_advanced_templates() 

444 return await integration.validate_template(template_source, template_name, context) 

445 

446 

447async def get_template_autocomplete( 

448 context: str, cursor_position: int = 0, template_name: str = "unknown" 

449) -> list[dict[str, t.Any]]: 

450 """Get autocomplete suggestions for template editing.""" 

451 integration = await get_advanced_templates() 

452 return await integration.get_autocomplete_suggestions( 

453 context, cursor_position, template_name 

454 ) 

455 

456 

457async def render_htmx_block( 

458 request: Request, 

459 block_id: str, 

460 context: dict[str, t.Any] | None = None, 

461 update_mode: str = "replace", 

462) -> Response: 

463 """Render HTMX block with appropriate headers.""" 

464 integration = await get_advanced_templates() 

465 return await integration.render_block(request, block_id, context, update_mode) 

466 

467 

468async def render_template_fragment( 

469 request: Request, 

470 fragment_name: str, 

471 context: dict[str, t.Any] | None = None, 

472 template_name: str | None = None, 

473) -> Response: 

474 """Render template fragment for HTMX.""" 

475 integration = await get_advanced_templates() 

476 return await integration.render_htmx_fragment( 

477 request, fragment_name, context, template_name 

478 ) 

479 

480 

481MODULE_ID = UUID("01937d8b-1234-7890-abcd-1234567890ab") 

482MODULE_STATUS = AdapterStatus.STABLE 

483 

484# Register the integration 

485with suppress(Exception): 

486 depends.set("advanced_templates", get_advanced_templates)