Coverage for src/alprina_cli/services/badge_generator.py: 30%

40 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-14 11:27 +0100

1""" 

2Badge Generator Service 

3Generates SVG badges for Alprina security verification. 

4""" 

5 

6from datetime import datetime 

7from typing import Optional, Literal 

8import xml.etree.ElementTree as ET 

9 

10 

11class BadgeGenerator: 

12 """Generates SVG security badges in various styles.""" 

13 

14 # Color schemes 

15 THEMES = { 

16 "light": { 

17 "background": "#ffffff", 

18 "text": "#1a1a1a", 

19 "accent": "#3b82f6", 

20 "border": "#e5e7eb", 

21 "grade_bg": "#f3f4f6" 

22 }, 

23 "dark": { 

24 "background": "#1a1a1a", 

25 "text": "#ffffff", 

26 "accent": "#60a5fa", 

27 "border": "#374151", 

28 "grade_bg": "#374151" 

29 } 

30 } 

31 

32 # Size configurations 

33 SIZES = { 

34 "small": {"width": 160, "height": 50, "font_size": 11}, 

35 "medium": {"width": 200, "height": 60, "font_size": 13}, 

36 "large": {"width": 240, "height": 70, "font_size": 15} 

37 } 

38 

39 def generate_svg( 

40 self, 

41 style: Literal["standard", "minimal", "detailed"] = "standard", 

42 theme: Literal["light", "dark"] = "light", 

43 size: Literal["small", "medium", "large"] = "medium", 

44 grade: Optional[str] = None, 

45 last_scan: Optional[datetime] = None, 

46 custom_text: Optional[str] = None 

47 ) -> str: 

48 """Generate SVG badge based on parameters.""" 

49 

50 if style == "minimal": 

51 return self._generate_minimal_badge(theme, size, grade) 

52 elif style == "detailed": 

53 return self._generate_detailed_badge(theme, size, grade, last_scan) 

54 else: 

55 return self._generate_standard_badge(theme, size, grade, custom_text) 

56 

57 def _generate_standard_badge( 

58 self, 

59 theme: str, 

60 size: str, 

61 grade: Optional[str], 

62 custom_text: Optional[str] 

63 ) -> str: 

64 """Generate standard badge style.""" 

65 colors = self.THEMES[theme] 

66 dimensions = self.SIZES[size] 

67 

68 text = custom_text or "Secured by Alprina" 

69 grade_display = grade if grade and grade != "N/A" else "" 

70 

71 svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{dimensions['width']}" height="{dimensions['height']}" viewBox="0 0 {dimensions['width']} {dimensions['height']}"> 

72 <defs> 

73 <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%"> 

74 <stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" /> 

75 <stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" /> 

76 </linearGradient> 

77 </defs> 

78 

79 <!-- Background --> 

80 <rect width="{dimensions['width']}" height="{dimensions['height']}" rx="8" fill="{colors['background']}" stroke="{colors['border']}" stroke-width="1.5"/> 

81 

82 <!-- Left section with gradient --> 

83 <rect width="50" height="{dimensions['height']}" rx="8" fill="url(#grad)"/> 

84 

85 <!-- Shield icon --> 

86 <g transform="translate(15, {dimensions['height']/2 - 10})"> 

87 <path d="M10 0 L0 4 L0 8 Q0 14 10 18 Q20 14 20 8 L20 4 Z" fill="white" opacity="0.9"/> 

88 <path d="M10 4 L10 14 M6 9 L10 13 L14 9" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round"/> 

89 </g> 

90 

91 <!-- Main text --> 

92 <text x="60" y="{dimensions['height']/2 - 5}" fill="{colors['text']}" font-family="Arial, sans-serif" font-size="{dimensions['font_size']}" font-weight="600"> 

93 {text} 

94 </text> 

95 

96 <!-- Grade badge (if provided) --> 

97 {f'<g transform="translate({dimensions["width"] - 40}, {dimensions["height"]/2 - 12})">' if grade_display else ''} 

98 {f'<rect width="32" height="24" rx="4" fill="{colors["grade_bg"]}" stroke="{colors["border"]}" stroke-width="1"/>' if grade_display else ''} 

99 {f'<text x="16" y="16" fill="{colors["accent"]}" font-family="Arial, sans-serif" font-size="{dimensions["font_size"] - 1}" font-weight="bold" text-anchor="middle">{grade_display}</text>' if grade_display else ''} 

100 {f'</g>' if grade_display else ''} 

101 

102 <!-- Checkmark --> 

103 <g transform="translate(60, {dimensions['height']/2 + 10})"> 

104 <circle cx="6" cy="6" r="6" fill="#10b981" opacity="0.2"/> 

105 <path d="M4 6 L6 8 L9 4" stroke="#10b981" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/> 

106 </g> 

107 <text x="74" y="{dimensions['height']/2 + 16}" fill="{colors['text']}" font-family="Arial, sans-serif" font-size="{dimensions['font_size'] - 3}" opacity="0.7">Verified</text> 

