hanzo 0.3.21__py3-none-any.whl → 0.3.23__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.

Potentially problematic release.


This version of hanzo might be problematic. Click here for more details.

hanzo/rate_limiter.py CHANGED
@@ -4,17 +4,18 @@ Prevents API overuse and handles failures gracefully.
4
4
  """
5
5
 
6
6
  import time
7
+ import random
7
8
  import asyncio
8
- from typing import Dict, Optional, Any, Callable
9
- from dataclasses import dataclass, field
9
+ from typing import Any, Dict, Callable, Optional
10
10
  from datetime import datetime, timedelta
11
11
  from collections import deque
12
- import random
12
+ from dataclasses import field, dataclass
13
13
 
14
14
 
15
15
  @dataclass
16
16
  class RateLimitConfig:
17
17
  """Configuration for rate limiting."""
18
+
18
19
  requests_per_minute: int = 20
19
20
  requests_per_hour: int = 100
20
21
  burst_size: int = 5
@@ -27,6 +28,7 @@ class RateLimitConfig:
27
28
  @dataclass
28
29
  class RateLimitState:
29
30
  """Current state of rate limiter."""
31
+
30
32
  minute_requests: deque = field(default_factory=lambda: deque(maxlen=60))
31
33
  hour_requests: deque = field(default_factory=lambda: deque(maxlen=3600))
32
34
  last_request: Optional[datetime] = None
@@ -39,18 +41,18 @@ class RateLimitState:
39
41
 
40
42
  class RateLimiter:
41
43
  """Rate limiter with error recovery."""
42
-
44
+
43
45
  def __init__(self, config: RateLimitConfig = None):
44
46
  """Initialize rate limiter."""
45
47
  self.config = config or RateLimitConfig()
46
48
  self.states: Dict[str, RateLimitState] = {}
47
-
49
+
48
50
  def get_state(self, key: str = "default") -> RateLimitState:
49
51
  """Get or create state for a key."""
50
52
  if key not in self.states:
51
53
  self.states[key] = RateLimitState()
52
54
  return self.states[key]
53
-
55
+
54
56
  async def check_rate_limit(self, key: str = "default") -> tuple[bool, float]:
55
57
  """
56
58
  Check if request is allowed.
@@ -58,7 +60,7 @@ class RateLimiter:
58
60
  """
59
61
  state = self.get_state(key)
60
62
  now = datetime.now()
61
-
63
+
62
64
  # Check if throttled
63
65
  if state.is_throttled and state.throttle_until:
64
66
  if now < state.throttle_until:
@@ -68,41 +70,41 @@ class RateLimiter:
68
70
  # Throttle period ended
69
71
  state.is_throttled = False
70
72
  state.throttle_until = None
71
-
73
+
72
74
  # Clean old requests
73
75
  minute_ago = now - timedelta(minutes=1)
74
76
  hour_ago = now - timedelta(hours=1)
75
-
77
+
76
78
  # Remove old requests from queues
77
79
  while state.minute_requests and state.minute_requests[0] < minute_ago:
78
80
  state.minute_requests.popleft()
79
-
81
+
80
82
  while state.hour_requests and state.hour_requests[0] < hour_ago:
81
83
  state.hour_requests.popleft()
82
-
84
+
83
85
  # Check minute limit
84
86
  if len(state.minute_requests) >= self.config.requests_per_minute:
85
87
  # Calculate wait time
86
88
  oldest = state.minute_requests[0]
87
89
  wait_seconds = (oldest + timedelta(minutes=1) - now).total_seconds()
88
90
  return False, max(0, wait_seconds)
89
-
91
+
90
92
  # Check hour limit
91
93
  if len(state.hour_requests) >= self.config.requests_per_hour:
92
94
  # Calculate wait time
93
95
  oldest = state.hour_requests[0]
94
96
  wait_seconds = (oldest + timedelta(hours=1) - now).total_seconds()
95
97
  return False, max(0, wait_seconds)
96
-
98
+
97
99
  # Check burst limit
98
100
  if state.last_request:
99
101
  time_since_last = (now - state.last_request).total_seconds()
100
102
  if time_since_last < 1.0 / self.config.burst_size:
101
103
  wait_seconds = (1.0 / self.config.burst_size) - time_since_last
102
104
  return False, wait_seconds
103
-
105
+
104
106
  return True, 0
105
-
107
+
106
108
  async def acquire(self, key: str = "default") -> bool:
107
109
  """
