Skip to content

Commit 786c493

Browse files
authored
Merge pull request #2 from jpbruinsslot/add-type-hints
Add type-hinting
2 parents 26cbddf + b2b10d5 commit 786c493

2 files changed

Lines changed: 70 additions & 24 deletions

File tree

alert2issue.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from pathlib import Path
88

99

10-
def run_gh_command(cmd, capture_json=True):
10+
def run_gh_command(cmd: str, capture_json: bool = True) -> str | dict | None:
11+
"""Run a GitHub CLI command and return the output as JSON or plain text."""
1112
try:
1213
result = subprocess.run(shlex.split(cmd), capture_output=True, check=True, text=True)
1314
return json.loads(result.stdout) if capture_json else result.stdout.strip()
@@ -18,12 +19,32 @@ def run_gh_command(cmd, capture_json=True):
1819
return None
1920

2021

21-
def check_rate_limit(min_remaining=100):
22+
def run_gh_command_json(cmd: str) -> dict | None:
23+
"""Run a GitHub CLI command and return the output as JSON."""
24+
output = run_gh_command(cmd, capture_json=True)
25+
if output is None:
26+
return None
27+
28+
return output if isinstance(output, dict) else json.loads(output)
29+
30+
31+
def run_gh_command_text(cmd: str) -> str | None:
32+
"""Run a GitHub CLI command and return the output as plain text."""
33+
output = run_gh_command(cmd, capture_json=False)
34+
if output is None:
35+
return None
36+
37+
return output if isinstance(output, str) else json.dumps(output, indent=2)
38+
39+
40+
def check_rate_limit(min_remaining: int = 100) -> bool:
41+
"""Check GitHub API rate limit and return True if sufficient calls remaining."""
2242
print("⏳ Checking GitHub API rate limit...")
23-
remaining = run_gh_command('gh api rate_limit --jq ".rate.remaining"', capture_json=False)
43+
remaining = run_gh_command_text('gh api rate_limit --jq ".rate.remaining"')
2444
if remaining is None:
2545
print("⚠️ Could not determine API rate limit. Proceeding with caution.")
2646
return True
47+
2748
try:
2849
remaining_int = int(remaining)
2950
print(f"🔢 API calls remaining: {remaining_int}")
@@ -36,13 +57,17 @@ def check_rate_limit(min_remaining=100):
3657
return True
3758

3859

