Skip to content

Commit

Permalink
Documentation/Docstring updates.
Browse files Browse the repository at this point in the history
* renamed RateLimitItem subclasses
* exposed RateLimitItem and parse/parse_many at the top level namespace.
  • Loading branch information
alisaifee committed Jan 10, 2015
1 parent 364de24 commit 495f35d
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 66 deletions.
17 changes: 17 additions & 0 deletions doc/source/api.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. currentmodule:: limits

API
----

Expand All @@ -7,6 +9,7 @@ Storage
.. autoclass:: limits.storage.MemoryStorage
.. autoclass:: limits.storage.RedisStorage
.. autoclass:: limits.storage.MemcachedStorage
.. autofunction:: limits.storage.storage_from_string

Strategies
==========
Expand All @@ -15,6 +18,20 @@ Strategies
.. autoclass:: limits.strategies.FixedWindowElasticExpiryRateLimiter
.. autoclass:: limits.strategies.MovingWindowRateLimiter

Rate Limits
===========

.. autoclass:: RateLimitItem
.. autoclass:: RateLimitItemPerYear
.. autoclass:: RateLimitItemPerMonth
.. autoclass:: RateLimitItemPerDay
.. autoclass:: RateLimitItemPerHour
.. autoclass:: RateLimitItemPerMinute
.. autoclass:: RateLimitItemPerSecond
.. autofunction:: parse
.. autofunction:: parse_many


Exceptions
==========
.. autoexception:: limits.errors.ConfigurationError
Expand Down
8 changes: 4 additions & 4 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ Build a rate limiter with the :ref:`moving-window`::

Build a rate limit using the :ref:`ratelimit-string`::

from limits.util import parse
from limits import parse
one_per_minute = parse("1/minute")

Build a rate limit explicitely::
Build a rate limit explicitly using a subclass of :class:`RateLimitItem`::

from limits.limits import GRANULARITIES
one_per_second = GRANULARITIES["second"](1)
from limits import RateLimitItemPerSecond
one_per_second = RateLimitItemPerSecond(1, 1)

Test the limits::

Expand Down
7 changes: 7 additions & 0 deletions limits/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@
__version__ = get_versions()['version']
del get_versions

from .limits import (
RateLimitItem, RateLimitItemPerYear, RateLimitItemPerMonth,
RateLimitItemPerDay, RateLimitItemPerHour, RateLimitItemPerMinute,
RateLimitItemPerSecond
)
from .util import parse, parse_many

63 changes: 42 additions & 21 deletions limits/limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
from .backports.total_ordering import total_ordering # pragma: no cover

TIME_TYPES = dict(
DAY=(60 * 60 * 24, "day"),
MONTH=(60 * 60 * 24 * 30, "month"),
YEAR=(60 * 60 * 24 * 30 * 12, "year"),
HOUR=(60 * 60, "hour"),
MINUTE=(60, "minute"),
SECOND=(1, "second")
day=(60 * 60 * 24, "day"),
month=(60 * 60 * 24 * 30, "month"),
year=(60 * 60 * 24 * 30 * 12, "year"),
hour=(60 * 60, "hour"),
minute=(60, "minute"),
second=(1, "second")
)

GRANULARITIES = {}
Expand All @@ -33,8 +33,12 @@ def __new__(cls, name, parents, dct):
@total_ordering
class RateLimitItem(object):
"""
defines a Rate limited resource which contains characteristics
namespace, amount and granularity of rate limiting window.
defines a Rate limited resource which contains the characteristic
namespace, amount and granularity multiples of the rate limiting window.
:param int amount: the rate limit amount
:param int multiples: multiple of the 'per' granularity (e.g. 'n' per 'm' seconds)
:param string namespace: category for the specific rate limit
"""
__metaclass__ = RateLimitItemMeta
__slots__ = ["namespace", "amount", "multiples", "granularity"]
Expand Down Expand Up @@ -85,27 +89,44 @@ def __repr__(self):
def __lt__(self, other):
return self.granularity[0] < other.granularity[0]

#pylint: disable=invalid-name
class PER_YEAR(RateLimitItem):
granularity = TIME_TYPES["YEAR"]
class RateLimitItemPerYear(RateLimitItem):
"""
per year rate limited resource.
"""
granularity = TIME_TYPES["year"]


class PER_MONTH(RateLimitItem):
granularity = TIME_TYPES["MONTH"]
class RateLimitItemPerMonth(RateLimitItem):
"""
per month rate limited resource.
"""
granularity = TIME_TYPES["month"]


class PER_DAY(RateLimitItem):
granularity = TIME_TYPES["DAY"]
class RateLimitItemPerDay(RateLimitItem):
"""
per day rate limited resource.
"""
granularity = TIME_TYPES["day"]


