Source code for syncgitlab2msproject.gitlab_issues

import dateutil.parser
from datetime import datetime
from gitlab import Gitlab
from logging import getLogger
from typing import List, Optional

from .custom_types import GitlabIssue, GitlabUserDict
from .exceptions import MovedIssueNotDefined

logger = getLogger(f"{__package__}.{__name__}")


[docs]def get_user_identifier(user_dict: GitlabUserDict) -> str: """ Return the user identifier keep as separate function to allow easier changes later if required """ return str(user_dict["name"])
[docs]class Issue: """ Wrapper class around Group/Project Issues """ # The issue object itself is not dynamic only the contained obj is! __slots__ = ["obj", "_moved_reference"] def __init__(self, obj: GitlabIssue): self.obj: GitlabIssue = obj self._moved_reference: Optional[Issue] = None def __getattr__(self, item: str): """Default to get the values from the original objext""" return getattr(self.obj, item) @property def moved_reference(self) -> Optional["Issue"]: """ get the reference to the moved issue if defined :exceptions MovedIssueNotDefined """ if self.moved_to_id is None: return None else: if self._moved_reference is None: raise MovedIssueNotDefined( "The issue is marked as moved but was not referenced " "in the loaded issues, so tracking is not possible." ) else: return self._moved_reference @moved_reference.setter def moved_reference(self, value: "Issue"): if not isinstance(value, Issue): raise ValueError("Can only set an Issue object as moved reference!") self._moved_reference = value def __str__(self): return f"'{self.title}' (ID: {self.id})" # ************************************************************** # *** Define some default properties to allow static typing *** # ************************************************************** @property def id(self) -> int: """ The id of an issue - it seems to be unique within an installation """ return self.obj.id @property def iid(self) -> int: return self.obj.iid @property def project_id(self) -> int: return self.obj.project_id @property def group_id(self) -> int: return self.obj.group_id @property def has_tasks(self) -> bool: return self.obj.has_tasks @property def is_closed(self) -> bool: return str(self.obj.state).lower().strip().startswith("closed") @property def is_open(self): return not self.is_closed @property def percentage_tasks_done(self) -> int: """ Percentage of tasks done, 0 if no tasks are defined and not closed. By definition always 100 if issue is closed (and not moved) :exceptions MovedIssueNotDefined """ if self.is_closed: if self.moved_to_id is not None: # Needed for assert self._moved_reference is not None return self._moved_reference.percentage_tasks_done return 100 if not self.has_tasks: return 0 task = self.task_completion_status return round(task["completed_count"] / task["count"] * 100) @property def moved_to_id(self) -> Optional[int]: return self.obj.moved_to_id @property def title(self) -> str: return self.obj.title @property def description(self) -> str: return self.obj.description @property def closed_at(self) -> Optional[datetime]: if (val := self.obj.closed_at) is not None: return dateutil.parser.parse(val) return None @property def due_date(self) -> Optional[datetime]: if (val := self.obj.due_date) is not None: return dateutil.parser.parse(val) return None @property def closed_by(self) -> Optional[str]: if (val := self.obj.closed_by) is not None: return get_user_identifier(val) return None @property def time_estimated(self) -> float: """ Time estimated in minutes """ return self.obj.time_stats["time_estimate"] / 60 @property def time_spent_total(self) -> float: """ Total time spent in minutes """ return self.obj.time_stats["total_time_spent"] / 60 @property def assignees(self) -> List[str]: """ list of Gitlab Assignees. Note in the community edition only one assignee is possible """ return [get_user_identifier(user) for user in self.obj.assignees] @property def labels(self) -> List[str]: """ list of labels """ return self.obj.labels @property def web_url(self) -> str: """ give the url from which the issue can be accessed """ return self.obj.web_url
[docs]def get_gitlab_class(server: str, personal_token: Optional[str] = None) -> Gitlab: if personal_token is None: return Gitlab(server) else: return Gitlab(server, private_token=personal_token)
[docs]def get_group_issues(gitlab: Gitlab, group_id: int) -> List[Issue]: group = gitlab.groups.get(group_id, lazy=True) return [Issue(issue) for issue in group.issues.list(all=True)]
[docs]def get_project_issues(gitlab: Gitlab, project_id: int) -> List[Issue]: project = gitlab.projects.get(project_id, lazy=True) return [Issue(issue) for issue in project.issues.list(all=True)]