Coverage for fastblocks/actions/gather/components.py: 59%

147 statements  

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

1"""FastBlocks Component Gathering Action. 

2 

3Provides unified component discovery and management for HTMY components, 

4integrating with the gather action system to provide comprehensive component 

5orchestration across the FastBlocks ecosystem. 

6 

7Features: 

8- Unified component discovery across all searchpaths 

9- Component metadata collection and analysis 

10- Performance-optimized gathering with parallel processing 

11- Integration with existing gather strategies 

12- HTMX-aware component categorization 

13- Advanced filtering and sorting capabilities 

14 

15Author: lesleslie <les@wedgwoodwebworks.com> 

16Created: 2025-01-13 

17""" 

18 

19import asyncio 

20from contextlib import suppress 

21from dataclasses import dataclass, field 

22from datetime import datetime 

23from typing import Any 

24 

25from acb.debug import debug 

26from acb.depends import depends 

27from anyio import Path as AsyncPath 

28 

29from .strategies import GatherResult, GatherStrategy 

30 

31 

32@dataclass 

33class ComponentGatherResult(GatherResult): 

34 """Result from component gathering operation.""" 

35 

36 components: dict[str, Any] = field(default_factory=dict) 

37 component_count: int = 0 

38 validation_errors: list[str] = field(default_factory=list) 

39 htmx_components: list[str] = field(default_factory=list) 

40 dataclass_components: list[str] = field(default_factory=list) 

41 composite_components: list[str] = field(default_factory=list) 

42 searchpaths: list[AsyncPath] = field(default_factory=list) 

43 execution_time: float = 0.0 

44 items_processed: int = 0 

45 cache_hits: int = 0 

46 parallel_tasks: int = 0 

47 error_message: str = "" 

48 

49 def __post_init__(self) -> None: 

50 # Initialize parent GatherResult 

51 success_items = list(self.components.values()) if self.components else [] 

52 GatherResult.__init__(self, success=success_items, errors=[], cache_key=None) 

53 # Set component count 

54 self.component_count = len(self.components) 

55 

56 

57class ComponentGatherStrategy(GatherStrategy): 

58 """Strategy for gathering HTMY components.""" 

59 

60 def __init__( 

61 self, 

62 include_metadata: bool = True, 

63 validate_components: bool = True, 

64 filter_types: list[str] | None = None, 

65 max_parallel: int = 10, 

66 timeout: int = 300, 

67 ) -> None: 

68 super().__init__( 

69 max_concurrent=max_parallel, # Map max_parallel to max_concurrent 

70 timeout=float(timeout), 

71 retry_attempts=2, 

72 ) 

73 self.include_metadata = include_metadata 

74 self.validate_components = validate_components 

75 self.filter_types = filter_types or [] 

76 # Store max_parallel for compatibility 

77 self.max_parallel = max_parallel 

78 

79 async def gather_single(self, item: Any) -> Any: 

80 """Gather a single component.""" 

81 if isinstance(item, dict) and "name" in item and "metadata" in item: 

82 return item 

83 return None 

84 

85 async def gather_batch(self, items: list[Any]) -> list[Any]: 

86 """Gather a batch of components in parallel.""" 

87 if not items: 

88 return [] 

89 

90 tasks = [self.gather_single(item) for item in items] 

91 results = await asyncio.gather(*tasks, return_exceptions=True) 

92 

93 gathered = [] 

94 for result in results: 

95 if isinstance(result, Exception): 

96 debug(f"Component gather error: {result}") 

97 continue 

98 if result is not None: 

99 gathered.append(result) 

100 

101 return gathered 

102 

103 

104def _get_htmy_adapter( 

105 start_time: datetime, 

106) -> tuple[Any | None, ComponentGatherResult | None]: 

107 """Get HTMY adapter or return error result.""" 

108 try: 

109 htmy_adapter = depends.get("htmy") 

110 return htmy_adapter, None 

111 except Exception as e: 

112 debug(f"Could not get HTMY adapter: {e}") 

113 result = ComponentGatherResult( 

114 error_message=f"HTMY adapter not available: {e}", 

115 execution_time=(datetime.now() - start_time).total_seconds(), 

116 ) 

117 result.errors = [e] 

118 return None, result 

119 

120 

121async def _discover_components_metadata(htmy_adapter: Any) -> dict[str, Any]: 

122 """Discover components using adapter's discovery methods.""" 

123 if hasattr(htmy_adapter, "discover_components"): 

124 metadata: dict[str, Any] = await htmy_adapter.discover_components() 

125 return metadata 

126 

