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,67 @@
|
|
|
1
|
+
"""Database module.
|
|
2
|
+
|
|
3
|
+
This module provides database connectivity, models, and repository patterns
|
|
4
|
+
for the truthound dashboard.
|
|
5
|
+
|
|
6
|
+
Exports:
|
|
7
|
+
- Database connection: get_session, get_db_session, init_db
|
|
8
|
+
- Base classes: Base, UUIDMixin, TimestampMixin
|
|
9
|
+
- Models: Source, Schema, Rule, Validation, Profile, Schedule, DriftComparison, AppSettings
|
|
10
|
+
- Repository: BaseRepository
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .base import Base, SoftDeleteMixin, TimestampMixin, UUIDMixin
|
|
14
|
+
from .database import (
|
|
15
|
+
get_db_session,
|
|
16
|
+
get_engine,
|
|
17
|
+
get_session,
|
|
18
|
+
get_session_factory,
|
|
19
|
+
init_db,
|
|
20
|
+
reset_connection,
|
|
21
|
+
reset_db,
|
|
22
|
+
)
|
|
23
|
+
from .models import (
|
|
24
|
+
AppSettings,
|
|
25
|
+
DriftComparison,
|
|
26
|
+
NotificationChannel,
|
|
27
|
+
NotificationLog,
|
|
28
|
+
NotificationRule,
|
|
29
|
+
Profile,
|
|
30
|
+
Rule,
|
|
31
|
+
Schedule,
|
|
32
|
+
Schema,
|
|
33
|
+
Source,
|
|
34
|
+
Validation,
|
|
35
|
+
)
|
|
36
|
+
from .repository import BaseRepository
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Base classes
|
|
40
|
+
"Base",
|
|
41
|
+
"UUIDMixin",
|
|
42
|
+
"TimestampMixin",
|
|
43
|
+
"SoftDeleteMixin",
|
|
44
|
+
# Database functions
|
|
45
|
+
"get_session",
|
|
46
|
+
"get_db_session",
|
|
47
|
+
"get_engine",
|
|
48
|
+
"get_session_factory",
|
|
49
|
+
"init_db",
|
|
50
|
+
"reset_db",
|
|
51
|
+
"reset_connection",
|
|
52
|
+
# Models
|
|
53
|
+
"Source",
|
|
54
|
+
"Schema",
|
|
55
|
+
"Rule",
|
|
56
|
+
"Validation",
|
|
57
|
+
"Profile",
|
|
58
|
+
"Schedule",
|
|
59
|
+
"DriftComparison",
|
|
60
|
+
"AppSettings",
|
|
61
|
+
# Notification models (Phase 3)
|
|
62
|
+
"NotificationChannel",
|
|
63
|
+
"NotificationRule",
|
|
64
|
+
"NotificationLog",
|
|
65
|
+
# Repository
|
|
66
|
+
"BaseRepository",
|
|
67
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Database base classes and mixins for extensibility.
|
|
2
|
+
|
|
3
|
+
This module provides reusable base classes and mixins for SQLAlchemy models,
|
|
4
|
+
enabling consistent patterns across all database entities.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
class MyModel(Base, TimestampMixin, UUIDMixin):
|
|
8
|
+
__tablename__ = "my_models"
|
|
9
|
+
name: Mapped[str] = mapped_column(String(255))
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
from uuid import uuid4
|
|
17
|
+
|
|
18
|
+
from sqlalchemy import DateTime, String, func
|
|
19
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Base(DeclarativeBase):
|
|
23
|
+
"""Base class for all SQLAlchemy models.
|
|
24
|
+
|
|
25
|
+
Provides common functionality like automatic table naming
|
|
26
|
+
and serialization methods.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@declared_attr.directive
|
|
30
|
+
@classmethod
|
|
31
|
+
def __tablename__(cls) -> str:
|
|
32
|
+
"""Generate table name from class name (snake_case)."""
|
|
33
|
+
import re
|
|
34
|
+
|
|
35
|
+
name = cls.__name__
|
|
36
|
+
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower() + "s"
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
"""Convert model instance to dictionary.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary representation of the model.
|
|
43
|
+
"""
|
|
44
|
+
return {
|
|
45
|
+
column.name: getattr(self, column.name) for column in self.__table__.columns
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UUIDMixin:
|
|
50
|
+
"""Mixin that adds a UUID primary key.
|
|
51
|
+
|
|
52
|
+
Automatically generates a UUID4 string as the primary key.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
id: Mapped[str] = mapped_column(
|
|
56
|
+
String(36),
|
|
57
|
+
primary_key=True,
|
|
58
|
+
default=lambda: str(uuid4()),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TimestampMixin:
|
|
63
|
+
"""Mixin that adds created_at and updated_at timestamps.
|
|
64
|
+
|
|
65
|
+
Automatically sets created_at on insert and updates
|
|
66
|
+
updated_at on every modification.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
70
|
+
DateTime,
|
|
71
|
+
default=datetime.utcnow,
|
|
72
|
+
server_default=func.now(),
|
|
73
|
+
nullable=False,
|
|
74
|
+
)
|
|
75
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
76
|
+
DateTime,
|
|
77
|
+
default=datetime.utcnow,
|
|
78
|
+
onupdate=datetime.utcnow,
|
|
79
|
+
server_default=func.now(),
|
|
80
|
+
nullable=False,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SoftDeleteMixin:
|
|
85
|
+
"""Mixin that adds soft delete functionality.
|
|
86
|
+
|
|
87
|
+
Instead of actually deleting records, sets deleted_at timestamp.
|
|
88
|
+
Use with query filters to exclude deleted records.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
deleted_at: Mapped[datetime | None] = mapped_column(
|
|
92
|
+
DateTime,
|
|
93
|
+
nullable=True,
|
|
94
|
+
default=None,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def is_deleted(self) -> bool:
|
|
99
|
+
"""Check if record is soft-deleted."""
|
|
100
|
+
return self.deleted_at is not None
|
|
101
|
+
|
|
102
|
+
def soft_delete(self) -> None:
|
|
103
|
+
"""Mark record as deleted."""
|
|
104
|
+
self.deleted_at = datetime.utcnow()
|
|
105
|
+
|
|
106
|
+
def restore(self) -> None:
|
|
107
|
+
"""Restore soft-deleted record."""
|
|
108
|
+
self.deleted_at = None
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Database connection and session management.
|
|
2
|
+
|
|
3
|
+
This module provides async database connection handling using SQLAlchemy 2.0.
|
|
4
|
+
It supports both production SQLite and in-memory databases for testing.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
# Using context manager
|
|
8
|
+
async with get_session() as session:
|
|
9
|
+
result = await session.execute(select(Source))
|
|
10
|
+
sources = result.scalars().all()
|
|
11
|
+
|
|
12
|
+
# Using FastAPI dependency
|
|
13
|
+
@router.get("/sources")
|
|
14
|
+
async def list_sources(session: AsyncSession = Depends(get_db_session)):
|
|
15
|
+
...
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import AsyncGenerator
|
|
21
|
+
from contextlib import asynccontextmanager
|
|
22
|
+
|
|
23
|
+
from sqlalchemy.ext.asyncio import (
|
|
24
|
+
AsyncEngine,
|
|
25
|
+
AsyncSession,
|
|
26
|
+
async_sessionmaker,
|
|
27
|
+
create_async_engine,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from truthound_dashboard.config import get_settings
|
|
31
|
+
|
|
32
|
+
from .base import Base
|
|
33
|
+
|
|
34
|
+
# Global engine and session factory
|
|
35
|
+
_engine: AsyncEngine | None = None
|
|
36
|
+
_session_factory: async_sessionmaker[AsyncSession] | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_database_url(in_memory: bool = False) -> str:
|
|
40
|
+
"""Get database URL.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
in_memory: If True, use in-memory SQLite for testing.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
SQLAlchemy async database URL.
|
|
47
|
+
"""
|
|
48
|
+
if in_memory:
|
|
49
|
+
return "sqlite+aiosqlite:///:memory:"
|
|
50
|
+
|
|
51
|
+
settings = get_settings()
|
|
52
|
+
settings.ensure_directories()
|
|
53
|
+
return f"sqlite+aiosqlite:///{settings.database_path}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_engine(in_memory: bool = False) -> AsyncEngine:
|
|
57
|
+
"""Get or create database engine.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
in_memory: If True, create in-memory database.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
AsyncEngine instance.
|
|
64
|
+
"""
|
|
65
|
+
global _engine
|
|
66
|
+
|
|
67
|
+
if _engine is None or in_memory:
|
|
68
|
+
url = get_database_url(in_memory)
|
|
69
|
+
engine = create_async_engine(
|
|
70
|
+
url,
|
|
71
|
+
echo=False,
|
|
72
|
+
pool_pre_ping=True,
|
|
73
|
+
connect_args={"check_same_thread": False} if "sqlite" in url else {},
|
|
74
|
+
)
|
|
75
|
+
if not in_memory:
|
|
76
|
+
_engine = engine
|
|
77
|
+
return engine
|
|
78
|
+
|
|
79
|
+
return _engine
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_session_factory(
|
|
83
|
+
engine: AsyncEngine | None = None,
|
|
84
|
+
) -> async_sessionmaker[AsyncSession]:
|
|
85
|
+
"""Get or create session factory.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
engine: Optional engine to use. If None, uses default engine.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Session factory for creating database sessions.
|
|
92
|
+
"""
|
|
93
|
+
global _session_factory
|
|
94
|
+
|
|
95
|
+
if engine is not None:
|
|
96
|
+
# Create new factory for provided engine (used in testing)
|
|
97
|
+
return async_sessionmaker(
|
|
98
|
+
engine,
|
|
99
|
+
class_=AsyncSession,
|
|
100
|
+
expire_on_commit=False,
|
|
101
|
+
autoflush=False,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if _session_factory is None:
|
|
105
|
+
_session_factory = async_sessionmaker(
|
|
106
|
+
get_engine(),
|
|
107
|
+
class_=AsyncSession,
|
|
108
|
+
expire_on_commit=False,
|
|
109
|
+
autoflush=False,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return _session_factory
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@asynccontextmanager
|
|
116
|
+
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|
117
|
+
"""Get database session as async context manager.
|
|
118
|
+
|
|
119
|
+
Yields:
|
|
120
|
+
AsyncSession for database operations.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
async with get_session() as session:
|
|
124
|
+
result = await session.execute(select(Source))
|
|
125
|
+
"""
|
|
126
|
+
factory = get_session_factory()
|
|
127
|
+
async with factory() as session:
|
|
128
|
+
try:
|
|
129
|
+
yield session
|
|
130
|
+
await session.commit()
|
|
131
|
+
except Exception:
|
|
132
|
+
await session.rollback()
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
|
137
|
+
"""FastAPI dependency for database sessions.
|
|
138
|
+
|
|
139
|
+
Yields:
|
|
140
|
+
AsyncSession for use in route handlers.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
@router.get("/sources")
|
|
144
|
+
async def get_sources(session: AsyncSession = Depends(get_db_session)):
|
|
145
|
+
...
|
|
146
|
+
"""
|
|
147
|
+
async with get_session() as session:
|
|
148
|
+
yield session
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def init_db(engine: AsyncEngine | None = None) -> None:
|
|
152
|
+
"""Initialize database tables.
|
|
153
|
+
|
|
154
|
+
Creates all tables defined in models if they don't exist.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
engine: Optional engine to use. If None, uses default engine.
|
|
158
|
+
"""
|
|
159
|
+
target_engine = engine or get_engine()
|
|
160
|
+
async with target_engine.begin() as conn:
|
|
161
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def drop_db(engine: AsyncEngine | None = None) -> None:
|
|
165
|
+
"""Drop all database tables.
|
|
166
|
+
|
|
167
|
+
Warning: This will delete all data!
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
engine: Optional engine to use. If None, uses default engine.
|
|
171
|
+
"""
|
|
172
|
+
target_engine = engine or get_engine()
|
|
173
|
+
async with target_engine.begin() as conn:
|
|
174
|
+
await conn.run_sync(Base.metadata.drop_all)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def reset_db(engine: AsyncEngine | None = None) -> None:
|
|
178
|
+
"""Reset database by dropping and recreating all tables.
|
|
179
|
+
|
|
180
|
+
Warning: This will delete all data!
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
engine: Optional engine to use. If None, uses default engine.
|
|
184
|
+
"""
|
|
185
|
+
await drop_db(engine)
|
|
186
|
+
await init_db(engine)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def reset_connection() -> None:
|
|
190
|
+
"""Reset global engine and session factory.
|
|
191
|
+
|
|
192
|
+
Useful for testing or when configuration changes.
|
|
193
|
+
"""
|
|
194
|
+
global _engine, _session_factory
|
|
195
|
+
_engine = None
|
|
196
|
+
_session_factory = None
|