From 71abeb8a842901bceaa7131e58739f5ffb29c169 Mon Sep 17 00:00:00 2001 From: "Ch.-David Blot" Date: Mon, 27 Oct 2025 13:39:12 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20ratelimit=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++++++++++++ osc_sdk_python/call.py | 24 ++++++++++++++++++++++-- osc_sdk_python/limiter.py | 30 ++++++++++++++++++++++++++++++ osc_sdk_python/outscale_gateway.py | 13 ++++++++++++- 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 osc_sdk_python/limiter.py diff --git a/README.md b/README.md index 18c085f..4e0423b 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,19 @@ Example: gw = Gateway(max_retries=5, retry_backoff_factor=0.5, retry_backoff_jitter=1.0, retry_backoff_max=120) ```` +## Rate Limit Options + +The following options can be provided when initializing the Gateway to customize the rate-limit behavior of the SDK. + +These options are: + - limiter_max_requests (integer, default 5) + - limiter_window (integer, default 1) + +Example: +```python +gw = Gateway(limiter_max_requests=20, limiter_window=5) +```` + # Example A simple example that prints all your Virtual Machine and Volume IDs. diff --git a/osc_sdk_python/call.py b/osc_sdk_python/call.py index 992bcfe..bc81fc7 100644 --- a/osc_sdk_python/call.py +++ b/osc_sdk_python/call.py @@ -9,12 +9,13 @@ class Call(object): - def __init__(self, logger=None, **kwargs): + def __init__(self, logger=None, limiter=None, **kwargs): self.version = kwargs.pop("version", "latest") self.host = kwargs.pop("host", None) self.ssl = kwargs.pop("_ssl", True) self.user_agent = kwargs.pop("user_agent", DEFAULT_USER_AGENT) self.logger = logger + self.limiter = limiter self.update_credentials( access_key=kwargs.pop("access_key", None), secret_key=kwargs.pop("secret_key", None), @@ -29,6 +30,10 @@ def __init__(self, logger=None, **kwargs): retry_backoff_jitter=kwargs.pop("retry_backoff_jitter", None), retry_backoff_max=kwargs.pop("retry_backoff_max", None) ) + self.update_limiter( + limiter_max_requests=kwargs.pop("limiter_max_requests", None), + limiter_window=kwargs.pop("limiter_window", None) + ) def update_credentials( self, @@ -59,6 +64,18 @@ def update_credentials( "retry_backoff_max": retry_backoff_max, } + def update_limiter( + self, + limiter_window=None, + limiter_max_requests=None, + ): + if limiter_window is not None: + self.limiter.window = limiter_window + + if limiter_max_requests is not None: + self.limiter.max_requests = limiter_max_requests + + def api(self, action, **data): try: credentials = Credentials(**self.credentials) @@ -78,6 +95,9 @@ def api(self, action, **data): else: endpoint = "{}{}".format(endpoint, uri) + if self.limiter is not None: + self.limiter.acquire() + requester = Requester( Authentication(credentials, host, user_agent=self.user_agent), endpoint, @@ -86,7 +106,7 @@ def api(self, action, **data): credentials.retry_backoff_jitter, credentials.retry_backoff_max, ) - if self.logger != None: + if self.logger is not None: self.logger.do_log( "uri: " + uri + "\npayload:\n" + json.dumps(data, indent=2) ) diff --git a/osc_sdk_python/limiter.py b/osc_sdk_python/limiter.py new file mode 100644 index 0000000..cd90bf2 --- /dev/null +++ b/osc_sdk_python/limiter.py @@ -0,0 +1,30 @@ +from datetime import datetime, timezone, timedelta +import time + + +class RateLimiter: + def __init__(self, window: int, max_requests: int): + self.window = window + self.max_requests = max_requests + self.requests = [] + + def acquire(self): + now = datetime.now(timezone.utc) + + self.clean_old_requests(now) + + if len(self.requests) >= self.max_requests: + oldest = self.requests[0] + wait_time = self.window - (now - oldest) + time.sleep(wait_time.total_seconds()) + + now = datetime.now(timezone.utc) + self.clean_old_requests(now) + + self.requests.append(now) + + def clean_old_requests(self, now): + while len(self.requests) > 0 and self.requests[0] <= now - timedelta( + seconds=self.window + ): + self.requests.pop(0) diff --git a/osc_sdk_python/outscale_gateway.py b/osc_sdk_python/outscale_gateway.py index 43931d6..fd33a6d 100644 --- a/osc_sdk_python/outscale_gateway.py +++ b/osc_sdk_python/outscale_gateway.py @@ -2,6 +2,7 @@ import sys from .call import Call from .credentials import Credentials +from .limiter import RateLimiter import ruamel.yaml from osc_sdk_python import __version__ @@ -17,6 +18,10 @@ LOG_ALL = 0 LOG_KEEP_ONLY_LAST_REQ = 1 +# Default +DEFAULT_LIMITER_WINDOW = 1 # 1 second +DEFAULT_LIMITER_MAX_REQUESTS = 5 # 5 requests / sec + class ActionNotExists(NotImplementedError): pass @@ -68,7 +73,13 @@ def __init__(self, **kwargs): self._load_gateway_structure() self._load_errors() self.log = Logger() - self.call = Call(logger=self.log, version=self.endpoint_api_version, **kwargs) + self.limiter = RateLimiter(DEFAULT_LIMITER_WINDOW, DEFAULT_LIMITER_MAX_REQUESTS) + self.call = Call( + logger=self.log, + version=self.endpoint_api_version, + limiter=self.limiter, + **kwargs, + ) def update_credentials(self, **kwargs): """