|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +import base64 |
| 4 | +import json |
| 5 | +import sys |
| 6 | +from enum import Enum |
| 7 | +from pathlib import Path |
| 8 | +from typing import Any |
| 9 | +from urllib.parse import quote_plus |
| 10 | + |
| 11 | +CategoryCounts = dict[str, dict[str, tuple[int, int]]] |
| 12 | + |
| 13 | + |
| 14 | +class ComplianceColor(str, Enum): |
| 15 | + GREY = "lightgrey" |
| 16 | + RED = "red" |
| 17 | + ORANGE = "orange" |
| 18 | + YELLOW = "yellow" |
| 19 | + GREEN = "green" |
| 20 | + |
| 21 | + |
| 22 | +class ShieldColor(str, Enum): |
| 23 | + BRONZE = "#CD7F32" |
| 24 | + SILVER = "#1C274C" |
| 25 | + GOLD = "#FFD700" |
| 26 | + |
| 27 | + |
| 28 | +def count_category_tier_bools(obj: Any) -> CategoryCounts: |
| 29 | + """Count number of trues and falses by tier and category.""" |
| 30 | + return { |
| 31 | + category: { |
| 32 | + tier: ( |
| 33 | + sum( |
| 34 | + val is True for val in obj.get(category, {}).get(tier, {}).values() |
| 35 | + ), |
| 36 | + sum( |
| 37 | + val is False for val in obj.get(category, {}).get(tier, {}).values() |
| 38 | + ), |
| 39 | + ) |
| 40 | + for tier in ("bronze", "silver", "gold") |
| 41 | + } |
| 42 | + for category in ("documentation", "infrastructure", "testing") |
| 43 | + } |
| 44 | + |
| 45 | + |
| 46 | +def count_total_bools(counts: CategoryCounts) -> tuple[int, int]: |
| 47 | + """Count total number of trues and falses.""" |
| 48 | + return ( |
| 49 | + sum(true_count for cat in counts.values() for true_count, _ in cat.values()), |
| 50 | + sum(false_count for cat in counts.values() for _, false_count in cat.values()), |
| 51 | + ) |
| 52 | + |
| 53 | + |
| 54 | +def get_highest_tier(counts: CategoryCounts) -> str | None: |
| 55 | + """Return highest completed tier.""" |
| 56 | + return next( |
| 57 | + ( |
| 58 | + tier |
| 59 | + for tier in ("gold", "silver", "bronze") |
| 60 | + if any( |
| 61 | + false_count == 0 |
| 62 | + for false_count in (counts[cat][tier][1] for cat in counts) |
| 63 | + ) |
| 64 | + ), |
| 65 | + None, |
| 66 | + ) |
| 67 | + |
| 68 | + |
| 69 | +def get_compliance_color(ratio: float) -> ComplianceColor: |
| 70 | + """Return compliance color.""" |
| 71 | + if ratio <= 0.25: |
| 72 | + return ComplianceColor.RED |
| 73 | + elif ratio <= 0.5: |
| 74 | + return ComplianceColor.ORANGE |
| 75 | + elif ratio <= 0.75: |
| 76 | + return ComplianceColor.YELLOW |
| 77 | + else: |
| 78 | + return ComplianceColor.GREEN |
| 79 | + |
| 80 | + |
| 81 | +def get_shield_color(tier: str) -> ShieldColor: |
| 82 | + """Return shield color.""" |
| 83 | + if tier == "bronze": |
| 84 | + return ShieldColor.BRONZE |
| 85 | + elif tier == "silver": |
| 86 | + return ShieldColor.SILVER |
| 87 | + elif tier == "gold": |
| 88 | + return ShieldColor.GOLD |
| 89 | + else: |
| 90 | + raise ValueError(f"Unknown tier provided: {tier}") |
| 91 | + |
| 92 | + |
| 93 | +def generate_shield(tier: str) -> ...: |
| 94 | + """Generate base64 encoded svg shield""" |
| 95 | + shield_color = get_shield_color(tier) |
| 96 | + shield_xml = f"""<svg viewBox="0 0 24 24" fill="{shield_color}" xmlns="http://www.w3.org/2000/svg"> |
| 97 | + <path d="M3.37752 5.08241C3 5.62028 3 7.21907 3 10.4167V11.9914C3 17.6294 7.23896 20.3655 9.89856 21.5273C10.62 21.8424 10.9807 22 12 22C13.0193 22 13.38 21.8424 14.1014 21.5273C16.761 20.3655 21 17.6294 21 11.9914V10.4167C21 7.21907 21 5.62028 20.6225 5.08241C20.245 4.54454 18.7417 4.02996 15.7351 3.00079L15.1623 2.80472C13.595 2.26824 12.8114 2 12 2C11.1886 2 10.405 2.26824 8.83772 2.80472L8.26491 3.00079C5.25832 4.02996 3.75503 4.54454 3.37752 5.08241Z"/> |
| 98 | +</svg> |
| 99 | +""" |
| 100 | + return base64.b64encode(shield_xml.encode()).decode() |
| 101 | + |
| 102 | + |
| 103 | +def generate_badge(true_count: int, false_count: int, highest_tier: str | None) -> str: |
| 104 | + """Generate a shields.io badge based on checklist counts.""" |
| 105 | + total = true_count + false_count |
| 106 | + if total == 0: |
| 107 | + return f"https://img.shields.io/badge/NMIND-{quote_plus('0/0')}-{ComplianceColor.GREY}" |
| 108 | + |
| 109 | + compliance_color = get_compliance_color(true_count / total) |
| 110 | + msg = f"{true_count}/{total}" |
| 111 | + badge_content = f"NMIND-{quote_plus(msg)}-{compliance_color}" |
| 112 | + if highest_tier: |
| 113 | + badge_content += ( |
| 114 | + f"?logo=data:image/svg+xml;base64,{generate_shield(highest_tier)}" |
| 115 | + ) |
| 116 | + |
| 117 | + return f"https://img.shields.io/badge/{badge_content}" |
| 118 | + |
| 119 | + |
| 120 | +def process_checklist(checklist_path: Path) -> str: |
| 121 | + """Process a checklist, generating a badge.""" |
| 122 | + checklist = json.loads(checklist_path.read_text()) |
| 123 | + category_counts = count_category_tier_bools(obj=checklist) |
| 124 | + true_count, false_count = count_total_bools(counts=category_counts) |
| 125 | + highest_tier = get_highest_tier(counts=category_counts) |
| 126 | + badge_url = generate_badge( |
| 127 | + true_count=true_count, false_count=false_count, highest_tier=highest_tier |
| 128 | + ) |
| 129 | + |
| 130 | + return badge_url |
| 131 | + |
| 132 | + |
| 133 | +if __name__ == "__main__": |
| 134 | + if len(sys.argv) != 2: |
| 135 | + print("Usage: create_badge.py <path_to_checklist_json>") |
| 136 | + sys.exit(1) |
| 137 | + checklist_fp = Path(sys.argv[1]) |
| 138 | + print(process_checklist(checklist_fp)) |
0 commit comments