diff --git a/creds-example.json b/creds-example.json index b32a030..823c716 100644 --- a/creds-example.json +++ b/creds-example.json @@ -1,11 +1,13 @@ { "bugzilla.suse.com": { + "user": "SUSE_BUGZILLA_USER", "api_key": "SUSE_BUGZILLA_API_KEY" }, "bugzilla.redhat.com": { "api_key": "REDHAT_BUGZILLA_API_KEY" }, "jira.suse.com": { + "username": "JIRA_USER", "cookies": { "JIRASESSIONID": "JIRA_SESSION_ID", "atlassian.xsrf.token": "JIRA_TOKEN" @@ -28,5 +30,8 @@ }, "code.opensuse.org": { "token": "PAGURE_TOKEN" + }, + "launchpad.net": { + "username": "LAUNCHPAD_USER" } } diff --git a/services/bugzilla.py b/services/bugzilla.py index 17b8439..d431381 100644 --- a/services/bugzilla.py +++ b/services/bugzilla.py @@ -14,6 +14,7 @@ from . import Service, Issue, debugme, status +# Reference: https://bugzilla.readthedocs.io/en/latest/api/index.html#apis class MyBugzilla(Service): """ Bugzilla @@ -39,6 +40,34 @@ def __del__(self): except (AttributeError, BugzillaError): pass + def _get_issues(self, filters: dict) -> list[Issue]: + return [ + self._to_issue(bug) for bug in self.client.query(filters) if bug.is_open + ] + + def get_assigned(self, username: str = "") -> list[Issue] | None: + """ + Get assigned Bugzilla issues + """ + username = username or self.client.user + try: + user = self.client.getuser(username) + return self._get_issues({"assigned_to": user.email}) + except (AttributeError, BugzillaError, RequestException) as exc: + logging.error("Bugzilla: %s: get_assigned(%s): %s", self.url, username, exc) + return None + + def get_created(self, username: str = "") -> list[Issue] | None: + """ + Get created Bugzilla issues + """ + try: + user = self.client.getuser(username) + return self._get_issues({"creator": user.email}) + except (AttributeError, BugzillaError, RequestException) as exc: + logging.error("Bugzilla: %s: get_created(%s): %s", self.url, username, exc) + return None + def get_issue(self, issue_id: str = "", **kwargs) -> Issue | None: """ Get Bugzilla issue diff --git a/services/gitea.py b/services/gitea.py index 4e36db5..006f395 100644 --- a/services/gitea.py +++ b/services/gitea.py @@ -9,6 +9,8 @@ # Reference: https://try.gitea.io/api/swagger +# Not possible to filter issues because of: +# https://github.com/go-gitea/gitea/issues/25979 class MyGitea(Generic): """ Gitea @@ -23,7 +25,7 @@ def __init__(self, url: str, creds: dict): def _to_issue(self, info: Any, **kwargs) -> Issue: repo = kwargs.pop("repo") - is_pr = kwargs.pop("is_pr") + is_pr = bool(kwargs.get("is_pr")) mark = "!" if is_pr else "#" return Issue( tag=f'{self.tag}#{repo}{mark}{info["number"]}', diff --git a/services/github.py b/services/github.py index ea846db..3865bc8 100644 --- a/services/github.py +++ b/services/github.py @@ -13,6 +13,8 @@ from . import Service, Issue, status +# Reference: +# https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests class MyGithub(Service): """ Github @@ -34,6 +36,44 @@ def __del__(self): except (AttributeError, GithubException): pass + def _get_issues(self, filters: str, is_pr: bool) -> list[Issue]: + issue_type = "pr" if is_pr else "issue" + filters = f"{filters} type:{issue_type}" + return [ + self._to_issue(issue, is_pr=is_pr) + for issue in self.client.search_issues(filters) + ] + + def get_assigned( + self, username: str | None = None, is_pr: bool = False, state: str = "open" + ) -> list[Issue] | None: + """ + Get assigned Github issues + """ + try: + user = ( + self.client.get_user(username) if username else self.client.get_user() + ) + return self._get_issues(f"assignee:{user.login} state:{state}", is_pr) + except (GithubException, RequestException) as exc: + logging.error("Github: get_assigned(%s): %s", username, exc) + return None + + def get_created( + self, username: str | None = None, is_pr: bool = False, state: str = "open" + ) -> list[Issue] | None: + """ + Get created Github issues + """ + try: + user = ( + self.client.get_user(username) if username else self.client.get_user() + ) + return self._get_issues(f"author:{user.login} state:{state}", is_pr) + except (GithubException, RequestException) as exc: + logging.error("Github: get_created(%s): %s", username, exc) + return None + def get_issue(self, issue_id: str = "", **kwargs) -> Issue | None: """ Get Github issue @@ -59,7 +99,8 @@ def get_issue(self, issue_id: str = "", **kwargs) -> Issue | None: return None return self._to_issue(info, repo, is_pr) - def _to_issue(self, info: Any, repo: str, is_pr: bool) -> Issue: + def _to_issue(self, info: Any, repo: str = "", is_pr: bool = False) -> Issue: + repo = repo or info.repository.full_name mark = "!" if is_pr else "#" return Issue( tag=f"{self.tag}#{repo}{mark}{info.number}", diff --git a/services/gitlab.py b/services/gitlab.py index 7250cb5..fc9a561 100644 --- a/services/gitlab.py +++ b/services/gitlab.py @@ -15,6 +15,9 @@ from . import Service, Issue, debugme, status +# References: +# https://docs.gitlab.com/ee/api/issues.html +# https://docs.gitlab.com/ee/api/merge_requests.html class MyGitlab(Service): """ Gitlab @@ -42,6 +45,54 @@ def __del__(self): except (AttributeError, GitlabError): pass + def _get_issues(self, **filters) -> list[Issue]: + return [ + self._to_issue(issue) + for issue in self.client.issues.list(all=True, **filters) + ] + + def _get_pullrequests(self, **filters) -> list[Issue]: + return [ + self._to_issue(issue) + for issue in self.client.mergerequests.list(all=True, **filters) + ] + + def get_assigned( + self, username: str | None = None, is_pr: bool = False, state: str = "opened" + ) -> list[Issue] | None: + """ + Get assigned Github issues + """ + try: + if username: + user = self.client.users.list(username=username)[0] # type: ignore + else: + user = self.client.user + if is_pr: + return self._get_pullrequests(assignee_id=user.id, state=state) + return self._get_issues(assignee_id=user.id, state=state) + except (GitlabError, RequestException) as exc: + logging.error("Gitlab: %s: get_assigned(%s): %s", self.url, username, exc) + return None + + def get_created( + self, username: str | None = None, is_pr: bool = False, state: str = "opened" + ) -> list[Issue] | None: + """ + Get created Github issues + """ + try: + if username: + user = self.client.users.list(username=username)[0] # type: ignore + else: + user = self.client.user + if is_pr: + return self._get_pullrequests(author=user.id, state=state) + return self._get_issues(author=user.id, state=state) + except (GitlabError, RequestException) as exc: + logging.error("Gitlab: %s: get_created(%s): %s", self.url, username, exc) + return None + def get_issue(self, issue_id: str = "", **kwargs) -> Issue | None: """ Get Gitlab issue diff --git a/services/jira.py b/services/jira.py index 003620d..26015e7 100644 --- a/services/jira.py +++ b/services/jira.py @@ -31,6 +31,37 @@ def __del__(self): except AttributeError: pass + def _get_issues(self, filters: str) -> list[Issue]: + filters = f"{filters} AND resolution IS EMPTY" + data = self.client.jql(filters) + issues = data["issues"] + while len(issues) < data["total"]: + data = self.client.jql(filters, start=len(issues)) + issues.extend(data["issues"]) + return [self._to_issue(issue) for issue in issues] + + def get_assigned(self, username: str = "") -> list[Issue] | None: + """ + Get assigned Jira issues + """ + username = username or self.client.username + try: + return self._get_issues(f"assignee = {username}") + except (ApiError, RequestException) as exc: + logging.error("Jira: %s: get_assigned(%s): %s", self.url, username, exc) + return None + + def get_created(self, username: str = "") -> list[Issue] | None: + """ + Get created Jira issues + """ + username = username or self.client.username + try: + return self._get_issues(f"reporter = {username}") + except (ApiError, RequestException) as exc: + logging.error("Jira: %s: get_created(%s): %s", self.url, username, exc) + return None + def get_issue(self, issue_id: str = "", **kwargs) -> Issue | None: """ Get Jira ticket diff --git a/services/launchpad.py b/services/launchpad.py index 2f8271e..0804b4c 100644 --- a/services/launchpad.py +++ b/services/launchpad.py @@ -30,6 +30,39 @@ def __init__(self, url: str, creds: dict): self.issue_api_url = "https://api.launchpad.net/1.0/{repo}/+bug/{issue}" self.issue_web_url = f"{self.url}/{{repo}}/+bug/{{issue}}" self.tag = "lp" + self.username = creds.get("username", "") + + # NOTE: Filter closed and PR's + def _get_issues(self, filters: dict) -> list[Issue]: + params = {"ws.op": "searchTasks"} | filters + got = self.session.get("https://api.launchpad.net/1.0/bugs", params=params) + got.raise_for_status() + entries = got.json()["entries"] + return [self._to_issue(entry) for entry in entries] + + def get_assigned(self, username: str = "") -> list[Issue] | None: + """ + Get assigned Launchpad issues + """ + username = username or self.username + url = f"https://api.launchpad.net/1.0/~{username}" + try: + return self._get_issues({"assignee": url}) + except RequestException as exc: + logging.error("Launchpad: get_assigned(%s): %s", username, exc) + return None + + def get_created(self, username: str = "") -> list[Issue] | None: + """ + Get created Launchpad issues + """ + username = username or self.username + url = f"https://api.launchpad.net/1.0/~{username}" + try: + return self._get_issues({"bug_reporter": url}) + except RequestException as exc: + logging.error("Launchpad: get_created(%s): %s", username, exc) + return None def get_issue(self, issue_id: str = "", **kwargs) -> Issue | None: if not kwargs.get("repo"): diff --git a/services/pagure.py b/services/pagure.py index 7acddad..64a50c0 100644 --- a/services/pagure.py +++ b/services/pagure.py @@ -2,8 +2,11 @@ Pagure """ +import logging from typing import Any +from requests.exceptions import RequestException + from utils import utc_date from . import Generic, Issue, status @@ -20,10 +23,96 @@ def __init__(self, url: str, creds: dict): self.issue_web_url = f"{self.url}/{{repo}}/issue/{{issue}}" self.pr_api_url = f"{self.url}/api/0/{{repo}}/pull-request/{{issue}}" self.pr_web_url = f"{self.url}/{{repo}}/pull-request/{{issue}}" + self._username: str | None = None + + @property + def username(self) -> str: + """ + Get username + """ + if self._username is None: + try: + response = self.session.post(f"{self.url}/api/0/-/whoami", timeout=10) + response.raise_for_status() + except RequestException as exc: + logging.error("Pagure: %s: whoami(): %s", self.url, exc) + raise + self._username = response.json()["username"] + return self._username + + def _get_issues(self, username: str, **params) -> list[Issue]: + if params["assignee"]: + key = "issues_assigned" + next_key = "pagination_issues_assigned" + else: + key = "issues_created" + next_key = "pagination_issues_created" + got = self.session.get( + f"{self.url}/api/0/user/{username}/issues", params=params + ) + got.raise_for_status() + data = got.json() + entries = data[key] + while data[next_key]["next"]: + got = self.session.get(data[next_key]["next"], params=params) + got.raise_for_status() + data = got.json() + entries.extend(data[key]) + return [self._to_issue(entry) for entry in entries] + + def _get_pullrequests( + self, username: str, created: bool = False, state: str = "Open" + ) -> list[Issue]: + pr_type = "filed" if created else "actionable" + params = { + "status": state, + } + got = self.session.get( + f"{self.url}/api/0/user/{username}/requests/{pr_type}", params=params + ) + got.raise_for_status() + data = got.json() + entries = data["requests"] + while data["pagination"]["next"]: + got = self.session.get(data["pagination"]["next"], params=params) + got.raise_for_status() + data = got.json() + entries.extend(data["requests"]) + return [self._to_issue(entry, is_pr=True) for entry in entries] + + def get_assigned( + self, username: str = "", is_pr: bool = False, state: str = "Open" + ) -> list[Issue] | None: + """ + Get assigned Pagure issues + """ + username = username or self.username + try: + if is_pr: + return self._get_pullrequests(username, created=False, state=state) + return self._get_issues(username, assignee=1, author=0, status=state) + except RequestException as exc: + logging.error("Pagure: %s: get_assigned(%s): %s", self.url, username, exc) + return None + + def get_created( + self, username: str, is_pr: bool = False, state: str = "Open" + ) -> list[Issue] | None: + """ + Get assigned Pagure issues + """ + username = username or self.username + try: + if is_pr: + return self._get_pullrequests(username, created=True, state=state) + return self._get_issues(username, assignee=0, author=1, status=state) + except RequestException as exc: + logging.error("Pagure: %s: get_created(%s): %s", self.url, username, exc) + return None def _to_issue(self, info: Any, **kwargs) -> Issue: - repo = kwargs.pop("repo") - is_pr = kwargs.pop("is_pr") + repo = kwargs.get("repo", "") or info["project"]["fullname"] + is_pr = bool(kwargs.get("is_pr")) mark = "!" if is_pr else "#" return Issue( tag=f'{self.tag}#{repo}{mark}{info["id"]}', diff --git a/services/redmine.py b/services/redmine.py index 8166376..24f20e6 100644 --- a/services/redmine.py +++ b/services/redmine.py @@ -36,6 +36,35 @@ def __del__(self): except AttributeError: pass + def _get_issues(self, **filters) -> list[Issue]: + return [self._to_issue(issue) for issue in self.client.issue.filter(**filters)] + + def get_assigned( + self, username: str = "me", state: str = "open" + ) -> list[Issue] | None: + """ + Get assigned Redmine tickets + """ + try: + user = self.client.user.get(username) + return self._get_issues(assigned_to_id=user.id, status=state) + except (BaseRedmineError, RequestException) as exc: + logging.error("Redmine: %s: get_assigned(%s): %s", self.url, username, exc) + return None + + def get_created( + self, username: str = "me", state: str = "open" + ) -> list[Issue] | None: + """ + Get created Redmine tickets + """ + try: + user = self.client.user.get(username) + return self._get_issues(author_id=user.id, status=state) + except (BaseRedmineError, RequestException) as exc: + logging.error("Redmine: %s: get_created(%s): %s", self.url, username, exc) + return None + def get_issue(self, issue_id: str = "", **kwargs) -> Issue | None: """ Get Redmine ticket