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

1"""Event tracking wrapper for template rendering operations. 

2 

3This module provides decorators and wrappers to integrate ACB Events 

4with template rendering, tracking performance metrics automatically. 

5 

6Author: lesleslie <les@wedgwoodwebworks.com> 

7Created: 2025-10-01 

8""" 

9 

10import functools 

11import sys 

12import time 

13import typing as t 

14from contextlib import suppress 

15 

16from acb.depends import depends 

17 

18 

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. 

23 

24 Publishes template render events with performance metrics. 

25 Gracefully degrades if events integration unavailable. 

26 

27 Usage: 

28 @track_template_render 

29 async def render_template(self, template_name, context): 

30 ... 

31 """ 

32 

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) 

38 

39 # Track render time 

40 start_time = time.perf_counter() 

41 

42 try: 

43 # Call the original function 

44 result = await func(*args, **kwargs) 

45 

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 ) 

54 

55 return result 

56 

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 

67 

68 return wrapper 

69 

70 

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 

80 

81 # Check positional args 

82 if len(args) > 2 and isinstance(args[2], str): 

83 return args[2] 

84 

85 return None 

86 

87 

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"] 

95 

96 # Check positional args 

97 if len(args) > 3 and isinstance(args[3], dict): 

98 return args[3] 

99 

100 return None 

101 

102 

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 

112 

113 context_size = sys.getsizeof(context) if context else 0 

114 cache_hit = render_time_ms < 5.0 if not error else False 

115 

116 with suppress(Exception): 

117 from ..._events_integration import get_event_publisher 

118 

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 ) 

129 

130 

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. 

138 

139 Helper function for cache operations to broadcast invalidation events. 

140 

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 

146 

147 Returns: 

148 True if event published successfully, False otherwise 

149 """ 

150 with suppress(Exception): 

151 from ..._events_integration import get_event_publisher 

152 

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 ) 

161 

162 return False 

163 

164 

165async def publish_htmx_refresh( 

166 target: str, 

167 swap_method: str | None = None, 

168) -> bool: 

169 """Publish HTMX refresh event to connected clients. 

170 

171 Args: 

172 target: CSS selector for element to refresh 

173 swap_method: How to swap content ("innerHTML", "outerHTML", etc.) 

174 

175 Returns: 

176 True if event published successfully, False otherwise 

177 """ 

178 with suppress(Exception): 

179 from ..._events_integration import get_event_publisher 

180 

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 ) 

188 

189 return False 

190 

191 

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. 

198 

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 

203 

204 Returns: 

205 True if event published successfully, False otherwise 

206 """ 

207 with suppress(Exception): 

208 from ..._events_integration import get_event_publisher 

209 

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 ) 

218 

219 return False 

220 

221 

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. 

231 

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 

239 

240 Returns: 

241 True if event published successfully, False otherwise 

242 """ 

243 with suppress(Exception): 

244 from ..._events_integration import get_event_publisher 

245 

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 ) 

256 

257 return False 

258 

259 

260def get_template_metrics(template_name: str) -> dict[str, t.Any]: 

261 """Get performance metrics for a specific template. 

262 

263 Args: 

264 template_name: Name of the template to get stats for 

265 

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 

275 

276 return {} 

277 

278 

279def get_recent_admin_actions(limit: int = 50) -> list[dict[str, t.Any]]: 

280 """Get recent admin actions from audit log. 

281 

282 Args: 

283 limit: Maximum number of actions to return 

284 

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 

294 

295 return []