| #!/usr/bin/env python3 |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| from __future__ import annotations |
| |
| import logging |
| import os |
| import re |
| import textwrap |
| from pathlib import Path |
| from typing import TYPE_CHECKING |
| |
| import rich_click as click |
| from attr import dataclass |
| from github import Github |
| from rich.console import Console |
| from tabulate import tabulate |
| |
| if TYPE_CHECKING: |
| from github.Issue import Issue |
| |
| PROVIDER_TESTING_LABEL = "testing status" |
| |
| logger = logging.getLogger(__name__) |
| |
| console = Console(width=400, color_system="standard") |
| |
| MY_DIR_PATH = Path(os.path.dirname(__file__)) |
| SOURCE_DIR_PATH = MY_DIR_PATH / os.pardir / os.pardir |
| |
| |
| @click.group(context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 500}) |
| def cli(): |
| ... |
| |
| |
| option_table = click.option( |
| "--table", |
| is_flag=True, |
| help="Print output as markdown table1", |
| ) |
| |
| option_github_token = click.option( |
| "--github-token", |
| type=str, |
| required=True, |
| help=textwrap.dedent( |
| """ |
| GitHub token used to authenticate. |
| You can set omit it if you have GITHUB_TOKEN env variable set |
| Can be generated with: |
| https://github.com/settings/tokens/new?description=Read%20Write%20isssues&scopes=repo""" |
| ), |
| envvar="GITHUB_TOKEN", |
| ) |
| |
| |
| @dataclass |
| class Stats: |
| issue_number: int |
| title: str |
| num_providers: int |
| num_issues: int |
| tested_issues: int |
| url: str |
| users_involved: set[str] |
| users_commented: set[str] |
| |
| def percent_tested(self) -> int: |
| return 100 * self.tested_issues // self.num_issues |
| |
| def num_involved_users_who_commented(self) -> int: |
| return len(self.users_involved.intersection(self.users_commented)) |
| |
| def num_commenting_not_involved(self) -> int: |
| return len(self.users_commented - self.users_involved) |
| |
| def percent_commented_among_involved(self) -> int: |
| return 100 * self.num_involved_users_who_commented() // len(self.users_involved) |
| |
| def __str__(self): |
| return ( |
| f"#{self.issue_number}: {self.title}: Num providers: {self.num_providers}, " |
| f"Issues: {self.num_issues}, Tested {self.tested_issues}, " |
| f"Percent Tested: {self.percent_tested()}%, " |
| f"Involved users: {len(self.users_involved)}, Commenting users: {len(self.users_commented)}, " |
| f"Involved who commented: {self.num_involved_users_who_commented()}, " |
| f"Extra people: {self.num_commenting_not_involved()}, " |
| f"Percent commented: {self.percent_commented_among_involved()}%, " |
| f"URL: {self.url}" |
| ) |
| |
| |
| def get_users_from_content(content: str) -> set[str]: |
| users_match = re.findall(r"@\S*", content, re.MULTILINE) |
| users: set[str] = set() |
| for user_match in users_match: |
| users.add(user_match) |
| return users |
| |
| |
| def get_users_who_commented(issue: Issue) -> set[str]: |
| users: set[str] = set() |
| for comment in issue.get_comments(): |
| users.add("@" + comment.user.login) |
| return users |
| |
| |
| def get_stats(issue: Issue) -> Stats: |
| content = issue.body |
| return Stats( |
| issue_number=issue.number, |
| title=issue.title, |
| num_providers=content.count("Provider "), |
| num_issues=content.count("- [") - 1, |
| tested_issues=content.count("[x]") + content.count("[X]") - 1, |
| url=issue.html_url, |
| users_involved=get_users_from_content(content), |
| users_commented=get_users_who_commented(issue), |
| ) |
| |
| |
| def stats_to_rows(stats_list: list[Stats]) -> list[tuple]: |
| total = Stats( |
| issue_number=0, |
| title="", |
| num_providers=0, |
| num_issues=0, |
| tested_issues=0, |
| url="", |
| users_commented=set(), |
| users_involved=set(), |
| ) |
| rows: list[tuple] = [] |
| for stat in stats_list: |
| total.num_providers += stat.num_providers |
| total.num_issues += stat.num_issues |
| total.tested_issues += stat.tested_issues |
| total.users_involved.update(stat.users_involved) |
| total.users_commented.update(stat.users_commented) |
| rows.append( |
| ( |
| f"[{stat.issue_number}]({stat.url})", |
| stat.num_providers, |
| stat.num_issues, |
| stat.tested_issues, |
| stat.percent_tested(), |
| len(stat.users_involved), |
| len(stat.users_commented), |
| stat.num_involved_users_who_commented(), |
| stat.num_commenting_not_involved(), |
| stat.percent_commented_among_involved(), |
| ) |
| ) |
| rows.append( |
| ( |
| "Total", |
| total.num_providers, |
| total.num_issues, |
| total.tested_issues, |
| total.percent_tested(), |
| len(total.users_involved), |
| len(total.users_commented), |
| total.num_involved_users_who_commented(), |
| total.num_commenting_not_involved(), |
| total.percent_commented_among_involved(), |
| ) |
| ) |
| return rows |
| |
| |
| @option_github_token |
| @option_table |
| @cli.command() |
| def provide_stats(github_token: str, table: bool): |
| g = Github(github_token) |
| repo = g.get_repo("apache/airflow") |
| issues = repo.get_issues(labels=[PROVIDER_TESTING_LABEL], state="closed", sort="created", direction="asc") |
| stats_list: list[Stats] = [] |
| for issue in issues: |
| stat = get_stats(issue) |
| if not table: |
| print(stat) |
| else: |
| stats_list.append(stat) |
| if table: |
| rows = stats_to_rows(stats_list) |
| print( |
| tabulate( |
| rows, |
| headers=( |
| "Issue", |
| "Num Providers", |
| "Num Issues", |
| "Tested Issues", |
| "Tested (%)", |
| "Involved", |
| "Commenting", |
| "Involved who commented", |
| "Extra people", |
| "User response (%)", |
| ), |
| tablefmt="github", |
| ) |
| ) |
| |
| |
| if __name__ == "__main__": |
| cli() |