| #!/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 <noreply@python.org>" | |
| 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() |