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
@@ -0,0 +1,270 @@
1
+ """Checkpoint runner for managing and executing checkpoints.
2
+
3
+ Provides the CheckpointRunner class for registering, managing,
4
+ and executing multiple checkpoints.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ from typing import Any
12
+
13
+ from truthound_dashboard.core.interfaces.checkpoint import (
14
+ CheckpointProtocol,
15
+ CheckpointResult,
16
+ CheckpointRunnerProtocol,
17
+ CheckpointStatus,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class CheckpointRunner(CheckpointRunnerProtocol):
24
+ """Runner for managing and executing checkpoints.
25
+
26
+ Provides a central registry for checkpoints and methods for
27
+ running them individually or in batch.
28
+
29
+ Example:
30
+ runner = CheckpointRunner()
31
+
32
+ # Register checkpoints
33
+ runner.register(orders_checkpoint)
34
+ runner.register(customers_checkpoint)
35
+
36
+ # Run a specific checkpoint
37
+ result = await runner.run("daily_orders")
38
+
39
+ # Run all enabled checkpoints
40
+ results = await runner.run_all(parallel=True)
41
+ """
42
+
43
+ def __init__(self, max_workers: int = 4) -> None:
44
+ """Initialize runner.
45
+
46
+ Args:
47
+ max_workers: Max parallel workers for run_all.
48
+ """
49
+ self._checkpoints: dict[str, CheckpointProtocol] = {}
50
+ self._max_workers = max_workers
51
+
52
+ def register(self, checkpoint: CheckpointProtocol) -> None:
53
+ """Register a checkpoint.
54
+
55
+ Args:
56
+ checkpoint: Checkpoint to register.
57
+ """
58
+ self._checkpoints[checkpoint.name] = checkpoint
59
+ logger.info(f"Registered checkpoint: {checkpoint.name}")
60
+
61
+ def unregister(self, name: str) -> bool:
62
+ """Unregister a checkpoint by name.
63
+
64
+ Args:
65
+ name: Checkpoint name.
66
+
67
+ Returns:
68
+ True if checkpoint was unregistered.
69
+ """
70
+ if name in self._checkpoints:
71
+ del self._checkpoints[name]
72
+ logger.info(f"Unregistered checkpoint: {name}")
73
+ return True
74
+ return False
75
+
76
+ def get(self, name: str) -> CheckpointProtocol | None:
77
+ """Get a checkpoint by name.
78
+
79
+ Args:
80
+ name: Checkpoint name.
81
+
82
+ Returns:
83
+ Checkpoint or None if not found.
84
+ """
85
+ return self._checkpoints.get(name)
86
+
87
+ def list_checkpoints(self) -> list[str]:
88
+ """List all registered checkpoint names.
89
+
90
+ Returns:
91
+ List of checkpoint names.
92
+ """
93
+ return list(self._checkpoints.keys())
94
+
95
+ def get_all(self) -> list[CheckpointProtocol]:
96
+ """Get all registered checkpoints.
97
+
98
+ Returns:
99
+ List of checkpoints.
100
+ """
101
+ return list(self._checkpoints.values())
102
+
103
+ async def run(
104
+ self,
105
+ name: str,
106
+ trigger_context: dict[str, Any] | None = None,
107
+ ) -> CheckpointResult:
108
+ """Run a checkpoint by name.
109
+
110
+ Args:
111
+ name: Checkpoint name.
112
+ trigger_context: Optional trigger context.
113
+
114
+ Returns:
115
+ Checkpoint result.
116
+
117
+ Raises:
118
+ KeyError: If checkpoint not found.
119
+ """
120
+ checkpoint = self.get(name)
121
+ if checkpoint is None:
122
+ raise KeyError(f"Checkpoint not found: {name}")
123
+
124
+ return await checkpoint.run(trigger_context=trigger_context)
125
+
126
+ async def run_all(
127
+ self,
128
+ parallel: bool = False,
129
+ max_workers: int | None = None,
130
+ filter_enabled: bool = True,
131
+ ) -> list[CheckpointResult]:
132
+ """Run all registered checkpoints.
133
+
134
+ Args:
135
+ parallel: Run in parallel.
136
+ max_workers: Max parallel workers (uses instance default if None).
137
+ filter_enabled: Only run enabled checkpoints.
138
+
139
+ Returns:
140
+ List of checkpoint results.
141
+ """
142
+ max_workers = max_workers or self._max_workers
143
+
144
+ # Filter checkpoints
145
+ checkpoints = self.get_all()
146
+ if filter_enabled:
147
+ checkpoints = [cp for cp in checkpoints if cp.config.enabled]
148
+
149
+ if not checkpoints:
150
+ logger.info("No checkpoints to run")
151
+ return []
152
+
153
+ logger.info(f"Running {len(checkpoints)} checkpoints (parallel={parallel})")
154
+
155
+ if parallel:
156
+ return await self._run_parallel(checkpoints, max_workers)
157
+ else:
158
+ return await self._run_sequential(checkpoints)
159
+
160
+ async def _run_sequential(
161
+ self, checkpoints: list[CheckpointProtocol]
162
+ ) -> list[CheckpointResult]:
163
+ """Run checkpoints sequentially.
164
+
165
+ Args:
166
+ checkpoints: Checkpoints to run.
167
+
168
+ Returns:
169
+ List of results.
170
+ """
171
+ results: list[CheckpointResult] = []
172
+ for checkpoint in checkpoints:
173
+ try:
174
+ result = await checkpoint.run()
175
+ results.append(result)
176
+ except Exception as e:
177
+ logger.error(f"Checkpoint failed: {checkpoint.name} error={str(e)}")
178
+ results.append(CheckpointResult(
179
+ checkpoint_name=checkpoint.name,
180
+ run_id="",
181
+ status=CheckpointStatus.ERROR,
182
+ error_message=str(e),
183
+ ))
184
+ return results
185
+
186
+ async def _run_parallel(
187
+ self,
188
+ checkpoints: list[CheckpointProtocol],
189
+ max_workers: int,
190
+ ) -> list[CheckpointResult]:
191
+ """Run checkpoints in parallel with semaphore.
192
+
193
+ Args:
194
+ checkpoints: Checkpoints to run.
195
+ max_workers: Max concurrent executions.
196
+
197
+ Returns:
198
+ List of results.
199
+ """
200
+ semaphore = asyncio.Semaphore(max_workers)
201
+
202
+ async def run_with_semaphore(cp: CheckpointProtocol) -> CheckpointResult:
203
+ async with semaphore:
204
+ try:
205
+ return await cp.run()
206
+ except Exception as e:
207
+ logger.error(f"Checkpoint failed: {cp.name} error={str(e)}")
208
+ return CheckpointResult(
209
+ checkpoint_name=cp.name,
210
+ run_id="",
211
+ status=CheckpointStatus.ERROR,
212
+ error_message=str(e),
213
+ )
214
+
215
+ results = await asyncio.gather(
216
+ *[run_with_semaphore(cp) for cp in checkpoints]
217
+ )
218
+ return list(results)
219
+
220
+ async def run_by_tag(
221
+ self,
222
+ tag_key: str,
223
+ tag_value: str,
224
+ parallel: bool = False,
225
+ ) -> list[CheckpointResult]:
226
+ """Run checkpoints matching a tag.
227
+
228
+ Args:
229
+ tag_key: Tag key to match.
230
+ tag_value: Tag value to match.
231
+ parallel: Run in parallel.
232
+
233
+ Returns:
234
+ List of checkpoint results.
235
+ """
236
+ checkpoints = [
237
+ cp for cp in self.get_all()
238
+ if cp.config.tags.get(tag_key) == tag_value and cp.config.enabled
239
+ ]
240
+
241
+ if not checkpoints:
242
+ logger.info(f"No checkpoints found with tag {tag_key}={tag_value}")
243
+ return []
244
+
245
+ if parallel:
246
+ return await self._run_parallel(checkpoints, self._max_workers)
247
+ else:
248
+ return await self._run_sequential(checkpoints)
249
+
250
+
251
+ # Global runner instance
252
+ _runner: CheckpointRunner | None = None
253
+
254
+
255
+ def get_checkpoint_runner() -> CheckpointRunner:
256
+ """Get the global checkpoint runner.
257
+
258
+ Returns:
259
+ Global CheckpointRunner instance.
260
+ """
261
+ global _runner
262
+ if _runner is None:
263
+ _runner = CheckpointRunner()
264
+ return _runner
265
+
266
+
267
+ def reset_checkpoint_runner() -> None:
268
+ """Reset the global checkpoint runner (for testing)."""
269
+ global _runner
270
+ _runner = None