127 if hasattr(htmy_adapter, "htmy_registry") and htmy_adapter.htmy_registry: 

128 from fastblocks.adapters.templates._htmy_components import ( 

129 ComponentMetadata, 

130 ComponentStatus, 

131 ComponentType, 

132 ) 

133 

134 discovered = await htmy_adapter.htmy_registry.discover_components() 

135 return { 

136 name: ComponentMetadata( 

137 name=name, 

138 path=path, 

139 type=ComponentType.BASIC, 

140 status=ComponentStatus.DISCOVERED, 

141 ) 

142 for name, path in discovered.items() 

143 } 

144 

145 return {} 

146 

147 

148def _categorize_and_validate( 

149 components_metadata: dict[str, Any], filter_types: list[str] 

150) -> tuple[list[str], list[str], list[str], list[str]]: 

151 """Categorize components and collect validation errors.""" 

152 htmx_components = [] 

153 dataclass_components = [] 

154 composite_components = [] 

155 validation_errors = [] 

156 

157 for name, metadata in components_metadata.items(): 

158 if filter_types and metadata.type.value not in filter_types: 

159 continue 

160 

161 if metadata.type.value == "htmx": 

162 htmx_components.append(name) 

163 elif metadata.type.value == "dataclass": 

164 dataclass_components.append(name) 

165 elif metadata.type.value == "composite": 

166 composite_components.append(name) 

167 

168 if metadata.status.value == "error" and metadata.error_message: 

169 validation_errors.append(f"{name}: {metadata.error_message}") 

170 

171 return ( 

172 htmx_components, 

173 dataclass_components, 

174 composite_components, 

175 validation_errors, 

176 ) 

177 

178 

179async def gather_components( 

180 htmy_adapter: Any | None = None, 

181 include_metadata: bool = True, 

182 validate_components: bool = True, 

183 filter_types: list[str] | None = None, 

184 searchpaths: list[AsyncPath] | None = None, 

185 strategy: ComponentGatherStrategy | None = None, 

186) -> ComponentGatherResult: 

187 """Gather all HTMY components with comprehensive metadata. 

188 

189 Args: 

190 htmy_adapter: HTMY adapter instance (auto-detected if None) 

191 include_metadata: Include component metadata in results 

192 validate_components: Validate component structure 

193 filter_types: Filter components by type (htmx, dataclass, etc.) 

194 searchpaths: Custom searchpaths (uses adapter defaults if None) 

195 strategy: Custom gathering strategy 

196 

197 Returns: 

198 ComponentGatherResult with discovered components and metadata 

199 """ 

200 start_time = datetime.now() 

201 

202 if strategy is None: 

203 strategy = ComponentGatherStrategy( 

204 include_metadata=include_metadata, 

205 validate_components=validate_components, 

206 filter_types=filter_types or [], 

207 ) 

208 

209 if htmy_adapter is None: 

210 htmy_adapter, error_result = _get_htmy_adapter(start_time) 

211 if error_result: 

212 return error_result 

213 

214 if htmy_adapter is None: 

215 result = ComponentGatherResult( 

216 error_message="HTMY adapter not found", 

217 execution_time=(datetime.now() - start_time).total_seconds(), 

218 ) 

219 result.errors = [Exception("HTMY adapter not found")] 

220 return result 

221 

222 try: 

223 if hasattr(htmy_adapter, "_init_htmy_registry"): 

224 await htmy_adapter._init_htmy_registry() 

225 

226 if searchpaths is None and hasattr(htmy_adapter, "component_searchpaths"): 

227 searchpaths = htmy_adapter.component_searchpaths 

228 elif searchpaths is None: 

229 searchpaths = [] 

230 

231 components_metadata = await _discover_components_metadata(htmy_adapter) 

232 

233 htmx, dataclass, composite, validation_errors = _categorize_and_validate( 

234 components_metadata, strategy.filter_types 

235 ) 

236 

237 component_items = [ 

238 { 

239 "name": name, 

240 "metadata": metadata, 

241 "type": metadata.type.value, 

242 "status": metadata.status.value, 

243 "path": str(metadata.path), 

244 "htmx_attributes": metadata.htmx_attributes, 

245 "dependencies": metadata.dependencies, 

246 "docstring": metadata.docstring, 

247 "last_modified": metadata.last_modified.isoformat() 

248 if metadata.last_modified 

249 else None, 

250 "error_message": metadata.error_message, 

251 } 

252 for name, metadata in components_metadata.items() 

253 ] 

254 

255 gathered_components = await strategy.gather_batch(component_items) 

256 

257 final_components = { 

258 item["name"]: item 

259 for item in gathered_components 

260 if item and "name" in item 

261 } 

