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,136 @@
1
+ """Schemas API endpoints.
2
+
3
+ This module provides endpoints for schema learning and management.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import APIRouter, HTTPException, Path
11
+
12
+ from truthound_dashboard.schemas import (
13
+ SchemaLearnRequest,
14
+ SchemaResponse,
15
+ SchemaUpdateRequest,
16
+ )
17
+
18
+ from .deps import SchemaServiceDep, SourceServiceDep
19
+
20
+ router = APIRouter()
21
+
22
+
23
+ @router.get(
24
+ "/sources/{source_id}/schema",
25
+ response_model=SchemaResponse | None,
26
+ summary="Get source schema",
27
+ description="Get the active schema for a data source",
28
+ )
29
+ async def get_schema(
30
+ service: SchemaServiceDep,
31
+ source_service: SourceServiceDep,
32
+ source_id: Annotated[str, Path(description="Source ID")],
33
+ ) -> SchemaResponse | None:
34
+ """Get the active schema for a source.
35
+
36
+ Args:
37
+ service: Injected schema service.
38
+ source_service: Injected source service.
39
+ source_id: Source to get schema for.
40
+
41
+ Returns:
42
+ Schema if exists, None otherwise.
43
+
44
+ Raises:
45
+ HTTPException: 404 if source not found.
46
+ """
47
+ # Verify source exists
48
+ source = await source_service.get_by_id(source_id)
49
+ if source is None:
50
+ raise HTTPException(status_code=404, detail="Source not found")
51
+
52
+ schema = await service.get_schema(source_id)
53
+ if schema is None:
54
+ return None
55
+ return SchemaResponse.from_model(schema)
56
+
57
+
58
+ @router.post(
59
+ "/sources/{source_id}/learn",
60
+ response_model=SchemaResponse,
61
+ summary="Learn schema",
62
+ description="Auto-learn schema from data source",
63
+ )
64
+ async def learn_schema(
65
+ service: SchemaServiceDep,
66
+ source_service: SourceServiceDep,
67
+ source_id: Annotated[str, Path(description="Source ID")],
68
+ request: SchemaLearnRequest,
69
+ ) -> SchemaResponse:
70
+ """Learn schema from a data source.
71
+
72
+ Args:
73
+ service: Injected schema service.
74
+ source_service: Injected source service.
75
+ source_id: Source to learn schema from.
76
+ request: Learning options.
77
+
78
+ Returns:
79
+ Learned schema.
80
+
81
+ Raises:
82
+ HTTPException: 404 if source not found.
83
+ """
84
+ # Verify source exists
85
+ source = await source_service.get_by_id(source_id)
86
+ if source is None:
87
+ raise HTTPException(status_code=404, detail="Source not found")
88
+
89
+ try:
90
+ schema = await service.learn_schema(
91
+ source_id,
92
+ infer_constraints=request.infer_constraints,
93
+ )
94
+ return SchemaResponse.from_model(schema)
95
+ except Exception as e:
96
+ raise HTTPException(status_code=500, detail=str(e))
97
+
98
+
99
+ @router.put(
100
+ "/sources/{source_id}/schema",
101
+ response_model=SchemaResponse,
102
+ summary="Update schema",
103
+ description="Update the schema YAML for a source",
104
+ )
105
+ async def update_schema(
106
+ service: SchemaServiceDep,
107
+ source_service: SourceServiceDep,
108
+ source_id: Annotated[str, Path(description="Source ID")],
109
+ request: SchemaUpdateRequest,
110
+ ) -> SchemaResponse:
111
+ """Update schema YAML for a source.
112
+
113
+ Args:
114
+ service: Injected schema service.
115
+ source_service: Injected source service.
116
+ source_id: Source to update schema for.
117
+ request: New schema YAML.
118
+
119
+ Returns:
120
+ Updated schema.
121
+
122
+ Raises:
123
+ HTTPException: 404 if source or schema not found.
124
+ """
125
+ # Verify source exists
126
+ source = await source_service.get_by_id(source_id)
127
+ if source is None:
128
+ raise HTTPException(status_code=404, detail="Source not found")
129
+
130
+ schema = await service.update_schema(source_id, request.schema_yaml)
131
+ if schema is None:
132
+ raise HTTPException(
133
+ status_code=404,
134
+ detail="No active schema found. Use /learn to create one first.",
135
+ )
136
+ return SchemaResponse.from_model(schema)
@@ -0,0 +1,229 @@
1
+ """Sources API endpoints.
2
+
3
+ This module provides CRUD endpoints for managing data sources.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import APIRouter, HTTPException, Path, Query
11
+
12
+ from truthound_dashboard.schemas import (
13
+ MessageResponse,
14
+ SourceCreate,
15
+ SourceListResponse,
16
+ SourceResponse,
17
+ SourceUpdate,
18
+ )
19
+
20
+ from .deps import SourceServiceDep
21
+
22
+ router = APIRouter()
23
+
24
+
25
+ @router.get(
26
+ "",
27
+ response_model=SourceListResponse,
28
+ summary="List sources",
29
+ description="Get a paginated list of all data sources",
30
+ )
31
+ async def list_sources(
32
+ service: SourceServiceDep,
33
+ offset: Annotated[int, Query(ge=0, description="Offset for pagination")] = 0,
34
+ limit: Annotated[
35
+ int, Query(ge=1, le=100, description="Maximum items to return")
36
+ ] = 100,
37
+ active_only: Annotated[
38
+ bool, Query(description="Only return active sources")
39
+ ] = True,
40
+ ) -> SourceListResponse:
41
+ """List all data sources with pagination.
42
+
43
+ Args:
44
+ service: Injected source service.
45
+ offset: Number of items to skip.
46
+ limit: Maximum items to return.
47
+ active_only: Filter to active sources only.
48
+
49
+ Returns:
50
+ Paginated list of sources.
51
+ """
52
+ sources = await service.list(offset=offset, limit=limit, active_only=active_only)
53
+
54
+ return SourceListResponse(
55
+ data=[SourceResponse.from_model(s) for s in sources],
56
+ total=len(sources), # TODO: Get actual total count
57
+ offset=offset,
58
+ limit=limit,
59
+ )
60
+
61
+
62
+ @router.post(
63
+ "",
64
+ response_model=SourceResponse,
65
+ status_code=201,
66
+ summary="Create source",
67
+ description="Create a new data source",
68
+ )
69
+ async def create_source(
70
+ service: SourceServiceDep,
71
+ source: SourceCreate,
72
+ ) -> SourceResponse:
73
+ """Create a new data source.
74
+
75
+ Args:
76
+ service: Injected source service.
77
+ source: Source creation data.
78
+
79
+ Returns:
80
+ Created source.
81
+ """
82
+ created = await service.create(
83
+ name=source.name,
84
+ type=source.type,
85
+ config=source.config,
86
+ description=source.description,
87
+ )
88
+ return SourceResponse.from_model(created)
89
+
90
+
91
+ @router.get(
92
+ "/{source_id}",
93
+ response_model=SourceResponse,
94
+ summary="Get source",
95
+ description="Get a specific data source by ID",
96
+ )
97
+ async def get_source(
98
+ service: SourceServiceDep,
99
+ source_id: Annotated[str, Path(description="Source ID")],
100
+ ) -> SourceResponse:
101
+ """Get a specific data source.
102
+
103
+ Args:
104
+ service: Injected source service.
105
+ source_id: Source unique identifier.
106
+
107
+ Returns:
108
+ Source details.
109
+
110
+ Raises:
111
+ HTTPException: 404 if source not found.
112
+ """
113
+ source = await service.get_by_id(source_id)
114
+ if source is None:
115
+ raise HTTPException(status_code=404, detail="Source not found")
116
+ return SourceResponse.from_model(source)
117
+
118
+
119
+ @router.put(
120
+ "/{source_id}",
121
+ response_model=SourceResponse,
122
+ summary="Update source",
123
+ description="Update an existing data source",
124
+ )
125
+ async def update_source(
126
+ service: SourceServiceDep,
127
+ source_id: Annotated[str, Path(description="Source ID")],
128
+ update: SourceUpdate,
129
+ ) -> SourceResponse:
130
+ """Update an existing data source.
131
+
132
+ Args:
133
+ service: Injected source service.
134
+ source_id: Source unique identifier.
135
+ update: Update data.
136
+
137
+ Returns:
138
+ Updated source.
139
+
140
+ Raises:
141
+ HTTPException: 404 if source not found.
142
+ """
143
+ updated = await service.update(
144
+ source_id,
145
+ name=update.name,
146
+ config=update.config,
147
+ description=update.description,
148
+ is_active=update.is_active,
149
+ )
150
+ if updated is None:
151
+ raise HTTPException(status_code=404, detail="Source not found")
152
+ return SourceResponse.from_model(updated)
153
+
154
+
155
+ @router.delete(
156
+ "/{source_id}",
157
+ response_model=MessageResponse,
158
+ summary="Delete source",
159
+ description="Delete a data source and all related data",
160
+ )
161
+ async def delete_source(
162
+ service: SourceServiceDep,
163
+ source_id: Annotated[str, Path(description="Source ID")],
164
+ ) -> MessageResponse:
165
+ """Delete a data source.
166
+
167
+ Args:
168
+ service: Injected source service.
169
+ source_id: Source unique identifier.
170
+
171
+ Returns:
172
+ Success message.
173
+
174
+ Raises:
175
+ HTTPException: 404 if source not found.
176
+ """
177
+ deleted = await service.delete(source_id)
178
+ if not deleted:
179
+ raise HTTPException(status_code=404, detail="Source not found")
180
+ return MessageResponse(message="Source deleted successfully")
181
+
182
+
183
+ @router.post(
184
+ "/{source_id}/test",
185
+ response_model=dict,
186
+ summary="Test source connection",
187
+ description="Test connection to a data source",
188
+ )
189
+ async def test_source_connection(
190
+ service: SourceServiceDep,
191
+ source_id: Annotated[str, Path(description="Source ID")],
192
+ ) -> dict:
193
+ """Test connection to a data source.
194
+
195
+ Args:
196
+ service: Injected source service.
197
+ source_id: Source unique identifier.
198
+
199
+ Returns:
200
+ Connection test result with success status and message.
201
+
202
+ Raises:
203
+ HTTPException: 404 if source not found.
204
+ """
205
+ from truthound_dashboard.core.connections import test_connection
206
+
207
+ source = await service.get_by_id(source_id)
208
+ if source is None:
209
+ raise HTTPException(status_code=404, detail="Source not found")
210
+
211
+ result = await test_connection(source.type, source.config)
212
+ return {"success": True, "data": result}
213
+
214
+
215
+ @router.get(
216
+ "/types/supported",
217
+ response_model=dict,
218
+ summary="Get supported source types",
219
+ description="Get list of supported data source types and their configuration",
220
+ )
221
+ async def get_supported_types() -> dict:
222
+ """Get list of supported source types.
223
+
224
+ Returns:
225
+ List of supported source types with required/optional fields.
226
+ """
227
+ from truthound_dashboard.core.connections import get_supported_source_types
228
+
229
+ return {"success": True, "data": get_supported_source_types()}
@@ -0,0 +1,125 @@
1
+ """Validations API endpoints.
2
+
3
+ This module provides endpoints for running and managing validations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import APIRouter, HTTPException, Path, Query
11
+
12
+ from truthound_dashboard.schemas import (
13
+ ValidationListItem,
14
+ ValidationListResponse,
15
+ ValidationResponse,
16
+ ValidationRunRequest,
17
+ )
18
+
19
+ from .deps import SourceServiceDep, ValidationServiceDep
20
+
21
+ router = APIRouter()
22
+
23
+
24
+ @router.post(
25
+ "/sources/{source_id}/validate",
26
+ response_model=ValidationResponse,
27
+ summary="Run validation",
28
+ description="Run validation on a data source",
29
+ )
30
+ async def run_validation(
31
+ service: ValidationServiceDep,
32
+ source_id: Annotated[str, Path(description="Source ID to validate")],
33
+ request: ValidationRunRequest,
34
+ ) -> ValidationResponse:
35
+ """Run validation on a data source.
36
+
37
+ Args:
38
+ service: Injected validation service.
39
+ source_id: Source to validate.
40
+ request: Validation options.
41
+
42
+ Returns:
43
+ Validation result.
44
+
45
+ Raises:
46
+ HTTPException: 404 if source not found.
47
+ """
48
+ try:
49
+ validation = await service.run_validation(
50
+ source_id,
51
+ validators=request.validators,
52
+ schema_path=request.schema_path,
53
+ auto_schema=request.auto_schema,
54
+ )
55
+ return ValidationResponse.from_model(validation)
56
+ except ValueError as e:
57
+ raise HTTPException(status_code=404, detail=str(e))
58
+
59
+
60
+ @router.get(
61
+ "/{validation_id}",
62
+ response_model=ValidationResponse,
63
+ summary="Get validation",
64
+ description="Get a specific validation result by ID",
65
+ )
66
+ async def get_validation(
67
+ service: ValidationServiceDep,
68
+ validation_id: Annotated[str, Path(description="Validation ID")],
69
+ ) -> ValidationResponse:
70
+ """Get a specific validation result.
71
+
72
+ Args:
73
+ service: Injected validation service.
74
+ validation_id: Validation unique identifier.
75
+
76
+ Returns:
77
+ Validation details with issues.
78
+
79
+ Raises:
80
+ HTTPException: 404 if validation not found.
81
+ """
82
+ validation = await service.get_validation(validation_id)
83
+ if validation is None:
84
+ raise HTTPException(status_code=404, detail="Validation not found")
85
+ return ValidationResponse.from_model(validation)
86
+
87
+
88
+ @router.get(
89
+ "/sources/{source_id}/validations",
90
+ response_model=ValidationListResponse,
91
+ summary="List source validations",
92
+ description="Get validation history for a source",
93
+ )
94
+ async def list_source_validations(
95
+ service: ValidationServiceDep,
96
+ source_service: SourceServiceDep,
97
+ source_id: Annotated[str, Path(description="Source ID")],
98
+ limit: Annotated[int, Query(ge=1, le=100, description="Maximum items")] = 20,
99
+ ) -> ValidationListResponse:
100
+ """List validation history for a source.
101
+
102
+ Args:
103
+ service: Injected validation service.
104
+ source_service: Injected source service.
105
+ source_id: Source to get validations for.
106
+ limit: Maximum validations to return.
107
+
108
+ Returns:
109
+ List of validation summaries.
110
+
111
+ Raises:
112
+ HTTPException: 404 if source not found.
113
+ """
114
+ # Verify source exists
115
+ source = await source_service.get_by_id(source_id)
116
+ if source is None:
117
+ raise HTTPException(status_code=404, detail="Source not found")
118
+
119
+ validations = await service.list_for_source(source_id, limit=limit)
120
+
121
+ return ValidationListResponse(
122
+ data=[ValidationListItem.from_model(v) for v in validations],
123
+ total=len(validations),
124
+ limit=limit,
125
+ )
@@ -0,0 +1,226 @@
1
+ """CLI entry point for truthound-dashboard.
2
+
3
+ This module provides the command-line interface using Typer.
4
+ It supports both standalone usage and integration with the
5
+ truthound CLI plugin system.
6
+
7
+ Example:
8
+ # Standalone usage
9
+ truthound-dashboard serve --port 8765
10
+
11
+ # Via truthound CLI plugin
12
+ truthound serve --port 8765
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import webbrowser
18
+ from pathlib import Path
19
+ from typing import Annotated
20
+
21
+ import typer
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+
25
+ from truthound_dashboard import __version__
26
+
27
+ app = typer.Typer(
28
+ name="truthound-dashboard",
29
+ help="Open-source data quality dashboard - GX Cloud alternative",
30
+ no_args_is_help=True,
31
+ rich_markup_mode="rich",
32
+ )
33
+ console = Console()
34
+
35
+
36
+ def version_callback(value: bool) -> None:
37
+ """Print version and exit."""
38
+ if value:
39
+ console.print(f"truthound-dashboard version {__version__}")
40
+ raise typer.Exit()
41
+
42
+
43
+ @app.callback()
44
+ def main(
45
+ version: Annotated[
46
+ bool | None,
47
+ typer.Option(
48
+ "--version",
49
+ "-v",
50
+ help="Show version and exit",
51
+ callback=version_callback,
52
+ is_eager=True,
53
+ ),
54
+ ] = None,
55
+ ) -> None:
56
+ """Truthound Dashboard - Open-source data quality monitoring."""
57
+ pass
58
+
59
+
60
+ @app.command()
61
+ def serve(
62
+ port: Annotated[
63
+ int,
64
+ typer.Option("--port", "-p", help="Port to run server on", min=1, max=65535),
65
+ ] = 8765,
66
+ host: Annotated[
67
+ str,
68
+ typer.Option("--host", "-h", help="Host to bind server to"),
69
+ ] = "127.0.0.1",
70
+ data_dir: Annotated[
71
+ Path | None,
72
+ typer.Option(
73
+ "--data-dir",
74
+ "-d",
75
+ help="Data directory for database and cache",
76
+ exists=False,
77
+ file_okay=False,
78
+ resolve_path=True,
79
+ ),
80
+ ] = None,
81
+ no_browser: Annotated[
82
+ bool,
83
+ typer.Option("--no-browser", help="Don't open browser automatically"),
84
+ ] = False,
85
+ reload: Annotated[
86
+ bool,
87
+ typer.Option("--reload", help="Enable hot reload for development"),
88
+ ] = False,
89
+ log_level: Annotated[
90
+ str,
91
+ typer.Option(
92
+ "--log-level",
93
+ "-l",
94
+ help="Logging level",
95
+ case_sensitive=False,
96
+ ),
97
+ ] = "warning",
98
+ ) -> None:
99
+ """Start the truthound dashboard server.
100
+
101
+ This command starts the FastAPI server and optionally opens
102
+ the dashboard in your default browser.
103
+
104
+ Examples:
105
+ truthound serve
106
+ truthound serve --port 9000
107
+ truthound serve --reload --log-level debug
108
+ """
109
+ import uvicorn
110
+
111
+ from truthound_dashboard.config import get_settings, reset_settings
112
+
113
+ # Reset settings cache to pick up any CLI overrides
114
+ reset_settings()
115
+ settings = get_settings()
116
+
117
+ # Override settings from CLI arguments
118
+ if data_dir:
119
+ settings.data_dir = data_dir
120
+
121
+ # Ensure directories exist
122
+ settings.ensure_directories()
123
+
124
+ # Display startup info
125
+ url = f"http://{host}:{port}"
126
+ console.print(
127
+ Panel(
128
+ f"[bold]Truthound Dashboard v{__version__}[/bold]\n\n"
129
+ f"[green]✓[/green] Database: {settings.database_path}\n"
130
+ f"[green]✓[/green] Server: {url}\n"
131
+ f"[green]✓[/green] API Docs: {url}/docs",
132
+ title="Starting Dashboard",
133
+ border_style="bright_blue",
134
+ )
135
+ )
136
+
137
+ # Open browser
138
+ if not no_browser:
139
+ webbrowser.open(url)
140
+ console.print("[dim]Opening browser...[/dim]")
141
+
142
+ # Configure and run server
143
+ uvicorn.run(
144
+ "truthound_dashboard.main:app",
145
+ host=host,
146
+ port=port,
147
+ reload=reload,
148
+ log_level=log_level.lower(),
149
+ access_log=log_level.lower() == "debug",
150
+ )
151
+
152
+
153
+ @app.command()
154
+ def info() -> None:
155
+ """Show dashboard configuration and status."""
156
+ from truthound_dashboard.config import get_settings
157
+
158
+ settings = get_settings()
159
+ settings.ensure_directories()
160
+
161
+ console.print(
162
+ Panel(
163
+ f"[bold]Configuration[/bold]\n\n"
164
+ f"Data Directory: {settings.data_dir}\n"
165
+ f"Database: {settings.database_path}\n"
166
+ f"Cache: {settings.cache_dir}\n"
167
+ f"Schemas: {settings.schema_dir}\n\n"
168
+ f"[bold]Server Defaults[/bold]\n\n"
169
+ f"Host: {settings.host}\n"
170
+ f"Port: {settings.port}\n"
171
+ f"Auth Enabled: {settings.auth_enabled}\n\n"
172
+ f"[bold]Validation Defaults[/bold]\n\n"
173
+ f"Sample Size: {settings.sample_size:,}\n"
174
+ f"Max Failed Rows: {settings.max_failed_rows:,}\n"
175
+ f"Timeout: {settings.default_timeout}s",
176
+ title=f"Truthound Dashboard v{__version__}",
177
+ border_style="bright_blue",
178
+ )
179
+ )
180
+
181
+
182
+ def register_commands(typer_app: typer.Typer) -> None:
183
+ """Register commands with truthound CLI plugin system.
184
+
185
+ This function is called by the truthound CLI when the
186
+ dashboard plugin is discovered via entry points.
187
+
188
+ Args:
189
+ typer_app: The parent typer application to register commands with.
190
+ """
191
+
192
+ @typer_app.command(name="serve")
193
+ def serve_dashboard(
194
+ port: Annotated[
195
+ int,
196
+ typer.Option("--port", "-p", help="Port to run on"),
197
+ ] = 8765,
198
+ host: Annotated[
199
+ str,
200
+ typer.Option("--host", help="Host to bind"),
201
+ ] = "127.0.0.1",
202
+ data_dir: Annotated[
203
+ Path | None,
204
+ typer.Option("--data-dir", "-d", help="Data directory path"),
205
+ ] = None,
206
+ no_browser: Annotated[
207
+ bool,
208
+ typer.Option("--no-browser", help="Don't open browser automatically"),
209
+ ] = False,
210
+ reload: Annotated[
211
+ bool,
212
+ typer.Option("--reload", help="Enable hot reload for development"),
213
+ ] = False,
214
+ ) -> None:
215
+ """Start the truthound dashboard server."""
216
+ serve(
217
+ port=port,
218
+ host=host,
219
+ data_dir=data_dir,
220
+ no_browser=no_browser,
221
+ reload=reload,
222
+ )
223
+
224
+
225
+ if __name__ == "__main__":
226
+ app()