33"""
44
55import time
6- import asyncio
6+ import asyncio
77from typing import Optional , Dict
88from dataclasses import dataclass
99from datetime import datetime
1010import random
11+ from typing import Callable , Optional
1112
1213from forklet .infrastructure .logger import logger
1314
15+
1416####
1517## RATE LIMIT INFO
1618#####
1719@dataclass
1820class RateLimitInfo :
1921 """Rate limit information from GitHub API headers."""
20-
22+
2123 limit : int = 5000
2224 remaining : int = 5000
2325 reset_time : Optional [datetime ] = None
2426 used : int = 0
25-
27+
2628 @property
2729 def is_exhausted (self ) -> bool :
2830 """Check if rate limit is exhausted."""
2931
3032 return self .remaining <= 10 # Keep a small buffer
31-
33+
3234 @property
3335 def reset_in_seconds (self ) -> float :
3436 """Get seconds until rate limit resets."""
@@ -44,15 +46,12 @@ def reset_in_seconds(self) -> float:
4446class RateLimiter :
4547 """
4648 Async rate limiter for GitHub API requests.
47-
49+
4850 Handles both primary and secondary rate limits with exponential backoff.
4951 """
50-
52+
5153 def __init__ (
52- self ,
53- default_delay : float = 1.0 ,
54- max_delay : float = 60.0 ,
55- adaptive : bool = True
54+ self , default_delay : float = 1.0 , max_delay : float = 60.0 , adaptive : bool = True
5655 ):
5756 self .default_delay = default_delay
5857 self .max_delay = max_delay
@@ -61,40 +60,47 @@ def __init__(
6160 self ._last_request = 0.0
6261 self ._rate_limit_info = RateLimitInfo ()
6362 self ._consecutive_limits = 0
64-
63+ self ._rate_limit_callback : Optional [Callable [[RateLimitInfo ], None ]] = None
64+
65+ def set_rate_limit_callback (
66+ self , callback : Callable [[RateLimitInfo ], None ]
67+ ) -> None :
68+ """Set a callback to be invoked when rate limit information is updated."""
69+ self ._rate_limit_callback = callback
70+ self ._rate_limit_callback : Optional [Callable [[RateLimitInfo ], None ]] = None
71+ self ._rate_limit_callback : Optional [Callable [[RateLimitInfo ], None ]] = None
72+
6573 async def acquire (self ) -> None :
6674 """Acquire rate limit permission."""
6775
6876 async with self ._lock :
6977 current_time = time .time ()
70-
78+
7179 # Check if we need to wait due to rate limiting
7280 if self ._rate_limit_info .is_exhausted :
7381 wait_time = self ._rate_limit_info .reset_in_seconds
7482 if wait_time > 0 :
75- logger .warning (
76- f"Rate limit exhausted, waiting { wait_time :.1f} s"
77- )
83+ logger .warning (f"Rate limit exhausted, waiting { wait_time :.1f} s" )
7884 await asyncio .sleep (wait_time )
79-
85+
8086 # Adaptive delay based on rate limit status
8187 delay = self ._calculate_adaptive_delay (current_time )
82-
88+
8389 if delay > 0 :
8490 await asyncio .sleep (delay )
85-
91+
8692 self ._last_request = time .time ()
87-
93+
8894 def _calculate_adaptive_delay (self , current_time : float ) -> float :
8995 """Calculate adaptive delay based on rate limit status."""
9096
9197 if not self .adaptive :
9298 return self .default_delay
93-
99+
94100 # Base delay from last request
95101 elapsed = current_time - self ._last_request
96102 base_delay = max (0 , self .default_delay - elapsed )
97-
103+
98104 # Adjust based on remaining rate limit
99105 if self ._rate_limit_info .remaining < 100 :
100106 # Very low remaining calls - be more conservative
@@ -111,43 +117,45 @@ def _calculate_adaptive_delay(self, current_time: float) -> float:
111117 else :
112118 # Plenty of calls remaining
113119 multiplier = 1.0
114-
120+
115121 # Add jitter to prevent thundering herd
116122 jitter = random .uniform (0.8 , 1.2 )
117-
123+
118124 final_delay = min (base_delay * multiplier * jitter , self .max_delay )
119125 return final_delay
120-
126+
121127 async def update_rate_limit_info (self , headers : Dict [str , str ]) -> None :
122128 """Update rate limit information from API response headers."""
123-
129+
124130 async with self ._lock :
125131 try :
126132 self ._rate_limit_info .limit = int (
127- headers .get (' x-ratelimit-limit' , 5000 )
133+ headers .get (" x-ratelimit-limit" , 5000 )
128134 )
129135 self ._rate_limit_info .remaining = int (
130- headers .get (' x-ratelimit-remaining' , 5000 )
136+ headers .get (" x-ratelimit-remaining" , 5000 )
131137 )
132- self ._rate_limit_info .used = int (
133- headers .get ('x-ratelimit-used' , 0 )
134- )
135-
136- reset_timestamp = headers .get ('x-ratelimit-reset' )
138+ self ._rate_limit_info .used = int (headers .get ("x-ratelimit-used" , 0 ))
139+
140+ reset_timestamp = headers .get ("x-ratelimit-reset" )
137141 if reset_timestamp :
138142 self ._rate_limit_info .reset_time = datetime .fromtimestamp (
139143 int (reset_timestamp )
140144 )
141-
145+
142146 # Track consecutive rate limit hits
143147 if self ._rate_limit_info .is_exhausted :
144148 self ._consecutive_limits += 1
145149 else :
146150 self ._consecutive_limits = 0
147-
151+
152+ # Invoke callback if set
153+ if self ._rate_limit_callback :
154+ self ._rate_limit_callback (self ._rate_limit_info )
155+
148156 except (ValueError , KeyError ) as e :
149157 logger .warning (f"Failed to parse rate limit headers: { e } " )
150-
158+
151159 @property
152160 def rate_limit_info (self ) -> RateLimitInfo :
153161 """Get current rate limit information."""
0 commit comments