dataquery-sdk 0.0.7__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.
- dataquery/__init__.py +215 -0
- dataquery/auth.py +380 -0
- dataquery/auto_download.py +448 -0
- dataquery/cli.py +244 -0
- dataquery/client.py +2825 -0
- dataquery/config.py +571 -0
- dataquery/connection_pool.py +538 -0
- dataquery/dataquery.py +2467 -0
- dataquery/exceptions.py +169 -0
- dataquery/logging_config.py +415 -0
- dataquery/models.py +1303 -0
- dataquery/py.typed +2 -0
- dataquery/rate_limiter.py +520 -0
- dataquery/retry.py +406 -0
- dataquery/utils.py +525 -0
- dataquery_sdk-0.0.7.dist-info/METADATA +996 -0
- dataquery_sdk-0.0.7.dist-info/RECORD +21 -0
- dataquery_sdk-0.0.7.dist-info/WHEEL +5 -0
- dataquery_sdk-0.0.7.dist-info/entry_points.txt +2 -0
- dataquery_sdk-0.0.7.dist-info/licenses/LICENSE +201 -0
- dataquery_sdk-0.0.7.dist-info/top_level.txt +1 -0
dataquery/py.typed
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced rate limiting implementation for the DATAQUERY SDK.
|
|
3
|
+
|
|
4
|
+
Provides token bucket rate limiting with configurable burst capacity,
|
|
5
|
+
requests per minute limits, and a queuing mechanism to prevent breaches.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import time
|
|
10
|
+
from collections import deque
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
logger = structlog.get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class QueuePriority(Enum):
|
|
21
|
+
"""Priority levels for queued requests."""
|
|
22
|
+
|
|
23
|
+
LOW = 1
|
|
24
|
+
NORMAL = 2
|
|
25
|
+
HIGH = 3
|
|
26
|
+
CRITICAL = 4
|
|
27
|
+
|
|
28
|
+
def __lt__(self, other):
|
|
29
|
+
"""Enable comparison of priorities."""
|
|
30
|
+
if isinstance(other, QueuePriority):
|
|
31
|
+
return self.value < other.value
|
|
32
|
+
return NotImplemented
|
|
33
|
+
|
|
34
|
+
def __le__(self, other):
|
|
35
|
+
"""Enable comparison of priorities."""
|
|
36
|
+
if isinstance(other, QueuePriority):
|
|
37
|
+
return self.value <= other.value
|
|
38
|
+
return NotImplemented
|
|
39
|
+
|
|
40
|
+
def __gt__(self, other):
|
|
41
|
+
"""Enable comparison of priorities."""
|
|
42
|
+
if isinstance(other, QueuePriority):
|
|
43
|
+
return self.value > other.value
|
|
44
|
+
return NotImplemented
|
|
45
|
+
|
|
46
|
+
def __ge__(self, other):
|
|
47
|
+
"""Enable comparison of priorities."""
|
|
48
|
+
if isinstance(other, QueuePriority):
|
|
49
|
+
return self.value >= other.value
|
|
50
|
+
return NotImplemented
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class QueuedRequest:
|
|
55
|
+
"""Represents a queued request."""
|
|
56
|
+
|
|
57
|
+
priority: QueuePriority
|
|
58
|
+
timestamp: float
|
|
59
|
+
request_id: str
|
|
60
|
+
operation: str
|
|
61
|
+
future: asyncio.Future
|
|
62
|
+
timeout: Optional[float] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class RateLimitConfig:
|
|
67
|
+
"""Configuration for rate limiting."""
|
|
68
|
+
|
|
69
|
+
requests_per_minute: int = 300 # Exactly 5 requests per second (200ms per request)
|
|
70
|
+
burst_capacity: int = 1 # No burst allowed per specification
|
|
71
|
+
window_size_seconds: int = 60
|
|
72
|
+
retry_after_header: str = "Retry-After"
|
|
73
|
+
enable_rate_limiting: bool = True
|
|
74
|
+
|
|
75
|
+
# Queue configuration
|
|
76
|
+
max_queue_size: int = 1000
|
|
77
|
+
enable_queuing: bool = True
|
|
78
|
+
queue_timeout: float = 600.0 # 10 minutes
|
|
79
|
+
priority_enabled: bool = True
|
|
80
|
+
|
|
81
|
+
# Adaptive rate limiting
|
|
82
|
+
adaptive_rate_limiting: bool = True
|
|
83
|
+
backoff_multiplier: float = 1.5
|
|
84
|
+
max_backoff_seconds: float = 60.0
|
|
85
|
+
|
|
86
|
+
def __post_init__(self):
|
|
87
|
+
"""Validate configuration."""
|
|
88
|
+
if self.requests_per_minute <= 0:
|
|
89
|
+
raise ValueError("requests_per_minute must be positive")
|
|
90
|
+
if self.burst_capacity <= 0:
|
|
91
|
+
raise ValueError("burst_capacity must be positive")
|
|
92
|
+
if self.window_size_seconds <= 0:
|
|
93
|
+
raise ValueError("window_size_seconds must be positive")
|
|
94
|
+
if self.burst_capacity > self.requests_per_minute:
|
|
95
|
+
raise ValueError("burst_capacity cannot exceed requests_per_minute")
|
|
96
|
+
if self.max_queue_size <= 0:
|
|
97
|
+
raise ValueError("max_queue_size must be positive")
|
|
98
|
+
if self.queue_timeout <= 0:
|
|
99
|
+
raise ValueError("queue_timeout must be positive")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class RateLimitState:
|
|
104
|
+
"""Internal state for rate limiting."""
|
|
105
|
+
|
|
106
|
+
tokens: float = field(default=0.0)
|
|
107
|
+
last_refill: float = field(default_factory=time.time)
|
|
108
|
+
request_count: int = 0
|
|
109
|
+
window_start: float = field(default_factory=time.time)
|
|
110
|
+
retry_after: Optional[float] = None
|
|
111
|
+
|
|
112
|
+
# Queue state
|
|
113
|
+
queue: deque = field(default_factory=deque)
|
|
114
|
+
processing: bool = False
|
|
115
|
+
last_request_time: float = field(default_factory=time.time)
|
|
116
|
+
|
|
117
|
+
# Adaptive rate limiting
|
|
118
|
+
consecutive_failures: int = 0
|
|
119
|
+
current_backoff: float = 0.0
|
|
120
|
+
|
|
121
|
+
# Request statistics
|
|
122
|
+
total_requests: int = 0
|
|
123
|
+
successful_requests: int = 0
|
|
124
|
+
failed_requests: int = 0
|
|
125
|
+
rate_limited_requests: int = 0
|
|
126
|
+
last_failure_time: float = 0.0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class EnhancedTokenBucketRateLimiter:
|
|
130
|
+
"""Enhanced token bucket rate limiter with queuing mechanism."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, config: RateLimitConfig):
|
|
133
|
+
self.config = config
|
|
134
|
+
self.state = RateLimitState()
|
|
135
|
+
self._lock: Optional[asyncio.Lock] = None
|
|
136
|
+
self._queue_processor_task: Optional[asyncio.Task] = None
|
|
137
|
+
self._shutdown_event: Optional[asyncio.Event] = None
|
|
138
|
+
self._queue_processor_started = False
|
|
139
|
+
|
|
140
|
+
# Initialize tokens to burst capacity
|
|
141
|
+
self.state.tokens = float(config.burst_capacity)
|
|
142
|
+
self.state.last_refill = time.time()
|
|
143
|
+
|
|
144
|
+
logger.info(
|
|
145
|
+
"Enhanced rate limiter initialized",
|
|
146
|
+
requests_per_minute=config.requests_per_minute,
|
|
147
|
+
burst_capacity=config.burst_capacity,
|
|
148
|
+
queuing_enabled=config.enable_queuing,
|
|
149
|
+
max_queue_size=config.max_queue_size,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _get_lock(self) -> asyncio.Lock:
|
|
153
|
+
"""Get the asyncio lock, creating it if necessary."""
|
|
154
|
+
if self._lock is None:
|
|
155
|
+
self._lock = asyncio.Lock()
|
|
156
|
+
return self._lock
|
|
157
|
+
|
|
158
|
+
def _get_shutdown_event(self) -> asyncio.Event:
|
|
159
|
+
"""Get the shutdown event, creating it if necessary."""
|
|
160
|
+
if self._shutdown_event is None:
|
|
161
|
+
self._shutdown_event = asyncio.Event()
|
|
162
|
+
return self._shutdown_event
|
|
163
|
+
|
|
164
|
+
def _start_queue_processor(self):
|
|
165
|
+
"""Start the background queue processor."""
|
|
166
|
+
if self._queue_processor_task is None or self._queue_processor_task.done():
|
|
167
|
+
try:
|
|
168
|
+
# Check if there's a running event loop
|
|
169
|
+
loop = asyncio.get_running_loop()
|
|
170
|
+
self._queue_processor_task = loop.create_task(self._process_queue())
|
|
171
|
+
self._queue_processor_started = True
|
|
172
|
+
except RuntimeError:
|
|
173
|
+
# No running event loop, will start when acquire is called
|
|
174
|
+
self._queue_processor_started = False
|
|
175
|
+
|
|
176
|
+
async def _process_queue(self):
|
|
177
|
+
"""Background task to process queued requests."""
|
|
178
|
+
while not self._get_shutdown_event().is_set():
|
|
179
|
+
try:
|
|
180
|
+
async with self._get_lock():
|
|
181
|
+
if not self.state.queue:
|
|
182
|
+
await asyncio.sleep(0.1)
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Get next request from queue (priority-based)
|
|
186
|
+
request = self._get_next_request()
|
|
187
|
+
if request is None:
|
|
188
|
+
await asyncio.sleep(0.1)
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# Check if request has timed out
|
|
192
|
+
if (
|
|
193
|
+
request.timeout
|
|
194
|
+
and time.time() - request.timestamp > request.timeout
|
|
195
|
+
):
|
|
196
|
+
if not request.future.done():
|
|
197
|
+
request.future.set_exception(
|
|
198
|
+
asyncio.TimeoutError("Request timed out in queue")
|
|
199
|
+
)
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
# Try to acquire token
|
|
203
|
+
if await self._try_acquire_token():
|
|
204
|
+
# Remove from queue and resolve future
|
|
205
|
+
self.state.queue.remove(request)
|
|
206
|
+
if not request.future.done():
|
|
207
|
+
request.future.set_result(True)
|
|
208
|
+
else:
|
|
209
|
+
# Put back at front of queue and wait
|
|
210
|
+
await asyncio.sleep(self._calculate_wait_time())
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.error("Error in queue processor", error=str(e))
|
|
214
|
+
await asyncio.sleep(1.0)
|
|
215
|
+
|
|
216
|
+
def _get_next_request(self) -> Optional[QueuedRequest]:
|
|
217
|
+
"""Get the next request from queue based on priority."""
|
|
218
|
+
if not self.state.queue:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
if not self.config.priority_enabled:
|
|
222
|
+
return self.state.queue[0]
|
|
223
|
+
|
|
224
|
+
# Find highest priority request
|
|
225
|
+
highest_priority = max(req.priority.value for req in self.state.queue)
|
|
226
|
+
for request in self.state.queue:
|
|
227
|
+
if request.priority.value == highest_priority:
|
|
228
|
+
return request
|
|
229
|
+
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
async def _try_acquire_token(self) -> bool:
|
|
233
|
+
"""Try to acquire a token without waiting."""
|
|
234
|
+
self._refill_tokens()
|
|
235
|
+
|
|
236
|
+
if self.state.tokens >= 1.0:
|
|
237
|
+
self.state.tokens -= 1.0
|
|
238
|
+
self.state.request_count += 1
|
|
239
|
+
self.state.last_request_time = time.time()
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
async def acquire(
|
|
245
|
+
self,
|
|
246
|
+
timeout: Optional[float] = None,
|
|
247
|
+
priority: QueuePriority = QueuePriority.NORMAL,
|
|
248
|
+
operation: str = "unknown",
|
|
249
|
+
) -> bool:
|
|
250
|
+
"""
|
|
251
|
+
Acquire a token for making a request with optimized performance.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
timeout: Maximum time to wait for a token (None = no timeout)
|
|
255
|
+
priority: Priority level for queuing (ignored for performance)
|
|
256
|
+
operation: Operation name for logging
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if token acquired, False if timeout
|
|
260
|
+
"""
|
|
261
|
+
if not self.config.enable_rate_limiting:
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
# Use optimized simple acquisition for better performance
|
|
265
|
+
return await self._optimized_acquire(timeout, operation)
|
|
266
|
+
|
|
267
|
+
async def _optimized_acquire(
|
|
268
|
+
self, timeout: Optional[float] = None, operation: str = "unknown"
|
|
269
|
+
) -> bool:
|
|
270
|
+
"""Optimized token acquisition with minimal overhead."""
|
|
271
|
+
start_time = time.time()
|
|
272
|
+
|
|
273
|
+
async with self._get_lock():
|
|
274
|
+
while True:
|
|
275
|
+
self._refill_tokens()
|
|
276
|
+
|
|
277
|
+
if self.state.tokens >= 1.0:
|
|
278
|
+
self.state.tokens -= 1.0
|
|
279
|
+
self.state.request_count += 1
|
|
280
|
+
self.state.last_request_time = time.time()
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
# Calculate wait time more efficiently
|
|
284
|
+
wait_time = self._calculate_wait_time()
|
|
285
|
+
|
|
286
|
+
if timeout is not None:
|
|
287
|
+
elapsed = time.time() - start_time
|
|
288
|
+
if elapsed + wait_time > timeout:
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
# Use shorter sleep intervals for better responsiveness
|
|
292
|
+
await asyncio.sleep(min(wait_time, 0.1))
|
|
293
|
+
|
|
294
|
+
async def _simple_acquire(self, timeout: Optional[float] = None) -> bool:
|
|
295
|
+
"""Simple token bucket acquisition without queuing."""
|
|
296
|
+
start_time = time.time()
|
|
297
|
+
|
|
298
|
+
async with self._get_lock():
|
|
299
|
+
while True:
|
|
300
|
+
self._refill_tokens()
|
|
301
|
+
|
|
302
|
+
if self.state.tokens >= 1.0:
|
|
303
|
+
self.state.tokens -= 1.0
|
|
304
|
+
self.state.request_count += 1
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
wait_time = self._calculate_wait_time()
|
|
308
|
+
|
|
309
|
+
if timeout is not None:
|
|
310
|
+
elapsed = time.time() - start_time
|
|
311
|
+
if elapsed + wait_time > timeout:
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
await asyncio.sleep(wait_time)
|
|
315
|
+
|
|
316
|
+
def _refill_tokens(self):
|
|
317
|
+
"""Refill tokens based on time elapsed."""
|
|
318
|
+
now = time.time()
|
|
319
|
+
time_elapsed = now - self.state.last_refill
|
|
320
|
+
|
|
321
|
+
# Apply adaptive backoff if needed
|
|
322
|
+
if self.config.adaptive_rate_limiting and self.state.current_backoff > 0:
|
|
323
|
+
time_elapsed = min(time_elapsed, self.state.current_backoff)
|
|
324
|
+
|
|
325
|
+
# Calculate tokens to add
|
|
326
|
+
tokens_per_second = self.config.requests_per_minute / 60.0
|
|
327
|
+
tokens_to_add = time_elapsed * tokens_per_second
|
|
328
|
+
|
|
329
|
+
# Add tokens up to burst capacity
|
|
330
|
+
self.state.tokens = min(
|
|
331
|
+
self.config.burst_capacity, self.state.tokens + tokens_to_add
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
self.state.last_refill = now
|
|
335
|
+
|
|
336
|
+
# Reset window if needed
|
|
337
|
+
if now - self.state.window_start >= self.config.window_size_seconds:
|
|
338
|
+
self.state.request_count = 0
|
|
339
|
+
self.state.window_start = now
|
|
340
|
+
|
|
341
|
+
def _calculate_wait_time(self) -> float:
|
|
342
|
+
"""Calculate time to wait for next token."""
|
|
343
|
+
tokens_per_second = self.config.requests_per_minute / 60.0
|
|
344
|
+
if tokens_per_second <= 0:
|
|
345
|
+
return 1.0
|
|
346
|
+
|
|
347
|
+
base_wait = 1.0 / tokens_per_second
|
|
348
|
+
|
|
349
|
+
# Enforce minimum 200ms delay per DataQuery API specification
|
|
350
|
+
min_wait = 0.2 # 200 milliseconds
|
|
351
|
+
base_wait = max(base_wait, min_wait)
|
|
352
|
+
|
|
353
|
+
# Apply adaptive backoff if needed
|
|
354
|
+
if self.config.adaptive_rate_limiting and self.state.current_backoff > 0:
|
|
355
|
+
return max(base_wait, self.state.current_backoff)
|
|
356
|
+
|
|
357
|
+
return base_wait
|
|
358
|
+
|
|
359
|
+
def handle_rate_limit_response(self, headers: Dict[str, str]) -> None:
|
|
360
|
+
"""
|
|
361
|
+
Handle rate limit response from server with adaptive backoff.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
headers: Response headers from the server
|
|
365
|
+
"""
|
|
366
|
+
retry_after = headers.get(self.config.retry_after_header)
|
|
367
|
+
if retry_after:
|
|
368
|
+
try:
|
|
369
|
+
self.state.retry_after = float(retry_after)
|
|
370
|
+
self.state.consecutive_failures += 1
|
|
371
|
+
self.state.last_failure_time = time.time()
|
|
372
|
+
|
|
373
|
+
# Apply adaptive backoff
|
|
374
|
+
if self.config.adaptive_rate_limiting:
|
|
375
|
+
self.state.current_backoff = min(
|
|
376
|
+
self.config.max_backoff_seconds,
|
|
377
|
+
self.state.current_backoff * self.config.backoff_multiplier,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
logger.warning(
|
|
381
|
+
"Server rate limit hit",
|
|
382
|
+
retry_after=retry_after,
|
|
383
|
+
consecutive_failures=self.state.consecutive_failures,
|
|
384
|
+
current_backoff=self.state.current_backoff,
|
|
385
|
+
)
|
|
386
|
+
except (ValueError, TypeError):
|
|
387
|
+
logger.warning("Invalid Retry-After header", retry_after=retry_after)
|
|
388
|
+
|
|
389
|
+
def handle_successful_request(self) -> None:
|
|
390
|
+
"""Handle successful request to reset adaptive backoff."""
|
|
391
|
+
if self.config.adaptive_rate_limiting and self.state.consecutive_failures > 0:
|
|
392
|
+
self.state.consecutive_failures = 0
|
|
393
|
+
self.state.current_backoff = 0.0
|
|
394
|
+
logger.debug("Rate limit backoff reset after successful request")
|
|
395
|
+
|
|
396
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
397
|
+
"""Get current rate limiting statistics."""
|
|
398
|
+
return {
|
|
399
|
+
"tokens_available": self.state.tokens,
|
|
400
|
+
"burst_capacity": self.config.burst_capacity,
|
|
401
|
+
"requests_per_minute": self.config.requests_per_minute,
|
|
402
|
+
"request_count": self.state.request_count,
|
|
403
|
+
"window_start": self.state.window_start,
|
|
404
|
+
"retry_after": self.state.retry_after,
|
|
405
|
+
"last_refill": self.state.last_refill,
|
|
406
|
+
"queue_size": len(self.state.queue),
|
|
407
|
+
"max_queue_size": self.config.max_queue_size,
|
|
408
|
+
"consecutive_failures": self.state.consecutive_failures,
|
|
409
|
+
"current_backoff": self.state.current_backoff,
|
|
410
|
+
"last_request_time": self.state.last_request_time,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
def reset(self) -> None:
|
|
414
|
+
"""Reset rate limiter state."""
|
|
415
|
+
|
|
416
|
+
async def _reset():
|
|
417
|
+
async with self._get_lock():
|
|
418
|
+
self.state = RateLimitState()
|
|
419
|
+
self.state.tokens = float(self.config.burst_capacity)
|
|
420
|
+
self.state.last_refill = time.time()
|
|
421
|
+
|
|
422
|
+
# Clear queue
|
|
423
|
+
for request in self.state.queue:
|
|
424
|
+
if not request.future.done():
|
|
425
|
+
request.future.cancel()
|
|
426
|
+
self.state.queue.clear()
|
|
427
|
+
|
|
428
|
+
# Schedule reset
|
|
429
|
+
asyncio.create_task(_reset())
|
|
430
|
+
logger.info("Rate limiter reset")
|
|
431
|
+
|
|
432
|
+
async def shutdown(self) -> None:
|
|
433
|
+
"""Shutdown the rate limiter and clear queue."""
|
|
434
|
+
self._get_shutdown_event().set()
|
|
435
|
+
|
|
436
|
+
if self._queue_processor_task:
|
|
437
|
+
self._queue_processor_task.cancel()
|
|
438
|
+
try:
|
|
439
|
+
await self._queue_processor_task
|
|
440
|
+
except asyncio.CancelledError:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
# Clear queue
|
|
444
|
+
async with self._get_lock():
|
|
445
|
+
for request in self.state.queue:
|
|
446
|
+
if not request.future.done():
|
|
447
|
+
request.future.cancel()
|
|
448
|
+
self.state.queue.clear()
|
|
449
|
+
|
|
450
|
+
logger.info("Rate limiter shutdown complete")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class RateLimitContext:
|
|
454
|
+
"""Context manager for rate limiting."""
|
|
455
|
+
|
|
456
|
+
def __init__(
|
|
457
|
+
self,
|
|
458
|
+
rate_limiter: EnhancedTokenBucketRateLimiter,
|
|
459
|
+
timeout: Optional[float] = None,
|
|
460
|
+
priority: QueuePriority = QueuePriority.NORMAL,
|
|
461
|
+
operation: str = "unknown",
|
|
462
|
+
):
|
|
463
|
+
self.rate_limiter = rate_limiter
|
|
464
|
+
self.timeout = timeout
|
|
465
|
+
self.priority = priority
|
|
466
|
+
self.operation = operation
|
|
467
|
+
self.acquired = False
|
|
468
|
+
|
|
469
|
+
async def __aenter__(self):
|
|
470
|
+
"""Enter rate limit context."""
|
|
471
|
+
self.acquired = await self.rate_limiter.acquire(
|
|
472
|
+
self.timeout, self.priority, self.operation
|
|
473
|
+
)
|
|
474
|
+
if not self.acquired:
|
|
475
|
+
raise TimeoutError(f"Rate limit timeout for operation: {self.operation}")
|
|
476
|
+
return self
|
|
477
|
+
|
|
478
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
479
|
+
"""Exit rate limit context."""
|
|
480
|
+
# Mark successful request if no exception occurred
|
|
481
|
+
if exc_type is None:
|
|
482
|
+
self.rate_limiter.handle_successful_request()
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def create_rate_limiter(
|
|
486
|
+
requests_per_minute: int = 300, # Exactly 5 requests per second (200ms per request)
|
|
487
|
+
burst_capacity: int = 1, # No burst allowed per specification
|
|
488
|
+
enable_rate_limiting: bool = True,
|
|
489
|
+
enable_queuing: bool = True,
|
|
490
|
+
max_queue_size: int = 1000,
|
|
491
|
+
adaptive_rate_limiting: bool = True,
|
|
492
|
+
) -> EnhancedTokenBucketRateLimiter:
|
|
493
|
+
"""
|
|
494
|
+
Create an enhanced rate limiter with the specified configuration.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
requests_per_minute: Maximum requests per minute
|
|
498
|
+
burst_capacity: Maximum burst capacity
|
|
499
|
+
enable_rate_limiting: Whether to enable rate limiting
|
|
500
|
+
enable_queuing: Whether to enable request queuing
|
|
501
|
+
max_queue_size: Maximum number of queued requests
|
|
502
|
+
adaptive_rate_limiting: Whether to enable adaptive backoff
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Configured enhanced rate limiter
|
|
506
|
+
"""
|
|
507
|
+
config = RateLimitConfig(
|
|
508
|
+
requests_per_minute=requests_per_minute,
|
|
509
|
+
burst_capacity=burst_capacity,
|
|
510
|
+
enable_rate_limiting=enable_rate_limiting,
|
|
511
|
+
enable_queuing=enable_queuing,
|
|
512
|
+
max_queue_size=max_queue_size,
|
|
513
|
+
adaptive_rate_limiting=adaptive_rate_limiting,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return EnhancedTokenBucketRateLimiter(config)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# Backward compatibility
|
|
520
|
+
TokenBucketRateLimiter = EnhancedTokenBucketRateLimiter
|