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
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""FastBlocks Component Gathering Action.
3Provides unified component discovery and management for HTMY components,
4integrating with the gather action system to provide comprehensive component
5orchestration across the FastBlocks ecosystem.
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
15Author: lesleslie <les@wedgwoodwebworks.com>
16Created: 2025-01-13
17"""
19import asyncio
20from contextlib import suppress
21from dataclasses import dataclass, field
22from datetime import datetime
23from typing import Any
25from acb.debug import debug
26from acb.depends import depends
27from anyio import Path as AsyncPath
29from .strategies import GatherResult, GatherStrategy
32@dataclass
33class ComponentGatherResult(GatherResult):
34 """Result from component gathering operation."""
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 = ""
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)
57class ComponentGatherStrategy(GatherStrategy):
58 """Strategy for gathering HTMY components."""
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
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
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 []
90 tasks = [self.gather_single(item) for item in items]
91 results = await asyncio.gather(*tasks, return_exceptions=True)
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)
101 return gathered
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
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
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 )
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 }
145 return {}
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 = []
157 for name, metadata in components_metadata.items():
158 if filter_types and metadata.type.value not in filter_types:
159 continue
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)
168 if metadata.status.value == "error" and metadata.error_message:
169 validation_errors.append(f"{name}: {metadata.error_message}")
171 return (
172 htmx_components,
173 dataclass_components,
174 composite_components,
175 validation_errors,
176 )
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.
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
197 Returns:
198 ComponentGatherResult with discovered components and metadata
199 """
200 start_time = datetime.now()
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 )
209 if htmy_adapter is None:
210 htmy_adapter, error_result = _get_htmy_adapter(start_time)
211 if error_result:
212 return error_result
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
222 try:
223 if hasattr(htmy_adapter, "_init_htmy_registry"):
224 await htmy_adapter._init_htmy_registry()
226 if searchpaths is None and hasattr(htmy_adapter, "component_searchpaths"):
227 searchpaths = htmy_adapter.component_searchpaths
228 elif searchpaths is None:
229 searchpaths = []
231 components_metadata = await _discover_components_metadata(htmy_adapter)
233 htmx, dataclass, composite, validation_errors = _categorize_and_validate(
234 components_metadata, strategy.filter_types
235 )
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 ]
255 gathered_components = await strategy.gather_batch(component_items)
257 final_components = {
258 item["name"]: item
259 for item in gathered_components
260 if item and "name" in item
261 }
263 execution_time = (datetime.now() - start_time).total_seconds()
264 debug(f"Gathered {len(final_components)} components in {execution_time:.2f}s")
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
283 except Exception as e:
284 execution_time = (datetime.now() - start_time).total_seconds()
285 debug(f"Component gathering failed: {e}")
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
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.
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
310 Returns:
311 Dictionary with component dependency tree
312 """
313 if htmy_adapter is None:
314 htmy_adapter = depends.get("htmy")
316 if htmy_adapter is None:
317 return {"error": "HTMY adapter not available"}
319 try:
320 # Get component metadata
321 if hasattr(htmy_adapter, "validate_component"):
322 metadata = await htmy_adapter.validate_component(component_name)
324 return {"error": "Component validation not available"}
326 dependencies = {
327 "name": component_name,
328 "type": metadata.type.value,
329 "direct_dependencies": metadata.dependencies,
330 "children": {},
331 }
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
341 return dependencies
343 except Exception as e:
344 return {"error": str(e)}
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.
352 Args:
353 htmy_adapter: HTMY adapter instance
354 include_templates: Whether to scan templates for component usage
356 Returns:
357 Dictionary with usage analysis
358 """
359 if htmy_adapter is None:
360 htmy_adapter = depends.get("htmy")
362 if htmy_adapter is None:
363 return {"error": "HTMY adapter not available"}
365 try:
366 components_result = await gather_components(htmy_adapter)
368 if not components_result.is_success:
369 return {"error": components_result.error_message}
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 }
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 }
394 return analysis
396 except Exception as e:
397 return {"error": str(e)}