truthound-dashboard 1.4.3__py3-none-any.whl → 1.5.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 (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +437 -10
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +11 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.3.dist-info/METADATA +0 -505
  203. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -105,27 +105,18 @@ def _register_default_reporters(registry: ReporterRegistry) -> None:
105
105
  Args:
106
106
  registry: Registry to populate.
107
107
 
108
- Registers 6 reporters:
108
+ Registers 3 reporters:
109
109
  - HTML: Rich visual reports with themes
110
110
  - CSV: Spreadsheet-compatible format
111
111
  - JSON: Machine-readable structured data
112
- - Markdown: Documentation-friendly format
113
- - PDF: Print-ready documents
114
- - JUnit: CI/CD integration (Jenkins, GitHub Actions, etc.)
115
112
  """
116
113
  from .csv_reporter import CSVReporter
117
114
  from .html_reporter import HTMLReporter
118
115
  from .json_reporter import JSONReporter
119
- from .junit_reporter import JUnitReporter
120
- from .markdown_reporter import MarkdownReporter
121
- from .pdf_reporter import PDFReporter
122
116
 
123
117
  registry.register(ReportFormat.HTML, HTMLReporter)
124
118
  registry.register(ReportFormat.CSV, CSVReporter)
125
119
  registry.register(ReportFormat.JSON, JSONReporter)
126
- registry.register(ReportFormat.MARKDOWN, MarkdownReporter)
127
- registry.register(ReportFormat.PDF, PDFReporter)
128
- registry.register(ReportFormat.JUNIT, JUnitReporter)
129
120
 
130
121
  logger.debug(f"Registered {len(registry.available_formats)} default reporters")
131
122
 
@@ -34,6 +34,8 @@ from .notifications.dispatcher import create_dispatcher
34
34
  from .services import ValidationService
35
35
  from .truthound_adapter import get_adapter
36
36
  from .triggers import TriggerFactory, TriggerContext, TriggerEvaluation
37
+ from .schema_watcher import process_due_watchers
38
+ from .tiering import process_tiering_policies
37
39
 
38
40
  logger = logging.getLogger(__name__)
39
41
 
@@ -76,6 +78,10 @@ class ValidationScheduler:
76
78
  DATA_CHANGE_CHECK_INTERVAL_SECONDS = 60 # 1 minute (reduced for better responsiveness)
77
79
  # Default per-schedule check interval
78
80
  DEFAULT_SCHEDULE_CHECK_INTERVAL_MINUTES = 5
81
+ # Schema watcher check interval (how often to check for due watchers)
82
+ SCHEMA_WATCHER_CHECK_INTERVAL_SECONDS = 10 # Check every 10 seconds for due watchers
83
+ # Storage tiering check interval (default: check every hour)
84
+ TIERING_CHECK_INTERVAL_SECONDS = 3600 # 1 hour
79
85
 
80
86
  def __init__(
81
87
  self,
@@ -99,9 +105,19 @@ class ValidationScheduler:
99
105
  self._maintenance_cron = maintenance_cron or self.DEFAULT_MAINTENANCE_CRON
100
106
  self._maintenance_job_id = "system_maintenance"
101
107
  self._data_change_job_id = "data_change_check"
108
+ self._schema_watcher_job_id = "schema_watcher_check"
102
109
  self._data_change_check_interval = (
103
110
  data_change_check_interval or self.DATA_CHANGE_CHECK_INTERVAL_SECONDS
104
111
  )
112
+ self._schema_watcher_check_interval = self.SCHEMA_WATCHER_CHECK_INTERVAL_SECONDS
113
+
114
+ # Tiering state
115
+ self._tiering_job_id = "tiering_policy_check"
116
+ self._tiering_running = False
117
+ self._last_tiering_run: datetime | None = None
118
+ self._tiering_check_count = 0
119
+ self._tiering_processed_count = 0
120
+ self._tiering_check_interval = self.TIERING_CHECK_INTERVAL_SECONDS
105
121
 
106
122
  # Trigger monitoring state
107
123
  self._trigger_check_times: dict[str, datetime] = {} # schedule_id -> last_check_at
@@ -111,6 +127,12 @@ class ValidationScheduler:
111
127
  self._last_checker_run: datetime | None = None
112
128
  self._checker_running = False
113
129
 
130
+ # Schema watcher monitoring state
131
+ self._last_schema_watcher_run: datetime | None = None
132
+ self._schema_watcher_running = False
133
+ self._schema_watcher_check_count = 0
134
+ self._schema_watcher_processed_count = 0
135
+
114
136
  async def start(self) -> None:
115
137
  """Start the scheduler and load existing schedules."""
116
138
  logger.info("Starting validation scheduler")
@@ -124,6 +146,12 @@ class ValidationScheduler:
124
146
  # Start data change trigger checker
125
147
  self._schedule_data_change_checker()
126
148
 
149
+ # Start schema watcher checker
150
+ self._schedule_schema_watcher_checker()
151
+
152
+ # Start tiering policy checker
153
+ self._schedule_tiering_checker()
154
+
127
155
  async def stop(self) -> None:
128
156
  """Stop the scheduler."""
129
157
  logger.info("Stopping validation scheduler")
@@ -500,6 +528,143 @@ class ValidationScheduler:
500
528
  except Exception as e:
501
529
  logger.error(f"Failed to schedule data change checker: {e}")
502
530
 
531
+ def _schedule_schema_watcher_checker(self) -> None:
532
+ """Schedule periodic checker for schema watchers."""
533
+ try:
534
+ self._scheduler.add_job(
535
+ self._check_schema_watchers,
536
+ trigger=IntervalTrigger(seconds=self._schema_watcher_check_interval),
537
+ id=self._schema_watcher_job_id,
538
+ name="Schema Watcher Checker",
539
+ replace_existing=True,
540
+ )
541
+ logger.info(
542
+ f"Scheduled schema watcher checker: every {self._schema_watcher_check_interval}s"
543
+ )
544
+ except Exception as e:
545
+ logger.error(f"Failed to schedule schema watcher checker: {e}")
546
+
547
+ async def _check_schema_watchers(self) -> None:
548
+ """Check all schema watchers that are due for checking.
549
+
550
+ This runs periodically to process watchers whose next_check_at
551
+ timestamp has passed.
552
+
553
+ Features:
554
+ - Processes all due watchers in order
555
+ - Updates watcher state after each check
556
+ - Creates alerts for detected changes
557
+ - Sends notifications if enabled
558
+ """
559
+ if self._schema_watcher_running:
560
+ logger.debug("Schema watcher check already running, skipping")
561
+ return
562
+
563
+ self._schema_watcher_running = True
564
+ self._last_schema_watcher_run = datetime.utcnow()
565
+ self._schema_watcher_check_count += 1
566
+ logger.debug("Checking schema watchers")
567
+
568
+ try:
569
+ async with get_session() as session:
570
+ processed = await process_due_watchers(session)
571
+ self._schema_watcher_processed_count += processed
572
+
573
+ if processed > 0:
574
+ logger.info(f"Processed {processed} schema watcher(s)")
575
+ except Exception as e:
576
+ logger.error(f"Error checking schema watchers: {e}")
577
+ finally:
578
+ self._schema_watcher_running = False
579
+
580
+ def get_schema_watcher_status(self) -> dict[str, Any]:
581
+ """Get current schema watcher checker status.
582
+
583
+ Returns:
584
+ Dictionary with schema watcher checker stats.
585
+ """
586
+ return {
587
+ "enabled": True,
588
+ "checker_running": self._schema_watcher_running,
589
+ "checker_interval_seconds": self._schema_watcher_check_interval,
590
+ "last_checker_run_at": (
591
+ self._last_schema_watcher_run.isoformat()
592
+ if self._last_schema_watcher_run else None
593
+ ),
594
+ "total_checks": self._schema_watcher_check_count,
595
+ "total_processed": self._schema_watcher_processed_count,
596
+ }
597
+
598
+ def _schedule_tiering_checker(self) -> None:
599
+ """Schedule periodic checker for storage tiering policies."""
600
+ try:
601
+ self._scheduler.add_job(
602
+ self._check_tiering_policies,
603
+ trigger=IntervalTrigger(seconds=self._tiering_check_interval),
604
+ id=self._tiering_job_id,
605
+ name="Tiering Policy Checker",
606
+ replace_existing=True,
607
+ )
608
+ logger.info(
609
+ f"Scheduled tiering policy checker: every {self._tiering_check_interval}s"
610
+ )
611
+ except Exception as e:
612
+ logger.error(f"Failed to schedule tiering policy checker: {e}")
613
+
614
+ async def _check_tiering_policies(self) -> None:
615
+ """Check and execute all active tiering policies.
616
+
617
+ This runs periodically to evaluate tier policies and migrate
618
+ eligible items between storage tiers.
619
+
620
+ Features:
621
+ - Processes all active policies in priority order
622
+ - Records migration history
623
+ - Respects batch size limits from configuration
624
+ """
625
+ if self._tiering_running:
626
+ logger.debug("Tiering policy check already running, skipping")
627
+ return
628
+
629
+ self._tiering_running = True
630
+ self._last_tiering_run = datetime.utcnow()
631
+ self._tiering_check_count += 1
632
+ logger.debug("Checking tiering policies")
633
+
634
+ try:
635
+ async with get_session() as session:
636
+ results = await process_tiering_policies(session)
637
+ total_migrated = sum(r.items_migrated for r in results)
638
+ self._tiering_processed_count += total_migrated
639
+
640
+ if total_migrated > 0:
641
+ logger.info(
642
+ f"Tiering: migrated {total_migrated} item(s) "
643
+ f"across {len(results)} policy(ies)"
644
+ )
645
+ except Exception as e:
646
+ logger.error(f"Error checking tiering policies: {e}")
647
+ finally:
648
+ self._tiering_running = False
649
+
650
+ def get_tiering_status(self) -> dict[str, Any]:
651
+ """Get current tiering checker status.
652
+
653
+ Returns:
654
+ Dictionary with tiering checker stats.
655
+ """
656
+ return {
657
+ "enabled": True,
658
+ "checker_running": self._tiering_running,
659
+ "checker_interval_seconds": self._tiering_check_interval,
660
+ "last_checker_run_at": (
661
+ self._last_tiering_run.isoformat()
662
+ if self._last_tiering_run else None
663
+ ),
664
+ "total_checks": self._tiering_check_count,
665
+ "total_migrated": self._tiering_processed_count,
666
+ }
667
+
503
668
  async def _check_data_change_triggers(self) -> None:
504
669
  """Check all data change and composite triggers.
505
670
 
@@ -84,7 +84,7 @@ class SchemaVersionRepository(BaseRepository[SchemaVersion]):
84
84
  offset=offset,
85
85
  limit=limit,
86
86
  filters=[SchemaVersion.source_id == source_id],
87
- order_by=[SchemaVersion.version_number.desc()],
87
+ order_by=SchemaVersion.version_number.desc(),
88
88
  )
89
89
 
90
90
  async def get_next_version_number(self, source_id: str) -> int:
@@ -151,7 +151,7 @@ class SchemaChangeRepository(BaseRepository[SchemaChange]):
151
151
  offset=offset,
152
152
  limit=limit,
153
153
  filters=[SchemaChange.source_id == source_id],
154
- order_by=[SchemaChange.created_at.desc()],
154
+ order_by=SchemaChange.created_at.desc(),
155
155
  )
156
156
 
157
157
  async def get_for_version(
@@ -167,7 +167,7 @@ class SchemaChangeRepository(BaseRepository[SchemaChange]):
167
167
  """
168
168
  return await self.list(
169
169
  filters=[SchemaChange.to_version_id == to_version_id],
170
- order_by=[SchemaChange.created_at.desc()],
170
+ order_by=SchemaChange.created_at.desc(),
171
171
  )
172
172
 
173
173
  async def count_breaking_changes(self, source_id: str) -> int: