File indexing completed on 2024-12-08 08:08:25

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."))