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,608 @@
1
+ """
2
+ Connection Tracker Middleware
3
+
4
+ Automatically tracks and manages connections (database, HTTP, etc.).
5
+ Eliminates manual connection lifecycle management.
6
+
7
+ Features:
8
+ - Automatic connection registration
9
+ - Health checking
10
+ - Auto-close on shutdown
11
+ - Connection pooling awareness
12
+ - Leak detection
13
+ """
14
+
15
+ import asyncio
16
+ import logging
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime
20
+ from enum import Enum
21
+ from typing import Any, Dict, Optional, List, Callable, Set
22
+ from functools import wraps
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ConnectionStatus(Enum):
28
+ """Connection status."""
29
+ CONNECTING = "connecting"
30
+ OPEN = "open"
31
+ CLOSING = "closing"
32
+ CLOSED = "closed"
33
+ ERROR = "error"
34
+
35
+
36
+ class ConnectionType(Enum):
37
+ """Type of connection."""
38
+ DATABASE = "database"
39
+ HTTP = "http"
40
+ WEBSOCKET = "websocket"
41
+ QUEUE = "queue"
42
+ CACHE = "cache"
43
+ CUSTOM = "custom"
44
+
45
+
46
+ @dataclass
47
+ class ConnectionInfo:
48
+ """
49
+ Information about a tracked connection.
50
+ """
51
+ connection_id: str
52
+ connection_type: ConnectionType
53
+ name: str
54
+ status: ConnectionStatus
55
+ created_at: float
56
+ last_used: float
57
+ use_count: int = 0
58
+ health_check_count: int = 0
59
+ last_health_check: Optional[float] = None
60
+ health_status: bool = True
61
+ error: Optional[str] = None
62
+ metadata: Dict[str, Any] = field(default_factory=dict)
63
+
64
+ def to_dict(self) -> Dict[str, Any]:
65
+ """Convert to dictionary."""
66
+ return {
67
+ "connection_id": self.connection_id,
68
+ "connection_type": self.connection_type.value,
69
+ "name": self.name,
70
+ "status": self.status.value,
71
+ "created_at": self.created_at,
72
+ "last_used": self.last_used,
73
+ "use_count": self.use_count,
74
+ "health_check_count": self.health_check_count,
75
+ "last_health_check": self.last_health_check,
76
+ "health_status": self.health_status,
77
+ "error": self.error,
78
+ "age_seconds": time.time() - self.created_at,
79
+ "idle_seconds": time.time() - self.last_used,
80
+ "metadata": self.metadata,
81
+ }
82
+
83
+ def is_idle(self, idle_threshold: float = 300.0) -> bool:
84
+ """
85
+ Check if connection is idle.
86
+
87
+ Args:
88
+ idle_threshold: Idle threshold in seconds
89
+
90
+ Returns:
91
+ True if idle for more than threshold
92
+ """
93
+ return (time.time() - self.last_used) > idle_threshold
94
+
95
+ def is_healthy(self) -> bool:
96
+ """Check if connection is healthy."""
97
+ return self.health_status and self.status == ConnectionStatus.OPEN
98
+
99
+
100
+ @dataclass
101
+ class ConnectionMetrics:
102
+ """Connection metrics."""
103
+ total_connections: int = 0
104
+ open_connections: int = 0
105
+ closed_connections: int = 0
106
+ failed_connections: int = 0
107
+ total_health_checks: int = 0
108
+ failed_health_checks: int = 0
109
+ total_use_count: int = 0
110
+
111
+ def to_dict(self) -> Dict[str, Any]:
112
+ """Convert to dictionary."""
113
+ return {
114
+ "total_connections": self.total_connections,
115
+ "open_connections": self.open_connections,
116
+ "closed_connections": self.closed_connections,
117
+ "failed_connections": self.failed_connections,
118
+ "total_health_checks": self.total_health_checks,
119
+ "failed_health_checks": self.failed_health_checks,
120
+ "total_use_count": self.total_use_count,
121
+ }
122
+
123
+
124
+ class ConnectionTracker:
125
+ """
126
+ Tracks and manages connections automatically.
127
+
128
+ Features:
129
+ - Auto-register connections
130
+ - Health checking
131
+ - Auto-close on shutdown
132
+ - Idle connection detection
133
+ - Connection metrics
134
+
135
+ Usage:
136
+ tracker = ConnectionTracker()
137
+
138
+ # Register connection
139
+ conn_id = await tracker.register_connection(
140
+ db_connection,
141
+ name="database",
142
+ connection_type=ConnectionType.DATABASE,
143
+ )
144
+
145
+ # Use connection (auto-tracked)
146
+ async with tracker.use_connection(conn_id):
147
+ result = await db_connection.execute(query)
148
+
149
+ # Health check
150
+ healthy = await tracker.health_check(conn_id)
151
+
152
+ # Auto-close on shutdown
153
+ await tracker.close_all_connections()
154
+ """
155
+
156
+ def __init__(
157
+ self,
158
+ enable_health_checks: bool = True,
159
+ health_check_interval: float = 60.0,
160
+ idle_timeout: float = 300.0,
161
+ auto_close_on_idle: bool = True,
162
+ on_connection_open: Optional[Callable] = None,
163
+ on_connection_close: Optional[Callable] = None,
164
+ ):
165
+ """
166
+ Initialize connection tracker.
167
+
168
+ Args:
169
+ enable_health_checks: Enable automatic health checks
170
+ health_check_interval: Interval between health checks (seconds)
171
+ idle_timeout: Timeout for idle connections (seconds)
172
+ auto_close_on_idle: Automatically close idle connections
173
+ on_connection_open: Callback when connection opens
174
+ on_connection_close: Callback when connection closes
175
+ """
176
+ self.enable_health_checks = enable_health_checks
177
+ self.health_check_interval = health_check_interval
178
+ self.idle_timeout = idle_timeout
179
+ self.auto_close_on_idle = auto_close_on_idle
180
+ self.on_connection_open = on_connection_open
181
+ self.on_connection_close = on_connection_close
182
+
183
+ # Tracked connections
184
+ self._connections: Dict[str, ConnectionInfo] = {}
185
+
186
+ # Connection objects
187
+ self._connection_objects: Dict[str, Any] = {}
188
+
189
+ # Health check functions
190
+ self._health_check_funcs: Dict[str, Callable] = {}
191
+
192
+ # Close functions
193
+ self._close_funcs: Dict[str, Callable] = {}
194
+
195
+ # Metrics
196
+ self._metrics = ConnectionMetrics()
197
+
198
+ # Health check task
199
+ self._health_check_task: Optional[asyncio.Task] = None
200
+
201
+ # Counter
202
+ self._connection_counter = 0
203
+
204
+ # Lock
205
+ self._lock = asyncio.Lock()
206
+
207
+ logger.info(
208
+ f"ConnectionTracker initialized: health_checks={enable_health_checks}, "
209
+ f"health_interval={health_check_interval}s, idle_timeout={idle_timeout}s"
210
+ )
211
+
212
+ # Start health check loop
213
+ if enable_health_checks:
214
+ self._start_health_check_loop()
215
+
216
+ def _generate_connection_id(self, name: str) -> str:
217
+ """Generate unique connection ID."""
218
+ self._connection_counter += 1
219
+ timestamp = int(time.time() * 1000)
220
+ return f"{name}_{timestamp}_{self._connection_counter}"
221
+
222
+ async def register_connection(
223
+ self,
224
+ connection: Any,
225
+ name: str,
226
+ connection_type: ConnectionType = ConnectionType.CUSTOM,
227
+ health_check_func: Optional[Callable] = None,
228
+ close_func: Optional[Callable] = None,
229
+ metadata: Optional[Dict[str, Any]] = None,
230
+ ) -> str:
231
+ """
232
+ Register a connection for tracking.
233
+
234
+ Args:
235
+ connection: Connection object
236
+ name: Connection name
237
+ connection_type: Type of connection
238
+ health_check_func: Optional health check function
239
+ close_func: Optional close function
240
+ metadata: Optional metadata
241
+
242
+ Returns:
243
+ Connection ID
244
+
245
+ Example:
246
+ conn_id = await tracker.register_connection(
247
+ db_connection,
248
+ name="postgres",
249
+ connection_type=ConnectionType.DATABASE,
250
+ health_check_func=lambda conn: conn.is_alive(),
251
+ close_func=lambda conn: conn.close(),
252
+ )
253
+ """
254
+ async with self._lock:
255
+ connection_id = self._generate_connection_id(name)
256
+
257
+ # Create connection info
258
+ conn_info = ConnectionInfo(
259
+ connection_id=connection_id,
260
+ connection_type=connection_type,
261
+ name=name,
262
+ status=ConnectionStatus.OPEN,
263
+ created_at=time.time(),
264
+ last_used=time.time(),
265
+ metadata=metadata or {},
266
+ )
267
+
268
+ # Store
269
+ self._connections[connection_id] = conn_info
270
+ self._connection_objects[connection_id] = connection
271
+
272
+ if health_check_func:
273
+ self._health_check_funcs[connection_id] = health_check_func
274
+ if close_func:
275
+ self._close_funcs[connection_id] = close_func
276
+
277
+ # Update metrics
278
+ self._metrics.total_connections += 1
279
+ self._metrics.open_connections += 1
280
+
281
+ # Call open callback
282
+ if self.on_connection_open:
283
+ try:
284
+ if asyncio.iscoroutinefunction(self.on_connection_open):
285
+ await self.on_connection_open(conn_info)
286
+ else:
287
+ self.on_connection_open(conn_info)
288
+ except Exception as e:
289
+ logger.error(f"Connection open callback failed: {e}")
290
+
291
+ logger.info(f"Connection registered: {connection_id} ({name})")
292
+
293
+ return connection_id
294
+
295
+ async def use_connection(self, connection_id: str):
296
+ """
297
+ Context manager to use a connection.
298
+
299
+ Updates last_used timestamp and use_count.
300
+
301
+ Args:
302
+ connection_id: Connection ID
303
+
304
+ Example:
305
+ async with tracker.use_connection(conn_id):
306
+ result = await connection.execute(query)
307
+ """
308
+ if connection_id not in self._connections:
309
+ raise ValueError(f"Connection not found: {connection_id}")
310
+
311
+ conn_info = self._connections[connection_id]
312
+
313
+ # Update usage
314
+ conn_info.last_used = time.time()
315
+ conn_info.use_count += 1
316
+ self._metrics.total_use_count += 1
317
+
318
+ try:
319
+ yield self._connection_objects[connection_id]
320
+ except Exception as e:
321
+ logger.error(f"Error using connection {connection_id}: {e}")
322
+ conn_info.error = str(e)
323
+ conn_info.health_status = False
324
+ raise
325
+
326
+ async def health_check(self, connection_id: str) -> bool:
327
+ """
328
+ Perform health check on a connection.
329
+
330
+ Args:
331
+ connection_id: Connection ID
332
+
333
+ Returns:
334
+ True if healthy
335
+ """
336
+ if connection_id not in self._connections:
337
+ return False
338
+
339
+ conn_info = self._connections[connection_id]
340
+ conn_info.health_check_count += 1
341
+ conn_info.last_health_check = time.time()
342
+ self._metrics.total_health_checks += 1
343
+
344
+ # Get health check function
345
+ health_check_func = self._health_check_funcs.get(connection_id)
346
+ if not health_check_func:
347
+ # No health check function, assume healthy if open
348
+ conn_info.health_status = conn_info.status == ConnectionStatus.OPEN
349
+ return conn_info.health_status
350
+
351
+ # Run health check
352
+ try:
353
+ connection = self._connection_objects[connection_id]
354
+ if asyncio.iscoroutinefunction(health_check_func):
355
+ healthy = await health_check_func(connection)
356
+ else:
357
+ healthy = health_check_func(connection)
358
+
359
+ conn_info.health_status = bool(healthy)
360
+
361
+ if not healthy:
362
+ self._metrics.failed_health_checks += 1
363
+ logger.warning(f"Health check failed: {connection_id}")
364
+
365
+ return conn_info.health_status
366
+
367
+ except Exception as e:
368
+ logger.error(f"Health check error for {connection_id}: {e}")
369
+ conn_info.health_status = False
370
+ conn_info.error = str(e)
371
+ self._metrics.failed_health_checks += 1
372
+ return False
373
+
374
+ async def close_connection(self, connection_id: str) -> bool:
375
+ """
376
+ Close a connection.
377
+
378
+ Args:
379
+ connection_id: Connection ID
380
+
381
+ Returns:
382
+ True if closed successfully
383
+ """
384
+ if connection_id not in self._connections:
385
+ logger.warning(f"Connection not found: {connection_id}")
386
+ return False
387
+
388
+ conn_info = self._connections[connection_id]
389
+ conn_info.status = ConnectionStatus.CLOSING
390
+
391
+ try:
392
+ # Get close function
393
+ close_func = self._close_funcs.get(connection_id)
394
+ if close_func:
395
+ connection = self._connection_objects[connection_id]
396
+ if asyncio.iscoroutinefunction(close_func):
397
+ await close_func(connection)
398
+ else:
399
+ close_func(connection)
400
+
401
+ # Update status
402
+ conn_info.status = ConnectionStatus.CLOSED
403
+
404
+ # Update metrics
405
+ async with self._lock:
406
+ self._metrics.open_connections -= 1
407
+ self._metrics.closed_connections += 1
408
+
409
+ # Remove from tracking
410
+ del self._connections[connection_id]
411
+ del self._connection_objects[connection_id]
412
+ self._health_check_funcs.pop(connection_id, None)
413
+ self._close_funcs.pop(connection_id, None)
414
+
415
+ # Call close callback
416
+ if self.on_connection_close:
417
+ try:
418
+ if asyncio.iscoroutinefunction(self.on_connection_close):
419
+ await self.on_connection_close(conn_info)
420
+ else:
421
+ self.on_connection_close(conn_info)
422
+ except Exception as e:
423
+ logger.error(f"Connection close callback failed: {e}")
424
+
425
+ logger.info(f"Connection closed: {connection_id}")
426
+ return True
427
+
428
+ except Exception as e:
429
+ logger.error(f"Failed to close connection {connection_id}: {e}")
430
+ conn_info.status = ConnectionStatus.ERROR
431
+ conn_info.error = str(e)
432
+ self._metrics.failed_connections += 1
433
+ return False
434
+
435
+ async def close_all_connections(self) -> int:
436
+ """
437
+ Close all tracked connections.
438
+
439
+ Returns:
440
+ Number of connections closed
441
+ """
442
+ logger.info("Closing all connections")
443
+
444
+ # Get list of connection IDs
445
+ connection_ids = list(self._connections.keys())
446
+
447
+ closed_count = 0
448
+ for conn_id in connection_ids:
449
+ if await self.close_connection(conn_id):
450
+ closed_count += 1
451
+
452
+ logger.info(f"Closed {closed_count} connections")
453
+ return closed_count
454
+
455
+ def get_connections(
456
+ self,
457
+ connection_type: Optional[ConnectionType] = None,
458
+ only_open: bool = False,
459
+ ) -> List[ConnectionInfo]:
460
+ """
461
+ Get tracked connections.
462
+
463
+ Args:
464
+ connection_type: Optional filter by type
465
+ only_open: Only return open connections
466
+
467
+ Returns:
468
+ List of connection info
469
+ """
470
+ connections = list(self._connections.values())
471
+
472
+ if connection_type:
473
+ connections = [c for c in connections if c.connection_type == connection_type]
474
+
475
+ if only_open:
476
+ connections = [c for c in connections if c.status == ConnectionStatus.OPEN]
477
+
478
+ return connections
479
+
480
+ def get_idle_connections(self, idle_threshold: Optional[float] = None) -> List[ConnectionInfo]:
481
+ """
482
+ Get idle connections.
483
+
484
+ Args:
485
+ idle_threshold: Optional threshold (uses default if None)
486
+
487
+ Returns:
488
+ List of idle connection info
489
+ """
490
+ threshold = idle_threshold or self.idle_timeout
491
+ return [
492
+ conn for conn in self._connections.values()
493
+ if conn.is_idle(threshold)
494
+ ]
495
+
496
+ def get_metrics(self) -> ConnectionMetrics:
497
+ """Get connection metrics."""
498
+ return self._metrics
499
+
500
+ def get_stats(self) -> Dict[str, Any]:
501
+ """
502
+ Get tracker statistics.
503
+
504
+ Returns:
505
+ Dictionary of statistics
506
+ """
507
+ return {
508
+ "total_tracked": len(self._connections),
509
+ "metrics": self._metrics.to_dict(),
510
+ "health_checks_enabled": self.enable_health_checks,
511
+ "auto_close_on_idle": self.auto_close_on_idle,
512
+ }
513
+
514
+ def _start_health_check_loop(self) -> None:
515
+ """Start health check background loop."""
516
+ if self._health_check_task and not self._health_check_task.done():
517
+ return
518
+
519
+ self._health_check_task = asyncio.create_task(self._health_check_loop())
520
+
521
+ async def _health_check_loop(self) -> None:
522
+ """Background health check loop."""
523
+ logger.info("Starting health check loop")
524
+
525
+ while True:
526
+ try:
527
+ await asyncio.sleep(self.health_check_interval)
528
+
529
+ # Health check all connections
530
+ connection_ids = list(self._connections.keys())
531
+ for conn_id in connection_ids:
532
+ await self.health_check(conn_id)
533
+
534
+ # Auto-close idle connections
535
+ if self.auto_close_on_idle:
536
+ idle_connections = self.get_idle_connections()
537
+ for conn_info in idle_connections:
538
+ logger.info(
539
+ f"Closing idle connection: {conn_info.connection_id} "
540
+ f"(idle for {conn_info.idle_seconds:.1f}s)"
541
+ )
542
+ await self.close_connection(conn_info.connection_id)
543
+
544
+ except asyncio.CancelledError:
545
+ logger.info("Health check loop cancelled")
546
+ break
547
+ except Exception as e:
548
+ logger.error(f"Health check loop error: {e}")
549
+
550
+ async def stop(self) -> None:
551
+ """Stop tracker and close all connections."""
552
+ logger.info("Stopping connection tracker")
553
+
554
+ # Stop health check loop
555
+ if self._health_check_task:
556
+ self._health_check_task.cancel()
557
+ try:
558
+ await self._health_check_task
559
+ except asyncio.CancelledError:
560
+ pass
561
+
562
+ # Close all connections
563
+ await self.close_all_connections()
564
+
565
+
566
+ # Decorator for automatic connection tracking
567
+
568
+ def track_connection(
569
+ tracker: ConnectionTracker,
570
+ name: str,
571
+ connection_type: ConnectionType = ConnectionType.CUSTOM,
572
+ ):
573
+ """
574
+ Decorator to automatically track connections.
575
+
576
+ Args:
577
+ tracker: ConnectionTracker instance
578
+ name: Connection name
579
+ connection_type: Type of connection
580
+
581
+ Example:
582
+ tracker = ConnectionTracker()
583
+
584
+ @track_connection(tracker, "database", ConnectionType.DATABASE)
585
+ async def create_db_connection():
586
+ return await create_connection()
587
+
588
+ # Connection automatically tracked
589
+ conn = await create_db_connection()
590
+ """
591
+ def decorator(func):
592
+ @wraps(func)
593
+ async def wrapper(*args, **kwargs):
594
+ # Create connection
595
+ connection = await func(*args, **kwargs)
596
+
597
+ # Register with tracker
598
+ await tracker.register_connection(
599
+ connection,
600
+ name=name,
601
+ connection_type=connection_type,
602
+ )
603
+
604
+ return connection
605
+
606
+ return wrapper
607
+
608
+ return decorator