Source code for gitlab_overviewer.rendering.renderer_base

  1from abc import ABC, abstractmethod
  2from typing import Any, List, Optional
  3from datetime import datetime
  4
  5
[docs] 6class Renderer(ABC): 7 """ 8 Abstract base class for all renderers. 9 10 Implements the required interface for all renderers as per: 11 12 * /specs/spec_renderer_markdown.md §6 13 * /specs/spec_renderer_quarto.md §6 14 * /specs/spec_model_mapping.md (for data model expectations) 15 16 # TODO: Open questions 17 # 1. Should future renderers (e.g., HTML, PDF) follow this interface? (TBD) 18 # 2. Are there additional common methods that should be included in the base interface? (TBD) 19 # Possible candidates: 20 # 21 # * get_supported_formats(cls) -> List[str] 22 # * validate_output(self, output: str) -> bool 23 # * get_default_config(cls) -> dict 24 """ 25
[docs] 26 @abstractmethod 27 def render(self, *args, **kwargs) -> Any: 28 """ 29 Render the output for the given data. 30 See /specs/spec_renderer_markdown.md and /specs/spec_renderer_quarto.md for required output. 31 """ 32 pass
33
[docs] 34 @abstractmethod 35 def write_files(self, *args, **kwargs) -> None: 36 """ 37 Write rendered output to files as specified. 38 See /specs/spec_renderer_markdown.md and /specs/spec_renderer_quarto.md for file output requirements. 39 """ 40 pass
41
[docs] 42 @staticmethod 43 def is_placeholder_value(val: Any) -> bool: 44 if val is None or val == "": 45 return True 46 if isinstance(val, str): 47 return val.strip() in ("-", "–", "—") 48 return False
49
[docs] 50 @staticmethod 51 def star_rating(val: Any, max_val: int = 5) -> str: 52 try: 53 rating = int(val) 54 if rating <= 0: 55 return "—" 56 if rating > max_val: 57 rating = max_val 58 return "★" * rating + "☆" * (max_val - rating) 59 except Exception: 60 return "—"
61
[docs] 62 @staticmethod 63 def safe_str(val: Any) -> str: 64 if Renderer.is_placeholder_value(val): 65 return "—" 66 return str(val)
67
[docs] 68 @staticmethod 69 def format_date(dt: Any) -> str: 70 if Renderer.is_placeholder_value(dt): 71 return "—" 72 if isinstance(dt, str): 73 parsed_dt = Renderer.parse_iso_date(dt) 74 if parsed_dt: 75 return parsed_dt.strftime("%Y-%m-%d") 76 return dt 77 if isinstance(dt, datetime): 78 return dt.strftime("%Y-%m-%d") 79 return str(dt)
80
[docs] 81 @staticmethod 82 def parse_iso_date(dt: Any) -> Optional[datetime]: 83 if dt is None: 84 return None 85 if isinstance(dt, datetime): 86 return dt 87 if isinstance(dt, str): 88 try: 89 dt = dt.replace("Z", "+00:00") 90 return datetime.fromisoformat(dt) 91 except Exception: 92 return None 93 return None
94
[docs] 95 @staticmethod 96 def parse_comma_list(value: Any) -> List[str]: 97 if value is None: 98 return [] 99 if isinstance(value, str): 100 return [item.strip() for item in value.split(",") if item.strip()] 101 if isinstance(value, list): 102 return [str(item) for item in value] 103 return []
104
[docs] 105 @staticmethod 106 def safe_int(value: Any, default: int = 0) -> int: 107 if value is None: 108 return default 109 try: 110 return int(value) 111 except (ValueError, TypeError): 112 return default
113
[docs] 114 @staticmethod 115 def safe_float(value: Any, default: float = 0.0) -> float: 116 if value is None: 117 return default 118 try: 119 return float(value) 120 except (ValueError, TypeError): 121 return default
122
[docs] 123 @staticmethod 124 def is_numeric_string(value: Any) -> bool: 125 if value is None: 126 return False 127 try: 128 str_val = str(value) 129 return str_val.replace(".", "", 1).replace("-", "", 1).isdigit() 130 except Exception: 131 return False
132
[docs] 133 @staticmethod 134 def get_with_fallback(data: dict, key: str, fallback: str = "—") -> str: 135 value = data.get(key, fallback) 136 if Renderer.is_placeholder_value(value): 137 return "—" 138 return str(value)
139
[docs] 140 @staticmethod 141 def format_table_row( 142 row: dict, keys: List[str], formatters: Optional[dict] = None 143 ) -> List[str]: 144 if formatters is None: 145 formatters = {} 146 result = [] 147 for key in keys: 148 if key in formatters: 149 result.append(formatters[key](row.get(key))) 150 else: 151 result.append(Renderer.safe_str(row.get(key, "—"))) 152 return result
153
[docs] 154 @staticmethod 155 def process_readme_lines(content: str, max_lines: int = 12) -> List[str]: 156 if not content: 157 return [] 158 lines = content.splitlines() 159 if lines and lines[0].strip() == "---": 160 try: 161 frontmatter_end = lines[1:].index("---") 162 lines = lines[frontmatter_end + 2 :] 163 except ValueError: 164 pass 165 if len(lines) > max_lines: 166 lines = lines[:max_lines] 167 lines.append("…continues…") 168 return lines
169
[docs] 170 @staticmethod 171 def format_priority_urgency( 172 row: dict, 173 priority_key: str = "priority", 174 urgency_key: str = "urgency", 175 max_priority: int = 5, 176 max_urgency: int = 5, 177 ) -> dict: 178 result = row.copy() 179 if priority_key in result: 180 val = result.get(priority_key, "-") 181 if val is None: 182 val = "-" 183 result[priority_key] = Renderer.star_rating(val, max_priority) 184 if urgency_key in result: 185 val = result.get(urgency_key, "-") 186 if val is None: 187 val = "-" 188 result[urgency_key] = Renderer.star_rating(val, max_urgency) 189 return result
190
[docs] 191 @staticmethod 192 def strip_trailing_whitespace(text: str) -> str: 193 if not isinstance(text, str): 194 return "" 195 return "\n".join(line.rstrip() for line in text.splitlines())