22from datetime import UTC , date , datetime
33
44import frontmatter
5+ from aiohttp import ClientResponse , ClientResponseError
6+ from tenacity import retry , retry_if_exception_type , stop_after_attempt , wait_exponential
57
68from bot .bot import Bot
79from bot .constants import Keys
@@ -71,6 +73,35 @@ def __str__(self) -> str:
7173 return f"<Event at '{ self .path } '>"
7274
7375
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+
74105class BrandingRepository :
75106 """
76107 Branding repository abstraction.
@@ -93,6 +124,7 @@ class BrandingRepository:
93124 def __init__ (self , bot : Bot ) -> None :
94125 self .bot = bot
95126
127+ @_retry_fetch
96128 async def fetch_directory (self , path : str , types : t .Container [str ] = ("file" , "dir" )) -> dict [str , RemoteObject ]:
97129 """
98130 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
105137 log .debug (f"Fetching directory from branding repository: '{ full_url } '." )
106138
107139 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 )
112141 json_directory = await response .json ()
113142
114143 return {file ["name" ]: RemoteObject (file ) for file in json_directory if file ["type" ] in types }
115144
145+ @_retry_fetch
116146 async def fetch_file (self , download_url : str ) -> bytes :
117147 """
118148 Fetch file as bytes from `download_url`.
@@ -122,10 +152,7 @@ async def fetch_file(self, download_url: str) -> bytes:
122152 log .debug (f"Fetching file from branding repository: '{ download_url } '." )
123153
124154 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 )
129156 return await response .read ()
130157
131158 def parse_meta_file (self , raw_file : bytes ) -> MetaFile :
0 commit comments