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
« 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"""
6import argparse
7import json
8import re
9from datetime import datetime
10from typing import List, NamedTuple, Tuple
12import pytz
13import requests
16class ReportMetadata(NamedTuple):
17 """Container for CI/CD metadata used in reports."""
19 timestamp: str
20 run_id: str
21 ref_name: str
22 github_sha: str
23 github_repo: str
26def add_common_args(parser: argparse.ArgumentParser) -> None:
27 """
28 Add shared CI/CD arguments to an argument parser.
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")
42def add_badge_args(parser: argparse.ArgumentParser) -> None:
43 """
44 Add shared arguments for badge generation scripts.
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")
56def report_metadata_creation(args: argparse.Namespace) -> ReportMetadata:
57 """
58 Create ReportMetadata from parsed arguments.
60 Args:
61 args: Parsed command line arguments.
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 )
75def report_main_runner(main_func, args: argparse.Namespace) -> int:
76 """
77 Common execution loop for report generation scripts.
79 Args:
80 main_func: Function that takes (args, metadata) and returns None.
81 args: Parsed command line arguments.
83 Returns:
84 Exit code (0 for success, 1 for failure).
85 """
86 metadata: ReportMetadata = report_metadata_creation(args)
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
99def badge_image_download(url: str, output_path: str) -> bool:
100 """
101 Download a badge SVG from a URL.
103 Args:
104 url: The URL of the badge.
105 output_path: Local file path to save the SVG.
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
121def badge_metadata_json_write(metadata: dict, output_path: str) -> None:
122 """
123 Write badge metadata to a JSON file.
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}")
136def get_score_color_lint(pylint_score: str) -> str:
137 """
138 Determine color based on pylint score.
140 Args:
141 pylint_score: The pylint score as string, e.g., "8.5", "7.0", etc.
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"
159def get_score_color_coverage(coverage_score: str) -> str:
160 """
161 Determines the color based on a coverage score.
163 Args:
164 coverage_score: The coverage score as a string, e.g., "92.5"
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"
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.
188 Args:
189 utc_now: The UTC datetime object.
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")
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)
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")
211 return utc_str, est_str, mst_str, pst_str
214def get_timestamp() -> str:
215 """
216 Get formatted timestamp with UTC, EST, MST, and PST times.
218 Returns:
219 Formatted timestamp string
220 """
221 # Get the current UTC time
222 utc_now: datetime = datetime.now(pytz.utc)
224 # Get the formatted strings for each timezone
225 utc_str, est_str, mst_str, pst_str = _get_timezone_strings(utc_now)
227 # Combine the formatted times
228 timestamp: str = f"{utc_str} ({est_str} / {mst_str} / {pst_str})"
230 return timestamp
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.
239 Args:
240 short: the UTC bash string, for example: 20250815_211112_UTC
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)
248 # lines[1] UTC, lines[2] EST, lines[3] MST, lines[4] PST
249 return f"{lines[1]} ({lines[2]} / {lines[3]} / {lines[4]})"
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.
257 Args:
258 short: the UTC bash string, for example: 20250815_211112_UTC
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)
267 if not match:
268 raise ValueError(f"Invalid timestamp format: '{short}'")
270 # Extract the date and time parts from the regex match
271 date_part, time_part, _ = match.groups()
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"
277 # Convert the input string to a datetime object
278 utc_datetime: datetime = datetime.strptime(datetime_str, input_format)
280 # Make the datetime object timezone-aware
281 utc_now: datetime = pytz.utc.localize(utc_datetime)
283 # Get the formatted strings for each timezone
284 utc_str, est_str, mst_str, pst_str = _get_timezone_strings(utc_now)
286 return ["Generated:", utc_str, est_str, mst_str, pst_str]
289def write_report(html_content: str, output_file: str) -> None:
290 """
291 Write HTML content to file.
293 Args:
294 html_content: The HTML content to write
295 output_file: Path for the output HTML file
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