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
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""Advanced Template Management Integration Module.
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
10This provides a unified interface for FastBlocks Week 7-8 template features.
12Requirements:
13- jinja2>=3.1.6
14- jinja2-async-environment>=0.14.3
15- starlette-async-jinja>=1.12.4
17Author: lesleslie <les@wedgwoodwebworks.com>
18Created: 2025-01-12
19"""
21import typing as t
22from contextlib import suppress
23from uuid import UUID
25from acb.adapters import AdapterStatus
26from acb.depends import depends
27from starlette.requests import Request
28from starlette.responses import Response
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
39class AdvancedTemplatesIntegration:
40 """Unified interface for advanced template management features."""
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
50 async def initialize(self) -> None:
51 """Initialize all components."""
52 if self._initialized:
53 return
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()
62 # Initialize advanced manager
63 self.advanced_manager = AdvancedTemplateManager(self.settings)
64 await self.advanced_manager.initialize()
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()
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()
78 # Register all filters
79 await self._register_filters()
81 self._initialized = True
83 async def _register_filters(self) -> None:
84 """Register all template filters with Jinja2 environments."""
85 if not self.base_templates:
86 return
88 all_filters = FASTBLOCKS_FILTERS | ENHANCED_FILTERS
90 all_async_filters = FASTBLOCKS_ASYNC_FILTERS | ENHANCED_ASYNC_FILTERS
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
97 for name, async_filter_func in all_async_filters.items():
98 self.base_templates.app.env.filters[name] = async_filter_func
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
107 for name, async_filter_func in all_async_filters.items():
108 self.base_templates.admin.env.filters[name] = async_filter_func
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()
121 result = await self.advanced_manager.validate_template( # type: ignore[union-attr]
122 template_source, template_name, context
123 )
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 }
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()
161 suggestions = await self.advanced_manager.get_autocomplete_suggestions( # type: ignore[union-attr]
162 context, cursor_position, template_name
163 )
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 ]
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()
185 fragments = await self.advanced_manager.get_fragments_for_template( # type: ignore[union-attr]
186 template_name
187 )
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 ]
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()
213 return await self.advanced_manager.render_fragment( # type: ignore[union-attr]
214 fragment_name, context, template_name, secure
215 )
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()
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 }
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 )
255 return await self.async_renderer.render_response( # type: ignore[union-attr]
256 request, template_name, context, **kwargs
257 )
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()
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 }
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 )
292 result = await self.block_renderer.render_block(block_request) # type: ignore[union-attr]
294 from starlette.responses import HTMLResponse
296 return HTMLResponse(content=result.content, headers=result.htmx_headers)
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()
310 return await self.async_renderer.render_htmx_fragment( # type: ignore[union-attr]
311 request, fragment_name, context, template_name, **kwargs
312 )
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")
330 from .block_renderer import BlockTrigger, BlockUpdateMode
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 }
342 trigger_mapping = {
343 "manual": BlockTrigger.MANUAL,
344 "auto": BlockTrigger.AUTO,
345 "lazy": BlockTrigger.LAZY,
346 "polling": BlockTrigger.POLLING,
347 "websocket": BlockTrigger.WEBSOCKET,
348 }
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 )
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 }
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()
376 return await self.block_renderer.get_block_info(block_id) # type: ignore[union-attr]
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 ""
383 return self.block_renderer.get_htmx_attributes_for_block(block_id)
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()
393 return await self.async_renderer.get_performance_metrics(template_name) # type: ignore[union-attr]
395 def clear_caches(self) -> None:
396 """Clear all template caches."""
397 if self.advanced_manager:
398 self.advanced_manager.clear_caches()
400 if self.async_renderer:
401 self.async_renderer.clear_cache()
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()
409 compiled = await self.advanced_manager.precompile_templates() # type: ignore[union-attr]
410 return {name: True for name in compiled.keys()}
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()
417 deps = await self.advanced_manager.get_template_dependencies(template_name) # type: ignore[union-attr]
418 return list(deps)
421# Global integration instance
422_integration_instance: AdvancedTemplatesIntegration | None = None
425async def get_advanced_templates() -> AdvancedTemplatesIntegration:
426 """Get or create the global advanced templates integration instance."""
427 global _integration_instance
429 if _integration_instance is None:
430 _integration_instance = AdvancedTemplatesIntegration()
431 await _integration_instance.initialize()
433 return _integration_instance
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)
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 )
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)
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 )
481MODULE_ID = UUID("01937d8b-1234-7890-abcd-1234567890ab")
482MODULE_STATUS = AdapterStatus.STABLE
484# Register the integration
485with suppress(Exception):
486 depends.set("advanced_templates", get_advanced_templates)