truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.0__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.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
"""Plugin Registry for managing installed plugins.
|
|
2
|
+
|
|
3
|
+
This module provides the central registry for plugin management,
|
|
4
|
+
including discovery, registration, and lifecycle management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import select
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
from truthound_dashboard.db.models import (
|
|
17
|
+
CustomReporter,
|
|
18
|
+
CustomValidator,
|
|
19
|
+
Plugin,
|
|
20
|
+
PluginStatus,
|
|
21
|
+
PluginType,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Sequence
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PluginRegistry:
|
|
31
|
+
"""Central registry for plugin management.
|
|
32
|
+
|
|
33
|
+
This class provides methods to register, discover, install,
|
|
34
|
+
enable/disable, and uninstall plugins.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
_validators: Cached custom validators by name.
|
|
38
|
+
_reporters: Cached custom reporters by name.
|
|
39
|
+
_plugins: Cached plugins by name.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
"""Initialize the plugin registry."""
|
|
44
|
+
self._validators: dict[str, CustomValidator] = {}
|
|
45
|
+
self._reporters: dict[str, CustomReporter] = {}
|
|
46
|
+
self._plugins: dict[str, Plugin] = {}
|
|
47
|
+
self._initialized = False
|
|
48
|
+
|
|
49
|
+
async def initialize(self, session: AsyncSession) -> None:
|
|
50
|
+
"""Initialize the registry by loading enabled plugins.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
session: Database session.
|
|
54
|
+
"""
|
|
55
|
+
if self._initialized:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
logger.info("Initializing plugin registry...")
|
|
59
|
+
|
|
60
|
+
# Load enabled plugins
|
|
61
|
+
stmt = select(Plugin).where(Plugin.is_enabled == True) # noqa: E712
|
|
62
|
+
result = await session.execute(stmt)
|
|
63
|
+
plugins = result.scalars().all()
|
|
64
|
+
|
|
65
|
+
for plugin in plugins:
|
|
66
|
+
self._plugins[plugin.name] = plugin
|
|
67
|
+
logger.debug(f"Loaded plugin: {plugin.name} v{plugin.version}")
|
|
68
|
+
|
|
69
|
+
# Load enabled validators
|
|
70
|
+
stmt = select(CustomValidator).where(CustomValidator.is_enabled == True) # noqa: E712
|
|
71
|
+
result = await session.execute(stmt)
|
|
72
|
+
validators = result.scalars().all()
|
|
73
|
+
|
|
74
|
+
for validator in validators:
|
|
75
|
+
self._validators[validator.name] = validator
|
|
76
|
+
logger.debug(f"Loaded validator: {validator.name}")
|
|
77
|
+
|
|
78
|
+
# Load enabled reporters
|
|
79
|
+
stmt = select(CustomReporter).where(CustomReporter.is_enabled == True) # noqa: E712
|
|
80
|
+
result = await session.execute(stmt)
|
|
81
|
+
reporters = result.scalars().all()
|
|
82
|
+
|
|
83
|
+
for reporter in reporters:
|
|
84
|
+
self._reporters[reporter.name] = reporter
|
|
85
|
+
logger.debug(f"Loaded reporter: {reporter.name}")
|
|
86
|
+
|
|
87
|
+
self._initialized = True
|
|
88
|
+
logger.info(
|
|
89
|
+
f"Plugin registry initialized: {len(self._plugins)} plugins, "
|
|
90
|
+
f"{len(self._validators)} validators, {len(self._reporters)} reporters"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def clear_cache(self) -> None:
|
|
94
|
+
"""Clear all cached plugins, validators, and reporters."""
|
|
95
|
+
self._validators.clear()
|
|
96
|
+
self._reporters.clear()
|
|
97
|
+
self._plugins.clear()
|
|
98
|
+
self._initialized = False
|
|
99
|
+
logger.info("Plugin registry cache cleared")
|
|
100
|
+
|
|
101
|
+
# =========================================================================
|
|
102
|
+
# Plugin Management
|
|
103
|
+
# =========================================================================
|
|
104
|
+
|
|
105
|
+
async def list_plugins(
|
|
106
|
+
self,
|
|
107
|
+
session: AsyncSession,
|
|
108
|
+
plugin_type: PluginType | None = None,
|
|
109
|
+
status: PluginStatus | None = None,
|
|
110
|
+
search: str | None = None,
|
|
111
|
+
offset: int = 0,
|
|
112
|
+
limit: int = 50,
|
|
113
|
+
) -> tuple[Sequence[Plugin], int]:
|
|
114
|
+
"""List plugins with optional filtering.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
session: Database session.
|
|
118
|
+
plugin_type: Filter by plugin type.
|
|
119
|
+
status: Filter by status.
|
|
120
|
+
search: Search in name, display_name, description.
|
|
121
|
+
offset: Pagination offset.
|
|
122
|
+
limit: Pagination limit.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Tuple of (plugins list, total count).
|
|
126
|
+
"""
|
|
127
|
+
stmt = select(Plugin)
|
|
128
|
+
|
|
129
|
+
if plugin_type:
|
|
130
|
+
stmt = stmt.where(Plugin.type == plugin_type.value)
|
|
131
|
+
if status:
|
|
132
|
+
stmt = stmt.where(Plugin.status == status.value)
|
|
133
|
+
if search:
|
|
134
|
+
search_pattern = f"%{search}%"
|
|
135
|
+
stmt = stmt.where(
|
|
136
|
+
(Plugin.name.ilike(search_pattern))
|
|
137
|
+
| (Plugin.display_name.ilike(search_pattern))
|
|
138
|
+
| (Plugin.description.ilike(search_pattern))
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Get total count
|
|
142
|
+
count_result = await session.execute(
|
|
143
|
+
select(Plugin.id).select_from(stmt.subquery())
|
|
144
|
+
)
|
|
145
|
+
total = len(count_result.all())
|
|
146
|
+
|
|
147
|
+
# Apply pagination
|
|
148
|
+
stmt = stmt.offset(offset).limit(limit).order_by(Plugin.created_at.desc())
|
|
149
|
+
result = await session.execute(stmt)
|
|
150
|
+
plugins = result.scalars().all()
|
|
151
|
+
|
|
152
|
+
return plugins, total
|
|
153
|
+
|
|
154
|
+
async def get_plugin(
|
|
155
|
+
self,
|
|
156
|
+
session: AsyncSession,
|
|
157
|
+
plugin_id: str | None = None,
|
|
158
|
+
name: str | None = None,
|
|
159
|
+
) -> Plugin | None:
|
|
160
|
+
"""Get a plugin by ID or name.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
session: Database session.
|
|
164
|
+
plugin_id: Plugin ID.
|
|
165
|
+
name: Plugin name.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Plugin if found, None otherwise.
|
|
169
|
+
"""
|
|
170
|
+
if plugin_id:
|
|
171
|
+
stmt = select(Plugin).where(Plugin.id == plugin_id)
|
|
172
|
+
elif name:
|
|
173
|
+
stmt = select(Plugin).where(Plugin.name == name)
|
|
174
|
+
else:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
result = await session.execute(stmt)
|
|
178
|
+
return result.scalar_one_or_none()
|
|
179
|
+
|
|
180
|
+
async def register_plugin(
|
|
181
|
+
self,
|
|
182
|
+
session: AsyncSession,
|
|
183
|
+
name: str,
|
|
184
|
+
display_name: str,
|
|
185
|
+
description: str,
|
|
186
|
+
version: str,
|
|
187
|
+
plugin_type: PluginType,
|
|
188
|
+
**kwargs: Any,
|
|
189
|
+
) -> Plugin:
|
|
190
|
+
"""Register a new plugin.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
session: Database session.
|
|
194
|
+
name: Plugin name (unique).
|
|
195
|
+
display_name: Display name.
|
|
196
|
+
description: Plugin description.
|
|
197
|
+
version: Plugin version.
|
|
198
|
+
plugin_type: Plugin type.
|
|
199
|
+
**kwargs: Additional plugin attributes.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Created plugin.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: If plugin with same name exists.
|
|
206
|
+
"""
|
|
207
|
+
existing = await self.get_plugin(session, name=name)
|
|
208
|
+
if existing:
|
|
209
|
+
raise ValueError(f"Plugin with name '{name}' already exists")
|
|
210
|
+
|
|
211
|
+
plugin = Plugin(
|
|
212
|
+
name=name,
|
|
213
|
+
display_name=display_name,
|
|
214
|
+
description=description,
|
|
215
|
+
version=version,
|
|
216
|
+
type=plugin_type.value,
|
|
217
|
+
**kwargs,
|
|
218
|
+
)
|
|
219
|
+
session.add(plugin)
|
|
220
|
+
await session.flush()
|
|
221
|
+
|
|
222
|
+
logger.info(f"Registered plugin: {name} v{version}")
|
|
223
|
+
return plugin
|
|
224
|
+
|
|
225
|
+
async def install_plugin(
|
|
226
|
+
self,
|
|
227
|
+
session: AsyncSession,
|
|
228
|
+
plugin_id: str,
|
|
229
|
+
enable: bool = True,
|
|
230
|
+
) -> Plugin:
|
|
231
|
+
"""Install a plugin.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
session: Database session.
|
|
235
|
+
plugin_id: Plugin ID.
|
|
236
|
+
enable: Whether to enable after installation.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Updated plugin.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
ValueError: If plugin not found.
|
|
243
|
+
"""
|
|
244
|
+
plugin = await self.get_plugin(session, plugin_id=plugin_id)
|
|
245
|
+
if not plugin:
|
|
246
|
+
raise ValueError(f"Plugin {plugin_id} not found")
|
|
247
|
+
|
|
248
|
+
plugin.install()
|
|
249
|
+
if enable:
|
|
250
|
+
plugin.enable()
|
|
251
|
+
self._plugins[plugin.name] = plugin
|
|
252
|
+
|
|
253
|
+
await session.flush()
|
|
254
|
+
logger.info(f"Installed plugin: {plugin.name} v{plugin.version}")
|
|
255
|
+
return plugin
|
|
256
|
+
|
|
257
|
+
async def uninstall_plugin(
|
|
258
|
+
self,
|
|
259
|
+
session: AsyncSession,
|
|
260
|
+
plugin_id: str,
|
|
261
|
+
remove_data: bool = False,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Uninstall a plugin.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
session: Database session.
|
|
267
|
+
plugin_id: Plugin ID.
|
|
268
|
+
remove_data: Whether to remove plugin data.
|
|
269
|
+
"""
|
|
270
|
+
plugin = await self.get_plugin(session, plugin_id=plugin_id)
|
|
271
|
+
if not plugin:
|
|
272
|
+
raise ValueError(f"Plugin {plugin_id} not found")
|
|
273
|
+
|
|
274
|
+
# Remove from cache
|
|
275
|
+
if plugin.name in self._plugins:
|
|
276
|
+
del self._plugins[plugin.name]
|
|
277
|
+
|
|
278
|
+
if remove_data:
|
|
279
|
+
# Delete plugin entirely
|
|
280
|
+
await session.delete(plugin)
|
|
281
|
+
logger.info(f"Removed plugin: {plugin.name}")
|
|
282
|
+
else:
|
|
283
|
+
# Just mark as uninstalled
|
|
284
|
+
plugin.uninstall()
|
|
285
|
+
logger.info(f"Uninstalled plugin: {plugin.name}")
|
|
286
|
+
|
|
287
|
+
await session.flush()
|
|
288
|
+
|
|
289
|
+
async def enable_plugin(
|
|
290
|
+
self,
|
|
291
|
+
session: AsyncSession,
|
|
292
|
+
plugin_id: str,
|
|
293
|
+
) -> Plugin:
|
|
294
|
+
"""Enable a plugin.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
session: Database session.
|
|
298
|
+
plugin_id: Plugin ID.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Updated plugin.
|
|
302
|
+
"""
|
|
303
|
+
plugin = await self.get_plugin(session, plugin_id=plugin_id)
|
|
304
|
+
if not plugin:
|
|
305
|
+
raise ValueError(f"Plugin {plugin_id} not found")
|
|
306
|
+
|
|
307
|
+
plugin.enable()
|
|
308
|
+
self._plugins[plugin.name] = plugin
|
|
309
|
+
await session.flush()
|
|
310
|
+
|
|
311
|
+
logger.info(f"Enabled plugin: {plugin.name}")
|
|
312
|
+
return plugin
|
|
313
|
+
|
|
314
|
+
async def disable_plugin(
|
|
315
|
+
self,
|
|
316
|
+
session: AsyncSession,
|
|
317
|
+
plugin_id: str,
|
|
318
|
+
) -> Plugin:
|
|
319
|
+
"""Disable a plugin.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
session: Database session.
|
|
323
|
+
plugin_id: Plugin ID.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Updated plugin.
|
|
327
|
+
"""
|
|
328
|
+
plugin = await self.get_plugin(session, plugin_id=plugin_id)
|
|
329
|
+
if not plugin:
|
|
330
|
+
raise ValueError(f"Plugin {plugin_id} not found")
|
|
331
|
+
|
|
332
|
+
plugin.disable()
|
|
333
|
+
if plugin.name in self._plugins:
|
|
334
|
+
del self._plugins[plugin.name]
|
|
335
|
+
await session.flush()
|
|
336
|
+
|
|
337
|
+
logger.info(f"Disabled plugin: {plugin.name}")
|
|
338
|
+
return plugin
|
|
339
|
+
|
|
340
|
+
# =========================================================================
|
|
341
|
+
# Custom Validator Management
|
|
342
|
+
# =========================================================================
|
|
343
|
+
|
|
344
|
+
async def list_validators(
|
|
345
|
+
self,
|
|
346
|
+
session: AsyncSession,
|
|
347
|
+
plugin_id: str | None = None,
|
|
348
|
+
category: str | None = None,
|
|
349
|
+
enabled_only: bool = False,
|
|
350
|
+
search: str | None = None,
|
|
351
|
+
offset: int = 0,
|
|
352
|
+
limit: int = 50,
|
|
353
|
+
) -> tuple[Sequence[CustomValidator], int]:
|
|
354
|
+
"""List custom validators with optional filtering.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
session: Database session.
|
|
358
|
+
plugin_id: Filter by plugin.
|
|
359
|
+
category: Filter by category.
|
|
360
|
+
enabled_only: Only return enabled validators.
|
|
361
|
+
search: Search in name, display_name, description.
|
|
362
|
+
offset: Pagination offset.
|
|
363
|
+
limit: Pagination limit.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Tuple of (validators list, total count).
|
|
367
|
+
"""
|
|
368
|
+
stmt = select(CustomValidator)
|
|
369
|
+
|
|
370
|
+
if plugin_id:
|
|
371
|
+
stmt = stmt.where(CustomValidator.plugin_id == plugin_id)
|
|
372
|
+
if category:
|
|
373
|
+
stmt = stmt.where(CustomValidator.category == category)
|
|
374
|
+
if enabled_only:
|
|
375
|
+
stmt = stmt.where(CustomValidator.is_enabled == True) # noqa: E712
|
|
376
|
+
if search:
|
|
377
|
+
search_pattern = f"%{search}%"
|
|
378
|
+
stmt = stmt.where(
|
|
379
|
+
(CustomValidator.name.ilike(search_pattern))
|
|
380
|
+
| (CustomValidator.display_name.ilike(search_pattern))
|
|
381
|
+
| (CustomValidator.description.ilike(search_pattern))
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Get total count
|
|
385
|
+
count_result = await session.execute(
|
|
386
|
+
select(CustomValidator.id).select_from(stmt.subquery())
|
|
387
|
+
)
|
|
388
|
+
total = len(count_result.all())
|
|
389
|
+
|
|
390
|
+
# Apply pagination
|
|
391
|
+
stmt = stmt.offset(offset).limit(limit).order_by(CustomValidator.created_at.desc())
|
|
392
|
+
result = await session.execute(stmt)
|
|
393
|
+
validators = result.scalars().all()
|
|
394
|
+
|
|
395
|
+
return validators, total
|
|
396
|
+
|
|
397
|
+
async def get_validator(
|
|
398
|
+
self,
|
|
399
|
+
session: AsyncSession,
|
|
400
|
+
validator_id: str | None = None,
|
|
401
|
+
name: str | None = None,
|
|
402
|
+
) -> CustomValidator | None:
|
|
403
|
+
"""Get a custom validator by ID or name.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
session: Database session.
|
|
407
|
+
validator_id: Validator ID.
|
|
408
|
+
name: Validator name.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
CustomValidator if found, None otherwise.
|
|
412
|
+
"""
|
|
413
|
+
if validator_id:
|
|
414
|
+
stmt = select(CustomValidator).where(CustomValidator.id == validator_id)
|
|
415
|
+
elif name:
|
|
416
|
+
stmt = select(CustomValidator).where(CustomValidator.name == name)
|
|
417
|
+
else:
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
result = await session.execute(stmt)
|
|
421
|
+
return result.scalar_one_or_none()
|
|
422
|
+
|
|
423
|
+
async def register_validator(
|
|
424
|
+
self,
|
|
425
|
+
session: AsyncSession,
|
|
426
|
+
name: str,
|
|
427
|
+
display_name: str,
|
|
428
|
+
description: str,
|
|
429
|
+
category: str,
|
|
430
|
+
code: str,
|
|
431
|
+
plugin_id: str | None = None,
|
|
432
|
+
**kwargs: Any,
|
|
433
|
+
) -> CustomValidator:
|
|
434
|
+
"""Register a new custom validator.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
session: Database session.
|
|
438
|
+
name: Validator name (unique).
|
|
439
|
+
display_name: Display name.
|
|
440
|
+
description: Validator description.
|
|
441
|
+
category: Validator category.
|
|
442
|
+
code: Python code implementing the validator.
|
|
443
|
+
plugin_id: Optional parent plugin ID.
|
|
444
|
+
**kwargs: Additional validator attributes.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Created validator.
|
|
448
|
+
|
|
449
|
+
Raises:
|
|
450
|
+
ValueError: If validator with same name exists.
|
|
451
|
+
"""
|
|
452
|
+
existing = await self.get_validator(session, name=name)
|
|
453
|
+
if existing:
|
|
454
|
+
raise ValueError(f"Validator with name '{name}' already exists")
|
|
455
|
+
|
|
456
|
+
validator = CustomValidator(
|
|
457
|
+
name=name,
|
|
458
|
+
display_name=display_name,
|
|
459
|
+
description=description,
|
|
460
|
+
category=category,
|
|
461
|
+
code=code,
|
|
462
|
+
plugin_id=plugin_id,
|
|
463
|
+
**kwargs,
|
|
464
|
+
)
|
|
465
|
+
session.add(validator)
|
|
466
|
+
await session.flush()
|
|
467
|
+
|
|
468
|
+
# Update plugin validator count if applicable
|
|
469
|
+
if plugin_id:
|
|
470
|
+
plugin = await self.get_plugin(session, plugin_id=plugin_id)
|
|
471
|
+
if plugin:
|
|
472
|
+
plugin.validators_count += 1
|
|
473
|
+
|
|
474
|
+
self._validators[name] = validator
|
|
475
|
+
logger.info(f"Registered custom validator: {name}")
|
|
476
|
+
return validator
|
|
477
|
+
|
|
478
|
+
async def update_validator(
|
|
479
|
+
self,
|
|
480
|
+
session: AsyncSession,
|
|
481
|
+
validator_id: str,
|
|
482
|
+
**updates: Any,
|
|
483
|
+
) -> CustomValidator:
|
|
484
|
+
"""Update a custom validator.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
session: Database session.
|
|
488
|
+
validator_id: Validator ID.
|
|
489
|
+
**updates: Fields to update.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Updated validator.
|
|
493
|
+
"""
|
|
494
|
+
validator = await self.get_validator(session, validator_id=validator_id)
|
|
495
|
+
if not validator:
|
|
496
|
+
raise ValueError(f"Validator {validator_id} not found")
|
|
497
|
+
|
|
498
|
+
for key, value in updates.items():
|
|
499
|
+
if hasattr(validator, key) and value is not None:
|
|
500
|
+
setattr(validator, key, value)
|
|
501
|
+
|
|
502
|
+
await session.flush()
|
|
503
|
+
|
|
504
|
+
# Update cache
|
|
505
|
+
if validator.is_enabled:
|
|
506
|
+
self._validators[validator.name] = validator
|
|
507
|
+
elif validator.name in self._validators:
|
|
508
|
+
del self._validators[validator.name]
|
|
509
|
+
|
|
510
|
+
logger.info(f"Updated custom validator: {validator.name}")
|
|
511
|
+
return validator
|
|
512
|
+
|
|
513
|
+
async def delete_validator(
|
|
514
|
+
self,
|
|
515
|
+
session: AsyncSession,
|
|
516
|
+
validator_id: str,
|
|
517
|
+
) -> None:
|
|
518
|
+
"""Delete a custom validator.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
session: Database session.
|
|
522
|
+
validator_id: Validator ID.
|
|
523
|
+
"""
|
|
524
|
+
validator = await self.get_validator(session, validator_id=validator_id)
|
|
525
|
+
if not validator:
|
|
526
|
+
raise ValueError(f"Validator {validator_id} not found")
|
|
527
|
+
|
|
528
|
+
# Remove from cache
|
|
529
|
+
if validator.name in self._validators:
|
|
530
|
+
del self._validators[validator.name]
|
|
531
|
+
|
|
532
|
+
# Update plugin validator count if applicable
|
|
533
|
+
if validator.plugin_id:
|
|
534
|
+
plugin = await self.get_plugin(session, plugin_id=validator.plugin_id)
|
|
535
|
+
if plugin and plugin.validators_count > 0:
|
|
536
|
+
plugin.validators_count -= 1
|
|
537
|
+
|
|
538
|
+
await session.delete(validator)
|
|
539
|
+
await session.flush()
|
|
540
|
+
logger.info(f"Deleted custom validator: {validator.name}")
|
|
541
|
+
|
|
542
|
+
def get_cached_validator(self, name: str) -> CustomValidator | None:
|
|
543
|
+
"""Get a validator from cache by name.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
name: Validator name.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
CustomValidator if found in cache, None otherwise.
|
|
550
|
+
"""
|
|
551
|
+
return self._validators.get(name)
|
|
552
|
+
|
|
553
|
+
# =========================================================================
|
|
554
|
+
# Custom Reporter Management
|
|
555
|
+
# =========================================================================
|
|
556
|
+
|
|
557
|
+
async def list_reporters(
|
|
558
|
+
self,
|
|
559
|
+
session: AsyncSession,
|
|
560
|
+
plugin_id: str | None = None,
|
|
561
|
+
enabled_only: bool = False,
|
|
562
|
+
search: str | None = None,
|
|
563
|
+
offset: int = 0,
|
|
564
|
+
limit: int = 50,
|
|
565
|
+
) -> tuple[Sequence[CustomReporter], int]:
|
|
566
|
+
"""List custom reporters with optional filtering.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
session: Database session.
|
|
570
|
+
plugin_id: Filter by plugin.
|
|
571
|
+
enabled_only: Only return enabled reporters.
|
|
572
|
+
search: Search in name, display_name, description.
|
|
573
|
+
offset: Pagination offset.
|
|
574
|
+
limit: Pagination limit.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
Tuple of (reporters list, total count).
|
|
578
|
+
"""
|
|
579
|
+
stmt = select(CustomReporter)
|
|
580
|
+
|
|
581
|
+
if plugin_id:
|
|
582
|
+
stmt = stmt.where(CustomReporter.plugin_id == plugin_id)
|
|
583
|
+
if enabled_only:
|
|
584
|
+
stmt = stmt.where(CustomReporter.is_enabled == True) # noqa: E712
|
|
585
|
+
if search:
|
|
586
|
+
search_pattern = f"%{search}%"
|
|
587
|
+
stmt = stmt.where(
|
|
588
|
+
(CustomReporter.name.ilike(search_pattern))
|
|
589
|
+
| (CustomReporter.display_name.ilike(search_pattern))
|
|
590
|
+
| (CustomReporter.description.ilike(search_pattern))
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
# Get total count
|
|
594
|
+
count_result = await session.execute(
|
|
595
|
+
select(CustomReporter.id).select_from(stmt.subquery())
|
|
596
|
+
)
|
|
597
|
+
total = len(count_result.all())
|
|
598
|
+
|
|
599
|
+
# Apply pagination
|
|
600
|
+
stmt = stmt.offset(offset).limit(limit).order_by(CustomReporter.created_at.desc())
|
|
601
|
+
result = await session.execute(stmt)
|
|
602
|
+
reporters = result.scalars().all()
|
|
603
|
+
|
|
604
|
+
return reporters, total
|
|
605
|
+
|
|
606
|
+
async def get_reporter(
|
|
607
|
+
self,
|
|
608
|
+
session: AsyncSession,
|
|
609
|
+
reporter_id: str | None = None,
|
|
610
|
+
name: str | None = None,
|
|
611
|
+
) -> CustomReporter | None:
|
|
612
|
+
"""Get a custom reporter by ID or name.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
session: Database session.
|
|
616
|
+
reporter_id: Reporter ID.
|
|
617
|
+
name: Reporter name.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
CustomReporter if found, None otherwise.
|
|
621
|
+
"""
|
|
622
|
+
if reporter_id:
|
|
623
|
+
stmt = select(CustomReporter).where(CustomReporter.id == reporter_id)
|
|
624
|
+
elif name:
|
|
625
|
+
stmt = select(CustomReporter).where(CustomReporter.name == name)
|
|
626
|
+
else:
|
|
627
|
+
return None
|
|
628
|
+
|
|
629
|
+
result = await session.execute(stmt)
|
|
630
|
+
return result.scalar_one_or_none()
|
|
631
|
+
|
|
632
|
+
async def register_reporter(
|
|
633
|
+
self,
|
|
634
|
+
session: AsyncSession,
|
|
635
|
+
name: str,
|
|
636
|
+
display_name: str,
|
|
637
|
+
description: str,
|
|
638
|
+
plugin_id: str | None = None,
|
|
639
|
+
**kwargs: Any,
|
|
640
|
+
) -> CustomReporter:
|
|
641
|
+
"""Register a new custom reporter.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
session: Database session.
|
|
645
|
+
name: Reporter name (unique).
|
|
646
|
+
display_name: Display name.
|
|
647
|
+
description: Reporter description.
|
|
648
|
+
plugin_id: Optional parent plugin ID.
|
|
649
|
+
**kwargs: Additional reporter attributes.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Created reporter.
|
|
653
|
+
|
|
654
|
+
Raises:
|
|
655
|
+
ValueError: If reporter with same name exists.
|
|
656
|
+
"""
|
|
657
|
+
existing = await self.get_reporter(session, name=name)
|
|
658
|
+
if existing:
|
|
659
|
+
raise ValueError(f"Reporter with name '{name}' already exists")
|
|
660
|
+
|
|
661
|
+
reporter = CustomReporter(
|
|
662
|
+
name=name,
|
|
663
|
+
display_name=display_name,
|
|
664
|
+
description=description,
|
|
665
|
+
plugin_id=plugin_id,
|
|
666
|
+
**kwargs,
|
|
667
|
+
)
|
|
668
|
+
session.add(reporter)
|
|
669
|
+
await session.flush()
|
|
670
|
+
|
|
671
|
+
# Update plugin reporter count if applicable
|
|
672
|
+
if plugin_id:
|
|
673
|
+
plugin = await self.get_plugin(session, plugin_id=plugin_id)
|
|
674
|
+
if plugin:
|
|
675
|
+
plugin.reporters_count += 1
|
|
676
|
+
|
|
677
|
+
self._reporters[name] = reporter
|
|
678
|
+
logger.info(f"Registered custom reporter: {name}")
|
|
679
|
+
return reporter
|
|
680
|
+
|
|
681
|
+
async def update_reporter(
|
|
682
|
+
self,
|
|
683
|
+
session: AsyncSession,
|
|
684
|
+
reporter_id: str,
|
|
685
|
+
**updates: Any,
|
|
686
|
+
) -> CustomReporter:
|
|
687
|
+
"""Update a custom reporter.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
session: Database session.
|
|
691
|
+
reporter_id: Reporter ID.
|
|
692
|
+
**updates: Fields to update.
|
|
693
|
+
|
|
694
|
+
Returns:
|
|
695
|
+
Updated reporter.
|
|
696
|
+
"""
|
|
697
|
+
reporter = await self.get_reporter(session, reporter_id=reporter_id)
|
|
698
|
+
if not reporter:
|
|
699
|
+
raise ValueError(f"Reporter {reporter_id} not found")
|
|
700
|
+
|
|
701
|
+
for key, value in updates.items():
|
|
702
|
+
if hasattr(reporter, key) and value is not None:
|
|
703
|
+
setattr(reporter, key, value)
|
|
704
|
+
|
|
705
|
+
await session.flush()
|
|
706
|
+
|
|
707
|
+
# Update cache
|
|
708
|
+
if reporter.is_enabled:
|
|
709
|
+
self._reporters[reporter.name] = reporter
|
|
710
|
+
elif reporter.name in self._reporters:
|
|
711
|
+
del self._reporters[reporter.name]
|
|
712
|
+
|
|
713
|
+
logger.info(f"Updated custom reporter: {reporter.name}")
|
|
714
|
+
return reporter
|
|
715
|
+
|
|
716
|
+
async def delete_reporter(
|
|
717
|
+
self,
|
|
718
|
+
session: AsyncSession,
|
|
719
|
+
reporter_id: str,
|
|
720
|
+
) -> None:
|
|
721
|
+
"""Delete a custom reporter.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
session: Database session.
|
|
725
|
+
reporter_id: Reporter ID.
|
|
726
|
+
"""
|
|
727
|
+
reporter = await self.get_reporter(session, reporter_id=reporter_id)
|
|
728
|
+
if not reporter:
|
|
729
|
+
raise ValueError(f"Reporter {reporter_id} not found")
|
|
730
|
+
|
|
731
|
+
# Remove from cache
|
|
732
|
+
if reporter.name in self._reporters:
|
|
733
|
+
del self._reporters[reporter.name]
|
|
734
|
+
|
|
735
|
+
# Update plugin reporter count if applicable
|
|
736
|
+
if reporter.plugin_id:
|
|
737
|
+
plugin = await self.get_plugin(session, plugin_id=reporter.plugin_id)
|
|
738
|
+
if plugin and plugin.reporters_count > 0:
|
|
739
|
+
plugin.reporters_count -= 1
|
|
740
|
+
|
|
741
|
+
await session.delete(reporter)
|
|
742
|
+
await session.flush()
|
|
743
|
+
logger.info(f"Deleted custom reporter: {reporter.name}")
|
|
744
|
+
|
|
745
|
+
def get_cached_reporter(self, name: str) -> CustomReporter | None:
|
|
746
|
+
"""Get a reporter from cache by name.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
name: Reporter name.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
CustomReporter if found in cache, None otherwise.
|
|
753
|
+
"""
|
|
754
|
+
return self._reporters.get(name)
|
|
755
|
+
|
|
756
|
+
# =========================================================================
|
|
757
|
+
# Statistics
|
|
758
|
+
# =========================================================================
|
|
759
|
+
|
|
760
|
+
async def get_statistics(self, session: AsyncSession) -> dict[str, Any]:
|
|
761
|
+
"""Get plugin system statistics.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
session: Database session.
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Dictionary of statistics.
|
|
768
|
+
"""
|
|
769
|
+
# Count plugins by type
|
|
770
|
+
plugins_result = await session.execute(select(Plugin))
|
|
771
|
+
plugins = plugins_result.scalars().all()
|
|
772
|
+
|
|
773
|
+
total_plugins = len(plugins)
|
|
774
|
+
installed = sum(1 for p in plugins if p.status in (PluginStatus.INSTALLED.value, PluginStatus.ENABLED.value))
|
|
775
|
+
enabled = sum(1 for p in plugins if p.is_enabled)
|
|
776
|
+
|
|
777
|
+
by_type = {}
|
|
778
|
+
for ptype in PluginType:
|
|
779
|
+
by_type[ptype.value] = sum(1 for p in plugins if p.type == ptype.value)
|
|
780
|
+
|
|
781
|
+
# Count validators and reporters
|
|
782
|
+
validators_result = await session.execute(select(CustomValidator))
|
|
783
|
+
validators = validators_result.scalars().all()
|
|
784
|
+
|
|
785
|
+
reporters_result = await session.execute(select(CustomReporter))
|
|
786
|
+
reporters = reporters_result.scalars().all()
|
|
787
|
+
|
|
788
|
+
# Get categories
|
|
789
|
+
categories = set()
|
|
790
|
+
for v in validators:
|
|
791
|
+
if v.category:
|
|
792
|
+
categories.add(v.category)
|
|
793
|
+
|
|
794
|
+
return {
|
|
795
|
+
"total_plugins": total_plugins,
|
|
796
|
+
"installed_plugins": installed,
|
|
797
|
+
"enabled_plugins": enabled,
|
|
798
|
+
"plugins_by_type": by_type,
|
|
799
|
+
"total_validators": len(validators),
|
|
800
|
+
"enabled_validators": sum(1 for v in validators if v.is_enabled),
|
|
801
|
+
"total_reporters": len(reporters),
|
|
802
|
+
"enabled_reporters": sum(1 for r in reporters if r.is_enabled),
|
|
803
|
+
"validator_categories": sorted(list(categories)),
|
|
804
|
+
"cached_validators": len(self._validators),
|
|
805
|
+
"cached_reporters": len(self._reporters),
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
# Global registry instance
|
|
810
|
+
plugin_registry = PluginRegistry()
|