1+ from fractions import Fraction
12from inspect import signature
23from logging import getLogger
34from time import time
@@ -42,7 +43,7 @@ def __init__(
4243 ):
4344 # Translate request rate values into RequestRate objects
4445 rates = [
45- RequestRate (limit , interval )
46+ _convert_rate (limit , interval )
4647 for interval , limit in {
4748 Duration .SECOND * burst : per_second * burst ,
4849 Duration .MINUTE : per_minute ,
@@ -52,6 +53,11 @@ def __init__(
5253 }.items ()
5354 if limit
5455 ]
56+ if rates and not limiter :
57+ logger .debug (
58+ "Creating Limiter with rates:\n %s" ,
59+ "\n " .join ([f"{ r .limit } /{ r .interval } s" for r in rates ]),
60+ )
5561
5662 # If using a persistent backend, we don't want to use monotonic time (the default)
5763 if bucket_class not in (MemoryListBucket , MemoryQueueBucket ) and not time_function :
@@ -69,7 +75,7 @@ def __init__(
6975 self ._default_bucket = str (uuid4 ())
7076
7177 # If the superclass is an adapter or custom Session, pass along any valid keyword arguments
72- session_kwargs = get_valid_kwargs (super ().__init__ , kwargs )
78+ session_kwargs = _get_valid_kwargs (super ().__init__ , kwargs )
7379 super ().__init__ (** session_kwargs ) # type: ignore # Base Session doesn't take any kwargs
7480
7581 # Conveniently, both Session.send() and HTTPAdapter.send() have a mostly consistent signature
@@ -108,7 +114,7 @@ def _fill_bucket(self, request: PreparedRequest):
108114 If the server also has an hourly limit, we don't have enough information to know if we've
109115 exceeded that limit or how long to delay, so we'll keep delaying in 1-minute intervals.
110116 """
111- logger .info (f' Rate limit exceeded for { request .url } ; filling limiter bucket' )
117+ logger .info (f" Rate limit exceeded for { request .url } ; filling limiter bucket" )
112118 bucket = self .limiter .bucket_group [self ._bucket_name (request )]
113119
114120 # Determine how many requests we've made within the smallest defined time interval
@@ -166,7 +172,16 @@ class LimiterAdapter(LimiterMixin, HTTPAdapter): # type: ignore # send signatu
166172 """
167173
168174
169- def get_valid_kwargs (func : Callable , kwargs : Dict ) -> Dict :
175+ def _convert_rate (limit : float , interval : float ) -> RequestRate :
176+ """Handle fractional rate limits by converting to a whole number of requests per interval"""
177+ # Convert both limit and interval to fractions, and adjust for floating point weirdness
178+ f1 = Fraction (limit ).limit_denominator (1000 )
179+ f2 = Fraction (interval ).limit_denominator (1000 )
180+ rate_fraction = f1 / f2
181+ return RequestRate (rate_fraction .numerator , rate_fraction .denominator )
182+
183+
184+ def _get_valid_kwargs (func : Callable , kwargs : Dict ) -> Dict :
170185 """Get the subset of non-None ``kwargs`` that are valid params for ``func``"""
171186 sig_params = list (signature (func ).parameters )
172187 return {k : v for k , v in kwargs .items () if k in sig_params and v is not None }
0 commit comments