django-cfg 1.4.10__py3-none-any.whl → 1.4.13__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 +73 -49
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- 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 +162 -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 +208 -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 +838 -0
- django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
- django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
- django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
- django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
- django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
- django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
- django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
- django_cfg/modules/django_client/core/generator/python/templates/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +153 -0
- django_cfg/modules/django_client/core/generator/python/templates/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +68 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/main_client_file.py.jinja +14 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +52 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/enum_class.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/schema_class.py.jinja +21 -0
- django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
- django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
- django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/client.ts.jinja +403 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/main_client_file.ts.jinja +10 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +268 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/errors.ts.jinja +116 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/logger.ts.jinja +259 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja +158 -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 +427 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/pytest.ini +30 -0
- django_cfg/modules/django_client/spectacular/__init__.py +10 -0
- django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
- django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
- django_cfg/modules/django_dashboard/management/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
- django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
- django_cfg/modules/django_dashboard/sections/documentation.py +391 -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/LOGGING_GUIDE.md +1 -1
- 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 +21 -10
- 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.13.dist-info}/METADATA +2 -2
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/RECORD +224 -74
- django_cfg/management/commands/generate.py +0 -107
- /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
- /django_cfg/{dashboard → modules/django_admin}/management/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_admin}/management/commands/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.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
|