truthound-dashboard 1.0.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 (62) hide show
  1. truthound_dashboard/__init__.py +11 -0
  2. truthound_dashboard/__main__.py +6 -0
  3. truthound_dashboard/api/__init__.py +15 -0
  4. truthound_dashboard/api/deps.py +153 -0
  5. truthound_dashboard/api/drift.py +179 -0
  6. truthound_dashboard/api/error_handlers.py +287 -0
  7. truthound_dashboard/api/health.py +78 -0
  8. truthound_dashboard/api/history.py +62 -0
  9. truthound_dashboard/api/middleware.py +626 -0
  10. truthound_dashboard/api/notifications.py +561 -0
  11. truthound_dashboard/api/profile.py +52 -0
  12. truthound_dashboard/api/router.py +83 -0
  13. truthound_dashboard/api/rules.py +277 -0
  14. truthound_dashboard/api/schedules.py +329 -0
  15. truthound_dashboard/api/schemas.py +136 -0
  16. truthound_dashboard/api/sources.py +229 -0
  17. truthound_dashboard/api/validations.py +125 -0
  18. truthound_dashboard/cli.py +226 -0
  19. truthound_dashboard/config.py +132 -0
  20. truthound_dashboard/core/__init__.py +264 -0
  21. truthound_dashboard/core/base.py +185 -0
  22. truthound_dashboard/core/cache.py +479 -0
  23. truthound_dashboard/core/connections.py +331 -0
  24. truthound_dashboard/core/encryption.py +409 -0
  25. truthound_dashboard/core/exceptions.py +627 -0
  26. truthound_dashboard/core/logging.py +488 -0
  27. truthound_dashboard/core/maintenance.py +542 -0
  28. truthound_dashboard/core/notifications/__init__.py +56 -0
  29. truthound_dashboard/core/notifications/base.py +390 -0
  30. truthound_dashboard/core/notifications/channels.py +557 -0
  31. truthound_dashboard/core/notifications/dispatcher.py +453 -0
  32. truthound_dashboard/core/notifications/events.py +155 -0
  33. truthound_dashboard/core/notifications/service.py +744 -0
  34. truthound_dashboard/core/sampling.py +626 -0
  35. truthound_dashboard/core/scheduler.py +311 -0
  36. truthound_dashboard/core/services.py +1531 -0
  37. truthound_dashboard/core/truthound_adapter.py +659 -0
  38. truthound_dashboard/db/__init__.py +67 -0
  39. truthound_dashboard/db/base.py +108 -0
  40. truthound_dashboard/db/database.py +196 -0
  41. truthound_dashboard/db/models.py +732 -0
  42. truthound_dashboard/db/repository.py +237 -0
  43. truthound_dashboard/main.py +309 -0
  44. truthound_dashboard/schemas/__init__.py +150 -0
  45. truthound_dashboard/schemas/base.py +96 -0
  46. truthound_dashboard/schemas/drift.py +118 -0
  47. truthound_dashboard/schemas/history.py +74 -0
  48. truthound_dashboard/schemas/profile.py +91 -0
  49. truthound_dashboard/schemas/rule.py +199 -0
  50. truthound_dashboard/schemas/schedule.py +88 -0
  51. truthound_dashboard/schemas/schema.py +121 -0
  52. truthound_dashboard/schemas/source.py +138 -0
  53. truthound_dashboard/schemas/validation.py +192 -0
  54. truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
  55. truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
  56. truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
  57. truthound_dashboard/static/index.html +15 -0
  58. truthound_dashboard/static/mockServiceWorker.js +349 -0
  59. truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
  60. truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
  61. truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
  62. truthound_dashboard-1.0.0.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,331 @@
