remove zone concept, better handled as prefix
This commit is contained in:
parent
79f799bd7b
commit
e0ab3d45e5
@ -36,7 +36,6 @@ limiter = RateLimiter(tiers=[bronze, silver])
|
|||||||
|
|
||||||
Then to apply limiting, you'll call the `check_limit` function, which takes three parameters:
|
Then to apply limiting, you'll call the `check_limit` function, which takes three parameters:
|
||||||
|
|
||||||
* `zone` - Limits are considered unique per zone. So if you want calls against your geocoding API to count against a different limit than your user API you could pass those as unique zones.
|
|
||||||
* `key` - A unique-per user key, often the user's API key or username. (Note: `rrl` does not know if a key is valid or not, that validation should be in your application and usually occur before the call to `check_limit`.)
|
* `key` - A unique-per user key, often the user's API key or username. (Note: `rrl` does not know if a key is valid or not, that validation should be in your application and usually occur before the call to `check_limit`.)
|
||||||
* `tier_name` - The name of one of the tiers as specified when instantiating the `RateLimiter` class. (Note: `rrl` does not have a concept of which users are in which tier, that logic should be handled by your key validation code.)
|
* `tier_name` - The name of one of the tiers as specified when instantiating the `RateLimiter` class. (Note: `rrl` does not have a concept of which users are in which tier, that logic should be handled by your key validation code.)
|
||||||
|
|
||||||
|
19
rrl.py
19
rrl.py
@ -32,9 +32,9 @@ def _get_redis_connection() -> Redis:
|
|||||||
|
|
||||||
class RateLimiter:
|
class RateLimiter:
|
||||||
"""
|
"""
|
||||||
<zone>:<key>:<hour><minute> expires in 2 minutes
|
<prefix>:<key>:<hour><minute> expires in 2 minutes
|
||||||
<zone>:<key>:<hour> expires in 2 hours
|
<prefix>:<key>:<hour> expires in 2 hours
|
||||||
<zone>:<key>:<day> never expires
|
<prefix>:<key>:<day> never expires
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -51,7 +51,7 @@ class RateLimiter:
|
|||||||
self.use_redis_time = use_redis_time
|
self.use_redis_time = use_redis_time
|
||||||
self.track_daily_usage = track_daily_usage
|
self.track_daily_usage = track_daily_usage
|
||||||
|
|
||||||
def check_limit(self, zone: str, key: str, tier_name: str) -> bool:
|
def check_limit(self, key: str, tier_name: str) -> bool:
|
||||||
try:
|
try:
|
||||||
tier = self.tiers[tier_name]
|
tier = self.tiers[tier_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -64,16 +64,16 @@ class RateLimiter:
|
|||||||
|
|
||||||
pipe = self.redis.pipeline()
|
pipe = self.redis.pipeline()
|
||||||
if tier.per_minute:
|
if tier.per_minute:
|
||||||
minute_key = f"{self.prefix}:{zone}:{key}:m{now.minute}"
|
minute_key = f"{self.prefix}:{key}:m{now.minute}"
|
||||||
pipe.incr(minute_key)
|
pipe.incr(minute_key)
|
||||||
pipe.expire(minute_key, 60)
|
pipe.expire(minute_key, 60)
|
||||||
if tier.per_hour:
|
if tier.per_hour:
|
||||||
hour_key = f"{self.prefix}:{zone}:{key}:h{now.hour}"
|
hour_key = f"{self.prefix}:{key}:h{now.hour}"
|
||||||
pipe.incr(hour_key)
|
pipe.incr(hour_key)
|
||||||
pipe.expire(hour_key, 3600)
|
pipe.expire(hour_key, 3600)
|
||||||
if tier.per_day or self.track_daily_usage:
|
if tier.per_day or self.track_daily_usage:
|
||||||
day = now.strftime("%Y%m%d")
|
day = now.strftime("%Y%m%d")
|
||||||
day_key = f"{self.prefix}:{zone}:{key}:d{day}"
|
day_key = f"{self.prefix}:{key}:d{day}"
|
||||||
pipe.incr(day_key)
|
pipe.incr(day_key)
|
||||||
# keep data around for usage tracking
|
# keep data around for usage tracking
|
||||||
if not self.track_daily_usage:
|
if not self.track_daily_usage:
|
||||||
@ -106,7 +106,6 @@ class RateLimiter:
|
|||||||
|
|
||||||
def get_usage_since(
|
def get_usage_since(
|
||||||
self,
|
self,
|
||||||
zone: str,
|
|
||||||
key: str,
|
key: str,
|
||||||
start: datetime.date,
|
start: datetime.date,
|
||||||
end: typing.Optional[datetime.date] = None,
|
end: typing.Optional[datetime.date] = None,
|
||||||
@ -120,9 +119,7 @@ class RateLimiter:
|
|||||||
while day <= end:
|
while day <= end:
|
||||||
days.append(day)
|
days.append(day)
|
||||||
day += datetime.timedelta(days=1)
|
day += datetime.timedelta(days=1)
|
||||||
day_keys = [
|
day_keys = [f"{self.prefix}:{key}:d{day.strftime('%Y%m%d')}" for day in days]
|
||||||
f"{self.prefix}:{zone}:{key}:d{day.strftime('%Y%m%d')}" for day in days
|
|
||||||
]
|
|
||||||
return [
|
return [
|
||||||
DailyUsage(d, int(calls.decode()) if calls else 0)
|
DailyUsage(d, int(calls.decode()) if calls else 0)
|
||||||
for d, calls in zip(days, self.redis.mget(day_keys))
|
for d, calls in zip(days, self.redis.mget(day_keys))
|
||||||
|
@ -31,7 +31,7 @@ def test_check_limit_per_minute(tier, reset_time):
|
|||||||
# don't loop infinitely if test is failing
|
# don't loop infinitely if test is failing
|
||||||
while count < 20:
|
while count < 20:
|
||||||
try:
|
try:
|
||||||
rl.check_limit("test-zone", "test-key", tier.name)
|
rl.check_limit("test-key", tier.name)
|
||||||
count += 1
|
count += 1
|
||||||
except RateLimitExceeded as e:
|
except RateLimitExceeded as e:
|
||||||
print(e)
|
print(e)
|
||||||
@ -40,7 +40,7 @@ def test_check_limit_per_minute(tier, reset_time):
|
|||||||
assert count == 10
|
assert count == 10
|
||||||
# resets after a given time
|
# resets after a given time
|
||||||
frozen.tick(reset_time)
|
frozen.tick(reset_time)
|
||||||
assert rl.check_limit("test-zone", "test-key", tier.name)
|
assert rl.check_limit("test-key", tier.name)
|
||||||
|
|
||||||
|
|
||||||
def test_using_redis_time():
|
def test_using_redis_time():
|
||||||
@ -51,7 +51,7 @@ def test_using_redis_time():
|
|||||||
count = 0
|
count = 0
|
||||||
while count < 20:
|
while count < 20:
|
||||||
try:
|
try:
|
||||||
rl.check_limit("test-zone", "test-key", simple_daily_tier.name)
|
rl.check_limit("test-key", simple_daily_tier.name)
|
||||||
count += 1
|
count += 1
|
||||||
except RateLimitExceeded:
|
except RateLimitExceeded:
|
||||||
break
|
break
|
||||||
@ -63,19 +63,20 @@ def test_invalid_tier():
|
|||||||
rl = RateLimiter(tiers=[simple_daily_tier], use_redis_time=True)
|
rl = RateLimiter(tiers=[simple_daily_tier], use_redis_time=True)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
rl.check_limit("test-zone", "test-key", "non-existent-tier")
|
rl.check_limit("test-key", "non-existent-tier")
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_zones():
|
def test_multiple_prefix():
|
||||||
redis.flushall()
|
redis.flushall()
|
||||||
rl = RateLimiter(tiers=[simple_daily_tier], use_redis_time=True)
|
rl1 = RateLimiter(tiers=[simple_daily_tier], use_redis_time=True, prefix="zone1")
|
||||||
|
rl2 = RateLimiter(tiers=[simple_daily_tier], use_redis_time=True, prefix="zone2")
|
||||||
|
|
||||||
# don't loop infinitely if test is failing
|
# don't loop infinitely if test is failing
|
||||||
count = 0
|
count = 0
|
||||||
while count < 20:
|
while count < 20:
|
||||||
try:
|
try:
|
||||||
rl.check_limit("zone1", "test-key", simple_daily_tier.name)
|
rl1.check_limit("test-key", simple_daily_tier.name)
|
||||||
rl.check_limit("zone2", "test-key", simple_daily_tier.name)
|
rl2.check_limit("test-key", simple_daily_tier.name)
|
||||||
count += 1
|
count += 1
|
||||||
except RateLimitExceeded:
|
except RateLimitExceeded:
|
||||||
break
|
break
|
||||||
@ -90,8 +91,8 @@ def test_multiple_keys():
|
|||||||
count = 0
|
count = 0
|
||||||
while count < 20:
|
while count < 20:
|
||||||
try:
|
try:
|
||||||
rl.check_limit("zone", "test-key1", simple_daily_tier.name)
|
rl.check_limit("test-key1", simple_daily_tier.name)
|
||||||
rl.check_limit("zone", "test-key2", simple_daily_tier.name)
|
rl.check_limit("test-key2", simple_daily_tier.name)
|
||||||
count += 1
|
count += 1
|
||||||
except RateLimitExceeded:
|
except RateLimitExceeded:
|
||||||
break
|
break
|
||||||
@ -108,10 +109,10 @@ def test_get_daily_usage():
|
|||||||
for n in range(1, 10):
|
for n in range(1, 10):
|
||||||
with freeze_time(f"2020-01-0{n}"):
|
with freeze_time(f"2020-01-0{n}"):
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
rl.check_limit("zone", "test-key", unlimited_tier.name)
|
rl.check_limit("test-key", unlimited_tier.name)
|
||||||
|
|
||||||
with freeze_time("2020-01-15"):
|
with freeze_time("2020-01-15"):
|
||||||
usage = rl.get_usage_since("zone", "test-key", datetime.date(2020, 1, 1))
|
usage = rl.get_usage_since("test-key", datetime.date(2020, 1, 1))
|
||||||
assert usage[0] == DailyUsage(datetime.date(2020, 1, 1), 1)
|
assert usage[0] == DailyUsage(datetime.date(2020, 1, 1), 1)
|
||||||
assert usage[3] == DailyUsage(datetime.date(2020, 1, 4), 4)
|
assert usage[3] == DailyUsage(datetime.date(2020, 1, 4), 4)
|
||||||
assert usage[8] == DailyUsage(datetime.date(2020, 1, 9), 9)
|
assert usage[8] == DailyUsage(datetime.date(2020, 1, 9), 9)
|
||||||
@ -130,8 +131,8 @@ def test_get_daily_usage_untracked():
|
|||||||
for n in range(1, 10):
|
for n in range(1, 10):
|
||||||
with freeze_time(f"2020-01-0{n}"):
|
with freeze_time(f"2020-01-0{n}"):
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
rl.check_limit("zone", "test-key", unlimited_tier.name)
|
rl.check_limit("test-key", unlimited_tier.name)
|
||||||
|
|
||||||
# values would be incorrect (likely zero), warn the caller
|
# values would be incorrect (likely zero), warn the caller
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
rl.get_usage_since("zone", "test-key", datetime.date(2020, 1, 1))
|
rl.get_usage_since("test-key", datetime.date(2020, 1, 1))
|
||||||
|
Loading…
Reference in New Issue
Block a user