truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.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 (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
  162. truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -38,10 +38,68 @@ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoin
38
38
  from starlette.responses import JSONResponse
39
39
 
40
40
  from truthound_dashboard.core.exceptions import ErrorCode
41
+ from truthound_dashboard.core.i18n import SupportedLocale, detect_locale
41
42
 
42
43
  logger = logging.getLogger(__name__)
43
44
 
44
45
 
46
+ # =============================================================================
47
+ # Locale Detection
48
+ # =============================================================================
49
+
50
+
51
+ class LocaleDetectionMiddleware(BaseHTTPMiddleware):
52
+ """Middleware to detect and set user locale from request.
53
+
54
+ Detection priority:
55
+ 1. Query parameter: ?lang=ko
56
+ 2. Accept-Language header
57
+ 3. Default locale (English)
58
+
59
+ The detected locale is stored in request.state.locale for use by
60
+ API endpoints that need to return localized error messages.
61
+
62
+ Example:
63
+ # In endpoint
64
+ locale = getattr(request.state, "locale", SupportedLocale.ENGLISH)
65
+ message = get_message("source_not_found", locale)
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ app: Any,
71
+ default_locale: SupportedLocale = SupportedLocale.ENGLISH,
72
+ ) -> None:
73
+ """Initialize locale detection middleware.
74
+
75
+ Args:
76
+ app: ASGI application.
77
+ default_locale: Default locale when detection fails.
78
+ """
79
+ super().__init__(app)
80
+ self._default_locale = default_locale
81
+
82
+ async def dispatch(
83
+ self,
84
+ request: Request,
85
+ call_next: RequestResponseEndpoint,
86
+ ) -> Response:
87
+ """Detect locale and store in request state."""
88
+ # Detect locale from request
89
+ locale = detect_locale(request, default=self._default_locale)
90
+
91
+ # Store in request state
92
+ request.state.locale = locale
93
+
94
+ # Process request
95
+ response = await call_next(request)
96
+
97
+ # Add Content-Language header
98
+ response.headers["Content-Language"] = locale.value
99
+
100
+ return response
101
+
102
+
45
103
  # =============================================================================
46
104
  # Rate Limiting
47
105
  # =============================================================================
@@ -586,6 +644,296 @@ class BasicAuthMiddleware(BaseHTTPMiddleware):
586
644
  )
587
645
 
588
646
 
647
+ # =============================================================================
648
+ # Notification Throttling Middleware
649
+ # =============================================================================
650
+
651
+
652
+ @dataclass
653
+ class NotificationThrottleConfig:
654
+ """Configuration for notification-specific throttling.
655
+
656
+ This middleware integrates with the notification throttling system
657
+ to provide endpoint-specific rate limiting for notification operations.
658
+
659
+ Attributes:
660
+ enabled: Whether throttling is enabled.
661
+ per_minute: Default requests per minute for notification endpoints.
662
+ per_hour: Default requests per hour for notification endpoints.
663
+ per_day: Default requests per day for notification endpoints.
664
+ burst_allowance: Burst allowance factor (e.g., 1.5 = 50% extra burst).
665
+ endpoint_overrides: Per-endpoint limit overrides.
666
+ """
667
+
668
+ enabled: bool = True
669
+ per_minute: int = 60
670
+ per_hour: int = 500
671
+ per_day: int = 5000
672
+ burst_allowance: float = 1.5
673
+ endpoint_overrides: dict[str, dict[str, int]] = field(default_factory=dict)
674
+
675
+
676
+ class NotificationThrottleMiddleware(BaseHTTPMiddleware):
677
+ """Middleware for notification-specific throttling.
678
+
679
+ Provides fine-grained rate limiting for notification operations with:
680
+ - Per-minute, per-hour, and per-day limits
681
+ - Endpoint-specific overrides
682
+ - Burst allowance
683
+ - Integration with notification throttling metrics
684
+
685
+ Response Headers:
686
+ X-RateLimit-Limit: Maximum requests in current window
687
+ X-RateLimit-Remaining: Remaining requests in current window
688
+ X-RateLimit-Reset: Unix timestamp when the limit resets
689
+ X-RateLimit-Window: Current limiting window (minute/hour/day)
690
+ Retry-After: Seconds until the limit resets (on 429 only)
691
+ """
692
+
693
+ NOTIFICATION_PATHS = [
694
+ "/api/v1/notifications",
695
+ ]
696
+
697
+ def __init__(
698
+ self,
699
+ app: Any,
700
+ config: NotificationThrottleConfig | None = None,
701
+ ) -> None:
702
+ """Initialize notification throttle middleware.
703
+
704
+ Args:
705
+ app: ASGI application.
706
+ config: Throttling configuration.
707
+ """
708
+ super().__init__(app)
709
+ self._config = config or NotificationThrottleConfig()
710
+
711
+ # Separate counters for minute/hour/day windows
712
+ self._minute_counts: dict[str, list[float]] = defaultdict(list)
713
+ self._hour_counts: dict[str, list[float]] = defaultdict(list)
714
+ self._day_counts: dict[str, list[float]] = defaultdict(list)
715
+
716
+ # Metrics
717
+ self._total_requests = 0
718
+ self._throttled_requests = 0
719
+
720
+ async def dispatch(
721
+ self,
722
+ request: Request,
723
+ call_next: RequestResponseEndpoint,
724
+ ) -> Response:
725
+ """Process request with notification-specific throttling."""
726
+ # Only apply to notification endpoints
727
+ if not self._config.enabled or not self._is_notification_endpoint(request.url.path):
728
+ return await call_next(request)
729
+
730
+ # Get client key
731
+ client_ip = request.client.host if request.client else "unknown"
732
+ key = f"notif:{client_ip}"
733
+
734
+ self._total_requests += 1
735
+
736
+ # Get limits for this endpoint
737
+ limits = self._get_limits(request.url.path)
738
+
739
+ # Check all windows
740
+ now = time.time()
741
+ allowed, info = self._check_limits(key, limits, now)
742
+
743
+ if not allowed:
744
+ self._throttled_requests += 1
745
+ logger.warning(
746
+ f"Notification throttle exceeded for {key}",
747
+ extra={
748
+ "path": request.url.path,
749
+ "window": info["window"],
750
+ "limit": info["limit"],
751
+ },
752
+ )
753
+
754
+ retry_after = info["reset_at"] - int(now)
755
+ return JSONResponse(
756
+ status_code=429,
757
+ content={
758
+ "success": False,
759
+ "error": {
760
+ "code": ErrorCode.RATE_LIMIT_EXCEEDED.value,
761
+ "message": f"Rate limit exceeded for {info['window']} window. "
762
+ f"Try again in {retry_after} seconds.",
763
+ "details": {
764
+ "window": info["window"],
765
+ "limit": info["limit"],
766
+ "remaining": 0,
767
+ "reset_at": info["reset_at"],
768
+ },
769
+ },
770
+ },
771
+ headers=self._build_headers(info, retry_after),
772
+ )
773
+
774
+ # Record request
775
+ self._record_request(key, now)
776
+
777
+ # Process request
778
+ response = await call_next(request)
779
+
780
+ # Add rate limit headers
781
+ response.headers.update(self._build_headers(info))
782
+
783
+ return response
784
+
785
+ def _is_notification_endpoint(self, path: str) -> bool:
786
+ """Check if path is a notification endpoint."""
787
+ return any(path.startswith(p) for p in self.NOTIFICATION_PATHS)
788
+
789
+ def _get_limits(self, path: str) -> dict[str, int]:
790
+ """Get rate limits for a specific path."""
791
+ # Check for endpoint-specific overrides
792
+ for pattern, limits in self._config.endpoint_overrides.items():
793
+ if path.startswith(pattern):
794
+ return {
795
+ "minute": limits.get("per_minute", self._config.per_minute),
796
+ "hour": limits.get("per_hour", self._config.per_hour),
797
+ "day": limits.get("per_day", self._config.per_day),
798
+ }
799
+
800
+ # Return default limits with burst allowance
801
+ burst = self._config.burst_allowance
802
+ return {
803
+ "minute": int(self._config.per_minute * burst),
804
+ "hour": int(self._config.per_hour * burst),
805
+ "day": int(self._config.per_day * burst),
806
+ }
807
+
808
+ def _check_limits(
809
+ self,
810
+ key: str,
811
+ limits: dict[str, int],
812
+ now: float,
813
+ ) -> tuple[bool, dict[str, Any]]:
814
+ """Check all rate limit windows.
815
+
816
+ Returns:
817
+ Tuple of (allowed, info dict).
818
+ """
819
+ # Clean old entries
820
+ self._cleanup_window(key, now)
821
+
822
+ # Check minute limit first (strictest)
823
+ minute_count = len(self._minute_counts[key])
824
+ if minute_count >= limits["minute"]:
825
+ return False, {
826
+ "window": "minute",
827
+ "limit": limits["minute"],
828
+ "remaining": 0,
829
+ "reset_at": int(now + 60 - (now % 60)),
830
+ }
831
+
832
+ # Check hour limit
833
+ hour_count = len(self._hour_counts[key])
834
+ if hour_count >= limits["hour"]:
835
+ return False, {
836
+ "window": "hour",
837
+ "limit": limits["hour"],
838
+ "remaining": 0,
839
+ "reset_at": int(now + 3600 - (now % 3600)),
840
+ }
841
+
842
+ # Check day limit
843
+ day_count = len(self._day_counts[key])
844
+ if day_count >= limits["day"]:
845
+ return False, {
846
+ "window": "day",
847
+ "limit": limits["day"],
848
+ "remaining": 0,
849
+ "reset_at": int(now + 86400 - (now % 86400)),
850
+ }
851
+
852
+ # All windows have capacity - return most restrictive remaining
853
+ minute_remaining = limits["minute"] - minute_count
854
+ hour_remaining = limits["hour"] - hour_count
855
+ day_remaining = limits["day"] - day_count
856
+
857
+ # Find the most restrictive window
858
+ if minute_remaining <= hour_remaining and minute_remaining <= day_remaining:
859
+ return True, {
860
+ "window": "minute",
861
+ "limit": limits["minute"],
862
+ "remaining": minute_remaining - 1,
863
+ "reset_at": int(now + 60 - (now % 60)),
864
+ }
865
+ elif hour_remaining <= day_remaining:
866
+ return True, {
867
+ "window": "hour",
868
+ "limit": limits["hour"],
869
+ "remaining": hour_remaining - 1,
870
+ "reset_at": int(now + 3600 - (now % 3600)),
871
+ }
872
+ else:
873
+ return True, {
874
+ "window": "day",
875
+ "limit": limits["day"],
876
+ "remaining": day_remaining - 1,
877
+ "reset_at": int(now + 86400 - (now % 86400)),
878
+ }
879
+
880
+ def _record_request(self, key: str, now: float) -> None:
881
+ """Record a request in all windows."""
882
+ self._minute_counts[key].append(now)
883
+ self._hour_counts[key].append(now)
884
+ self._day_counts[key].append(now)
885
+
886
+ def _cleanup_window(self, key: str, now: float) -> None:
887
+ """Remove expired entries from all windows."""
888
+ minute_start = now - 60
889
+ hour_start = now - 3600
890
+ day_start = now - 86400
891
+
892
+ self._minute_counts[key] = [
893
+ t for t in self._minute_counts[key] if t > minute_start
894
+ ]
895
+ self._hour_counts[key] = [
896
+ t for t in self._hour_counts[key] if t > hour_start
897
+ ]
898
+ self._day_counts[key] = [
899
+ t for t in self._day_counts[key] if t > day_start
900
+ ]
901
+
902
+ def _build_headers(
903
+ self,
904
+ info: dict[str, Any],
905
+ retry_after: int | None = None,
906
+ ) -> dict[str, str]:
907
+ """Build rate limit response headers."""
908
+ headers = {
909
+ "X-RateLimit-Limit": str(info["limit"]),
910
+ "X-RateLimit-Remaining": str(max(0, info["remaining"])),
911
+ "X-RateLimit-Reset": str(info["reset_at"]),
912
+ "X-RateLimit-Window": info["window"],
913
+ }
914
+
915
+ if retry_after is not None:
916
+ headers["Retry-After"] = str(max(0, retry_after))
917
+
918
+ return headers
919
+
920
+ def get_metrics(self) -> dict[str, Any]:
921
+ """Get throttling metrics.
922
+
923
+ Returns:
924
+ Dictionary with throttling statistics.
925
+ """
926
+ return {
927
+ "total_requests": self._total_requests,
928
+ "throttled_requests": self._throttled_requests,
929
+ "throttle_rate": (
930
+ self._throttled_requests / self._total_requests * 100
931
+ if self._total_requests > 0 else 0
932
+ ),
933
+ "active_keys": len(self._minute_counts),
934
+ }
935
+
936
+
589
937
  # =============================================================================
590
938
  # Middleware Setup Helper
591
939
  # =============================================================================
@@ -606,13 +954,34 @@ def setup_middleware(app: Any) -> None:
606
954
  # Request logging (always enabled)
607
955
  app.add_middleware(RequestLoggingMiddleware)
608
956
 
609
- # Rate limiting
957
+ # Rate limiting (general)
610
958
  rate_limit_config = RateLimitConfig(
611
959
  requests_per_minute=120,
612
960
  exclude_paths=["/health", "/docs", "/redoc", "/openapi.json", "/api/openapi.json"],
613
961
  )
614
962
  app.add_middleware(RateLimitMiddleware, config=rate_limit_config)
615
963
 
964
+ # Notification-specific throttling (more granular)
965
+ notif_throttle_config = NotificationThrottleConfig(
966
+ per_minute=60,
967
+ per_hour=500,
968
+ per_day=5000,
969
+ burst_allowance=1.5,
970
+ endpoint_overrides={
971
+ # Higher limits for read operations
972
+ "/api/v1/notifications/routing/rules": {
973
+ "per_minute": 120,
974
+ "per_hour": 1000,
975
+ },
976
+ # Lower limits for write operations
977
+ "/api/v1/notifications/escalation/incidents": {
978
+ "per_minute": 30,
979
+ "per_hour": 200,
980
+ },
981
+ },
982
+ )
983
+ app.add_middleware(NotificationThrottleMiddleware, config=notif_throttle_config)
984
+
616
985
  # Security headers
617
986
  app.add_middleware(SecurityHeadersMiddleware)
618
987
 
@@ -623,4 +992,7 @@ def setup_middleware(app: Any) -> None:
623
992
  password=settings.auth_password,
624
993
  )
625
994
 
995
+ # Locale detection (runs first, so it's available to all other middleware)
996
+ app.add_middleware(LocaleDetectionMiddleware)
997
+
626
998
  logger.info("Middleware configured successfully")