pvw-cli 1.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pvw-cli might be problematic. Click here for more details.

Files changed (60) hide show
  1. purviewcli/__init__.py +27 -0
  2. purviewcli/__main__.py +15 -0
  3. purviewcli/cli/__init__.py +5 -0
  4. purviewcli/cli/account.py +199 -0
  5. purviewcli/cli/cli.py +170 -0
  6. purviewcli/cli/collections.py +502 -0
  7. purviewcli/cli/domain.py +361 -0
  8. purviewcli/cli/entity.py +2436 -0
  9. purviewcli/cli/glossary.py +533 -0
  10. purviewcli/cli/health.py +250 -0
  11. purviewcli/cli/insight.py +113 -0
  12. purviewcli/cli/lineage.py +1103 -0
  13. purviewcli/cli/management.py +141 -0
  14. purviewcli/cli/policystore.py +103 -0
  15. purviewcli/cli/relationship.py +75 -0
  16. purviewcli/cli/scan.py +357 -0
  17. purviewcli/cli/search.py +527 -0
  18. purviewcli/cli/share.py +478 -0
  19. purviewcli/cli/types.py +831 -0
  20. purviewcli/cli/unified_catalog.py +3540 -0
  21. purviewcli/cli/workflow.py +402 -0
  22. purviewcli/client/__init__.py +21 -0
  23. purviewcli/client/_account.py +1877 -0
  24. purviewcli/client/_collections.py +1761 -0
  25. purviewcli/client/_domain.py +414 -0
  26. purviewcli/client/_entity.py +3545 -0
  27. purviewcli/client/_glossary.py +3233 -0
  28. purviewcli/client/_health.py +501 -0
  29. purviewcli/client/_insight.py +2873 -0
  30. purviewcli/client/_lineage.py +2138 -0
  31. purviewcli/client/_management.py +2202 -0
  32. purviewcli/client/_policystore.py +2915 -0
  33. purviewcli/client/_relationship.py +1351 -0
  34. purviewcli/client/_scan.py +2607 -0
  35. purviewcli/client/_search.py +1472 -0
  36. purviewcli/client/_share.py +272 -0
  37. purviewcli/client/_types.py +2708 -0
  38. purviewcli/client/_unified_catalog.py +5112 -0
  39. purviewcli/client/_workflow.py +2734 -0
  40. purviewcli/client/api_client.py +1295 -0
  41. purviewcli/client/business_rules.py +675 -0
  42. purviewcli/client/config.py +231 -0
  43. purviewcli/client/data_quality.py +433 -0
  44. purviewcli/client/endpoint.py +123 -0
  45. purviewcli/client/endpoints.py +554 -0
  46. purviewcli/client/exceptions.py +38 -0
  47. purviewcli/client/lineage_visualization.py +797 -0
  48. purviewcli/client/monitoring_dashboard.py +712 -0
  49. purviewcli/client/rate_limiter.py +30 -0
  50. purviewcli/client/retry_handler.py +125 -0
  51. purviewcli/client/scanning_operations.py +523 -0
  52. purviewcli/client/settings.py +1 -0
  53. purviewcli/client/sync_client.py +250 -0
  54. purviewcli/plugins/__init__.py +1 -0
  55. purviewcli/plugins/plugin_system.py +709 -0
  56. pvw_cli-1.2.8.dist-info/METADATA +1618 -0
  57. pvw_cli-1.2.8.dist-info/RECORD +60 -0
  58. pvw_cli-1.2.8.dist-info/WHEEL +5 -0
  59. pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
  60. pvw_cli-1.2.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,709 @@
