truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__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.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
  162. truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,804 @@
1
+ """Documentation Renderers.
2
+
3
+ This module provides renderers for converting extracted documentation
4
+ into various output formats (Markdown, HTML, JSON).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import html
10
+ import json
11
+ import logging
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass, field
14
+ from typing import Any
15
+
16
+ from .extractor import (
17
+ ModuleDoc,
18
+ ClassDoc,
19
+ FunctionDoc,
20
+ ParameterDoc,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class RenderOptions:
28
+ """Options for documentation rendering.
29
+
30
+ Attributes:
31
+ include_private: Include private members (starting with _).
32
+ include_source: Include source code snippets.
33
+ include_examples: Include usage examples.
34
+ include_deprecation: Include deprecation notices.
35
+ max_depth: Maximum nesting depth for classes/methods.
36
+ toc: Generate table of contents.
37
+ syntax_highlight: Enable syntax highlighting (HTML).
38
+ css_class_prefix: CSS class prefix for HTML output.
39
+ """
40
+
41
+ include_private: bool = False
42
+ include_source: bool = True
43
+ include_examples: bool = True
44
+ include_deprecation: bool = True
45
+ max_depth: int = 3
46
+ toc: bool = True
47
+ syntax_highlight: bool = True
48
+ css_class_prefix: str = "plugin-doc"
49
+
50
+
51
+ class DocumentationRenderer(ABC):
52
+ """Abstract base class for documentation renderers."""
53
+
54
+ def __init__(self, options: RenderOptions | None = None) -> None:
55
+ """Initialize the renderer.
56
+
57
+ Args:
58
+ options: Rendering options.
59
+ """
60
+ self.options = options or RenderOptions()
61
+
62
+ @abstractmethod
63
+ def render_module(self, doc: ModuleDoc) -> str:
64
+ """Render module documentation.
65
+
66
+ Args:
67
+ doc: Module documentation.
68
+
69
+ Returns:
70
+ Rendered documentation string.
71
+ """
72
+ pass
73
+
74
+ @abstractmethod
75
+ def render_class(self, doc: ClassDoc, depth: int = 0) -> str:
76
+ """Render class documentation.
77
+
78
+ Args:
79
+ doc: Class documentation.
80
+ depth: Current nesting depth.
81
+
82
+ Returns:
83
+ Rendered documentation string.
84
+ """
85
+ pass
86
+
87
+ @abstractmethod
88
+ def render_function(self, doc: FunctionDoc, depth: int = 0) -> str:
89
+ """Render function documentation.
90
+
91
+ Args:
92
+ doc: Function documentation.
93
+ depth: Current nesting depth.
94
+
95
+ Returns:
96
+ Rendered documentation string.
97
+ """
98
+ pass
99
+
100
+ def _should_include(self, name: str) -> bool:
101
+ """Check if a member should be included.
102
+
103
+ Args:
104
+ name: Member name.
105
+
106
+ Returns:
107
+ True if should be included.
108
+ """
109
+ if name.startswith("__") and name.endswith("__"):
110
+ # Always include dunder methods
111
+ return True
112
+ if name.startswith("_"):
113
+ return self.options.include_private
114
+ return True
115
+
116
+
117
+ class MarkdownRenderer(DocumentationRenderer):
118
+ """Renders documentation as Markdown."""
119
+
120
+ def render_module(self, doc: ModuleDoc) -> str:
121
+ """Render module documentation as Markdown.
122
+
123
+ Args:
124
+ doc: Module documentation.
125
+
126
+ Returns:
127
+ Markdown string.
128
+ """
129
+ lines: list[str] = []
130
+
131
+ # Module header
132
+ lines.append(f"# {doc.name}")
133
+ lines.append("")
134
+
135
+ if doc.description:
136
+ lines.append(doc.description)
137
+ lines.append("")
138
+
139
+ # Table of contents
140
+ if self.options.toc and (doc.classes or doc.functions):
141
+ lines.append("## Table of Contents")
142
+ lines.append("")
143
+
144
+ if doc.classes:
145
+ lines.append("### Classes")
146
+ lines.append("")
147
+ for cls in doc.classes:
148
+ if self._should_include(cls.name):
149
+ lines.append(f"- [{cls.name}](#{cls.name.lower()})")
150
+ lines.append("")
151
+
152
+ if doc.functions:
153
+ lines.append("### Functions")
154
+ lines.append("")
155
+ for func in doc.functions:
156
+ if self._should_include(func.name):
157
+ lines.append(f"- [{func.name}](#{func.name.lower()})")
158
+ lines.append("")
159
+
160
+ # Classes
161
+ if doc.classes:
162
+ lines.append("## Classes")
163
+ lines.append("")
164
+ for cls in doc.classes:
165
+ if self._should_include(cls.name):
166
+ lines.append(self.render_class(cls, depth=0))
167
+ lines.append("")
168
+
169
+ # Functions
170
+ if doc.functions:
171
+ lines.append("## Functions")
172
+ lines.append("")
173
+ for func in doc.functions:
174
+ if self._should_include(func.name):
175
+ lines.append(self.render_function(func, depth=0))
176
+ lines.append("")
177
+
178
+ return "\n".join(lines)
179
+
180
+ def render_class(self, doc: ClassDoc, depth: int = 0) -> str:
181
+ """Render class documentation as Markdown.
182
+
183
+ Args:
184
+ doc: Class documentation.
185
+ depth: Current nesting depth.
186
+
187
+ Returns:
188
+ Markdown string.
189
+ """
190
+ if depth > self.options.max_depth:
191
+ return ""
192
+
193
+ lines: list[str] = []
194
+ header_level = "###" + "#" * depth
195
+
196
+ # Class header
197
+ lines.append(f"{header_level} {doc.name}")
198
+ lines.append("")
199
+
200
+ # Inheritance
201
+ if doc.bases:
202
+ bases_str = ", ".join(doc.bases)
203
+ lines.append(f"**Inherits from:** `{bases_str}`")
204
+ lines.append("")
205
+
206
+ # Description
207
+ if doc.description:
208
+ lines.append(doc.description)
209
+ lines.append("")
210
+
211
+ # Deprecation warning
212
+ if self.options.include_deprecation and doc.deprecated:
213
+ lines.append("> **Deprecated:** This class is deprecated.")
214
+ lines.append("")
215
+
216
+ # Constructor
217
+ if doc.init_params:
218
+ lines.append(f"{header_level}# Constructor")
219
+ lines.append("")
220
+ lines.append("**Parameters:**")
221
+ lines.append("")
222
+ for param in doc.init_params:
223
+ lines.append(self._render_parameter(param))
224
+ lines.append("")
225
+
226
+ # Attributes
227
+ if doc.attributes:
228
+ lines.append(f"{header_level}# Attributes")
229
+ lines.append("")
230
+ for attr_name, attr_info in doc.attributes.items():
231
+ if self._should_include(attr_name):
232
+ type_str = f": `{attr_info['type']}`" if attr_info.get("type") else ""
233
+ desc = attr_info.get("description", "")
234
+ lines.append(f"- **{attr_name}**{type_str} - {desc}")
235
+ lines.append("")
236
+
237
+ # Methods
238
+ if doc.methods:
239
+ lines.append(f"{header_level}# Methods")
240
+ lines.append("")
241
+ for method in doc.methods:
242
+ if self._should_include(method.name):
243
+ lines.append(self.render_function(method, depth=depth + 1))
244
+ lines.append("")
245
+
246
+ # Examples
247
+ if self.options.include_examples and doc.examples:
248
+ lines.append(f"{header_level}# Examples")
249
+ lines.append("")
250
+ for example in doc.examples:
251
+ lines.append("```python")
252
+ lines.append(example)
253
+ lines.append("```")
254
+ lines.append("")
255
+
256
+ return "\n".join(lines)
257
+
258
+ def render_function(self, doc: FunctionDoc, depth: int = 0) -> str:
259
+ """Render function documentation as Markdown.
260
+
261
+ Args:
262
+ doc: Function documentation.
263
+ depth: Current nesting depth.
264
+
265
+ Returns:
266
+ Markdown string.
267
+ """
268
+ if depth > self.options.max_depth:
269
+ return ""
270
+
271
+ lines: list[str] = []
272
+ header_level = "####" + "#" * depth
273
+
274
+ # Function signature
275
+ params_str = ", ".join(
276
+ f"{p.name}: {p.type}" if p.type else p.name
277
+ for p in doc.parameters
278
+ )
279
+ return_str = f" -> {doc.return_type}" if doc.return_type else ""
280
+ async_prefix = "async " if doc.is_async else ""
281
+
282
+ lines.append(f"{header_level} `{async_prefix}{doc.name}({params_str}){return_str}`")
283
+ lines.append("")
284
+
285
+ # Deprecation warning
286
+ if self.options.include_deprecation and doc.deprecated:
287
+ lines.append("> **Deprecated:** This function is deprecated.")
288
+ lines.append("")
289
+
290
+ # Description
291
+ if doc.description:
292
+ lines.append(doc.description)
293
+ lines.append("")
294
+
295
+ # Parameters
296
+ if doc.parameters:
297
+ lines.append("**Parameters:**")
298
+ lines.append("")
299
+ for param in doc.parameters:
300
+ lines.append(self._render_parameter(param))
301
+ lines.append("")
302
+
303
+ # Returns
304
+ if doc.return_type or doc.return_description:
305
+ lines.append("**Returns:**")
306
+ lines.append("")
307
+ return_type = f"`{doc.return_type}`" if doc.return_type else "Any"
308
+ return_desc = doc.return_description or ""
309
+ lines.append(f"- {return_type} - {return_desc}")
310
+ lines.append("")
311
+
312
+ # Raises
313
+ if doc.raises:
314
+ lines.append("**Raises:**")
315
+ lines.append("")
316
+ for exc_type, exc_desc in doc.raises.items():
317
+ lines.append(f"- `{exc_type}` - {exc_desc}")
318
+ lines.append("")
319
+
320
+ # Examples
321
+ if self.options.include_examples and doc.examples:
322
+ lines.append("**Examples:**")
323
+ lines.append("")
324
+ for example in doc.examples:
325
+ lines.append("```python")
326
+ lines.append(example)
327
+ lines.append("```")
328
+ lines.append("")
329
+
330
+ return "\n".join(lines)
331
+
332
+ def _render_parameter(self, param: ParameterDoc) -> str:
333
+ """Render a parameter as Markdown list item.
334
+
335
+ Args:
336
+ param: Parameter documentation.
337
+
338
+ Returns:
339
+ Markdown list item.
340
+ """
341
+ type_str = f": `{param.type}`" if param.type else ""
342
+ default_str = f" (default: `{param.default}`)" if param.default else ""
343
+ required_str = " **(required)**" if param.required and not param.default else ""
344
+ desc = param.description or ""
345
+
346
+ return f"- **{param.name}**{type_str}{required_str}{default_str} - {desc}"
347
+
348
+
349
+ class HTMLRenderer(DocumentationRenderer):
350
+ """Renders documentation as HTML."""
351
+
352
+ def render_module(self, doc: ModuleDoc) -> str:
353
+ """Render module documentation as HTML.
354
+
355
+ Args:
356
+ doc: Module documentation.
357
+
358
+ Returns:
359
+ HTML string.
360
+ """
361
+ prefix = self.options.css_class_prefix
362
+ lines: list[str] = []
363
+
364
+ lines.append(f'<div class="{prefix}-module">')
365
+
366
+ # Module header
367
+ lines.append(f'<h1 class="{prefix}-module-title">{html.escape(doc.name)}</h1>')
368
+
369
+ if doc.description:
370
+ lines.append(f'<div class="{prefix}-description">{html.escape(doc.description)}</div>')
371
+
372
+ # Table of contents
373
+ if self.options.toc and (doc.classes or doc.functions):
374
+ lines.append(f'<nav class="{prefix}-toc">')
375
+ lines.append(f'<h2 class="{prefix}-toc-title">Table of Contents</h2>')
376
+
377
+ if doc.classes:
378
+ lines.append(f'<div class="{prefix}-toc-section">')
379
+ lines.append("<h3>Classes</h3>")
380
+ lines.append("<ul>")
381
+ for cls in doc.classes:
382
+ if self._should_include(cls.name):
383
+ anchor = cls.name.lower().replace("_", "-")
384
+ lines.append(f'<li><a href="#{anchor}">{html.escape(cls.name)}</a></li>')
385
+ lines.append("</ul>")
386
+ lines.append("</div>")
387
+
388
+ if doc.functions:
389
+ lines.append(f'<div class="{prefix}-toc-section">')
390
+ lines.append("<h3>Functions</h3>")
391
+ lines.append("<ul>")
392
+ for func in doc.functions:
393
+ if self._should_include(func.name):
394
+ anchor = func.name.lower().replace("_", "-")
395
+ lines.append(f'<li><a href="#{anchor}">{html.escape(func.name)}</a></li>')
396
+ lines.append("</ul>")
397
+ lines.append("</div>")
398
+
399
+ lines.append("</nav>")
400
+
401
+ # Classes
402
+ if doc.classes:
403
+ lines.append(f'<section class="{prefix}-classes">')
404
+ lines.append(f'<h2 class="{prefix}-section-title">Classes</h2>')
405
+ for cls in doc.classes:
406
+ if self._should_include(cls.name):
407
+ lines.append(self.render_class(cls, depth=0))
408
+ lines.append("</section>")
409
+
410
+ # Functions
411
+ if doc.functions:
412
+ lines.append(f'<section class="{prefix}-functions">')
413
+ lines.append(f'<h2 class="{prefix}-section-title">Functions</h2>')
414
+ for func in doc.functions:
415
+ if self._should_include(func.name):
416
+ lines.append(self.render_function(func, depth=0))
417
+ lines.append("</section>")
418
+
419
+ lines.append("</div>")
420
+
421
+ return "\n".join(lines)
422
+
423
+ def render_class(self, doc: ClassDoc, depth: int = 0) -> str:
424
+ """Render class documentation as HTML.
425
+
426
+ Args:
427
+ doc: Class documentation.
428
+ depth: Current nesting depth.
429
+
430
+ Returns:
431
+ HTML string.
432
+ """
433
+ if depth > self.options.max_depth:
434
+ return ""
435
+
436
+ prefix = self.options.css_class_prefix
437
+ anchor = doc.name.lower().replace("_", "-")
438
+ lines: list[str] = []
439
+
440
+ lines.append(f'<article class="{prefix}-class" id="{anchor}">')
441
+
442
+ # Class header
443
+ header_tag = f"h{min(3 + depth, 6)}"
444
+ lines.append(f'<{header_tag} class="{prefix}-class-title">{html.escape(doc.name)}</{header_tag}>')
445
+
446
+ # Inheritance
447
+ if doc.bases:
448
+ bases_str = ", ".join(html.escape(b) for b in doc.bases)
449
+ lines.append(f'<div class="{prefix}-inheritance">Inherits from: <code>{bases_str}</code></div>')
450
+
451
+ # Deprecation warning
452
+ if self.options.include_deprecation and doc.deprecated:
453
+ lines.append(f'<div class="{prefix}-deprecated">Deprecated: This class is deprecated.</div>')
454
+
455
+ # Description
456
+ if doc.description:
457
+ lines.append(f'<div class="{prefix}-description">{html.escape(doc.description)}</div>')
458
+
459
+ # Constructor
460
+ if doc.init_params:
461
+ lines.append(f'<div class="{prefix}-constructor">')
462
+ lines.append("<h4>Constructor</h4>")
463
+ lines.append(self._render_parameters_html(doc.init_params))
464
+ lines.append("</div>")
465
+
466
+ # Attributes
467
+ if doc.attributes:
468
+ lines.append(f'<div class="{prefix}-attributes">')
469
+ lines.append("<h4>Attributes</h4>")
470
+ lines.append("<dl>")
471
+ for attr_name, attr_info in doc.attributes.items():
472
+ if self._should_include(attr_name):
473
+ type_str = f": <code>{html.escape(attr_info.get('type', ''))}</code>" if attr_info.get("type") else ""
474
+ desc = html.escape(attr_info.get("description", ""))
475
+ lines.append(f"<dt><strong>{html.escape(attr_name)}</strong>{type_str}</dt>")
476
+ lines.append(f"<dd>{desc}</dd>")
477
+ lines.append("</dl>")
478
+ lines.append("</div>")
479
+
480
+ # Methods
481
+ if doc.methods:
482
+ lines.append(f'<div class="{prefix}-methods">')
483
+ lines.append("<h4>Methods</h4>")
484
+ for method in doc.methods:
485
+ if self._should_include(method.name):
486
+ lines.append(self.render_function(method, depth=depth + 1))
487
+ lines.append("</div>")
488
+
489
+ # Examples
490
+ if self.options.include_examples and doc.examples:
491
+ lines.append(f'<div class="{prefix}-examples">')
492
+ lines.append("<h4>Examples</h4>")
493
+ for example in doc.examples:
494
+ code_class = "language-python" if self.options.syntax_highlight else ""
495
+ lines.append(f'<pre><code class="{code_class}">{html.escape(example)}</code></pre>')
496
+ lines.append("</div>")
497
+
498
+ lines.append("</article>")
499
+
500
+ return "\n".join(lines)
501
+
502
+ def render_function(self, doc: FunctionDoc, depth: int = 0) -> str:
503
+ """Render function documentation as HTML.
504
+
505
+ Args:
506
+ doc: Function documentation.
507
+ depth: Current nesting depth.
508
+
509
+ Returns:
510
+ HTML string.
511
+ """
512
+ if depth > self.options.max_depth:
513
+ return ""
514
+
515
+ prefix = self.options.css_class_prefix
516
+ anchor = doc.name.lower().replace("_", "-")
517
+ lines: list[str] = []
518
+
519
+ lines.append(f'<div class="{prefix}-function" id="{anchor}">')
520
+
521
+ # Function signature
522
+ params_str = ", ".join(
523
+ f"{html.escape(p.name)}: {html.escape(p.type or '')}" if p.type else html.escape(p.name)
524
+ for p in doc.parameters
525
+ )
526
+ return_str = f" -&gt; {html.escape(doc.return_type)}" if doc.return_type else ""
527
+ async_prefix = "async " if doc.is_async else ""
528
+
529
+ header_tag = f"h{min(4 + depth, 6)}"
530
+ lines.append(
531
+ f'<{header_tag} class="{prefix}-function-signature">'
532
+ f'<code>{async_prefix}{html.escape(doc.name)}({params_str}){return_str}</code>'
533
+ f'</{header_tag}>'
534
+ )
535
+
536
+ # Deprecation warning
537
+ if self.options.include_deprecation and doc.deprecated:
538
+ lines.append(f'<div class="{prefix}-deprecated">Deprecated: This function is deprecated.</div>')
539
+
540
+ # Description
541
+ if doc.description:
542
+ lines.append(f'<div class="{prefix}-description">{html.escape(doc.description)}</div>')
543
+
544
+ # Parameters
545
+ if doc.parameters:
546
+ lines.append(f'<div class="{prefix}-params">')
547
+ lines.append("<h5>Parameters</h5>")
548
+ lines.append(self._render_parameters_html(doc.parameters))
549
+ lines.append("</div>")
550
+
551
+ # Returns
552
+ if doc.return_type or doc.return_description:
553
+ lines.append(f'<div class="{prefix}-returns">')
554
+ lines.append("<h5>Returns</h5>")
555
+ return_type = html.escape(doc.return_type) if doc.return_type else "Any"
556
+ return_desc = html.escape(doc.return_description or "")
557
+ lines.append(f"<p><code>{return_type}</code> - {return_desc}</p>")
558
+ lines.append("</div>")
559
+
560
+ # Raises
561
+ if doc.raises:
562
+ lines.append(f'<div class="{prefix}-raises">')
563
+ lines.append("<h5>Raises</h5>")
564
+ lines.append("<dl>")
565
+ for exc_type, exc_desc in doc.raises.items():
566
+ lines.append(f"<dt><code>{html.escape(exc_type)}</code></dt>")
567
+ lines.append(f"<dd>{html.escape(exc_desc)}</dd>")
568
+ lines.append("</dl>")
569
+ lines.append("</div>")
570
+
571
+ # Examples
572
+ if self.options.include_examples and doc.examples:
573
+ lines.append(f'<div class="{prefix}-examples">')
574
+ lines.append("<h5>Examples</h5>")
575
+ for example in doc.examples:
576
+ code_class = "language-python" if self.options.syntax_highlight else ""
577
+ lines.append(f'<pre><code class="{code_class}">{html.escape(example)}</code></pre>')
578
+ lines.append("</div>")
579
+
580
+ lines.append("</div>")
581
+
582
+ return "\n".join(lines)
583
+
584
+ def _render_parameters_html(self, params: list[ParameterDoc]) -> str:
585
+ """Render parameters as HTML definition list.
586
+
587
+ Args:
588
+ params: List of parameters.
589
+
590
+ Returns:
591
+ HTML string.
592
+ """
593
+ lines: list[str] = ["<dl>"]
594
+
595
+ for param in params:
596
+ type_str = f": <code>{html.escape(param.type)}</code>" if param.type else ""
597
+ default_str = f" (default: <code>{html.escape(str(param.default))}</code>)" if param.default else ""
598
+ required_str = " <em>(required)</em>" if param.required and not param.default else ""
599
+ desc = html.escape(param.description or "")
600
+
601
+ lines.append(f"<dt><strong>{html.escape(param.name)}</strong>{type_str}{required_str}{default_str}</dt>")
602
+ lines.append(f"<dd>{desc}</dd>")
603
+
604
+ lines.append("</dl>")
605
+ return "\n".join(lines)
606
+
607
+
608
+ class JSONRenderer(DocumentationRenderer):
609
+ """Renders documentation as JSON."""
610
+
611
+ def render_module(self, doc: ModuleDoc) -> str:
612
+ """Render module documentation as JSON.
613
+
614
+ Args:
615
+ doc: Module documentation.
616
+
617
+ Returns:
618
+ JSON string.
619
+ """
620
+ data = self._module_to_dict(doc)
621
+ return json.dumps(data, indent=2, ensure_ascii=False)
622
+
623
+ def render_class(self, doc: ClassDoc, depth: int = 0) -> str:
624
+ """Render class documentation as JSON.
625
+
626
+ Args:
627
+ doc: Class documentation.
628
+ depth: Current nesting depth.
629
+
630
+ Returns:
631
+ JSON string.
632
+ """
633
+ data = self._class_to_dict(doc, depth)
634
+ return json.dumps(data, indent=2, ensure_ascii=False)
635
+
636
+ def render_function(self, doc: FunctionDoc, depth: int = 0) -> str:
637
+ """Render function documentation as JSON.
638
+
639
+ Args:
640
+ doc: Function documentation.
641
+ depth: Current nesting depth.
642
+
643
+ Returns:
644
+ JSON string.
645
+ """
646
+ data = self._function_to_dict(doc)
647
+ return json.dumps(data, indent=2, ensure_ascii=False)
648
+
649
+ def _module_to_dict(self, doc: ModuleDoc) -> dict[str, Any]:
650
+ """Convert module documentation to dictionary.
651
+
652
+ Args:
653
+ doc: Module documentation.
654
+
655
+ Returns:
656
+ Dictionary representation.
657
+ """
658
+ return {
659
+ "type": "module",
660
+ "name": doc.name,
661
+ "description": doc.description,
662
+ "file_path": doc.file_path,
663
+ "classes": [
664
+ self._class_to_dict(cls, 0)
665
+ for cls in doc.classes
666
+ if self._should_include(cls.name)
667
+ ],
668
+ "functions": [
669
+ self._function_to_dict(func)
670
+ for func in doc.functions
671
+ if self._should_include(func.name)
672
+ ],
673
+ "constants": doc.constants,
674
+ "metadata": doc.metadata,
675
+ }
676
+
677
+ def _class_to_dict(self, doc: ClassDoc, depth: int) -> dict[str, Any]:
678
+ """Convert class documentation to dictionary.
679
+
680
+ Args:
681
+ doc: Class documentation.
682
+ depth: Current nesting depth.
683
+
684
+ Returns:
685
+ Dictionary representation.
686
+ """
687
+ methods = []
688
+ if depth < self.options.max_depth:
689
+ methods = [
690
+ self._function_to_dict(method)
691
+ for method in doc.methods
692
+ if self._should_include(method.name)
693
+ ]
694
+
695
+ return {
696
+ "type": "class",
697
+ "name": doc.name,
698
+ "description": doc.description,
699
+ "bases": doc.bases,
700
+ "deprecated": doc.deprecated,
701
+ "init_params": [self._param_to_dict(p) for p in doc.init_params],
702
+ "attributes": doc.attributes,
703
+ "methods": methods,
704
+ "examples": doc.examples if self.options.include_examples else [],
705
+ }
706
+
707
+ def _function_to_dict(self, doc: FunctionDoc) -> dict[str, Any]:
708
+ """Convert function documentation to dictionary.
709
+
710
+ Args:
711
+ doc: Function documentation.
712
+
713
+ Returns:
714
+ Dictionary representation.
715
+ """
716
+ return {
717
+ "type": "function",
718
+ "name": doc.name,
719
+ "description": doc.description,
720
+ "parameters": [self._param_to_dict(p) for p in doc.parameters],
721
+ "return_type": doc.return_type,
722
+ "return_description": doc.return_description,
723
+ "raises": doc.raises,
724
+ "is_async": doc.is_async,
725
+ "is_generator": doc.is_generator,
726
+ "is_classmethod": doc.is_classmethod,
727
+ "is_staticmethod": doc.is_staticmethod,
728
+ "deprecated": doc.deprecated,
729
+ "examples": doc.examples if self.options.include_examples else [],
730
+ }
731
+
732
+ def _param_to_dict(self, param: ParameterDoc) -> dict[str, Any]:
733
+ """Convert parameter documentation to dictionary.
734
+
735
+ Args:
736
+ param: Parameter documentation.
737
+
738
+ Returns:
739
+ Dictionary representation.
740
+ """
741
+ return {
742
+ "name": param.name,
743
+ "type": param.type,
744
+ "description": param.description,
745
+ "default": param.default,
746
+ "required": param.required,
747
+ }
748
+
749
+
750
+ def render_documentation(
751
+ doc: ModuleDoc | ClassDoc | FunctionDoc,
752
+ format: str = "markdown",
753
+ options: RenderOptions | None = None,
754
+ ) -> str:
755
+ """Render documentation in the specified format.
756
+
757
+ This is a convenience function for rendering documentation
758
+ without creating a renderer instance directly.
759
+
760
+ Args:
761
+ doc: Documentation to render.
762
+ format: Output format (markdown, html, json).
763
+ options: Rendering options.
764
+
765
+ Returns:
766
+ Rendered documentation string.
767
+
768
+ Raises:
769
+ ValueError: If format is not supported.
770
+
771
+ Examples:
772
+ >>> from truthound_dashboard.core.plugins.docs import (
773
+ ... DocumentationExtractor,
774
+ ... render_documentation,
775
+ ... )
776
+ >>> extractor = DocumentationExtractor()
777
+ >>> module_doc = extractor.extract_module("my_plugin.py")
778
+ >>> markdown = render_documentation(module_doc, "markdown")
779
+ >>> html = render_documentation(module_doc, "html")
780
+ """
781
+ renderers = {
782
+ "markdown": MarkdownRenderer,
783
+ "md": MarkdownRenderer,
784
+ "html": HTMLRenderer,
785
+ "json": JSONRenderer,
786
+ }
787
+
788
+ format_lower = format.lower()
789
+ if format_lower not in renderers:
790
+ raise ValueError(
791
+ f"Unsupported format: {format}. "
792
+ f"Supported formats: {', '.join(renderers.keys())}"
793
+ )
794
+
795
+ renderer = renderers[format_lower](options)
796
+
797
+ if isinstance(doc, ModuleDoc):
798
+ return renderer.render_module(doc)
799
+ elif isinstance(doc, ClassDoc):
800
+ return renderer.render_class(doc)
801
+ elif isinstance(doc, FunctionDoc):
802
+ return renderer.render_function(doc)
803
+ else:
804
+ raise TypeError(f"Unsupported documentation type: {type(doc)}")