3636}
3737
3838
39- # Simple tracking
40- _last_call_time = {}
39+ # Per-IP per-API rate limit tracking: {ip: {api_name: last_call_time}}
40+ _per_ip_rate_limits = {}
4141
4242countries = {}
4343
@@ -52,35 +52,38 @@ def __init__(self, message: str, rate_limited: dict = None):
5252
5353def get_country (ip : str ):
5454 """
55- Get the country code of the ip
55+ Get the country code of the ip with aggressive API calling until rate limited.
56+ Single pass through APIs - infinite retry is handled by caller.
5657 """
5758 ip_ipv4 = get_ipv4 (ip )
58- errors = []
59- rate_limited = {}
6059
6160 # APIs ordered by accuracy (highest to lowest)
6261 apis = [
63- (_get_country_by_ipinfo_io , "ipinfo" ), # Highest accuracy (~99%)
64- (_get_country_by_ip_api , "ip_api" ), # Very high accuracy (~98%)
65- (_get_country_by_country_is , "country_is" ), # Good accuracy (~95%)
62+ (_get_country_by_ipinfo_io , "ipinfo" ), # Highest accuracy (~99%)
63+ (_get_country_by_ip_api , "ip_api" ), # Very high accuracy (~98%)
64+ (_get_country_by_country_is , "country_is" ), # Good accuracy (~95%)
6665 # (_get_country_by_subvortex_api, "subvortex"), # Custom API - Not ready yet
6766 # (_get_country_by_my_api, "my_api"), # Down
6867 ]
6968
69+ # Initialize per-IP tracking if not exists
70+ if ip_ipv4 not in _per_ip_rate_limits :
71+ _per_ip_rate_limits [ip_ipv4 ] = {}
72+
73+ errors = []
74+ rate_limited = {}
75+
7076 for api_func , api_name in apis :
71- # Check if this API is still in cooldown
77+ # Check if this API is still in rate limit cooldown for this specific IP
7278 now = time .time ()
73- last_call = _last_call_time .get (api_name , 0 )
74- cooldown = API_RATE_LIMITS [api_name ]
75-
76- if now - last_call < cooldown :
77- remaining = cooldown - (now - last_call )
78- rate_limited [api_name ] = remaining
79- continue
79+ if api_name in _per_ip_rate_limits [ip_ipv4 ]:
80+ rate_limit_end = _per_ip_rate_limits [ip_ipv4 ][api_name ]
81+ if now < rate_limit_end :
82+ remaining = rate_limit_end - now
83+ rate_limited [api_name ] = remaining
84+ continue
8085
8186 try :
82- _last_call_time [api_name ] = now
83-
8487 country , reason = api_func (ip_ipv4 )
8588 if country :
8689 return country
@@ -89,22 +92,106 @@ def get_country(ip: str):
8992 errors .append (f"{ api_name } : { reason } " )
9093
9194 except requests .HTTPError as e :
92- status_code = getattr (e .response , 'status_code' , 'unknown' )
93- errors .append (f"{ api_name } ({ status_code } ): { e } " )
95+ status_code = getattr (e .response , "status_code" , "unknown" )
9496
95- except Exception as e :
96- errors .append (f"{ api_name } : { e } " )
97+ # Check for rate limit status codes
98+ if status_code in [429 , 403 ]: # Common rate limit codes
99+ rate_limit_duration = _extract_rate_limit_from_response (e .response , api_name )
100+ rate_limit_end = now + rate_limit_duration
101+ _per_ip_rate_limits [ip_ipv4 ][api_name ] = rate_limit_end
102+
103+ btul .logging .warning (
104+ f"🚫 { api_name } rate limited for { ip_ipv4 } (HTTP { status_code } ). "
105+ f"Will retry after { rate_limit_duration } s"
106+ )
107+ rate_limited [api_name ] = rate_limit_duration
108+
109+ else :
110+ errors .append (f"{ api_name } ({ status_code } ): { e } " )
97111
98- # Build error message with rate limit info
112+ except Exception as e :
113+ # Check if error message indicates rate limiting
114+ error_msg = str (e ).lower ()
115+ if any (phrase in error_msg for phrase in ['rate limit' , 'too many requests' , 'quota exceeded' ]):
116+ # Apply default rate limit if we detect rate limiting but no HTTP status
117+ rate_limit_duration = API_RATE_LIMITS .get (api_name , 60 ) # Default to 60s
118+ rate_limit_end = now + rate_limit_duration
119+ _per_ip_rate_limits [ip_ipv4 ][api_name ] = rate_limit_end
120+
121+ btul .logging .warning (
122+ f"🚫 { api_name } rate limited for { ip_ipv4 } (detected from error). "
123+ f"Will retry after { rate_limit_duration } s"
124+ )
125+ rate_limited [api_name ] = rate_limit_duration
126+
127+ else :
128+ errors .append (f"{ api_name } : { e } " )
129+
130+ # Combine all errors and rate limits
99131 all_errors = errors + [
100132 f"{ api } : { remaining :.0f} s cooldown" for api , remaining in rate_limited .items ()
101133 ]
102134
135+ if all_errors :
136+ raise CountryApiException (
137+ f"All APIs failed for { ip_ipv4 } - { '; ' .join (all_errors )} " , rate_limited
138+ )
139+
140+ # This should not happen, but safety fallback
103141 raise CountryApiException (
104- f"All APIs failed for { ip_ipv4 } - { '; ' . join ( all_errors ) } " , rate_limited
142+ f"No APIs available for { ip_ipv4 } " , rate_limited
105143 )
106144
107145
146+ def _extract_rate_limit_from_response (response , api_name : str ) -> float :
147+ """
148+ Extract rate limit duration from HTTP response headers.
149+ Returns duration in seconds to wait before retrying.
150+ """
151+ try :
152+ # Try common rate limit headers
153+ headers_to_check = [
154+ 'retry-after' , # Standard header
155+ 'x-ratelimit-reset' , # Unix timestamp
156+ 'x-rate-limit-reset' , # Unix timestamp
157+ 'x-ratelimit-retry-after' , # Seconds
158+ 'rate-limit-reset' , # Seconds
159+ ]
160+
161+ for header in headers_to_check :
162+ if header in response .headers :
163+ value = response .headers [header ]
164+
165+ # Handle different formats
166+ if header in ['x-ratelimit-reset' , 'x-rate-limit-reset' ]:
167+ # Unix timestamp - calculate difference from now
168+ try :
169+ reset_time = int (value )
170+ current_time = int (time .time ())
171+ duration = max (reset_time - current_time , 1 ) # At least 1 second
172+ btul .logging .debug (f"Extracted rate limit from { header } : { duration } s" )
173+ return float (duration )
174+ except (ValueError , TypeError ):
175+ continue
176+ else :
177+ # Direct seconds value
178+ try :
179+ duration = float (value )
180+ btul .logging .debug (f"Extracted rate limit from { header } : { duration } s" )
181+ return max (duration , 1 ) # At least 1 second
182+ except (ValueError , TypeError ):
183+ continue
184+
185+ # If no headers found, use the official API rate limits
186+ duration = API_RATE_LIMITS .get (api_name , 60 ) # Default to 60s
187+ btul .logging .debug (f"No rate limit headers found, using default for { api_name } : { duration } s" )
188+ return float (duration )
189+
190+ except Exception as e :
191+ btul .logging .warning (f"Error extracting rate limit from response: { e } " )
192+ return 60.0 # Safe default
193+
194+
108195def _get_country_by_subvortex_api (ip : str ):
109196 """
110197 Get the country code of the ip (use Maxwind and IpInfo)
@@ -199,3 +286,61 @@ def get_ipv4(ip):
199286 pass
200287
201288 return ip
289+
290+
291+ def cleanup_rate_limits_for_ips (ips : list [str ]):
292+ """
293+ Clean up rate limit tracking for specific IPs.
294+ Call this when neurons with these IPs are deleted from the metagraph.
295+
296+ Args:
297+ ips: List of IP addresses to clean up
298+ """
299+ cleaned_count = 0
300+ for ip in ips :
301+ ip_ipv4 = get_ipv4 (ip )
302+ if ip_ipv4 in _per_ip_rate_limits :
303+ del _per_ip_rate_limits [ip_ipv4 ]
304+ cleaned_count += 1
305+
306+ if cleaned_count > 0 :
307+ btul .logging .debug (f"🧹 Cleaned up rate limit data for { cleaned_count } IPs" )
308+
309+ return cleaned_count
310+
311+
312+ def cleanup_rate_limits_for_api (api_name : str ):
313+ """
314+ Clean up rate limit tracking for a specific API across all IPs.
315+ Call this if an API is permanently disabled or changed.
316+
317+ Args:
318+ api_name: Name of the API to clean up ("ipinfo", "ip_api", "country_is", etc.)
319+ """
320+ cleaned_count = 0
321+ for ip_dict in _per_ip_rate_limits .values ():
322+ if api_name in ip_dict :
323+ del ip_dict [api_name ]
324+ cleaned_count += 1
325+
326+ if cleaned_count > 0 :
327+ btul .logging .debug (
328+ f"🧹 Cleaned up { api_name } rate limit data for { cleaned_count } IPs"
329+ )
330+
331+ return cleaned_count
332+
333+
334+ def cleanup_all_rate_limits ():
335+ """
336+ Clean up all rate limit tracking data.
337+ Call this for complete reset or during shutdown.
338+ """
339+ global _per_ip_rate_limits
340+ count = len (_per_ip_rate_limits )
341+ _per_ip_rate_limits .clear ()
342+
343+ if count > 0 :
344+ btul .logging .info (f"🧹 Cleaned up all rate limit data ({ count } IPs)" )
345+
346+ return count
0 commit comments