diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 000000000..d5f8f29d7 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.25.0,<3 +types-requests>=2.25.0,<3 diff --git a/scripts/typing-summary.py b/scripts/typing-summary.py new file mode 100755 index 000000000..f5f9825d0 --- /dev/null +++ b/scripts/typing-summary.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +""" +Generate a summary of last week's issues tagged with "topic: feature". + +The summary will include a list of new and changed issues and is sent each +Monday at 0200 CE(S)T to the typing-sig mailing list. Due to limitation +with GitHub Actions, the mail is sent from a private server, currently +maintained by @srittau. +""" + +from __future__ import annotations + +import datetime +from dataclasses import dataclass +from typing import Any, Iterable, Sequence + +import requests + +ISSUES_API_URL = "https://api.github.com/repos/python/typing/issues" +ISSUES_URL = "https://github.com/python/typing/issues?q=label%3A%22topic%3A+feature%22" +ISSUES_LABEL = "topic: feature" +SENDER_EMAIL = "Typing Bot " +RECEIVER_EMAIL = "typing-sig@python.org" + + +@dataclass +class Issue: + number: int + title: str + url: str + created: datetime.datetime + user: str + pull_request: bool = False + + +def main() -> None: + since = previous_week_start() + issues = fetch_issues(since) + new, updated = split_issues(issues, since) + print_summary(since, new, updated) + + +def previous_week_start() -> datetime.date: + today = datetime.date.today() + return today - datetime.timedelta(days=today.weekday() + 7) + + +def fetch_issues(since: datetime.date) -> list[Issue]: + """Return (new, updated) issues.""" + j = requests.get( + ISSUES_API_URL, + params={ + "labels": ISSUES_LABEL, + "since": f"{since:%Y-%m-%d}T00:00:00Z", + "per_page": "100", + "state": "open", + }, + headers={"Accept": "application/vnd.github.v3+json"}, + ).json() + assert isinstance(j, list) + return [parse_issue(j_i) for j_i in j] + + +def parse_issue(j: Any) -> Issue: + number = j["number"] + title = j["title"] + url = j["html_url"] + created_at = datetime.datetime.fromisoformat(j["created_at"][:-1]) + user = j["user"]["login"] + pull_request = "pull_request" in j + assert isinstance(number, int) + assert isinstance(title, str) + assert isinstance(url, str) + assert isinstance(user, str) + return Issue(number, title, url, created_at, user, pull_request) + + +def split_issues( + issues: Iterable[Issue], since: datetime.date +) -> tuple[list[Issue], list[Issue]]: + new = [] + updated = [] + for issue in issues: + if issue.created.date() >= since: + new.append(issue) + else: + updated.append(issue) + new.sort(key=lambda i: i.number) + updated.sort(key=lambda i: i.number) + return new, updated + + +def print_summary( + since: datetime.date, new: Sequence[Issue], changed: Sequence[Issue] +) -> None: + print(f"From: {SENDER_EMAIL}") + print(f"To: {RECEIVER_EMAIL}") + print(f"Subject: Opened and changed typing issues week {since:%G-W%V}") + print() + print(generate_mail(new, changed)) + + +def generate_mail(new: Sequence[Issue], changed: Sequence[Issue]) -> str: + if len(new) == 0 and len(changed) == 0: + s = ( + "No issues or pull requests with the label 'topic: feature' were opened\n" + "or updated last week in the typing repository on GitHub.\n\n" + ) + else: + s = ( + "The following is an overview of all issues and pull requests in the\n" + "typing repository on GitHub with the label 'topic: feature'\n" + "that were opened or updated last week, excluding closed issues.\n\n" + "---------------------------------------------------\n\n" + ) + if len(new) > 0: + s += "The following issues and pull requests were opened last week: \n\n" + s += "".join(generate_issue_text(issue) for issue in new) + s += "\n---------------------------------------------------\n\n" + if len(changed) > 0: + s += "The following issues and pull requests were updated last week: \n\n" + s += "".join(generate_issue_text(issue) for issue in changed) + s += "\n---------------------------------------------------\n\n" + s += ( + "All issues and pull requests with the label 'topic: feature'\n" + "can be viewed under the following URL:\n\n" + ) + s += ISSUES_URL + return s + + +def generate_issue_text(issue: Issue) -> str: + s = f"#{issue.number:<5} " + if issue.pull_request: + s += "[PR] " + s += f"{issue.title}\n" + s += f" opened by @{issue.user}\n" + s += f" {issue.url}\n" + return s + + +if __name__ == "__main__": + main()