Skip to content

Commit c52eba1

Browse files
fetch-configlet: Honor rate limits and errors. (exercism#2186)
* fetch-configlet: Honor rate limits and errors. Closes exercism#2182. Also removed dependency on requests module. * Update fetch-configlet * Removed requests from .travis.yml * Update fetch-configlet: address feedback from PR Changed tarfile call Used tarfile as context manager Add global constants * Update fetch-configlet per PR feedback Added loop for download_and_extract Changed unused variables per style * fetch-configlet: fix error handling and tweaks * fetch-configlet: tabnanny Co-authored-by: Michael Morehouse <640167+yawpitch@users.noreply.github.com>
1 parent 8f06a36 commit c52eba1

2 files changed

Lines changed: 66 additions & 15 deletions

File tree

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ matrix:
1616
- env: JOB=HOUSEKEEPING
1717
python: 3.7
1818
install:
19-
- pip install requests
2019
- ./bin/fetch-configlet
2120
- git clone https://github.com/exercism/problem-specifications spec
2221
- pip install -r requirements-generator.txt

bin/fetch-configlet

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
#!/usr/bin/env python3.7
2+
from datetime import datetime, timezone
23
import io
3-
import tarfile
4+
import json
45
import platform
5-
import requests
66
import sys
7+
import tarfile
8+
import time
9+
from urllib.request import urlopen
10+
from urllib.error import HTTPError, URLError
711

812

13+
BACKOFF_TIME = 5 # in seconds
14+
MAX_RETRIES = 5
15+
916
def download_and_extract(url):
10-
resp = requests.get(url)
11-
print('extracting...')
12-
file = tarfile.open(fileobj=io.BytesIO(resp.content), mode='r:gz')
13-
file.extractall(path='bin/')
17+
try:
18+
with urlopen(url) as request:
19+
status = request.status
20+
response = request.read()
21+
except HTTPError as err:
22+
status = err.code
23+
response = err.reason
24+
except URLError as err:
25+
print(err.reason)
26+
time.sleep(BACKOFF_TIME)
27+
return False
28+
if status >= 400:
29+
print(f"Unexpected {status} status from download server.")
30+
time.sleep(BACKOFF_TIME)
31+
return False
32+
33+
print("extracting...")
34+
with tarfile.open(fileobj=io.BytesIO(response), mode="r:*") as file:
35+
file.extractall(path="bin/")
36+
return True
1437

1538

1639
def get_os():
@@ -23,27 +46,56 @@ def get_os():
2346

2447

2548
def get_arch():
26-
return '64bit' if sys.maxsize > 2**32 else '32bit'
49+
return "64bit" if sys.maxsize > 2 ** 32 else "32bit"
2750

2851

2952
def fetch_configlet():
30-
latest = "https://api.github.com/repos/exercism/configlet/releases/latest"
31-
resp = requests.get(latest)
32-
data = resp.json()
53+
try:
54+
with urlopen(
55+
"https://api.github.com/repos/exercism/configlet/releases/latest"
56+
) as request:
57+
status = request.status
58+
headers = dict(request.getheaders())
59+
response = request.read()
60+
except HTTPError as err:
61+
status = err.code
62+
headers = err.headers
63+
response = err.reason
64+
except URLError as err:
65+
print(err.reason)
66+
time.sleep(BACKOFF_TIME)
67+
return 1
68+
if status >= 500:
69+
print(f"Sleeping due to {status} response from API.")
70+
time.sleep(BACKOFF_TIME)
71+
return 1
72+
if status == 403:
73+
wait_until = datetime.fromtimestamp(
74+
int(headers["X-RateLimit-Reset"]), timezone.utc
75+
)
76+
delta = wait_until - datetime.now(timezone.utc)
77+
seconds = delta.total_seconds()
78+
wait = seconds + 5 if seconds > BACKOFF_TIME else BACKOFF_TIME
79+
print(f"Rate limited, sleeping {wait} seconds.")
80+
time.sleep(wait)
81+
return 1
82+
if status >= 400:
83+
sys.exit(f"Received unexpected {status} response from API.")
84+
data = json.loads(response.decode("utf-8"))
3385
version = data["tag_name"]
3486
machine_info = f"{get_os()}-{get_arch()}"
3587
name = f"configlet-{machine_info}.tgz"
3688
for asset in data["assets"]:
3789
if asset["name"] == name:
3890
print(f"Downloading configlet {version} for {machine_info}")
39-
download_and_extract(asset["browser_download_url"])
40-
return 0
91+
for _ in range(MAX_RETRIES):
92+
if download_and_extract(asset["browser_download_url"]):
93+
return 0
4194
return 1
4295

4396

4497
def main():
45-
max_retries = 5
46-
for i in range(max_retries):
98+
for _ in range(MAX_RETRIES):
4799
ret = fetch_configlet()
48100
if ret == 0:
49101
break

0 commit comments

Comments
 (0)