mirror of https://github.com/zulip/zulip.git
159 lines
6.1 KiB
Python
Executable File
159 lines
6.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Fetch contributors data from Github using their API, convert it to structured
|
|
JSON data for the /team page contributors section.
|
|
"""
|
|
|
|
# check for the venv
|
|
from lib import sanity_check
|
|
sanity_check.check_venv(__file__)
|
|
|
|
from typing import Any, Dict, List, Optional, Union
|
|
from typing_extensions import TypedDict
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import argparse
|
|
from time import sleep
|
|
from datetime import date
|
|
|
|
import requests
|
|
import json
|
|
|
|
FIXTURE_FILE = os.path.join(os.path.dirname(__file__), '../zerver/tests/fixtures/authors.json')
|
|
duplicate_commits_file = os.path.join(os.path.dirname(__file__),
|
|
'../zerver/tests/fixtures/duplicate_commits.json')
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--max-retries', type=int, default=3,
|
|
help='Number of times to retry fetching data from Github')
|
|
# In Travis CI and development environment, we use test fixture to avoid
|
|
# fetching from Github constantly.
|
|
parser.add_argument('--use-fixture', action='store_true', default=False,
|
|
help='Use fixture data instead of fetching from Github')
|
|
parser.add_argument('--not-required', action='store_true', default=False,
|
|
help='Consider failures to reach GitHub nonfatal')
|
|
args = parser.parse_args()
|
|
|
|
|
|
ContributorsJSON = TypedDict('ContributorsJSON', {
|
|
'date': str,
|
|
'contrib': List[Dict[str, Union[str, int]]],
|
|
})
|
|
|
|
|
|
def fetch_contributors(repo_link: str) -> Optional[List[Dict[str, Dict[str, Any]]]]:
|
|
r = requests.get(repo_link, verify=os.environ.get('CUSTOM_CA_CERTIFICATES')) # type: requests.Response
|
|
return r.json() if r.status_code == 200 else None
|
|
|
|
def write_to_disk(json_data: ContributorsJSON, out_file: str) -> None:
|
|
with open(out_file, 'w') as f:
|
|
try:
|
|
f.write("{}\n".format(json.dumps(json_data, indent=2, sort_keys=True)))
|
|
except IOError as e:
|
|
print(e)
|
|
sys.exit(1)
|
|
|
|
|
|
def run_production() -> None:
|
|
"""
|
|
Get contributors data from Github and insert them into a temporary
|
|
dictionary. Retry fetching each repository if responded with non HTTP 200
|
|
status.
|
|
"""
|
|
|
|
# This dictionary should hold all repositories that should be included in
|
|
# the total count, including those that should *not* have tabs on the team
|
|
# page (e.g. if they are deprecated).
|
|
repositories = {
|
|
'server': 'https://api.github.com/repos/zulip/zulip/stats/contributors',
|
|
'desktop': 'https://api.github.com/repos/zulip/zulip-desktop/stats/contributors',
|
|
'mobile': 'https://api.github.com/repos/zulip/zulip-mobile/stats/contributors',
|
|
'python-zulip-api': 'https://api.github.com/repos/zulip/python-zulip-api/stats/contributors',
|
|
'zulip-js': 'https://api.github.com/repos/zulip/zulip-js/stats/contributors',
|
|
'zulipbot': 'https://api.github.com/repos/zulip/zulipbot/stats/contributors',
|
|
'terminal': 'https://api.github.com/repos/zulip/zulip-terminal/stats/contributors',
|
|
'zulip-ios-legacy': 'https://api.github.com/repos/zulip/zulip-ios-legacy/stats/contributors',
|
|
'zulip-android': 'https://api.github.com/repos/zulip/zulip-android/stats/contributors',
|
|
}
|
|
|
|
data = dict(date=str(date.today()), contrib=[]) # type: ContributorsJSON
|
|
contribs_list = {} # type: Dict[str, Dict[str, Union[str, int]]]
|
|
|
|
for _ in range(args.max_retries):
|
|
repos_done = []
|
|
for name, link in repositories.items():
|
|
contribs = fetch_contributors(link)
|
|
if contribs:
|
|
repos_done.append(name)
|
|
for contrib in contribs:
|
|
assert contrib is not None # TODO: To improve/clarify
|
|
|
|
author = contrib.get('author')
|
|
if author is None:
|
|
# This happens for users who've deleted their GitHub account.
|
|
continue
|
|
|
|
username = author.get('login')
|
|
assert username is not None # TODO: To improve/clarify
|
|
|
|
avatar = author.get('avatar_url')
|
|
assert avatar is not None # TODO: To improve/clarify
|
|
total = contrib.get('total')
|
|
assert total is not None # TODO: To improve/clarify
|
|
|
|
contrib_data = {
|
|
'avatar': avatar,
|
|
name: total,
|
|
}
|
|
if username in contribs_list:
|
|
contribs_list[username].update(contrib_data)
|
|
else:
|
|
contribs_list[username] = contrib_data
|
|
|
|
# remove duplicate contributions count
|
|
# find commits at the time of split and subtract from zulip-server
|
|
with open(duplicate_commits_file, 'r') as f:
|
|
duplicate_commits = json.load(f)
|
|
for committer in duplicate_commits:
|
|
if committer in contribs_list and contribs_list[committer].get('server'):
|
|
total_commits = contribs_list[committer]['server']
|
|
assert isinstance(total_commits, int)
|
|
duplicate_commits_count = duplicate_commits[committer]
|
|
original_commits = total_commits - duplicate_commits_count
|
|
contribs_list[committer]['server'] = original_commits
|
|
|
|
for repo in repos_done:
|
|
del repositories[repo]
|
|
|
|
if not repositories:
|
|
break
|
|
|
|
# Wait before retrying failed requests for Github to aggregate data.
|
|
sleep(2)
|
|
else:
|
|
print("ERROR: Failed fetching contributors data from Github.")
|
|
if not args.not_required:
|
|
sys.exit(1)
|
|
|
|
for contributor_name, contributor_data in contribs_list.items():
|
|
contributor_data['name'] = contributor_name
|
|
data['contrib'].append(contributor_data)
|
|
|
|
write_to_disk(data, 'static/generated/github-contributors.json')
|
|
|
|
|
|
def copy_fixture() -> None:
|
|
"""
|
|
Copy test fixture file from zerver/tests/fixtures. This is used to avoid
|
|
constantly fetching data from Github during testing.
|
|
"""
|
|
shutil.copyfile(FIXTURE_FILE, 'static/generated/github-contributors.json')
|
|
|
|
|
|
if args.use_fixture:
|
|
copy_fixture()
|
|
else:
|
|
run_production()
|