Skip to content

Commit 2081af7

Browse files
committed
Add a script to render imported tasks' dependencies to PDF
1 parent c063b2b commit 2081af7

File tree

1 file changed

+137
-0
lines changed

1 file changed

+137
-0
lines changed

scripts/github-dependencies.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import argparse
2+
import io
3+
import itertools
4+
import re
5+
import subprocess
6+
import sys
7+
from pathlib import Path
8+
from typing import Collection, Dict, Iterable, List, Optional
9+
10+
import github
11+
from import_backends import get_github_credential
12+
13+
14+
def dropuntil(iterable: List[str], key: str) -> Iterable[str]:
15+
found = False
16+
for x in iterable:
17+
if found and x:
18+
yield x
19+
found = found or x == key
20+
21+
22+
def parse_issue_id(text: str) -> Optional[int]:
23+
match = re.search(r'#(\d+) ', text)
24+
if match is not None:
25+
return int(match.group(1))
26+
return None
27+
28+
29+
class GitHubBackend:
30+
def __init__(self, repo_name: str, milestones: Collection[str]) -> None:
31+
self.github = github.Github(get_github_credential())
32+
self.repo = self.github.get_repo(repo_name)
33+
34+
self.milestones: Dict[str, github.Milestone.Milestone] = {
35+
x.title: x for x in self.repo.get_milestones()
36+
}
37+
38+
self.labels: Dict[str, github.Label.Label] = {
39+
x.name: x for x in self.repo.get_labels()
40+
}
41+
42+
self.issues: Dict[int, github.Issue.Issue] = {
43+
x.number: x for x in itertools.chain.from_iterable(
44+
self.repo.get_issues(
45+
state='all',
46+
milestone=self.milestones[m],
47+
)
48+
for m in milestones
49+
)
50+
}
51+
52+
self.dependencies: Dict[int, List[int]] = {
53+
x: self.get_dependencies(x) for x in self.issues.keys()
54+
}
55+
56+
def get_issue(self, number: int) -> Optional[github.Issue.Issue]:
57+
try:
58+
return self.issues[number]
59+
except KeyError:
60+
print(f"Warning: unknown issue {number}", file=sys.stderr)
61+
return None
62+
63+
def get_dependencies(self, number: int) -> List[int]:
64+
issue = self.get_issue(number)
65+
if not issue:
66+
return []
67+
68+
lines = dropuntil(issue.body.splitlines(), key='### Dependencies')
69+
parsed = [parse_issue_id(y) for y in lines]
70+
return [x for x in parsed if x is not None]
71+
72+
def as_dot(self) -> str:
73+
def issue_as_dot(number: int, deps: List[int]) -> str:
74+
issue = self.get_issue(number)
75+
if issue is None:
76+
raise ValueError(number)
77+
78+
deps_str = ' '.join(str(y) for y in deps)
79+
title = issue.title.replace('"', '\\"')
80+
colour = "black"
81+
82+
if issue.state == 'closed':
83+
title = f"{title} (closed)"
84+
colour = "grey"
85+
86+
return "\n".join((
87+
f' {number} [ label="{title}" fontcolor={colour} color={colour} ]',
88+
f' {number} -> {{ {deps_str} }}',
89+
))
90+
91+
body = "\n".join(
92+
issue_as_dot(number, deps)
93+
for number, deps in self.dependencies.items()
94+
)
95+
return f"digraph {{ {body} }}"
96+
97+
98+
def parse_args() -> argparse.Namespace:
99+
parser = argparse.ArgumentParser()
100+
parser.add_argument(
101+
'--github-repo',
102+
help="GitHub repository name (default: %(default)s)",
103+
default='srobo/tasks',
104+
)
105+
parser.add_argument(
106+
'milestones',
107+
nargs=argparse.ONE_OR_MORE,
108+
help="The milestones to pull tasks from",
109+
)
110+
parser.add_argument(
111+
'--output',
112+
type=Path,
113+
help="The milestones to pull tasks from",
114+
)
115+
116+
return parser.parse_args()
117+
118+
119+
def main(arguments: argparse.Namespace) -> None:
120+
backend = GitHubBackend(
121+
arguments.github_repo,
122+
arguments.milestones,
123+
)
124+
125+
dot = backend.as_dot()
126+
127+
with arguments.output.open(mode='wb') as f:
128+
subprocess.run(
129+
['dot', '-Grankdir=LR', '-Tpdf'],
130+
input=dot.encode(),
131+
stdout=f,
132+
check=True,
133+
)
134+
135+
136+
if __name__ == '__main__':
137+
main(parse_args())

0 commit comments

Comments
 (0)