Coverage for scripts / get_request_status.py: 18%

101 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-25 18:08 -0500

1#!/usr/bin/env python3 

2""" 

3Get JSM request status history. 

4 

5Usage: 

6 python get_request_status.py SD-101 

7 python get_request_status.py SD-101 --show-durations 

8 python get_request_status.py SD-101 --output json 

9""" 

10 

11import sys 

12import os 

13import argparse 

14import json 

15from pathlib import Path 

16from typing import Optional, Dict, Any, List 

17import time 

18 

19sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'shared' / 'scripts' / 'lib')) 

20 

21from config_manager import get_jira_client 

22from error_handler import print_error, JiraError, NotFoundError 

23 

24 

25def get_status_history(issue_key: str, profile: Optional[str] = None) -> Dict[str, Any]: 

26 """ 

27 Get request status history. 

28 

29 Args: 

30 issue_key: Request key 

31 profile: JIRA profile to use 

32 

33 Returns: 

34 Status history data 

35 

36 Raises: 

37 NotFoundError: If request doesn't exist 

38 """ 

39 with get_jira_client(profile) as client: 

40 return client.get_request_status(issue_key) 

41 

42 

43def format_duration(millis: int) -> str: 

44 """Format milliseconds to human-readable duration.""" 

45 hours = millis // 3600000 

46 minutes = (millis % 3600000) // 60000 

47 

48 if hours > 0: 

49 return f"{hours}h {minutes}m" 

50 else: 

51 return f"{minutes}m" 

52 

53 

54def calculate_durations(statuses: List[Dict[str, Any]]) -> Dict[str, int]: 

55 """ 

56 Calculate time spent in each status. 

57 

58 Args: 

59 statuses: List of status history entries 

60 

61 Returns: 

62 Dictionary mapping status names to duration in milliseconds 

63 """ 

64 durations = {} 

65 current_time = int(time.time() * 1000) 

66 

67 for i, status in enumerate(statuses): 

68 status_name = status['status'] 

69 start_time = status['statusDate']['epochMillis'] 

70 

71 if i + 1 < len(statuses): 

72 end_time = statuses[i + 1]['statusDate']['epochMillis'] 

73 else: 

74 # Current status - use now 

75 end_time = current_time 

76 

77 duration = end_time - start_time 

78 durations[status_name] = duration 

79 

80 return durations 

81 

82 

83def calculate_metrics(statuses: List[Dict[str, Any]]) -> Dict[str, Any]: 

84 """ 

85 Calculate status change metrics. 

86 

87 Args: 

88 statuses: List of status history entries 

89 

90 Returns: 

91 Dictionary with metrics 

92 """ 

93 if not statuses: 

94 return {} 

95 

96 durations = calculate_durations(statuses) 

97 total_time = sum(durations.values()) 

98 

99 # Time to first response (first status change) 

100 time_to_first_response = None 

101 if len(statuses) > 1: 

102 time_to_first_response = statuses[1]['statusDate']['epochMillis'] - \ 

103 statuses[0]['statusDate']['epochMillis'] 

104 

105 # Time to resolution (if resolved) 

106 time_to_resolution = None 

107 last_status = statuses[-1] 

108 if last_status.get('statusCategory') == 'DONE': 

109 time_to_resolution = total_time 

110 

111 return { 

112 'total_time': total_time, 

113 'status_changes': len(statuses), 

114 'time_to_first_response': time_to_first_response, 

115 'time_to_resolution': time_to_resolution 

116 } 

117 

118 

119def format_timeline(statuses: List[Dict[str, Any]], show_durations: bool = False) -> str: 

120 """Format status history as timeline.""" 

121 if not statuses: 

122 return "No status history found." 

123 

124 lines = [] 

125 header = f"\n{'Status':<25} {'Category':<15} {'Changed':<25}" 

126 if show_durations: 

127 header += f" {'Duration'}" 

128 lines.append(header) 

129 

130 lines.append("-" * 80) 

131 

132 durations = calculate_durations(statuses) if show_durations else {} 

133 

134 for status in statuses: 

135 status_name = status.get('status', 'N/A') 

136 category = status.get('statusCategory', 'N/A') 

137 changed = status.get('statusDate', {}).get('friendly', 'N/A') 

138 

139 line = f"{status_name:<25} {category:<15} {changed:<25}" 

140 

141 if show_durations and status_name in durations: 

142 duration = format_duration(durations[status_name]) 

143 line += f" {duration}" 

144 

145 lines.append(line) 

146 

147 # Add metrics 

148 if show_durations: 

149 metrics = calculate_metrics(statuses) 

150 

151 lines.append('') 

152 lines.append(f"Total Time: {format_duration(metrics['total_time'])}") 

153 if metrics.get('time_to_first_response'): 

154 lines.append(f"Time to First Response: {format_duration(metrics['time_to_first_response'])}") 

155 if metrics.get('time_to_resolution'): 

156 lines.append(f"Time to Resolution: {format_duration(metrics['time_to_resolution'])}") 

157 lines.append(f"Status Changes: {metrics['status_changes']}") 

158 

159 return '\n'.join(lines) 

160 

161 

162def format_json(statuses: List[Dict[str, Any]]) -> str: 

163 """Format status history as JSON.""" 

164 return json.dumps(statuses, indent=2) 

165 

166 

167def main(): 

168 """Main entry point.""" 

169 parser = argparse.ArgumentParser( 

170 description='Get JSM request status history', 

171 formatter_class=argparse.RawDescriptionHelpFormatter, 

172 epilog=""" 

173Examples: 

174 Basic usage: 

175 %(prog)s SD-101 

176 

177 With durations: 

178 %(prog)s SD-101 --show-durations 

179 

180 JSON output: 

181 %(prog)s SD-101 --output json 

182 """ 

183 ) 

184 

185 parser.add_argument('request_key', 

186 help='Request key (e.g., SD-101)') 

187 parser.add_argument('--show-durations', action='store_true', 

188 help='Show time spent in each status') 

189 parser.add_argument('--output', choices=['text', 'json'], default='text', 

190 help='Output format (default: text)') 

191 parser.add_argument('--profile', 

192 help='JIRA profile to use from config') 

193 

194 args = parser.parse_args() 

195 

196 try: 

197 history = get_status_history(args.request_key, args.profile) 

198 

199 statuses = history.get('values', []) 

200 

201 if args.output == 'json': 

202 print(format_json(statuses)) 

203 else: 

204 print(f"Status History for {args.request_key}:") 

205 print(format_timeline(statuses, show_durations=args.show_durations)) 

206 

207 return 0 

208 

209 except NotFoundError as e: 

210 print_error(f"Request not found: {e}") 

211 return 1 

212 except JiraError as e: 

213 print_error(f"Failed to get status history: {e}") 

214 return 1 

215 except Exception as e: 

216 print_error(f"Unexpected error: {e}") 

217 return 1 

218 

219 

220if __name__ == '__main__': 

221 sys.exit(main())