Coverage for src/pytribeam/cicd/utilities.py: 19%

119 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2026-06-16 18:30 +0000

1#!/usr/bin/env python3 

2""" 

3Utilities for CICD processes. 

4""" 

5 

6import argparse 

7import json 

8import re 

9from datetime import datetime 

10from typing import List, NamedTuple, Tuple 

11 

12import pytz 

13import requests 

14 

15 

16class ReportMetadata(NamedTuple): 

17 """Container for CI/CD metadata used in reports.""" 

18 

19 timestamp: str 

20 run_id: str 

21 ref_name: str 

22 github_sha: str 

23 github_repo: str 

24 

25 

26def add_common_args(parser: argparse.ArgumentParser) -> None: 

27 """ 

28 Add shared CI/CD arguments to an argument parser. 

29 

30 Args: 

31 parser: The argparse.ArgumentParser to update. 

32 """ 

33 parser.add_argument( 

34 "--timestamp", required=True, help="UTC timestamp, e.g., 20240101_120000_UTC" 

35 ) 

36 parser.add_argument("--run_id", required=True, help="GitHub Actions run ID") 

37 parser.add_argument("--ref_name", required=True, help="Git reference name (branch)") 

38 parser.add_argument("--github_sha", required=True, help="GitHub commit SHA") 

39 parser.add_argument("--github_repo", required=True, help="GitHub repository name") 

40 

41 

42def add_badge_args(parser: argparse.ArgumentParser) -> None: 

43 """ 

44 Add shared arguments for badge generation scripts. 

45 

46 Args: 

47 parser: The argparse.ArgumentParser to update. 

48 """ 

49 parser.add_argument("--output_dir", help="Directory to save badges") 

50 parser.add_argument("--github_repo", help="owner/repo") 

51 parser.add_argument("--deploy_subdir", help="main or dev") 

52 parser.add_argument("--run_id", help="GitHub Run ID") 

53 parser.add_argument("--github_server_url", default="https://github.com") 

54 

55 

56def report_metadata_creation(args: argparse.Namespace) -> ReportMetadata: 

57 """ 

58 Create ReportMetadata from parsed arguments. 

59 

60 Args: 

61 args: Parsed command line arguments. 

62 

63 Returns: 

64 ReportMetadata instance. 

65 """ 

66 return ReportMetadata( 

67 timestamp=args.timestamp, 

68 run_id=args.run_id, 

69 ref_name=args.ref_name, 

70 github_sha=args.github_sha, 

71 github_repo=args.github_repo, 

72 ) 

73 

74 

75def report_main_runner(main_func, args: argparse.Namespace) -> int: 

76 """ 

77 Common execution loop for report generation scripts. 

78 

79 Args: 

80 main_func: Function that takes (args, metadata) and returns None. 

81 args: Parsed command line arguments. 

82 

83 Returns: 

84 Exit code (0 for success, 1 for failure). 

85 """ 

86 metadata: ReportMetadata = report_metadata_creation(args) 

87 

88 try: 

89 main_func(args, metadata) 

90 except (FileNotFoundError, IOError) as e: 

91 print(f"[X] File Error: {e}") 

92 return 1 

93 except ValueError as e: 

94 print(f"[X] Input Error: {e}") 

95 return 1 

96 return 0 

97 

98 

99def badge_image_download(url: str, output_path: str) -> bool: 

100 """ 

101 Download a badge SVG from a URL. 

102 

103 Args: 

104 url: The URL of the badge. 

105 output_path: Local file path to save the SVG. 

106 

107 Returns: 

108 True if successful, False otherwise. 

109 """ 

110 try: 

111 response = requests.get(url, timeout=10) 

112 response.raise_for_status() 

113 with open(output_path, "wb") as f: 

114 f.write(response.content) 

115 return True 

116 except requests.RequestException as e: 

117 print(f"[X] Failed to download badge from {url}: {e}") 

118 return False 

119 

120 

121def badge_metadata_json_write(metadata: dict, output_path: str) -> None: 

122 """ 

123 Write badge metadata to a JSON file. 

124 

125 Args: 

126 metadata: Dictionary containing metadata. 

127 output_path: Local file path to save the JSON. 

128 """ 

129 try: 

130 with open(output_path, "w", encoding="utf-8") as f: 

131 json.dump(metadata, f, indent=2) 

132 except (IOError, TypeError) as e: 

133 print(f"[X] Failed to write JSON metadata to {output_path}: {e}") 

134 

135 

136def get_score_color_lint(pylint_score: str) -> str: 

137 """ 

138 Determine color based on pylint score. 

139 

140 Args: 

141 pylint_score: The pylint score as string, e.g., "8.5", "7.0", etc. 

142 

143 Returns: 

144 Hex color code for the score, as a string. 

145 """ 

146 try: 

147 score_val: float = float(pylint_score) 

148 if score_val >= 8.0: 

149 return "brightgreen" 

150 if score_val >= 6.0: 

151 return "yellow" 

152 if score_val >= 4.0: 

153 return "orange" 

154 return "red" 

155 except ValueError: 

156 return "gray" 

157 

158 

159def get_score_color_coverage(coverage_score: str) -> str: 

160 """ 

161 Determines the color based on a coverage score. 

162 

163 Args: 

164 coverage_score: The coverage score as a string, e.g., "92.5" 

165 

166 Returns: 

167 The color for the badge as a string. 

168 """ 

