From 4ba2dfdec0c83b7c41aea4f77e1663ee1041477a Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 19 Mar 2022 12:57:19 +0000 Subject: [PATCH 1/5] Add a script to render imported tasks' dependencies to PDF --- scripts/github-dependencies.py | 136 +++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 scripts/github-dependencies.py diff --git a/scripts/github-dependencies.py b/scripts/github-dependencies.py new file mode 100644 index 0000000..3c1414a --- /dev/null +++ b/scripts/github-dependencies.py @@ -0,0 +1,136 @@ +import argparse +import itertools +import re +import subprocess +import sys +from pathlib import Path +from typing import Collection, Dict, Iterable, List, Optional + +import github +from import_backends import get_github_credential + + +def dropuntil(iterable: List[str], key: str) -> Iterable[str]: + found = False + for x in iterable: + if found and x: + yield x + found = found or x == key + + +def parse_issue_id(text: str) -> Optional[int]: + match = re.search(r'#(\d+) ', text) + if match is not None: + return int(match.group(1)) + return None + + +class GitHubBackend: + def __init__(self, repo_name: str, milestones: Collection[str]) -> None: + self.github = github.Github(get_github_credential()) + self.repo = self.github.get_repo(repo_name) + + self.milestones: Dict[str, github.Milestone.Milestone] = { + x.title: x for x in self.repo.get_milestones() + } + + self.labels: Dict[str, github.Label.Label] = { + x.name: x for x in self.repo.get_labels() + } + + self.issues: Dict[int, github.Issue.Issue] = { + x.number: x for x in itertools.chain.from_iterable( + self.repo.get_issues( + state='all', + milestone=self.milestones[m], + ) + for m in milestones + ) + } + + self.dependencies: Dict[int, List[int]] = { + x: self.get_dependencies(x) for x in self.issues.keys() + } + + def get_issue(self, number: int) -> Optional[github.Issue.Issue]: + try: + return self.issues[number] + except KeyError: + print(f"Warning: unknown issue {number}", file=sys.stderr) + return None + + def get_dependencies(self, number: int) -> List[int]: + issue = self.get_issue(number) + if not issue: + return [] + + lines = dropuntil(issue.body.splitlines(), key='### Dependencies') + parsed = [parse_issue_id(y) for y in lines] + return [x for x in parsed if x is not None] + + def as_dot(self) -> str: + def issue_as_dot(number: int, deps: List[int]) -> str: + issue = self.get_issue(number) + if issue is None: + raise ValueError(number) + + deps_str = ' '.join(str(y) for y in deps) + title = issue.title.replace('"', '\\"') + colour = "black" + + if issue.state == 'closed': + title = f"{title} (closed)" + colour = "grey" + + return "\n".join(( + f' {number} [ label="{title}" fontcolor={colour} color={colour} ]', + f' {number} -> {{ {deps_str} }}', + )) + + body = "\n".join( + issue_as_dot(number, deps) + for number, deps in self.dependencies.items() + ) + return f"digraph {{ {body} }}" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + '--github-repo', + help="GitHub repository name (default: %(default)s)", + default='srobo/tasks', + ) + parser.add_argument( + 'milestones', + nargs=argparse.ONE_OR_MORE, + help="The milestones to pull tasks from", + ) + parser.add_argument( + '--output', + type=Path, + help="The milestones to pull tasks from", + ) + + return parser.parse_args() + + +def main(arguments: argparse.Namespace) -> None: + backend = GitHubBackend( + arguments.github_repo, + arguments.milestones, + ) + + dot = backend.as_dot() + + with arguments.output.open(mode='wb') as f: + subprocess.run( + ['dot', '-Grankdir=LR', '-Tpdf'], + input=dot.encode(), + stdout=f, + check=True, + ) + + +if __name__ == '__main__': + main(parse_args()) From 887141fc51042ad4c6805342db1949413ffdb9da Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 9 Apr 2022 15:19:38 +0100 Subject: [PATCH 2/5] Make this directly runnable --- scripts/github-dependencies.py | 2 ++ 1 file changed, 2 insertions(+) mode change 100644 => 100755 scripts/github-dependencies.py diff --git a/scripts/github-dependencies.py b/scripts/github-dependencies.py old mode 100644 new mode 100755 index 3c1414a..92c608b --- a/scripts/github-dependencies.py +++ b/scripts/github-dependencies.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import argparse import itertools import re From 9b62f6537fe3b8943dbc9227ea969b16b8d76ef1 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 9 Apr 2022 15:21:12 +0100 Subject: [PATCH 3/5] Fix this help message --- scripts/github-dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/github-dependencies.py b/scripts/github-dependencies.py index 92c608b..8fe575f 100755 --- a/scripts/github-dependencies.py +++ b/scripts/github-dependencies.py @@ -111,7 +111,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( '--output', type=Path, - help="The milestones to pull tasks from", + help="Where to output the dependencies PDF", ) return parser.parse_args() From ffc9cd07945e232fafafc2f3cb96c61418a80854 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 9 Apr 2022 15:24:40 +0100 Subject: [PATCH 4/5] Issues without a body return None rather than empty string --- scripts/github-dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/github-dependencies.py b/scripts/github-dependencies.py index 8fe575f..ebb7a71 100755 --- a/scripts/github-dependencies.py +++ b/scripts/github-dependencies.py @@ -63,7 +63,7 @@ def get_issue(self, number: int) -> Optional[github.Issue.Issue]: def get_dependencies(self, number: int) -> List[int]: issue = self.get_issue(number) - if not issue: + if not issue or not issue.body: return [] lines = dropuntil(issue.body.splitlines(), key='### Dependencies') From 7fa40c5b0297a055462a2a57789cd97ba105208b Mon Sep 17 00:00:00 2001 From: Peter Law Date: Wed, 15 Mar 2023 20:58:30 +0000 Subject: [PATCH 5/5] Describe what this does --- scripts/github-dependencies.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/github-dependencies.py b/scripts/github-dependencies.py index ebb7a71..942b90b 100755 --- a/scripts/github-dependencies.py +++ b/scripts/github-dependencies.py @@ -97,7 +97,9 @@ def issue_as_dot(number: int, deps: List[int]) -> str: def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + description="Fetch tasks from GitHub and render a tree showing dependencies", + ) parser.add_argument( '--github-repo', help="GitHub repository name (default: %(default)s)",