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.
Files changed (69) hide show
  1. dory/__init__.py +70 -0
  2. dory/auto_instrument.py +142 -0
  3. dory/cli/__init__.py +5 -0
  4. dory/cli/main.py +290 -0
  5. dory/cli/templates.py +333 -0
  6. dory/config/__init__.py +23 -0
  7. dory/config/defaults.py +50 -0
  8. dory/config/loader.py +361 -0
  9. dory/config/presets.py +325 -0
  10. dory/config/schema.py +152 -0
  11. dory/core/__init__.py +27 -0
  12. dory/core/app.py +404 -0
  13. dory/core/context.py +209 -0
  14. dory/core/lifecycle.py +214 -0
  15. dory/core/meta.py +121 -0
  16. dory/core/modes.py +479 -0
  17. dory/core/processor.py +654 -0
  18. dory/core/signals.py +122 -0
  19. dory/decorators.py +142 -0
  20. dory/errors/__init__.py +117 -0
  21. dory/errors/classification.py +362 -0
  22. dory/errors/codes.py +495 -0
  23. dory/health/__init__.py +10 -0
  24. dory/health/probes.py +210 -0
  25. dory/health/server.py +306 -0
  26. dory/k8s/__init__.py +11 -0
  27. dory/k8s/annotation_watcher.py +184 -0
  28. dory/k8s/client.py +251 -0
  29. dory/k8s/pod_metadata.py +182 -0
  30. dory/logging/__init__.py +9 -0
  31. dory/logging/logger.py +175 -0
  32. dory/metrics/__init__.py +7 -0
  33. dory/metrics/collector.py +301 -0
  34. dory/middleware/__init__.py +36 -0
  35. dory/middleware/connection_tracker.py +608 -0
  36. dory/middleware/request_id.py +321 -0
  37. dory/middleware/request_tracker.py +501 -0
  38. dory/migration/__init__.py +11 -0
  39. dory/migration/configmap.py +260 -0
  40. dory/migration/serialization.py +167 -0
  41. dory/migration/state_manager.py +301 -0
  42. dory/monitoring/__init__.py +23 -0
  43. dory/monitoring/opentelemetry.py +462 -0
  44. dory/py.typed +2 -0
  45. dory/recovery/__init__.py +60 -0
  46. dory/recovery/golden_image.py +480 -0
  47. dory/recovery/golden_snapshot.py +561 -0
  48. dory/recovery/golden_validator.py +518 -0
  49. dory/recovery/partial_recovery.py +479 -0
  50. dory/recovery/recovery_decision.py +242 -0
  51. dory/recovery/restart_detector.py +142 -0
  52. dory/recovery/state_validator.py +187 -0
  53. dory/resilience/__init__.py +45 -0
  54. dory/resilience/circuit_breaker.py +454 -0
  55. dory/resilience/retry.py +389 -0
  56. dory/sidecar/__init__.py +6 -0
  57. dory/sidecar/main.py +75 -0
  58. dory/sidecar/server.py +329 -0
  59. dory/simple.py +342 -0
  60. dory/types.py +75 -0
  61. dory/utils/__init__.py +25 -0
  62. dory/utils/errors.py +59 -0
  63. dory/utils/retry.py +115 -0
  64. dory/utils/timeout.py +80 -0
  65. dory_sdk-2.1.0.dist-info/METADATA +663 -0
  66. dory_sdk-2.1.0.dist-info/RECORD +69 -0
  67. dory_sdk-2.1.0.dist-info/WHEEL +5 -0
  68. dory_sdk-2.1.0.dist-info/entry_points.txt +3 -0
  69. 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 {}