omnata-plugin-runtime 0.3.27a80__py3-none-any.whl → 0.3.28a81__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omnata_plugin_runtime/omnata_plugin.py +20 -0
- omnata_plugin_runtime/rate_limiting.py +87 -2
- {omnata_plugin_runtime-0.3.27a80.dist-info → omnata_plugin_runtime-0.3.28a81.dist-info}/METADATA +2 -1
- {omnata_plugin_runtime-0.3.27a80.dist-info → omnata_plugin_runtime-0.3.28a81.dist-info}/RECORD +6 -6
- {omnata_plugin_runtime-0.3.27a80.dist-info → omnata_plugin_runtime-0.3.28a81.dist-info}/LICENSE +0 -0
- {omnata_plugin_runtime-0.3.27a80.dist-info → omnata_plugin_runtime-0.3.28a81.dist-info}/WHEEL +0 -0
@@ -77,6 +77,7 @@ from .rate_limiting import (
|
|
77
77
|
HttpMethodType,
|
78
78
|
InterruptedWhileWaitingException,
|
79
79
|
RateLimitState,
|
80
|
+
RateLimitedSession
|
80
81
|
)
|
81
82
|
|
82
83
|
logger = getLogger(__name__)
|
@@ -251,6 +252,25 @@ class SyncRequest(ABC):
|
|
251
252
|
f"thread_cancellation_token: {self._thread_cancellation_token.is_set()}"
|
252
253
|
)
|
253
254
|
|
255
|
+
def get_ratelimit_retrying_http_session(self,
|
256
|
+
max_retries: int = 5,
|
257
|
+
backoff_factor: int = 1,
|
258
|
+
statuses_to_include: List[int] = [429]
|
259
|
+
):
|
260
|
+
"""
|
261
|
+
Returns a requests.Session object which can respond to 429 responses by waiting and retrying.
|
262
|
+
Takes into account the run deadline and cancellation status.
|
263
|
+
This is an alternative which can be used when the target API does not publish specific rate limits, and instead just asks you to respond to 429s as they are sent.
|
264
|
+
"""
|
265
|
+
return RateLimitedSession(
|
266
|
+
run_deadline=self._run_deadline,
|
267
|
+
cancellation_token=self._thread_cancellation_token,
|
268
|
+
max_retries=max_retries,
|
269
|
+
backoff_factor=backoff_factor,
|
270
|
+
statuses_to_include=statuses_to_include
|
271
|
+
)
|
272
|
+
|
273
|
+
|
254
274
|
def __apply_results_worker(self, cancellation_token:threading.Event):
|
255
275
|
"""
|
256
276
|
Designed to be run in a thread, this method polls the results every 20 seconds and sends them back to Snowflake.
|
@@ -13,6 +13,11 @@ import requests
|
|
13
13
|
from pydantic import Field, root_validator
|
14
14
|
from pydantic.json import pydantic_encoder
|
15
15
|
from .configuration import SubscriptableBaseModel
|
16
|
+
import time
|
17
|
+
import pytz
|
18
|
+
import dateparser
|
19
|
+
from requests.adapters import HTTPAdapter
|
20
|
+
from urllib3.util.retry import Retry
|
16
21
|
|
17
22
|
logger = getLogger(__name__)
|
18
23
|
|
@@ -350,13 +355,93 @@ class RetryLaterException(Exception):
|
|
350
355
|
self.message = message
|
351
356
|
super().__init__(self.message)
|
352
357
|
|
358
|
+
class RateLimitedSession(requests.Session):
|
359
|
+
"""
|
360
|
+
Creates a requests session that will automatically handle rate limiting.
|
361
|
+
It will retry requests that return a 429 status code, and will wait until the rate limit is reset.
|
362
|
+
The thread_cancellation_token is observed when waiting, as well as the overall run deadline.
|
363
|
+
In case this is used across threads, the retry count will be tracked per request URL (minus query parameters). It will be cleared when the request is successful.
|
364
|
+
"""
|
365
|
+
def __init__(self, run_deadline:datetime.datetime, thread_cancellation_token:threading.Event, max_retries=5, backoff_factor=1, statuses_to_include:List[int] = [429]):
|
366
|
+
super().__init__()
|
367
|
+
self.max_retries = max_retries
|
368
|
+
self.backoff_factor = backoff_factor
|
369
|
+
self.retries:Dict[str,int] = {}
|
370
|
+
self._retries_lock = threading.Lock()
|
371
|
+
self.run_deadline = run_deadline
|
372
|
+
self.thread_cancellation_token = thread_cancellation_token
|
373
|
+
self.statuses_to_include = statuses_to_include
|
374
|
+
|
375
|
+
retry_strategy = Retry(
|
376
|
+
total=max_retries,
|
377
|
+
backoff_factor=backoff_factor,
|
378
|
+
status_forcelist=statuses_to_include,
|
379
|
+
method_whitelist=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"]
|
380
|
+
)
|
381
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
382
|
+
self.mount("https://", adapter)
|
383
|
+
self.mount("http://", adapter)
|
384
|
+
|
385
|
+
def set_retries(self, url:str, retries:int):
|
386
|
+
# ensure that the query parameters are not included in the key
|
387
|
+
url = re.sub(r'\?.*$', '', url)
|
388
|
+
with self._retries_lock:
|
389
|
+
self.retries[url] = retries
|
390
|
+
|
391
|
+
def get_retries(self, url:str):
|
392
|
+
# ensure that the query parameters are not included in the key
|
393
|
+
url = re.sub(r'\?.*$', '', url)
|
394
|
+
with self._retries_lock:
|
395
|
+
return self.retries.get(url,0)
|
396
|
+
|
397
|
+
def increment_retries(self, url:str) -> int:
|
398
|
+
# ensure that the query parameters are not included in the key
|
399
|
+
url = re.sub(r'\?.*$', '', url)
|
400
|
+
with self._retries_lock:
|
401
|
+
self.retries[url] = self.retries.get(url,0) + 1
|
402
|
+
return self.retries[url]
|
403
|
+
|
404
|
+
def request(self, method, url, **kwargs):
|
405
|
+
while True:
|
406
|
+
response = super().request(method, url, **kwargs)
|
407
|
+
|
408
|
+
if response.status_code in self.statuses_to_include:
|
409
|
+
if 'Retry-After' in response.headers:
|
410
|
+
retry_after = response.headers['Retry-After']
|
411
|
+
if retry_after.isdigit():
|
412
|
+
wait_time = int(retry_after)
|
413
|
+
else:
|
414
|
+
retry_datetime = dateparser.parse(retry_after)
|
415
|
+
retry_datetime = retry_datetime.replace(tzinfo=pytz.UTC)
|
416
|
+
current_datetime = datetime.datetime.now(pytz.UTC)
|
417
|
+
wait_time = (retry_datetime - current_datetime).total_seconds()
|
418
|
+
# first check that the wait time is not longer than the run deadline
|
419
|
+
if datetime.datetime.now(pytz.UTC) + datetime.timedelta(seconds=wait_time) > self.run_deadline:
|
420
|
+
raise InterruptedWhileWaitingException(message=f"The rate limiting wait time ({wait_time} seconds) would exceed the run deadline")
|
421
|
+
logger.info(f"Waiting for {wait_time} seconds before retrying {method} request to {url}")
|
422
|
+
# if wait() returns true, it means that the thread was cancelled
|
423
|
+
if self.thread_cancellation_token.wait(wait_time):
|
424
|
+
raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting to expire")
|
425
|
+
else:
|
426
|
+
current_url_retries = self.increment_retries(url)
|
427
|
+
if current_url_retries >= self.max_retries:
|
428
|
+
raise requests.exceptions.RetryError(f"Maximum retries reached: {self.max_retries}")
|
429
|
+
backoff_time = self.backoff_factor * (2 ** (current_url_retries - 1))
|
430
|
+
if datetime.datetime.now(pytz.UTC) + datetime.timedelta(seconds=backoff_time) > self.run_deadline:
|
431
|
+
raise InterruptedWhileWaitingException(message=f"The rate limiting backoff time ({backoff_time} seconds) would exceed the run deadline")
|
432
|
+
logger.info(f"Waiting for {backoff_time} seconds before retrying {method} request to {url}")
|
433
|
+
if self.thread_cancellation_token.wait(backoff_time):
|
434
|
+
raise InterruptedWhileWaitingException(message="The sync was interrupted while waiting for rate limiting backoff")
|
435
|
+
else:
|
436
|
+
self.set_retries(url,0) # Reset retries if the request is successful
|
437
|
+
return response
|
438
|
+
|
353
439
|
|
354
440
|
class InterruptedWhileWaitingException(Exception):
|
355
441
|
"""
|
356
442
|
Indicates that while waiting for rate limiting to expire, the sync was interrupted
|
357
443
|
"""
|
358
|
-
def __init__(self):
|
359
|
-
message = "The sync was interrupted while waiting"
|
444
|
+
def __init__(self, message:str = "The sync was interrupted while waiting"):
|
360
445
|
self.message = message
|
361
446
|
super().__init__(self.message)
|
362
447
|
|
{omnata_plugin_runtime-0.3.27a80.dist-info → omnata_plugin_runtime-0.3.28a81.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: omnata-plugin-runtime
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.28a81
|
4
4
|
Summary: Classes and common runtime components for building and running Omnata Plugins
|
5
5
|
Author: James Weakley
|
6
6
|
Author-email: james.weakley@omnata.com
|
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
12
12
|
Requires-Dist: certifi (<=2023.11.17)
|
13
13
|
Requires-Dist: charset-normalizer (<=2.0.4)
|
14
|
+
Requires-Dist: dateparser (<=1.1.8)
|
14
15
|
Requires-Dist: idna (<=3.4)
|
15
16
|
Requires-Dist: jinja2 (>=3.1.2)
|
16
17
|
Requires-Dist: markupsafe (<=2.1.3)
|
{omnata_plugin_runtime-0.3.27a80.dist-info → omnata_plugin_runtime-0.3.28a81.dist-info}/RECORD
RENAMED
@@ -3,10 +3,10 @@ omnata_plugin_runtime/api.py,sha256=_N5ok5LN7GDO4J9n3yduXp3tpjmhpySY__U2baiygrs,
|
|
3
3
|
omnata_plugin_runtime/configuration.py,sha256=7cMekoY8CeZAJHpASU6tCMidF55Hzfr7CD74jtebqIY,35742
|
4
4
|
omnata_plugin_runtime/forms.py,sha256=pw_aKVsXSz47EP8PFBI3VDwdSN5IjvZxp8JTjO1V130,18421
|
5
5
|
omnata_plugin_runtime/logging.py,sha256=bn7eKoNWvtuyTk7RTwBS9UARMtqkiICtgMtzq3KA2V0,3272
|
6
|
-
omnata_plugin_runtime/omnata_plugin.py,sha256=
|
6
|
+
omnata_plugin_runtime/omnata_plugin.py,sha256=Ptf0wPdgC1HPbfxIIzhwRWAXmH9kYQT4UXWhhuCCt2Q,105241
|
7
7
|
omnata_plugin_runtime/plugin_entrypoints.py,sha256=D2f0Qih7KTJNXIDYkA-E55RTEK_7Jtv9ySqspWxERVA,28272
|
8
|
-
omnata_plugin_runtime/rate_limiting.py,sha256=
|
9
|
-
omnata_plugin_runtime-0.3.
|
10
|
-
omnata_plugin_runtime-0.3.
|
11
|
-
omnata_plugin_runtime-0.3.
|
12
|
-
omnata_plugin_runtime-0.3.
|
8
|
+
omnata_plugin_runtime/rate_limiting.py,sha256=izUOQluYERhlerUbPNnpNLTWUfKRPHTyAb_Sx3xYVJk,21492
|
9
|
+
omnata_plugin_runtime-0.3.28a81.dist-info/LICENSE,sha256=IMF9i4xIpgCADf0U-V1cuf9HBmqWQd3qtI3FSuyW4zE,26526
|
10
|
+
omnata_plugin_runtime-0.3.28a81.dist-info/METADATA,sha256=IlzVqE-nKShgqEdr_iAfEdxMgogluchcxQsloc1GfLo,1640
|
11
|
+
omnata_plugin_runtime-0.3.28a81.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
12
|
+
omnata_plugin_runtime-0.3.28a81.dist-info/RECORD,,
|
{omnata_plugin_runtime-0.3.27a80.dist-info → omnata_plugin_runtime-0.3.28a81.dist-info}/LICENSE
RENAMED
File without changes
|
{omnata_plugin_runtime-0.3.27a80.dist-info → omnata_plugin_runtime-0.3.28a81.dist-info}/WHEEL
RENAMED
File without changes
|