django-cfg 1.4.10__py3-none-any.whl → 1.4.11__py3-none-any.whl
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.
- django_cfg/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/views/api/currencies.py +49 -6
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +72 -49
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/dashboard/sections/documentation.py +391 -0
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -348
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +238 -0
- django_cfg/modules/django_admin/management/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +188 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +767 -0
- django_cfg/modules/django_client/core/generator/python.py +751 -0
- django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
- django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
- django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/typescript.py +872 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/spectacular/__init__.py +9 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +16 -5
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/RECORD +180 -59
- django_cfg/management/commands/generate.py +0 -107
- /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,480 @@
|
|
1
|
+
"""Issue reporter for formatting validation results."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from collections import defaultdict
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Dict, List, Any
|
7
|
+
|
8
|
+
from .rules import Issue, Severity
|
9
|
+
|
10
|
+
|
11
|
+
class IssueReporter:
|
12
|
+
"""
|
13
|
+
Formats and displays validation issues in various formats.
|
14
|
+
|
15
|
+
Example:
|
16
|
+
>>> reporter = IssueReporter()
|
17
|
+
>>> reporter.display_console(issues)
|
18
|
+
>>> reporter.save_json(issues, Path('report.json'))
|
19
|
+
>>> reporter.save_html(issues, Path('report.html'))
|
20
|
+
"""
|
21
|
+
|
22
|
+
# ANSI color codes for terminal
|
23
|
+
COLORS = {
|
24
|
+
'reset': '\033[0m',
|
25
|
+
'red': '\033[91m',
|
26
|
+
'yellow': '\033[93m',
|
27
|
+
'blue': '\033[94m',
|
28
|
+
'green': '\033[92m',
|
29
|
+
'gray': '\033[90m',
|
30
|
+
'bold': '\033[1m',
|
31
|
+
}
|
32
|
+
|
33
|
+
# Severity symbols and colors
|
34
|
+
SEVERITY_CONFIG = {
|
35
|
+
Severity.ERROR: {'symbol': '❌', 'color': 'red', 'label': 'ERROR'},
|
36
|
+
Severity.WARNING: {'symbol': '⚠️ ', 'color': 'yellow', 'label': 'WARNING'},
|
37
|
+
Severity.INFO: {'symbol': 'ℹ️ ', 'color': 'blue', 'label': 'INFO'},
|
38
|
+
}
|
39
|
+
|
40
|
+
def __init__(self, use_colors: bool = True):
|
41
|
+
"""
|
42
|
+
Initialize reporter.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
use_colors: If True, use ANSI colors in console output
|
46
|
+
"""
|
47
|
+
self.use_colors = use_colors
|
48
|
+
|
49
|
+
def display_console(
|
50
|
+
self,
|
51
|
+
issues: List[Issue],
|
52
|
+
show_suggestions: bool = True,
|
53
|
+
group_by_file: bool = True,
|
54
|
+
verbose: bool = False
|
55
|
+
) -> None:
|
56
|
+
"""
|
57
|
+
Display issues in console with colors and formatting.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
issues: List of issues to display
|
61
|
+
show_suggestions: If True, show fix suggestions
|
62
|
+
group_by_file: If True, group issues by file
|
63
|
+
verbose: If True, show additional context
|
64
|
+
"""
|
65
|
+
if not issues:
|
66
|
+
self._print("✅ No issues found!", 'green', bold=True)
|
67
|
+
return
|
68
|
+
|
69
|
+
# Statistics
|
70
|
+
stats = self._get_statistics(issues)
|
71
|
+
self._print_header(stats)
|
72
|
+
|
73
|
+
if group_by_file:
|
74
|
+
self._display_by_file(issues, show_suggestions, verbose)
|
75
|
+
else:
|
76
|
+
self._display_flat(issues, show_suggestions, verbose)
|
77
|
+
|
78
|
+
self._print_footer(stats)
|
79
|
+
|
80
|
+
def display_summary(self, issues: List[Issue]) -> None:
|
81
|
+
"""
|
82
|
+
Display compact summary of issues.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
issues: List of issues to summarize
|
86
|
+
"""
|
87
|
+
if not issues:
|
88
|
+
self._print("✅ No issues found!", 'green', bold=True)
|
89
|
+
return
|
90
|
+
|
91
|
+
stats = self._get_statistics(issues)
|
92
|
+
|
93
|
+
print(f"\n📊 Validation Summary")
|
94
|
+
print(f" Total: {stats['total']} issue(s)")
|
95
|
+
print(f" Errors: {stats['by_severity']['error']} | "
|
96
|
+
f"Warnings: {stats['by_severity']['warning']} | "
|
97
|
+
f"Info: {stats['by_severity']['info']}")
|
98
|
+
print(f" Auto-fixable: {stats['fixable']} ({stats['fixable_percent']:.1f}%)")
|
99
|
+
print(f" Files affected: {stats['file_count']}")
|
100
|
+
print()
|
101
|
+
|
102
|
+
def save_json(
|
103
|
+
self,
|
104
|
+
issues: List[Issue],
|
105
|
+
output_path: Path,
|
106
|
+
include_stats: bool = True
|
107
|
+
) -> None:
|
108
|
+
"""
|
109
|
+
Save issues as JSON report.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
output_path: Path to save JSON file
|
113
|
+
issues: List of issues
|
114
|
+
include_stats: If True, include statistics
|
115
|
+
"""
|
116
|
+
data = {
|
117
|
+
'issues': [self._issue_to_dict(issue) for issue in issues]
|
118
|
+
}
|
119
|
+
|
120
|
+
if include_stats:
|
121
|
+
data['statistics'] = self._get_statistics(issues)
|
122
|
+
|
123
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
124
|
+
output_path.write_text(json.dumps(data, indent=2), encoding='utf-8')
|
125
|
+
print(f"📄 JSON report saved to: {output_path}")
|
126
|
+
|
127
|
+
def save_html(
|
128
|
+
self,
|
129
|
+
issues: List[Issue],
|
130
|
+
output_path: Path,
|
131
|
+
title: str = "Validation Report"
|
132
|
+
) -> None:
|
133
|
+
"""
|
134
|
+
Save issues as HTML report.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
output_path: Path to save HTML file
|
138
|
+
issues: List of issues
|
139
|
+
title: Report title
|
140
|
+
"""
|
141
|
+
stats = self._get_statistics(issues)
|
142
|
+
by_file = self._group_by_file(issues)
|
143
|
+
|
144
|
+
html = self._generate_html(title, stats, by_file, issues)
|
145
|
+
|
146
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
147
|
+
output_path.write_text(html, encoding='utf-8')
|
148
|
+
print(f"📄 HTML report saved to: {output_path}")
|
149
|
+
|
150
|
+
# Private methods
|
151
|
+
|
152
|
+
def _get_statistics(self, issues: List[Issue]) -> Dict[str, Any]:
|
153
|
+
"""Calculate statistics about issues."""
|
154
|
+
by_severity = defaultdict(int)
|
155
|
+
by_rule = defaultdict(int)
|
156
|
+
files = set()
|
157
|
+
fixable = 0
|
158
|
+
|
159
|
+
for issue in issues:
|
160
|
+
by_severity[issue.severity.value] += 1
|
161
|
+
by_rule[issue.rule_id] += 1
|
162
|
+
files.add(str(issue.file))
|
163
|
+
if issue.auto_fixable:
|
164
|
+
fixable += 1
|
165
|
+
|
166
|
+
total = len(issues)
|
167
|
+
fixable_percent = (fixable / total * 100) if total > 0 else 0
|
168
|
+
|
169
|
+
return {
|
170
|
+
'total': total,
|
171
|
+
'fixable': fixable,
|
172
|
+
'fixable_percent': fixable_percent,
|
173
|
+
'file_count': len(files),
|
174
|
+
'by_severity': {
|
175
|
+
'error': by_severity.get('error', 0),
|
176
|
+
'warning': by_severity.get('warning', 0),
|
177
|
+
'info': by_severity.get('info', 0),
|
178
|
+
},
|
179
|
+
'by_rule': dict(by_rule),
|
180
|
+
}
|
181
|
+
|
182
|
+
def _group_by_file(self, issues: List[Issue]) -> Dict[Path, List[Issue]]:
|
183
|
+
"""Group issues by file path."""
|
184
|
+
by_file = defaultdict(list)
|
185
|
+
for issue in issues:
|
186
|
+
by_file[issue.file].append(issue)
|
187
|
+
return dict(by_file)
|
188
|
+
|
189
|
+
def _print_header(self, stats: Dict[str, Any]) -> None:
|
190
|
+
"""Print report header."""
|
191
|
+
total = stats['total']
|
192
|
+
errors = stats['by_severity']['error']
|
193
|
+
warnings = stats['by_severity']['warning']
|
194
|
+
infos = stats['by_severity']['info']
|
195
|
+
|
196
|
+
self._print("\n" + "=" * 80, 'gray')
|
197
|
+
self._print("🔍 Validation Report", 'bold')
|
198
|
+
self._print("=" * 80, 'gray')
|
199
|
+
|
200
|
+
msg = f"Found {total} issue(s): "
|
201
|
+
if errors > 0:
|
202
|
+
msg += f"{errors} error(s), "
|
203
|
+
if warnings > 0:
|
204
|
+
msg += f"{warnings} warning(s), "
|
205
|
+
if infos > 0:
|
206
|
+
msg += f"{infos} info"
|
207
|
+
|
208
|
+
color = 'red' if errors > 0 else 'yellow' if warnings > 0 else 'blue'
|
209
|
+
self._print(msg, color)
|
210
|
+
|
211
|
+
if stats['fixable'] > 0:
|
212
|
+
self._print(
|
213
|
+
f"✨ {stats['fixable']} issue(s) can be auto-fixed "
|
214
|
+
f"({stats['fixable_percent']:.1f}%)",
|
215
|
+
'green'
|
216
|
+
)
|
217
|
+
print()
|
218
|
+
|
219
|
+
def _print_footer(self, stats: Dict[str, Any]) -> None:
|
220
|
+
"""Print report footer."""
|
221
|
+
self._print("=" * 80, 'gray')
|
222
|
+
print(f"Total: {stats['total']} issue(s) in {stats['file_count']} file(s)")
|
223
|
+
self._print("=" * 80 + "\n", 'gray')
|
224
|
+
|
225
|
+
def _display_by_file(
|
226
|
+
self,
|
227
|
+
issues: List[Issue],
|
228
|
+
show_suggestions: bool,
|
229
|
+
verbose: bool
|
230
|
+
) -> None:
|
231
|
+
"""Display issues grouped by file."""
|
232
|
+
by_file = self._group_by_file(issues)
|
233
|
+
|
234
|
+
for file_path, file_issues in sorted(by_file.items()):
|
235
|
+
# File header
|
236
|
+
self._print(f"\n📝 {file_path.name}", 'bold')
|
237
|
+
self._print(f" {file_path}", 'gray')
|
238
|
+
|
239
|
+
# Issues for this file
|
240
|
+
for issue in sorted(file_issues, key=lambda i: i.line):
|
241
|
+
self._display_issue(issue, show_suggestions, verbose, indent=3)
|
242
|
+
|
243
|
+
def _display_flat(
|
244
|
+
self,
|
245
|
+
issues: List[Issue],
|
246
|
+
show_suggestions: bool,
|
247
|
+
verbose: bool
|
248
|
+
) -> None:
|
249
|
+
"""Display issues in flat list."""
|
250
|
+
for issue in sorted(issues, key=lambda i: (str(i.file), i.line)):
|
251
|
+
self._display_issue(issue, show_suggestions, verbose, indent=0)
|
252
|
+
|
253
|
+
def _display_issue(
|
254
|
+
self,
|
255
|
+
issue: Issue,
|
256
|
+
show_suggestions: bool,
|
257
|
+
verbose: bool,
|
258
|
+
indent: int = 0
|
259
|
+
) -> None:
|
260
|
+
"""Display single issue."""
|
261
|
+
prefix = " " * indent
|
262
|
+
|
263
|
+
# Severity symbol and location
|
264
|
+
config = self.SEVERITY_CONFIG[issue.severity]
|
265
|
+
symbol = config['symbol']
|
266
|
+
color = config['color']
|
267
|
+
|
268
|
+
location = f"{issue.file.name}:{issue.line}:{issue.column}"
|
269
|
+
self._print(f"{prefix}{symbol} {location}", color)
|
270
|
+
|
271
|
+
# Message
|
272
|
+
print(f"{prefix} {issue.message}")
|
273
|
+
|
274
|
+
# Rule ID
|
275
|
+
if verbose:
|
276
|
+
self._print(f"{prefix} [{issue.rule_id}]", 'gray')
|
277
|
+
|
278
|
+
# Suggestion
|
279
|
+
if show_suggestions and issue.suggestion:
|
280
|
+
auto_fix = " (auto-fixable)" if issue.auto_fixable else ""
|
281
|
+
self._print(f"{prefix} 💡 {issue.suggestion}{auto_fix}", 'blue')
|
282
|
+
|
283
|
+
# Context
|
284
|
+
if verbose and issue.context:
|
285
|
+
print(f"{prefix} Context: {issue.context}")
|
286
|
+
|
287
|
+
print()
|
288
|
+
|
289
|
+
def _issue_to_dict(self, issue: Issue) -> Dict[str, Any]:
|
290
|
+
"""Convert issue to dictionary."""
|
291
|
+
return {
|
292
|
+
'rule_id': issue.rule_id,
|
293
|
+
'severity': issue.severity.value,
|
294
|
+
'file': str(issue.file),
|
295
|
+
'line': issue.line,
|
296
|
+
'column': issue.column,
|
297
|
+
'message': issue.message,
|
298
|
+
'suggestion': issue.suggestion,
|
299
|
+
'auto_fixable': issue.auto_fixable,
|
300
|
+
'context': issue.context,
|
301
|
+
}
|
302
|
+
|
303
|
+
def _generate_html(
|
304
|
+
self,
|
305
|
+
title: str,
|
306
|
+
stats: Dict[str, Any],
|
307
|
+
by_file: Dict[Path, List[Issue]],
|
308
|
+
all_issues: List[Issue]
|
309
|
+
) -> str:
|
310
|
+
"""Generate HTML report."""
|
311
|
+
# Simple HTML template
|
312
|
+
html = f"""<!DOCTYPE html>
|
313
|
+
<html lang="en">
|
314
|
+
<head>
|
315
|
+
<meta charset="UTF-8">
|
316
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
317
|
+
<title>{title}</title>
|
318
|
+
<style>
|
319
|
+
body {{
|
320
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
321
|
+
max-width: 1200px;
|
322
|
+
margin: 40px auto;
|
323
|
+
padding: 20px;
|
324
|
+
background: #f5f5f5;
|
325
|
+
}}
|
326
|
+
.header {{
|
327
|
+
background: white;
|
328
|
+
padding: 30px;
|
329
|
+
border-radius: 8px;
|
330
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
331
|
+
margin-bottom: 20px;
|
332
|
+
}}
|
333
|
+
h1 {{ margin: 0 0 20px 0; color: #333; }}
|
334
|
+
.stats {{
|
335
|
+
display: grid;
|
336
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
337
|
+
gap: 15px;
|
338
|
+
margin-top: 20px;
|
339
|
+
}}
|
340
|
+
.stat {{
|
341
|
+
background: #f8f9fa;
|
342
|
+
padding: 15px;
|
343
|
+
border-radius: 5px;
|
344
|
+
text-align: center;
|
345
|
+
}}
|
346
|
+
.stat-value {{
|
347
|
+
font-size: 32px;
|
348
|
+
font-weight: bold;
|
349
|
+
margin-bottom: 5px;
|
350
|
+
}}
|
351
|
+
.stat-label {{
|
352
|
+
color: #666;
|
353
|
+
font-size: 14px;
|
354
|
+
}}
|
355
|
+
.file-section {{
|
356
|
+
background: white;
|
357
|
+
padding: 20px;
|
358
|
+
border-radius: 8px;
|
359
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
360
|
+
margin-bottom: 15px;
|
361
|
+
}}
|
362
|
+
.file-header {{
|
363
|
+
font-size: 18px;
|
364
|
+
font-weight: bold;
|
365
|
+
margin-bottom: 15px;
|
366
|
+
color: #333;
|
367
|
+
}}
|
368
|
+
.issue {{
|
369
|
+
padding: 15px;
|
370
|
+
border-left: 4px solid;
|
371
|
+
margin-bottom: 10px;
|
372
|
+
background: #f8f9fa;
|
373
|
+
}}
|
374
|
+
.issue.error {{ border-color: #dc3545; }}
|
375
|
+
.issue.warning {{ border-color: #ffc107; }}
|
376
|
+
.issue.info {{ border-color: #17a2b8; }}
|
377
|
+
.issue-location {{
|
378
|
+
font-family: monospace;
|
379
|
+
font-size: 13px;
|
380
|
+
color: #666;
|
381
|
+
margin-bottom: 5px;
|
382
|
+
}}
|
383
|
+
.issue-message {{
|
384
|
+
margin-bottom: 8px;
|
385
|
+
}}
|
386
|
+
.issue-suggestion {{
|
387
|
+
color: #0066cc;
|
388
|
+
font-size: 14px;
|
389
|
+
}}
|
390
|
+
.auto-fixable {{
|
391
|
+
display: inline-block;
|
392
|
+
background: #28a745;
|
393
|
+
color: white;
|
394
|
+
padding: 2px 8px;
|
395
|
+
border-radius: 3px;
|
396
|
+
font-size: 12px;
|
397
|
+
margin-left: 10px;
|
398
|
+
}}
|
399
|
+
</style>
|
400
|
+
</head>
|
401
|
+
<body>
|
402
|
+
<div class="header">
|
403
|
+
<h1>🔍 {title}</h1>
|
404
|
+
<div class="stats">
|
405
|
+
<div class="stat">
|
406
|
+
<div class="stat-value">{stats['total']}</div>
|
407
|
+
<div class="stat-label">Total Issues</div>
|
408
|
+
</div>
|
409
|
+
<div class="stat">
|
410
|
+
<div class="stat-value">{stats['by_severity']['error']}</div>
|
411
|
+
<div class="stat-label">Errors</div>
|
412
|
+
</div>
|
413
|
+
<div class="stat">
|
414
|
+
<div class="stat-value">{stats['by_severity']['warning']}</div>
|
415
|
+
<div class="stat-label">Warnings</div>
|
416
|
+
</div>
|
417
|
+
<div class="stat">
|
418
|
+
<div class="stat-value">{stats['fixable']}</div>
|
419
|
+
<div class="stat-label">Auto-fixable</div>
|
420
|
+
</div>
|
421
|
+
<div class="stat">
|
422
|
+
<div class="stat-value">{stats['file_count']}</div>
|
423
|
+
<div class="stat-label">Files</div>
|
424
|
+
</div>
|
425
|
+
</div>
|
426
|
+
</div>
|
427
|
+
"""
|
428
|
+
|
429
|
+
# Group by file
|
430
|
+
for file_path, file_issues in sorted(by_file.items()):
|
431
|
+
html += f"""
|
432
|
+
<div class="file-section">
|
433
|
+
<div class="file-header">📝 {file_path.name}</div>
|
434
|
+
<div style="color: #666; font-size: 14px; margin-bottom: 15px;">{file_path}</div>
|
435
|
+
"""
|
436
|
+
|
437
|
+
for issue in sorted(file_issues, key=lambda i: i.line):
|
438
|
+
severity_class = issue.severity.value
|
439
|
+
auto_fix_badge = '<span class="auto-fixable">auto-fixable</span>' if issue.auto_fixable else ''
|
440
|
+
|
441
|
+
html += f"""
|
442
|
+
<div class="issue {severity_class}">
|
443
|
+
<div class="issue-location">Line {issue.line}:{issue.column} [{issue.rule_id}]</div>
|
444
|
+
<div class="issue-message">{issue.message} {auto_fix_badge}</div>
|
445
|
+
"""
|
446
|
+
if issue.suggestion:
|
447
|
+
html += f"""
|
448
|
+
<div class="issue-suggestion">💡 {issue.suggestion}</div>
|
449
|
+
"""
|
450
|
+
html += """
|
451
|
+
</div>
|
452
|
+
"""
|
453
|
+
|
454
|
+
html += """
|
455
|
+
</div>
|
456
|
+
"""
|
457
|
+
|
458
|
+
html += """
|
459
|
+
</body>
|
460
|
+
</html>
|
461
|
+
"""
|
462
|
+
return html
|
463
|
+
|
464
|
+
def _print(self, text: str, color: str = '', bold: bool = False) -> None:
|
465
|
+
"""Print with optional color."""
|
466
|
+
if not self.use_colors:
|
467
|
+
print(text)
|
468
|
+
return
|
469
|
+
|
470
|
+
codes = []
|
471
|
+
if bold:
|
472
|
+
codes.append(self.COLORS['bold'])
|
473
|
+
if color and color in self.COLORS:
|
474
|
+
codes.append(self.COLORS[color])
|
475
|
+
|
476
|
+
if codes:
|
477
|
+
reset = self.COLORS['reset']
|
478
|
+
print(f"{''.join(codes)}{text}{reset}")
|
479
|
+
else:
|
480
|
+
print(text)
|
@@ -0,0 +1,96 @@
|
|
1
|
+
"""Base classes for validation rules."""
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from enum import Enum
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import List
|
8
|
+
|
9
|
+
|
10
|
+
class Severity(Enum):
|
11
|
+
"""Issue severity levels."""
|
12
|
+
ERROR = "error"
|
13
|
+
WARNING = "warning"
|
14
|
+
INFO = "info"
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class Issue:
|
19
|
+
"""Represents a validation issue."""
|
20
|
+
rule_id: str
|
21
|
+
severity: Severity
|
22
|
+
file: Path
|
23
|
+
line: int
|
24
|
+
column: int
|
25
|
+
message: str
|
26
|
+
suggestion: str
|
27
|
+
auto_fixable: bool
|
28
|
+
context: dict # Additional context for fixing
|
29
|
+
|
30
|
+
def __str__(self) -> str:
|
31
|
+
return f"{self.file}:{self.line}:{self.column} [{self.severity.value}] {self.message}"
|
32
|
+
|
33
|
+
|
34
|
+
class ValidationRule(ABC):
|
35
|
+
"""Base class for all validation rules."""
|
36
|
+
|
37
|
+
@property
|
38
|
+
@abstractmethod
|
39
|
+
def rule_id(self) -> str:
|
40
|
+
"""Unique rule identifier (e.g., 'type-hint-001')."""
|
41
|
+
pass
|
42
|
+
|
43
|
+
@property
|
44
|
+
@abstractmethod
|
45
|
+
def name(self) -> str:
|
46
|
+
"""Human-readable rule name."""
|
47
|
+
pass
|
48
|
+
|
49
|
+
@property
|
50
|
+
@abstractmethod
|
51
|
+
def description(self) -> str:
|
52
|
+
"""What this rule checks."""
|
53
|
+
pass
|
54
|
+
|
55
|
+
@abstractmethod
|
56
|
+
def check(self, file_path: Path) -> List[Issue]:
|
57
|
+
"""
|
58
|
+
Check file for issues.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
file_path: Path to Python file to check
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
List of found issues
|
65
|
+
"""
|
66
|
+
pass
|
67
|
+
|
68
|
+
@abstractmethod
|
69
|
+
def can_fix(self, issue: Issue) -> bool:
|
70
|
+
"""
|
71
|
+
Check if this issue can be auto-fixed.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
issue: Issue to check
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
True if auto-fixable
|
78
|
+
"""
|
79
|
+
pass
|
80
|
+
|
81
|
+
@abstractmethod
|
82
|
+
def fix(self, issue: Issue) -> bool:
|
83
|
+
"""
|
84
|
+
Apply fix for this issue.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
issue: Issue to fix
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
True if fix was successful
|
91
|
+
|
92
|
+
Note:
|
93
|
+
This method modifies files directly!
|
94
|
+
Caller is responsible for backups/rollback.
|
95
|
+
"""
|
96
|
+
pass
|