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
@@ -0,0 +1,83 @@
1
+ """Notification throttling system.
2
+
3
+ This module provides rate limiting for notifications to prevent
4
+ overwhelming recipients during incidents or high-frequency events.
5
+
6
+ Features:
7
+ - 5 throttler types (TokenBucket, FixedWindow, SlidingWindow, Composite, NoOp)
8
+ - Fluent builder API for configuration
9
+ - Per-channel and global throttling
10
+ - Burst allowance support
11
+
12
+ Example:
13
+ from truthound_dashboard.core.notifications.throttling import (
14
+ ThrottlerBuilder,
15
+ TokenBucketThrottler,
16
+ )
17
+
18
+ # Using builder
19
+ throttler = (
20
+ ThrottlerBuilder()
21
+ .with_per_minute_limit(10)
22
+ .with_per_hour_limit(100)
23
+ .with_burst_allowance(1.5)
24
+ .build()
25
+ )
26
+
27
+ # Check if allowed
28
+ result = throttler.allow("slack-channel")
29
+ if result.allowed:
30
+ send_notification()
31
+ else:
32
+ print(f"Retry after {result.retry_after} seconds")
33
+ """
34
+
35
+ from .builder import ThrottlerBuilder
36
+ from .stores import (
37
+ REDIS_AVAILABLE,
38
+ BaseThrottlingStore,
39
+ InMemoryThrottlingStore,
40
+ RedisThrottlingStore,
41
+ SQLiteThrottlingStore,
42
+ ThrottlingEntry,
43
+ ThrottlingMetrics,
44
+ ThrottlingStoreType,
45
+ create_throttling_store,
46
+ )
47
+ from .throttlers import (
48
+ BaseThrottler,
49
+ CompositeThrottler,
50
+ FixedWindowThrottler,
51
+ NoOpThrottler,
52
+ NotificationThrottler,
53
+ SlidingWindowThrottler,
54
+ ThrottleResult,
55
+ ThrottlerRegistry,
56
+ TokenBucketThrottler,
57
+ )
58
+
59
+ __all__ = [
60
+ # Throttlers
61
+ "BaseThrottler",
62
+ "TokenBucketThrottler",
63
+ "FixedWindowThrottler",
64
+ "SlidingWindowThrottler",
65
+ "CompositeThrottler",
66
+ "NoOpThrottler",
67
+ "ThrottlerRegistry",
68
+ "ThrottleResult",
69
+ # Service
70
+ "NotificationThrottler",
71
+ # Builder
72
+ "ThrottlerBuilder",
73
+ # Stores
74
+ "BaseThrottlingStore",
75
+ "InMemoryThrottlingStore",
76
+ "SQLiteThrottlingStore",
77
+ "RedisThrottlingStore",
78
+ "ThrottlingEntry",
79
+ "ThrottlingMetrics",
80
+ "ThrottlingStoreType",
81
+ "create_throttling_store",
82
+ "REDIS_AVAILABLE",
83
+ ]
@@ -0,0 +1,311 @@
1
+ """Fluent builder for throttler configuration.
2
+
3
+ This module provides a builder pattern for easily configuring
4
+ multi-level rate limiting with a fluent API.
5
+
6
+ Example:
7
+ throttler = (
8
+ ThrottlerBuilder()
9
+ .with_per_minute_limit(10)
10
+ .with_per_hour_limit(100)
11
+ .with_per_day_limit(500)
12
+ .with_burst_allowance(1.5)
13
+ .build()
14
+ )
15
+
16
+ result = throttler.allow("channel-1")
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING
22
+
23
+ from .stores import BaseThrottlingStore, InMemoryThrottlingStore
24
+ from .throttlers import (
25
+ BaseThrottler,
26
+ CompositeThrottler,
27
+ FixedWindowThrottler,
28
+ NotificationThrottler,
29
+ TokenBucketThrottler,
30
+ )
31
+
32
+ if TYPE_CHECKING:
33
+ from typing import Self
34
+
35
+
36
+ class ThrottlerBuilder:
37
+ """Fluent builder for creating throttlers.
38
+
39
+ Provides a convenient API for configuring multi-level
40
+ rate limiting with various algorithms.
41
+
42
+ Example:
43
+ # Simple per-minute limit
44
+ throttler = (
45
+ ThrottlerBuilder()
46
+ .with_per_minute_limit(10)
47
+ .build()
48
+ )
49
+
50
+ # Multi-level limits with burst
51
+ throttler = (
52
+ ThrottlerBuilder()
53
+ .with_per_minute_limit(10)
54
+ .with_per_hour_limit(100)
55
+ .with_burst_allowance(1.5)
56
+ .build()
57
+ )
58
+
59
+ # Token bucket for smooth limiting
60
+ throttler = (
61
+ ThrottlerBuilder()
62
+ .with_token_bucket(capacity=10, refill_rate=1)
63
+ .build()
64
+ )
65
+ """
66
+
67
+ def __init__(self) -> None:
68
+ """Initialize builder."""
69
+ self._per_second: int | None = None
70
+ self._per_minute: int | None = None
71
+ self._per_hour: int | None = None
72
+ self._per_day: int | None = None
73
+ self._burst_allowance: float = 1.0
74
+ self._token_bucket_capacity: float | None = None
75
+ self._token_bucket_rate: float | None = None
76
+ self._store: BaseThrottlingStore | None = None
77
+ self._throttlers: list[BaseThrottler] = []
78
+
79
+ def with_per_second_limit(self, limit: int) -> "Self":
80
+ """Set per-second limit.
81
+
82
+ Args:
83
+ limit: Maximum requests per second.
84
+
85
+ Returns:
86
+ Self for chaining.
87
+ """
88
+ self._per_second = limit
89
+ return self
90
+
91
+ def with_per_minute_limit(self, limit: int) -> "Self":
92
+ """Set per-minute limit.
93
+
94
+ Args:
95
+ limit: Maximum requests per minute.
96
+
97
+ Returns:
98
+ Self for chaining.
99
+ """
100
+ self._per_minute = limit
101
+ return self
102
+
103
+ def with_per_hour_limit(self, limit: int) -> "Self":
104
+ """Set per-hour limit.
105
+
106
+ Args:
107
+ limit: Maximum requests per hour.
108
+
109
+ Returns:
110
+ Self for chaining.
111
+ """
112
+ self._per_hour = limit
113
+ return self
114
+
115
+ def with_per_day_limit(self, limit: int) -> "Self":
116
+ """Set per-day limit.
117
+
118
+ Args:
119
+ limit: Maximum requests per day.
120
+
121
+ Returns:
122
+ Self for chaining.
123
+ """
124
+ self._per_day = limit
125
+ return self
126
+
127
+ def with_burst_allowance(self, factor: float) -> "Self":
128
+ """Set burst allowance factor.
129
+
130
+ Multiplies the limit to allow temporary bursts.
131
+ For example, 1.5 allows 150% of normal rate for short periods.
132
+
133
+ Args:
134
+ factor: Burst multiplier (1.0 = no burst allowed).
135
+
136
+ Returns:
137
+ Self for chaining.
138
+ """
139
+ self._burst_allowance = max(1.0, factor)
140
+ return self
141
+
142
+ def with_token_bucket(
143
+ self,
144
+ capacity: float,
145
+ refill_rate: float,
146
+ ) -> "Self":
147
+ """Configure token bucket algorithm.
148
+
149
+ Args:
150
+ capacity: Maximum tokens (burst capacity).
151
+ refill_rate: Tokens added per second.
152
+
153
+ Returns:
154
+ Self for chaining.
155
+ """
156
+ self._token_bucket_capacity = capacity
157
+ self._token_bucket_rate = refill_rate
158
+ return self
159
+
160
+ def with_store(self, store: BaseThrottlingStore) -> "Self":
161
+ """Set storage backend.
162
+
163
+ Args:
164
+ store: Storage backend to use.
165
+
166
+ Returns:
167
+ Self for chaining.
168
+ """
169
+ self._store = store
170
+ return self
171
+
172
+ def add_throttler(self, throttler: BaseThrottler) -> "Self":
173
+ """Add a custom throttler to the composite.
174
+
175
+ Args:
176
+ throttler: Custom throttler to add.
177
+
178
+ Returns:
179
+ Self for chaining.
180
+ """
181
+ self._throttlers.append(throttler)
182
+ return self
183
+
184
+ def build(self) -> BaseThrottler:
185
+ """Build the throttler.
186
+
187
+ Creates a CompositeThrottler if multiple limits are
188
+ configured, or a single throttler for simple configs.
189
+
190
+ Returns:
191
+ Configured throttler.
192
+ """
193
+ store = self._store or InMemoryThrottlingStore()
194
+ throttlers: list[BaseThrottler] = list(self._throttlers)
195
+
196
+ # Add token bucket if configured
197
+ if self._token_bucket_capacity and self._token_bucket_rate:
198
+ throttlers.append(
199
+ TokenBucketThrottler(
200
+ capacity=self._token_bucket_capacity * self._burst_allowance,
201
+ refill_rate=self._token_bucket_rate,
202
+ store=store,
203
+ )
204
+ )
205
+
206
+ # Add window-based throttlers
207
+ if self._per_second:
208
+ throttlers.append(
209
+ FixedWindowThrottler(
210
+ limit=int(self._per_second * self._burst_allowance),
211
+ window_seconds=1,
212
+ store=store,
213
+ )
214
+ )
215
+
216
+ if self._per_minute:
217
+ throttlers.append(
218
+ FixedWindowThrottler(
219
+ limit=int(self._per_minute * self._burst_allowance),
220
+ window_seconds=60,
221
+ store=store,
222
+ )
223
+ )
224
+
225
+ if self._per_hour:
226
+ throttlers.append(
227
+ FixedWindowThrottler(
228
+ limit=int(self._per_hour * self._burst_allowance),
229
+ window_seconds=3600,
230
+ store=store,
231
+ )
232
+ )
233
+
234
+ if self._per_day:
235
+ throttlers.append(
236
+ FixedWindowThrottler(
237
+ limit=int(self._per_day * self._burst_allowance),
238
+ window_seconds=86400,
239
+ store=store,
240
+ )
241
+ )
242
+
243
+ # Return appropriate throttler
244
+ if not throttlers:
245
+ # No limits configured - use simple token bucket default
246
+ return TokenBucketThrottler(
247
+ capacity=100,
248
+ refill_rate=10,
249
+ store=store,
250
+ )
251
+
252
+ if len(throttlers) == 1:
253
+ return throttlers[0]
254
+
255
+ return CompositeThrottler(throttlers=throttlers)
256
+
257
+ def build_notification_throttler(
258
+ self,
259
+ global_limits: bool = True,
260
+ ) -> NotificationThrottler:
261
+ """Build a NotificationThrottler service.
262
+
263
+ Args:
264
+ global_limits: Apply limits globally (vs per-channel).
265
+
266
+ Returns:
267
+ Configured NotificationThrottler.
268
+ """
269
+ throttler = self.build()
270
+
271
+ if global_limits:
272
+ return NotificationThrottler(
273
+ global_throttler=throttler,
274
+ )
275
+ else:
276
+ return NotificationThrottler(
277
+ default_throttler=throttler,
278
+ )
279
+
280
+
281
+ def configure_global_throttling(
282
+ per_minute: int | None = None,
283
+ per_hour: int | None = None,
284
+ per_day: int | None = None,
285
+ burst_allowance: float = 1.5,
286
+ ) -> NotificationThrottler:
287
+ """Configure global notification throttling.
288
+
289
+ Convenience function for common throttling setup.
290
+
291
+ Args:
292
+ per_minute: Max notifications per minute.
293
+ per_hour: Max notifications per hour.
294
+ per_day: Max notifications per day.
295
+ burst_allowance: Burst factor.
296
+
297
+ Returns:
298
+ Configured NotificationThrottler.
299
+ """
300
+ builder = ThrottlerBuilder()
301
+
302
+ if per_minute:
303
+ builder.with_per_minute_limit(per_minute)
304
+ if per_hour:
305
+ builder.with_per_hour_limit(per_hour)
306
+ if per_day:
307
+ builder.with_per_day_limit(per_day)
308
+
309
+ builder.with_burst_allowance(burst_allowance)
310
+
311
+ return builder.build_notification_throttler(global_limits=True)