diff --git a/requirements.txt b/requirements.txt index 6b62a17..1a151bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +python-dateutil==2.8.1 todoist-python==8.1.1 urlextract==0.14.0 diff --git a/todoist-github/cli.py b/todoist-github/cli.py index 767f68b..b3bba8f 100755 --- a/todoist-github/cli.py +++ b/todoist-github/cli.py @@ -1,21 +1,71 @@ #!/usr/bin/env python3 -from .clients import todoist -from .utils import get_github_task +import datetime + +from dateutil.relativedelta import relativedelta + +from .clients import github, todoist +from .utils import get_github_issue_details, get_github_task, get_issue + + +def get_issue_link(issue_or_pr) -> str: + return "[#{id}]({url})".format(id=issue_or_pr.number, url=issue_or_pr.html_url) + + +def issue_to_task_name(issue) -> str: + return get_issue_link(issue) + ": " + issue.title def get_relevant_todoist_tasks(): todoist.items.sync() tasks = {} for task in todoist.items.all(): - if get_github_task(task["content"]): - tasks[task['content']] = task + github_task = get_github_task(task["content"]) + if github_task: + tasks[github_task] = task return tasks +def is_task_completed(task): + return task.data.get("checked", 0) + + def main(): todoist_tasks = get_relevant_todoist_tasks() + relevant_since = datetime.datetime.now() - relativedelta( + weeks=30 + ) # TODO: Make this a sane number + tasks_actioned = [] + me = github.get_user() + for assigned_issue in me.get_issues(state="all", since=relevant_since): + task = todoist_tasks.get(assigned_issue.html_url) + if not task and assigned_issue.state == "open": + task = todoist.items.add(issue_to_task_name(assigned_issue)) + if not task: + continue + tasks_actioned.append(task["id"]) + if assigned_issue == "closed" and not is_task_completed(task): + print("completing", assigned_issue) + task.complete() + if is_task_completed(task): + print("uncompleting task", assigned_issue) + task.uncomplete() + if task["content"] != issue_to_task_name(assigned_issue): + print("updating issue name for", assigned_issue) + task.update(content=issue_to_task_name(assigned_issue)) + if assigned_issue.milestone and assigned_issue.milestone.due_on: + task.update( + date_string=assigned_issue.milestone.due_on.strftime("%d/%m/%Y") + ) - + for task in todoist_tasks.values(): + if not is_task_completed(task) or task["id"] in tasks_actioned: + continue + org, repo, issue_number = get_github_issue_details(task["content"]) + issue = get_issue(me, org, repo, issue_number) + me_assigned = me.login in {assignee.login for assignee in issue.assignees} + if not me_assigned: + print("Deleting", issue) + task.delete() if __name__ == "__main__": diff --git a/todoist-github/clients.py b/todoist-github/clients.py index 5a31eec..e06f69e 100644 --- a/todoist-github/clients.py +++ b/todoist-github/clients.py @@ -1,4 +1,7 @@ -from todoist import TodoistAPI import os +from github import Github +from todoist import TodoistAPI + todoist = TodoistAPI(os.environ["TODOIST_TOKEN"]) +github = Github(os.environ["GITHUB_TOKEN"]) diff --git a/todoist-github/utils.py b/todoist-github/utils.py index 6eb62ce..27aa207 100644 --- a/todoist-github/utils.py +++ b/todoist-github/utils.py @@ -1,7 +1,11 @@ +import re from typing import Optional -from urlextract import URLExtract from urllib.parse import urlparse +from github.Issue import Issue +from urlextract import URLExtract + +GITHUB_ISSUE_PR_RE = re.compile(r"\/(.+?)\/(.+?)\/(pull|issues)\/(\d+?)$") extractor = URLExtract() @@ -10,5 +14,24 @@ def get_github_task(content) -> Optional[str]: if "github" not in content.lower(): return None for url in extractor.gen_urls(content): - if urlparse(url).netloc == "github.com": + parsed_url = urlparse(url) + if parsed_url.netloc == "github.com" and GITHUB_ISSUE_PR_RE.search( + parsed_url.path + ): return url + + +def get_github_issue_details(content): + url = get_github_task(content) + if not url: + return + parsed_url = urlparse(url) + match = GITHUB_ISSUE_PR_RE.search(parsed_url.path) + return match.group(1), match.group(2), match.group(4) + + +def get_issue(me, org, repo, issue_num): + headers, data = me._requester.requestJsonAndCheck( + "GET", f"/repos/{org}/{repo}/issues/{issue_num}" + ) + return Issue(me._requester, headers, data, completed=True)