Coverage for integrations / channels / plugins / registry.py: 0.0%
204 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2Plugin Registry for HevolveBot Integration.
4Provides a registry for discovering, installing, and managing plugins
5from various sources.
6"""
8import logging
9import json
10import hashlib
11from dataclasses import dataclass, field
12from typing import Any, Dict, List, Optional
13from datetime import datetime
14from enum import Enum
15import os
17logger = logging.getLogger(__name__)
20class PluginSource(Enum):
21 """Source types for plugins."""
22 LOCAL = "local"
23 REMOTE = "remote"
24 GIT = "git"
27@dataclass
28class PluginInfo:
29 """Information about an available plugin."""
30 name: str
31 version: str
32 description: str
33 author: str
34 source: PluginSource
35 source_url: str = ""
36 dependencies: List[str] = field(default_factory=list)
37 tags: List[str] = field(default_factory=list)
38 downloads: int = 0
39 rating: float = 0.0
40 checksum: str = ""
41 created_at: Optional[datetime] = None
42 updated_at: Optional[datetime] = None
44 def to_dict(self) -> Dict[str, Any]:
45 """Convert to dictionary."""
46 return {
47 "name": self.name,
48 "version": self.version,
49 "description": self.description,
50 "author": self.author,
51 "source": self.source.value,
52 "source_url": self.source_url,
53 "dependencies": self.dependencies,
54 "tags": self.tags,
55 "downloads": self.downloads,
56 "rating": self.rating,
57 "checksum": self.checksum,
58 "created_at": self.created_at.isoformat() if self.created_at else None,
59 "updated_at": self.updated_at.isoformat() if self.updated_at else None
60 }
62 @classmethod
63 def from_dict(cls, data: Dict[str, Any]) -> "PluginInfo":
64 """Create from dictionary."""
65 return cls(
66 name=data["name"],
67 version=data["version"],
68 description=data.get("description", ""),
69 author=data.get("author", ""),
70 source=PluginSource(data.get("source", "local")),
71 source_url=data.get("source_url", ""),
72 dependencies=data.get("dependencies", []),
73 tags=data.get("tags", []),
74 downloads=data.get("downloads", 0),
75 rating=data.get("rating", 0.0),
76 checksum=data.get("checksum", ""),
77 created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else None,
78 updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else None
79 )
82@dataclass
83class InstalledPlugin:
84 """Information about an installed plugin."""
85 info: PluginInfo
86 install_path: str
87 installed_at: datetime
88 enabled: bool = False
89 config: Dict[str, Any] = field(default_factory=dict)
92class PluginRegistry:
93 """
94 Registry for plugin discovery and management.
96 Handles searching for plugins, installing, uninstalling,
97 and checking for updates.
98 """
100 def __init__(self, install_dir: str = "./plugins",
101 registry_url: str = ""):
102 self._install_dir = install_dir
103 self._registry_url = registry_url
104 self._available_plugins: Dict[str, PluginInfo] = {}
105 self._installed_plugins: Dict[str, InstalledPlugin] = {}
106 self._cache_file = os.path.join(install_dir, ".registry_cache.json")
108 # Ensure install directory exists
109 os.makedirs(install_dir, exist_ok=True)
111 @property
112 def install_dir(self) -> str:
113 """Return the plugin installation directory."""
114 return self._install_dir
116 @property
117 def available_plugins(self) -> Dict[str, PluginInfo]:
118 """Return available plugins."""
119 return self._available_plugins.copy()
121 @property
122 def installed_plugins(self) -> Dict[str, InstalledPlugin]:
123 """Return installed plugins."""
124 return self._installed_plugins.copy()
126 def register_plugin(self, info: PluginInfo) -> bool:
127 """
128 Register a plugin in the registry.
130 Args:
131 info: The plugin information.
133 Returns:
134 True if registration was successful.
135 """
136 if info.name in self._available_plugins:
137 existing = self._available_plugins[info.name]
138 # Update if newer version
139 if self._compare_versions(info.version, existing.version) > 0:
140 self._available_plugins[info.name] = info
141 logger.info(f"Updated plugin {info.name} to v{info.version}")
142 else:
143 logger.debug(f"Plugin {info.name} already registered with same or newer version")
144 return True
146 self._available_plugins[info.name] = info
147 logger.info(f"Registered plugin {info.name} v{info.version}")
148 return True
150 def unregister_plugin(self, name: str) -> bool:
151 """
152 Unregister a plugin from the registry.
154 Args:
155 name: The plugin name.
157 Returns:
158 True if unregistration was successful.
159 """
160 if name not in self._available_plugins:
161 logger.warning(f"Plugin {name} not found in registry")
162 return False
164 del self._available_plugins[name]
165 logger.info(f"Unregistered plugin {name}")
166 return True
168 def search(self, query: str = "", tags: Optional[List[str]] = None,
169 author: str = "", min_rating: float = 0.0) -> List[PluginInfo]:
170 """
171 Search for plugins in the registry.
173 Args:
174 query: Search query string (searches name and description).
175 tags: Filter by tags.
176 author: Filter by author.
177 min_rating: Minimum rating filter.
179 Returns:
180 List of matching plugins.
181 """
182 results = []
183 query_lower = query.lower()
185 for info in self._available_plugins.values():
186 # Check query match
187 if query and query_lower not in info.name.lower() and \
188 query_lower not in info.description.lower():
189 continue
191 # Check tags
192 if tags and not any(t in info.tags for t in tags):
193 continue
195 # Check author
196 if author and author.lower() != info.author.lower():
197 continue
199 # Check rating
200 if info.rating < min_rating:
201 continue
203 results.append(info)
205 # Sort by downloads (popularity)
206 results.sort(key=lambda x: x.downloads, reverse=True)
207 return results
209 def install(self, plugin_name: str, version: str = "",
210 config: Optional[Dict[str, Any]] = None) -> Optional[InstalledPlugin]:
211 """
212 Install a plugin from the registry.
214 Args:
215 plugin_name: The name of the plugin to install.
216 version: Specific version to install (latest if empty).
217 config: Optional configuration for the plugin.
219 Returns:
220 InstalledPlugin if successful, None otherwise.
221 """
222 if plugin_name not in self._available_plugins:
223 logger.error(f"Plugin {plugin_name} not found in registry")
224 return None
226 info = self._available_plugins[plugin_name]
228 # Check version
229 if version and version != info.version:
230 logger.error(f"Version {version} not available for {plugin_name}")
231 return None
233 # Check if already installed
234 if plugin_name in self._installed_plugins:
235 installed = self._installed_plugins[plugin_name]
236 if installed.info.version == info.version:
237 logger.warning(f"Plugin {plugin_name} v{info.version} already installed")
238 return installed
239 logger.info(f"Upgrading {plugin_name} from v{installed.info.version} to v{info.version}")
241 # Check dependencies
242 for dep in info.dependencies:
243 if dep not in self._installed_plugins:
244 logger.warning(f"Missing dependency: {dep}")
245 # Attempt to install dependency
246 if not self.install(dep):
247 logger.error(f"Failed to install dependency {dep}")
248 return None
250 # Create install path
251 install_path = os.path.join(self._install_dir, plugin_name)
252 os.makedirs(install_path, exist_ok=True)
254 # Create installed plugin record
255 installed = InstalledPlugin(
256 info=info,
257 install_path=install_path,
258 installed_at=datetime.utcnow(),
259 config=config or {}
260 )
262 self._installed_plugins[plugin_name] = installed
263 logger.info(f"Installed plugin {plugin_name} v{info.version}")
265 # Save cache
266 self._save_cache()
268 return installed
270 def uninstall(self, plugin_name: str, remove_config: bool = False) -> bool:
271 """
272 Uninstall a plugin.
274 Args:
275 plugin_name: The name of the plugin to uninstall.
276 remove_config: Whether to remove configuration files.
278 Returns:
279 True if uninstallation was successful.
280 """
281 if plugin_name not in self._installed_plugins:
282 logger.warning(f"Plugin {plugin_name} is not installed")
283 return False
285 installed = self._installed_plugins[plugin_name]
287 # Check for dependents
288 dependents = self._find_dependents(plugin_name)
289 if dependents:
290 logger.error(f"Cannot uninstall {plugin_name}: required by {dependents}")
291 return False
293 # Remove installation
294 if remove_config and os.path.exists(installed.install_path):
295 try:
296 import shutil
297 shutil.rmtree(installed.install_path)
298 except Exception as e:
299 logger.warning(f"Could not remove install directory: {e}")
301 del self._installed_plugins[plugin_name]
302 logger.info(f"Uninstalled plugin {plugin_name}")
304 # Save cache
305 self._save_cache()
307 return True
309 def update(self, plugin_name: str) -> Optional[InstalledPlugin]:
310 """
311 Update a plugin to the latest version.
313 Args:
314 plugin_name: The name of the plugin to update.
316 Returns:
317 Updated InstalledPlugin if successful, None otherwise.
318 """
319 if plugin_name not in self._installed_plugins:
320 logger.warning(f"Plugin {plugin_name} is not installed")
321 return None
323 installed = self._installed_plugins[plugin_name]
325 if plugin_name not in self._available_plugins:
326 logger.warning(f"Plugin {plugin_name} not found in registry")
327 return None
329 available = self._available_plugins[plugin_name]
331 # Check if update is needed
332 if self._compare_versions(available.version, installed.info.version) <= 0:
333 logger.info(f"Plugin {plugin_name} is already up to date")
334 return installed
336 # Preserve config
337 config = installed.config.copy()
339 # Reinstall with new version
340 return self.install(plugin_name, config=config)
342 def check_updates(self) -> List[Dict[str, Any]]:
343 """
344 Check for available updates for installed plugins.
346 Returns:
347 List of plugins with available updates.
348 """
349 updates = []
351 for name, installed in self._installed_plugins.items():
352 if name in self._available_plugins:
353 available = self._available_plugins[name]
354 if self._compare_versions(available.version, installed.info.version) > 0:
355 updates.append({
356 "name": name,
357 "current_version": installed.info.version,
358 "available_version": available.version,
359 "description": available.description
360 })
362 return updates
364 def refresh(self) -> bool:
365 """
366 Refresh the registry from remote sources.
368 Returns:
369 True if refresh was successful.
370 """
371 # In a real implementation, this would fetch from remote registry
372 logger.info("Refreshing plugin registry")
373 return True
375 def _find_dependents(self, plugin_name: str) -> List[str]:
376 """Find plugins that depend on the given plugin."""
377 dependents = []
378 for name, installed in self._installed_plugins.items():
379 if plugin_name in installed.info.dependencies:
380 dependents.append(name)
381 return dependents
383 def _compare_versions(self, v1: str, v2: str) -> int:
384 """
385 Compare two version strings.
387 Returns:
388 1 if v1 > v2, -1 if v1 < v2, 0 if equal.
389 """
390 def parse_version(v):
391 return [int(x) for x in v.split('.')]
393 try:
394 parts1 = parse_version(v1)
395 parts2 = parse_version(v2)
397 for p1, p2 in zip(parts1, parts2):
398 if p1 > p2:
399 return 1
400 if p1 < p2:
401 return -1
403 if len(parts1) > len(parts2):
404 return 1
405 if len(parts1) < len(parts2):
406 return -1
408 return 0
409 except Exception:
410 # Fall back to string comparison
411 if v1 > v2:
412 return 1
413 if v1 < v2:
414 return -1
415 return 0
417 def _save_cache(self) -> None:
418 """Save the registry cache to disk."""
419 try:
420 cache_data = {
421 "available": {k: v.to_dict() for k, v in self._available_plugins.items()},
422 "installed": {
423 k: {
424 "info": v.info.to_dict(),
425 "install_path": v.install_path,
426 "installed_at": v.installed_at.isoformat(),
427 "enabled": v.enabled,
428 "config": v.config
429 }
430 for k, v in self._installed_plugins.items()
431 }
432 }
433 with open(self._cache_file, 'w') as f:
434 json.dump(cache_data, f, indent=2)
435 except Exception as e:
436 logger.warning(f"Failed to save registry cache: {e}")
438 def _load_cache(self) -> None:
439 """Load the registry cache from disk."""
440 if not os.path.exists(self._cache_file):
441 return
443 try:
444 with open(self._cache_file, 'r') as f:
445 cache_data = json.load(f)
447 for name, data in cache_data.get("available", {}).items():
448 self._available_plugins[name] = PluginInfo.from_dict(data)
450 for name, data in cache_data.get("installed", {}).items():
451 self._installed_plugins[name] = InstalledPlugin(
452 info=PluginInfo.from_dict(data["info"]),
453 install_path=data["install_path"],
454 installed_at=datetime.fromisoformat(data["installed_at"]),
455 enabled=data.get("enabled", False),
456 config=data.get("config", {})
457 )
458 except Exception as e:
459 logger.warning(f"Failed to load registry cache: {e}")
461 def get_plugin_info(self, plugin_name: str) -> Optional[PluginInfo]:
462 """Get information about a plugin."""
463 return self._available_plugins.get(plugin_name)
465 def is_installed(self, plugin_name: str) -> bool:
466 """Check if a plugin is installed."""
467 return plugin_name in self._installed_plugins
469 def get_installed_version(self, plugin_name: str) -> Optional[str]:
470 """Get the installed version of a plugin."""
471 if plugin_name in self._installed_plugins:
472 return self._installed_plugins[plugin_name].info.version
473 return None