Skip to content

Commit 41de010

Browse files
committed
Created root downloads shield update scripts
1 parent bc81f3a commit 41de010

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Update Downloads shield in root README
2+
3+
on:
4+
schedule:
5+
- cron: "45 35 * * 5" # every Fri @ 5:45 AM
6+
workflow_dispatch:
7+
8+
jobs:
9+
update-root-downloads-shield:
10+
runs-on: ubuntu-24.04
11+
permissions:
12+
contents: write
13+
env:
14+
TZ: PST8PDT
15+
16+
steps:
17+
18+
- name: Checkout adamlui/python-utils
19+
uses: actions/checkout@v6.0.2
20+
with:
21+
token: ${{ secrets.REPO_SYNC_PAT }}
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@6.2.0
25+
with:
26+
python-version: 3.14
27+
28+
- name: Update README shield
29+
id: update_shield
30+
run: |
31+
python3 utils/update_root_downloads_shield.py
32+
if [[ $? -eq 0 ]] ; then
33+
echo "Shield updated!"
34+
echo "SHIELDS_UPDATED=true" >> $GITHUB_ENV
35+
else
36+
echo "No update needed."
37+
echo "SHIELDS_UPDATED=false" >> $GITHUB_ENV
38+
fi
39+
40+
- name: Push changes
41+
if: env.SHIELDS_UPDATED == 'true'
42+
env:
43+
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
44+
GPG_PRIVATE_ID: ${{ secrets.GPG_PRIVATE_ID }}
45+
run: |
46+
gpg --batch --import <(echo "$GPG_PRIVATE_KEY")
47+
git config --global commit.gpgsign true
48+
git config --global user.name "kudo-sync-bot"
49+
git config --global user.email "auto-sync@kudoai.com"
50+
git config --global user.signingkey "$GPG_PRIVATE_ID"
51+
git add docs/README.md
52+
git commit -m -n "Updated Downloads shield counter in root README"
53+
git push
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import time
2+
from typing import List
3+
4+
PKGS = [
5+
'computer-languages',
6+
'data-languages',
7+
'find-project-root',
8+
'get-min-py',
9+
'is-legacy-terminal',
10+
'is-unicode-supported',
11+
'latin-locales',
12+
'markup-languages',
13+
'non-latin-locales',
14+
'programming-languages',
15+
'project-markers',
16+
'prose-languages',
17+
'remove-json-keys',
18+
'translate-messages'
19+
]
20+
STATS_API_URL = 'https://pypistats.org/api/packages/{pkg}/overall'
21+
22+
def format_total(num: int) -> str: # abbr ints to e.g. 1.5k, 2b
23+
return f'{num / 1000000000:.1f}B' if num >= 1000000000 \
24+
else f'{num / 1000000:.1f}M' if num >= 1000000 \
25+
else f'{num / 1000:.1f}K' if num >= 1000 \
26+
else str(num)
27+
28+
def get_downloads(pkg: str, max_retries: int = 5, get_delay: int = 2) -> int:
29+
import json
30+
from urllib.request import urlopen
31+
from urllib.error import HTTPError
32+
url = STATS_API_URL.format(pkg=pkg)
33+
for idx in range(max_retries):
34+
try:
35+
with urlopen(url) as resp:
36+
return sum(item['downloads'] for item in json.load(resp)['data'])
37+
except HTTPError as err:
38+
if err.code == 429: # Rate limited
39+
retry_delay = (idx +1) *2 # Exponential backoff
40+
print(f'{pkg}: Rate limited. Retrying in {retry_delay}s...')
41+
time.sleep(retry_delay)
42+
else:
43+
print(f'{pkg}: ERROR ({err.code})')
44+
return 0
45+
except Exception as err:
46+
print(f'{pkg}: Exception: {err}')
47+
time.sleep(get_delay)
48+
print(f'{pkg}: Failed after {max_retries} retries')
49+
return 0
50+
51+
def read_file(file_path: str) -> List[str]:
52+
with open(file_path, 'r', encoding='utf-8') as file:
53+
return file.readlines()
54+
55+
def write_file(file_path: str, lines: List[str]) -> None:
56+
with open(file_path, 'w', encoding='utf-8') as file:
57+
file.writelines(lines)
58+
59+
def update_downloads_shield(readme_path: str, downloads: int) -> None:
60+
import re
61+
lines = read_file(readme_path)
62+
shield_re = r'(<img[^>]+src="https://img.shields.io/badge/Downloads-)([\d\.kM]+)(-[a-f0-9]{6})'
63+
formatted_downloads = format_total(downloads)
64+
downloads_str = f'{formatted_downloads.lower()}'
65+
for idx, line in enumerate(lines):
66+
shield_match = re.search(shield_re, line)
67+
if shield_match:
68+
new_line = re.sub(shield_match.group(2), downloads_str, line)
69+
lines[idx] = new_line
70+
print(f'>>> {new_line.strip()}')
71+
write_file(readme_path, lines)
72+
73+
def main() -> None:
74+
grand_total_dls = 0
75+
for pkg in PKGS: # get downloads
76+
downloads = get_downloads(pkg)
77+
grand_total_dls += downloads
78+
print(f'{pkg:30} {downloads:,}')
79+
time.sleep(1)
80+
print('-' *45)
81+
print(f"{'TOTAL DOWNLOADS':20} {grand_total_dls:,}\n")
82+
README_PATH = 'docs/README.md'
83+
print(f'Updating {README_PATH}...')
84+
update_downloads_shield(README_PATH, grand_total_dls)
85+
print('Done!')
86+
87+
if __name__ == '__main__' : main()

0 commit comments

Comments
 (0)