108
110
  Acquire a rate limit slot.
@@ -110,7 +112,7 @@ class RateLimiter:
110
112
  """
111
113
  while True:
112
114
  allowed, wait_seconds = await self.check_rate_limit(key)
113
-
115
+
114
116
  if allowed:
115
117
  # Record request
116
118
  state = self.get_state(key)
@@ -120,37 +122,37 @@ class RateLimiter:
120
122
  state.last_request = now
121
123
  state.total_requests += 1
122
124
  return True
123
-
125
+
124
126
  # Wait before retrying
125
127
  if wait_seconds > 0:
126
128
  await asyncio.sleep(min(wait_seconds, 5)) # Check every 5 seconds max
127
-
129
+
128
130
  def record_error(self, key: str = "default", error: Exception = None):
129
131
  """Record an error for the key."""
130
132
  state = self.get_state(key)
131
133
  state.consecutive_errors += 1
132
134
  state.total_errors += 1
133
-
135
+
134
136
  # Implement exponential backoff on errors
135
137
  if state.consecutive_errors >= 3:
136
138
  # Throttle for increasing periods
137
139
  backoff_minutes = min(
138
140
  self.config.backoff_base ** (state.consecutive_errors - 2),
139
- 60 # Max 1 hour
141
+ 60, # Max 1 hour
140
142
  )
141
143
  state.is_throttled = True
142
144
  state.throttle_until = datetime.now() + timedelta(minutes=backoff_minutes)
143
-
145
+
144
146
  def record_success(self, key: str = "default"):
145
147
  """Record a successful request."""
146
148
  state = self.get_state(key)
147
149
  state.consecutive_errors = 0
148
-
150
+
149
151
  def get_status(self, key: str = "default") -> Dict[str, Any]:
150
152
  """Get current status for monitoring."""
151
153
  state = self.get_state(key)
152
154
  now = datetime.now()
153
-
155
+
154
156
  return {
155
157
  "requests_last_minute": len(state.minute_requests),
156
158
  "requests_last_hour": len(state.hour_requests),
@@ -170,47 +172,47 @@ class RateLimiter:
170
172
 
171
173
  class ErrorRecovery:
172
174
  """Error recovery with retries and fallback."""
173
-
175
+
174
176
  def __init__(self, rate_limiter: RateLimiter = None):
175
177
  """Initialize error recovery."""
176
178
  self.rate_limiter = rate_limiter or RateLimiter()
177
179
  self.fallback_handlers: Dict[type, Callable] = {}
178
-
180
+
179
181
  def register_fallback(self, error_type: type, handler: Callable):
180
182
  """Register a fallback handler for an error type."""
181
183
  self.fallback_handlers[error_type] = handler
182
-
184
+
183
185
  async def with_retry(
184
186
  self,
185
187
  func: Callable,
186
188
  *args,
187
189
  key: str = "default",
188
190
  max_retries: Optional[int] = None,
189
- **kwargs
191
+ **kwargs,
190
192
  ) -> Any:
191
193
  """
192
194
  Execute function with retry logic.
193
195
  """
194
196
  max_retries = max_retries or self.rate_limiter.config.max_retries
195
197
  last_error = None
196
-
198
+
197
199
  for attempt in range(max_retries):
198
200
  try:
199
201
  # Check rate limit
200
202
  await self.rate_limiter.acquire(key)
201
-
203
+
202
204
  # Execute function
203
205
  result = await func(*args, **kwargs)
204
-
206
+
205
207
  # Record success
206
208
  self.rate_limiter.record_success(key)
207
-
209
+
208
210
  return result
209
-
211
+
210
212
  except Exception as e:
211
213
  last_error = e
212
214
  self.rate_limiter.record_error(key, e)
213
-
215
+
214
216
  # Check for fallback handler
215
217
  for error_type, handler in self.fallback_handlers.items():
216
218
  if isinstance(e, error_type):
@@ -218,20 +220,20 @@ class ErrorRecovery:
218
220
  return await handler(*args, **kwargs)
219
221
  except:
220
222
  pass # Fallback failed, continue with retry
221
-
223
+
222
224
  # Calculate backoff
223
225
  if attempt < max_retries - 1:
224
- backoff = self.rate_limiter.config.backoff_base ** attempt
225
-
226
+ backoff = self.rate_limiter.config.backoff_base**attempt
227
+
226
228
  # Add jitter if configured
227
229
  if self.rate_limiter.config.jitter:
228
- backoff *= (0.5 + random.random())
229
-
230
+ backoff *= 0.5 + random.random()
231
+
230
232
  await asyncio.sleep(min(backoff, 60)) # Max 60 seconds
231
-
233
+
232
234
  # All retries failed
233
235
  raise last_error or Exception("All retry attempts failed")
234
-
236
+
235
237
  async def with_circuit_breaker(
236
238
  self,
237
239
  func: Callable,
@@ -239,21 +241,21 @@ class ErrorRecovery:
239
241
  key: str = "default",
240
242
  threshold: int = 5,
241
243
  timeout: int = 60,
242
- **kwargs
244
+ **kwargs,
243
245
  ) -> Any:
244
246
  """
