diff --git a/src/ghlabel/cli.py b/src/ghlabel/cli.py index 02615a1..1d51f7c 100644 --- a/src/ghlabel/cli.py +++ b/src/ghlabel/cli.py @@ -195,6 +195,16 @@ def setup_labels( # noqa: PLR0913 gh_label.add_labels(labels=parse_add_labels(add_labels), preview=preview) + if gh_label.labels_unsafe_to_remove: + if not preview: + rich.print() + rich.print(" The following labels are not [red]removed[/red]:") + for label_name in gh_label.labels_unsafe_to_remove: + rich.print( + f' - {label_name} \[{", ".join(url for url in gh_label.label_name_urls_map[label_name])}]' + ) + rich.print() + @app.command("dump", help="Generate starter labels config files.") # type: ignore[misc] def app_dump( diff --git a/src/ghlabel/utils/github_api.py b/src/ghlabel/utils/github_api.py index e4f0105..d3da2b7 100644 --- a/src/ghlabel/utils/github_api.py +++ b/src/ghlabel/utils/github_api.py @@ -8,7 +8,14 @@ from requests.exceptions import HTTPError, Timeout from requests.models import Response -from ghlabel.utils.github_api_types import GithubIssue, GithubLabel, StatusCode +from ghlabel.utils.github_api_types import ( + GithubIssue, + GithubIssueParams, + GithubLabel, + GithubPullRequest, + StatusCode, +) +from ghlabel.utils.helpers import STATUS_OK, validate_env Path("logs").mkdir(exist_ok=True) fileConfig(os.path.join(os.path.dirname(__file__), "../logging.ini")) @@ -166,7 +173,7 @@ def delete_label(self, label_name: str) -> tuple[None, StatusCode]: return None, res.status_code def list_issues( - self, labels: list[GithubLabel] | None = None + self, label_names: set[str] | None = None, state: str = "all" ) -> tuple[list[GithubIssue], StatusCode]: """ Issue queried include PRs. PR has "pull_request" key. @@ -174,10 +181,13 @@ def list_issues( url: str = f"{self.base_url}/issues" res: Response + params: GithubIssueParams = {} - if labels: - labels_str: str = ",".join(label["name"] for label in labels) - url += f"?labels={labels_str}" + if label_names: + params["labels"] = ",".join(label_name for label_name in label_names) + + if state: + params["state"] = state page: int = 1 per_page: int = 100 @@ -187,13 +197,14 @@ def list_issues( ) github_issues: list[GithubIssue] = [] while True: - params: dict[str, int] = {"page": page, "per_page": per_page} + params["page"] = page + params["per_page"] = per_page logging.info(f"Fetching page {page}.") try: res = requests.get( url, headers=self.headers, - params=params, + params=params, # type: ignore[arg-type] timeout=10, ) res.raise_for_status() @@ -215,3 +226,19 @@ def list_issues( page += 1 return github_issues, res.status_code + + +if __name__ == "__main__": + gh_api = GithubApi( + validate_env("GITHUB_TOKEN"), + validate_env("GITHUB_REPO_OWNER"), + validate_env("GITHUB_REPO_NAME"), + ) + gh_issues, status_code = gh_api.list_issues() + if status_code != STATUS_OK: + sys.exit() + gh_pull_requests: list[GithubPullRequest] = [] + for issue in gh_issues: + if "pull_request" in issue: + gh_pull_requests.append(issue) + print(gh_pull_requests) diff --git a/src/ghlabel/utils/github_api_types.py b/src/ghlabel/utils/github_api_types.py index ccbed4a..32b74df 100644 --- a/src/ghlabel/utils/github_api_types.py +++ b/src/ghlabel/utils/github_api_types.py @@ -3,6 +3,11 @@ StatusCode = int +class GithubParams(TypedDict): + page: NotRequired[int] + per_page: NotRequired[int] + + class GithubLabel(TypedDict): name: str new_name: NotRequired[str] @@ -11,4 +16,15 @@ class GithubLabel(TypedDict): class GithubIssue(TypedDict): + html_url: str + pull_request: NotRequired[dict[str, str]] + labels: list[GithubLabel] + + +class GithubIssueParams(GithubParams): + labels: NotRequired[str] + state: NotRequired[str] + + +class GithubPullRequest(TypedDict): labels: list[GithubLabel] diff --git a/src/ghlabel/utils/helpers.py b/src/ghlabel/utils/helpers.py index e8e94d1..589050e 100644 --- a/src/ghlabel/utils/helpers.py +++ b/src/ghlabel/utils/helpers.py @@ -10,6 +10,8 @@ Path("logs").mkdir(exist_ok=True) fileConfig(os.path.join(os.path.dirname(__file__), "../logging.ini")) +STATUS_OK: int = 200 + def validate_env(env: str) -> str: _env: str | None = os.getenv(env) diff --git a/src/ghlabel/utils/setup_github_label.py b/src/ghlabel/utils/setup_github_label.py index 4f588e2..0433497 100644 --- a/src/ghlabel/utils/setup_github_label.py +++ b/src/ghlabel/utils/setup_github_label.py @@ -24,15 +24,13 @@ from dotenv import load_dotenv from ghlabel.utils.github_api import GithubApi -from ghlabel.utils.github_api_types import GithubLabel -from ghlabel.utils.helpers import validate_env +from ghlabel.utils.github_api_types import GithubIssue, GithubLabel +from ghlabel.utils.helpers import STATUS_OK, validate_env load_dotenv() Path("logs").mkdir(exist_ok=True) fileConfig(os.path.join(os.path.dirname(__file__), "../logging.ini")) -STATUS_OK: int = 200 - class SetupGithubLabel: def __init__( @@ -40,18 +38,19 @@ def __init__( gh_api: GithubApi, labels_dir: str = "labels", ) -> None: - self._labels_dir: str = labels_dir - self._gh_api: GithubApi = gh_api - self._github_labels = self._fetch_formatted_github_labels() - self._github_label_names: list[ - str - ] = [ # requires index, so using list index instead of set - github_label["name"] for github_label in self.github_labels + self._labels_dir = labels_dir + self._gh_api = gh_api + + self._github_labels: list[GithubLabel] = self._fetch_formatted_github_labels() + self._github_label_names: list[str] = [ + # requires index, so using list instead of set + github_label["name"] + for github_label in self.github_labels ] self._labels: list[GithubLabel] = self._load_labels_from_config() or [] - self._labels_to_remove: set[str] = set( - self._load_labels_to_remove_from_config() or [] - ) + self._label_name_urls_map: dict[str, set[str]] = {} + self._labels_unsafe_to_remove: set[str] = set() + self._labels_safe_to_remove: set[str] = self._list_labels_safe_to_remove() @property def labels_dir(self) -> str: @@ -70,8 +69,16 @@ def labels(self) -> list[GithubLabel]: return self._labels @property - def labels_to_remove(self) -> set[str]: - return self._labels_to_remove + def label_name_urls_map(self) -> dict[str, set[str]]: + return self._label_name_urls_map + + @property + def labels_unsafe_to_remove(self) -> set[str]: + return self._labels_unsafe_to_remove + + @property + def labels_safe_to_remove(self) -> set[str]: + return self._labels_safe_to_remove @property def gh_api(self) -> GithubApi: @@ -90,6 +97,31 @@ def _format_github_label(self, github_label: GithubLabel) -> GithubLabel: if key not in ["id", "node_id", "url", "default"] } + def _list_labels_safe_to_remove(self) -> set[str]: + all_labels_to_remove: set[str] = set( + self._load_labels_to_remove_from_config() or [] + ) + labels_unsafe_to_remove: set[str] = set() + + gh_issues: list[GithubIssue] + gh_issues, status_code = self.gh_api.list_issues() + if status_code != STATUS_OK: + sys.exit() + for issue in gh_issues: + url = issue["html_url"] + if "pull_request" in issue: + url = issue["pull_request"]["html_url"] + + for label in issue["labels"]: + labels_unsafe_to_remove.add(label["name"]) + if label["name"] not in self._label_name_urls_map: + self._label_name_urls_map[label["name"]] = set([url]) + else: + self._label_name_urls_map[label["name"]].add(url) + + self._labels_unsafe_to_remove = labels_unsafe_to_remove + return all_labels_to_remove - labels_unsafe_to_remove + def _load_labels_from_config(self) -> list[GithubLabel]: use_labels: list[GithubLabel] = [] labels: list[GithubLabel] = [] @@ -223,7 +255,7 @@ def remove_labels( strict: bool = False, preview: bool = False, ) -> None: - labels_to_remove: set[str] = self.labels_to_remove + labels_to_remove: set[str] = self.labels_safe_to_remove if strict: labels_to_remove.update(