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.
- truthound_dashboard/__init__.py +11 -0
- truthound_dashboard/__main__.py +6 -0
- truthound_dashboard/api/__init__.py +15 -0
- truthound_dashboard/api/deps.py +153 -0
- truthound_dashboard/api/drift.py +179 -0
- truthound_dashboard/api/error_handlers.py +287 -0
- truthound_dashboard/api/health.py +78 -0
- truthound_dashboard/api/history.py +62 -0
- truthound_dashboard/api/middleware.py +626 -0
- truthound_dashboard/api/notifications.py +561 -0
- truthound_dashboard/api/profile.py +52 -0
- truthound_dashboard/api/router.py +83 -0
- truthound_dashboard/api/rules.py +277 -0
- truthound_dashboard/api/schedules.py +329 -0
- truthound_dashboard/api/schemas.py +136 -0
- truthound_dashboard/api/sources.py +229 -0
- truthound_dashboard/api/validations.py +125 -0
- truthound_dashboard/cli.py +226 -0
- truthound_dashboard/config.py +132 -0
- truthound_dashboard/core/__init__.py +264 -0
- truthound_dashboard/core/base.py +185 -0
- truthound_dashboard/core/cache.py +479 -0
- truthound_dashboard/core/connections.py +331 -0
- truthound_dashboard/core/encryption.py +409 -0
- truthound_dashboard/core/exceptions.py +627 -0
- truthound_dashboard/core/logging.py +488 -0
- truthound_dashboard/core/maintenance.py +542 -0
- truthound_dashboard/core/notifications/__init__.py +56 -0
- truthound_dashboard/core/notifications/base.py +390 -0
- truthound_dashboard/core/notifications/channels.py +557 -0
- truthound_dashboard/core/notifications/dispatcher.py +453 -0
- truthound_dashboard/core/notifications/events.py +155 -0
- truthound_dashboard/core/notifications/service.py +744 -0
- truthound_dashboard/core/sampling.py +626 -0
- truthound_dashboard/core/scheduler.py +311 -0
- truthound_dashboard/core/services.py +1531 -0
- truthound_dashboard/core/truthound_adapter.py +659 -0
- truthound_dashboard/db/__init__.py +67 -0
- truthound_dashboard/db/base.py +108 -0
- truthound_dashboard/db/database.py +196 -0
- truthound_dashboard/db/models.py +732 -0
- truthound_dashboard/db/repository.py +237 -0
- truthound_dashboard/main.py +309 -0
- truthound_dashboard/schemas/__init__.py +150 -0
- truthound_dashboard/schemas/base.py +96 -0
- truthound_dashboard/schemas/drift.py +118 -0
- truthound_dashboard/schemas/history.py +74 -0
- truthound_dashboard/schemas/profile.py +91 -0
- truthound_dashboard/schemas/rule.py +199 -0
- truthound_dashboard/schemas/schedule.py +88 -0
- truthound_dashboard/schemas/schema.py +121 -0
- truthound_dashboard/schemas/source.py +138 -0
- truthound_dashboard/schemas/validation.py +192 -0
- truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
- truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
- truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
- truthound_dashboard/static/index.html +15 -0
- truthound_dashboard/static/mockServiceWorker.js +349 -0
- truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
- truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
- truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
- 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()
|