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
« 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"""
6from datetime import datetime
7from typing import Optional, Literal
8import xml.etree.ElementTree as ET
11class BadgeGenerator:
12 """Generates SVG security badges in various styles."""
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 }
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 }
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."""
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)
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]
68 text = custom_text or "Secured by Alprina"
69 grade_display = grade if grade and grade != "N/A" else ""
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>
79 <!-- Background -->
80 <rect width="{dimensions['width']}" height="{dimensions['height']}" rx="8" fill="{colors['background']}" stroke="{colors['border']}" stroke-width="1.5"/>
82 <!-- Left section with gradient -->
83 <rect width="50" height="{dimensions['height']}" rx="8" fill="url(#grad)"/>
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>
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>
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 ''}
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
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]
121 grade_display = grade if grade and grade != "N/A" else ""
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"/>
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>
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>
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
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]
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"
161 # Increase height for detailed view
162 height = dimensions['height'] + 20
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>
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)"/>
176 <!-- Header section -->
177 <rect width="{dimensions['width']}" height="30" rx="10" fill="{colors['accent']}" opacity="0.1"/>
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>
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>
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>
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>
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
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"
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}"