dory-sdk 2.1.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.
- dory/__init__.py +70 -0
- dory/auto_instrument.py +142 -0
- dory/cli/__init__.py +5 -0
- dory/cli/main.py +290 -0
- dory/cli/templates.py +333 -0
- dory/config/__init__.py +23 -0
- dory/config/defaults.py +50 -0
- dory/config/loader.py +361 -0
- dory/config/presets.py +325 -0
- dory/config/schema.py +152 -0
- dory/core/__init__.py +27 -0
- dory/core/app.py +404 -0
- dory/core/context.py +209 -0
- dory/core/lifecycle.py +214 -0
- dory/core/meta.py +121 -0
- dory/core/modes.py +479 -0
- dory/core/processor.py +654 -0
- dory/core/signals.py +122 -0
- dory/decorators.py +142 -0
- dory/errors/__init__.py +117 -0
- dory/errors/classification.py +362 -0
- dory/errors/codes.py +495 -0
- dory/health/__init__.py +10 -0
- dory/health/probes.py +210 -0
- dory/health/server.py +306 -0
- dory/k8s/__init__.py +11 -0
- dory/k8s/annotation_watcher.py +184 -0
- dory/k8s/client.py +251 -0
- dory/k8s/pod_metadata.py +182 -0
- dory/logging/__init__.py +9 -0
- dory/logging/logger.py +175 -0
- dory/metrics/__init__.py +7 -0
- dory/metrics/collector.py +301 -0
- dory/middleware/__init__.py +36 -0
- dory/middleware/connection_tracker.py +608 -0
- dory/middleware/request_id.py +321 -0
- dory/middleware/request_tracker.py +501 -0
- dory/migration/__init__.py +11 -0
- dory/migration/configmap.py +260 -0
- dory/migration/serialization.py +167 -0
- dory/migration/state_manager.py +301 -0
- dory/monitoring/__init__.py +23 -0
- dory/monitoring/opentelemetry.py +462 -0
- dory/py.typed +2 -0
- dory/recovery/__init__.py +60 -0
- dory/recovery/golden_image.py +480 -0
- dory/recovery/golden_snapshot.py +561 -0
- dory/recovery/golden_validator.py +518 -0
- dory/recovery/partial_recovery.py +479 -0
- dory/recovery/recovery_decision.py +242 -0
- dory/recovery/restart_detector.py +142 -0
- dory/recovery/state_validator.py +187 -0
- dory/resilience/__init__.py +45 -0
- dory/resilience/circuit_breaker.py +454 -0
- dory/resilience/retry.py +389 -0
- dory/sidecar/__init__.py +6 -0
- dory/sidecar/main.py +75 -0
- dory/sidecar/server.py +329 -0
- dory/simple.py +342 -0
- dory/types.py +75 -0
- dory/utils/__init__.py +25 -0
- dory/utils/errors.py +59 -0
- dory/utils/retry.py +115 -0
- dory/utils/timeout.py +80 -0
- dory_sdk-2.1.0.dist-info/METADATA +663 -0
- dory_sdk-2.1.0.dist-info/RECORD +69 -0
- dory_sdk-2.1.0.dist-info/WHEEL +5 -0
- dory_sdk-2.1.0.dist-info/entry_points.txt +3 -0
- dory_sdk-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Partial State Recovery
|
|
3
|
+
|
|
4
|
+
Implements field-level state recovery to minimize data loss during recovery.
|
|
5
|
+
Instead of losing all state, recovers what can be recovered and fills in
|
|
6
|
+
defaults for corrupted or missing fields.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Field-level validation
|
|
10
|
+
- Smart default values
|
|
11
|
+
- Partial recovery strategies
|
|
12
|
+
- Recovery statistics
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, Optional, List, Callable, Set
|
|
18
|
+
from enum import Enum
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FieldStatus(Enum):
|
|
24
|
+
"""Status of a field after recovery attempt."""
|
|
25
|
+
VALID = "valid" # Field is valid, no recovery needed
|
|
26
|
+
RECOVERED = "recovered" # Field was recovered from snapshot
|
|
27
|
+
DEFAULTED = "defaulted" # Field was set to default value
|
|
28
|
+
MISSING = "missing" # Field is missing and has no default
|
|
29
|
+
CORRUPTED = "corrupted" # Field is corrupted and cannot be recovered
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class FieldRecovery:
|
|
34
|
+
"""
|
|
35
|
+
Represents recovery information for a single field.
|
|
36
|
+
"""
|
|
37
|
+
field_name: str
|
|
38
|
+
status: FieldStatus
|
|
39
|
+
original_value: Optional[Any] = None
|
|
40
|
+
recovered_value: Optional[Any] = None
|
|
41
|
+
error_message: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
44
|
+
"""Convert to dictionary."""
|
|
45
|
+
return {
|
|
46
|
+
"field_name": self.field_name,
|
|
47
|
+
"status": self.status.value,
|
|
48
|
+
"original_value": str(self.original_value) if self.original_value is not None else None,
|
|
49
|
+
"recovered_value": str(self.recovered_value) if self.recovered_value is not None else None,
|
|
50
|
+
"error_message": self.error_message,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class RecoveryResult:
|
|
56
|
+
"""
|
|
57
|
+
Result of a partial recovery operation.
|
|
58
|
+
"""
|
|
59
|
+
success: bool
|
|
60
|
+
recovered_state: Dict[str, Any]
|
|
61
|
+
field_recoveries: List[FieldRecovery] = field(default_factory=list)
|
|
62
|
+
valid_count: int = 0
|
|
63
|
+
recovered_count: int = 0
|
|
64
|
+
defaulted_count: int = 0
|
|
65
|
+
missing_count: int = 0
|
|
66
|
+
corrupted_count: int = 0
|
|
67
|
+
|
|
68
|
+
def get_recovery_rate(self) -> float:
|
|
69
|
+
"""
|
|
70
|
+
Calculate recovery rate (percentage of fields recovered).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Recovery rate from 0.0 to 1.0
|
|
74
|
+
"""
|
|
75
|
+
total = len(self.field_recoveries)
|
|
76
|
+
if total == 0:
|
|
77
|
+
return 1.0
|
|
78
|
+
|
|
79
|
+
recovered = self.valid_count + self.recovered_count + self.defaulted_count
|
|
80
|
+
return recovered / total
|
|
81
|
+
|
|
82
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
83
|
+
"""Convert to dictionary."""
|
|
84
|
+
return {
|
|
85
|
+
"success": self.success,
|
|
86
|
+
"recovered_state": self.recovered_state,
|
|
87
|
+
"field_recoveries": [fr.to_dict() for fr in self.field_recoveries],
|
|
88
|
+
"valid_count": self.valid_count,
|
|
89
|
+
"recovered_count": self.recovered_count,
|
|
90
|
+
"defaulted_count": self.defaulted_count,
|
|
91
|
+
"missing_count": self.missing_count,
|
|
92
|
+
"corrupted_count": self.corrupted_count,
|
|
93
|
+
"recovery_rate": self.get_recovery_rate(),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PartialRecoveryManager:
|
|
98
|
+
"""
|
|
99
|
+
Manages partial recovery of state data.
|
|
100
|
+
|
|
101
|
+
Features:
|
|
102
|
+
- Field-level validation and recovery
|
|
103
|
+
- Default value provision
|
|
104
|
+
- Custom recovery strategies
|
|
105
|
+
- Recovery statistics
|
|
106
|
+
|
|
107
|
+
Usage:
|
|
108
|
+
manager = PartialRecoveryManager()
|
|
109
|
+
|
|
110
|
+
# Define field defaults
|
|
111
|
+
manager.set_field_default("counter", 0)
|
|
112
|
+
manager.set_field_default("status", "initialized")
|
|
113
|
+
|
|
114
|
+
# Add field validator
|
|
115
|
+
manager.add_field_validator(
|
|
116
|
+
"counter",
|
|
117
|
+
lambda value: isinstance(value, int) and value >= 0
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Recover state
|
|
121
|
+
result = await manager.recover_state(
|
|
122
|
+
corrupted_state={"counter": "invalid", "status": "active"},
|
|
123
|
+
snapshot_state={"counter": 42},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Check result
|
|
127
|
+
print(f"Recovery rate: {result.get_recovery_rate() * 100}%")
|
|
128
|
+
print(f"Recovered state: {result.recovered_state}")
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
strict_validation: bool = False,
|
|
134
|
+
allow_partial: bool = True,
|
|
135
|
+
):
|
|
136
|
+
"""
|
|
137
|
+
Initialize partial recovery manager.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
strict_validation: Fail if any field cannot be recovered
|
|
141
|
+
allow_partial: Allow partial recovery (True) or all-or-nothing (False)
|
|
142
|
+
"""
|
|
143
|
+
self.strict_validation = strict_validation
|
|
144
|
+
self.allow_partial = allow_partial
|
|
145
|
+
|
|
146
|
+
# Field defaults
|
|
147
|
+
self._field_defaults: Dict[str, Any] = {}
|
|
148
|
+
|
|
149
|
+
# Field validators
|
|
150
|
+
self._field_validators: Dict[str, Callable[[Any], bool]] = {}
|
|
151
|
+
|
|
152
|
+
# Required fields
|
|
153
|
+
self._required_fields: Set[str] = set()
|
|
154
|
+
|
|
155
|
+
# Recovery strategies
|
|
156
|
+
self._recovery_strategies: Dict[str, Callable] = {}
|
|
157
|
+
|
|
158
|
+
# Statistics
|
|
159
|
+
self._recovery_count = 0
|
|
160
|
+
self._total_fields_recovered = 0
|
|
161
|
+
|
|
162
|
+
logger.info(
|
|
163
|
+
f"PartialRecoveryManager initialized: strict={strict_validation}, "
|
|
164
|
+
f"allow_partial={allow_partial}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def set_field_default(self, field_name: str, default_value: Any) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Set default value for a field.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
field_name: Field name
|
|
173
|
+
default_value: Default value to use if field is missing/corrupted
|
|
174
|
+
"""
|
|
175
|
+
self._field_defaults[field_name] = default_value
|
|
176
|
+
logger.debug(f"Field default set: {field_name} = {default_value}")
|
|
177
|
+
|
|
178
|
+
def add_field_validator(
|
|
179
|
+
self,
|
|
180
|
+
field_name: str,
|
|
181
|
+
validator: Callable[[Any], bool],
|
|
182
|
+
) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Add a validator for a field.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
field_name: Field name
|
|
188
|
+
validator: Function that returns True if value is valid
|
|
189
|
+
"""
|
|
190
|
+
self._field_validators[field_name] = validator
|
|
191
|
+
logger.debug(f"Field validator added: {field_name}")
|
|
192
|
+
|
|
193
|
+
def set_required_field(self, field_name: str) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Mark a field as required.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
field_name: Field name
|
|
199
|
+
"""
|
|
200
|
+
self._required_fields.add(field_name)
|
|
201
|
+
logger.debug(f"Required field set: {field_name}")
|
|
202
|
+
|
|
203
|
+
def add_recovery_strategy(
|
|
204
|
+
self,
|
|
205
|
+
field_name: str,
|
|
206
|
+
strategy: Callable[[Any, Optional[Any]], Any],
|
|
207
|
+
) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Add a custom recovery strategy for a field.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
field_name: Field name
|
|
213
|
+
strategy: Function(corrupted_value, snapshot_value) -> recovered_value
|
|
214
|
+
"""
|
|
215
|
+
self._recovery_strategies[field_name] = strategy
|
|
216
|
+
logger.debug(f"Recovery strategy added: {field_name}")
|
|
217
|
+
|
|
218
|
+
async def recover_state(
|
|
219
|
+
self,
|
|
220
|
+
corrupted_state: Dict[str, Any],
|
|
221
|
+
snapshot_state: Optional[Dict[str, Any]] = None,
|
|
222
|
+
field_names: Optional[List[str]] = None,
|
|
223
|
+
) -> RecoveryResult:
|
|
224
|
+
"""
|
|
225
|
+
Recover state data using partial recovery.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
corrupted_state: State that needs recovery
|
|
229
|
+
snapshot_state: Optional snapshot to recover from
|
|
230
|
+
field_names: Optional list of field names to recover (all if None)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
RecoveryResult with recovered state and statistics
|
|
234
|
+
"""
|
|
235
|
+
logger.info("Starting partial state recovery")
|
|
236
|
+
|
|
237
|
+
snapshot_state = snapshot_state or {}
|
|
238
|
+
recovered_state = {}
|
|
239
|
+
field_recoveries = []
|
|
240
|
+
|
|
241
|
+
# Determine fields to process
|
|
242
|
+
if field_names is None:
|
|
243
|
+
all_fields = set(corrupted_state.keys()) | set(snapshot_state.keys()) | set(self._field_defaults.keys())
|
|
244
|
+
else:
|
|
245
|
+
all_fields = set(field_names)
|
|
246
|
+
|
|
247
|
+
# Process each field
|
|
248
|
+
for field_name in all_fields:
|
|
249
|
+
recovery = await self._recover_field(
|
|
250
|
+
field_name=field_name,
|
|
251
|
+
corrupted_value=corrupted_state.get(field_name),
|
|
252
|
+
snapshot_value=snapshot_state.get(field_name),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
field_recoveries.append(recovery)
|
|
256
|
+
|
|
257
|
+
# Add to recovered state if successful
|
|
258
|
+
if recovery.status in [FieldStatus.VALID, FieldStatus.RECOVERED, FieldStatus.DEFAULTED]:
|
|
259
|
+
recovered_state[field_name] = recovery.recovered_value
|
|
260
|
+
|
|
261
|
+
# Count by status
|
|
262
|
+
valid_count = sum(1 for fr in field_recoveries if fr.status == FieldStatus.VALID)
|
|
263
|
+
recovered_count = sum(1 for fr in field_recoveries if fr.status == FieldStatus.RECOVERED)
|
|
264
|
+
defaulted_count = sum(1 for fr in field_recoveries if fr.status == FieldStatus.DEFAULTED)
|
|
265
|
+
missing_count = sum(1 for fr in field_recoveries if fr.status == FieldStatus.MISSING)
|
|
266
|
+
corrupted_count = sum(1 for fr in field_recoveries if fr.status == FieldStatus.CORRUPTED)
|
|
267
|
+
|
|
268
|
+
# Determine success
|
|
269
|
+
if self.strict_validation:
|
|
270
|
+
success = missing_count == 0 and corrupted_count == 0
|
|
271
|
+
else:
|
|
272
|
+
success = not self.allow_partial or (
|
|
273
|
+
# At least some fields recovered
|
|
274
|
+
(valid_count + recovered_count + defaulted_count) > 0 and
|
|
275
|
+
# All required fields present
|
|
276
|
+
all(field in recovered_state for field in self._required_fields)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Update statistics
|
|
280
|
+
self._recovery_count += 1
|
|
281
|
+
self._total_fields_recovered += valid_count + recovered_count + defaulted_count
|
|
282
|
+
|
|
283
|
+
result = RecoveryResult(
|
|
284
|
+
success=success,
|
|
285
|
+
recovered_state=recovered_state,
|
|
286
|
+
field_recoveries=field_recoveries,
|
|
287
|
+
valid_count=valid_count,
|
|
288
|
+
recovered_count=recovered_count,
|
|
289
|
+
defaulted_count=defaulted_count,
|
|
290
|
+
missing_count=missing_count,
|
|
291
|
+
corrupted_count=corrupted_count,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
logger.info(
|
|
295
|
+
f"Partial recovery complete: {result.get_recovery_rate() * 100:.1f}% recovered "
|
|
296
|
+
f"({valid_count} valid, {recovered_count} recovered, {defaulted_count} defaulted)"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return result
|
|
300
|
+
|
|
301
|
+
async def _recover_field(
|
|
302
|
+
self,
|
|
303
|
+
field_name: str,
|
|
304
|
+
corrupted_value: Optional[Any],
|
|
305
|
+
snapshot_value: Optional[Any],
|
|
306
|
+
) -> FieldRecovery:
|
|
307
|
+
"""
|
|
308
|
+
Recover a single field.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
field_name: Field name
|
|
312
|
+
corrupted_value: Current (possibly corrupted) value
|
|
313
|
+
snapshot_value: Value from snapshot (if available)
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
FieldRecovery result
|
|
317
|
+
"""
|
|
318
|
+
# Try to validate corrupted value first
|
|
319
|
+
if corrupted_value is not None and self._validate_field(field_name, corrupted_value):
|
|
320
|
+
return FieldRecovery(
|
|
321
|
+
field_name=field_name,
|
|
322
|
+
status=FieldStatus.VALID,
|
|
323
|
+
original_value=corrupted_value,
|
|
324
|
+
recovered_value=corrupted_value,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Try custom recovery strategy
|
|
328
|
+
if field_name in self._recovery_strategies:
|
|
329
|
+
try:
|
|
330
|
+
strategy = self._recovery_strategies[field_name]
|
|
331
|
+
recovered_value = strategy(corrupted_value, snapshot_value)
|
|
332
|
+
if self._validate_field(field_name, recovered_value):
|
|
333
|
+
return FieldRecovery(
|
|
334
|
+
field_name=field_name,
|
|
335
|
+
status=FieldStatus.RECOVERED,
|
|
336
|
+
original_value=corrupted_value,
|
|
337
|
+
recovered_value=recovered_value,
|
|
338
|
+
)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Recovery strategy failed for {field_name}: {e}")
|
|
341
|
+
|
|
342
|
+
# Try to recover from snapshot
|
|
343
|
+
if snapshot_value is not None and self._validate_field(field_name, snapshot_value):
|
|
344
|
+
return FieldRecovery(
|
|
345
|
+
field_name=field_name,
|
|
346
|
+
status=FieldStatus.RECOVERED,
|
|
347
|
+
original_value=corrupted_value,
|
|
348
|
+
recovered_value=snapshot_value,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Use default value
|
|
352
|
+
if field_name in self._field_defaults:
|
|
353
|
+
default_value = self._field_defaults[field_name]
|
|
354
|
+
return FieldRecovery(
|
|
355
|
+
field_name=field_name,
|
|
356
|
+
status=FieldStatus.DEFAULTED,
|
|
357
|
+
original_value=corrupted_value,
|
|
358
|
+
recovered_value=default_value,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Cannot recover
|
|
362
|
+
if corrupted_value is None and snapshot_value is None:
|
|
363
|
+
return FieldRecovery(
|
|
364
|
+
field_name=field_name,
|
|
365
|
+
status=FieldStatus.MISSING,
|
|
366
|
+
original_value=corrupted_value,
|
|
367
|
+
recovered_value=None,
|
|
368
|
+
error_message="Field is missing and has no default",
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
return FieldRecovery(
|
|
372
|
+
field_name=field_name,
|
|
373
|
+
status=FieldStatus.CORRUPTED,
|
|
374
|
+
original_value=corrupted_value,
|
|
375
|
+
recovered_value=None,
|
|
376
|
+
error_message="Field is corrupted and cannot be recovered",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def _validate_field(self, field_name: str, value: Any) -> bool:
|
|
380
|
+
"""
|
|
381
|
+
Validate a field value.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
field_name: Field name
|
|
385
|
+
value: Value to validate
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
True if valid
|
|
389
|
+
"""
|
|
390
|
+
# Check if validator exists
|
|
391
|
+
if field_name not in self._field_validators:
|
|
392
|
+
# No validator, assume valid if not None
|
|
393
|
+
return value is not None
|
|
394
|
+
|
|
395
|
+
# Run validator
|
|
396
|
+
try:
|
|
397
|
+
validator = self._field_validators[field_name]
|
|
398
|
+
return validator(value)
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error(f"Field validator failed for {field_name}: {e}")
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
404
|
+
"""
|
|
405
|
+
Get recovery statistics.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Dictionary of statistics
|
|
409
|
+
"""
|
|
410
|
+
return {
|
|
411
|
+
"recovery_count": self._recovery_count,
|
|
412
|
+
"total_fields_recovered": self._total_fields_recovered,
|
|
413
|
+
"field_defaults_count": len(self._field_defaults),
|
|
414
|
+
"field_validators_count": len(self._field_validators),
|
|
415
|
+
"required_fields_count": len(self._required_fields),
|
|
416
|
+
"recovery_strategies_count": len(self._recovery_strategies),
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# Helper function for common recovery strategies
|
|
421
|
+
|
|
422
|
+
def numeric_recovery_strategy(corrupted_value: Any, snapshot_value: Optional[Any]) -> int:
|
|
423
|
+
"""
|
|
424
|
+
Recovery strategy for numeric fields.
|
|
425
|
+
|
|
426
|
+
Tries to convert corrupted value to int, falls back to snapshot or 0.
|
|
427
|
+
"""
|
|
428
|
+
if snapshot_value is not None and isinstance(snapshot_value, (int, float)):
|
|
429
|
+
return int(snapshot_value)
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
return int(corrupted_value)
|
|
433
|
+
except (TypeError, ValueError):
|
|
434
|
+
return 0
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def string_recovery_strategy(corrupted_value: Any, snapshot_value: Optional[Any]) -> str:
|
|
438
|
+
"""
|
|
439
|
+
Recovery strategy for string fields.
|
|
440
|
+
|
|
441
|
+
Tries to convert corrupted value to str, falls back to snapshot or empty string.
|
|
442
|
+
"""
|
|
443
|
+
if snapshot_value is not None and isinstance(snapshot_value, str):
|
|
444
|
+
return snapshot_value
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
return str(corrupted_value) if corrupted_value is not None else ""
|
|
448
|
+
except Exception:
|
|
449
|
+
return ""
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def list_recovery_strategy(corrupted_value: Any, snapshot_value: Optional[Any]) -> List:
|
|
453
|
+
"""
|
|
454
|
+
Recovery strategy for list fields.
|
|
455
|
+
|
|
456
|
+
Falls back to snapshot or empty list.
|
|
457
|
+
"""
|
|
458
|
+
if isinstance(corrupted_value, list):
|
|
459
|
+
return corrupted_value
|
|
460
|
+
|
|
461
|
+
if snapshot_value is not None and isinstance(snapshot_value, list):
|
|
462
|
+
return snapshot_value
|
|
463
|
+
|
|
464
|
+
return []
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def dict_recovery_strategy(corrupted_value: Any, snapshot_value: Optional[Any]) -> Dict:
|
|
468
|
+
"""
|
|
469
|
+
Recovery strategy for dict fields.
|
|
470
|
+
|
|
471
|
+
Falls back to snapshot or empty dict.
|
|
472
|
+
"""
|
|
473
|
+
if isinstance(corrupted_value, dict):
|
|
474
|
+
return corrupted_value
|
|
475
|
+
|
|
476
|
+
if snapshot_value is not None and isinstance(snapshot_value, dict):
|
|
477
|
+
return snapshot_value
|
|
478
|
+
|
|
479
|
+
return {}
|