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,132 @@
|
|
|
1
|
+
"""Configuration settings with validation and extensibility.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized configuration management system using
|
|
4
|
+
pydantic-settings for type-safe environment variable handling.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
# Get settings singleton
|
|
8
|
+
settings = get_settings()
|
|
9
|
+
|
|
10
|
+
# Access configuration
|
|
11
|
+
print(settings.database_path)
|
|
12
|
+
|
|
13
|
+
# Override via environment variables
|
|
14
|
+
# TRUTHOUND_DATA_DIR=/custom/path
|
|
15
|
+
# TRUTHOUND_PORT=9000
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from functools import lru_cache
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
from pydantic import Field, field_validator
|
|
25
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Settings(BaseSettings):
|
|
29
|
+
"""Dashboard configuration settings.
|
|
30
|
+
|
|
31
|
+
All settings can be overridden via environment variables with
|
|
32
|
+
the TRUTHOUND_ prefix.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
data_dir: Directory for storing database and cache files.
|
|
36
|
+
host: Server host address.
|
|
37
|
+
port: Server port number.
|
|
38
|
+
log_level: Logging verbosity level.
|
|
39
|
+
auth_enabled: Whether authentication is required.
|
|
40
|
+
auth_password: Password for basic auth (if enabled).
|
|
41
|
+
sample_size: Default sample size for validation.
|
|
42
|
+
max_failed_rows: Maximum failed rows to store.
|
|
43
|
+
default_timeout: Default timeout for operations in seconds.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
model_config = SettingsConfigDict(
|
|
47
|
+
env_prefix="TRUTHOUND_",
|
|
48
|
+
env_file=".env",
|
|
49
|
+
env_file_encoding="utf-8",
|
|
50
|
+
extra="ignore",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Data storage
|
|
54
|
+
data_dir: Path = Field(
|
|
55
|
+
default_factory=lambda: Path.home() / ".truthound",
|
|
56
|
+
description="Directory for database and cache files",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Server configuration
|
|
60
|
+
host: str = Field(default="127.0.0.1", description="Server host address")
|
|
61
|
+
port: int = Field(default=8765, ge=1, le=65535, description="Server port")
|
|
62
|
+
log_level: Literal["debug", "info", "warning", "error"] = Field(
|
|
63
|
+
default="info", description="Logging level"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Authentication (optional)
|
|
67
|
+
auth_enabled: bool = Field(default=False, description="Enable authentication")
|
|
68
|
+
auth_password: str | None = Field(
|
|
69
|
+
default=None, description="Password for basic auth"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Validation defaults
|
|
73
|
+
sample_size: int = Field(
|
|
74
|
+
default=10000, ge=100, description="Default sample size for validation"
|
|
75
|
+
)
|
|
76
|
+
max_failed_rows: int = Field(
|
|
77
|
+
default=1000, ge=10, description="Maximum failed rows to store"
|
|
78
|
+
)
|
|
79
|
+
default_timeout: int = Field(
|
|
80
|
+
default=300, ge=10, description="Default operation timeout in seconds"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Worker configuration
|
|
84
|
+
max_workers: int = Field(
|
|
85
|
+
default=4, ge=1, le=32, description="Maximum worker threads"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@field_validator("data_dir", mode="before")
|
|
89
|
+
@classmethod
|
|
90
|
+
def expand_data_dir(cls, v: str | Path) -> Path:
|
|
91
|
+
"""Expand user home directory and resolve path."""
|
|
92
|
+
return Path(v).expanduser().resolve()
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def database_path(self) -> Path:
|
|
96
|
+
"""Get SQLite database file path."""
|
|
97
|
+
return self.data_dir / "dashboard.db"
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def cache_dir(self) -> Path:
|
|
101
|
+
"""Get cache directory path."""
|
|
102
|
+
return self.data_dir / "cache"
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def schema_dir(self) -> Path:
|
|
106
|
+
"""Get schema storage directory path."""
|
|
107
|
+
return self.data_dir / "schemas"
|
|
108
|
+
|
|
109
|
+
def ensure_directories(self) -> None:
|
|
110
|
+
"""Create all required directories if they don't exist."""
|
|
111
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
self.schema_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@lru_cache
|
|
117
|
+
def get_settings() -> Settings:
|
|
118
|
+
"""Get cached settings singleton.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Settings: The application settings instance.
|
|
122
|
+
|
|
123
|
+
Note:
|
|
124
|
+
This function is cached, so the settings are only loaded once.
|
|
125
|
+
To reload settings, clear the cache with get_settings.cache_clear().
|
|
126
|
+
"""
|
|
127
|
+
return Settings()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def reset_settings() -> None:
|
|
131
|
+
"""Reset settings cache for testing purposes."""
|
|
132
|
+
get_settings.cache_clear()
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Core business logic module.
|
|
2
|
+
|
|
3
|
+
This module contains the core business logic for the dashboard,
|
|
4
|
+
including services, adapters, and domain models.
|
|
5
|
+
|
|
6
|
+
Exports:
|
|
7
|
+
- Adapter: TruthoundAdapter, get_adapter
|
|
8
|
+
- Services: SourceService, ValidationService, SchemaService, RuleService, ProfileService,
|
|
9
|
+
HistoryService, DriftService, ScheduleService
|
|
10
|
+
- Result types: CheckResult, LearnResult, ProfileResult, CompareResult
|
|
11
|
+
- Scheduler: ValidationScheduler, get_scheduler, start_scheduler, stop_scheduler
|
|
12
|
+
- Notifications: NotificationDispatcher, create_dispatcher, get_dispatcher
|
|
13
|
+
- Cache: CacheBackend, MemoryCache, FileCache, get_cache, get_cache_manager
|
|
14
|
+
- Maintenance: MaintenanceManager, get_maintenance_manager, cleanup_old_validations
|
|
15
|
+
- Sampling: DataSampler, SamplingStrategy, get_sampler (Large Dataset Handling)
|
|
16
|
+
- Exceptions: TruthoundDashboardError, SourceNotFoundError, ValidationError, etc.
|
|
17
|
+
- Encryption: encrypt_value, decrypt_value, encrypt_config, decrypt_config
|
|
18
|
+
- Logging: setup_logging, get_logger, get_audit_logger
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .base import BaseService, CRUDService
|
|
22
|
+
from .cache import (
|
|
23
|
+
CacheBackend,
|
|
24
|
+
CacheManager,
|
|
25
|
+
FileCache,
|
|
26
|
+
MemoryCache,
|
|
27
|
+
get_cache,
|
|
28
|
+
get_cache_manager,
|
|
29
|
+
reset_cache,
|
|
30
|
+
)
|
|
31
|
+
from .encryption import (
|
|
32
|
+
EncryptionError,
|
|
33
|
+
Encryptor,
|
|
34
|
+
decrypt_config,
|
|
35
|
+
decrypt_value,
|
|
36
|
+
encrypt_config,
|
|
37
|
+
encrypt_value,
|
|
38
|
+
get_encryptor,
|
|
39
|
+
is_sensitive_field,
|
|
40
|
+
mask_sensitive_value,
|
|
41
|
+
)
|
|
42
|
+
from .exceptions import (
|
|
43
|
+
AuthenticationFailedError,
|
|
44
|
+
AuthenticationRequiredError,
|
|
45
|
+
AuthorizationError,
|
|
46
|
+
DatabaseConnectionError,
|
|
47
|
+
DatabaseError,
|
|
48
|
+
DatabaseIntegrityError,
|
|
49
|
+
ErrorCode,
|
|
50
|
+
NotificationChannelNotFoundError,
|
|
51
|
+
NotificationError,
|
|
52
|
+
NotificationInvalidConfigError,
|
|
53
|
+
NotificationRuleNotFoundError,
|
|
54
|
+
NotificationSendError,
|
|
55
|
+
RateLimitExceededError,
|
|
56
|
+
RuleError,
|
|
57
|
+
RuleInvalidError,
|
|
58
|
+
RuleNotFoundError,
|
|
59
|
+
RuleParseError,
|
|
60
|
+
ScheduleConflictError,
|
|
61
|
+
ScheduleError,
|
|
62
|
+
ScheduleInvalidCronError,
|
|
63
|
+
ScheduleNotFoundError,
|
|
64
|
+
SchemaError,
|
|
65
|
+
SchemaInvalidError,
|
|
66
|
+
SchemaNotFoundError,
|
|
67
|
+
SchemaParseError,
|
|
68
|
+
SecurityError,
|
|
69
|
+
SourceAccessDeniedError,
|
|
70
|
+
SourceConnectionError,
|
|
71
|
+
SourceError,
|
|
72
|
+
SourceInvalidConfigError,
|
|
73
|
+
SourceNotFoundError,
|
|
74
|
+
TruthoundDashboardError,
|
|
75
|
+
ValidationError,
|
|
76
|
+
ValidationFailedError,
|
|
77
|
+
ValidationNotFoundError,
|
|
78
|
+
ValidationTimeoutError,
|
|
79
|
+
get_error_message,
|
|
80
|
+
)
|
|
81
|
+
from .logging import (
|
|
82
|
+
AuditLogger,
|
|
83
|
+
LogConfig,
|
|
84
|
+
LoggerAdapter,
|
|
85
|
+
get_audit_logger,
|
|
86
|
+
get_logger,
|
|
87
|
+
setup_logging,
|
|
88
|
+
)
|
|
89
|
+
from .maintenance import (
|
|
90
|
+
CleanupResult,
|
|
91
|
+
CleanupStrategy,
|
|
92
|
+
MaintenanceConfig,
|
|
93
|
+
MaintenanceManager,
|
|
94
|
+
MaintenanceReport,
|
|
95
|
+
cleanup_notification_logs,
|
|
96
|
+
cleanup_old_profiles,
|
|
97
|
+
cleanup_old_validations,
|
|
98
|
+
get_maintenance_manager,
|
|
99
|
+
reset_maintenance_manager,
|
|
100
|
+
vacuum_database,
|
|
101
|
+
)
|
|
102
|
+
from .notifications import (
|
|
103
|
+
NotificationDispatcher,
|
|
104
|
+
create_dispatcher,
|
|
105
|
+
get_dispatcher,
|
|
106
|
+
)
|
|
107
|
+
from .sampling import (
|
|
108
|
+
DataSampler,
|
|
109
|
+
HeadSamplingStrategy,
|
|
110
|
+
RandomSamplingStrategy,
|
|
111
|
+
SamplingConfig,
|
|
112
|
+
SamplingMethod,
|
|
113
|
+
SamplingResult,
|
|
114
|
+
SamplingStrategy,
|
|
115
|
+
StratifiedSamplingStrategy,
|
|
116
|
+
TailSamplingStrategy,
|
|
117
|
+
get_sampler,
|
|
118
|
+
reset_sampler,
|
|
119
|
+
)
|
|
120
|
+
from .scheduler import (
|
|
121
|
+
ValidationScheduler,
|
|
122
|
+
get_scheduler,
|
|
123
|
+
start_scheduler,
|
|
124
|
+
stop_scheduler,
|
|
125
|
+
)
|
|
126
|
+
from .services import (
|
|
127
|
+
DriftService,
|
|
128
|
+
HistoryService,
|
|
129
|
+
ProfileService,
|
|
130
|
+
RuleService,
|
|
131
|
+
ScheduleService,
|
|
132
|
+
SchemaService,
|
|
133
|
+
SourceService,
|
|
134
|
+
ValidationService,
|
|
135
|
+
)
|
|
136
|
+
from .truthound_adapter import (
|
|
137
|
+
CheckResult,
|
|
138
|
+
CompareResult,
|
|
139
|
+
LearnResult,
|
|
140
|
+
ProfileResult,
|
|
141
|
+
TruthoundAdapter,
|
|
142
|
+
get_adapter,
|
|
143
|
+
reset_adapter,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
__all__ = [
|
|
147
|
+
# Base classes
|
|
148
|
+
"BaseService",
|
|
149
|
+
"CRUDService",
|
|
150
|
+
# Services
|
|
151
|
+
"SourceService",
|
|
152
|
+
"ValidationService",
|
|
153
|
+
"SchemaService",
|
|
154
|
+
"RuleService",
|
|
155
|
+
"ProfileService",
|
|
156
|
+
"HistoryService",
|
|
157
|
+
"DriftService",
|
|
158
|
+
"ScheduleService",
|
|
159
|
+
# Adapter
|
|
160
|
+
"TruthoundAdapter",
|
|
161
|
+
"get_adapter",
|
|
162
|
+
"reset_adapter",
|
|
163
|
+
# Result types
|
|
164
|
+
"CheckResult",
|
|
165
|
+
"LearnResult",
|
|
166
|
+
"ProfileResult",
|
|
167
|
+
"CompareResult",
|
|
168
|
+
# Scheduler
|
|
169
|
+
"ValidationScheduler",
|
|
170
|
+
"get_scheduler",
|
|
171
|
+
"start_scheduler",
|
|
172
|
+
"stop_scheduler",
|
|
173
|
+
# Notifications
|
|
174
|
+
"NotificationDispatcher",
|
|
175
|
+
"create_dispatcher",
|
|
176
|
+
"get_dispatcher",
|
|
177
|
+
# Cache (Phase 4)
|
|
178
|
+
"CacheBackend",
|
|
179
|
+
"MemoryCache",
|
|
180
|
+
"FileCache",
|
|
181
|
+
"CacheManager",
|
|
182
|
+
"get_cache",
|
|
183
|
+
"get_cache_manager",
|
|
184
|
+
"reset_cache",
|
|
185
|
+
# Maintenance (Phase 4)
|
|
186
|
+
"MaintenanceManager",
|
|
187
|
+
"MaintenanceConfig",
|
|
188
|
+
"MaintenanceReport",
|
|
189
|
+
"CleanupResult",
|
|
190
|
+
"CleanupStrategy",
|
|
191
|
+
"get_maintenance_manager",
|
|
192
|
+
"reset_maintenance_manager",
|
|
193
|
+
"cleanup_old_validations",
|
|
194
|
+
"cleanup_old_profiles",
|
|
195
|
+
"cleanup_notification_logs",
|
|
196
|
+
"vacuum_database",
|
|
197
|
+
# Exceptions (Phase 4)
|
|
198
|
+
"TruthoundDashboardError",
|
|
199
|
+
"ErrorCode",
|
|
200
|
+
"get_error_message",
|
|
201
|
+
"SourceError",
|
|
202
|
+
"SourceNotFoundError",
|
|
203
|
+
"SourceConnectionError",
|
|
204
|
+
"SourceInvalidConfigError",
|
|
205
|
+
"SourceAccessDeniedError",
|
|
206
|
+
"SchemaError",
|
|
207
|
+
"SchemaNotFoundError",
|
|
208
|
+
"SchemaInvalidError",
|
|
209
|
+
"SchemaParseError",
|
|
210
|
+
"RuleError",
|
|
211
|
+
"RuleNotFoundError",
|
|
212
|
+
"RuleInvalidError",
|
|
213
|
+
"RuleParseError",
|
|
214
|
+
"ValidationError",
|
|
215
|
+
"ValidationNotFoundError",
|
|
216
|
+
"ValidationFailedError",
|
|
217
|
+
"ValidationTimeoutError",
|
|
218
|
+
"ScheduleError",
|
|
219
|
+
"ScheduleNotFoundError",
|
|
220
|
+
"ScheduleInvalidCronError",
|
|
221
|
+
"ScheduleConflictError",
|
|
222
|
+
"NotificationError",
|
|
223
|
+
"NotificationChannelNotFoundError",
|
|
224
|
+
"NotificationRuleNotFoundError",
|
|
225
|
+
"NotificationSendError",
|
|
226
|
+
"NotificationInvalidConfigError",
|
|
227
|
+
"SecurityError",
|
|
228
|
+
"AuthenticationRequiredError",
|
|
229
|
+
"AuthenticationFailedError",
|
|
230
|
+
"AuthorizationError",
|
|
231
|
+
"RateLimitExceededError",
|
|
232
|
+
"DatabaseError",
|
|
233
|
+
"DatabaseConnectionError",
|
|
234
|
+
"DatabaseIntegrityError",
|
|
235
|
+
# Encryption (Phase 4)
|
|
236
|
+
"Encryptor",
|
|
237
|
+
"EncryptionError",
|
|
238
|
+
"get_encryptor",
|
|
239
|
+
"encrypt_value",
|
|
240
|
+
"decrypt_value",
|
|
241
|
+
"encrypt_config",
|
|
242
|
+
"decrypt_config",
|
|
243
|
+
"is_sensitive_field",
|
|
244
|
+
"mask_sensitive_value",
|
|
245
|
+
# Logging (Phase 4)
|
|
246
|
+
"LogConfig",
|
|
247
|
+
"LoggerAdapter",
|
|
248
|
+
"AuditLogger",
|
|
249
|
+
"setup_logging",
|
|
250
|
+
"get_logger",
|
|
251
|
+
"get_audit_logger",
|
|
252
|
+
# Sampling (Phase 4 - Large Dataset Handling)
|
|
253
|
+
"DataSampler",
|
|
254
|
+
"SamplingConfig",
|
|
255
|
+
"SamplingMethod",
|
|
256
|
+
"SamplingResult",
|
|
257
|
+
"SamplingStrategy",
|
|
258
|
+
"RandomSamplingStrategy",
|
|
259
|
+
"HeadSamplingStrategy",
|
|
260
|
+
"TailSamplingStrategy",
|
|
261
|
+
"StratifiedSamplingStrategy",
|
|
262
|
+
"get_sampler",
|
|
263
|
+
"reset_sampler",
|
|
264
|
+
]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Base classes for core services.
|
|
2
|
+
|
|
3
|
+
This module provides abstract base classes and protocols for
|
|
4
|
+
implementing core business logic services with consistent patterns.
|
|
5
|
+
|
|
6
|
+
The service pattern separates business logic from API handlers,
|
|
7
|
+
enabling better testability and reusability.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Any, Generic, TypeVar
|
|
14
|
+
|
|
15
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
16
|
+
|
|
17
|
+
from truthound_dashboard.db.repository import BaseRepository
|
|
18
|
+
|
|
19
|
+
# Type variables
|
|
20
|
+
ModelT = TypeVar("ModelT")
|
|
21
|
+
CreateSchemaT = TypeVar("CreateSchemaT")
|
|
22
|
+
UpdateSchemaT = TypeVar("UpdateSchemaT")
|
|
23
|
+
ResponseSchemaT = TypeVar("ResponseSchemaT")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseService(ABC, Generic[ModelT]):
|
|
27
|
+
"""Abstract base class for services.
|
|
28
|
+
|
|
29
|
+
Services encapsulate business logic and orchestrate
|
|
30
|
+
operations between repositories and external systems.
|
|
31
|
+
|
|
32
|
+
Type Parameters:
|
|
33
|
+
ModelT: The model type this service manages.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
37
|
+
"""Initialize service with database session.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
session: Async database session.
|
|
41
|
+
"""
|
|
42
|
+
self.session = session
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def get_by_id(self, id: str) -> ModelT | None:
|
|
46
|
+
"""Get entity by ID."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def list(self, *, offset: int = 0, limit: int = 100) -> list[ModelT]:
|
|
51
|
+
"""List entities with pagination."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CRUDService(BaseService[ModelT], Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
56
|
+
"""Base service with full CRUD operations.
|
|
57
|
+
|
|
58
|
+
Extends BaseService with create, update, and delete operations.
|
|
59
|
+
|
|
60
|
+
Type Parameters:
|
|
61
|
+
ModelT: The model type.
|
|
62
|
+
CreateSchemaT: Pydantic schema for creation.
|
|
63
|
+
UpdateSchemaT: Pydantic schema for updates.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
repository_class: type[BaseRepository[ModelT]]
|
|
67
|
+
|
|
68
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
69
|
+
super().__init__(session)
|
|
70
|
+
self.repository = self.repository_class(session)
|
|
71
|
+
|
|
72
|
+
async def get_by_id(self, id: str) -> ModelT | None:
|
|
73
|
+
"""Get entity by ID.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
id: Entity's unique identifier.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Model instance or None if not found.
|
|
80
|
+
"""
|
|
81
|
+
return await self.repository.get_by_id(id)
|
|
82
|
+
|
|
83
|
+
async def list(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
offset: int = 0,
|
|
87
|
+
limit: int = 100,
|
|
88
|
+
**filters: Any,
|
|
89
|
+
) -> list[ModelT]:
|
|
90
|
+
"""List entities with pagination and filtering.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
offset: Number of records to skip.
|
|
94
|
+
limit: Maximum records to return.
|
|
95
|
+
**filters: Additional filter criteria.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of model instances.
|
|
99
|
+
"""
|
|
100
|
+
filter_conditions = self._build_filters(**filters)
|
|
101
|
+
result = await self.repository.list(
|
|
102
|
+
offset=offset,
|
|
103
|
+
limit=limit,
|
|
104
|
+
filters=filter_conditions,
|
|
105
|
+
)
|
|
106
|
+
return list(result)
|
|
107
|
+
|
|
108
|
+
async def create(self, data: CreateSchemaT) -> ModelT:
|
|
109
|
+
"""Create new entity.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
data: Pydantic schema with creation data.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Created model instance.
|
|
116
|
+
"""
|
|
117
|
+
create_data = self._prepare_create_data(data)
|
|
118
|
+
return await self.repository.create(**create_data)
|
|
119
|
+
|
|
120
|
+
async def update(self, id: str, data: UpdateSchemaT) -> ModelT | None:
|
|
121
|
+
"""Update existing entity.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
id: Entity's unique identifier.
|
|
125
|
+
data: Pydantic schema with update data.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Updated model instance or None if not found.
|
|
129
|
+
"""
|
|
130
|
+
update_data = self._prepare_update_data(data)
|
|
131
|
+
return await self.repository.update(id, **update_data)
|
|
132
|
+
|
|
133
|
+
async def delete(self, id: str) -> bool:
|
|
134
|
+
"""Delete entity by ID.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
id: Entity's unique identifier.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if deleted, False if not found.
|
|
141
|
+
"""
|
|
142
|
+
return await self.repository.delete(id)
|
|
143
|
+
|
|
144
|
+
def _prepare_create_data(self, data: CreateSchemaT) -> dict[str, Any]:
|
|
145
|
+
"""Prepare data for creation.
|
|
146
|
+
|
|
147
|
+
Override to customize creation data processing.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
data: Pydantic schema with creation data.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dictionary of field values.
|
|
154
|
+
"""
|
|
155
|
+
if hasattr(data, "model_dump"):
|
|
156
|
+
return data.model_dump(exclude_unset=True) # type: ignore
|
|
157
|
+
return dict(data) # type: ignore
|
|
158
|
+
|
|
159
|
+
def _prepare_update_data(self, data: UpdateSchemaT) -> dict[str, Any]:
|
|
160
|
+
"""Prepare data for update.
|
|
161
|
+
|
|
162
|
+
Override to customize update data processing.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
data: Pydantic schema with update data.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dictionary of field values (excluding None).
|
|
169
|
+
"""
|
|
170
|
+
if hasattr(data, "model_dump"):
|
|
171
|
+
return data.model_dump(exclude_unset=True, exclude_none=True) # type: ignore
|
|
172
|
+
return {k: v for k, v in dict(data).items() if v is not None} # type: ignore
|
|
173
|
+
|
|
174
|
+
def _build_filters(self, **_filters: Any) -> list[Any]:
|
|
175
|
+
"""Build SQLAlchemy filter conditions.
|
|
176
|
+
|
|
177
|
+
Override to customize filter building.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
**filters: Filter criteria.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of SQLAlchemy filter conditions.
|
|
184
|
+
"""
|
|
185
|
+
return []
|