245
247
  Execute function with circuit breaker pattern.
246
248
  """
247
249
  state = self.rate_limiter.get_state(key)
248
-
250
+
249
251
  # Check if circuit is open
250
252
  if state.is_throttled:
251
253
  raise Exception(f"Circuit breaker open for {key}")
252
-
254
+
253
255
  try:
254
256
  result = await self.with_retry(func, *args, key=key, **kwargs)
255
257
  return result
256
-
258
+
257
259
  except Exception as e:
258
260
  # Check if we should open the circuit
259
261
  if state.consecutive_errors >= threshold:
@@ -265,61 +267,44 @@ class ErrorRecovery:
265
267
 
266
268
  class SmartRateLimiter:
267
269
  """Smart rate limiter that adapts to API responses."""
268
-
270
+
269
271
  def __init__(self):
270
272
  """Initialize smart rate limiter."""
271
273
  self.limiters: Dict[str, RateLimiter] = {}
272
274
  self.recovery = ErrorRecovery()
273
-
275
+
274
276
  # Default configs for known APIs
275
277
  self.configs = {
276
278
  "openai": RateLimitConfig(
277
- requests_per_minute=60,
278
- requests_per_hour=1000,
279
- burst_size=10
279
+ requests_per_minute=60, requests_per_hour=1000, burst_size=10
280
280
  ),
281
281
  "anthropic": RateLimitConfig(
282
- requests_per_minute=50,
283
- requests_per_hour=1000,
284
- burst_size=5
282
+ requests_per_minute=50, requests_per_hour=1000, burst_size=5
285
283
  ),
286
284
  "local": RateLimitConfig(
287
- requests_per_minute=100,
288
- requests_per_hour=10000,
289
- burst_size=20
285
+ requests_per_minute=100, requests_per_hour=10000, burst_size=20
290
286
  ),
291
287
  "free": RateLimitConfig(
292
- requests_per_minute=10,
293
- requests_per_hour=100,
294
- burst_size=2
288
+ requests_per_minute=10, requests_per_hour=100, burst_size=2
295
289
  ),
296
290
  }
297
-
291
+
298
292
  def get_limiter(self, api_type: str) -> RateLimiter:
299
293
  """Get or create limiter for API type."""
300
294
  if api_type not in self.limiters:
301
295
  config = self.configs.get(api_type, RateLimitConfig())
302
296
  self.limiters[api_type] = RateLimiter(config)
303
297
  return self.limiters[api_type]
304
-
298
+
305
299
  async def execute_with_limit(
306
- self,
307
- api_type: str,
308
- func: Callable,
309
- *args,
310
- **kwargs
300
+ self, api_type: str, func: Callable, *args, **kwargs
311
301
  ) -> Any:
312
302
  """Execute function with appropriate rate limiting."""
313
303
  limiter = self.get_limiter(api_type)
314
304
  recovery = ErrorRecovery(limiter)
315
-
316
- return await recovery.with_retry(
317
- func,
318
- *args,
319
- key=api_type,
320
- **kwargs
321
- )
322
-
305
+
306
+ return await recovery.with_retry(func, *args, key=api_type, **kwargs)
307
+
323
308
  def get_all_status(self) -> Dict[str, Dict[str, Any]]:
324
309
  """Get status of all limiters."""
325
310
  return {
@@ -329,4 +314,4 @@ class SmartRateLimiter:
329
314
 
330
315
 
331
316
  # Global instance for easy use
332
- smart_limiter = SmartRateLimiter()
317
+ smart_limiter = SmartRateLimiter()