1
+ """Database connection factory and utilities.
2
+
3
+ Provides utilities for building connection strings and testing connections
4
+ for various database types supported by truthound.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from urllib.parse import quote_plus
13
+
14
+
15
+ class ConnectionBuilder(ABC):
16
+ """Abstract base class for connection string builders."""
17
+
18
+ @abstractmethod
19
+ def build(self, config: dict[str, Any]) -> str:
20
+ """Build connection string from configuration.
21
+
22
+ Args:
23
+ config: Database-specific configuration.
24
+
25
+ Returns:
26
+ Connection string.
27
+
28
+ Raises:
29
+ ValueError: If required config is missing.
30
+ """
31
+ pass
32
+
33
+ @abstractmethod
34
+ def validate_config(self, config: dict[str, Any]) -> list[str]:
35
+ """Validate configuration and return list of errors.
36
+
37
+ Args:
38
+ config: Configuration to validate.
39
+
40
+ Returns:
41
+ List of error messages (empty if valid).
42
+ """
43
+ pass
44
+
45
+
46
+ class FileConnectionBuilder(ConnectionBuilder):
47
+ """Connection builder for file-based sources."""
48
+
49
+ SUPPORTED_EXTENSIONS = {".csv", ".parquet", ".json", ".xlsx", ".xls"}
50
+
51
+ def build(self, config: dict[str, Any]) -> str:
52
+ """Build file path from config."""
53
+ return config.get("path", "")
54
+
55
+ def validate_config(self, config: dict[str, Any]) -> list[str]:
56
+ """Validate file configuration."""
57
+ errors = []
58
+ path = config.get("path")
59
+
60
+ if not path:
61
+ errors.append("path is required")
62
+ else:
63
+ p = Path(path)
64
+ if p.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
65
+ errors.append(
66
+ f"Unsupported file type: {p.suffix}. "
67
+ f"Supported: {', '.join(self.SUPPORTED_EXTENSIONS)}"
68
+ )
69
+
70
+ return errors
71
+
72
+
73
+ class PostgreSQLConnectionBuilder(ConnectionBuilder):
74
+ """Connection builder for PostgreSQL databases."""
75
+
76
+ def build(self, config: dict[str, Any]) -> str:
77
+ """Build PostgreSQL connection string."""
78
+ host = config.get("host", "localhost")
79
+ port = config.get("port", 5432)
80
+ database = config.get("database", "")
81
+ username = config.get("username", "")
82
+ password = quote_plus(config.get("password", ""))
83
+ schema = config.get("schema")
84
+
85
+ conn = f"postgresql://{username}:{password}@{host}:{port}/{database}"
86
+
87
+ if schema:
88
+ conn += f"?options=-csearch_path%3D{schema}"
89
+
90
+ return conn
91
+
92
+ def validate_config(self, config: dict[str, Any]) -> list[str]:
93
+ """Validate PostgreSQL configuration."""
94
+ errors = []
95
+ required = ["host", "database", "username"]
96
+
97
+ for field in required:
98
+ if not config.get(field):
99
+ errors.append(f"{field} is required")
100
+
101
+ return errors
102
+
103
+
104
+ class MySQLConnectionBuilder(ConnectionBuilder):
105
+ """Connection builder for MySQL databases."""
106
+
107
+ def build(self, config: dict[str, Any]) -> str:
108
+ """Build MySQL connection string."""
109
+ host = config.get("host", "localhost")
110
+ port = config.get("port", 3306)
111
+ database = config.get("database", "")
112
+ username = config.get("username", "")
113
+ password = quote_plus(config.get("password", ""))
114
+
115
+ return f"mysql://{username}:{password}@{host}:{port}/{database}"
116
+
117
+ def validate_config(self, config: dict[str, Any]) -> list[str]:
118
+ """Validate MySQL configuration."""
119
+ errors = []
120
+ required = ["host", "database", "username"]
121
+
122
+ for field in required:
123
+ if not config.get(field):
124
+ errors.append(f"{field} is required")
125
+
126
+ return errors
127
+
128
+
129
+ class SnowflakeConnectionBuilder(ConnectionBuilder):
130
+ """Connection builder for Snowflake databases."""
131
+
132
+ def build(self, config: dict[str, Any]) -> str:
133
+ """Build Snowflake connection string."""
134
+ account = config.get("account", "")
135
+ username = config.get("username", "")
136
+ password = quote_plus(config.get("password", ""))
137
+ database = config.get("database", "")
138
+ schema = config.get("schema", "PUBLIC")
139
+ warehouse = config.get("warehouse", "")
140
+ role = config.get("role")
141
+
142
+ conn = (
143
+ f"snowflake://{username}:{password}@{account}"
144
+ f"/{database}/{schema}?warehouse={warehouse}"
145
+ )
146
+
147
+ if role:
148
+ conn += f"&role={role}"
149
+
150
+ return conn
151
+
152
+ def validate_config(self, config: dict[str, Any]) -> list[str]:
153
+ """Validate Snowflake configuration."""
154
+ errors = []
155
+ required = ["account", "database", "warehouse", "username"]
156
+
157
+ for field in required:
158
+ if not config.get(field):
159
+ errors.append(f"{field} is required")
160
+
161
+ return errors
162
+
163
+
164
+ class BigQueryConnectionBuilder(ConnectionBuilder):
165
+ """Connection builder for BigQuery."""
166
+
167
+ def build(self, config: dict[str, Any]) -> str:
168
+ """Build BigQuery connection string."""
169
+ project = config.get("project", "")
170
+ dataset = config.get("dataset", "")
171
+
172
+ conn = f"bigquery://{project}"
173
+ if dataset:
174
+ conn += f"/{dataset}"
175
+
176
+ return conn
177
+
178
+ def validate_config(self, config: dict[str, Any]) -> list[str]:
179
+ """Validate BigQuery configuration."""
180
+ errors = []
181
+
182
+ if not config.get("project"):
183
+ errors.append("project is required")
184
+
185
+ return errors
186
+
187
+
188
+ # Registry of connection builders
189
+ CONNECTION_BUILDERS: dict[str, type[ConnectionBuilder]] = {
190
+ "file": FileConnectionBuilder,
191
+ "postgresql": PostgreSQLConnectionBuilder,
192
+ "mysql": MySQLConnectionBuilder,
193
+ "snowflake": SnowflakeConnectionBuilder,
194
+ "bigquery": BigQueryConnectionBuilder,
195
+ }
196
+
197
+
198
+ def get_connection_builder(source_type: str) -> ConnectionBuilder:
199
+ """Get connection builder for source type.
200
+
201
+ Args:
202
+ source_type: Type of data source.
203
+
204
+ Returns:
205
+ ConnectionBuilder instance.
206
+
207
+ Raises:
208
+ ValueError: If source type is not supported.
209
+ """
210
+ builder_class = CONNECTION_BUILDERS.get(source_type)
211
+ if builder_class is None:
212
+ supported = ", ".join(CONNECTION_BUILDERS.keys())
213
+ raise ValueError(f"Unknown source type: {source_type}. Supported: {supported}")
214
+
215
+ return builder_class()
216
+
217
+
218
+ def build_connection_string(source_type: str, config: dict[str, Any]) -> str:
219
+ """Build connection string from source type and configuration.
220
+
221
+ Args:
222
+ source_type: Type of data source.
223
+ config: Source-specific configuration.
224
+
225
+ Returns:
226
+ Connection string.
227
+
228
+ Raises:
229
+ ValueError: If source type unknown or config invalid.
230
+ """
231
+ builder = get_connection_builder(source_type)
232
+ errors = builder.validate_config(config)
233
+
234
+ if errors:
235
+ raise ValueError(f"Invalid configuration: {'; '.join(errors)}")
236
+
237
+ return builder.build(config)
238
+
239
+
240
+ async def test_connection(source_type: str, config: dict[str, Any]) -> dict[str, Any]:
241
+ """Test database connection.
242
+
243
+ Args:
244
+ source_type: Type of data source.
245
+ config: Source-specific configuration.
246
+
247
+ Returns:
248
+ Dictionary with success status and message or error.
249
+ """
250
+ try:
251
+ # Validate config first
252
+ builder = get_connection_builder(source_type)
253
+ errors = builder.validate_config(config)
254
+
255
+ if errors:
256
+ return {
257
+ "success": False,
258
+ "error": f"Configuration errors: {'; '.join(errors)}",
259
+ }
260
+
261
+ connection_string = builder.build(config)
262
+
263
+ if source_type == "file":
264
+ # For files, just check if path exists
265
+ path = Path(config["path"])
266
+ if not path.exists():
267
+ return {"success": False, "error": f"File not found: {path}"}
268
+ return {
269
+ "success": True,
270
+ "message": f"File exists: {path.name} ({path.stat().st_size:,} bytes)",
271
+ }
272
+
273
+ # For databases, use truthound to test connection
274
+ import truthound as th
275
+
276
+ # Quick profile to test connection
277
+ result = th.profile(connection_string)
278
+ return {
279
+ "success": True,
280
+ "message": f"Connected! Found {result.column_count} columns, {result.row_count:,} rows",
281
+ }
282
+
283
+ except ImportError:
284
+ return {"success": False, "error": "truthound package not available"}
285
+ except Exception as e:
286
+ return {"success": False, "error": str(e)}
287
+
288
+
289
+ def get_supported_source_types() -> list[dict[str, Any]]:
290
+ """Get list of supported source types with their required fields.
291
+
292
+ Returns:
293
+ List of source type descriptions.
294
+ """
295
+ return [
296
+ {
297
+ "type": "file",
298
+ "name": "File",
299
+ "description": "CSV, Parquet, JSON, Excel files",
300
+ "required_fields": ["path"],
301
+ "optional_fields": [],
302
+ },
303
+ {
304
+ "type": "postgresql",
305
+ "name": "PostgreSQL",
306
+ "description": "PostgreSQL database",
307
+ "required_fields": ["host", "database", "username"],
308
+ "optional_fields": ["port", "password", "schema"],
309
+ },
310
+ {
311
+ "type": "mysql",
312
+ "name": "MySQL",
313
+ "description": "MySQL database",
314
+ "required_fields": ["host", "database", "username"],
315
+ "optional_fields": ["port", "password"],
316
+ },
317
+ {
318
+ "type": "snowflake",
319
+ "name": "Snowflake",
320
+ "description": "Snowflake data warehouse",
321
+ "required_fields": ["account", "database", "warehouse", "username"],
322
+ "optional_fields": ["password", "schema", "role"],
323
+ },
324
+ {
325
+ "type": "bigquery",
326
+ "name": "BigQuery",
327
+ "description": "Google BigQuery",
328
+ "required_fields": ["project"],
329
+ "optional_fields": ["dataset"],
330
+ },
331
+ ]