Coverage for fastblocks/adapters/templates/_events_wrapper.py: 48%
88 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"""Event tracking wrapper for template rendering operations.
3This module provides decorators and wrappers to integrate ACB Events
4with template rendering, tracking performance metrics automatically.
6Author: lesleslie <les@wedgwoodwebworks.com>
7Created: 2025-10-01
8"""
10import functools
11import sys
12import time
13import typing as t
14from contextlib import suppress
16from acb.depends import depends
19def track_template_render(
20 func: t.Callable[..., t.Awaitable[t.Any]],
21) -> t.Callable[..., t.Awaitable[t.Any]]:
22 """Decorator to track template rendering performance.
24 Publishes template render events with performance metrics.
25 Gracefully degrades if events integration unavailable.
27 Usage:
28 @track_template_render
29 async def render_template(self, template_name, context):
30 ...
31 """
33 @functools.wraps(func)
34 async def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
35 # Extract arguments
36 template_name = _extract_template_name(args, kwargs)
37 context = _extract_context(args, kwargs)
39 # Track render time
40 start_time = time.perf_counter()
42 try:
43 # Call the original function
44 result = await func(*args, **kwargs)
46 # Calculate and publish success metrics
47 render_time_ms = (time.perf_counter() - start_time) * 1000
48 await _publish_render_event(
49 template_name=template_name,
50 render_time_ms=render_time_ms,
51 context=context,
52 error=None,
53 )
55 return result
57 except Exception as e:
58 # Calculate and publish error metrics
59 render_time_ms = (time.perf_counter() - start_time) * 1000
60 await _publish_render_event(
61 template_name=template_name,
62 render_time_ms=render_time_ms,
63 context=context,
64 error=str(e),
65 )
66 raise
68 return wrapper
71def _extract_template_name(
72 args: tuple[t.Any, ...], kwargs: dict[str, t.Any]
73) -> str | None:
74 """Extract template name from function arguments."""
75 # Check kwargs first (more explicit)
76 for key in ("template", "name"):
77 if key in kwargs:
78 template_name: str | None = kwargs[key]
79 return template_name
81 # Check positional args
82 if len(args) > 2 and isinstance(args[2], str):
83 return args[2]
85 return None
88def _extract_context(
89 args: tuple[t.Any, ...], kwargs: dict[str, t.Any]
90) -> dict[str, t.Any] | None:
91 """Extract context dictionary from function arguments."""
92 # Check kwargs first
93 if "context" in kwargs and isinstance(kwargs["context"], dict):
94 return kwargs["context"]
96 # Check positional args
97 if len(args) > 3 and isinstance(args[3], dict):
98 return args[3]
100 return None
103async def _publish_render_event(
104 template_name: str | None,
105 render_time_ms: float,
106 context: dict[str, t.Any] | None,
107 error: str | None,
108) -> None:
109 """Publish template render event with metrics."""
110 if not template_name:
111 return
113 context_size = sys.getsizeof(context) if context else 0
114 cache_hit = render_time_ms < 5.0 if not error else False
116 with suppress(Exception):
117 from ..._events_integration import get_event_publisher
119 publisher = get_event_publisher()
120 if publisher:
121 await publisher.publish_template_render(
122 template_name=template_name,
123 render_time_ms=render_time_ms,
124 cache_hit=cache_hit,
125 context_size=context_size,
126 fragment_count=0, # TODO: Extract from result
127 error=error,
128 )
131async def publish_cache_invalidation(
132 cache_key: str,
133 reason: str = "manual",
134 invalidated_by: str | None = None,
135 affected_templates: list[str] | None = None,
136) -> bool:
137 """Publish cache invalidation event.
139 Helper function for cache operations to broadcast invalidation events.
141 Args:
142 cache_key: The cache key being invalidated
143 reason: Reason for invalidation (e.g., "content_updated", "manual")
144 invalidated_by: User/system that triggered invalidation
145 affected_templates: List of template names affected by this invalidation
147 Returns:
148 True if event published successfully, False otherwise
149 """
150 with suppress(Exception):
151 from ..._events_integration import get_event_publisher
153 publisher = get_event_publisher()
154 if publisher:
155 return await publisher.publish_cache_invalidation(
156 cache_key=cache_key,
157 reason=reason,
158 invalidated_by=invalidated_by,
159 affected_templates=affected_templates,
160 )
162 return False
165async def publish_htmx_refresh(
166 target: str,
167 swap_method: str | None = None,
168) -> bool:
169 """Publish HTMX refresh event to connected clients.
171 Args:
172 target: CSS selector for element to refresh
173 swap_method: How to swap content ("innerHTML", "outerHTML", etc.)
175 Returns:
176 True if event published successfully, False otherwise
177 """
178 with suppress(Exception):
179 from ..._events_integration import get_event_publisher
181 publisher = get_event_publisher()
182 if publisher:
183 return await publisher.publish_htmx_update(
184 update_type="refresh",
185 target=target,
186 swap_method=swap_method,
187 )
189 return False
192async def publish_htmx_trigger(
193 trigger_name: str,
194 trigger_data: dict[str, t.Any] | None = None,
195 target: str | None = None,
196) -> bool:
197 """Publish custom HTMX trigger event.
199 Args:
200 trigger_name: Name of the custom event
201 trigger_data: Data to send with the event
202 target: Optional CSS selector for targeted delivery
204 Returns:
205 True if event published successfully, False otherwise
206 """
207 with suppress(Exception):
208 from ..._events_integration import get_event_publisher
210 publisher = get_event_publisher()
211 if publisher:
212 return await publisher.publish_htmx_update(
213 update_type="trigger",
214 trigger_name=trigger_name,
215 trigger_data=trigger_data,
216 target=target,
217 )
219 return False
222async def publish_admin_action(
223 action_type: str,
224 user_id: str,
225 resource_type: str | None = None,
226 resource_id: str | None = None,
227 changes: dict[str, t.Any] | None = None,
228 ip_address: str | None = None,
229) -> bool:
230 """Publish admin action event for audit logging.
232 Args:
233 action_type: Type of action ("create", "update", "delete", "login", "logout")
234 user_id: ID of user performing the action
235 resource_type: Type of resource being modified
236 resource_id: ID of specific resource
237 changes: Dictionary of changes made
238 ip_address: IP address of the user
240 Returns:
241 True if event published successfully, False otherwise
242 """
243 with suppress(Exception):
244 from ..._events_integration import get_event_publisher
246 publisher = get_event_publisher()
247 if publisher:
248 return await publisher.publish_admin_action(
249 action_type=action_type,
250 user_id=user_id,
251 resource_type=resource_type,
252 resource_id=resource_id,
253 changes=changes,
254 ip_address=ip_address,
255 )
257 return False
260def get_template_metrics(template_name: str) -> dict[str, t.Any]:
261 """Get performance metrics for a specific template.
263 Args:
264 template_name: Name of the template to get stats for
266 Returns:
267 Dictionary with performance statistics or empty dict if unavailable
268 """
269 with suppress(Exception):
270 # Get the template metrics handler
271 handler = depends.get("template_metrics")
272 if handler and hasattr(handler, "get_template_stats"):
273 stats: dict[str, t.Any] = handler.get_template_stats(template_name)
274 return stats
276 return {}
279def get_recent_admin_actions(limit: int = 50) -> list[dict[str, t.Any]]:
280 """Get recent admin actions from audit log.
282 Args:
283 limit: Maximum number of actions to return
285 Returns:
286 List of recent admin actions or empty list if unavailable
287 """
288 with suppress(Exception):
289 # Get the admin audit handler
290 handler = depends.get("admin_audit")
291 if handler and hasattr(handler, "get_recent_actions"):
292 actions: list[dict[str, t.Any]] = handler.get_recent_actions(limit=limit)
293 return actions
295 return []