robotframework-quality-scanner 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner.egg-info → robotframework_quality_scanner-0.4.0}/PKG-INFO +20 -6
  2. {robotframework-quality-scanner-0.3.0 → robotframework_quality_scanner-0.4.0}/README.md +18 -4
  3. {robotframework-quality-scanner-0.3.0 → robotframework_quality_scanner-0.4.0}/setup.py +7 -2
  4. robotframework_quality_scanner-0.4.0/src/analyzers/__init__.py +12 -0
  5. robotframework_quality_scanner-0.4.0/src/models/__init__.py +6 -0
  6. robotframework_quality_scanner-0.4.0/src/models/issue.py +22 -0
  7. robotframework_quality_scanner-0.4.0/src/reporters/__init__.py +10 -0
  8. robotframework_quality_scanner-0.4.0/src/reporters/console_reporter.py +7 -0
  9. robotframework_quality_scanner-0.4.0/src/reporters/coverage_report.py +313 -0
  10. robotframework_quality_scanner-0.4.0/src/reporters/executive_report.py +381 -0
  11. {robotframework-quality-scanner-0.3.0 → robotframework_quality_scanner-0.4.0/src/robotframework_quality_scanner.egg-info}/PKG-INFO +19 -5
  12. robotframework_quality_scanner-0.4.0/src/robotframework_quality_scanner.egg-info/SOURCES.txt +25 -0
  13. robotframework_quality_scanner-0.4.0/src/robotframework_quality_scanner.egg-info/entry_points.txt +3 -0
  14. robotframework_quality_scanner-0.4.0/src/robotframework_quality_scanner.egg-info/top_level.txt +5 -0
  15. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner → robotframework_quality_scanner-0.4.0/src}/utils/__init__.py +7 -0
  16. robotframework_quality_scanner-0.4.0/src/utils/logger.py +260 -0
  17. robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner/__init__.py +0 -5
  18. robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner/analyzers/__init__.py +0 -1
  19. robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner/scanner.py +0 -173
  20. robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner.egg-info/SOURCES.txt +0 -19
  21. robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner.egg-info/top_level.txt +0 -1
  22. {robotframework-quality-scanner-0.3.0 → robotframework_quality_scanner-0.4.0}/pyproject.toml +0 -0
  23. {robotframework-quality-scanner-0.3.0 → robotframework_quality_scanner-0.4.0}/setup.cfg +0 -0
  24. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner → robotframework_quality_scanner-0.4.0/src}/analyzers/dependency_analyzer.py +0 -0
  25. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner → robotframework_quality_scanner-0.4.0/src}/analyzers/duplication_analyzer.py +0 -0
  26. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner → robotframework_quality_scanner-0.4.0/src}/analyzers/performance_analyzer.py +0 -0
  27. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner → robotframework_quality_scanner-0.4.0/src}/analyzers/test_data_analyzer.py +0 -0
  28. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner → robotframework_quality_scanner-0.4.0/src}/api/__init__.py +0 -0
  29. {robotframework-quality-scanner-0.3.0 → robotframework_quality_scanner-0.4.0}/src/robotframework_quality_scanner.egg-info/dependency_links.txt +0 -0
  30. {robotframework-quality-scanner-0.3.0 → robotframework_quality_scanner-0.4.0}/src/robotframework_quality_scanner.egg-info/requires.txt +0 -0
  31. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner → robotframework_quality_scanner-0.4.0/src}/utils/autofix.py +0 -0
  32. {robotframework-quality-scanner-0.3.0/src/robotframework_quality_scanner → robotframework_quality_scanner-0.4.0/src}/utils/history.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: robotframework-quality-scanner
3
- Version: 0.3.0
2
+ Name: robotframework_quality_scanner
3
+ Version: 0.4.0
4
4
  Summary: Quality scanner for Robot Framework automation - static analysis, performance, duplication detection, and automatic report generation
5
5
  Home-page: https://github.com/luisPinheiro536/qa-static-analysis
6
6
  Author: Luis