169 try: 

170 score_val: float = float(coverage_score) 

171 if score_val >= 90: 

172 return "brightgreen" 

173 if score_val >= 80: 

174 return "green" 

175 if score_val >= 70: 

176 return "yellow" 

177 if score_val >= 60: 

178 return "orange" 

179 return "red" 

180 except ValueError: 

181 return "gray" 

182 

183 

184def _get_timezone_strings(utc_now: datetime) -> Tuple[str, str, str, str]: 

185 """ 

186 Helper to get formatted UTC, EST, MST, and PST strings from a UTC datetime. 

187 

188 Args: 

189 utc_now: The UTC datetime object. 

190 

191 Returns: 

192 A tuple of four strings: (UTC string, EST string, MST string, PST string). 

193 """ 

194 # Define the time zones 

195 timezone_est: pytz.BaseTzInfo = pytz.timezone("America/New_York") 

196 timezone_mst: pytz.BaseTzInfo = pytz.timezone("America/Denver") 

197 timezone_pst: pytz.BaseTzInfo = pytz.timezone("America/Los_Angeles") 

198 

199 # Convert UTC time to EST, MST, and PST 

200 est_now: datetime = utc_now.astimezone(timezone_est) 

201 mst_now: datetime = utc_now.astimezone(timezone_mst) 

202 pst_now: datetime = utc_now.astimezone(timezone_pst) 

203 

204 # Format the output 

205 df: str = "%Y-%m-%d %H:%M:%S" # Date format 

206 utc_str: str = utc_now.strftime(f"{df} UTC") 

207 est_str: str = est_now.strftime(f"{df} EST") 

208 mst_str: str = mst_now.strftime(f"{df} MST") 

209 pst_str: str = pst_now.strftime(f"{df} PST") 

210 

211 return utc_str, est_str, mst_str, pst_str 

212 

213 

214def get_timestamp() -> str: 

215 """ 

216 Get formatted timestamp with UTC, EST, MST, and PST times. 

217 

218 Returns: 

219 Formatted timestamp string 

220 """ 

221 # Get the current UTC time 

222 utc_now: datetime = datetime.now(pytz.utc) 

223 

224 # Get the formatted strings for each timezone 

225 utc_str, est_str, mst_str, pst_str = _get_timezone_strings(utc_now) 

226 

227 # Combine the formatted times 

228 timestamp: str = f"{utc_str} ({est_str} / {mst_str} / {pst_str})" 

229 

230 return timestamp 

231 

232 

233def extend_timestamp(short: str) -> str: 

234 """ 

235 Given a timestamp string from CI/CD in the form of 

236 20250815_211112_UTC, extend the timestamp to include EST, MST, and PST times 

237 and return the extended string. 

238 

239 Args: 

240 short: the UTC bash string, for example: 20250815_211112_UTC 

241 

242 Returns: 

243 Extended timestamp string. 

244 """ 

245 # Call the multiline function to get the individual strings 

246 lines: list[str] = get_multiline_timestamp(short) 

247 

248 # lines[1] UTC, lines[2] EST, lines[3] MST, lines[4] PST 

249 return f"{lines[1]} ({lines[2]} / {lines[3]} / {lines[4]})" 

250 

251 

252def get_multiline_timestamp(short: str) -> List[str]: 

253 """ 

254 Given a timestamp string from CI/CD in the form of 

255 20250815_211112_UTC, return a list of strings for multiple timezones. 

256 

257 Args: 

258 short: the UTC bash string, for example: 20250815_211112_UTC 

259 

260 Returns: 

261 List of 5 formatted strings. 

262 """ 

263 # Regex pattern to match the required format: YYYYMMDD_HHMMSS_TZ 

264 pattern: re.Pattern = re.compile(r"^(\d{8})_(\d{6})_(UTC|GMT|Z)$") 

265 match = pattern.match(short) 

266 

267 if not match: 

268 raise ValueError(f"Invalid timestamp format: '{short}'") 

269 

270 # Extract the date and time parts from the regex match 

271 date_part, time_part, _ = match.groups() 

272 

273 # Combine the parts into a format that can be parsed by datetime 

274 datetime_str: str = f"{date_part}_{time_part}_UTC" 

275 input_format: str = "%Y%m%d_%H%M%S_%Z" 

276 

277 # Convert the input string to a datetime object 

278 utc_datetime: datetime = datetime.strptime(datetime_str, input_format) 

279 

280 # Make the datetime object timezone-aware 

281 utc_now: datetime = pytz.utc.localize(utc_datetime) 

282 

283 # Get the formatted strings for each timezone 

284 utc_str, est_str, mst_str, pst_str = _get_timezone_strings(utc_now) 

285 

286 return ["Generated:", utc_str, est_str, mst_str, pst_str] 

287 

288 

289def write_report(html_content: str, output_file: str) -> None: 

290 """ 

291 Write HTML content to file. 

292 

293 Args: 

294 html_content: The HTML content to write 

295 output_file: Path for the output HTML file 

296 

297 Raises: 

298 IOError: If the file cannot be written. 

299 """ 

300 try: 

301 with open(output_file, "w", encoding="utf-8") as f: 

302 f.write(html_content) 

303 except IOError as e: 

304 raise IOError(f'Error writing output file "{output_file}": {e}') from e