39-
def ensure_label(repo, label, color, description, dry_run=False):
40-
existing = run_gh_command(f"gh label list --repo {repo} --limit 100", capture_json=False)
60+
def ensure_label(
61+
repo: str, label: str, color: str, description: str, dry_run: bool = False
62+
) -> None:
63+
"""Ensure a label exists in the specified repository."""
64+
existing = run_gh_command_text(f"gh label list --repo {repo} --limit 100")
4165
if not existing or not any(line.startswith(label) for line in existing.splitlines()):
4266
action = "Would create" if dry_run else "Creating"
4367
print(f"🛠️ {action} label: {label} in {repo}")
4468
if dry_run:
4569
return
70+
4671
try:
4772
subprocess.run(
4873
[
@@ -63,7 +88,10 @@ def ensure_label(repo, label, color, description, dry_run=False):
6388
print(f"⚠️ Failed to create label: {label} in {repo}")
6489

6590

66-
def create_issue(repo, title, body, dry_run=False, labels=None):
91+
def create_issue(
92+
repo: str, title: str, body: str, dry_run: bool = False, labels: list[str] | None = None
93+
) -> None:
94+
"""Create a new issue in the specified repository."""
6795
labels = labels or ["security", "dependabot"]
6896

6997
if dry_run:
@@ -83,22 +111,22 @@ def create_issue(repo, title, body, dry_run=False, labels=None):
83111
print(f"❌ Failed to create issue in {repo}: {title}")
84112

85113

86-
def get_open_issue_titles(repo):
114+
def get_open_issue_titles(repo: str) -> set[str]:
115+
"""Get titles of all open issues in the specified repository."""
87116
# Get all open issue titles for repo in one go (up to 100)
88-
output = run_gh_command(
89-
f"gh issue list --repo {repo} --state open --json title", capture_json=True
90-
)
117+
output = run_gh_command_json(f"gh issue list --repo {repo} --state open --json title")
91118
if output is None:
92119
return set()
120+
93121
return set(issue["title"] for issue in output)
94122

95123

96-
def process_repo(repo, dry_run=False):
124+
def process_repo(repo: str, dry_run: bool = False) -> None:
125+
"""Process a single repository to check for Dependabot alerts and create issues."""
97126
print(f"🔍 Checking alerts for: {repo}")
98127

99-
alerts = run_gh_command(
128+
alerts = run_gh_command_json(
100129
f'gh api -X GET "/repos/{repo}/dependabot/alerts?per_page=100" --paginate',
101-
capture_json=True,
102130
)
103131
if not alerts:
104132
print(f"✅ No open dependabot alerts found for {repo}.")
@@ -145,19 +173,37 @@ def process_repo(repo, dry_run=False):
145173
print(f"⚠️ Issue already exists in {repo}: '{title}'. Skipping...")
146174
continue
147175

148-
ensure_label(repo, "security", "d73a4a", "Security-related issues", dry_run)
149-
ensure_label(repo, "dependabot", "0366d6", "Dependabot alerts", dry_run)
176+
labels = [
177+
{
178+
"name": "security",
179+
"color": "d73a4a",
180+
"description": "Security-related issues",
181+
},
182+
{
183+
"name": "dependabot",
184+
"color": "0366d6",
185+
"description": "Dependabot alerts",
186+
},
187+
]
150188

151-
labels = ["security", "dependabot"]
152189
if fpv is None:
153-
ensure_label(repo, "no-patch", "ededed", "No patched version available", dry_run)
154-
labels.append("no-patch")
190+
labels.append(
191+
{
192+
"name": "no-patch",
193+
"color": "ededed",
194+
"description": "No patched version available",
195+
}
196+
)
197+
198+
for label in labels:
199+
ensure_label(repo, label["name"], label["color"], label["description"], dry_run)
155200

156-
create_issue(repo, title, body, dry_run=dry_run, labels=labels)
201+
create_issue(repo, title, body, dry_run=dry_run, labels=[label["name"] for label in labels])
157202

158203

159-
def load_repos(path):
160-
with open(path) as f:
204+
def load_repos(path: Path) -> list[str]:
205+
"""Load repository names from a file, ignoring comments and empty lines."""
206+
with open(path, "r", encoding="utf-8") as f:
161207
return [
162208
line.split("#")[0].strip()
163209
for line in f

test_alert2issue.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def test_run_gh_command_failure(self, mock_run, mock_print):
6565
result = alert2issue.run_gh_command("gh test")
6666
self.assertIsNone(result)
6767

68-
@patch("alert2issue.run_gh_command")
68+
@patch("alert2issue.run_gh_command_json")
6969
def test_process_repo_no_alerts(self, mock_run, mock_print):
7070
mock_run.side_effect = [[], None] # Empty alert list
7171
alert2issue.process_repo("user/repo")
@@ -75,7 +75,7 @@ def test_process_repo_no_alerts(self, mock_run, mock_print):
7575
"Expected 'No open dependabot alerts' in printed output.",
7676
)
7777

78-
@patch("alert2issue.run_gh_command")
78+
@patch("alert2issue.run_gh_command_json")
7979
@patch("alert2issue.ensure_label")
8080
@patch("alert2issue.create_issue")
8181
def test_process_repo_with_alert(self, mock_create, mock_label, mock_run, mock_print):
@@ -104,7 +104,7 @@ def test_process_repo_with_alert(self, mock_create, mock_label, mock_run, mock_p
104104
"user/repo", "security", "d73a4a", "Security-related issues", True
105105
)
106106

107-
@patch("alert2issue.run_gh_command")
107+
@patch("alert2issue.run_gh_command_json")
108108
@patch("alert2issue.ensure_label")
109109
@patch("alert2issue.create_issue")
110110
def test_process_repo_no_patch(self, mock_create, mock_label, mock_run, mock_print):

0 commit comments

Comments
 (0)