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,501 @@
1
+ """
2
+ Request Tracker Middleware
3
+
4
+ Automatically tracks request lifecycle, duration, success/failure, and metrics.
5
+ Eliminates boilerplate for request tracking.
6
+
7
+ Features:
8
+ - Automatic request lifecycle tracking
9
+ - Duration measurement
10
+ - Success/failure counting
11
+ - Active request tracking
12
+ - Request metrics aggregation
13
+ - Decorators for easy integration
14
+ """
15
+
16
+ import asyncio
17
+ import logging
18
+ import time
19
+ from contextlib import asynccontextmanager
20
+ from dataclasses import dataclass, field
21
+ from datetime import datetime
22
+ from enum import Enum
23
+ from functools import wraps
24
+ from typing import Any, Dict, Optional, List, Callable, Set
25
+ from collections import defaultdict
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class RequestStatus(Enum):
31
+ """Request status."""
32
+ PENDING = "pending"
33
+ IN_PROGRESS = "in_progress"
34
+ SUCCESS = "success"
35
+ FAILED = "failed"
36
+ TIMEOUT = "timeout"
37
+ CANCELLED = "cancelled"
38
+
39
+
40
+ @dataclass
41
+ class RequestInfo:
42
+ """
43
+ Information about a tracked request.
44
+ """
45
+ request_id: str
46
+ request_type: str
47
+ status: RequestStatus
48
+ start_time: float
49
+ end_time: Optional[float] = None
50
+ duration: Optional[float] = None
51
+ error: Optional[str] = None
52
+ metadata: Dict[str, Any] = field(default_factory=dict)
53
+
54
+ def to_dict(self) -> Dict[str, Any]:
55
+ """Convert to dictionary."""
56
+ return {
57
+ "request_id": self.request_id,
58
+ "request_type": self.request_type,
59
+ "status": self.status.value,
60
+ "start_time": self.start_time,
61
+ "end_time": self.end_time,
62
+ "duration": self.duration,
63
+ "error": self.error,
64
+ "metadata": self.metadata,
65
+ }
66
+
67
+ def is_active(self) -> bool:
68
+ """Check if request is still active."""
69
+ return self.status in [RequestStatus.PENDING, RequestStatus.IN_PROGRESS]
70
+
71
+
72
+ @dataclass
73
+ class RequestMetrics:
74
+ """
75
+ Aggregated request metrics.
76
+ """
77
+ total_requests: int = 0
78
+ active_requests: int = 0
79
+ successful_requests: int = 0
80
+ failed_requests: int = 0
81
+ timeout_requests: int = 0
82
+ cancelled_requests: int = 0
83
+ total_duration: float = 0.0
84
+ min_duration: Optional[float] = None
85
+ max_duration: Optional[float] = None
86
+ avg_duration: float = 0.0
87
+
88
+ def update_duration(self, duration: float) -> None:
89
+ """Update duration statistics."""
90
+ self.total_duration += duration
91
+ if self.min_duration is None or duration < self.min_duration:
92
+ self.min_duration = duration
93
+ if self.max_duration is None or duration > self.max_duration:
94
+ self.max_duration = duration
95
+
96
+ completed = self.successful_requests + self.failed_requests + self.timeout_requests + self.cancelled_requests
97
+ if completed > 0:
98
+ self.avg_duration = self.total_duration / completed
99
+
100
+ def to_dict(self) -> Dict[str, Any]:
101
+ """Convert to dictionary."""
102
+ return {
103
+ "total_requests": self.total_requests,
104
+ "active_requests": self.active_requests,
105
+ "successful_requests": self.successful_requests,
106
+ "failed_requests": self.failed_requests,
107
+ "timeout_requests": self.timeout_requests,
108
+ "cancelled_requests": self.cancelled_requests,
109
+ "total_duration": self.total_duration,
110
+ "min_duration": self.min_duration,
111
+ "max_duration": self.max_duration,
112
+ "avg_duration": self.avg_duration,
113
+ "success_rate": self.get_success_rate(),
114
+ }
115
+
116
+ def get_success_rate(self) -> float:
117
+ """Calculate success rate."""
118
+ completed = self.successful_requests + self.failed_requests + self.timeout_requests + self.cancelled_requests
119
+ if completed == 0:
120
+ return 0.0
121
+ return self.successful_requests / completed
122
+
123
+
124
+ class RequestTracker:
125
+ """
126
+ Tracks requests automatically.
127
+
128
+ Features:
129
+ - Automatic request tracking
130
+ - Duration measurement
131
+ - Success/failure counting
132
+ - Active request monitoring
133
+ - Metrics aggregation by request type
134
+ - Request history
135
+
136
+ Usage:
137
+ tracker = RequestTracker()
138
+
139
+ # Track request manually
140
+ async with tracker.track("process_item") as request_id:
141
+ await process_item()
142
+
143
+ # Or use decorator
144
+ @track_request(tracker, "process_item")
145
+ async def process_item():
146
+ pass
147
+
148
+ # Get metrics
149
+ metrics = tracker.get_metrics()
150
+ print(f"Active requests: {metrics.active_requests}")
151
+ """
152
+
153
+ def __init__(
154
+ self,
155
+ max_history: int = 1000,
156
+ enable_history: bool = True,
157
+ on_request_start: Optional[Callable] = None,
158
+ on_request_complete: Optional[Callable] = None,
159
+ ):
160
+ """
161
+ Initialize request tracker.
162
+
163
+ Args:
164
+ max_history: Maximum number of completed requests to keep in history
165
+ enable_history: Whether to keep request history
166
+ on_request_start: Callback when request starts
167
+ on_request_complete: Callback when request completes
168
+ """
169
+ self.max_history = max_history
170
+ self.enable_history = enable_history
171
+ self.on_request_start = on_request_start
172
+ self.on_request_complete = on_request_complete
173
+
174
+ # Active requests
175
+ self._active_requests: Dict[str, RequestInfo] = {}
176
+
177
+ # Request history (completed requests)
178
+ self._request_history: List[RequestInfo] = []
179
+
180
+ # Metrics by request type
181
+ self._metrics: Dict[str, RequestMetrics] = defaultdict(RequestMetrics)
182
+
183
+ # Overall metrics
184
+ self._overall_metrics = RequestMetrics()
185
+
186
+ # Request counter
187
+ self._request_counter = 0
188
+
189
+ # Lock for thread safety
190
+ self._lock = asyncio.Lock()
191
+
192
+ logger.info(
193
+ f"RequestTracker initialized: max_history={max_history}, "
194
+ f"enable_history={enable_history}"
195
+ )
196
+
197
+ def _generate_request_id(self, request_type: str) -> str:
198
+ """Generate unique request ID."""
199
+ self._request_counter += 1
200
+ timestamp = int(time.time() * 1000)
201
+ return f"{request_type}_{timestamp}_{self._request_counter}"
202
+
203
+ @asynccontextmanager
204
+ async def track(
205
+ self,
206
+ request_type: str,
207
+ metadata: Optional[Dict[str, Any]] = None,
208
+ timeout: Optional[float] = None,
209
+ ):
210
+ """
211
+ Context manager to track a request.
212
+
213
+ Args:
214
+ request_type: Type of request
215
+ metadata: Optional metadata
216
+ timeout: Optional timeout in seconds
217
+
218
+ Yields:
219
+ Request ID
220
+
221
+ Example:
222
+ async with tracker.track("process_item") as request_id:
223
+ await process_item()
224
+ """
225
+ request_id = self._generate_request_id(request_type)
226
+
227
+ async with self._lock:
228
+ # Create request info
229
+ request_info = RequestInfo(
230
+ request_id=request_id,
231
+ request_type=request_type,
232
+ status=RequestStatus.IN_PROGRESS,
233
+ start_time=time.time(),
234
+ metadata=metadata or {},
235
+ )
236
+
237
+ # Add to active requests
238
+ self._active_requests[request_id] = request_info
239
+
240
+ # Update metrics
241
+ self._metrics[request_type].total_requests += 1
242
+ self._metrics[request_type].active_requests += 1
243
+ self._overall_metrics.total_requests += 1
244
+ self._overall_metrics.active_requests += 1
245
+
246
+ # Call start callback
247
+ if self.on_request_start:
248
+ try:
249
+ if asyncio.iscoroutinefunction(self.on_request_start):
250
+ await self.on_request_start(request_info)
251
+ else:
252
+ self.on_request_start(request_info)
253
+ except Exception as e:
254
+ logger.error(f"Request start callback failed: {e}")
255
+
256
+ logger.debug(f"Request started: {request_id} ({request_type})")
257
+
258
+ try:
259
+ # Execute with optional timeout
260
+ if timeout:
261
+ # Python 3.10 compatible timeout implementation
262
+ timeout_handle = None
263
+ timed_out = False
264
+
265
+ def _timeout_callback():
266
+ nonlocal timed_out
267
+ timed_out = True
268
+
269
+ try:
270
+ # Set up timeout
271
+ loop = asyncio.get_event_loop()
272
+ timeout_handle = loop.call_later(timeout, _timeout_callback)
273
+
274
+ yield request_id
275
+
276
+ # Check if timed out
277
+ if timed_out:
278
+ await self._complete_request(request_id, RequestStatus.TIMEOUT, "Request timeout")
279
+ raise asyncio.TimeoutError("Request timeout")
280
+
281
+ await self._complete_request(request_id, RequestStatus.SUCCESS)
282
+
283
+ finally:
284
+ # Cancel timeout if still pending
285
+ if timeout_handle is not None:
286
+ timeout_handle.cancel()
287
+ else:
288
+ yield request_id
289
+ await self._complete_request(request_id, RequestStatus.SUCCESS)
290
+
291
+ except asyncio.CancelledError:
292
+ await self._complete_request(request_id, RequestStatus.CANCELLED, "Request cancelled")
293
+ raise
294
+
295
+ except Exception as e:
296
+ await self._complete_request(request_id, RequestStatus.FAILED, str(e))
297
+ raise
298
+
299
+ async def _complete_request(
300
+ self,
301
+ request_id: str,
302
+ status: RequestStatus,
303
+ error: Optional[str] = None,
304
+ ) -> None:
305
+ """Complete a request."""
306
+ async with self._lock:
307
+ if request_id not in self._active_requests:
308
+ logger.warning(f"Request {request_id} not found in active requests")
309
+ return
310
+
311
+ request_info = self._active_requests[request_id]
312
+ request_type = request_info.request_type
313
+
314
+ # Update request info
315
+ request_info.status = status
316
+ request_info.end_time = time.time()
317
+ request_info.duration = request_info.end_time - request_info.start_time
318
+ request_info.error = error
319
+
320
+ # Update metrics
321
+ self._metrics[request_type].active_requests -= 1
322
+ self._overall_metrics.active_requests -= 1
323
+
324
+ if status == RequestStatus.SUCCESS:
325
+ self._metrics[request_type].successful_requests += 1
326
+ self._overall_metrics.successful_requests += 1
327
+ elif status == RequestStatus.FAILED:
328
+ self._metrics[request_type].failed_requests += 1
329
+ self._overall_metrics.failed_requests += 1
330
+ elif status == RequestStatus.TIMEOUT:
331
+ self._metrics[request_type].timeout_requests += 1
332
+ self._overall_metrics.timeout_requests += 1
333
+ elif status == RequestStatus.CANCELLED:
334
+ self._metrics[request_type].cancelled_requests += 1
335
+ self._overall_metrics.cancelled_requests += 1
336
+
337
+ # Update duration stats
338
+ if request_info.duration is not None:
339
+ self._metrics[request_type].update_duration(request_info.duration)
340
+ self._overall_metrics.update_duration(request_info.duration)
341
+
342
+ # Move to history
343
+ if self.enable_history:
344
+ self._request_history.append(request_info)
345
+ if len(self._request_history) > self.max_history:
346
+ self._request_history.pop(0)
347
+
348
+ # Remove from active
349
+ del self._active_requests[request_id]
350
+
351
+ # Call complete callback
352
+ if self.on_request_complete:
353
+ try:
354
+ if asyncio.iscoroutinefunction(self.on_request_complete):
355
+ await self.on_request_complete(request_info)
356
+ else:
357
+ self.on_request_complete(request_info)
358
+ except Exception as e:
359
+ logger.error(f"Request complete callback failed: {e}")
360
+
361
+ logger.debug(
362
+ f"Request completed: {request_id} ({request_type}) - "
363
+ f"{status.value} in {request_info.duration:.3f}s"
364
+ )
365
+
366
+ def get_active_requests(self, request_type: Optional[str] = None) -> List[RequestInfo]:
367
+ """
368
+ Get active requests.
369
+
370
+ Args:
371
+ request_type: Optional filter by request type
372
+
373
+ Returns:
374
+ List of active request info
375
+ """
376
+ if request_type:
377
+ return [
378
+ req for req in self._active_requests.values()
379
+ if req.request_type == request_type
380
+ ]
381
+ return list(self._active_requests.values())
382
+
383
+ def get_request_history(
384
+ self,
385
+ request_type: Optional[str] = None,
386
+ limit: Optional[int] = None,
387
+ ) -> List[RequestInfo]:
388
+ """
389
+ Get request history.
390
+
391
+ Args:
392
+ request_type: Optional filter by request type
393
+ limit: Optional limit on number of requests
394
+
395
+ Returns:
396
+ List of completed request info (most recent first)
397
+ """
398
+ history = list(reversed(self._request_history))
399
+
400
+ if request_type:
401
+ history = [req for req in history if req.request_type == request_type]
402
+
403
+ if limit:
404
+ history = history[:limit]
405
+
406
+ return history
407
+
408
+ def get_metrics(self, request_type: Optional[str] = None) -> RequestMetrics:
409
+ """
410
+ Get request metrics.
411
+
412
+ Args:
413
+ request_type: Optional filter by request type (None for overall)
414
+
415
+ Returns:
416
+ RequestMetrics
417
+ """
418
+ if request_type:
419
+ return self._metrics[request_type]
420
+ return self._overall_metrics
421
+
422
+ def get_all_metrics(self) -> Dict[str, RequestMetrics]:
423
+ """
424
+ Get metrics for all request types.
425
+
426
+ Returns:
427
+ Dictionary mapping request type to metrics
428
+ """
429
+ return dict(self._metrics)
430
+
431
+ def reset_metrics(self, request_type: Optional[str] = None) -> None:
432
+ """
433
+ Reset metrics.
434
+
435
+ Args:
436
+ request_type: Optional request type (None resets all)
437
+ """
438
+ if request_type:
439
+ self._metrics[request_type] = RequestMetrics()
440
+ else:
441
+ self._metrics.clear()
442
+ self._overall_metrics = RequestMetrics()
443
+
444
+ logger.info(f"Metrics reset: {request_type or 'all'}")
445
+
446
+ def get_stats(self) -> Dict[str, Any]:
447
+ """
448
+ Get tracker statistics.
449
+
450
+ Returns:
451
+ Dictionary of statistics
452
+ """
453
+ return {
454
+ "active_requests_count": len(self._active_requests),
455
+ "history_count": len(self._request_history),
456
+ "tracked_request_types": len(self._metrics),
457
+ "overall_metrics": self._overall_metrics.to_dict(),
458
+ }
459
+
460
+
461
+ # Decorator for automatic request tracking
462
+
463
+ def track_request(
464
+ tracker: RequestTracker,
465
+ request_type: Optional[str] = None,
466
+ metadata: Optional[Dict[str, Any]] = None,
467
+ timeout: Optional[float] = None,
468
+ ):
469
+ """
470
+ Decorator to automatically track requests.
471
+
472
+ Args:
473
+ tracker: RequestTracker instance
474
+ request_type: Type of request (uses function name if None)
475
+ metadata: Optional metadata
476
+ timeout: Optional timeout in seconds
477
+
478
+ Example:
479
+ tracker = RequestTracker()
480
+
481
+ @track_request(tracker, "process_item")
482
+ async def process_item(item):
483
+ # Processing logic
484
+ pass
485
+
486
+ # Request automatically tracked with duration, success/failure
487
+ await process_item(my_item)
488
+ """
489
+ def decorator(func):
490
+ nonlocal request_type
491
+ if request_type is None:
492
+ request_type = func.__name__
493
+
494
+ @wraps(func)
495
+ async def wrapper(*args, **kwargs):
496
+ async with tracker.track(request_type, metadata, timeout):
497
+ return await func(*args, **kwargs)
498
+
499
+ return wrapper
500
+
501
+ return decorator
@@ -0,0 +1,11 @@
1
+ """Migration modules for state management during pod transitions."""
2
+
3
+ from dory.migration.state_manager import StateManager
4
+ from dory.migration.serialization import StateSerializer
5
+ from dory.migration.configmap import ConfigMapStore
6
+
7
+ __all__ = [
8
+ "StateManager",
9
+ "StateSerializer",
10
+ "ConfigMapStore",
11
+ ]