Skip to content

Commit

Permalink
feat(util): add retry decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
YogiLiu committed Aug 31, 2023
1 parent 02519ca commit 15a5a8a
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 76 deletions.
3 changes: 2 additions & 1 deletion podmaker/util/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__all__ = ['exit_signal', 'ExitSignalError']
__all__ = ['exit_signal', 'ExitSignalError', 'retry']

from podmaker.util.exit import ExitSignalError, exit_signal
from podmaker.util.retry import retry
52 changes: 52 additions & 0 deletions podmaker/util/retry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

import logging
import sys
import time
from datetime import timedelta
from typing import Callable, Tuple, Type, TypeVar

if sys.version_info < (3, 10):
from typing_extensions import ParamSpec
else:
from typing import ParamSpec


P = ParamSpec('P')
T = TypeVar('T')
_logger = logging.getLogger(__name__)


def retry(
cnt: int,
*,
wait: timedelta = timedelta(seconds=0),
catch: Type[Exception] | Tuple[Type[Exception], ...] = Exception,
logger: logging.Logger = _logger,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""
A decorator to retry the function when exception raised.
The function will be called at least once and at most cnt + 1 times.
:param cnt: retry count
:param wait: wait time between retries
:param catch: the exception to retry
:param logger: logger to log retry info
"""
if cnt <= 0:
raise ValueError('cnt must be positive')
wait_seconds = wait.total_seconds()

def deco(func: Callable[P, T]) -> Callable[P, T]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
for _ in range(cnt):
try:
return func(*args, **kwargs)
except catch:
logger.warning('retrying...')
if wait_seconds > 0:
logger.warning(f'wait {wait_seconds}s before retry')
time.sleep(wait_seconds)
return func(*args, **kwargs)
return wrapper
return deco
172 changes: 98 additions & 74 deletions poetry.lock

Large diffs are not rendered by default.

File renamed without changes.
2 changes: 1 addition & 1 deletion tests/provider/test_youtube.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from podmaker.config import OwnerConfig, SourceConfig
from podmaker.fetcher import YouTube
from podmaker.storage import ObjectInfo, Storage
from tests.util import network_available
from tests.helper import network_available

if sys.version_info >= (3, 11):
pass
Expand Down
Empty file added tests/util/__init__.py
Empty file.
35 changes: 35 additions & 0 deletions tests/util/test_retry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import unittest
from unittest import mock

from podmaker.util import retry


class TestRetry(unittest.TestCase):
def test_no_exception(self) -> None:
spy = mock.Mock(return_value=1)
func = retry(3)(spy)
self.assertEqual(1, func())
self.assertEqual(1, spy.call_count)

def test_retry_success(self) -> None:
spy = mock.Mock(side_effect=[Exception, 1])
func = retry(3)(spy)
self.assertEqual(1, func())
self.assertEqual(2, spy.call_count)

def test_retry_failed(self) -> None:
spy = mock.Mock(side_effect=Exception)
func = retry(3)(spy)
self.assertRaises(Exception, func)
self.assertEqual(4, spy.call_count)

def test_specify_exception(self) -> None:
spy = mock.Mock(side_effect=ValueError)
func = retry(3, catch=TypeError)(spy)
self.assertRaises(ValueError, func)
self.assertEqual(1, spy.call_count)

spy = mock.Mock(side_effect=ValueError)
func = retry(3, catch=ValueError)(spy)
self.assertRaises(ValueError, func)
self.assertEqual(4, spy.call_count)

0 comments on commit 15a5a8a

Please sign in to comment.