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
dory/core/processor.py ADDED
@@ -0,0 +1,654 @@
1
+ """
2
+ BaseProcessor - Abstract base class for processor implementations.
3
+
4
+ Developers implement this class to create their processor applications.
5
+ The SDK handles all lifecycle, state management, and health concerns.
6
+
7
+ SDK v2.1 Auto-Features:
8
+ 1. Auto-Initialization - All components created from config:
9
+ - Circuit breakers (self.circuit_breakers)
10
+ - Error classifier (self.error_classifier)
11
+ - OpenTelemetry (self.otel)
12
+ - Request tracker (self.request_tracker)
13
+ - Request ID middleware (self.request_id_middleware)
14
+ - Connection tracker (self.connection_tracker)
15
+
16
+ 2. Auto-Instrumentation - All handler methods automatically get:
17
+ - Request ID generation
18
+ - Request tracking
19
+ - OpenTelemetry spans
20
+ - Error classification
21
+ - No decorators needed!
22
+ """
23
+
24
+ import asyncio
25
+ import logging
26
+ from abc import ABC, abstractmethod
27
+ from typing import TYPE_CHECKING, AsyncIterator, Dict, Any, Optional
28
+
29
+ from dory.decorators import get_stateful_vars, set_stateful_vars
30
+ from dory.core.meta import AutoInstrumentMeta
31
+
32
+ if TYPE_CHECKING:
33
+ from dory.core.context import ExecutionContext
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class BaseProcessor(ABC, metaclass=AutoInstrumentMeta):
39
+ """
40
+ Abstract base class for processor implementations.
41
+
42
+ Required method:
43
+ - run(): Main processing loop
44
+
45
+ Optional methods (have sensible defaults):
46
+ - startup(): Initialize resources (default: no-op)
47
+ - shutdown(): Cleanup resources (default: no-op)
48
+ - get_state(): Return state dict (default: returns @stateful vars or {})
49
+ - restore_state(): Restore state (default: restores @stateful vars)
50
+
51
+ Optional fault handling hooks:
52
+ - on_state_restore_failed(): Handle state restore errors
53
+ - on_rapid_restart_detected(): Handle restart loop
54
+ - on_health_check_failed(): Handle health check errors
55
+ - reset_caches(): Clean caches during golden image reset
56
+
57
+ Usage:
58
+ # Minimal implementation (just run method)
59
+ class MyProcessor(BaseProcessor):
60
+ counter = stateful(0)
61
+
62
+ async def run(self):
63
+ async for _ in self.run_loop(interval=1):
64
+ self.counter += 1
65
+
66
+ # Full implementation
67
+ class MyProcessor(BaseProcessor):
68
+ async def startup(self):
69
+ self.model = load_model()
70
+
71
+ async def run(self):
72
+ while not self.context.is_shutdown_requested():
73
+ process()
74
+
75
+ async def shutdown(self):
76
+ self.model.close()
77
+
78
+ def get_state(self):
79
+ return {"processed": self.count}
80
+
81
+ async def restore_state(self, state):
82
+ self.count = state.get("processed", 0)
83
+ """
84
+
85
+ # Optional: Define state schema for validation
86
+ # Schema example: {'processed_count': int, 'last_frame_id': int}
87
+ state_schema: dict[str, type] | None = None
88
+
89
+ # Context is auto-injected by DoryApp (no need to accept in __init__)
90
+ context: "ExecutionContext"
91
+
92
+ def __init__(self, context: "ExecutionContext | None" = None):
93
+ """
94
+ Initialize processor with auto-initialization of SDK components.
95
+
96
+ Args:
97
+ context: ExecutionContext (optional - will be auto-injected if not provided)
98
+
99
+ Auto-Initialized Components (SDK v2.1):
100
+ - self.error_classifier: Automatic error classification
101
+ - self.circuit_breakers: Dict of circuit breakers (database, external_api, cache)
102
+ - self.otel: OpenTelemetry manager (if enabled in config)
103
+ - self.request_tracker: Request tracking middleware (if enabled)
104
+ - self.request_id_middleware: Request ID generation (if enabled)
105
+ - self.connection_tracker: Connection lifecycle tracking (if enabled)
106
+
107
+ Note:
108
+ You can override __init__ and call super().__init__(context) to get
109
+ auto-initialization, or skip super() call to manually initialize.
110
+ """
111
+ if context is not None:
112
+ self.context = context
113
+
114
+ # Auto-initialize SDK components if context is available
115
+ self._auto_initialize_components()
116
+
117
+ # =========================================================================
118
+ # Required Method
119
+ # =========================================================================
120
+
121
+ @abstractmethod
122
+ async def run(self) -> None:
123
+ """
124
+ Main processing loop.
125
+
126
+ Called after startup() and restore_state(). Must check
127
+ context.is_shutdown_requested() periodically to exit gracefully.
128
+
129
+ You can use self.run_loop() helper for cleaner code:
130
+
131
+ async def run(self):
132
+ async for _ in self.run_loop(interval=1):
133
+ self.counter += 1
134
+
135
+ Or traditional while loop:
136
+
137
+ async def run(self):
138
+ while not self.context.is_shutdown_requested():
139
+ self.counter += 1
140
+ await asyncio.sleep(1)
141
+
142
+ Raises:
143
+ Any exception will cause pod crash
144
+ """
145
+ raise NotImplementedError
146
+
147
+ # =========================================================================
148
+ # Optional Lifecycle Methods (Override if needed)
149
+ # =========================================================================
150
+
151
+ async def startup(self) -> None:
152
+ """
153
+ Initialize processor resources (optional).
154
+
155
+ Called once at pod startup after __init__ but before run().
156
+ Override to load models, open connections, etc.
157
+
158
+ Default: No-op
159
+ """
160
+ pass
161
+
162
+ async def shutdown(self) -> None:
163
+ """
164
+ Cleanup processor resources (optional).
165
+
166
+ Called on graceful shutdown (SIGTERM). Has max timeout
167
+ (configurable via DORY_SHUTDOWN_TIMEOUT_SEC, default 30s).
168
+ Override to close connections, flush buffers, etc.
169
+
170
+ Default: No-op
171
+ """
172
+ pass
173
+
174
+ def get_state(self) -> dict:
175
+ """
176
+ Return state to migrate to next pod (optional).
177
+
178
+ Called during migration (must be fast, <1s). State must be
179
+ JSON-serializable.
180
+
181
+ Default: Returns all @stateful decorated attributes, or {} if none.
182
+
183
+ Override for custom state:
184
+ def get_state(self):
185
+ return {"counter": self.counter, "data": self.data}
186
+ """
187
+ # Auto-collect @stateful decorated attributes
188
+ stateful_state = get_stateful_vars(self)
189
+ if stateful_state:
190
+ return stateful_state
191
+ return {}
192
+
193
+ async def restore_state(self, state: dict) -> None:
194
+ """
195
+ Restore state from previous pod (optional).
196
+
197
+ Called after startup() but before run() if state exists.
198
+
199
+ Default: Restores all @stateful decorated attributes from state.
200
+
201
+ Override for custom restoration:
202
+ async def restore_state(self, state):
203
+ self.counter = state.get("counter", 0)
204
+ """
205
+ # Auto-restore @stateful decorated attributes
206
+ set_stateful_vars(self, state)
207
+
208
+ # =========================================================================
209
+ # Helper Methods
210
+ # =========================================================================
211
+
212
+ async def run_loop(
213
+ self,
214
+ interval: float = 1.0,
215
+ check_migration: bool = True,
216
+ ) -> AsyncIterator[int]:
217
+ """
218
+ Async iterator that yields until shutdown is requested.
219
+
220
+ Simplifies the common pattern of checking shutdown in a loop.
221
+
222
+ Args:
223
+ interval: Sleep interval between iterations (seconds)
224
+ check_migration: If True, also yields when migration is imminent
225
+
226
+ Yields:
227
+ Iteration count (0, 1, 2, ...)
228
+
229
+ Usage:
230
+ async def run(self):
231
+ async for i in self.run_loop(interval=1):
232
+ self.counter += 1
233
+ print(f"Iteration {i}")
234
+
235
+ # Equivalent to:
236
+ async def run(self):
237
+ i = 0
238
+ while not self.context.is_shutdown_requested():
239
+ self.counter += 1
240
+ print(f"Iteration {i}")
241
+ i += 1
242
+ await asyncio.sleep(1)
243
+ """
244
+ iteration = 0
245
+ while not self.context.is_shutdown_requested():
246
+ yield iteration
247
+ iteration += 1
248
+
249
+ # Check if migration is imminent
250
+ if check_migration and self.context.is_migration_imminent():
251
+ self.context.logger().info(
252
+ f"Migration imminent, completing iteration {iteration}"
253
+ )
254
+
255
+ await asyncio.sleep(interval)
256
+
257
+ def is_shutting_down(self) -> bool:
258
+ """
259
+ Convenience method to check if shutdown is requested.
260
+
261
+ Returns:
262
+ True if shutdown has been requested
263
+ """
264
+ return self.context.is_shutdown_requested()
265
+
266
+ # =========================================================================
267
+ # Optional Fault Handling Hooks
268
+ # =========================================================================
269
+
270
+ async def on_state_restore_failed(self, error: Exception) -> bool:
271
+ """
272
+ Called if state restore fails.
273
+
274
+ Override to attempt recovery (e.g., fetch from external backup).
275
+ Return True to start with golden image, False to exit and crash.
276
+
277
+ Args:
278
+ error: Exception from restore_state() or validation
279
+
280
+ Returns:
281
+ True to continue with golden image, False to exit
282
+ """
283
+ return True # Default: continue with golden image
284
+
285
+ async def on_rapid_restart_detected(self, restart_count: int) -> bool:
286
+ """
287
+ Called if restart loop detected (3+ restarts in 5 minutes).
288
+
289
+ Override to attempt recovery (e.g., reinitialize state, reset
290
+ connections). Return True to continue, False to trigger golden reset.
291
+
292
+ Args:
293
+ restart_count: Number of restarts detected
294
+
295
+ Returns:
296
+ True to continue, False to force golden reset
297
+ """
298
+ return True # Default: continue (SDK will start golden)
299
+
300
+ async def on_health_check_failed(self, error: Exception) -> bool:
301
+ """
302
+ Called if health check fails.
303
+
304
+ Override to attempt recovery (e.g., reconnect to external services).
305
+ Return True to retry health check, False to fail.
306
+
307
+ Args:
308
+ error: Exception from health check
309
+
310
+ Returns:
311
+ True to retry, False to fail
312
+ """
313
+ return False # Default: fail health check
314
+
315
+ def reset_caches(self) -> None:
316
+ """
317
+ Called during golden image reset.
318
+
319
+ Override to clear any in-memory caches, buffers, or temporary
320
+ state that should not persist through a golden reset.
321
+ """
322
+ pass # Default: no caches to reset
323
+
324
+ # =========================================================================
325
+ # Auto-Initialization (SDK v2.1)
326
+ # =========================================================================
327
+
328
+ def _auto_initialize_components(self) -> None:
329
+ """
330
+ Auto-initialize SDK components from configuration.
331
+
332
+ This method is called automatically during __init__ if context is available.
333
+ Components are only initialized if enabled in configuration.
334
+
335
+ Initialized components:
336
+ - error_classifier: Always available
337
+ - circuit_breakers: Dict of circuit breakers
338
+ - otel: OpenTelemetry (if enabled)
339
+ - request_tracker: Request tracking (if enabled)
340
+ - request_id_middleware: Request ID generation (if enabled)
341
+ - connection_tracker: Connection tracking (if enabled)
342
+ """
343
+ if not hasattr(self, "context") or self.context is None:
344
+ logger.debug("Context not available, skipping auto-initialization")
345
+ return
346
+
347
+ config = self.context.config
348
+
349
+ # 1. Error Classifier (always available)
350
+ self._init_error_classifier()
351
+
352
+ # 2. Circuit Breakers (auto-created from config)
353
+ self._init_circuit_breakers(config)
354
+
355
+ # 3. OpenTelemetry (auto-initialized if enabled)
356
+ self._init_opentelemetry(config)
357
+
358
+ # 4. Request Tracking (auto-initialized if enabled)
359
+ self._init_request_tracking(config)
360
+
361
+ # 5. Request ID Middleware (auto-initialized if enabled)
362
+ self._init_request_id(config)
363
+
364
+ # 6. Connection Tracker (auto-initialized if enabled)
365
+ self._init_connection_tracking(config)
366
+
367
+ logger.debug("Auto-initialization complete")
368
+
369
+ def _init_error_classifier(self) -> None:
370
+ """Initialize error classifier (always available)."""
371
+ try:
372
+ from dory.errors import ErrorClassifier
373
+
374
+ self.error_classifier = ErrorClassifier()
375
+ logger.debug("Initialized error classifier")
376
+ except ImportError:
377
+ logger.debug("Error classifier not available (dory.errors not installed)")
378
+ self.error_classifier = None
379
+
380
+ def _init_circuit_breakers(self, config: Any) -> None:
381
+ """Auto-initialize circuit breakers from configuration."""
382
+ self.circuit_breakers: Dict[str, Any] = {}
383
+
384
+ try:
385
+ from dory.resilience import CircuitBreaker
386
+ except ImportError:
387
+ logger.debug("Circuit breakers not available (dory.resilience not installed)")
388
+ return
389
+
390
+ # Get circuit breaker config
391
+ cb_config_dict = {}
392
+ if hasattr(config, "__dict__"):
393
+ config_dict = config.__dict__
394
+ elif hasattr(config, "model_dump"):
395
+ config_dict = config.model_dump()
396
+ else:
397
+ config_dict = {}
398
+
399
+ # Try to get from nested config structure
400
+ if "circuit_breaker" in config_dict:
401
+ cb_config_dict = config_dict["circuit_breaker"]
402
+ elif hasattr(config, "get"):
403
+ cb_config_dict = config.get("circuit_breaker", {})
404
+
405
+ # Check if circuit breakers are enabled
406
+ if isinstance(cb_config_dict, dict) and not cb_config_dict.get("enabled", True):
407
+ logger.info("Circuit breakers disabled in configuration")
408
+ return
409
+
410
+ # Get default parameters
411
+ if isinstance(cb_config_dict, dict):
412
+ failure_threshold = cb_config_dict.get("failure_threshold", 5)
413
+ success_threshold = cb_config_dict.get("success_threshold", 2)
414
+ timeout_seconds = cb_config_dict.get("timeout", 30.0)
415
+ half_open_max_calls = cb_config_dict.get("half_open_max_calls", 3)
416
+ else:
417
+ failure_threshold = 5
418
+ success_threshold = 2
419
+ timeout_seconds = 30.0
420
+ half_open_max_calls = 3
421
+
422
+ # Create default circuit breakers for common services
423
+ common_names = ["database", "external_api", "cache"]
424
+ for name in common_names:
425
+ self.circuit_breakers[name] = CircuitBreaker(
426
+ name=name,
427
+ failure_threshold=failure_threshold,
428
+ success_threshold=success_threshold,
429
+ timeout_seconds=timeout_seconds,
430
+ half_open_max_calls=half_open_max_calls,
431
+ )
432
+ logger.debug(f"Created circuit breaker: {name}")
433
+
434
+ # Create custom circuit breakers from config
435
+ if isinstance(cb_config_dict, dict) and "breakers" in cb_config_dict:
436
+ custom_breakers = cb_config_dict["breakers"]
437
+ for name, breaker_config in custom_breakers.items():
438
+ self.circuit_breakers[name] = CircuitBreaker(
439
+ name=name,
440
+ failure_threshold=breaker_config.get("failure_threshold", failure_threshold),
441
+ success_threshold=breaker_config.get("success_threshold", success_threshold),
442
+ timeout_seconds=breaker_config.get("timeout", timeout_seconds),
443
+ half_open_max_calls=breaker_config.get("half_open_max_calls", half_open_max_calls),
444
+ )
445
+ logger.debug(f"Created custom circuit breaker: {name}")
446
+
447
+ logger.info(f"Initialized {len(self.circuit_breakers)} circuit breakers")
448
+
449
+ def _init_opentelemetry(self, config: Any) -> None:
450
+ """Auto-initialize OpenTelemetry if enabled."""
451
+ self.otel: Optional[Any] = None
452
+
453
+ # Get config dict
454
+ if hasattr(config, "__dict__"):
455
+ config_dict = config.__dict__
456
+ elif hasattr(config, "model_dump"):
457
+ config_dict = config.model_dump()
458
+ else:
459
+ config_dict = {}
460
+
461
+ # Get OpenTelemetry config
462
+ otel_config = {}
463
+ if "opentelemetry" in config_dict:
464
+ otel_config = config_dict["opentelemetry"]
465
+ elif hasattr(config, "get"):
466
+ otel_config = config.get("opentelemetry", {})
467
+
468
+ # Check if OpenTelemetry is enabled
469
+ if isinstance(otel_config, dict) and not otel_config.get("enabled", True):
470
+ logger.info("OpenTelemetry disabled in configuration")
471
+ return
472
+
473
+ try:
474
+ from dory.monitoring import OpenTelemetryManager
475
+
476
+ # Get app config for service name/version
477
+ app_config = {}
478
+ if "app" in config_dict:
479
+ app_config = config_dict["app"]
480
+ elif hasattr(config, "get"):
481
+ app_config = config.get("app", {})
482
+
483
+ # Initialize OpenTelemetry
484
+ if isinstance(otel_config, dict):
485
+ self.otel = OpenTelemetryManager(
486
+ service_name=otel_config.get("service_name", app_config.get("name", "dory-app")),
487
+ service_version=otel_config.get(
488
+ "service_version", app_config.get("version", "1.0.0")
489
+ ),
490
+ environment=otel_config.get("environment", app_config.get("environment", "production")),
491
+ console_export=otel_config.get("otlp", {}).get("console_export", True),
492
+ otlp_endpoint=otel_config.get("otlp", {}).get("endpoint"),
493
+ )
494
+ else:
495
+ self.otel = OpenTelemetryManager(
496
+ service_name=app_config.get("name", "dory-app"),
497
+ service_version=app_config.get("version", "1.0.0"),
498
+ environment=app_config.get("environment", "production"),
499
+ )
500
+
501
+ self.otel.initialize()
502
+ logger.info("OpenTelemetry initialized")
503
+
504
+ except ImportError:
505
+ logger.debug(
506
+ "OpenTelemetry not available. Install with: pip install dory-sdk[tracing]"
507
+ )
508
+ self.otel = None
509
+ except Exception as e:
510
+ logger.warning(f"Failed to initialize OpenTelemetry: {e}")
511
+ self.otel = None
512
+
513
+ def _init_request_tracking(self, config: Any) -> None:
514
+ """Auto-initialize request tracking if enabled."""
515
+ self.request_tracker: Optional[Any] = None
516
+
517
+ # Get config dict
518
+ if hasattr(config, "__dict__"):
519
+ config_dict = config.__dict__
520
+ elif hasattr(config, "model_dump"):
521
+ config_dict = config.model_dump()
522
+ else:
523
+ config_dict = {}
524
+
525
+ # Get bookkeeping config
526
+ bookkeeping_config = {}
527
+ if "bookkeeping" in config_dict:
528
+ bookkeeping_config = config_dict["bookkeeping"]
529
+ elif hasattr(config, "get"):
530
+ bookkeeping_config = config.get("bookkeeping", {})
531
+
532
+ # Get request tracking config
533
+ if isinstance(bookkeeping_config, dict):
534
+ tracking_config = bookkeeping_config.get("request_tracking", {})
535
+ else:
536
+ tracking_config = {}
537
+
538
+ # Check if request tracking is enabled
539
+ if isinstance(tracking_config, dict) and not tracking_config.get("enabled", True):
540
+ logger.info("Request tracking disabled in configuration")
541
+ return
542
+
543
+ try:
544
+ from dory.middleware import RequestTracker
545
+
546
+ if isinstance(tracking_config, dict):
547
+ self.request_tracker = RequestTracker(
548
+ max_concurrent=tracking_config.get("max_concurrent", 100),
549
+ timeout=tracking_config.get("timeout", 30.0),
550
+ )
551
+ else:
552
+ self.request_tracker = RequestTracker()
553
+
554
+ logger.info("Request tracking initialized")
555
+
556
+ except ImportError:
557
+ logger.debug("Request tracking not available (dory.middleware not installed)")
558
+ self.request_tracker = None
559
+ except Exception as e:
560
+ logger.warning(f"Failed to initialize request tracking: {e}")
561
+ self.request_tracker = None
562
+
563
+ def _init_request_id(self, config: Any) -> None:
564
+ """Auto-initialize request ID middleware if enabled."""
565
+ self.request_id_middleware: Optional[Any] = None
566
+
567
+ # Get config dict
568
+ if hasattr(config, "__dict__"):
569
+ config_dict = config.__dict__
570
+ elif hasattr(config, "model_dump"):
571
+ config_dict = config.model_dump()
572
+ else:
573
+ config_dict = {}
574
+
575
+ # Get bookkeeping config
576
+ bookkeeping_config = {}
577
+ if "bookkeeping" in config_dict:
578
+ bookkeeping_config = config_dict["bookkeeping"]
579
+ elif hasattr(config, "get"):
580
+ bookkeeping_config = config.get("bookkeeping", {})
581
+
582
+ # Get request ID config
583
+ if isinstance(bookkeeping_config, dict):
584
+ request_id_config = bookkeeping_config.get("request_id", {})
585
+ else:
586
+ request_id_config = {}
587
+
588
+ # Check if request ID is enabled
589
+ if isinstance(request_id_config, dict) and not request_id_config.get("enabled", True):
590
+ logger.info("Request ID generation disabled in configuration")
591
+ return
592
+
593
+ try:
594
+ from dory.middleware import RequestIdMiddleware
595
+
596
+ if isinstance(request_id_config, dict):
597
+ self.request_id_middleware = RequestIdMiddleware(
598
+ format=request_id_config.get("format", "uuid4"),
599
+ add_to_response=request_id_config.get("add_to_response", True),
600
+ )
601
+ else:
602
+ self.request_id_middleware = RequestIdMiddleware()
603
+
604
+ logger.info("Request ID middleware initialized")
605
+
606
+ except ImportError:
607
+ logger.debug("Request ID middleware not available (dory.middleware not installed)")
608
+ self.request_id_middleware = None
609
+ except Exception as e:
610
+ logger.warning(f"Failed to initialize request ID middleware: {e}")
611
+ self.request_id_middleware = None
612
+
613
+ def _init_connection_tracking(self, config: Any) -> None:
614
+ """Auto-initialize connection tracking if enabled."""
615
+ self.connection_tracker: Optional[Any] = None
616
+
617
+ # Get config dict
618
+ if hasattr(config, "__dict__"):
619
+ config_dict = config.__dict__
620
+ elif hasattr(config, "model_dump"):
621
+ config_dict = config.model_dump()
622
+ else:
623
+ config_dict = {}
624
+
625
+ # Get bookkeeping config
626
+ bookkeeping_config = {}
627
+ if "bookkeeping" in config_dict:
628
+ bookkeeping_config = config_dict["bookkeeping"]
629
+ elif hasattr(config, "get"):
630
+ bookkeeping_config = config.get("bookkeeping", {})
631
+
632
+ # Get connection tracking config
633
+ if isinstance(bookkeeping_config, dict):
634
+ connection_config = bookkeeping_config.get("connection_tracking", {})
635
+ else:
636
+ connection_config = {}
637
+
638
+ # Check if connection tracking is enabled
639
+ if isinstance(connection_config, dict) and not connection_config.get("enabled", True):
640
+ logger.info("Connection tracking disabled in configuration")
641
+ return
642
+
643
+ try:
644
+ from dory.middleware import ConnectionTracker
645
+
646
+ self.connection_tracker = ConnectionTracker()
647
+ logger.info("Connection tracking initialized")
648
+
649
+ except ImportError:
650
+ logger.debug("Connection tracking not available (dory.middleware not installed)")
651
+ self.connection_tracker = None
652
+ except Exception as e:
653
+ logger.warning(f"Failed to initialize connection tracking: {e}")
654
+ self.connection_tracker = None