108</svg>''' 

109 return svg 

110 

111 def _generate_minimal_badge( 

112 self, 

113 theme: str, 

114 size: str, 

115 grade: Optional[str] 

116 ) -> str: 

117 """Generate minimal badge style.""" 

118 colors = self.THEMES[theme] 

119 dimensions = self.SIZES[size] 

120 

121 grade_display = grade if grade and grade != "N/A" else "" 

122 

123 svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{dimensions['width']}" height="{dimensions['height'] - 10}" viewBox="0 0 {dimensions['width']} {dimensions['height'] - 10}"> 

124 <!-- Background --> 

125 <rect width="{dimensions['width']}" height="{dimensions['height'] - 10}" rx="6" fill="{colors['background']}" stroke="{colors['border']}" stroke-width="1"/> 

126 

127 <!-- Shield icon --> 

128 <g transform="translate(10, {(dimensions['height'] - 10)/2 - 8})"> 

129 <path d="M8 0 L0 3 L0 6 Q0 10 8 13 Q16 10 16 6 L16 3 Z" fill="{colors['accent']}" opacity="0.9"/> 

130 <path d="M8 3 L8 10 M5 7 L8 10 L11 7" stroke="white" stroke-width="1.2" fill="none" stroke-linecap="round"/> 

131 </g> 

132 

133 <!-- Text --> 

134 <text x="35" y="{(dimensions['height'] - 10)/2 + 5}" fill="{colors['text']}" font-family="Arial, sans-serif" font-size="{dimensions['font_size'] - 1}" font-weight="600"> 

135 Alprina 

136 </text> 

137 

138 <!-- Grade (if provided) --> 

139 {f'<text x="{dimensions["width"] - 35}" y="{(dimensions["height"] - 10)/2 + 5}" fill="{colors["accent"]}" font-family="Arial, sans-serif" font-size="{dimensions["font_size"]}" font-weight="bold">{grade_display}</text>' if grade_display else ''} 

140</svg>''' 

141 return svg 

142 

143 def _generate_detailed_badge( 

144 self, 

145 theme: str, 

146 size: str, 

147 grade: Optional[str], 

148 last_scan: Optional[datetime] 

149 ) -> str: 

150 """Generate detailed badge style with scan date.""" 

151 colors = self.THEMES[theme] 

152 dimensions = self.SIZES[size] 

153 

154 grade_display = grade if grade and grade != "N/A" else "N/A" 

155 date_text = "" 

156 if last_scan: 

157 date_text = f"Scanned: {last_scan.strftime('%b %d, %Y')}" 

158 else: 

159 date_text = "No recent scan" 

160 

161 # Increase height for detailed view 

162 height = dimensions['height'] + 20 

163 

164 svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{dimensions['width']}" height="{height}" viewBox="0 0 {dimensions['width']} {height}"> 

165 <defs> 

166 <linearGradient id="grad" x1="0%" y1="0%" x2="0%" y2="100%"> 

167 <stop offset="0%" style="stop-color:#3b82f6;stop-opacity:0.1" /> 

168 <stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.1" /> 

169 </linearGradient> 

170 </defs> 

171 

172 <!-- Background --> 

173 <rect width="{dimensions['width']}" height="{height}" rx="10" fill="{colors['background']}" stroke="{colors['border']}" stroke-width="1.5"/> 

174 <rect width="{dimensions['width']}" height="{height}" rx="10" fill="url(#grad)"/> 

175 

176 <!-- Header section --> 

177 <rect width="{dimensions['width']}" height="30" rx="10" fill="{colors['accent']}" opacity="0.1"/> 

178 

179 <!-- Shield icon --> 

180 <g transform="translate({dimensions['width']/2 - 12}, 8)"> 

181 <path d="M12 0 L0 4 L0 8 Q0 14 12 20 Q24 14 24 8 L24 4 Z" fill="{colors['accent']}"/> 

182 <path d="M12 5 L12 16 M8 11 L12 15 L16 11" stroke="white" stroke-width="2" fill="none" stroke-linecap="round"/> 

183 </g> 

184 

185 <!-- Title --> 

186 <text x="{dimensions['width']/2}" y="45" fill="{colors['text']}" font-family="Arial, sans-serif" font-size="{dimensions['font_size']}" font-weight="bold" text-anchor="middle"> 

187 Secured by Alprina 

188 </text> 

189 

190 <!-- Grade section --> 

191 <g transform="translate({dimensions['width']/2 - 30}, 55)"> 

192 <rect width="60" height="30" rx="6" fill="{colors['grade_bg']}" stroke="{colors['border']}" stroke-width="1"/> 

193 <text x="30" y="20" fill="{colors['accent']}" font-family="Arial, sans-serif" font-size="{dimensions['font_size'] + 4}" font-weight="bold" text-anchor="middle">{grade_display}</text> 

194 </g> 

195 

196 <!-- Status indicators --> 

197 <g transform="translate(15, {height - 20})"> 

198 <circle cx="4" cy="4" r="4" fill="#10b981"/> 

199 <text x="12" y="7" fill="{colors['text']}" font-family="Arial, sans-serif" font-size="{dimensions['font_size'] - 4}" opacity="0.7">Verified</text> 

200 </g> 

201 

202 <!-- Last scan date --> 

203 <text x="{dimensions['width']/2}" y="{height - 10}" fill="{colors['text']}" font-family="Arial, sans-serif" font-size="{dimensions['font_size'] - 5}" opacity="0.6" text-anchor="middle"> 

204 {date_text} 

205 </text> 

206</svg>''' 

207 return svg 

208 

209 def generate_static_url( 

210 self, 

211 user_id: str, 

212 base_url: str = "https://alprina.com" 

213 ) -> str: 

214 """Generate static badge URL.""" 

215 return f"{base_url}/api/v1/badge/{user_id}/svg" 

216 

217 def generate_verification_url( 

218 self, 

219 user_id: str, 

220 base_url: str = "https://alprina.com" 

221 ) -> str: 

222 """Generate verification page URL.""" 

223 return f"{base_url}/verify/{user_id}"