import os import subprocess import sys from datetime import datetime, timedelta import requests LIMIT_COMMITS_30_DAYS = 50 LIMIT_STARS = 200 LIMIT_NLOC = 100000 MARGIN = 0.1 CLONE_DIR = "~/temp/PICable" def get_stars_info(owner, repository, request_headers): stars_url = f"https://api.github.com/repos/{owner}/{repository}" stars_response = requests.get(stars_url, headers=request_headers) if stars_response.status_code != 200: return -1 stars = stars_response.json()["stargazers_count"] return stars def get_commits_last_30_days(owner, repository, request_headers): since = (datetime.now() - timedelta(days=30)).isoformat() commits_url = f"https://api.github.com/repos/{owner}/{repository}/commits?since={since}" commits_response = requests.get(commits_url, headers=request_headers) if commits_response.status_code != 200: return -1 if "link" in commits_response.headers: page_count = int( commits_response.headers["link"].split(", ")[-1].split("; ")[0].split("page=")[1].split(">")[0] ) last_commits_url = f"{commits_url}&page={page_count}" last_commits_response = requests.get(last_commits_url, headers=request_headers) if last_commits_response.status_code != 200: return -1 return (page_count - 1) * 30 + len(last_commits_response.json()) return len(commits_response.json()) def get_lines_of_code(owner, repository): repo_url = f"https://github.com/{owner}/{repository}.git" repository_clone_dir = os.path.expanduser(f"{CLONE_DIR}/{repository}") try: subprocess.run( ["git", "clone", repo_url, repository_clone_dir], check=True, stdout=sys.stdout, stderr=sys.stderr ) except: return -1 result = subprocess.run(["sloccount", repository_clone_dir], capture_output=True, text=True, check=True) for line in result.stdout.splitlines(): if "Total Physical Source Lines of Code (SLOC)" in line: loc = int(line.split()[8].replace(",", "")) break else: loc = 0 subprocess.run(["rm", "-rf", repository_clone_dir], check=True) return loc def is_valid_repository(owner, repository, request_headers): repository_url = f"https://api.github.com/repos/{owner}/{repository}" repository_response = requests.get(repository_url, headers=request_headers) return repository_response.status_code == 200 def PICable(owner, repository, token): margin_percentage = round(MARGIN * 100) request_headers = {"Authorization": f"token {token}"} picable_report = "" repository_url = f"https://www.github.com/{owner}/{repository}" if not is_valid_repository(owner, repository, request_headers): return f"Invalid repository: {owner}/{repository} :x:" stars = get_stars_info(owner, repository, request_headers) if stars == -1: return "Error fetching stars info. :x:" if stars >= LIMIT_STARS: picable_report += ( f"The repository has {stars} stars, which meets the requirement of {LIMIT_STARS}. :white_check_mark:\n" ) elif stars >= LIMIT_STARS * (1 - MARGIN): picable_report += f"The repository has {stars} stars, which is close to the requirement of {LIMIT_STARS}, considering a {margin_percentage}% margin. :warning:\n" else: picable_report += f"The repository has {stars} stars, which is way below the requirement of {LIMIT_STARS}, even if considering a {margin_percentage}% margin. :x:\n" commits_30_days = get_commits_last_30_days(owner, repository, request_headers) if commits_30_days == -1: return "Error fetching commit info. :x:" if commits_30_days >= LIMIT_COMMITS_30_DAYS: picable_report += f"The repository has {commits_30_days} commits on the default branch in the last 30 days, which meets the requirement of {LIMIT_COMMITS_30_DAYS}. :white_check_mark:\n" elif commits_30_days >= LIMIT_COMMITS_30_DAYS * (1 - MARGIN): picable_report += f"The repository has {commits_30_days} commits on the default branch in the last 30 days, which is close to the requirement of {LIMIT_COMMITS_30_DAYS}, considering a {margin_percentage}% margin. :warning:\n" else: picable_report += f"The repository has {commits_30_days} commits on the default branch in the last 30 days, which is way below the requirement of {LIMIT_COMMITS_30_DAYS}, even if considering a {margin_percentage}% margin. :x:\n" lines_of_code = get_lines_of_code(owner, repository) if lines_of_code == -1: return "Error fetching lines of code info. :x:" if lines_of_code >= LIMIT_NLOC: picable_report += f"The repository has {lines_of_code} lines of code, which meets the requirement of at least {LIMIT_NLOC}. :white_check_mark:\n" elif lines_of_code >= LIMIT_NLOC * (1 - MARGIN): picable_report += f"The repository has {lines_of_code} lines of code, which is close to the requirement of {LIMIT_NLOC}, considering a {margin_percentage}% margin. :warning:\n" else: picable_report += f"The repository has {lines_of_code} lines of code, which is way below the requirement of {LIMIT_NLOC}, even if considering a {margin_percentage}% margin. :x:\n" picable_report += "\n" if ( stars < LIMIT_STARS * (1 - MARGIN) or commits_30_days < LIMIT_COMMITS_30_DAYS * (1 - MARGIN) or lines_of_code < LIMIT_NLOC * (1 - MARGIN) ): picable_report += f"The repository {repository_url} is not PICable. :x:\nThere is at least one requirement which is more than {margin_percentage}% below the requirement.\n" elif all([stars >= LIMIT_STARS, commits_30_days >= LIMIT_COMMITS_30_DAYS, lines_of_code >= LIMIT_NLOC]): picable_report += f"The repository {repository_url} is PICable. :white_check_mark:\n" else: picable_report += f"The repository {repository_url} is almost PICable. :warning:\nThere is at least one requirement which is below the requirement, but by less than {margin_percentage}%. Consult with a professor before proceeding.\n" return picable_report print(PICable("numpy", "numpy", "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"))