2
2
from datetime import UTC , date , datetime
3
3
4
4
import frontmatter
5
+ from aiohttp import ClientResponse , ClientResponseError
6
+ from tenacity import retry , retry_if_exception_type , stop_after_attempt , wait_exponential
5
7
6
8
from bot .bot import Bot
7
9
from bot .constants import Keys
@@ -71,6 +73,35 @@ def __str__(self) -> str:
71
73
return f"<Event at '{ self .path } '>"
72
74
73
75
76
+ class GitHubServerError (Exception ):
77
+ """
78
+ GitHub responded with 5xx status code.
79
+
80
+ Such error shall be retried.
81
+ """
82
+
83
+
84
+ def _raise_for_status (resp : ClientResponse ) -> None :
85
+ """Raise custom error if resp status is 5xx."""
86
+ # Use the response's raise_for_status so that we can
87
+ # attach the full traceback to our custom error.
88
+ log .trace (f"GitHub response status: { resp .status } " )
89
+ try :
90
+ resp .raise_for_status ()
91
+ except ClientResponseError as err :
92
+ if resp .status >= 500 :
93
+ raise GitHubServerError from err
94
+ raise
95
+
96
+
97
+ _retry_fetch = retry (
98
+ retry = retry_if_exception_type (GitHubServerError ), # Only retry this error.
99
+ stop = stop_after_attempt (5 ), # Up to 5 attempts.
100
+ wait = wait_exponential (), # Exponential backoff: 1, 2, 4, 8 seconds.
101
+ reraise = True , # After final failure, re-raise original exception.
102
+ )
103
+
104
+
74
105
class BrandingRepository :
75
106
"""
76
107
Branding repository abstraction.
@@ -93,6 +124,7 @@ class BrandingRepository:
93
124
def __init__ (self , bot : Bot ) -> None :
94
125
self .bot = bot
95
126
127
+ @_retry_fetch
96
128
async def fetch_directory (self , path : str , types : t .Container [str ] = ("file" , "dir" )) -> dict [str , RemoteObject ]:
97
129
"""
98
130
Fetch directory found at `path` in the branding repository.
@@ -105,14 +137,12 @@ async def fetch_directory(self, path: str, types: t.Container[str] = ("file", "d
105
137
log .debug (f"Fetching directory from branding repository: '{ full_url } '." )
106
138
107
139
async with self .bot .http_session .get (full_url , params = PARAMS , headers = HEADERS ) as response :
108
- if response .status != 200 :
109
- raise RuntimeError (f"Failed to fetch directory due to status: { response .status } " )
110
-
111
- log .debug ("Fetch successful, reading JSON response." )
140
+ _raise_for_status (response )
112
141
json_directory = await response .json ()
113
142
114
143
return {file ["name" ]: RemoteObject (file ) for file in json_directory if file ["type" ] in types }
115
144
145
+ @_retry_fetch
116
146
async def fetch_file (self , download_url : str ) -> bytes :
117
147
"""
118
148
Fetch file as bytes from `download_url`.
@@ -122,10 +152,7 @@ async def fetch_file(self, download_url: str) -> bytes:
122
152
log .debug (f"Fetching file from branding repository: '{ download_url } '." )
123
153
124
154
async with self .bot .http_session .get (download_url , params = PARAMS , headers = HEADERS ) as response :
125
- if response .status != 200 :
126
- raise RuntimeError (f"Failed to fetch file due to status: { response .status } " )
127
-
128
- log .debug ("Fetch successful, reading payload." )
155
+ _raise_for_status (response )
129
156
return await response .read ()
130
157
131
158
def parse_meta_file (self , raw_file : bytes ) -> MetaFile :
0 commit comments