truthound-dashboard 1.3.0__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.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
  162. truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
  164. truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
  166. truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,588 @@
1
+ """Custom Reporter Executor.
2
+
3
+ This module provides execution of custom reporters for
4
+ generating various report formats.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ import json
11
+ import logging
12
+ import uuid
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime
15
+ from typing import Any
16
+
17
+ from sqlalchemy.ext.asyncio import AsyncSession
18
+
19
+ from truthound_dashboard.db.models import CustomReporter, PluginExecutionLog
20
+
21
+ from .sandbox import create_sandbox, SandboxConfig
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class ReportContext:
28
+ """Context provided to custom reporters.
29
+
30
+ Attributes:
31
+ data: Data to include in the report.
32
+ config: Reporter configuration.
33
+ format: Output format requested.
34
+ metadata: Additional metadata.
35
+ """
36
+
37
+ data: dict[str, Any]
38
+ config: dict[str, Any] = field(default_factory=dict)
39
+ format: str = "html"
40
+ metadata: dict[str, Any] = field(default_factory=dict)
41
+
42
+ def to_dict(self) -> dict[str, Any]:
43
+ """Convert to dictionary for execution."""
44
+ return {
45
+ "data": self.data,
46
+ "config": self.config,
47
+ "format": self.format,
48
+ "metadata": self.metadata,
49
+ }
50
+
51
+
52
+ @dataclass
53
+ class ReportResult:
54
+ """Result of custom reporter execution.
55
+
56
+ Attributes:
57
+ success: Whether report generation succeeded.
58
+ content: Generated report content.
59
+ content_type: MIME type of the content.
60
+ filename: Suggested filename.
61
+ error: Error message if failed.
62
+ execution_time_ms: Execution time in milliseconds.
63
+ """
64
+
65
+ success: bool
66
+ content: str | bytes = ""
67
+ content_type: str = "text/html"
68
+ filename: str = "report.html"
69
+ error: str | None = None
70
+ execution_time_ms: float = 0
71
+
72
+
73
+ # Template code for Jinja2-based reporters
74
+ JINJA2_REPORTER_CODE = '''
75
+ import json
76
+ from datetime import datetime
77
+
78
+ def generate_report(data, config, format, metadata):
79
+ """Generate report using Jinja2 template.
80
+
81
+ Args:
82
+ data: Report data.
83
+ config: Reporter configuration.
84
+ format: Output format.
85
+ metadata: Report metadata.
86
+
87
+ Returns:
88
+ Dictionary with 'content', 'content_type', 'filename'.
89
+ """
90
+ # Template will be inserted here
91
+ template = """{template}"""
92
+
93
+ # Simple template rendering (subset of Jinja2)
94
+ result = template
95
+
96
+ # Replace simple variables
97
+ for key, value in data.items():
98
+ placeholder = "{{{{ {key} }}}}".format(key=key)
99
+ if placeholder in result:
100
+ result = result.replace(placeholder, str(value))
101
+
102
+ # Replace metadata
103
+ for key, value in metadata.items():
104
+ placeholder = "{{{{ metadata.{key} }}}}".format(key=key)
105
+ if placeholder in result:
106
+ result = result.replace(placeholder, str(value))
107
+
108
+ # Replace config
109
+ for key, value in config.items():
110
+ placeholder = "{{{{ config.{key} }}}}".format(key=key)
111
+ if placeholder in result:
112
+ result = result.replace(placeholder, str(value))
113
+
114
+ # Determine content type
115
+ content_type = "text/html"
116
+ filename = "report.html"
117
+
118
+ if format == "json":
119
+ result = json.dumps(data, indent=2, default=str)
120
+ content_type = "application/json"
121
+ filename = "report.json"
122
+ elif format == "markdown":
123
+ content_type = "text/markdown"
124
+ filename = "report.md"
125
+ elif format == "csv":
126
+ content_type = "text/csv"
127
+ filename = "report.csv"
128
+
129
+ return {{
130
+ "content": result,
131
+ "content_type": content_type,
132
+ "filename": filename
133
+ }}
134
+ '''
135
+
136
+ # Template code for code-based reporters
137
+ CODE_REPORTER_WRAPPER = '''
138
+ import json
139
+ from datetime import datetime
140
+ from collections import Counter
141
+
142
+ # User-provided reporter code
143
+ {user_code}
144
+
145
+ def _execute_reporter(data, config, format, metadata):
146
+ """Execute the custom reporter.
147
+
148
+ Args:
149
+ data: Report data.
150
+ config: Reporter configuration.
151
+ format: Output format.
152
+ metadata: Report metadata.
153
+
154
+ Returns:
155
+ Dictionary with 'content', 'content_type', 'filename'.
156
+ """
157
+ result = generate_report(data, config, format, metadata)
158
+
159
+ if isinstance(result, str):
160
+ return {{
161
+ "content": result,
162
+ "content_type": "text/html",
163
+ "filename": "report.html"
164
+ }}
165
+ elif isinstance(result, dict):
166
+ return {{
167
+ "content": result.get("content", ""),
168
+ "content_type": result.get("content_type", "text/html"),
169
+ "filename": result.get("filename", "report.html")
170
+ }}
171
+ else:
172
+ return {{
173
+ "content": str(result),
174
+ "content_type": "text/plain",
175
+ "filename": "report.txt"
176
+ }}
177
+ '''
178
+
179
+
180
+ class CustomReporterExecutor:
181
+ """Executor for custom reporters.
182
+
183
+ This class handles the execution of custom reporters
184
+ for generating various report formats.
185
+
186
+ Attributes:
187
+ sandbox: Plugin sandbox for secure execution.
188
+ log_executions: Whether to log executions.
189
+ """
190
+
191
+ def __init__(
192
+ self,
193
+ sandbox_config: SandboxConfig | None = None,
194
+ log_executions: bool = True,
195
+ ) -> None:
196
+ """Initialize the executor.
197
+
198
+ Args:
199
+ sandbox_config: Sandbox configuration.
200
+ log_executions: Whether to log executions.
201
+ """
202
+ self.sandbox = create_sandbox(sandbox_config or SandboxConfig())
203
+ self.log_executions = log_executions
204
+
205
+ def validate_reporter_code(self, code: str) -> tuple[bool, list[str]]:
206
+ """Validate custom reporter code.
207
+
208
+ Args:
209
+ code: Python code implementing the reporter.
210
+
211
+ Returns:
212
+ Tuple of (is_valid, list of issues).
213
+ """
214
+ issues = []
215
+
216
+ # Check for required function
217
+ if "def generate_report(" not in code:
218
+ issues.append("Missing required 'generate_report' function")
219
+
220
+ # Check for dangerous patterns
221
+ dangerous = [
222
+ "os.system",
223
+ "subprocess",
224
+ "__import__",
225
+ "open(",
226
+ "file(",
227
+ ]
228
+ for pattern in dangerous:
229
+ if pattern in code:
230
+ issues.append(f"Dangerous pattern detected: {pattern}")
231
+
232
+ # Analyze with sandbox
233
+ sandbox_issues, _ = self.sandbox.analyze_code(code)
234
+ issues.extend(sandbox_issues)
235
+
236
+ return len(issues) == 0, issues
237
+
238
+ def validate_template(self, template: str) -> tuple[bool, list[str]]:
239
+ """Validate Jinja2 template.
240
+
241
+ Args:
242
+ template: Jinja2 template string.
243
+
244
+ Returns:
245
+ Tuple of (is_valid, list of issues).
246
+ """
247
+ issues = []
248
+
249
+ # Check for potentially dangerous Jinja2 constructs
250
+ dangerous_patterns = [
251
+ "{% import",
252
+ "{% include",
253
+ "{{ self",
254
+ "{{ config.__",
255
+ "{{ request",
256
+ "{{ session",
257
+ ]
258
+
259
+ for pattern in dangerous_patterns:
260
+ if pattern in template:
261
+ issues.append(f"Dangerous template pattern: {pattern}")
262
+
263
+ return len(issues) == 0, issues
264
+
265
+ async def execute(
266
+ self,
267
+ reporter: CustomReporter,
268
+ context: ReportContext,
269
+ session: AsyncSession | None = None,
270
+ source_id: str | None = None,
271
+ ) -> ReportResult:
272
+ """Execute a custom reporter.
273
+
274
+ Args:
275
+ reporter: CustomReporter model.
276
+ context: Report context.
277
+ session: Optional database session for logging.
278
+ source_id: Optional source ID for logging.
279
+
280
+ Returns:
281
+ ReportResult with generated report.
282
+ """
283
+ execution_id = str(uuid.uuid4())
284
+ start_time = datetime.utcnow()
285
+
286
+ logger.debug(f"Executing custom reporter: {reporter.name}")
287
+
288
+ # Create execution log if logging enabled
289
+ log_entry = None
290
+ if self.log_executions and session and reporter.plugin_id:
291
+ log_entry = PluginExecutionLog(
292
+ plugin_id=reporter.plugin_id,
293
+ reporter_id=str(reporter.id),
294
+ execution_id=execution_id,
295
+ source_id=source_id,
296
+ status="running",
297
+ )
298
+ session.add(log_entry)
299
+ await session.flush()
300
+
301
+ try:
302
+ # Determine execution method
303
+ if reporter.template:
304
+ result = await self._execute_template(reporter.template, context)
305
+ elif reporter.code:
306
+ result = await self._execute_code(reporter.code, context)
307
+ else:
308
+ result = ReportResult(
309
+ success=False,
310
+ error="Reporter has no template or code",
311
+ )
312
+
313
+ # Update reporter usage stats
314
+ reporter.increment_usage()
315
+
316
+ # Update execution log
317
+ if log_entry:
318
+ if result.success:
319
+ log_entry.mark_completed(
320
+ result={
321
+ "filename": result.filename,
322
+ "content_type": result.content_type,
323
+ "size": len(result.content) if result.content else 0,
324
+ }
325
+ )
326
+ else:
327
+ log_entry.mark_failed(result.error or "Unknown error")
328
+
329
+ if session:
330
+ await session.flush()
331
+
332
+ return result
333
+
334
+ except Exception as e:
335
+ logger.error(f"Custom reporter execution failed: {e}")
336
+
337
+ if log_entry:
338
+ log_entry.mark_failed(str(e))
339
+ if session:
340
+ await session.flush()
341
+
342
+ return ReportResult(
343
+ success=False,
344
+ error=f"Execution error: {e}",
345
+ )
346
+
347
+ async def _execute_template(
348
+ self,
349
+ template: str,
350
+ context: ReportContext,
351
+ ) -> ReportResult:
352
+ """Execute a Jinja2 template reporter.
353
+
354
+ Args:
355
+ template: Jinja2 template string.
356
+ context: Report context.
357
+
358
+ Returns:
359
+ ReportResult.
360
+ """
361
+ # Validate template
362
+ is_valid, issues = self.validate_template(template)
363
+ if not is_valid:
364
+ return ReportResult(
365
+ success=False,
366
+ error=f"Template validation failed: {'; '.join(issues)}",
367
+ )
368
+
369
+ # Prepare code with template
370
+ code = JINJA2_REPORTER_CODE.format(template=template.replace('"', '\\"'))
371
+
372
+ sandbox_result = self.sandbox.execute(
373
+ code=code,
374
+ entry_point="generate_report",
375
+ entry_args=context.to_dict(),
376
+ )
377
+
378
+ if sandbox_result.success and sandbox_result.result:
379
+ return ReportResult(
380
+ success=True,
381
+ content=sandbox_result.result.get("content", ""),
382
+ content_type=sandbox_result.result.get("content_type", "text/html"),
383
+ filename=sandbox_result.result.get("filename", "report.html"),
384
+ execution_time_ms=sandbox_result.execution_time_ms,
385
+ )
386
+ else:
387
+ return ReportResult(
388
+ success=False,
389
+ error=sandbox_result.error,
390
+ execution_time_ms=sandbox_result.execution_time_ms,
391
+ )
392
+
393
+ async def _execute_code(
394
+ self,
395
+ code: str,
396
+ context: ReportContext,
397
+ ) -> ReportResult:
398
+ """Execute a code-based reporter.
399
+
400
+ Args:
401
+ code: Python code.
402
+ context: Report context.
403
+
404
+ Returns:
405
+ ReportResult.
406
+ """
407
+ # Validate code
408
+ is_valid, issues = self.validate_reporter_code(code)
409
+ if not is_valid:
410
+ return ReportResult(
411
+ success=False,
412
+ error=f"Code validation failed: {'; '.join(issues)}",
413
+ )
414
+
415
+ # Wrap code
416
+ wrapped_code = CODE_REPORTER_WRAPPER.format(user_code=code)
417
+
418
+ sandbox_result = self.sandbox.execute(
419
+ code=wrapped_code,
420
+ entry_point="_execute_reporter",
421
+ entry_args=context.to_dict(),
422
+ )
423
+
424
+ if sandbox_result.success and sandbox_result.result:
425
+ return ReportResult(
426
+ success=True,
427
+ content=sandbox_result.result.get("content", ""),
428
+ content_type=sandbox_result.result.get("content_type", "text/html"),
429
+ filename=sandbox_result.result.get("filename", "report.html"),
430
+ execution_time_ms=sandbox_result.execution_time_ms,
431
+ )
432
+ else:
433
+ return ReportResult(
434
+ success=False,
435
+ error=sandbox_result.error,
436
+ execution_time_ms=sandbox_result.execution_time_ms,
437
+ )
438
+
439
+ async def preview_report(
440
+ self,
441
+ template: str | None = None,
442
+ code: str | None = None,
443
+ sample_data: dict[str, Any] | None = None,
444
+ config: dict[str, Any] | None = None,
445
+ format: str = "html",
446
+ ) -> ReportResult:
447
+ """Preview a report without saving.
448
+
449
+ Args:
450
+ template: Jinja2 template.
451
+ code: Python code.
452
+ sample_data: Sample data for preview.
453
+ config: Reporter configuration.
454
+ format: Output format.
455
+
456
+ Returns:
457
+ ReportResult with preview.
458
+ """
459
+ context = ReportContext(
460
+ data=sample_data or {"message": "Sample report data"},
461
+ config=config or {},
462
+ format=format,
463
+ metadata={
464
+ "generated_at": datetime.utcnow().isoformat(),
465
+ "is_preview": True,
466
+ },
467
+ )
468
+
469
+ if template:
470
+ return await self._execute_template(template, context)
471
+ elif code:
472
+ return await self._execute_code(code, context)
473
+ else:
474
+ return ReportResult(
475
+ success=False,
476
+ error="No template or code provided",
477
+ )
478
+
479
+ def get_reporter_template(self) -> str:
480
+ """Get a template for creating custom reporters.
481
+
482
+ Returns:
483
+ Template code string.
484
+ """
485
+ return '''
486
+ def generate_report(data, config, format, metadata):
487
+ """Custom report generator function.
488
+
489
+ Args:
490
+ data: Dictionary of data to include in report.
491
+ config: Dictionary of configuration options.
492
+ format: Output format ('html', 'json', 'markdown', 'csv').
493
+ metadata: Dictionary of metadata (generated_at, etc.).
494
+
495
+ Returns:
496
+ Dictionary with:
497
+ - content: str - Generated report content
498
+ - content_type: str - MIME type
499
+ - filename: str - Suggested filename
500
+ """
501
+ # Generate HTML report
502
+ html = f"""
503
+ <!DOCTYPE html>
504
+ <html>
505
+ <head>
506
+ <title>Validation Report</title>
507
+ <style>
508
+ body {{ font-family: Arial, sans-serif; margin: 20px; }}
509
+ h1 {{ color: #333; }}
510
+ .summary {{ background: #f5f5f5; padding: 15px; border-radius: 5px; }}
511
+ .issues {{ margin-top: 20px; }}
512
+ .issue {{ padding: 10px; margin: 5px 0; border-left: 3px solid #fd9e4b; }}
513
+ </style>
514
+ </head>
515
+ <body>
516
+ <h1>Validation Report</h1>
517
+ <div class="summary">
518
+ <p>Generated: {metadata.get('generated_at', 'Unknown')}</p>
519
+ <p>Total Issues: {len(data.get('issues', []))}</p>
520
+ </div>
521
+ <div class="issues">
522
+ <h2>Issues</h2>
523
+ {''.join(f'<div class="issue">{issue}</div>' for issue in data.get('issues', []))}
524
+ </div>
525
+ </body>
526
+ </html>
527
+ """
528
+
529
+ return {
530
+ "content": html,
531
+ "content_type": "text/html",
532
+ "filename": "validation_report.html"
533
+ }
534
+ '''
535
+
536
+ def get_jinja2_template(self) -> str:
537
+ """Get a Jinja2 template example.
538
+
539
+ Returns:
540
+ Jinja2 template string.
541
+ """
542
+ return '''<!DOCTYPE html>
543
+ <html>
544
+ <head>
545
+ <title>{{ title }}</title>
546
+ <style>
547
+ body { font-family: Arial, sans-serif; margin: 20px; }
548
+ h1 { color: #fd9e4b; }
549
+ .card { background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 10px 0; }
550
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; }
551
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
552
+ th { background: #fd9e4b; color: white; }
553
+ </style>
554
+ </head>
555
+ <body>
556
+ <h1>{{ title }}</h1>
557
+
558
+ <div class="card">
559
+ <h3>Summary</h3>
560
+ <p>Generated: {{ metadata.generated_at }}</p>
561
+ <p>Source: {{ source_name }}</p>
562
+ <p>Status: {{ status }}</p>
563
+ </div>
564
+
565
+ <table>
566
+ <thead>
567
+ <tr>
568
+ <th>Column</th>
569
+ <th>Issue</th>
570
+ <th>Severity</th>
571
+ </tr>
572
+ </thead>
573
+ <tbody>
574
+ {% for issue in issues %}
575
+ <tr>
576
+ <td>{{ issue.column }}</td>
577
+ <td>{{ issue.message }}</td>
578
+ <td>{{ issue.severity }}</td>
579
+ </tr>
580
+ {% endfor %}
581
+ </tbody>
582
+ </table>
583
+ </body>
584
+ </html>'''
585
+
586
+
587
+ # Default executor instance
588
+ reporter_executor = CustomReporterExecutor()
@@ -0,0 +1,59 @@
1
+ """Plugin Sandbox Module.
2
+
3
+ This module provides sandboxed execution environments with multiple
4
+ isolation levels for secure plugin code execution.
5
+
6
+ Isolation Levels:
7
+ - NONE: No isolation (trusted plugins only)
8
+ - PROCESS: Subprocess isolation with resource limits
9
+ - CONTAINER: Docker/Podman container isolation
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from .protocols import (
15
+ SandboxResult,
16
+ SandboxConfig,
17
+ SandboxError,
18
+ SandboxTimeoutError,
19
+ SandboxMemoryError,
20
+ SandboxSecurityError,
21
+ )
22
+ from .engines import (
23
+ SandboxEngine,
24
+ NoOpSandbox,
25
+ ProcessSandbox,
26
+ ContainerSandbox,
27
+ create_sandbox,
28
+ get_available_engines,
29
+ )
30
+ from .code_validator import (
31
+ CodeValidator,
32
+ RestrictedImporter,
33
+ create_safe_builtins,
34
+ )
35
+
36
+ # Alias for backward compatibility
37
+ PluginSandbox = SandboxEngine
38
+
39
+ __all__ = [
40
+ # Protocols
41
+ "SandboxResult",
42
+ "SandboxConfig",
43
+ "SandboxError",
44
+ "SandboxTimeoutError",
45
+ "SandboxMemoryError",
46
+ "SandboxSecurityError",
47
+ # Engines
48
+ "SandboxEngine",
49
+ "PluginSandbox", # Alias
50
+ "NoOpSandbox",
51
+ "ProcessSandbox",
52
+ "ContainerSandbox",
53
+ "create_sandbox",
54
+ "get_available_engines",
55
+ # Validation
56
+ "CodeValidator",
57
+ "RestrictedImporter",
58
+ "create_safe_builtins",
59
+ ]