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.
- purviewcli/__init__.py +27 -0
- purviewcli/__main__.py +15 -0
- purviewcli/cli/__init__.py +5 -0
- purviewcli/cli/account.py +199 -0
- purviewcli/cli/cli.py +170 -0
- purviewcli/cli/collections.py +502 -0
- purviewcli/cli/domain.py +361 -0
- purviewcli/cli/entity.py +2436 -0
- purviewcli/cli/glossary.py +533 -0
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/insight.py +113 -0
- purviewcli/cli/lineage.py +1103 -0
- purviewcli/cli/management.py +141 -0
- purviewcli/cli/policystore.py +103 -0
- purviewcli/cli/relationship.py +75 -0
- purviewcli/cli/scan.py +357 -0
- purviewcli/cli/search.py +527 -0
- purviewcli/cli/share.py +478 -0
- purviewcli/cli/types.py +831 -0
- purviewcli/cli/unified_catalog.py +3540 -0
- purviewcli/cli/workflow.py +402 -0
- purviewcli/client/__init__.py +21 -0
- purviewcli/client/_account.py +1877 -0
- purviewcli/client/_collections.py +1761 -0
- purviewcli/client/_domain.py +414 -0
- purviewcli/client/_entity.py +3545 -0
- purviewcli/client/_glossary.py +3233 -0
- purviewcli/client/_health.py +501 -0
- purviewcli/client/_insight.py +2873 -0
- purviewcli/client/_lineage.py +2138 -0
- purviewcli/client/_management.py +2202 -0
- purviewcli/client/_policystore.py +2915 -0
- purviewcli/client/_relationship.py +1351 -0
- purviewcli/client/_scan.py +2607 -0
- purviewcli/client/_search.py +1472 -0
- purviewcli/client/_share.py +272 -0
- purviewcli/client/_types.py +2708 -0
- purviewcli/client/_unified_catalog.py +5112 -0
- purviewcli/client/_workflow.py +2734 -0
- purviewcli/client/api_client.py +1295 -0
- purviewcli/client/business_rules.py +675 -0
- purviewcli/client/config.py +231 -0
- purviewcli/client/data_quality.py +433 -0
- purviewcli/client/endpoint.py +123 -0
- purviewcli/client/endpoints.py +554 -0
- purviewcli/client/exceptions.py +38 -0
- purviewcli/client/lineage_visualization.py +797 -0
- purviewcli/client/monitoring_dashboard.py +712 -0
- purviewcli/client/rate_limiter.py +30 -0
- purviewcli/client/retry_handler.py +125 -0
- purviewcli/client/scanning_operations.py +523 -0
- purviewcli/client/settings.py +1 -0
- purviewcli/client/sync_client.py +250 -0
- purviewcli/plugins/__init__.py +1 -0
- purviewcli/plugins/plugin_system.py +709 -0
- pvw_cli-1.2.8.dist-info/METADATA +1618 -0
- pvw_cli-1.2.8.dist-info/RECORD +60 -0
- pvw_cli-1.2.8.dist-info/WHEEL +5 -0
- pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
- 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)
|