diff --git a/.vscode/settings.json b/.vscode/settings.json index 8afc6eb..a5c5f48 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,13 @@ { "cSpell.words": [ "ftnfurina", + "levelname", + "pycache", "pytest", - "pycache" + "setattr" ], "cSpell.ignorePaths": [ ".*", "*.lock" ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index bf71c2a..644571d 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,12 @@ from requests import Response from rate_keeper import RateKeeper -timestamp_clock = datetime.now(timezone.utc).timestamp + +# UTC timestamp clock +def timestamp_clock(): + return datetime.now(timezone.utc).timestamp() + + rate_keeper = RateKeeper(limit=5000, period=3600, clock=timestamp_clock) @@ -65,15 +70,15 @@ def fetch( response = requests.request(method, url, headers=headers, params=params) headers_map = { - "x-ratelimit-limit": rate_keeper.update_limit, - "x-ratelimit-used": rate_keeper.update_used, - "x-ratelimit-reset": rate_keeper.update_reset, + "x-ratelimit-limit": lambda x: setattr(rate_keeper, "limit", int(x)), + "x-ratelimit-used": lambda x: setattr(rate_keeper, "used", int(x)), + "x-ratelimit-reset": lambda x: setattr(rate_keeper, "reset", float(x)), } for key, value in response.headers.items(): lower_key = key.lower() if lower_key in headers_map: - headers_map[lower_key](int(value)) + headers_map[lower_key](value) return response diff --git a/README_ZH.md b/README_ZH.md index 533f7a3..a4b4608 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -53,7 +53,12 @@ from requests import Response from rate_keeper import RateKeeper -timestamp_clock = datetime.now(timezone.utc).timestamp + +# UTC timestamp clock +def timestamp_clock(): + return datetime.now(timezone.utc).timestamp() + + rate_keeper = RateKeeper(limit=5000, period=3600, clock=timestamp_clock) @@ -65,15 +70,15 @@ def fetch( response = requests.request(method, url, headers=headers, params=params) headers_map = { - "x-ratelimit-limit": rate_keeper.update_limit, - "x-ratelimit-used": rate_keeper.update_used, - "x-ratelimit-reset": rate_keeper.update_reset, + "x-ratelimit-limit": lambda x: setattr(rate_keeper, "limit", int(x)), + "x-ratelimit-used": lambda x: setattr(rate_keeper, "used", int(x)), + "x-ratelimit-reset": lambda x: setattr(rate_keeper, "reset", float(x)), } for key, value in response.headers.items(): lower_key = key.lower() if lower_key in headers_map: - headers_map[lower_key](int(value)) + headers_map[lower_key](value) return response diff --git a/examples/github_api.py b/examples/github_api.py index 249bbcb..b9da2c5 100644 --- a/examples/github_api.py +++ b/examples/github_api.py @@ -6,7 +6,12 @@ from rate_keeper import RateKeeper -timestamp_clock = datetime.now(timezone.utc).timestamp + +# UTC timestamp clock +def timestamp_clock(): + return datetime.now(timezone.utc).timestamp() + + rate_keeper = RateKeeper(limit=5000, period=3600, clock=timestamp_clock) @@ -18,15 +23,15 @@ def fetch( response = requests.request(method, url, headers=headers, params=params) headers_map = { - "x-ratelimit-limit": rate_keeper.update_limit, - "x-ratelimit-used": rate_keeper.update_used, - "x-ratelimit-reset": rate_keeper.update_reset, + "x-ratelimit-limit": lambda x: setattr(rate_keeper, "limit", int(x)), + "x-ratelimit-used": lambda x: setattr(rate_keeper, "used", int(x)), + "x-ratelimit-reset": lambda x: setattr(rate_keeper, "reset", float(x)), } for key, value in response.headers.items(): lower_key = key.lower() if lower_key in headers_map: - headers_map[lower_key](int(value)) + headers_map[lower_key](value) return response diff --git a/examples/use_logging.py b/examples/use_logging.py new file mode 100644 index 0000000..2d78df4 --- /dev/null +++ b/examples/use_logging.py @@ -0,0 +1,24 @@ +import logging + +from rate_keeper import RateKeeper + +# from rate_keeper import LOGGER_NAME + +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s %(levelname)-6s : %(message)s" +) + +logger = logging.getLogger(__name__) +rate_keeper = RateKeeper(limit=3, period=1) + +# rate_keeper_logger = logging.getLogger(LOGGER_NAME) +# rate_keeper_logger.setLevel(logging.INFO) + + +@rate_keeper.decorator +def logging_msg(msg: str) -> None: + logger.info(msg) + + +if __name__ == "__main__": + [logging_msg(f"msg {i}") for i in range(10)] diff --git a/pyproject.toml b/pyproject.toml index 264912e..ad9dcb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "rate-keeper" -version = "0.2.0" -description = "Add your description here" +version = "0.4.1" +description = "Used to limit function call frequency. It ensures your function is called evenly within the limit rather than being called intensively in a short time." authors = [{ name = "ftnfurina", email = "ftnfurina@gmail.com" }] dependencies = ["requests>=2.32.3"] readme = "README.md" diff --git a/src/rate_keeper/__init__.py b/src/rate_keeper/__init__.py index bb198b0..b8cd9ee 100644 --- a/src/rate_keeper/__init__.py +++ b/src/rate_keeper/__init__.py @@ -1,6 +1,7 @@ -from .rate_keeper import RateKeeper, clock +from .rate_keeper import LOGGER_NAME, RateKeeper, clock __all__ = [ "RateKeeper", "clock", + "LOGGER_NAME", ] diff --git a/src/rate_keeper/rate_keeper.py b/src/rate_keeper/rate_keeper.py index 4ab95b3..a9ac8dc 100644 --- a/src/rate_keeper/rate_keeper.py +++ b/src/rate_keeper/rate_keeper.py @@ -1,9 +1,14 @@ +import logging import sys import threading import time from functools import wraps from typing import Callable +LOGGER_NAME = "rate_keeper" + +logger = logging.getLogger(LOGGER_NAME) + # Prefer time.monotonic if available, otherwise fall back to time.time clock = time.monotonic if hasattr(time, "monotonic") else time.time @@ -58,6 +63,8 @@ def __init__( self._lock = threading.RLock() + logger.info(f"RateKeeper initialized: {self}") + @property def limit(self) -> int: """ @@ -67,6 +74,18 @@ def limit(self) -> int: """ return self._limit + @limit.setter + @_synchronized + def limit(self, limit: int) -> None: + """ + Set call limit. + + :param limit: Call limit + """ + old_limit = self._limit + self._limit = max(1, min(sys.maxsize, limit)) + logger.debug(f"RateKeeper 'limit' updated: {old_limit} -> {self._limit}") + @property def period(self) -> int: """ @@ -76,6 +95,18 @@ def period(self) -> int: """ return self._period + @period.setter + @_synchronized + def period(self, period: int) -> None: + """ + Set limit period. + + :param period: New limit period (seconds) + """ + old_period = self._period + self._period = max(1, period) + logger.debug(f"RateKeeper 'period' updated: {old_period} -> {self._period}") + @property def used(self) -> int: """ @@ -85,6 +116,18 @@ def used(self) -> int: """ return self._used + @used.setter + @_synchronized + def used(self, used: int) -> None: + """ + Set used call count. + + :param used: New used call count + """ + old_used = self._used + self._used = max(0, min(self._limit, used)) + logger.debug(f"RateKeeper 'used' updated: {old_used} -> {self._used}") + @property def reset(self) -> float: """ @@ -94,6 +137,18 @@ def reset(self) -> float: """ return self._reset + @reset.setter + @_synchronized + def reset(self, reset: float) -> None: + """ + Set counter reset time. + + :param reset: New reset time (seconds) + """ + old_reset = self._reset + self._reset = max(self.clock(), reset) + logger.debug(f"RateKeeper 'reset' updated: {old_reset} -> {self._reset}") + @property def delay_time(self) -> float: """ @@ -103,41 +158,29 @@ def delay_time(self) -> float: """ return self._delay_time - @_synchronized def update_limit(self, limit: int) -> None: """ - Update call limit. - - :param limit: New call limit + Deprecated. Use 'limit' property instead. """ - self._limit = max(1, min(sys.maxsize, limit)) + self.limit = limit - @_synchronized def update_period(self, period: int) -> None: """ - Update limit period. - - :param period: New limit period (seconds) + Deprecated. Use 'period' property instead. """ - self._period = max(1, period) + self.period = period - @_synchronized def update_used(self, used: int) -> None: """ - Update used call count. - - :param used: New used call count + Deprecated. Use 'used' property instead. """ - self._used = max(0, min(self._limit, used)) + self.used = used - @_synchronized def update_reset(self, reset: float) -> None: """ - Update counter reset time. - - :param reset: New reset time (seconds) + Deprecated. Use 'reset' property instead. """ - self._reset = max(self.clock(), reset) + self.reset = reset @property def remaining_period(self) -> float: @@ -192,15 +235,22 @@ def wrapper(*args, **kwargs): self._delay_time = self.recommend_delay if self.auto_sleep: if self._delay_time > 0: + logger.info(f"Auto sleeping for {self._delay_time:.2f} seconds") time.sleep(self._delay_time) # Reset counter if self.remaining_period == 0: + old_reset = self._reset self._used = 0 self._reset = self.clock() + self._period + logger.debug( + f"RateKeeper counter reset: {old_reset:.2f} -> {self._reset:.2f}" + ) self._used += 1 - + logger.debug( + f"Calling function {func.__name__}. Current used: {self._used}/{self._limit}" + ) return func(*args, **kwargs) return wrapper diff --git a/tests/test_rate_keeper.py b/tests/test_rate_keeper.py index 580283e..f7d9138 100644 --- a/tests/test_rate_keeper.py +++ b/tests/test_rate_keeper.py @@ -65,42 +65,42 @@ def test_delay_time(): def test_update_limit(): rate_keeper = RateKeeper(limit=2, period=1, auto_sleep=False) - assert rate_keeper._limit == 2, "limit should be 2" + assert rate_keeper.limit == 2, "limit should be 2" - rate_keeper.update_limit(3) - assert rate_keeper._limit == 3, "limit should be 3" + rate_keeper.limit = 3 + assert rate_keeper.limit == 3, "limit should be 3" - rate_keeper.update_limit(-1) - assert rate_keeper._limit == 1, "limit should be 1" + rate_keeper.limit = -1 + assert rate_keeper.limit == 1, "limit should be 1" - rate_keeper.update_limit(sys.maxsize) - assert rate_keeper._limit == sys.maxsize, "limit should be sys.maxsize" + rate_keeper.limit = sys.maxsize + assert rate_keeper.limit == sys.maxsize, "limit should be sys.maxsize" def test_update_period(): rate_keeper = RateKeeper(limit=2, period=1, auto_sleep=False) - assert rate_keeper._period == 1, "period should be 1" + assert rate_keeper.period == 1, "period should be 1" - rate_keeper.update_period(2) - assert rate_keeper._period == 2, "period should be 2" + rate_keeper.period = 2 + assert rate_keeper.period == 2, "period should be 2" - rate_keeper.update_period(-1) - assert rate_keeper._period == 1, "period should be 1" + rate_keeper.period = -1 + assert rate_keeper.period == 1, "period should be 1" def test_update_used(): rate_keeper = RateKeeper(limit=2, period=1, auto_sleep=False) - assert rate_keeper._used == 0, "used should be 0" + assert rate_keeper.used == 0, "used should be 0" - rate_keeper.update_used(1) - assert rate_keeper._used == 1, "used should be 1" + rate_keeper.used = 1 + assert rate_keeper.used == 1, "used should be 1" - rate_keeper.update_used(3) - assert rate_keeper._used == 2, "used should less than or equal to limit" + rate_keeper.used = 3 + assert rate_keeper.used == 2, "used should less than or equal to limit" - rate_keeper.update_used(-1) - assert rate_keeper._used == 0, "used should be 0" + rate_keeper.used = -1 + assert rate_keeper.used == 0, "used should be 0" def test_update_reset(): @@ -112,10 +112,10 @@ def test_update_reset(): reset = clock() + 100 - rate_keeper.update_reset(reset) + rate_keeper.reset = reset assert rate_keeper.reset == reset, "reset should be next_reset" - rate_keeper.update_reset(clock() - 100) + rate_keeper.reset = clock() - 100 assert rate_keeper.reset <= clock(), "reset should be greater than clock" @@ -136,21 +136,21 @@ def test_func(param: str) -> str: def test_minimum_calls_and_period(): rate_keeper = RateKeeper(limit=0, period=0) - assert rate_keeper._limit == 1, "calls should be 1" - assert rate_keeper._period == 1, "period should be 1" + assert rate_keeper.limit == 1, "calls should be 1" + assert rate_keeper.period == 1, "period should be 1" def test_maximum_calls(): rate_keeper = RateKeeper(limit=sys.maxsize) - assert rate_keeper._limit == sys.maxsize, "calls should be sys.maxsize" + assert rate_keeper.limit == sys.maxsize, "calls should be sys.maxsize" def test_negative_calls_and_period(): rate_keeper = RateKeeper(limit=-1, period=-1) - assert rate_keeper._limit == 1, "calls should be 1" - assert rate_keeper._period == 1, "period should be 1" + assert rate_keeper.limit == 1, "calls should be 1" + assert rate_keeper.period == 1, "period should be 1" def test_thread_safety():