Skip to content

Commit d18b23a

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

File tree

1 file changed

+136
-0
lines changed

1 file changed

+136
-0
lines changed

scripts/github-dependencies.py

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

0 commit comments

Comments
 (0)