| from __future__ import annotations |
|
|
| import json |
| import sqlite3 |
| from dataclasses import dataclass, field |
| from pathlib import Path |
|
|
|
|
| @dataclass |
| class IssueRow: |
| issue_id: int |
| number: int |
| repo: str |
| title: str |
| body: str |
| author_login: str |
| is_pr: bool |
| tier: int |
| is_synthetic_spam: bool |
| spam_pattern: str | None |
| truth_action: str |
| truth_labels: list[str] |
| truth_close_reason: str | None |
| truth_assignee: str | None |
| state: str |
|
|
|
|
| @dataclass |
| class ContributorRow: |
| login: str |
| public_pr_count: int |
| public_issue_count: int |
| account_age_days: int |
| hidden_reputation: float |
| hidden_is_spammer: bool |
|
|
|
|
| @dataclass |
| class CommentRow: |
| comment_id: int |
| issue_id: int |
| author_login: str |
| body: str |
| created_at: str |
|
|
|
|
| def _row_to_issue(r: sqlite3.Row) -> IssueRow: |
| return IssueRow( |
| issue_id=r["issue_id"], |
| number=r["number"], |
| repo=r["repo"], |
| title=r["title"], |
| body=r["body"] or "", |
| author_login=r["author_login"], |
| is_pr=bool(r["is_pr"]), |
| tier=r["tier"], |
| is_synthetic_spam=bool(r["is_synthetic_spam"]), |
| spam_pattern=r["spam_pattern"], |
| truth_action=r["truth_action"], |
| truth_labels=json.loads(r["truth_labels"] or "[]"), |
| truth_close_reason=r["truth_close_reason"], |
| truth_assignee=r["truth_assignee"], |
| state=r["state"], |
| ) |
|
|
|
|
| class RepoDB: |
| def __init__(self, path: str | Path): |
| self.path = str(path) |
| self._con = sqlite3.connect(self.path, check_same_thread=False) |
| self._con.row_factory = sqlite3.Row |
| self._labels_cache: dict[str, list[str]] = {} |
|
|
| def labels_for_repo(self, repo: str) -> list[str]: |
| if repo in self._labels_cache: |
| return self._labels_cache[repo] |
| rows = self._con.execute( |
| "SELECT name FROM labels WHERE repo=? ORDER BY name", (repo,) |
| ).fetchall() |
| out = [r["name"] for r in rows] |
| self._labels_cache[repo] = out |
| return out |
|
|
| def issues_for_tier(self, repo: str, tiers: list[int], limit: int) -> list[IssueRow]: |
| placeholders = ",".join("?" * len(tiers)) |
| rows = self._con.execute( |
| f"SELECT * FROM issues WHERE repo=? AND tier IN ({placeholders}) " |
| f"AND is_synthetic_spam=0 ORDER BY RANDOM() LIMIT ?", |
| (repo, *tiers, limit), |
| ).fetchall() |
| return [_row_to_issue(r) for r in rows] |
|
|
| def contributor(self, login: str) -> ContributorRow | None: |
| r = self._con.execute( |
| "SELECT * FROM contributors WHERE login=?", (login,) |
| ).fetchone() |
| if not r: |
| return None |
| return ContributorRow( |
| login=r["login"], |
| public_pr_count=r["public_pr_count"], |
| public_issue_count=r["public_issue_count"], |
| account_age_days=r["account_age_days"], |
| hidden_reputation=r["hidden_reputation"], |
| hidden_is_spammer=bool(r["hidden_is_spammer"]), |
| ) |
|
|
| def comments_for_issue(self, issue_id: int, limit: int = 6) -> list[CommentRow]: |
| rows = self._con.execute( |
| "SELECT * FROM comments WHERE issue_id=? ORDER BY created_at LIMIT ?", |
| (issue_id, limit), |
| ).fetchall() |
| return [ |
| CommentRow( |
| comment_id=r["comment_id"], |
| issue_id=r["issue_id"], |
| author_login=r["author_login"], |
| body=r["body"], |
| created_at=r["created_at"], |
| ) |
| for r in rows |
| ] |
|
|
| def search_history(self, repo: str, query: str, limit: int = 5) -> list[IssueRow]: |
| like = f"%{query.strip()[:60]}%" |
| rows = self._con.execute( |
| "SELECT * FROM issues WHERE repo=? AND (title LIKE ? OR body LIKE ?) LIMIT ?", |
| (repo, like, like, limit), |
| ).fetchall() |
| return [_row_to_issue(r) for r in rows] |
|
|
| def count_issues(self, repo: str) -> int: |
| r = self._con.execute( |
| "SELECT COUNT(*) AS c FROM issues WHERE repo=? AND is_synthetic_spam=0", (repo,) |
| ).fetchone() |
| return r["c"] if r else 0 |
|
|