22"""Simple dockerhub readme generator."""
33
44import argparse
5+ import contextlib
6+ import json
57import pathlib
8+ import random
69import re
710import sys
11+ import time
12+ import types
813import typing
14+ import xml .etree .ElementTree as ET
915
1016from ._helpers import parse_last_git_tag , replace_tag_in_readme
1117from whole_app .settings import SETTINGS
1218
1319
1420PARENT_DIR : typing .Final = pathlib .Path (__file__ ).parent .parent
1521README_PATH : typing .Final = PARENT_DIR / "README.md"
22+ COVERAGE_XML_PATH : typing .Final = pathlib .Path ("coverage.xml" )
23+ BADGE_JSON_PATH : typing .Final = pathlib .Path (".github/badges/coverage.json" )
24+ LOW_BOUNDARY : typing .Final [float ] = 60
25+ HIGH_BOUNDARY : typing .Final [float ] = 80
26+ RETRY_ATTEMPTS : typing .Final [int ] = 3
1627
1728
1829def _update_dockerhub_readme () -> None :
@@ -65,6 +76,55 @@ def _update_readme() -> None:
6576 README_PATH .write_text (new_content )
6677
6778
79+ def _fetch_xml_text () -> str :
80+ for _attempt_index in range (RETRY_ATTEMPTS ):
81+ with contextlib .suppress (OSError ):
82+ return COVERAGE_XML_PATH .read_text ()
83+ time .sleep (random .uniform (0.1 , 0.3 )) # noqa: S311
84+ error_message : typing .Final = f"Failed to read { COVERAGE_XML_PATH } after { RETRY_ATTEMPTS } attempts"
85+ raise OSError (error_message )
86+
87+
88+ def _persist_badge_text (badge_text : str ) -> None :
89+ BADGE_JSON_PATH .parent .mkdir (parents = True , exist_ok = True )
90+ for _attempt_index in range (RETRY_ATTEMPTS ):
91+ with contextlib .suppress (OSError ):
92+ BADGE_JSON_PATH .write_text (badge_text )
93+ return
94+ time .sleep (random .uniform (0.1 , 0.3 )) # noqa: S311
95+ error_message : typing .Final = f"Failed to write { BADGE_JSON_PATH } after { RETRY_ATTEMPTS } attempts"
96+ raise OSError (error_message )
97+
98+
99+ def _build_coverage_badge () -> None :
100+ xml_source_text : typing .Final [str ] = _fetch_xml_text ()
101+ root_element : typing .Final [ET .Element ] = ET .fromstring (xml_source_text ) # noqa: S314
102+ line_rate_text : typing .Final [str | None ] = root_element .attrib .get ("line-rate" )
103+ if line_rate_text is None :
104+ missing_attr_message : typing .Final [str ] = "Missing 'line-rate' attribute in coverage report"
105+ raise KeyError (missing_attr_message )
106+ coverage_percent : typing .Final [float ] = float (line_rate_text ) * 100.0
107+
108+ message_text : typing .Final [str ] = f"{ coverage_percent :.0f} %"
109+ color_text : str
110+ if coverage_percent < LOW_BOUNDARY :
111+ color_text = "#E63946"
112+ elif coverage_percent < HIGH_BOUNDARY :
113+ color_text = "#FFB347"
114+ else :
115+ color_text = "#2A9D8F"
116+
117+ badge_mapping : typing .Final [typing .Mapping [str , typing .Any ]] = types .MappingProxyType (
118+ {
119+ "schemaVersion" : 1 ,
120+ "label" : "coverage" ,
121+ "message" : message_text ,
122+ "color" : color_text ,
123+ },
124+ )
125+ _persist_badge_text (json .dumps (dict (badge_mapping )))
126+
127+
68128if __name__ == "__main__" :
69129 sys .path .append (str (PARENT_DIR .resolve ()))
70130
@@ -76,5 +136,7 @@ def _update_readme() -> None:
76136 _update_dockerhub_readme ()
77137 case "update-readme" :
78138 _update_readme ()
139+ case "build-coverage-badge" :
140+ _build_coverage_badge ()
79141 case _:
80142 print ("Unknown action" ) # noqa: T201
0 commit comments