File indexing completed on 2024-12-01 13:46:01
0001 """ 0002 Module with functionality around single issues. 0003 """ 0004 import argparse 0005 import sys 0006 from typing import Dict, Any, Callable 0007 0008 from gitlab import GitlabGetError 0009 from gitlab.v4.objects import ProjectIssue 0010 0011 from lab.repositoryconnection import RepositoryConnection 0012 from lab.utils import Utils, LogType, TextFormatting, is_valid_time_str 0013 0014 0015 def parser( 0016 subparsers: argparse._SubParsersAction, # pylint: disable=protected-access 0017 ) -> argparse.ArgumentParser: 0018 """ 0019 Subparser for issue command 0020 :param subparsers: subparsers object from global parser 0021 :return: issues subparser 0022 """ 0023 0024 issue_parser: argparse.ArgumentParser = subparsers.add_parser( 0025 "issue", help="Gitlab issue commands." 0026 ) 0027 0028 issue_parser.add_argument("issue_id", help="Issue ID", metavar="issue_id", type=int) 0029 0030 issue_subparsers = issue_parser.add_subparsers( 0031 dest="command", required=True, help="Issue sub command" 0032 ) 0033 0034 estimate_parser = issue_subparsers.add_parser("estimate") 0035 estimate_parser_group = estimate_parser.add_mutually_exclusive_group() 0036 0037 spend_parser = issue_subparsers.add_parser("spend") 0038 spend_parser_group = spend_parser.add_mutually_exclusive_group() 0039 0040 estimate_parser_group.add_argument( 0041 "--update", 0042 help="Update estimated time (override). E.g. '2d4h'.", 0043 metavar="time_str", 0044 type=str, 0045 ) 0046 estimate_parser_group.add_argument( 0047 "--reset", 0048 help="Reset a time estimate for an issue.", 0049 action="store_true", 0050 ) 0051 0052 spend_parser_group.add_argument( 0053 "--update", 0054 help="Add new time entry (time spent). E.g. '5h30m'.", 0055 metavar="time_str", 0056 type=str, 0057 ) 0058 spend_parser_group.add_argument( 0059 "--reset", 0060 help="Reset spent time for an issue.", 0061 action="store_true", 0062 ) 0063 0064 return issue_parser 0065 0066 0067 def run(args: argparse.Namespace) -> None: 0068 """ 0069 Run issue command. 0070 :param args: parsed arguments 0071 """ 0072 issue = IssueConnection(args.issue_id) 0073 if args.command == "estimate": 0074 if args.update: 0075 issue.update_estimated(args.update) 0076 elif args.reset: 0077 issue.reset_time_estimate() 0078 else: 0079 issue.print_estimated() 0080 elif args.command == "spend": 0081 if args.update: 0082 issue.update_spent(args.update) 0083 elif args.reset: 0084 issue.reset_spent_time() 0085 else: 0086 issue.print_spent() 0087 0088 0089 class IssueConnection(RepositoryConnection): 0090 def __init__(self, issue_id: int): 0091 """ 0092 Creates a new issue connection. Requires a valid issue ID for the current project. 0093 """ 0094 RepositoryConnection.__init__(self) 0095 try: 0096 self.issue: ProjectIssue = self._remote_project.issues.get(issue_id, lazy=False) 0097 except GitlabGetError: 0098 Utils.log(LogType.WARNING, f"No issue with ID {issue_id}") 0099 sys.exit(1) 0100 0101 @property 0102 def title_bold(self) -> str: 0103 """Get the title formatted as f´bold text.""" 0104 return f"{TextFormatting.BOLD}{self.issue.title}{TextFormatting.END}" 0105 0106 @property 0107 def overdue(self) -> bool: 0108 """True if the issue has more time spent than was originally estimated.""" 0109 ts: Dict[str, Any] = self.issue.attributes["time_stats"] 0110 return bool(ts["time_estimate"] <= ts["total_time_spent"]) 0111 0112 def print_estimated(self) -> None: 0113 """Print short info about the estimated time for the issue.""" 0114 # API endpoint https://python-gitlab.readthedocs.io/en/stable/gl_objects/issues.html 0115 ts: Dict[str, Any] = self.issue.attributes["time_stats"] 0116 0117 color: Callable[[str], str] = TextFormatting.red if self.overdue else TextFormatting.green 0118 estimated: str = ts["human_time_estimate"] or "0h" 0119 spent: str = color(ts["human_total_time_spent"] or "0h") 0120 0121 text = f"{self.title_bold} is estimated at {estimated} (spent: {spent})" 0122 0123 print(text) 0124 0125 def update_estimated(self, time_str: str) -> None: 0126 """Updates the estimated time for the issue. Overrides the old value.""" 0127 if not is_valid_time_str(time_str): 0128 Utils.log(LogType.WARNING, f"{time_str} is an invalid time string.") 0129 sys.exit(1) 0130 0131 self.issue.time_estimate(time_str) 0132 self.issue.save() 0133 print(TextFormatting.green(f"Set estimate to {time_str}")) 0134 0135 def print_spent(self) -> None: 0136 """Prints a short info about the total time spent on this issue.""" 0137 # API endpoint https://python-gitlab.readthedocs.io/en/stable/gl_objects/issues.html 0138 ts: Dict[str, Any] = self.issue.attributes["time_stats"] 0139 0140 color: Callable[[str], str] = TextFormatting.red if self.overdue else TextFormatting.green 0141 estimated: str = color(ts["human_time_estimate"] or "0h") 0142 spent: str = ts["human_total_time_spent"] or "0h" 0143 0144 text = f"{self.title_bold} has {spent} tracked (estimated: {estimated})" 0145 0146 print(text) 0147 0148 def update_spent(self, time_str: str) -> None: 0149 """Adds time spent to the already existing time spent.""" 0150 if not is_valid_time_str(time_str): 0151 Utils.log(LogType.WARNING, f"{time_str} is an invalid time string.") 0152 sys.exit(1) 0153 0154 self.issue.add_spent_time(time_str) 0155 self.issue.save() 0156 print(TextFormatting.green(f"Added time entry of {time_str}")) 0157 0158 def reset_time_estimate(self) -> None: 0159 """Reset time estimate on an issue""" 0160 self.issue.reset_time_estimate() 0161 print(TextFormatting.green(f"Time estimate reset.")) 0162 0163 def reset_spent_time(self) -> None: 0164 """Rest time spent on an issue""" 0165 self.issue.reset_spent_time() 0166 print(TextFormatting.green(f"Spent time reset."))