truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__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 +645 -23
  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 +15 -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.1.dist-info/METADATA +312 -0
  146. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.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.4.dist-info/METADATA +0 -507
  203. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,822 @@
1
+ """Pydantic schemas for storage tiering features.
2
+
3
+ This module provides schemas for:
4
+ - Storage tiers (hot, warm, cold, archive)
5
+ - Tier policies (age-based, access-based, size-based, scheduled, composite, custom)
6
+ - Tiering configurations
7
+ - Migration history
8
+
9
+ Based on truthound 1.2.10+ storage tiering capabilities.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import datetime
15
+ from enum import Enum
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel, Field, field_validator, model_validator
19
+
20
+ from .base import BaseSchema, IDMixin, ListResponseWrapper, TimestampMixin
21
+
22
+
23
+ # =============================================================================
24
+ # Enums
25
+ # =============================================================================
26
+
27
+
28
+ class TierType(str, Enum):
29
+ """Storage tier types."""
30
+
31
+ HOT = "hot"
32
+ WARM = "warm"
33
+ COLD = "cold"
34
+ ARCHIVE = "archive"
35
+
36
+
37
+ class MigrationDirection(str, Enum):
38
+ """Migration direction for tier policies."""
39
+
40
+ DEMOTE = "demote"
41
+ PROMOTE = "promote"
42
+
43
+
44
+ class TierPolicyType(str, Enum):
45
+ """Types of tier policies."""
46
+
47
+ AGE_BASED = "age_based"
48
+ ACCESS_BASED = "access_based"
49
+ SIZE_BASED = "size_based"
50
+ SCHEDULED = "scheduled"
51
+ COMPOSITE = "composite"
52
+ CUSTOM = "custom"
53
+
54
+
55
+ class MigrationStatus(str, Enum):
56
+ """Migration operation status."""
57
+
58
+ PENDING = "pending"
59
+ IN_PROGRESS = "in_progress"
60
+ COMPLETED = "completed"
61
+ FAILED = "failed"
62
+
63
+
64
+ # =============================================================================
65
+ # Storage Tier Schemas
66
+ # =============================================================================
67
+
68
+
69
+ class StorageTierBase(BaseSchema):
70
+ """Base storage tier schema."""
71
+
72
+ name: str = Field(
73
+ ...,
74
+ description="Unique tier name",
75
+ min_length=1,
76
+ max_length=50,
77
+ examples=["hot", "warm", "cold", "archive"],
78
+ )
79
+ tier_type: TierType = Field(
80
+ default=TierType.HOT,
81
+ description="Tier classification",
82
+ )
83
+ store_type: str = Field(
84
+ ...,
85
+ description="Backend store type",
86
+ min_length=1,
87
+ max_length=50,
88
+ examples=["filesystem", "s3", "gcs", "azure_blob"],
89
+ )
90
+ store_config: dict[str, Any] = Field(
91
+ default_factory=dict,
92
+ description="Store backend configuration",
93
+ )
94
+ priority: int = Field(
95
+ default=1,
96
+ description="Read order priority (lower = higher priority)",
97
+ ge=1,
98
+ le=100,
99
+ )
100
+ cost_per_gb: float | None = Field(
101
+ default=None,
102
+ description="Cost per GB for cost analysis",
103
+ ge=0,
104
+ )
105
+ retrieval_time_ms: int | None = Field(
106
+ default=None,
107
+ description="Expected retrieval latency in milliseconds",
108
+ ge=0,
109
+ )
110
+ metadata: dict[str, Any] | None = Field(
111
+ default=None,
112
+ description="Additional tier metadata",
113
+ )
114
+ is_active: bool = Field(
115
+ default=True,
116
+ description="Whether the tier is active",
117
+ )
118
+
119
+
120
+ class StorageTierCreate(StorageTierBase):
121
+ """Schema for creating a storage tier."""
122
+
123
+ pass
124
+
125
+
126
+ class StorageTierUpdate(BaseSchema):
127
+ """Schema for updating a storage tier."""
128
+
129
+ name: str | None = Field(None, min_length=1, max_length=50)
130
+ tier_type: TierType | None = None
131
+ store_type: str | None = Field(None, min_length=1, max_length=50)
132
+ store_config: dict[str, Any] | None = None
133
+ priority: int | None = Field(None, ge=1, le=100)
134
+ cost_per_gb: float | None = Field(None, ge=0)
135
+ retrieval_time_ms: int | None = Field(None, ge=0)
136
+ metadata: dict[str, Any] | None = None
137
+ is_active: bool | None = None
138
+
139
+
140
+ class StorageTierResponse(StorageTierBase, IDMixin, TimestampMixin):
141
+ """Schema for storage tier response."""
142
+
143
+ pass
144
+
145
+
146
+ class StorageTierListResponse(ListResponseWrapper):
147
+ """Schema for storage tier list response."""
148
+
149
+ items: list[StorageTierResponse]
150
+
151
+
152
+ # =============================================================================
153
+ # Tier Policy Schemas
154
+ # =============================================================================
155
+
156
+
157
+ class PolicyConfigBase(BaseModel):
158
+ """Base configuration for all policy types."""
159
+
160
+ pass
161
+
162
+
163
+ class AgeBasedPolicyConfig(PolicyConfigBase):
164
+ """Configuration for age-based tier policy.
165
+
166
+ Migrate items based on age.
167
+ """
168
+
169
+ after_days: int = Field(
170
+ default=0,
171
+ description="Days before migration",
172
+ ge=0,
173
+ le=3650, # 10 years max
174
+ )
175
+ after_hours: int = Field(
176
+ default=0,
177
+ description="Additional hours before migration",
178
+ ge=0,
179
+ le=23,
180
+ )
181
+
182
+ @model_validator(mode="after")
183
+ def validate_at_least_one_time(self) -> "AgeBasedPolicyConfig":
184
+ """Ensure at least one time unit is specified."""
185
+ if self.after_days == 0 and self.after_hours == 0:
186
+ raise ValueError("At least one time unit (days or hours) must be specified")
187
+ return self
188
+
189
+
190
+ class AccessBasedPolicyConfig(PolicyConfigBase):
191
+ """Configuration for access-based tier policy.
192
+
193
+ Migrate based on access patterns.
194
+ """
195
+
196
+ inactive_days: int | None = Field(
197
+ default=None,
198
+ description="Days without access for demotion",
199
+ ge=1,
200
+ le=3650,
201
+ )
202
+ min_access_count: int | None = Field(
203
+ default=None,
204
+ description="Minimum accesses for promotion",
205
+ ge=1,
206
+ le=1000000,
207
+ )
208
+ access_window_days: int = Field(
209
+ default=7,
210
+ description="Window for counting accesses (days)",
211
+ ge=1,
212
+ le=365,
213
+ )
214
+
215
+ @model_validator(mode="after")
216
+ def validate_at_least_one_condition(self) -> "AccessBasedPolicyConfig":
217
+ """Ensure at least one condition is specified."""
218
+ if self.inactive_days is None and self.min_access_count is None:
219
+ raise ValueError(
220
+ "At least one condition (inactive_days or min_access_count) must be specified"
221
+ )
222
+ return self
223
+
224
+
225
+ class SizeBasedPolicyConfig(PolicyConfigBase):
226
+ """Configuration for size-based tier policy.
227
+
228
+ Migrate based on item size or tier capacity.
229
+ """
230
+
231
+ min_size_bytes: int = Field(
232
+ default=0,
233
+ description="Minimum item size in bytes",
234
+ ge=0,
235
+ )
236
+ min_size_kb: int = Field(
237
+ default=0,
238
+ description="Minimum item size in KB",
239
+ ge=0,
240
+ )
241
+ min_size_mb: int = Field(
242
+ default=0,
243
+ description="Minimum item size in MB",
244
+ ge=0,
245
+ )
246
+ min_size_gb: int = Field(
247
+ default=0,
248
+ description="Minimum item size in GB",
249
+ ge=0,
250
+ )
251
+ tier_max_size_bytes: int = Field(
252
+ default=0,
253
+ description="Maximum total tier size in bytes",
254
+ ge=0,
255
+ )
256
+ tier_max_size_gb: int = Field(
257
+ default=0,
258
+ description="Maximum total tier size in GB",
259
+ ge=0,
260
+ )
261
+
262
+ @model_validator(mode="after")
263
+ def validate_at_least_one_size(self) -> "SizeBasedPolicyConfig":
264
+ """Ensure at least one size condition is specified."""
265
+ has_item_size = any([
266
+ self.min_size_bytes > 0,
267
+ self.min_size_kb > 0,
268
+ self.min_size_mb > 0,
269
+ self.min_size_gb > 0,
270
+ ])
271
+ has_tier_size = any([
272
+ self.tier_max_size_bytes > 0,
273
+ self.tier_max_size_gb > 0,
274
+ ])
275
+ if not has_item_size and not has_tier_size:
276
+ raise ValueError("At least one size condition must be specified")
277
+ return self
278
+
279
+ @property
280
+ def total_min_size_bytes(self) -> int:
281
+ """Calculate total minimum size in bytes."""
282
+ return (
283
+ self.min_size_bytes
284
+ + self.min_size_kb * 1024
285
+ + self.min_size_mb * 1024 * 1024
286
+ + self.min_size_gb * 1024 * 1024 * 1024
287
+ )
288
+
289
+ @property
290
+ def total_tier_max_bytes(self) -> int:
291
+ """Calculate total tier max size in bytes."""
292
+ return self.tier_max_size_bytes + self.tier_max_size_gb * 1024 * 1024 * 1024
293
+
294
+
295
+ class ScheduledPolicyConfig(PolicyConfigBase):
296
+ """Configuration for scheduled tier policy.
297
+
298
+ Migrate on a schedule (specific days/times).
299
+ """
300
+
301
+ on_days: list[int] | None = Field(
302
+ default=None,
303
+ description="Days to run (0=Monday, 6=Sunday)",
304
+ examples=[[0, 1, 2, 3, 4], [5, 6]],
305
+ )
306
+ at_hour: int | None = Field(
307
+ default=None,
308
+ description="Hour to run (0-23)",
309
+ ge=0,
310
+ le=23,
311
+ )
312
+ min_age_days: int = Field(
313
+ default=0,
314
+ description="Minimum item age in days",
315
+ ge=0,
316
+ le=3650,
317
+ )
318
+
319
+ @field_validator("on_days")
320
+ @classmethod
321
+ def validate_days(cls, v: list[int] | None) -> list[int] | None:
322
+ """Validate day values."""
323
+ if v is not None:
324
+ for day in v:
325
+ if day < 0 or day > 6:
326
+ raise ValueError(f"Day must be 0-6, got {day}")
327
+ return v
328
+
329
+
330
+ class CompositePolicyConfig(PolicyConfigBase):
331
+ """Configuration for composite tier policy.
332
+
333
+ Combine multiple policies with AND/OR logic.
334
+ """
335
+
336
+ require_all: bool = Field(
337
+ default=True,
338
+ description="True = AND logic (all must match), False = OR logic (any match)",
339
+ )
340
+ child_policy_ids: list[str] = Field(
341
+ default_factory=list,
342
+ description="IDs of child policies to combine",
343
+ )
344
+
345
+ @field_validator("child_policy_ids")
346
+ @classmethod
347
+ def validate_min_children(cls, v: list[str]) -> list[str]:
348
+ """Ensure at least 2 child policies for composite."""
349
+ if len(v) < 2:
350
+ raise ValueError("Composite policy requires at least 2 child policies")
351
+ return v
352
+
353
+
354
+ class CustomPolicyConfig(PolicyConfigBase):
355
+ """Configuration for custom tier policy.
356
+
357
+ Define custom migration logic with a predicate expression.
358
+ """
359
+
360
+ predicate_expression: str = Field(
361
+ ...,
362
+ description="Python expression for migration predicate",
363
+ min_length=1,
364
+ max_length=1000,
365
+ )
366
+ description: str = Field(
367
+ default="",
368
+ description="Human-readable description of the custom logic",
369
+ max_length=500,
370
+ )
371
+
372
+
373
+ class TierPolicyBase(BaseSchema):
374
+ """Base tier policy schema."""
375
+
376
+ name: str = Field(
377
+ ...,
378
+ description="Policy name",
379
+ min_length=1,
380
+ max_length=255,
381
+ )
382
+ description: str | None = Field(
383
+ default=None,
384
+ description="Policy description",
385
+ max_length=1000,
386
+ )
387
+ policy_type: TierPolicyType = Field(
388
+ ...,
389
+ description="Type of policy",
390
+ )
391
+ from_tier_id: str = Field(
392
+ ...,
393
+ description="Source tier ID",
394
+ )
395
+ to_tier_id: str = Field(
396
+ ...,
397
+ description="Destination tier ID",
398
+ )
399
+ direction: MigrationDirection = Field(
400
+ default=MigrationDirection.DEMOTE,
401
+ description="Migration direction",
402
+ )
403
+ config: dict[str, Any] = Field(
404
+ default_factory=dict,
405
+ description="Policy-specific configuration",
406
+ )
407
+ is_active: bool = Field(
408
+ default=True,
409
+ description="Whether policy is active",
410
+ )
411
+ priority: int = Field(
412
+ default=0,
413
+ description="Execution priority (lower = runs first)",
414
+ ge=0,
415
+ le=1000,
416
+ )
417
+
418
+
419
+ class TierPolicyCreate(TierPolicyBase):
420
+ """Schema for creating a tier policy."""
421
+
422
+ parent_id: str | None = Field(
423
+ default=None,
424
+ description="Parent composite policy ID (for nested policies)",
425
+ )
426
+
427
+
428
+ class TierPolicyUpdate(BaseSchema):
429
+ """Schema for updating a tier policy."""
430
+
431
+ name: str | None = Field(None, min_length=1, max_length=255)
432
+ description: str | None = Field(None, max_length=1000)
433
+ policy_type: TierPolicyType | None = None
434
+ from_tier_id: str | None = None
435
+ to_tier_id: str | None = None
436
+ direction: MigrationDirection | None = None
437
+ config: dict[str, Any] | None = None
438
+ is_active: bool | None = None
439
+ priority: int | None = Field(None, ge=0, le=1000)
440
+ parent_id: str | None = None
441
+
442
+
443
+ class TierPolicyResponse(TierPolicyBase, IDMixin, TimestampMixin):
444
+ """Schema for tier policy response."""
445
+
446
+ parent_id: str | None = Field(
447
+ default=None,
448
+ description="Parent composite policy ID",
449
+ )
450
+ child_count: int = Field(
451
+ default=0,
452
+ description="Number of child policies (for composite)",
453
+ )
454
+ from_tier_name: str | None = Field(
455
+ default=None,
456
+ description="Source tier name",
457
+ )
458
+ to_tier_name: str | None = Field(
459
+ default=None,
460
+ description="Destination tier name",
461
+ )
462
+
463
+
464
+ class TierPolicyListResponse(ListResponseWrapper):
465
+ """Schema for tier policy list response."""
466
+
467
+ items: list[TierPolicyResponse]
468
+
469
+
470
+ class TierPolicyWithChildren(TierPolicyResponse):
471
+ """Schema for tier policy with nested children."""
472
+
473
+ children: list["TierPolicyWithChildren"] = Field(
474
+ default_factory=list,
475
+ description="Nested child policies",
476
+ )
477
+
478
+
479
+ # Rebuild for self-referencing model
480
+ TierPolicyWithChildren.model_rebuild()
481
+
482
+
483
+ # =============================================================================
484
+ # Tiering Configuration Schemas
485
+ # =============================================================================
486
+
487
+
488
+ class TieringConfigBase(BaseSchema):
489
+ """Base tiering configuration schema."""
490
+
491
+ name: str = Field(
492
+ ...,
493
+ description="Configuration name",
494
+ min_length=1,
495
+ max_length=255,
496
+ )
497
+ description: str | None = Field(
498
+ default=None,
499
+ description="Configuration description",
500
+ max_length=1000,
501
+ )
502
+ default_tier_id: str | None = Field(
503
+ default=None,
504
+ description="Default tier for new items",
505
+ )
506
+ enable_promotion: bool = Field(
507
+ default=True,
508
+ description="Whether to auto-promote on frequent access",
509
+ )
510
+ promotion_threshold: int = Field(
511
+ default=10,
512
+ description="Access count to trigger promotion",
513
+ ge=1,
514
+ le=10000,
515
+ )
516
+ check_interval_hours: int = Field(
517
+ default=24,
518
+ description="Hours between auto-checks",
519
+ ge=1,
520
+ le=168, # 1 week max
521
+ )
522
+ batch_size: int = Field(
523
+ default=100,
524
+ description="Items per migration batch",
525
+ ge=1,
526
+ le=10000,
527
+ )
528
+ enable_parallel_migration: bool = Field(
529
+ default=False,
530
+ description="Whether to enable parallel migration",
531
+ )
532
+ max_parallel_migrations: int = Field(
533
+ default=4,
534
+ description="Maximum concurrent migrations",
535
+ ge=1,
536
+ le=100,
537
+ )
538
+ is_active: bool = Field(
539
+ default=True,
540
+ description="Whether configuration is active",
541
+ )
542
+
543
+
544
+ class TieringConfigCreate(TieringConfigBase):
545
+ """Schema for creating a tiering configuration."""
546
+
547
+ pass
548
+
549
+
550
+ class TieringConfigUpdate(BaseSchema):
551
+ """Schema for updating a tiering configuration."""
552
+
553
+ name: str | None = Field(None, min_length=1, max_length=255)
554
+ description: str | None = Field(None, max_length=1000)
555
+ default_tier_id: str | None = None
556
+ enable_promotion: bool | None = None
557
+ promotion_threshold: int | None = Field(None, ge=1, le=10000)
558
+ check_interval_hours: int | None = Field(None, ge=1, le=168)
559
+ batch_size: int | None = Field(None, ge=1, le=10000)
560
+ enable_parallel_migration: bool | None = None
561
+ max_parallel_migrations: int | None = Field(None, ge=1, le=100)
562
+ is_active: bool | None = None
563
+
564
+
565
+ class TieringConfigResponse(TieringConfigBase, IDMixin, TimestampMixin):
566
+ """Schema for tiering configuration response."""
567
+
568
+ default_tier_name: str | None = Field(
569
+ default=None,
570
+ description="Default tier name",
571
+ )
572
+
573
+
574
+ class TieringConfigListResponse(ListResponseWrapper):
575
+ """Schema for tiering configuration list response."""
576
+
577
+ items: list[TieringConfigResponse]
578
+
579
+
580
+ # =============================================================================
581
+ # Migration History Schemas
582
+ # =============================================================================
583
+
584
+
585
+ class MigrationHistoryBase(BaseSchema):
586
+ """Base migration history schema."""
587
+
588
+ policy_id: str | None = Field(
589
+ default=None,
590
+ description="Policy that triggered migration",
591
+ )
592
+ item_id: str = Field(
593
+ ...,
594
+ description="ID of the migrated item",
595
+ min_length=1,
596
+ max_length=255,
597
+ )
598
+ from_tier_id: str = Field(
599
+ ...,
600
+ description="Source tier ID",
601
+ )
602
+ to_tier_id: str = Field(
603
+ ...,
604
+ description="Destination tier ID",
605
+ )
606
+ size_bytes: int = Field(
607
+ default=0,
608
+ description="Size of migrated item",
609
+ ge=0,
610
+ )
611
+ status: MigrationStatus = Field(
612
+ default=MigrationStatus.PENDING,
613
+ description="Migration status",
614
+ )
615
+ error_message: str | None = Field(
616
+ default=None,
617
+ description="Error message if failed",
618
+ max_length=1000,
619
+ )
620
+
621
+
622
+ class MigrationHistoryCreate(MigrationHistoryBase):
623
+ """Schema for creating a migration history entry."""
624
+
625
+ pass
626
+
627
+
628
+ class MigrationHistoryResponse(MigrationHistoryBase, IDMixin):
629
+ """Schema for migration history response."""
630
+
631
+ started_at: datetime = Field(..., description="When migration started")
632
+ completed_at: datetime | None = Field(
633
+ default=None,
634
+ description="When migration completed",
635
+ )
636
+ duration_ms: float | None = Field(
637
+ default=None,
638
+ description="Migration duration in milliseconds",
639
+ )
640
+ from_tier_name: str | None = Field(
641
+ default=None,
642
+ description="Source tier name",
643
+ )
644
+ to_tier_name: str | None = Field(
645
+ default=None,
646
+ description="Destination tier name",
647
+ )
648
+ policy_name: str | None = Field(
649
+ default=None,
650
+ description="Policy name",
651
+ )
652
+
653
+
654
+ class MigrationHistoryListResponse(ListResponseWrapper):
655
+ """Schema for migration history list response."""
656
+
657
+ items: list[MigrationHistoryResponse]
658
+
659
+
660
+ # =============================================================================
661
+ # Statistics Schemas
662
+ # =============================================================================
663
+
664
+
665
+ class TierStatistics(BaseModel):
666
+ """Statistics for a single tier."""
667
+
668
+ tier_id: str
669
+ tier_name: str
670
+ tier_type: TierType
671
+ item_count: int = 0
672
+ total_size_bytes: int = 0
673
+ total_size_gb: float = 0.0
674
+ estimated_cost: float | None = None
675
+ policy_count: int = 0
676
+
677
+
678
+ class TieringStatistics(BaseModel):
679
+ """Overall tiering statistics."""
680
+
681
+ total_tiers: int = 0
682
+ active_tiers: int = 0
683
+ total_policies: int = 0
684
+ active_policies: int = 0
685
+ composite_policies: int = 0
686
+ total_migrations: int = 0
687
+ successful_migrations: int = 0
688
+ failed_migrations: int = 0
689
+ total_bytes_migrated: int = 0
690
+ tier_stats: list[TierStatistics] = Field(default_factory=list)
691
+
692
+
693
+ class PolicyTypeInfo(BaseModel):
694
+ """Information about a policy type."""
695
+
696
+ type: TierPolicyType
697
+ name: str
698
+ description: str
699
+ config_schema: dict[str, Any]
700
+
701
+
702
+ class PolicyTypesResponse(BaseModel):
703
+ """Response for available policy types."""
704
+
705
+ policy_types: list[PolicyTypeInfo]
706
+
707
+
708
+ # =============================================================================
709
+ # Policy Execution Schemas
710
+ # =============================================================================
711
+
712
+
713
+ class PolicyExecutionRequest(BaseModel):
714
+ """Request for policy execution."""
715
+
716
+ dry_run: bool = Field(
717
+ default=False,
718
+ description="If True, don't actually migrate, just report what would happen",
719
+ )
720
+ batch_size: int = Field(
721
+ default=100,
722
+ description="Maximum items to process",
723
+ ge=1,
724
+ le=10000,
725
+ )
726
+
727
+
728
+ class MigrationItemResponse(BaseModel):
729
+ """Response for a single migration operation."""
730
+
731
+ item_id: str
732
+ from_tier: str
733
+ to_tier: str
734
+ success: bool
735
+ size_bytes: int = 0
736
+ error_message: str | None = None
737
+ duration_ms: float = 0.0
738
+
739
+
740
+ class PolicyExecutionResponse(BaseModel):
741
+ """Response for policy execution."""
742
+
743
+ policy_id: str
744
+ dry_run: bool
745
+ start_time: datetime
746
+ end_time: datetime
747
+ duration_seconds: float
748
+ items_scanned: int = 0
749
+ items_migrated: int = 0
750
+ items_failed: int = 0
751
+ bytes_migrated: int = 0
752
+ success_rate: float = 1.0
753
+ errors: list[str] = Field(default_factory=list)
754
+ migrations: list[MigrationItemResponse] = Field(default_factory=list)
755
+
756
+
757
+ class MigrateItemRequest(BaseModel):
758
+ """Request for migrating a single item."""
759
+
760
+ from_tier_id: str = Field(..., description="Source tier ID")
761
+ to_tier_id: str = Field(..., description="Destination tier ID")
762
+
763
+
764
+ class PolicyExecutionSummary(BaseModel):
765
+ """Summary of a single policy execution."""
766
+
767
+ items_scanned: int = 0
768
+ items_migrated: int = 0
769
+ items_failed: int = 0
770
+ bytes_migrated: int = 0
771
+ duration_seconds: float = 0.0
772
+ success_rate: float = 1.0
773
+ errors: list[str] = Field(default_factory=list)
774
+
775
+
776
+ class ProcessPoliciesResponse(BaseModel):
777
+ """Response for processing all policies."""
778
+
779
+ policies_executed: int = 0
780
+ total_items_scanned: int = 0
781
+ total_items_migrated: int = 0
782
+ total_items_failed: int = 0
783
+ total_bytes_migrated: int = 0
784
+ errors: list[str] = Field(default_factory=list)
785
+ policy_results: list[PolicyExecutionSummary] = Field(default_factory=list)
786
+
787
+
788
+ class TieringStatusResponse(BaseModel):
789
+ """Response for tiering system status."""
790
+
791
+ truthound_available: bool = Field(
792
+ ...,
793
+ description="Whether truthound tiering module is available",
794
+ )
795
+ tiering_enabled: bool = Field(
796
+ ...,
797
+ description="Whether tiering is enabled via active config",
798
+ )
799
+ active_config_id: str | None = Field(
800
+ default=None,
801
+ description="Active configuration ID",
802
+ )
803
+ active_config_name: str | None = Field(
804
+ default=None,
805
+ description="Active configuration name",
806
+ )
807
+ check_interval_hours: int | None = Field(
808
+ default=None,
809
+ description="Hours between automatic policy checks",
810
+ )
811
+ active_tiers: int = Field(
812
+ default=0,
813
+ description="Number of active storage tiers",
814
+ )
815
+ active_policies: int = Field(
816
+ default=0,
817
+ description="Number of active policies",
818
+ )
819
+ migrations_last_24h: int = Field(
820
+ default=0,
821
+ description="Number of migrations in the last 24 hours",
822
+ )