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,352 @@
1
+ """Chart library configuration and selection system.
2
+
3
+ This module provides a pluggable chart library system supporting:
4
+ - Recharts (default, React-based)
5
+ - Chart.js (Canvas-based)
6
+ - ECharts (Apache ECharts)
7
+ - Plotly (Interactive scientific charts)
8
+ - SVG (Native SVG generation)
9
+
10
+ Example:
11
+ from truthound_dashboard.core.charts import (
12
+ ChartLibrary,
13
+ get_chart_config,
14
+ set_chart_library,
15
+ )
16
+
17
+ # Set preferred library
18
+ set_chart_library(ChartLibrary.CHARTJS)
19
+
20
+ # Get configuration for frontend
21
+ config = get_chart_config()
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from dataclasses import dataclass, field
28
+ from enum import Enum
29
+ from typing import Any
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class ChartLibrary(str, Enum):
35
+ """Supported chart libraries."""
36
+
37
+ RECHARTS = "recharts" # Default, React-based
38
+ CHARTJS = "chartjs" # Chart.js (canvas)
39
+ ECHARTS = "echarts" # Apache ECharts
40
+ PLOTLY = "plotly" # Plotly.js
41
+ SVG = "svg" # Native SVG rendering
42
+
43
+
44
+ class ChartType(str, Enum):
45
+ """Available chart types."""
46
+
47
+ LINE = "line"
48
+ BAR = "bar"
49
+ AREA = "area"
50
+ PIE = "pie"
51
+ DONUT = "donut"
52
+ SCATTER = "scatter"
53
+ RADAR = "radar"
54
+ HEATMAP = "heatmap"
55
+ TREEMAP = "treemap"
56
+ GAUGE = "gauge"
57
+
58
+
59
+ @dataclass
60
+ class ChartLibraryConfig:
61
+ """Configuration for a chart library.
62
+
63
+ Attributes:
64
+ library: The chart library to use.
65
+ npm_package: NPM package name for installation.
66
+ version: Recommended version.
67
+ supported_charts: List of supported chart types.
68
+ options: Library-specific default options.
69
+ cdn_url: CDN URL for script loading (if applicable).
70
+ """
71
+
72
+ library: ChartLibrary
73
+ npm_package: str
74
+ version: str
75
+ supported_charts: list[ChartType] = field(default_factory=list)
76
+ options: dict[str, Any] = field(default_factory=dict)
77
+ cdn_url: str | None = None
78
+
79
+ def to_dict(self) -> dict[str, Any]:
80
+ """Convert to dictionary."""
81
+ return {
82
+ "library": self.library.value,
83
+ "npm_package": self.npm_package,
84
+ "version": self.version,
85
+ "supported_charts": [c.value for c in self.supported_charts],
86
+ "options": self.options,
87
+ "cdn_url": self.cdn_url,
88
+ }
89
+
90
+
91
+ # Library configurations
92
+ CHART_LIBRARY_CONFIGS: dict[ChartLibrary, ChartLibraryConfig] = {
93
+ ChartLibrary.RECHARTS: ChartLibraryConfig(
94
+ library=ChartLibrary.RECHARTS,
95
+ npm_package="recharts",
96
+ version="^2.10.0",
97
+ supported_charts=[
98
+ ChartType.LINE,
99
+ ChartType.BAR,
100
+ ChartType.AREA,
101
+ ChartType.PIE,
102
+ ChartType.SCATTER,
103
+ ChartType.RADAR,
104
+ ChartType.TREEMAP,
105
+ ],
106
+ options={
107
+ "responsive": True,
108
+ "animationDuration": 300,
109
+ "margin": {"top": 5, "right": 30, "left": 20, "bottom": 5},
110
+ },
111
+ ),
112
+ ChartLibrary.CHARTJS: ChartLibraryConfig(
113
+ library=ChartLibrary.CHARTJS,
114
+ npm_package="chart.js",
115
+ version="^4.4.0",
116
+ supported_charts=[
117
+ ChartType.LINE,
118
+ ChartType.BAR,
119
+ ChartType.PIE,
120
+ ChartType.DONUT,
121
+ ChartType.SCATTER,
122
+ ChartType.RADAR,
123
+ ],
124
+ options={
125
+ "responsive": True,
126
+ "maintainAspectRatio": False,
127
+ "animation": {"duration": 300},
128
+ "plugins": {"legend": {"display": True}},
129
+ },
130
+ cdn_url="https://cdn.jsdelivr.net/npm/chart.js",
131
+ ),
132
+ ChartLibrary.ECHARTS: ChartLibraryConfig(
133
+ library=ChartLibrary.ECHARTS,
134
+ npm_package="echarts",
135
+ version="^5.4.0",
136
+ supported_charts=[
137
+ ChartType.LINE,
138
+ ChartType.BAR,
139
+ ChartType.AREA,
140
+ ChartType.PIE,
141
+ ChartType.SCATTER,
142
+ ChartType.RADAR,
143
+ ChartType.HEATMAP,
144
+ ChartType.TREEMAP,
145
+ ChartType.GAUGE,
146
+ ],
147
+ options={
148
+ "animation": True,
149
+ "animationDuration": 300,
150
+ "tooltip": {"trigger": "axis"},
151
+ "grid": {"left": "3%", "right": "4%", "bottom": "3%", "containLabel": True},
152
+ },
153
+ cdn_url="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js",
154
+ ),
155
+ ChartLibrary.PLOTLY: ChartLibraryConfig(
156
+ library=ChartLibrary.PLOTLY,
157
+ npm_package="plotly.js",
158
+ version="^2.27.0",
159
+ supported_charts=[
160
+ ChartType.LINE,
161
+ ChartType.BAR,
162
+ ChartType.AREA,
163
+ ChartType.PIE,
164
+ ChartType.SCATTER,
165
+ ChartType.HEATMAP,
166
+ ],
167
+ options={
168
+ "responsive": True,
169
+ "displayModeBar": False,
170
+ "staticPlot": False,
171
+ },
172
+ cdn_url="https://cdn.plot.ly/plotly-2.27.0.min.js",
173
+ ),
174
+ ChartLibrary.SVG: ChartLibraryConfig(
175
+ library=ChartLibrary.SVG,
176
+ npm_package="d3", # Uses D3 for SVG generation
177
+ version="^7.8.0",
178
+ supported_charts=[
179
+ ChartType.LINE,
180
+ ChartType.BAR,
181
+ ChartType.AREA,
182
+ ChartType.PIE,
183
+ ],
184
+ options={
185
+ "viewBox": "0 0 400 300",
186
+ "preserveAspectRatio": "xMidYMid meet",
187
+ },
188
+ cdn_url="https://cdn.jsdelivr.net/npm/d3@7",
189
+ ),
190
+ }
191
+
192
+
193
+ @dataclass
194
+ class ChartSettings:
195
+ """Global chart settings.
196
+
197
+ Attributes:
198
+ library: Active chart library.
199
+ theme: Chart color theme (light/dark/custom).
200
+ primary_color: Primary accent color.
201
+ animation_enabled: Whether animations are enabled.
202
+ default_height: Default chart height in pixels.
203
+ custom_options: Additional library-specific options.
204
+ """
205
+
206
+ library: ChartLibrary = ChartLibrary.RECHARTS
207
+ theme: str = "light"
208
+ primary_color: str = "#fd9e4b" # Truthound primary color
209
+ animation_enabled: bool = True
210
+ default_height: int = 300
211
+ custom_options: dict[str, Any] = field(default_factory=dict)
212
+
213
+ def get_library_config(self) -> ChartLibraryConfig:
214
+ """Get configuration for the active library."""
215
+ return CHART_LIBRARY_CONFIGS[self.library]
216
+
217
+ def to_dict(self) -> dict[str, Any]:
218
+ """Convert to dictionary for API response."""
219
+ library_config = self.get_library_config()
220
+ return {
221
+ "library": self.library.value,
222
+ "theme": self.theme,
223
+ "primary_color": self.primary_color,
224
+ "animation_enabled": self.animation_enabled,
225
+ "default_height": self.default_height,
226
+ "custom_options": self.custom_options,
227
+ "library_config": library_config.to_dict(),
228
+ "available_libraries": [
229
+ {
230
+ "library": lib.value,
231
+ "name": lib.value.title(),
232
+ "npm_package": cfg.npm_package,
233
+ "supported_charts": [c.value for c in cfg.supported_charts],
234
+ }
235
+ for lib, cfg in CHART_LIBRARY_CONFIGS.items()
236
+ ],
237
+ }
238
+
239
+
240
+ # Singleton settings instance
241
+ _chart_settings: ChartSettings | None = None
242
+
243
+
244
+ def get_chart_settings() -> ChartSettings:
245
+ """Get global chart settings singleton.
246
+
247
+ Returns:
248
+ ChartSettings instance.
249
+ """
250
+ global _chart_settings
251
+ if _chart_settings is None:
252
+ _chart_settings = ChartSettings()
253
+ return _chart_settings
254
+
255
+
256
+ def set_chart_library(library: ChartLibrary | str) -> None:
257
+ """Set the active chart library.
258
+
259
+ Args:
260
+ library: Chart library to use.
261
+ """
262
+ if isinstance(library, str):
263
+ library = ChartLibrary(library.lower())
264
+
265
+ settings = get_chart_settings()
266
+ settings.library = library
267
+ logger.info(f"Chart library set to: {library.value}")
268
+
269
+
270
+ def set_chart_theme(theme: str, primary_color: str | None = None) -> None:
271
+ """Set chart theme settings.
272
+
273
+ Args:
274
+ theme: Theme name (light, dark, custom).
275
+ primary_color: Primary accent color (hex).
276
+ """
277
+ settings = get_chart_settings()
278
+ settings.theme = theme
279
+ if primary_color:
280
+ settings.primary_color = primary_color
281
+
282
+
283
+ def get_chart_config() -> dict[str, Any]:
284
+ """Get current chart configuration for frontend.
285
+
286
+ Returns:
287
+ Dictionary with chart settings and library info.
288
+ """
289
+ return get_chart_settings().to_dict()
290
+
291
+
292
+ def update_chart_settings(
293
+ library: str | None = None,
294
+ theme: str | None = None,
295
+ primary_color: str | None = None,
296
+ animation_enabled: bool | None = None,
297
+ default_height: int | None = None,
298
+ custom_options: dict[str, Any] | None = None,
299
+ ) -> ChartSettings:
300
+ """Update chart settings.
301
+
302
+ Args:
303
+ library: Chart library name.
304
+ theme: Color theme.
305
+ primary_color: Primary color hex.
306
+ animation_enabled: Enable animations.
307
+ default_height: Default chart height.
308
+ custom_options: Custom library options.
309
+
310
+ Returns:
311
+ Updated ChartSettings.
312
+ """
313
+ settings = get_chart_settings()
314
+
315
+ if library is not None:
316
+ settings.library = ChartLibrary(library.lower())
317
+ if theme is not None:
318
+ settings.theme = theme
319
+ if primary_color is not None:
320
+ settings.primary_color = primary_color
321
+ if animation_enabled is not None:
322
+ settings.animation_enabled = animation_enabled
323
+ if default_height is not None:
324
+ settings.default_height = default_height
325
+ if custom_options is not None:
326
+ settings.custom_options.update(custom_options)
327
+
328
+ return settings
329
+
330
+
331
+ def get_supported_chart_types(library: ChartLibrary | str | None = None) -> list[str]:
332
+ """Get chart types supported by a library.
333
+
334
+ Args:
335
+ library: Library to check. Uses active library if None.
336
+
337
+ Returns:
338
+ List of supported chart type names.
339
+ """
340
+ if library is None:
341
+ library = get_chart_settings().library
342
+ elif isinstance(library, str):
343
+ library = ChartLibrary(library.lower())
344
+
345
+ config = CHART_LIBRARY_CONFIGS[library]
346
+ return [ct.value for ct in config.supported_charts]
347
+
348
+
349
+ def reset_chart_settings() -> None:
350
+ """Reset chart settings singleton (for testing)."""
351
+ global _chart_settings
352
+ _chart_settings = None