@@ -13,8 +13,8 @@ Description: # robotframework-quality-scanner
13
13
 
14
14
  Uma **Robot Framework Library** para escanear projetos de automação **Web, Mobile e API**, identificar **más práticas**, gerar **logs estruturados**, **relatórios** e **sugestões de correção baseadas em boas práticas oficiais**.
15
15
 
16
- **Versão**: 0.2.0
17
- **Status**: Beta com suporte para caching, histórico, 4 analisadores especializados e API REST.
16
+ **Versão**: 0.3.0
17
+ **Status**: Beta com suporte para caching, histórico, 4 analisadores especializados, API REST e logging estruturado com documentação automática.
18
18
 
19
19
  ---
20
20
 
@@ -27,11 +27,12 @@ Description: # robotframework-quality-scanner
27
27
  * **Cache 10x mais rápido** para análises repetidas
28
28
  * Rastrear **histórico e tendências** de qualidade
29
29
  * Gerar **múltiplos relatórios** (JSON, HTML, TXT)
30
+ * **Capturar logs, erros e traces** estruturados em documentação automática
30
31
  * Integrar facilmente com **CI/CD e ferramentas externas** via API REST
31
32
 
32
33
  ---
33
34
 
34
- ## ✨ Features v0.2.0
35
+ ## ✨ Features v0.3.0
35
36
 
36
37
  ### ✅ Implementado
37
38
 
@@ -56,7 +57,20 @@ Description: # robotframework-quality-scanner
56
57
  - Adiciona [Documentation]
57
58
  - Capitaliza keywords
58
59
 
59
- 5. **API REST**
60
+ 5. **Relatórios Executivos e de Cobertura** (v0.3.0)
61
+ - Score de qualidade (0-100)
62
+ - Distribuição por severidade e categoria
63
+ - Cobertura de testes
64
+ - Formatos: Text, JSON, HTML
65
+
66
+ 6. **Logging Estruturado com Documentação** (v0.3.0)
67
+ - Captura automática de erros e traces
68
+ - Relatórios em Markdown com formatação
69
+ - Exportação em JSON para análise programática
70
+ - Pasta `.docs/` para armazenar documentação
71
+ - Histórico de todas as execuções
72
+
73
+ 7. **API REST**
60
74
  - Endpoints para análise de arquivo/diretório
61
75
  - Geração de relatórios (JSON, HTML, TXT)
62
76
  - Health check e sumário
@@ -2,8 +2,8 @@
2
2
 
3
3
  Uma **Robot Framework Library** para escanear projetos de automação **Web, Mobile e API**, identificar **más práticas**, gerar **logs estruturados**, **relatórios** e **sugestões de correção baseadas em boas práticas oficiais**.
4
4
 
5
- **Versão**: 0.2.0
6
- **Status**: Beta com suporte para caching, histórico, 4 analisadores especializados e API REST.
5
+ **Versão**: 0.3.0
6
+ **Status**: Beta com suporte para caching, histórico, 4 analisadores especializados, API REST e logging estruturado com documentação automática.
7
7
 
8
8
  ---
9
9
 
@@ -16,11 +16,12 @@ Uma **Robot Framework Library** para escanear projetos de automação **Web, Mob
16
16
  * **Cache 10x mais rápido** para análises repetidas
17
17
  * Rastrear **histórico e tendências** de qualidade
18
18
  * Gerar **múltiplos relatórios** (JSON, HTML, TXT)
19
+ * **Capturar logs, erros e traces** estruturados em documentação automática
19
20
  * Integrar facilmente com **CI/CD e ferramentas externas** via API REST
20
21
 
21
22
  ---
22
23
 
23
- ## ✨ Features v0.2.0
24
+ ## ✨ Features v0.3.0
24
25
 
25
26
  ### ✅ Implementado
26
27
 
@@ -45,7 +46,20 @@ Uma **Robot Framework Library** para escanear projetos de automação **Web, Mob
45
46
  - Adiciona [Documentation]
46
47
  - Capitaliza keywords
47
48
 