262 

263 execution_time = (datetime.now() - start_time).total_seconds() 

264 debug(f"Gathered {len(final_components)} components in {execution_time:.2f}s") 

265 

266 result = ComponentGatherResult( 

267 components=final_components, 

268 component_count=len(final_components), 

269 validation_errors=validation_errors, 

270 htmx_components=htmx, 

271 dataclass_components=dataclass, 

272 composite_components=composite, 

273 searchpaths=searchpaths or [], 

274 execution_time=execution_time, 

275 items_processed=len(component_items), 

276 cache_hits=0, 

277 parallel_tasks=min(strategy.max_parallel, len(component_items)), 

278 ) 

279 # Set success list with component values 

280 result.success = list(final_components.values()) if final_components else [] 

281 return result 

282 

283 except Exception as e: 

284 execution_time = (datetime.now() - start_time).total_seconds() 

285 debug(f"Component gathering failed: {e}") 

286 

287 result = ComponentGatherResult( 

288 error_message=str(e), 

289 execution_time=execution_time, 

290 searchpaths=searchpaths or [], 

291 ) 

292 result.errors = [e] 

293 return result 

294 

295 

296async def gather_component_dependencies( 

297 component_name: str, 

298 htmy_adapter: Any | None = None, 

299 recursive: bool = True, 

300 max_depth: int = 5, 

301) -> dict[str, Any]: 

302 """Gather component dependencies recursively. 

303 

304 Args: 

305 component_name: Name of component to analyze 

306 htmy_adapter: HTMY adapter instance 

307 recursive: Whether to gather dependencies recursively 

308 max_depth: Maximum recursion depth 

309 

310 Returns: 

311 Dictionary with component dependency tree 

312 """ 

313 if htmy_adapter is None: 

314 htmy_adapter = depends.get("htmy") 

315 

316 if htmy_adapter is None: 

317 return {"error": "HTMY adapter not available"} 

318 

319 try: 

320 # Get component metadata 

321 if hasattr(htmy_adapter, "validate_component"): 

322 metadata = await htmy_adapter.validate_component(component_name) 

323 

324 return {"error": "Component validation not available"} 

325 

326 dependencies = { 

327 "name": component_name, 

328 "type": metadata.type.value, 

329 "direct_dependencies": metadata.dependencies, 

330 "children": {}, 

331 } 

332 

333 if recursive and max_depth > 0: 

334 for dep in metadata.dependencies: 

335 with suppress(Exception): 

336 child_deps = await gather_component_dependencies( 

337 dep, htmy_adapter, recursive, max_depth - 1 

338 ) 

339 dependencies["children"][dep] = child_deps 

340 

341 return dependencies 

342 

343 except Exception as e: 

344 return {"error": str(e)} 

345 

346 

347async def analyze_component_usage( 

348 htmy_adapter: Any | None = None, include_templates: bool = True 

349) -> dict[str, Any]: 

350 """Analyze component usage patterns across the application. 

351 

352 Args: 

353 htmy_adapter: HTMY adapter instance 

354 include_templates: Whether to scan templates for component usage 

355 

356 Returns: 

357 Dictionary with usage analysis 

358 """ 

359 if htmy_adapter is None: 

360 htmy_adapter = depends.get("htmy") 

361 

362 if htmy_adapter is None: 

363 return {"error": "HTMY adapter not available"} 

364 

365 try: 

366 components_result = await gather_components(htmy_adapter) 

367 

368 if not components_result.is_success: 

369 return {"error": components_result.error_message} 

370 

371 analysis: dict[str, Any] = { 

372 "total_components": components_result.component_count, 

373 "by_type": { 

374 "htmx": len(components_result.htmx_components), 

375 "dataclass": len(components_result.dataclass_components), 

376 "composite": len(components_result.composite_components), 

377 }, 

378 "validation_errors": len(components_result.validation_errors), 

379 "searchpaths": [str(p) for p in components_result.searchpaths], 

380 "components": {}, 

381 } 

382 

383 # Analyze each component 

384 for name, component_data in components_result.components.items(): 

385 analysis["components"][name] = { 

386 "type": component_data.get("type"), 

387 "status": component_data.get("status"), 

388 "has_htmx": bool(component_data.get("htmx_attributes")), 

389 "dependency_count": len(component_data.get("dependencies", [])), 

390 "has_documentation": bool(component_data.get("docstring")), 

391 "last_modified": component_data.get("last_modified"), 

392 } 

393 

394 return analysis 

395 

396 except Exception as e: 

397 return {"error": str(e)}