1
+ """
2
+ Plugin System for Purview CLI
3
+ Provides extensible architecture for third-party integrations and custom functionality
4
+ """
5
+
6
+ import asyncio
7
+ import importlib
8
+ import inspect
9
+ import json
10
+ import sys
11
+ from abc import ABC, abstractmethod
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Any, Callable, Type, Union, Tuple
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+ import yaml
18
+ from rich.console import Console
19
+ from rich.table import Table
20
+ from rich.panel import Panel
21
+
22
+ console = Console()
23
+
24
+ class PluginType(Enum):
25
+ """Types of plugins supported"""
26
+ DATA_SOURCE = "data_source"
27
+ CLASSIFICATION = "classification"
28
+ LINEAGE = "lineage"
29
+ EXPORT = "export"
30
+ NOTIFICATION = "notification"
31
+ VALIDATION = "validation"
32
+ ENRICHMENT = "enrichment"
33
+ CUSTOM = "custom"
34
+
35
+ class PluginStatus(Enum):
36
+ """Plugin status"""
37
+ LOADED = "loaded"
38
+ ACTIVE = "active"
39
+ INACTIVE = "inactive"
40
+ ERROR = "error"
41
+ DISABLED = "disabled"
42
+
43
+ @dataclass
44
+ class PluginMetadata:
45
+ """Plugin metadata information"""
46
+ name: str
47
+ version: str
48
+ description: str
49
+ author: str
50
+ plugin_type: PluginType
51
+ dependencies: List[str] = field(default_factory=list)
52
+ configuration_schema: Dict[str, Any] = field(default_factory=dict)
53
+ supported_operations: List[str] = field(default_factory=list)
54
+ entry_point: str = "main"
55
+ min_cli_version: str = "1.0.0"
56
+ max_cli_version: str = ""
57
+
58
+ @dataclass
59
+ class PluginConfig:
60
+ """Plugin configuration"""
61
+ plugin_name: str
62
+ enabled: bool = True
63
+ configuration: Dict[str, Any] = field(default_factory=dict)
64
+ priority: int = 100
65
+
66
+ class PluginInterface(ABC):
67
+ """Base interface that all plugins must implement"""
68
+
69
+ def __init__(self, config: Dict[str, Any]):
70
+ self.config = config
71
+ self.console = Console()
72
+
73
+ @abstractmethod
74
+ def get_metadata(self) -> PluginMetadata:
75
+ """Return plugin metadata"""
76
+ pass
77
+
78
+ @abstractmethod
79
+ async def initialize(self) -> bool:
80
+ """Initialize the plugin. Return True if successful."""
81
+ pass
82
+
83
+ @abstractmethod
84
+ async def cleanup(self):
85
+ """Cleanup plugin resources"""
86
+ pass
87
+
88
+ @abstractmethod
89
+ async def execute(self, operation: str, **kwargs) -> Any:
90
+ """Execute a plugin operation"""
91
+ pass
92
+
93
+ def validate_configuration(self, config: Dict[str, Any]) -> Tuple[bool, List[str]]:
94
+ """Validate plugin configuration. Return (is_valid, error_messages)"""
95
+ return True, []
96
+
97
+ class DataSourcePlugin(PluginInterface):
98
+ """Base class for data source plugins"""
99
+
100
+ @abstractmethod
101
+ async def discover_assets(self, connection_info: Dict) -> List[Dict]:
102
+ """Discover assets from the data source"""
103
+ pass
104
+
105
+ @abstractmethod
106
+ async def extract_metadata(self, asset_info: Dict) -> Dict:
107
+ """Extract metadata from a specific asset"""
108
+ pass
109
+
110
+ @abstractmethod
111
+ async def test_connection(self, connection_info: Dict) -> bool:
112
+ """Test connection to the data source"""
113
+ pass
114
+
115
+ class ClassificationPlugin(PluginInterface):
116
+ """Base class for classification plugins"""
117
+
118
+ @abstractmethod
119
+ async def classify_entity(self, entity_data: Dict) -> List[str]:
120
+ """Return list of suggested classifications for an entity"""
121
+ pass
122
+
123
+ @abstractmethod
124
+ async def get_classification_confidence(self, entity_data: Dict, classification: str) -> float:
125
+ """Return confidence score (0.0-1.0) for a classification"""
126
+ pass
127
+
128
+ class ExportPlugin(PluginInterface):
129
+ """Base class for export plugins"""
130
+
131
+ @abstractmethod
132
+ async def export_data(self, data: Any, export_config: Dict) -> str:
133
+ """Export data to external system. Return export identifier."""
134
+ pass
135
+
136
+ @abstractmethod
137
+ def get_supported_formats(self) -> List[str]:
138
+ """Return list of supported export formats"""
139
+ pass
140
+
141
+ class NotificationPlugin(PluginInterface):
142
+ """Base class for notification plugins"""
143
+
144
+ @abstractmethod
145
+ async def send_notification(self, message: str, recipients: List[str], **kwargs) -> bool:
146
+ """Send notification. Return True if successful."""
147
+ pass
148
+
149
+ @abstractmethod
150
+ def get_supported_channels(self) -> List[str]:
151
+ """Return list of supported notification channels"""
152
+ pass
153
+
154
+ class PluginManager:
155
+ """Main plugin management system"""
156
+
157
+ def __init__(self, plugins_directory: str = "plugins"):
158
+ self.plugins_directory = Path(plugins_directory)
159
+ self.loaded_plugins: Dict[str, PluginInterface] = {}
160
+ self.plugin_configs: Dict[str, PluginConfig] = {}
161
+ self.plugin_metadata: Dict[str, PluginMetadata] = {}
162
+ self.plugin_status: Dict[str, PluginStatus] = {}
163
+ self.console = Console()
164
+
165
+ # Create plugins directory if it doesn't exist
166
+ self.plugins_directory.mkdir(exist_ok=True)
167
+
168
+ async def load_plugins(self, config_file: Optional[str] = None):
169
+ """Load all plugins from the plugins directory"""
170
+
171
+ # Load plugin configurations
172
+ if config_file:
173
+ await self._load_plugin_configurations(config_file)
174
+
175
+ # Discover plugin files
176
+ plugin_files = list(self.plugins_directory.glob("**/*.py"))
177
+ plugin_files.extend(list(self.plugins_directory.glob("**/*.yaml")))
178
+
179
+ for plugin_file in plugin_files:
180
+ if plugin_file.name.startswith("__"):
181
+ continue
182
+
183
+ try:
184
+ await self._load_single_plugin(plugin_file)
185
+ except Exception as e:
186
+ self.console.print(f"[red]Failed to load plugin {plugin_file}: {e}[/red]")
187
+ continue
188
+
189
+ self.console.print(f"[green]Loaded {len(self.loaded_plugins)} plugins[/green]")
190
+
191
+ async def _load_plugin_configurations(self, config_file: str):
192
+ """Load plugin configurations from file"""
193
+ try:
194
+ config_path = Path(config_file)
195
+ if config_path.exists():
196
+ if config_path.suffix.lower() == '.yaml':
197
+ with open(config_path, 'r') as f:
198
+ config_data = yaml.safe_load(f)
199
+ else:
200
+ with open(config_path, 'r') as f:
201
+ config_data = json.load(f)
202
+
203
+ for plugin_name, plugin_config in config_data.get('plugins', {}).items():
204
+ self.plugin_configs[plugin_name] = PluginConfig(
205
+ plugin_name=plugin_name,
206
+ enabled=plugin_config.get('enabled', True),
207
+ configuration=plugin_config.get('configuration', {}),
208
+ priority=plugin_config.get('priority', 100)
209
+ )
210
+ except Exception as e:
211
+ self.console.print(f"[yellow]Warning: Could not load plugin config: {e}[/yellow]")
212
+
213
+ async def _load_single_plugin(self, plugin_file: Path):
214
+ """Load a single plugin file"""
215
+
216
+ if plugin_file.suffix == '.py':
217
+ await self._load_python_plugin(plugin_file)
218
+ elif plugin_file.suffix in ['.yaml', '.yml']:
219
+ await self._load_yaml_plugin(plugin_file)
220
+
221
+ async def _load_python_plugin(self, plugin_file: Path):
222
+ """Load a Python plugin"""
223
+
224
+ try:
225
+ # Add plugin directory to Python path
226
+ plugin_dir = plugin_file.parent
227
+ if str(plugin_dir) not in sys.path:
228
+ sys.path.insert(0, str(plugin_dir))
229
+
230
+ # Import the plugin module
231
+ module_name = plugin_file.stem
232
+ spec = importlib.util.spec_from_file_location(module_name, plugin_file)
233
+ module = importlib.util.module_from_spec(spec)
234
+ spec.loader.exec_module(module)
235
+
236
+ # Find plugin classes
237
+ for name, obj in inspect.getmembers(module, inspect.isclass):
238
+ if (issubclass(obj, PluginInterface) and
239
+ obj != PluginInterface and
240
+ not inspect.isabstract(obj)):
241
+
242
+ # Get plugin configuration
243
+ plugin_config = self.plugin_configs.get(name, PluginConfig(name))
244
+
245
+ if not plugin_config.enabled:
246
+ self.plugin_status[name] = PluginStatus.DISABLED
247
+ continue
248
+
249
+ # Create plugin instance
250
+ plugin_instance = obj(plugin_config.configuration)
251
+
252
+ # Get metadata
253
+ metadata = plugin_instance.get_metadata()
254
+ self.plugin_metadata[name] = metadata
255
+
256
+ # Initialize plugin
257
+ if await plugin_instance.initialize():
258
+ self.loaded_plugins[name] = plugin_instance
259
+ self.plugin_status[name] = PluginStatus.ACTIVE
260
+ self.console.print(f"[green]✓ Loaded plugin: {name} v{metadata.version}[/green]")
261
+ else:
262
+ self.plugin_status[name] = PluginStatus.ERROR
263
+ self.console.print(f"[red]✗ Failed to initialize plugin: {name}[/red]")
264
+
265
+ except Exception as e:
266
+ self.console.print(f"[red]Error loading Python plugin {plugin_file}: {e}[/red]")
267
+
268
+ async def _load_yaml_plugin(self, plugin_file: Path):
269
+ """Load a YAML-based plugin configuration"""
270
+
271
+ try:
272
+ with open(plugin_file, 'r') as f:
273
+ plugin_spec = yaml.safe_load(f)
274
+
275
+ plugin_name = plugin_spec.get('name')
276
+ if not plugin_name:
277
+ return
278
+
279
+ # Create metadata from YAML
280
+ metadata = PluginMetadata(
281
+ name=plugin_name,
282
+ version=plugin_spec.get('version', '1.0.0'),
283
+ description=plugin_spec.get('description', ''),
284
+ author=plugin_spec.get('author', ''),
285
+ plugin_type=PluginType(plugin_spec.get('type', 'custom')),
286
+ dependencies=plugin_spec.get('dependencies', []),
287
+ configuration_schema=plugin_spec.get('configuration_schema', {}),
288
+ supported_operations=plugin_spec.get('supported_operations', [])
289
+ )
290
+
291
+ self.plugin_metadata[plugin_name] = metadata
292
+ self.plugin_status[plugin_name] = PluginStatus.LOADED
293
+
294
+ except Exception as e:
295
+ self.console.print(f"[red]Error loading YAML plugin {plugin_file}: {e}[/red]")
296
+
297
+ async def execute_plugin_operation(
298
+ self,
299
+ plugin_name: str,
300
+ operation: str,
301
+ **kwargs
302
+ ) -> Any:
303
+ """Execute an operation on a specific plugin"""
304
+
305
+ if plugin_name not in self.loaded_plugins:
306
+ raise ValueError(f"Plugin '{plugin_name}' not found or not loaded")
307
+
308
+ plugin = self.loaded_plugins[plugin_name]
309
+
310
+ try:
311
+ result = await plugin.execute(operation, **kwargs)
312
+ return result
313
+ except Exception as e:
314
+ self.console.print(f"[red]Error executing {operation} on {plugin_name}: {e}[/red]")
315
+ raise
316
+
317
+ async def execute_plugin_chain(
318
+ self,
319
+ plugin_type: PluginType,
320
+ operation: str,
321
+ data: Any,
322
+ **kwargs
323
+ ) -> List[Any]:
324
+ """Execute an operation across all plugins of a specific type"""
325
+
326
+ results = []
327
+
328
+ # Get plugins of the specified type, sorted by priority
329
+ type_plugins = [
330
+ (name, plugin) for name, plugin in self.loaded_plugins.items()
331
+ if self.plugin_metadata[name].plugin_type == plugin_type
332
+ ]
333
+
334
+ # Sort by priority (lower numbers = higher priority)
335
+ type_plugins.sort(key=lambda x: self.plugin_configs.get(x[0], PluginConfig(x[0])).priority)
336
+
337
+ for plugin_name, plugin in type_plugins:
338
+ try:
339
+ result = await plugin.execute(operation, data=data, **kwargs)
340
+ results.append({
341
+ 'plugin_name': plugin_name,
342
+ 'result': result,
343
+ 'success': True
344
+ })
345
+ except Exception as e:
346
+ results.append({
347
+ 'plugin_name': plugin_name,
348
+ 'result': None,
349
+ 'success': False,
350
+ 'error': str(e)
351
+ })
352
+ self.console.print(f"[yellow]Warning: Plugin {plugin_name} failed: {e}[/yellow]")
353
+
354
+ return results
355
+
356
+ def get_plugins_by_type(self, plugin_type: PluginType) -> List[str]:
357
+ """Get list of plugin names by type"""
358
+ return [
359
+ name for name, metadata in self.plugin_metadata.items()
360
+ if metadata.plugin_type == plugin_type and name in self.loaded_plugins
361
+ ]
362
+
363
+ def get_plugin_info(self, plugin_name: str) -> Optional[Dict[str, Any]]:
364
+ """Get detailed information about a specific plugin"""
365
+
366
+ if plugin_name not in self.plugin_metadata:
367
+ return None
368
+
369
+ metadata = self.plugin_metadata[plugin_name]
370
+ status = self.plugin_status.get(plugin_name, PluginStatus.ERROR)
371
+ config = self.plugin_configs.get(plugin_name, PluginConfig(plugin_name))
372
+
373
+ return {
374
+ 'name': metadata.name,
375
+ 'version': metadata.version,
376
+ 'description': metadata.description,
377
+ 'author': metadata.author,
378
+ 'type': metadata.plugin_type.value,
379
+ 'status': status.value,
380
+ 'enabled': config.enabled,
381
+ 'dependencies': metadata.dependencies,
382
+ 'supported_operations': metadata.supported_operations,
383
+ 'configuration': config.configuration
384
+ }
385
+
386
+ def list_plugins(self) -> Table:
387
+ """Create a table listing all plugins"""
388
+
389
+ table = Table(title="Loaded Plugins", show_header=True, header_style="bold magenta")
390
+ table.add_column("Name", style="cyan", no_wrap=True)
391
+ table.add_column("Version", style="green")
392
+ table.add_column("Type", style="yellow")
393
+ table.add_column("Status", style="blue")
394
+ table.add_column("Description")
395
+
396
+ for plugin_name in sorted(self.plugin_metadata.keys()):
397
+ metadata = self.plugin_metadata[plugin_name]
398
+ status = self.plugin_status.get(plugin_name, PluginStatus.ERROR)
399
+
400
+ # Set status color
401
+ status_color = {
402
+ PluginStatus.ACTIVE: "green",
403
+ PluginStatus.LOADED: "yellow",
404
+ PluginStatus.INACTIVE: "blue",
405
+ PluginStatus.ERROR: "red",
406
+ PluginStatus.DISABLED: "gray"
407
+ }.get(status, "white")
408
+
409
+ table.add_row(
410
+ plugin_name,
411
+ metadata.version,
412
+ metadata.plugin_type.value,
413
+ f"[{status_color}]{status.value}[/{status_color}]",
414
+ metadata.description[:50] + "..." if len(metadata.description) > 50 else metadata.description
415
+ )
416
+
417
+ return table
418
+
419
+ async def reload_plugin(self, plugin_name: str) -> bool:
420
+ """Reload a specific plugin"""
421
+
422
+ try:
423
+ # Cleanup existing plugin
424
+ if plugin_name in self.loaded_plugins:
425
+ await self.loaded_plugins[plugin_name].cleanup()
426
+ del self.loaded_plugins[plugin_name]
427
+
428
+ # Find and reload plugin file
429
+ plugin_files = list(self.plugins_directory.glob(f"**/{plugin_name}.py"))
430
+ plugin_files.extend(list(self.plugins_directory.glob(f"**/{plugin_name}.yaml")))
431
+
432
+ if plugin_files:
433
+ await self._load_single_plugin(plugin_files[0])
434
+ return plugin_name in self.loaded_plugins
435
+
436
+ return False
437
+
438
+ except Exception as e:
439
+ self.console.print(f"[red]Error reloading plugin {plugin_name}: {e}[/red]")
440
+ return False
441
+
442
+ async def enable_plugin(self, plugin_name: str) -> bool:
443
+ """Enable a disabled plugin"""
444
+
445
+ if plugin_name in self.plugin_configs:
446
+ self.plugin_configs[plugin_name].enabled = True
447
+ else:
448
+ self.plugin_configs[plugin_name] = PluginConfig(plugin_name, enabled=True)
449
+
450
+ return await self.reload_plugin(plugin_name)
451
+
452
+ async def disable_plugin(self, plugin_name: str) -> bool:
453
+ """Disable an active plugin"""
454
+
455
+ try:
456
+ if plugin_name in self.loaded_plugins:
457
+ await self.loaded_plugins[plugin_name].cleanup()
458
+ del self.loaded_plugins[plugin_name]
459
+
460
+ if plugin_name in self.plugin_configs:
461
+ self.plugin_configs[plugin_name].enabled = False
462
+ else:
463
+ self.plugin_configs[plugin_name] = PluginConfig(plugin_name, enabled=False)
464
+
465
+ self.plugin_status[plugin_name] = PluginStatus.DISABLED
466
+ return True
467
+
468
+ except Exception as e:
469
+ self.console.print(f"[red]Error disabling plugin {plugin_name}: {e}[/red]")
470
+ return False
471
+
472
+ async def cleanup_all_plugins(self):
473
+ """Cleanup all loaded plugins"""
474
+
475
+ for plugin_name, plugin in self.loaded_plugins.items():
476
+ try:
477
+ await plugin.cleanup()
478
+ except Exception as e:
479
+ self.console.print(f"[yellow]Warning: Error cleaning up {plugin_name}: {e}[/yellow]")
480
+
481
+ self.loaded_plugins.clear()
482
+
483
+ def export_plugin_configuration(self, output_path: str):
484
+ """Export current plugin configuration to file"""
485
+
486
+ config_data = {
487
+ 'plugins': {}
488
+ }
489
+
490
+ for plugin_name, config in self.plugin_configs.items():
491
+ config_data['plugins'][plugin_name] = {
492
+ 'enabled': config.enabled,
493
+ 'configuration': config.configuration,
494
+ 'priority': config.priority
495
+ }
496
+
497
+ output_file = Path(output_path)
498
+
499
+ if output_file.suffix.lower() == '.yaml':
500
+ with open(output_file, 'w') as f:
501
+ yaml.dump(config_data, f, default_flow_style=False)
502
+ else:
503
+ with open(output_file, 'w') as f:
504
+ json.dump(config_data, f, indent=2)
505
+
506
+ self.console.print(f"[green]Plugin configuration exported to {output_path}[/green]")
507
+
508
+ class PluginRegistry:
509
+ """Registry for discovering and managing available plugins"""
510
+
511
+ def __init__(self):
512
+ self.console = Console()
513
+ self.registry_url = "https://pvw-cli-plugins.registry.example.com" # Example URL
514
+
515
+ def search_plugins(self, query: str, plugin_type: Optional[PluginType] = None) -> List[Dict]:
516
+ """Search for plugins in the registry"""
517
+
518
+ # This would normally query a remote registry
519
+ # For now, return mock data
520
+
521
+ mock_plugins = [
522
+ {
523
+ 'name': 'snowflake-connector',
524
+ 'version': '1.2.0',
525
+ 'description': 'Snowflake data source connector',
526
+ 'type': 'data_source',
527
+ 'author': 'Community',
528
+ 'downloads': 1250,
529
+ 'rating': 4.5
530
+ },
531
+ {
532
+ 'name': 'pii-classifier',
533
+ 'version': '2.1.0',
534
+ 'description': 'Advanced PII classification plugin',
535
+ 'type': 'classification',
536
+ 'author': 'Security Team',
537
+ 'downloads': 890,
538
+ 'rating': 4.8
539
+ },
540
+ {
541
+ 'name': 'teams-notifications',
542
+ 'version': '1.0.3',
543
+ 'description': 'Microsoft Teams notification plugin',
544
+ 'type': 'notification',
545
+ 'author': 'Integration Team',
546
+ 'downloads': 650,
547
+ 'rating': 4.2
548
+ }
549
+ ]
550
+
551
+ # Filter by query
552
+ if query:
553
+ query_lower = query.lower()
554
+ mock_plugins = [
555
+ p for p in mock_plugins
556
+ if query_lower in p['name'].lower() or query_lower in p['description'].lower()
557
+ ]
558
+
559
+ # Filter by type
560
+ if plugin_type:
561
+ mock_plugins = [
562
+ p for p in mock_plugins
563
+ if p['type'] == plugin_type.value
564
+ ]
565
+
566
+ return mock_plugins
567
+
568
+ def install_plugin(self, plugin_name: str, version: str = "latest") -> bool:
569
+ """Install a plugin from the registry"""
570
+
571
+ try:
572
+ # This would normally download and install the plugin
573
+ self.console.print(f"[green]✓ Plugin '{plugin_name}' v{version} installed successfully[/green]")
574
+ return True
575
+ except Exception as e:
576
+ self.console.print(f"[red]✗ Failed to install plugin '{plugin_name}': {e}[/red]")
577
+ return False
578
+
579
+ def uninstall_plugin(self, plugin_name: str) -> bool:
580
+ """Uninstall a plugin"""
581
+
582
+ try:
583
+ # This would normally remove the plugin files
584
+ self.console.print(f"[green]✓ Plugin '{plugin_name}' uninstalled successfully[/green]")
585
+ return True
586
+ except Exception as e:
587
+ self.console.print(f"[red]✗ Failed to uninstall plugin '{plugin_name}': {e}[/red]")
588
+ return False
589
+
590
+ def update_plugin(self, plugin_name: str) -> bool:
591
+ """Update a plugin to the latest version"""
592
+
593
+ try:
594
+ # This would normally check for updates and install them
595
+ self.console.print(f"[green]✓ Plugin '{plugin_name}' updated successfully[/green]")
596
+ return True
597
+ except Exception as e:
598
+ self.console.print(f"[red]✗ Failed to update plugin '{plugin_name}': {e}[/red]")
599
+ return False
600
+
601
+ # Example plugin implementations
602
+
603
+ class ExampleDataSourcePlugin(DataSourcePlugin):
604
+ """Example data source plugin implementation"""
605
+
606
+ def get_metadata(self) -> PluginMetadata:
607
+ return PluginMetadata(
608
+ name="example-datasource",
609
+ version="1.0.0",
610
+ description="Example data source plugin for demonstration",
611
+ author="CLI Team",
612
+ plugin_type=PluginType.DATA_SOURCE,
613
+ supported_operations=["discover_assets", "extract_metadata", "test_connection"]
614
+ )
615
+
616
+ async def initialize(self) -> bool:
617
+ self.console.print("[green]Example DataSource Plugin initialized[/green]")
618
+ return True
619
+
620
+ async def cleanup(self):
621
+ self.console.print("[yellow]Example DataSource Plugin cleaned up[/yellow]")
622
+
623
+ async def execute(self, operation: str, **kwargs) -> Any:
624
+ if operation == "discover_assets":
625
+ return await self.discover_assets(kwargs.get('connection_info', {}))
626
+ elif operation == "extract_metadata":
627
+ return await self.extract_metadata(kwargs.get('asset_info', {}))
628
+ elif operation == "test_connection":
629
+ return await self.test_connection(kwargs.get('connection_info', {}))
630
+ else:
631
+ raise ValueError(f"Unsupported operation: {operation}")
632
+
633
+ async def discover_assets(self, connection_info: Dict) -> List[Dict]:
634
+ # Mock asset discovery
635
+ return [
636
+ {"name": "table1", "type": "table", "schema": "public"},
637
+ {"name": "table2", "type": "table", "schema": "public"}
638
+ ]
639
+
640
+ async def extract_metadata(self, asset_info: Dict) -> Dict:
641
+ # Mock metadata extraction
642
+ return {
643
+ "columns": [
644
+ {"name": "id", "type": "integer"},
645
+ {"name": "name", "type": "varchar"}
646
+ ],
647
+ "row_count": 1000
648
+ }
649
+
650
+ async def test_connection(self, connection_info: Dict) -> bool:
651
+ # Mock connection test
652
+ return True
653
+
654
+ class ExampleClassificationPlugin(ClassificationPlugin):
655
+ """Example classification plugin implementation"""
656
+
657
+ def get_metadata(self) -> PluginMetadata:
658
+ return PluginMetadata(
659
+ name="example-classifier",
660
+ version="1.0.0",
661
+ description="Example classification plugin for demonstration",
662
+ author="CLI Team",
663
+ plugin_type=PluginType.CLASSIFICATION,
664
+ supported_operations=["classify_entity", "get_classification_confidence"]
665
+ )
666
+
667
+ async def initialize(self) -> bool:
668
+ self.console.print("[green]Example Classification Plugin initialized[/green]")
669
+ return True
670
+
671
+ async def cleanup(self):
672
+ self.console.print("[yellow]Example Classification Plugin cleaned up[/yellow]")
673
+
674
+ async def execute(self, operation: str, **kwargs) -> Any:
675
+ if operation == "classify_entity":
676
+ return await self.classify_entity(kwargs.get('entity_data', {}))
677
+ elif operation == "get_classification_confidence":
678
+ return await self.get_classification_confidence(
679
+ kwargs.get('entity_data', {}),
680
+ kwargs.get('classification', '')
681
+ )
682
+ else:
683
+ raise ValueError(f"Unsupported operation: {operation}")
684
+
685
+ async def classify_entity(self, entity_data: Dict) -> List[str]:
686
+ # Mock classification logic
687
+ entity_name = entity_data.get('name', '').lower()
688
+
689
+ classifications = []
690
+ if 'customer' in entity_name:
691
+ classifications.append('CustomerData')
692
+ if 'email' in entity_name:
693
+ classifications.append('PersonalData')
694
+ if 'financial' in entity_name or 'money' in entity_name:
695
+ classifications.append('FinancialData')
696
+
697
+ return classifications or ['GeneralData']
698
+
699
+ async def get_classification_confidence(self, entity_data: Dict, classification: str) -> float:
700
+ # Mock confidence calculation
701
+ entity_name = entity_data.get('name', '').lower()
702
+
703
+ confidence_map = {
704
+ 'CustomerData': 0.9 if 'customer' in entity_name else 0.1,
705
+ 'PersonalData': 0.8 if 'email' in entity_name else 0.2,
706
+ 'FinancialData': 0.85 if any(term in entity_name for term in ['financial', 'money']) else 0.15
707
+ }
708
+
709
+ return confidence_map.get(classification, 0.5)