Skip to content

Commit 4d73aad

Browse files
committed
refactor: Move the duplicated retry classifier out of the cogs into a shared utility #50
1 parent b5fc5b2 commit 4d73aad

6 files changed

Lines changed: 42 additions & 56 deletions

File tree

bot/exts/filtering/filtering.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from bot.utils.channel import is_mod_channel
5757
from bot.utils.lock import lock_arg
5858
from bot.utils.message_cache import MessageCache
59+
from bot.utils.retry import is_retryable_api_error
5960

6061
log = get_logger(__name__)
6162

@@ -114,7 +115,7 @@ async def cog_load(self) -> None:
114115
raw_filter_lists = await self.bot.api_client.get("bot/filter/filter_lists")
115116
break
116117
except Exception as error:
117-
is_retryable = self._retryable_filter_load_error(error)
118+
is_retryable = is_retryable_api_error(error)
118119
is_last_attempt = attempt == URLs.connect_max_retries
119120

120121
if not is_retryable:
@@ -147,14 +148,6 @@ async def cog_load(self) -> None:
147148
await self.schedule_offending_messages_deletion()
148149
self.weekly_auto_infraction_report_task.start()
149150

150-
@staticmethod
151-
def _retryable_filter_load_error(error: Exception) -> bool:
152-
"""Return whether loading filter lists failed due to some temporary error, thus retrying could help."""
153-
if isinstance(error, ResponseCodeError):
154-
return error.status in (408, 429) or error.status >= 500
155-
156-
return isinstance(error, (TimeoutError, OSError))
157-
158151
def subscribe(self, filter_list: FilterList, *events: Event) -> None:
159152
"""
160153
Subscribe a filter list to the given events.

bot/exts/info/python_news.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from bot.bot import Bot
1616
from bot.constants import URLs
1717
from bot.log import get_logger
18+
from bot.utils.retry import is_retryable_api_error
1819
from bot.utils.webhooks import send_webhook
1920

2021
PEPS_RSS_URL = "https://peps.python.org/peps.rss"
@@ -46,12 +47,6 @@ def __init__(self, bot: Bot):
4647
self.webhook: discord.Webhook | None = None
4748
self.seen_items: dict[str, set[str]] = {}
4849

49-
@staticmethod
50-
def _retryable_site_load_error(error: Exception) -> bool:
51-
if isinstance(error, ResponseCodeError):
52-
return error.status in (408, 429) or error.status >= 500
53-
return isinstance(error, (TimeoutError, OSError))
54-
5550
async def cog_load(self) -> None:
5651
"""Load all existing seen items from db and create any missing mailing lists."""
5752
for attempt in range(1, URLs.connect_max_retries + 1):
@@ -74,7 +69,7 @@ async def cog_load(self) -> None:
7469
return
7570

7671
except Exception as error:
77-
if not self._retryable_site_load_error(error):
72+
if not is_retryable_api_error(error):
7873
raise
7974

8075
if attempt == URLs.connect_max_retries:

bot/utils/retry.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pydis_core.site_api import ResponseCodeError
2+
3+
4+
def is_retryable_api_error(error: Exception) -> bool:
5+
"""Return whether an API error is temporary and worth retrying."""
6+
if isinstance(error, ResponseCodeError):
7+
return error.status in (408, 429) or error.status >= 500
8+
9+
return isinstance(error, (TimeoutError, OSError))

tests/bot/exts/filtering/test_filtering_cog.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import unittest
22
from unittest.mock import AsyncMock, MagicMock, patch
33

4-
from pydis_core.site_api import ResponseCodeError
5-
64
from bot.exts.filtering.filtering import Filtering
75

86

@@ -77,22 +75,3 @@ async def test_retries_three_times_fails_and_reraises(self):
7775
self.cog._fetch_or_generate_filtering_webhook.assert_not_awaited()
7876
self.cog.schedule_offending_messages_deletion.assert_not_awaited()
7977
self.mock_weekly_task_start.assert_not_called()
80-
81-
def test_retryable_filter_load_error(self):
82-
"""`_retryable_filter_load_error` should classify temporary failures as retryable."""
83-
test_cases = (
84-
(ResponseCodeError(MagicMock(status=408)), True),
85-
(ResponseCodeError(MagicMock(status=429)), True),
86-
(ResponseCodeError(MagicMock(status=500)), True),
87-
(ResponseCodeError(MagicMock(status=503)), True),
88-
(ResponseCodeError(MagicMock(status=400)), False),
89-
(ResponseCodeError(MagicMock(status=404)), False),
90-
(TimeoutError("timeout"), True),
91-
(OSError("os error"), True),
92-
(AttributeError("attr"), False),
93-
(ValueError("value"), False),
94-
)
95-
96-
for error, expected_retryable in test_cases:
97-
with self.subTest(error=error):
98-
self.assertEqual(self.cog._retryable_filter_load_error(error), expected_retryable)

tests/bot/exts/info/test_python_news.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -84,25 +84,6 @@ async def test_retries_max_times_fails_and_reraises(self):
8484
# Task should never start if load fails.
8585
self.mock_fetch_new_media_start.assert_not_called()
8686

87-
def test_retryable_python_news_load_error(self):
88-
"""`_retryable_site_load_error` should classify temporary failures as retryable."""
89-
test_cases = (
90-
(ResponseCodeError(MagicMock(status=408)), True),
91-
(ResponseCodeError(MagicMock(status=429)), True),
92-
(ResponseCodeError(MagicMock(status=500)), True),
93-
(ResponseCodeError(MagicMock(status=503)), True),
94-
(ResponseCodeError(MagicMock(status=400)), False),
95-
(ResponseCodeError(MagicMock(status=404)), False),
96-
(TimeoutError("timeout"), True),
97-
(OSError("os error"), True),
98-
(AttributeError("attr"), False),
99-
(ValueError("value"), False),
100-
)
101-
102-
for error, expected_retryable in test_cases:
103-
with self.subTest(error=error):
104-
self.assertEqual(self.cog._retryable_site_load_error(error), expected_retryable)
105-
10687
async def test_cog_load_does_not_retry_non_retryable_error(self):
10788
"""`cog_load` should not retry when the error is non-retryable."""
10889
# 404 should be considered non-retryable by your predicate.

tests/bot/utils/test_retry.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import unittest
2+
from unittest.mock import MagicMock
3+
4+
from pydis_core.site_api import ResponseCodeError
5+
6+
from bot.utils.retry import is_retryable_api_error
7+
8+
9+
class RetryTests(unittest.TestCase):
10+
"""Tests for retry classification helpers."""
11+
12+
def test_is_retryable_api_error(self):
13+
"""`is_retryable_api_error` should classify temporary failures as retryable."""
14+
test_cases = (
15+
(ResponseCodeError(MagicMock(status=408)), True),
16+
(ResponseCodeError(MagicMock(status=429)), True),
17+
(ResponseCodeError(MagicMock(status=500)), True),
18+
(ResponseCodeError(MagicMock(status=503)), True),
19+
(ResponseCodeError(MagicMock(status=400)), False),
20+
(ResponseCodeError(MagicMock(status=404)), False),
21+
(TimeoutError("timeout"), True),
22+
(OSError("os error"), True),
23+
(AttributeError("attr"), False),
24+
(ValueError("value"), False),
25+
)
26+
27+
for error, expected_retryable in test_cases:
28+
with self.subTest(error=error):
29+
self.assertEqual(is_retryable_api_error(error), expected_retryable)

0 commit comments

Comments
 (0)