Files
PICable/PICable.py
2025-11-18 21:43:17 +00:00

133 lines
6.0 KiB
Python

import os
import subprocess
import sys
from datetime import datetime, timedelta
import requests
LIMIT_COMMITS_30_DAYS = int(os.getenv("LIMIT_COMMITS_30_DAYS", "50"))
LIMIT_STARS = int(os.getenv("LIMIT_STARS", "200"))
LIMIT_NLOC = int(os.getenv("LIMIT_NLOC", "100000"))
MARGIN = 0.1
CLONE_DIR = "/temp"
EXCLUDED_LANGUAGES = ["CSV", "diff", "INI", "JSON", "Markdown", "reStructuredText", "SVG", "Text", "YAML"]
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):
repository_url = f"https://github.com/{owner}/{repository}.git"
parent_dir = os.path.expanduser(f"{CLONE_DIR}/{owner}")
repository_clone_dir = os.path.expanduser(f"{parent_dir}/{repository}")
try:
subprocess.run(
["git", "clone", "--depth", "1", repository_url, repository_clone_dir],
check=True,
stdout=sys.stdout,
stderr=sys.stderr,
)
result = subprocess.run(
["cloc", f"--exclude-lang={','.join(EXCLUDED_LANGUAGES)}", repository_clone_dir],
capture_output=True,
text=True,
check=True,
)
number_of_lines_of_code = int(result.stdout.split("\n")[-3].split()[-1])
except Exception:
return -1
finally:
subprocess.run(["rm", "-rf", parent_dir], check=True)
return number_of_lines_of_code
def PICable(owner, repository, token):
margin_percentage = round(MARGIN * 100)
request_headers = {"Authorization": f"token {token}"}
picable_report = f"Analysis complete for the repository {owner}/{repository}.\n\n"
repository_url = f"https://www.github.com/{owner}/{repository}"
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 stars >= LIMIT_STARS and commits_30_days >= LIMIT_COMMITS_30_DAYS and 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 parameter which is below the requirement, but by less than {margin_percentage}%.\nConsult with a professor before proceeding.\n"
print("Analysis complete.")
return picable_report