Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/lysis/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Rank a repo's open issues: python src\\lysis\\cli.py owner/repo [top]"""

import sys

import pandas as pd
from dotenv import load_dotenv

from lysis.features.rows import issue_row
from lysis.ingestion.github_client import GitHubClient
from lysis.models.evaluate import load_dataset
from lysis.models.gbm import make_gbm_ranker


def rank_repo(gh, full_name, ranker, top=10):
owner, name = full_name.split("/")
repo = gh.get(f"/repos/{full_name}")
issues = [i for i in gh.list_issues(owner, name, state="open") if not i.get("assignees")]
if not issues:
return pd.DataFrame(columns=["score", "number", "title", "url"])
df = pd.DataFrame([issue_row(i, repo) for i in issues])
df["score"] = list(ranker(df))
df = df.sort_values("score", ascending=False).head(top)
df["number"] = df["issue_number"]
df["title"] = df["title"].str[:60]
df["score"] = df["score"].round(3)
return df[["score", "number", "title", "url"]]


def main():
load_dotenv()
if len(sys.argv) < 2:
sys.exit("usage: python src\\lysis\\cli.py owner/repo [top]")
top = int(sys.argv[2]) if len(sys.argv) > 2 else 10
ranker = make_gbm_ranker(load_dataset())
print(rank_repo(GitHubClient(), sys.argv[1], ranker, top).to_string(index=False))


if __name__ == "__main__":
main()
23 changes: 23 additions & 0 deletions src/lysis/features/rows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Build one model-input row from a GitHub issue and its repo."""


def issue_row(issue, repo):
body = issue.get("body") or ""
return {
"repo": repo["full_name"],
"issue_number": issue["number"],
"title": issue.get("title") or "",
"url": issue.get("html_url", ""),
"labels": [x["name"].lower() for x in issue["labels"]],
"comments": issue["comments"],
"body_len": len(body),
"title_len": len(issue.get("title") or ""),
"has_code": int("```" in body),
"author_assoc": issue.get("author_association") or "NONE",
"stars": repo["stargazers_count"],
"forks": repo["forks_count"],
"repo_size": repo["size"],
"has_license": int(bool(repo.get("license"))),
"repo_created_at": repo["created_at"],
"created_at": issue["created_at"],
}
21 changes: 2 additions & 19 deletions src/lysis/models/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from sklearn.metrics import average_precision_score, roc_auc_score
from sklearn.model_selection import GroupKFold

from lysis.features.rows import issue_row
from lysis.models.baseline import labels_only_ranker
from lysis.models.gbm import make_gbm_ranker

Expand All @@ -22,34 +23,16 @@ def load_dataset(raw_dir="data/raw", labels_path="data/labels.csv"):
for path in Path(raw_dir).glob("*.json"):
d = json.loads(path.read_text())
r = d["repo"]
name = r["full_name"]
cutoff = min((p["created_at"] for p in d["merged_prs"]), default=None)
rows += [
{
"repo": name,
"issue_number": i["number"],
"labels": [x["name"].lower() for x in i["labels"]],
"comments": i["comments"],
"body_len": len(i.get("body") or ""),
"title_len": len(i.get("title") or ""),
"text": f"{i.get('title') or ''} {i.get('body') or ''}"[:2000],
"has_code": int("```" in (i.get("body") or "")),
"author_assoc": i.get("author_association") or "NONE",
"stars": r["stargazers_count"],
"forks": r["forks_count"],
"repo_size": r["size"],
"has_license": int(bool(r.get("license"))),
"repo_created_at": r["created_at"],
"created_at": i["created_at"],
}
issue_row(i, r)
for i in d["closed_issues"]
if cutoff and i["created_at"] >= cutoff
]
df = pd.DataFrame(rows).merge(labels, on=["repo", "issue_number"], how="left")
df["first_timer"] = df["first_timer"].fillna(0).astype(int)
return df


def split_by_repo(df, test_frac=0.3, seed=42):
repos = pd.Series(sorted(df["repo"].unique())).sample(frac=1, random_state=seed)
test_repos = set(repos[: max(1, int(len(repos) * test_frac))])
Expand Down
33 changes: 33 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from lysis.cli import rank_repo


class FakeGH:
def get(self, path, params=None):
return {
"full_name": "a/b",
"stargazers_count": 10,
"forks_count": 1,
"size": 100,
"license": None,
"created_at": "2020-01-01T00:00:00Z",
}

def list_issues(self, owner, repo, state="open"):
base = {
"labels": [],
"comments": 0,
"body": "x",
"created_at": "2026-01-01T00:00:00Z",
"html_url": "u",
"assignees": [],
}
return [
{**base, "number": 1, "title": "one"},
{**base, "number": 2, "title": "two", "labels": [{"name": "good first issue"}]},
{**base, "number": 3, "title": "taken", "assignees": [{"login": "x"}]},
]


def test_rank_repo():
out = rank_repo(FakeGH(), "a/b", lambda df: df["labels"].apply(len), top=5)
assert list(out["number"]) == [2, 1]
Loading