class PER_HOUR(RateLimitItem):
granularity = TIME_TYPES["HOUR"]
class RateLimitItemPerHour(RateLimitItem):
"""
per hour rate limited resource.
"""
granularity = TIME_TYPES["hour"]


class PER_MINUTE(RateLimitItem):
granularity = TIME_TYPES["MINUTE"]
class RateLimitItemPerMinute(RateLimitItem):
"""
per minute rate limited resource.
"""
granularity = TIME_TYPES["minute"]


class PER_SECOND(RateLimitItem):
granularity = TIME_TYPES["SECOND"]
class RateLimitItemPerSecond(RateLimitItem):
"""
per second rate limited resource.
"""
granularity = TIME_TYPES["second"]

2 changes: 2 additions & 0 deletions limits/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

def storage_from_string(storage_string):
"""
factory function to get the storage class based on the url of
the storage
:param storage_string: a string of the form method://host:port
:return: a subclass of :class:`flask_limiter.storage.Storage`
Expand Down
21 changes: 15 additions & 6 deletions limits/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,30 @@ def get_dependency(dep):

def parse_many(limit_string):
"""
parses rate limits in string notation containing multiple rate limits
(e.g. '1/second; 5/minute')
:param string limit_string: rate limit string using :ref:`ratelimit-string`
:raise ValueError: if the string notation is invalid.
:return: a list of :class:`RateLimitItem` instances.
:param limit_string:
:raise ValueError:
"""
if not EXPR.match(limit_string):
raise ValueError("couldn't parse rate limit string '%s'" % limit_string)
limits = []
for amount, _, multiples, granularity_string in EXPR.findall(limit_string):
granularity = granularity_from_string(granularity_string)
yield granularity(amount, multiples)
limits.append(granularity(amount, multiples))
return limits

def parse(limit_string):
"""
parses a single rate limit in string notation (e.g. '1/second' or '1 per second'
:param string limit_string: rate limit string using :ref:`ratelimit-string`
:raise ValueError: if the string notation is invalid.
:return: an instance of :class:`RateLimitItem`
:param limit_string:
:return:
"""
return list(parse_many(limit_string))[0]

Expand All @@ -49,7 +58,7 @@ def granularity_from_string(granularity_string):
"""
:param granularity_string:
:return: a :class:`flask_ratelimit.limits.Item`
:return: a subclass of :class:`RateLimitItem`
:raise ValueError:
"""
for granularity in GRANULARITIES.values():
Expand Down
24 changes: 12 additions & 12 deletions tests/test_limit_granularities.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@

class GranularityTests(unittest.TestCase):
def test_seconds_value(self):
self.assertEqual(limits.PER_HOUR(1).get_expiry(), 60*60)
self.assertEqual(limits.PER_MINUTE(1).get_expiry(), 60)
self.assertEqual(limits.PER_SECOND(1).get_expiry(), 1)
self.assertEqual(limits.PER_DAY(1).get_expiry(), 60*60*24)
self.assertEqual(limits.PER_MONTH(1).get_expiry(), 60*60*24*30)
self.assertEqual(limits.PER_YEAR(1).get_expiry(), 60*60*24*30*12)
self.assertEqual(limits.RateLimitItemPerHour(1).get_expiry(), 60*60)
self.assertEqual(limits.RateLimitItemPerMinute(1).get_expiry(), 60)
self.assertEqual(limits.RateLimitItemPerSecond(1).get_expiry(), 1)
self.assertEqual(limits.RateLimitItemPerDay(1).get_expiry(), 60*60*24)
self.assertEqual(limits.RateLimitItemPerMonth(1).get_expiry(), 60*60*24*30)
self.assertEqual(limits.RateLimitItemPerYear(1).get_expiry(), 60*60*24*30*12)

def test_representation(self):
self.assertTrue("1 per 1 hour" in str(limits.PER_HOUR(1)))
self.assertTrue("1 per 1 minute" in str(limits.PER_MINUTE(1)))
self.assertTrue("1 per 1 second" in str(limits.PER_SECOND(1)))
self.assertTrue("1 per 1 day" in str(limits.PER_DAY(1)))
self.assertTrue("1 per 1 month" in str(limits.PER_MONTH(1)))
self.assertTrue("1 per 1 year" in str(limits.PER_YEAR(1)))
self.assertTrue("1 per 1 hour" in str(limits.RateLimitItemPerHour(1)))
self.assertTrue("1 per 1 minute" in str(limits.RateLimitItemPerMinute(1)))
self.assertTrue("1 per 1 second" in str(limits.RateLimitItemPerSecond(1)))
self.assertTrue("1 per 1 day" in str(limits.RateLimitItemPerDay(1)))
self.assertTrue("1 per 1 month" in str(limits.RateLimitItemPerMonth(1)))
self.assertTrue("1 per 1 year" in str(limits.RateLimitItemPerYear(1)))
12 changes: 6 additions & 6 deletions tests/test_ratelimit_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,32 @@ def test_singles(self):
for rl_string in ["1 per hour", "1/HOUR", "1/Hour"]:
self.assertEqual(
parse( rl_string),
limits.PER_HOUR(1)
limits.RateLimitItemPerHour(1)
)
for rl_string in ["1 per minute", "1/MINUTE", "1/Minute"]:
self.assertEqual(
parse( rl_string),
limits.PER_MINUTE(1)
limits.RateLimitItemPerMinute(1)
)
for rl_string in ["1 per second", "1/SECOND", "1 / Second"]:
self.assertEqual(
parse( rl_string),
limits.PER_SECOND(1)
limits.RateLimitItemPerSecond(1)
)
for rl_string in ["1 per day", "1/DAY", "1 / Day"]:
self.assertEqual(
parse( rl_string),
limits.PER_DAY(1)
limits.RateLimitItemPerDay(1)
)
for rl_string in ["1 per month", "1/MONTH", "1 / Month"]:
self.assertEqual(
parse( rl_string),
limits.PER_MONTH(1)
limits.RateLimitItemPerMonth(1)
)
for rl_string in ["1 per year", "1/Year", "1 / year"]:
self.assertEqual(
parse( rl_string),
limits.PER_YEAR(1)
limits.RateLimitItemPerYear(1)
)

def test_multiples(self):
Expand Down
18 changes: 9 additions & 9 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from limits.strategies import FixedWindowRateLimiter, MovingWindowRateLimiter
from limits.errors import ConfigurationError
from limits.limits import PER_MINUTE, PER_SECOND
from limits.limits import RateLimitItemPerMinute, RateLimitItemPerSecond
from limits.storage import (
MemoryStorage, RedisStorage, MemcachedStorage,
Storage, storage_from_string
Expand All @@ -30,7 +30,7 @@ def test_in_memory(self):
with hiro.Timeline().freeze() as timeline:
storage = MemoryStorage()
limiter = FixedWindowRateLimiter(storage)
per_min = PER_MINUTE(10)
per_min = RateLimitItemPerMinute(10)
for i in range(0,10):
self.assertTrue(limiter.hit(per_min))
self.assertFalse(limiter.hit(per_min))
Expand All @@ -41,21 +41,21 @@ def test_in_memory_expiry(self):
with hiro.Timeline().freeze() as timeline:
storage = MemoryStorage()
limiter = FixedWindowRateLimiter(storage)
per_min = PER_MINUTE(10)
per_min = RateLimitItemPerMinute(10)
for i in range(0,10):
self.assertTrue(limiter.hit(per_min))
timeline.forward(60)
# touch another key and yield
limiter.hit(PER_SECOND(1))
limiter.hit(RateLimitItemPerSecond(1))
time.sleep(0.1)
self.assertTrue(per_min.key_for() not in storage.storage)

def test_in_memory_expiry_moving_window(self):
with hiro.Timeline().freeze() as timeline:
storage = MemoryStorage()
limiter = MovingWindowRateLimiter(storage)
per_min = PER_MINUTE(10)
per_sec = PER_SECOND(1)
per_min = RateLimitItemPerMinute(10)
per_sec = RateLimitItemPerSecond(1)
for i in range(0,2):
for i in range(0,10):
self.assertTrue(limiter.hit(per_min))
Expand All @@ -68,7 +68,7 @@ def test_in_memory_expiry_moving_window(self):
def test_redis(self):
storage = RedisStorage("redis://localhost:6379")
limiter = FixedWindowRateLimiter(storage)
per_min = PER_SECOND(10)
per_min = RateLimitItemPerSecond(10)
start = time.time()
count = 0
while time.time() - start < 0.5 and count < 10:
Expand Down Expand Up @@ -121,7 +121,7 @@ def get_moving_window(self, *a, **k):
def test_memcached(self):
storage = MemcachedStorage("memcached://localhost:11211")
limiter = FixedWindowRateLimiter(storage)
per_min = PER_SECOND(10)
per_min = RateLimitItemPerSecond(10)
start = time.time()
count = 0
while time.time() - start < 0.5 and count < 10:
Expand All @@ -136,7 +136,7 @@ def test_memcached(self):
def test_large_dataset_redis_moving_window_expiry(self):
storage = RedisStorage("redis://localhost:6379")
limiter = MovingWindowRateLimiter(storage)
limit = PER_SECOND(1000)
limit = RateLimitItemPerSecond(1000)
keys_start = storage.storage.keys('%s/*' % limit.namespace)
# 100 routes
fake_routes = [uuid4().hex for _ in range(0,100)]
Expand Down

0 comments on commit 495f35d

Please sign in to comment.