48
- 5. **API REST**
49
+ 5. **Relatórios Executivos e de Cobertura** (v0.3.0)
50
+ - Score de qualidade (0-100)
51
+ - Distribuição por severidade e categoria
52
+ - Cobertura de testes
53
+ - Formatos: Text, JSON, HTML
54
+
55
+ 6. **Logging Estruturado com Documentação** (v0.3.0)
56
+ - Captura automática de erros e traces
57
+ - Relatórios em Markdown com formatação
58
+ - Exportação em JSON para análise programática
59
+ - Pasta `.docs/` para armazenar documentação
60
+ - Histórico de todas as execuções
61
+
62
+ 7. **API REST**
49
63
  - Endpoints para análise de arquivo/diretório
50
64
  - Geração de relatórios (JSON, HTML, TXT)
51
65
  - Health check e sumário
@@ -4,8 +4,8 @@
4
4
  from setuptools import setup, find_packages
5
5
 
6
6
  setup(
7
- name="robotframework-quality-scanner",
8
- version="0.3.0",
7
+ name="robotframework_quality_scanner",
8
+ version="0.4.0",
9
9
  author="Luis",
10
10
  author_email="luis@example.com",
11
11
  description="Quality scanner for Robot Framework automation - static analysis, performance, duplication detection, and automatic report generation",
@@ -29,6 +29,11 @@ setup(
29
29
  "api": ["flask>=2.0"],
30
30
  "all": ["pytest>=7.0", "black>=22.0", "flake8>=4.0", "flask>=2.0"],
31
31
  },
32
+ entry_points={
33
+ "console_scripts": [
34
+ "qa-scanner=robotframework_quality_scanner.cli:main",
35
+ ],
36
+ },
32
37
  keywords=["robotframework", "quality", "testing", "static-analysis", "automation"],
33
38
  classifiers=[
34
39
  "Development Status :: 4 - Beta",
@@ -0,0 +1,12 @@
1
+ # Analisadores especializados
2
+ from .performance_analyzer import PerformanceAnalyzer
3
+ from .duplication_analyzer import DuplicationAnalyzer
4
+ from .dependency_analyzer import DependencyAnalyzer
5
+ from .test_data_analyzer import TestDataAnalyzer
6
+
7
+ __all__ = [
8
+ 'PerformanceAnalyzer',
9
+ 'DuplicationAnalyzer',
10
+ 'DependencyAnalyzer',
11
+ 'TestDataAnalyzer',
12
+ ]
@@ -0,0 +1,6 @@
1
+ # Models para representar dados da análise
2
+ from .issue import Issue
3
+
4
+ __all__ = [
5
+ 'Issue',
6
+ ]
@@ -0,0 +1,22 @@
1
+ class Issue:
2
+ def __init__(self, rule_id, category, severity, description, file, line, recommendation, reference=None):
3
+ self.rule_id = rule_id
4
+ self.category = category
5
+ self.severity = severity
6
+ self.description = description
7
+ self.file = file
8
+ self.line = line
9
+ self.recommendation = recommendation
10
+ self.reference = reference
11
+
12
+ def to_dict(self):
13
+ return {
14
+ "rule_id": self.rule_id,
15
+ "category": self.category,
16
+ "severity": self.severity,
17
+ "description": self.description,
18
+ "file": self.file,
19
+ "line": self.line,
20
+ "recommendation": self.recommendation,
21
+ "reference": self.reference,
22
+ }
@@ -0,0 +1,10 @@
1
+ # Reporters para geração de relatórios
2
+ from .executive_report import ExecutiveReport
3
+ from .coverage_report import CoverageReport
4
+ from . import console_reporter
5
+
6
+ __all__ = [
7
+ 'ExecutiveReport',
8
+ 'CoverageReport',
9
+ 'console_reporter',
10
+ ]
@@ -0,0 +1,7 @@
1
+ def print_issues(issues):
2
+ for iss in issues:
3
+ d = iss.to_dict()
4
+ print(f"[{d['severity']}] {d['rule_id']} - {d['file']}:{d['line']}")
5
+ print(d['description'])
6
+ print("Recommendation:", d['recommendation'])
7
+ print()
@@ -0,0 +1,313 @@
1
+ """Relatório de cobertura de testes."""
2
+ import re
3
+ from collections import defaultdict
4
+
5
+
6
+ class CoverageReport:
7
+ """Analisa cobertura de testes em arquivos Robot Framework."""
8
+
9
+ def __init__(self, files_analyzed):
10
+ """
11
+ Args:
12
+ files_analyzed: Lista de tuplas (filepath, content)
13
+ """
14
+ self.files_analyzed = files_analyzed
15
+ self.coverage_data = self._analyze_coverage()
16
+
17
+ def _analyze_coverage(self):
18
+ """Analisa cobertura em cada arquivo."""
19
+ coverage = defaultdict(lambda: {
20
+ "total_keywords": 0,
21
+ "documented_keywords": 0,
22
+ "used_keywords": 0,
23
+ "unused_keywords": [],
24
+ "test_count": 0,
25
+ "keyword_count": 0,
26
+ "documentation_coverage": 0,
27
+ "keyword_usage_coverage": 0,
28
+ })
29
+
30
+ for filepath, content in self.files_analyzed:
31
+ file_key = filepath if isinstance(filepath, str) else filepath.name
32
+
33
+ # Parse sections
34
+ in_keywords = False
35
+ in_tests = False
36
+ keywords_defined = []
37
+ keywords_used = set()
38
+ documented_kw = 0
39
+ test_count = 0
40
+
41
+ lines = content.split('\n')
42
+ for i, line in enumerate(lines):
43
+ # Detectar seções
44
+ if '*** Keywords ***' in line:
45
+ in_keywords = True
46
+ in_tests = False
47
+ continue
48
+ elif '*** Test Cases ***' in line:
49
+ in_tests = True
50
+ in_keywords = False
51
+ continue
52
+ elif '*** Settings ***' in line or '*** Variables ***' in line:
53
+ in_keywords = False
54
+ in_tests = False
55
+ continue
56
+
57
+ # Contar keywords definidas
58
+ if in_keywords and line.strip() and not line.startswith(' '):
59
+ keywords_defined.append(line.strip())
60
+ # Verificar se tem documentation
61
+ if i + 1 < len(lines) and '[Documentation]' in lines[i + 1]:
62
+ documented_kw += 1
63
+
64
+ # Contar testes
65
+ if in_tests and line.strip() and not line.startswith(' '):
66
+ test_count += 1
67
+
68
+ # Coletar keywords usadas (heurística)
69
+ for kw in keywords_defined:
70
+ if kw in line and not in_keywords:
71
+ keywords_used.add(kw)
72
+
73
+ # Calcular métricas
74
+ total_kw = len(keywords_defined)
75
+ used_kw = len(keywords_used)
76
+ unused_kw = [kw for kw in keywords_defined if kw not in keywords_used]
77
+
78
+ coverage[file_key] = {
79
+ "total_keywords": total_kw,
80
+ "documented_keywords": documented_kw,
81
+ "used_keywords": used_kw,
82
+ "unused_keywords": unused_kw,
83
+ "test_count": test_count,
84
+ "documentation_coverage": (documented_kw / total_kw * 100) if total_kw > 0 else 0,
85
+ "keyword_usage_coverage": (used_kw / total_kw * 100) if total_kw > 0 else 0,
86
+ }
87
+
88
+ return dict(coverage)
89
+
90
+ def get_overall_stats(self):
91
+ """Estatísticas gerais de cobertura."""
92
+ total_files = len(self.coverage_data)
93
+ total_keywords = sum(d["total_keywords"] for d in self.coverage_data.values())
94
+ total_documented = sum(d["documented_keywords"] for d in self.coverage_data.values())
95
+ total_used = sum(d["used_keywords"] for d in self.coverage_data.values())
96
+ total_tests = sum(d["test_count"] for d in self.coverage_data.values())
97
+
98
+ return {
99
+ "files": total_files,
100
+ "total_keywords": total_keywords,
101
+ "documented_keywords": total_documented,
102
+ "used_keywords": total_used,
103
+ "total_tests": total_tests,
104
+ "documentation_coverage_percent": (total_documented / total_keywords * 100) if total_keywords > 0 else 0,
105
+ "keyword_usage_coverage_percent": (total_used / total_keywords * 100) if total_keywords > 0 else 0,
106
+ }
107
+
108
+ def to_dict(self):
109
+ """Exporta como dicionário."""
110
+ return {
111
+ "overall": self.get_overall_stats(),
112
+ "by_file": self.coverage_data,
113
+ }
114
+
115
+ def to_text(self):
116
+ """Exporta como texto."""
117
+ stats = self.get_overall_stats()
118
+
119
+ text = "╔" + "=" * 78 + "╗\n"
120
+ text += "║" + " RELATÓRIO DE COBERTURA DE TESTES ".center(78) + "║\n"
121
+ text += "╚" + "=" * 78 + "╝\n\n"
122
+
123
+ # Resumo geral
124
+ text += "📊 RESUMO GERAL\n"
125
+ text += "-" * 80 + "\n"
126
+ text += f"Arquivos Analisados: {stats['files']}\n"
127
+ text += f"Total de Keywords: {stats['total_keywords']}\n"
128
+ text += f"Keywords Documentadas: {stats['documented_keywords']} ({stats['documentation_coverage_percent']:.1f}%)\n"
129
+ text += f"Keywords Utilizadas: {stats['used_keywords']} ({stats['keyword_usage_coverage_percent']:.1f}%)\n"
130
+ text += f"Total de Testes: {stats['total_tests']}\n\n"
131
+
132
+ # Por arquivo
133
+ text += "📄 POR ARQUIVO\n"
134
+ text += "-" * 80 + "\n"
135
+ for file, data in sorted(self.coverage_data.items()):
136
+ text += f"\n{file}\n"
137
+ text += f" Keywords: {data['total_keywords']} (Docs: {data['documentation_coverage']:.0f}%, Uso: {data['keyword_usage_coverage']:.0f}%)\n"
138
+ text += f" Testes: {data['test_count']}\n"
139
+
140
+ if data['unused_keywords']:
141
+ text += f" ⚠️ Keywords não utilizadas: {', '.join(data['unused_keywords'][:5])}\n"
142
+
143
+ text += "\n" + "=" * 80 + "\n"
144
+
145
+ return text
146
+
147
+ def to_html(self):
148
+ """Exporta como HTML."""
149
+ stats = self.get_overall_stats()
150
+
151
+ doc_color = "#27ae60" if stats["documentation_coverage_percent"] >= 80 else "#f39c12" if stats["documentation_coverage_percent"] >= 60 else "#e74c3c"
152
+ usage_color = "#27ae60" if stats["keyword_usage_coverage_percent"] >= 80 else "#f39c12" if stats["keyword_usage_coverage_percent"] >= 60 else "#e74c3c"
153
+
154
+ html = f"""
155
+ <html>
156
+ <head>
157
+ <meta charset="UTF-8">
158
+ <title>Relatório de Cobertura</title>
159
+ <style>
160
+ body {{
161
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
162
+ margin: 0;
163
+ padding: 20px;
164
+ background: #f5f5f5;
165
+ }}
166
+ .container {{
167
+ max-width: 1200px;
168
+ margin: 0 auto;
169
+ background: white;
170
+ border-radius: 8px;
171
+ padding: 30px;
172
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
173
+ }}
174
+ h1 {{
175
+ color: #2c3e50;
176
+ border-bottom: 3px solid #9b59b6;
177
+ padding-bottom: 10px;
178
+ }}
179
+ .metrics {{
180
+ display: grid;
181
+ grid-template-columns: repeat(5, 1fr);
182
+ gap: 15px;
183
+ margin: 20px 0;
184
+ }}
185
+ .metric-card {{
186
+ background: #ecf0f1;
187
+ padding: 20px;
188
+ border-radius: 5px;
189
+ text-align: center;
190
+ }}
191
+ .metric-card h3 {{
192
+ margin: 0;
193
+ color: #7f8c8d;
194
+ font-size: 12px;
195
+ text-transform: uppercase;
196
+ }}
197
+ .metric-card .value {{
198
+ font-size: 28px;
199
+ font-weight: bold;
200
+ color: #2c3e50;
201
+ }}
202
+ .coverage-bar {{
203
+ width: 100%;
204
+ height: 30px;
205
+ background: #ecf0f1;
206
+ border-radius: 4px;
207
+ overflow: hidden;
208
+ margin: 10px 0;
209
+ }}
210
+ .coverage-fill {{
211
+ height: 100%;
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ color: white;
216
+ font-weight: bold;
217
+ font-size: 12px;
218
+ }}
219
+ table {{
220
+ width: 100%;
221
+ border-collapse: collapse;
222
+ margin: 20px 0;
223
+ }}
224
+ th, td {{
225
+ padding: 12px;
226
+ text-align: left;
227
+ border-bottom: 1px solid #ecf0f1;
228
+ }}
229
+ th {{
230
+ background: #34495e;
231
+ color: white;
232
+ }}
233
+ tr:hover {{
234
+ background: #f8f9fa;
235
+ }}
236
+ </style>
237
+ </head>
238
+ <body>
239
+ <div class="container">
240
+ <h1>📊 Relatório de Cobertura de Testes</h1>
241
+
242
+ <div class="metrics">
243
+ <div class="metric-card">
244
+ <h3>Arquivos</h3>
245
+ <div class="value">{stats['files']}</div>
246
+ </div>
247
+ <div class="metric-card">
248
+ <h3>Keywords</h3>
249
+ <div class="value">{stats['total_keywords']}</div>
250
+ </div>
251
+ <div class="metric-card">
252
+ <h3>Documentação</h3>
253
+ <div class="value">{stats['documentation_coverage_percent']:.0f}%</div>
254
+ </div>
255
+ <div class="metric-card">
256
+ <h3>Utilização</h3>
257
+ <div class="value">{stats['keyword_usage_coverage_percent']:.0f}%</div>
258
+ </div>
259
+ <div class="metric-card">
260
+ <h3>Testes</h3>
261
+ <div class="value">{stats['total_tests']}</div>
262
+ </div>
263
+ </div>
264
+
265
+ <h2>Cobertura Geral</h2>
266
+ <div>
267
+ <strong>Documentação</strong>
268
+ <div class="coverage-bar">
269
+ <div class="coverage-fill" style="width: {stats['documentation_coverage_percent']}%; background: {doc_color};">
270
+ {stats['documentation_coverage_percent']:.1f}%
271
+ </div>
272
+ </div>
273
+ </div>
274
+
275
+ <div>
276
+ <strong>Utilização de Keywords</strong>
277
+ <div class="coverage-bar">
278
+ <div class="coverage-fill" style="width: {stats['keyword_usage_coverage_percent']}%; background: {usage_color};">
279
+ {stats['keyword_usage_coverage_percent']:.1f}%
280
+ </div>
281
+ </div>
282
+ </div>
283
+
284
+ <h2>Cobertura por Arquivo</h2>
285
+ <table>
286
+ <tr>
287
+ <th>Arquivo</th>
288
+ <th>Keywords</th>
289
+ <th>Documentação</th>
290
+ <th>Utilização</th>
291
+ <th>Testes</th>
292
+ </tr>
293
+ """
294
+
295
+ for file, data in sorted(self.coverage_data.items()):
296
+ html += f"""
297
+ <tr>
298
+ <td>{file}</td>
299
+ <td>{data['total_keywords']}</td>
300
+ <td>{data['documentation_coverage']:.0f}%</td>
301
+ <td>{data['keyword_usage_coverage']:.0f}%</td>
302
+ <td>{data['test_count']}</td>
303
+ </tr>
304
+ """
305
+
306
+ html += """
307
+ </table>
308
+ </div>
309
+ </body>
310
+ </html>
311
+ """
312
+
313
+ return html