dory-processor-sdk 0.0.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 (86) hide show
  1. dory/__init__.py +101 -0
  2. dory/auth/__init__.py +10 -0
  3. dory/auth/oauth2.py +153 -0
  4. dory/auto_instrument.py +142 -0
  5. dory/cli/__init__.py +5 -0
  6. dory/cli/main.py +137 -0
  7. dory/cli/templates.py +123 -0
  8. dory/config/__init__.py +23 -0
  9. dory/config/defaults.py +24 -0
  10. dory/config/loader.py +430 -0
  11. dory/config/presets.py +73 -0
  12. dory/config/schema.py +84 -0
  13. dory/core/__init__.py +27 -0
  14. dory/core/app.py +434 -0
  15. dory/core/context.py +209 -0
  16. dory/core/lifecycle.py +214 -0
  17. dory/core/meta.py +121 -0
  18. dory/core/modes.py +479 -0
  19. dory/core/processor.py +564 -0
  20. dory/core/signals.py +122 -0
  21. dory/decorators.py +142 -0
  22. dory/edge/__init__.py +88 -0
  23. dory/edge/adaptive.py +644 -0
  24. dory/edge/detector.py +546 -0
  25. dory/edge/fencing.py +488 -0
  26. dory/edge/heartbeat.py +598 -0
  27. dory/edge/role.py +419 -0
  28. dory/errors/__init__.py +139 -0
  29. dory/errors/classification.py +362 -0
  30. dory/errors/codes.py +498 -0
  31. dory/geo/__init__.py +40 -0
  32. dory/geo/geolocalizer.py +1034 -0
  33. dory/health/__init__.py +12 -0
  34. dory/health/probes.py +210 -0
  35. dory/health/server.py +635 -0
  36. dory/k8s/__init__.py +80 -0
  37. dory/k8s/annotation_watcher.py +184 -0
  38. dory/k8s/client.py +251 -0
  39. dory/k8s/labels.py +505 -0
  40. dory/k8s/pod_metadata.py +182 -0
  41. dory/logging/__init__.py +9 -0
  42. dory/logging/logger.py +148 -0
  43. dory/metrics/__init__.py +7 -0
  44. dory/metrics/collector.py +301 -0
  45. dory/middleware/__init__.py +46 -0
  46. dory/middleware/connection_tracker.py +608 -0
  47. dory/middleware/request_id.py +325 -0
  48. dory/middleware/request_tracker.py +511 -0
  49. dory/migration/__init__.py +33 -0
  50. dory/migration/configmap.py +232 -0
  51. dory/migration/s3_store.py +594 -0
  52. dory/migration/serialization.py +135 -0
  53. dory/migration/state_manager.py +286 -0
  54. dory/migration/transfer.py +382 -0
  55. dory/monitoring/__init__.py +29 -0
  56. dory/monitoring/opentelemetry.py +489 -0
  57. dory/output/__init__.py +31 -0
  58. dory/output/envelope.py +137 -0
  59. dory/output/formatter.py +113 -0
  60. dory/output/rabbitmq.py +632 -0
  61. dory/output/routing.py +318 -0
  62. dory/output/validator.py +199 -0
  63. dory/py.typed +2 -0
  64. dory/recovery/__init__.py +60 -0
  65. dory/recovery/golden_image.py +487 -0
  66. dory/recovery/golden_snapshot.py +713 -0
  67. dory/recovery/golden_validator.py +518 -0
  68. dory/recovery/partial_recovery.py +482 -0
  69. dory/recovery/recovery_decision.py +242 -0
  70. dory/recovery/restart_detector.py +142 -0
  71. dory/recovery/state_validator.py +183 -0
  72. dory/resilience/__init__.py +45 -0
  73. dory/resilience/circuit_breaker.py +457 -0
  74. dory/resilience/retry.py +389 -0
  75. dory/simple.py +342 -0
  76. dory/types.py +68 -0
  77. dory/utils/__init__.py +31 -0
  78. dory/utils/errors.py +59 -0
  79. dory/utils/retry.py +115 -0
  80. dory/utils/timeout.py +80 -0
  81. dory_processor_sdk-0.0.1.dist-info/METADATA +424 -0
  82. dory_processor_sdk-0.0.1.dist-info/RECORD +86 -0
  83. dory_processor_sdk-0.0.1.dist-info/WHEEL +5 -0
  84. dory_processor_sdk-0.0.1.dist-info/entry_points.txt +2 -0
  85. dory_processor_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
  86. dory_processor_sdk-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,511 @@
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, deque
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) - deque for O(1) append/trim
178
+ self._request_history: deque[RequestInfo] = deque(maxlen=max_history)
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
+ def track_request(self, request_id: str = None, **kwargs):
204
+ """Track a request by ID. Used by auto_instrument for compatibility.
205
+
206
+ Args:
207
+ request_id: Optional request ID to use as request type identifier.
208
+
209
+ Returns:
210
+ Async context manager for tracking the request.
211
+ """
212
+ request_type = request_id or "auto_instrumented"
213
+ return self.track(request_type=request_type, **kwargs)
214
+
215
+ @asynccontextmanager
216
+ async def track(
217
+ self,
218
+ request_type: str,
219
+ metadata: Optional[Dict[str, Any]] = None,
220
+ timeout: Optional[float] = None,
221
+ ):
222
+ """
223
+ Context manager to track a request.
224
+
225
+ Args:
226
+ request_type: Type of request
227
+ metadata: Optional metadata
228
+ timeout: Optional timeout in seconds
229
+
230
+ Yields:
231
+ Request ID
232
+
233
+ Example:
234
+ async with tracker.track("process_item") as request_id:
235
+ await process_item()
236
+ """
237
+ request_id = self._generate_request_id(request_type)
238
+
239
+ async with self._lock:
240
+ # Create request info
241
+ request_info = RequestInfo(
242
+ request_id=request_id,
243
+ request_type=request_type,
244
+ status=RequestStatus.IN_PROGRESS,
245
+ start_time=time.time(),
246
+ metadata=metadata or {},
247
+ )
248
+
249
+ # Add to active requests
250
+ self._active_requests[request_id] = request_info
251
+
252
+ # Update metrics
253
+ self._metrics[request_type].total_requests += 1
254
+ self._metrics[request_type].active_requests += 1
255
+ self._overall_metrics.total_requests += 1
256
+ self._overall_metrics.active_requests += 1
257
+
258
+ # Call start callback
259
+ if self.on_request_start:
260
+ try:
261
+ if asyncio.iscoroutinefunction(self.on_request_start):
262
+ await self.on_request_start(request_info)
263
+ else:
264
+ self.on_request_start(request_info)
265
+ except Exception as e:
266
+ logger.error(f"Request start callback failed: {e}")
267
+
268
+ logger.debug(f"Request started: {request_id} ({request_type})")
269
+
270
+ try:
271
+ # Execute with optional timeout
272
+ if timeout:
273
+ # Python 3.10 compatible timeout implementation
274
+ timeout_handle = None
275
+ timed_out = False
276
+
277
+ def _timeout_callback():
278
+ nonlocal timed_out
279
+ timed_out = True
280
+
281
+ try:
282
+ # Set up timeout
283
+ loop = asyncio.get_event_loop()
284
+ timeout_handle = loop.call_later(timeout, _timeout_callback)
285
+
286
+ yield request_id
287
+
288
+ # Check if timed out
289
+ if timed_out:
290
+ await self._complete_request(request_id, RequestStatus.TIMEOUT, "Request timeout")
291
+ raise asyncio.TimeoutError("Request timeout")
292
+
293
+ await self._complete_request(request_id, RequestStatus.SUCCESS)
294
+
295
+ finally:
296
+ # Cancel timeout if still pending
297
+ if timeout_handle is not None:
298
+ timeout_handle.cancel()
299
+ else:
300
+ yield request_id
301
+ await self._complete_request(request_id, RequestStatus.SUCCESS)
302
+
303
+ except asyncio.CancelledError:
304
+ await self._complete_request(request_id, RequestStatus.CANCELLED, "Request cancelled")
305
+ raise
306
+
307
+ except Exception as e:
308
+ await self._complete_request(request_id, RequestStatus.FAILED, str(e))
309
+ raise
310
+
311
+ async def _complete_request(
312
+ self,
313
+ request_id: str,
314
+ status: RequestStatus,
315
+ error: Optional[str] = None,
316
+ ) -> None:
317
+ """Complete a request."""
318
+ async with self._lock:
319
+ if request_id not in self._active_requests:
320
+ logger.warning(f"Request {request_id} not found in active requests")
321
+ return
322
+
323
+ request_info = self._active_requests[request_id]
324
+ request_type = request_info.request_type
325
+
326
+ # Update request info
327
+ request_info.status = status
328
+ request_info.end_time = time.time()
329
+ request_info.duration = request_info.end_time - request_info.start_time
330
+ request_info.error = error
331
+
332
+ # Update metrics
333
+ self._metrics[request_type].active_requests -= 1
334
+ self._overall_metrics.active_requests -= 1
335
+
336
+ if status == RequestStatus.SUCCESS:
337
+ self._metrics[request_type].successful_requests += 1
338
+ self._overall_metrics.successful_requests += 1
339
+ elif status == RequestStatus.FAILED:
340
+ self._metrics[request_type].failed_requests += 1
341
+ self._overall_metrics.failed_requests += 1
342
+ elif status == RequestStatus.TIMEOUT:
343
+ self._metrics[request_type].timeout_requests += 1
344
+ self._overall_metrics.timeout_requests += 1
345
+ elif status == RequestStatus.CANCELLED:
346
+ self._metrics[request_type].cancelled_requests += 1
347
+ self._overall_metrics.cancelled_requests += 1
348
+
349
+ # Update duration stats
350
+ if request_info.duration is not None:
351
+ self._metrics[request_type].update_duration(request_info.duration)
352
+ self._overall_metrics.update_duration(request_info.duration)
353
+
354
+ # Move to history (deque maxlen handles pruning automatically)
355
+ if self.enable_history:
356
+ self._request_history.append(request_info)
357
+
358
+ # Remove from active
359
+ del self._active_requests[request_id]
360
+
361
+ # Call complete callback
362
+ if self.on_request_complete:
363
+ try:
364
+ if asyncio.iscoroutinefunction(self.on_request_complete):
365
+ await self.on_request_complete(request_info)
366
+ else:
367
+ self.on_request_complete(request_info)
368
+ except Exception as e:
369
+ logger.error(f"Request complete callback failed: {e}")
370
+
371
+ logger.debug(
372
+ f"Request completed: {request_id} ({request_type}) - "
373
+ f"{status.value} in {request_info.duration:.3f}s"
374
+ )
375
+
376
+ def get_active_requests(self, request_type: Optional[str] = None) -> List[RequestInfo]:
377
+ """
378
+ Get active requests.
379
+
380
+ Args:
381
+ request_type: Optional filter by request type
382
+
383
+ Returns:
384
+ List of active request info
385
+ """
386
+ if request_type:
387
+ return [
388
+ req for req in self._active_requests.values()
389
+ if req.request_type == request_type
390
+ ]
391
+ return list(self._active_requests.values())
392
+
393
+ def get_request_history(
394
+ self,
395
+ request_type: Optional[str] = None,
396
+ limit: Optional[int] = None,
397
+ ) -> List[RequestInfo]:
398
+ """
399
+ Get request history.
400
+
401
+ Args:
402
+ request_type: Optional filter by request type
403
+ limit: Optional limit on number of requests
404
+
405
+ Returns:
406
+ List of completed request info (most recent first)
407
+ """
408
+ history = list(reversed(self._request_history))
409
+
410
+ if request_type:
411
+ history = [req for req in history if req.request_type == request_type]
412
+
413
+ if limit:
414
+ history = history[:limit]
415
+
416
+ return history
417
+
418
+ def get_metrics(self, request_type: Optional[str] = None) -> RequestMetrics:
419
+ """
420
+ Get request metrics.
421
+
422
+ Args:
423
+ request_type: Optional filter by request type (None for overall)
424
+
425
+ Returns:
426
+ RequestMetrics
427
+ """
428
+ if request_type:
429
+ return self._metrics[request_type]
430
+ return self._overall_metrics
431
+
432
+ def get_all_metrics(self) -> Dict[str, RequestMetrics]:
433
+ """
434
+ Get metrics for all request types.
435
+
436
+ Returns:
437
+ Dictionary mapping request type to metrics
438
+ """
439
+ return dict(self._metrics)
440
+
441
+ def reset_metrics(self, request_type: Optional[str] = None) -> None:
442
+ """
443
+ Reset metrics.
444
+
445
+ Args:
446
+ request_type: Optional request type (None resets all)
447
+ """
448
+ if request_type:
449
+ self._metrics[request_type] = RequestMetrics()
450
+ else:
451
+ self._metrics.clear()
452
+ self._overall_metrics = RequestMetrics()
453
+
454
+ logger.info(f"Metrics reset: {request_type or 'all'}")
455
+
456
+ def get_stats(self) -> Dict[str, Any]:
457
+ """
458
+ Get tracker statistics.
459
+
460
+ Returns:
461
+ Dictionary of statistics
462
+ """
463
+ return {
464
+ "active_requests_count": len(self._active_requests),
465
+ "history_count": len(self._request_history),
466
+ "tracked_request_types": len(self._metrics),
467
+ "overall_metrics": self._overall_metrics.to_dict(),
468
+ }
469
+
470
+
471
+ # Decorator for automatic request tracking
472
+
473
+ def track_request(
474
+ tracker: RequestTracker,
475
+ request_type: Optional[str] = None,
476
+ metadata: Optional[Dict[str, Any]] = None,
477
+ timeout: Optional[float] = None,
478
+ ):
479
+ """
480
+ Decorator to automatically track requests.
481
+
482
+ Args:
483
+ tracker: RequestTracker instance
484
+ request_type: Type of request (uses function name if None)
485
+ metadata: Optional metadata
486
+ timeout: Optional timeout in seconds
487
+
488
+ Example:
489
+ tracker = RequestTracker()
490
+
491
+ @track_request(tracker, "process_item")
492
+ async def process_item(item):
493
+ # Processing logic
494
+ pass
495
+
496
+ # Request automatically tracked with duration, success/failure
497
+ await process_item(my_item)
498
+ """
499
+ def decorator(func):
500
+ nonlocal request_type
501
+ if request_type is None:
502
+ request_type = func.__name__
503
+
504
+ @wraps(func)
505
+ async def wrapper(*args, **kwargs):
506
+ async with tracker.track(request_type, metadata, timeout):
507
+ return await func(*args, **kwargs)
508
+
509
+ return wrapper
510
+
511
+ return decorator
@@ -0,0 +1,33 @@
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
+ from dory.migration.s3_store import S3Store, S3Config, OfflineBuffer
7
+ from dory.migration.transfer import (
8
+ TransferConfig,
9
+ TransferMetrics,
10
+ StateTransferError,
11
+ StateTransferTimeout,
12
+ StateSizeExceeded,
13
+ validate_state_size,
14
+ ORCHESTRATOR_STATE_TIMEOUT_SEC,
15
+ ORCHESTRATOR_MAX_STATE_SIZE,
16
+ )
17
+
18
+ __all__ = [
19
+ "StateManager",
20
+ "StateSerializer",
21
+ "ConfigMapStore",
22
+ "S3Store",
23
+ "S3Config",
24
+ "OfflineBuffer",
25
+ "TransferConfig",
26
+ "TransferMetrics",
27
+ "StateTransferError",
28
+ "StateTransferTimeout",
29
+ "StateSizeExceeded",
30
+ "validate_state_size",
31
+ "ORCHESTRATOR_STATE_TIMEOUT_SEC",
32
+ "ORCHESTRATOR_MAX_STATE_SIZE",
33
+ ]