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,343 @@
|
|
1
|
+
"""
|
2
|
+
Django management command for OpenAPI schema validation.
|
3
|
+
|
4
|
+
Usage:
|
5
|
+
python manage.py validate_openapi # Check all serializers
|
6
|
+
python manage.py validate_openapi --app accounts # Check specific app
|
7
|
+
python manage.py validate_openapi --fix # Auto-fix issues
|
8
|
+
python manage.py validate_openapi --fix --dry-run # Preview fixes
|
9
|
+
python manage.py validate_openapi --report html # Generate HTML report
|
10
|
+
"""
|
11
|
+
|
12
|
+
from django.core.management.base import BaseCommand, CommandError
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import List, Optional
|
15
|
+
|
16
|
+
|
17
|
+
class Command(BaseCommand):
|
18
|
+
"""Validate and fix OpenAPI schema quality issues in DRF serializers."""
|
19
|
+
|
20
|
+
help = "Validate and auto-fix OpenAPI schema quality issues"
|
21
|
+
|
22
|
+
def add_arguments(self, parser):
|
23
|
+
"""Add command arguments."""
|
24
|
+
# Scope options
|
25
|
+
parser.add_argument(
|
26
|
+
"--app",
|
27
|
+
type=str,
|
28
|
+
help="Check specific Django app only",
|
29
|
+
)
|
30
|
+
|
31
|
+
parser.add_argument(
|
32
|
+
"--file",
|
33
|
+
type=str,
|
34
|
+
help="Check specific file only",
|
35
|
+
)
|
36
|
+
|
37
|
+
parser.add_argument(
|
38
|
+
"--pattern",
|
39
|
+
type=str,
|
40
|
+
default="*serializers.py",
|
41
|
+
help="File pattern to match (default: *serializers.py)",
|
42
|
+
)
|
43
|
+
|
44
|
+
# Action options
|
45
|
+
parser.add_argument(
|
46
|
+
"--fix",
|
47
|
+
action="store_true",
|
48
|
+
help="Apply auto-fixes to issues",
|
49
|
+
)
|
50
|
+
|
51
|
+
parser.add_argument(
|
52
|
+
"--dry-run",
|
53
|
+
action="store_true",
|
54
|
+
help="Show what would be fixed without applying changes",
|
55
|
+
)
|
56
|
+
|
57
|
+
parser.add_argument(
|
58
|
+
"--no-confirm",
|
59
|
+
action="store_true",
|
60
|
+
help="Skip confirmation prompt when fixing",
|
61
|
+
)
|
62
|
+
|
63
|
+
# Reporting options
|
64
|
+
parser.add_argument(
|
65
|
+
"--report",
|
66
|
+
type=str,
|
67
|
+
choices=["console", "json", "html"],
|
68
|
+
default="console",
|
69
|
+
help="Report format (default: console)",
|
70
|
+
)
|
71
|
+
|
72
|
+
parser.add_argument(
|
73
|
+
"--output",
|
74
|
+
type=str,
|
75
|
+
help="Output file for JSON/HTML reports",
|
76
|
+
)
|
77
|
+
|
78
|
+
parser.add_argument(
|
79
|
+
"--summary",
|
80
|
+
action="store_true",
|
81
|
+
help="Show summary only (compact output)",
|
82
|
+
)
|
83
|
+
|
84
|
+
# Filtering options
|
85
|
+
parser.add_argument(
|
86
|
+
"--severity",
|
87
|
+
type=str,
|
88
|
+
choices=["error", "warning", "info"],
|
89
|
+
help="Filter by minimum severity level",
|
90
|
+
)
|
91
|
+
|
92
|
+
parser.add_argument(
|
93
|
+
"--rule",
|
94
|
+
type=str,
|
95
|
+
help="Check specific rule only (e.g., type-hint-001)",
|
96
|
+
)
|
97
|
+
|
98
|
+
parser.add_argument(
|
99
|
+
"--fixable-only",
|
100
|
+
action="store_true",
|
101
|
+
help="Show only auto-fixable issues",
|
102
|
+
)
|
103
|
+
|
104
|
+
# Utility options
|
105
|
+
parser.add_argument(
|
106
|
+
"--list-rules",
|
107
|
+
action="store_true",
|
108
|
+
help="List available validation rules and exit",
|
109
|
+
)
|
110
|
+
|
111
|
+
parser.add_argument(
|
112
|
+
"--verbose",
|
113
|
+
action="store_true",
|
114
|
+
help="Show detailed output",
|
115
|
+
)
|
116
|
+
|
117
|
+
def handle(self, *args, **options):
|
118
|
+
"""Handle command execution."""
|
119
|
+
try:
|
120
|
+
# Import validation components
|
121
|
+
from django_cfg.modules.django_client.core.validation import (
|
122
|
+
ValidationChecker,
|
123
|
+
SafeFixer,
|
124
|
+
IssueReporter,
|
125
|
+
SafetyManager,
|
126
|
+
)
|
127
|
+
|
128
|
+
# List rules
|
129
|
+
if options["list_rules"]:
|
130
|
+
self._list_rules()
|
131
|
+
return
|
132
|
+
|
133
|
+
# Get workspace directory
|
134
|
+
workspace = self._get_workspace(options)
|
135
|
+
|
136
|
+
# Create checker
|
137
|
+
checker = ValidationChecker()
|
138
|
+
|
139
|
+
# Check files
|
140
|
+
self.stdout.write(self.style.SUCCESS("\n🔍 Scanning for issues...\n"))
|
141
|
+
issues = self._check_files(checker, workspace, options)
|
142
|
+
|
143
|
+
# Filter issues
|
144
|
+
issues = self._filter_issues(issues, options)
|
145
|
+
|
146
|
+
if not issues:
|
147
|
+
self.stdout.write(self.style.SUCCESS("✅ No issues found!\n"))
|
148
|
+
return
|
149
|
+
|
150
|
+
# Report issues
|
151
|
+
reporter = IssueReporter(use_colors=True)
|
152
|
+
|
153
|
+
if options["summary"]:
|
154
|
+
reporter.display_summary(issues)
|
155
|
+
elif options["report"] == "console":
|
156
|
+
reporter.display_console(
|
157
|
+
issues,
|
158
|
+
show_suggestions=True,
|
159
|
+
group_by_file=True,
|
160
|
+
verbose=options["verbose"]
|
161
|
+
)
|
162
|
+
elif options["report"] == "json":
|
163
|
+
output_path = self._get_output_path(options, "validation_report.json")
|
164
|
+
reporter.save_json(issues, output_path, include_stats=True)
|
165
|
+
elif options["report"] == "html":
|
166
|
+
output_path = self._get_output_path(options, "validation_report.html")
|
167
|
+
reporter.save_html(issues, output_path, title="OpenAPI Validation Report")
|
168
|
+
|
169
|
+
# Apply fixes if requested
|
170
|
+
if options["fix"]:
|
171
|
+
self._apply_fixes(issues, workspace, options)
|
172
|
+
elif not options["summary"]:
|
173
|
+
# Suggest fix command
|
174
|
+
fixable = checker.get_fixable_issues(issues)
|
175
|
+
if fixable:
|
176
|
+
self.stdout.write(
|
177
|
+
self.style.WARNING(
|
178
|
+
f"\n💡 Tip: Run with --fix to auto-fix {len(fixable)} issue(s)"
|
179
|
+
)
|
180
|
+
)
|
181
|
+
|
182
|
+
except Exception as e:
|
183
|
+
raise CommandError(f"Validation failed: {e}")
|
184
|
+
|
185
|
+
def _list_rules(self):
|
186
|
+
"""List available validation rules."""
|
187
|
+
from django_cfg.modules.django_client.core.validation import ValidationChecker
|
188
|
+
|
189
|
+
checker = ValidationChecker()
|
190
|
+
|
191
|
+
self.stdout.write(self.style.SUCCESS(f"\n📋 Available Validation Rules ({len(checker.rules)}):\n"))
|
192
|
+
|
193
|
+
for rule in checker.rules:
|
194
|
+
self.stdout.write(f" • {rule.rule_id}: {rule.name}")
|
195
|
+
self.stdout.write(f" {rule.description}")
|
196
|
+
self.stdout.write("")
|
197
|
+
|
198
|
+
def _get_workspace(self, options) -> Path:
|
199
|
+
"""Get workspace directory to check."""
|
200
|
+
from django.conf import settings
|
201
|
+
|
202
|
+
if options["file"]:
|
203
|
+
# Specific file
|
204
|
+
file_path = Path(options["file"])
|
205
|
+
if not file_path.is_absolute():
|
206
|
+
file_path = Path.cwd() / file_path
|
207
|
+
return file_path.parent
|
208
|
+
|
209
|
+
if options["app"]:
|
210
|
+
# Specific app
|
211
|
+
from django.apps import apps
|
212
|
+
try:
|
213
|
+
app_config = apps.get_app_config(options["app"])
|
214
|
+
return Path(app_config.path)
|
215
|
+
except LookupError:
|
216
|
+
raise CommandError(f"App '{options['app']}' not found")
|
217
|
+
|
218
|
+
# Default: all apps in project
|
219
|
+
# Try BASE_DIR first, fallback to current directory
|
220
|
+
base_dir = getattr(settings, 'BASE_DIR', None)
|
221
|
+
if base_dir:
|
222
|
+
return Path(base_dir)
|
223
|
+
else:
|
224
|
+
return Path.cwd()
|
225
|
+
|
226
|
+
def _check_files(self, checker, workspace: Path, options) -> List:
|
227
|
+
"""Check files for issues."""
|
228
|
+
from django_cfg.modules.django_client.core.validation import Issue
|
229
|
+
|
230
|
+
if options["file"]:
|
231
|
+
# Check specific file
|
232
|
+
file_path = Path(options["file"])
|
233
|
+
if not file_path.is_absolute():
|
234
|
+
file_path = Path.cwd() / file_path
|
235
|
+
|
236
|
+
if not file_path.exists():
|
237
|
+
raise CommandError(f"File not found: {file_path}")
|
238
|
+
|
239
|
+
return checker.check_file(file_path)
|
240
|
+
|
241
|
+
# Check directory
|
242
|
+
pattern = options["pattern"]
|
243
|
+
return checker.check_directory(workspace, pattern=pattern, recursive=True)
|
244
|
+
|
245
|
+
def _filter_issues(self, issues: List, options) -> List:
|
246
|
+
"""Filter issues based on options."""
|
247
|
+
from django_cfg.modules.django_client.core.validation import Severity
|
248
|
+
|
249
|
+
filtered = issues
|
250
|
+
|
251
|
+
# Filter by severity
|
252
|
+
if options["severity"]:
|
253
|
+
min_severity = Severity[options["severity"].upper()]
|
254
|
+
severity_order = {Severity.ERROR: 3, Severity.WARNING: 2, Severity.INFO: 1}
|
255
|
+
min_level = severity_order[min_severity]
|
256
|
+
filtered = [
|
257
|
+
i for i in filtered
|
258
|
+
if severity_order[i.severity] >= min_level
|
259
|
+
]
|
260
|
+
|
261
|
+
# Filter by rule
|
262
|
+
if options["rule"]:
|
263
|
+
filtered = [i for i in filtered if i.rule_id == options["rule"]]
|
264
|
+
|
265
|
+
# Filter by fixability
|
266
|
+
if options["fixable_only"]:
|
267
|
+
filtered = [i for i in filtered if i.auto_fixable]
|
268
|
+
|
269
|
+
return filtered
|
270
|
+
|
271
|
+
def _get_output_path(self, options, default_name: str) -> Path:
|
272
|
+
"""Get output file path."""
|
273
|
+
if options["output"]:
|
274
|
+
output = Path(options["output"])
|
275
|
+
if not output.is_absolute():
|
276
|
+
output = Path.cwd() / output
|
277
|
+
return output
|
278
|
+
|
279
|
+
return Path.cwd() / default_name
|
280
|
+
|
281
|
+
def _apply_fixes(self, issues: List, workspace: Path, options):
|
282
|
+
"""Apply fixes to issues."""
|
283
|
+
from django_cfg.modules.django_client.core.validation import (
|
284
|
+
SafeFixer,
|
285
|
+
SafetyManager,
|
286
|
+
ValidationChecker,
|
287
|
+
)
|
288
|
+
|
289
|
+
# Get fixable issues
|
290
|
+
checker = ValidationChecker()
|
291
|
+
fixable = checker.get_fixable_issues(issues)
|
292
|
+
|
293
|
+
if not fixable:
|
294
|
+
self.stdout.write(self.style.WARNING("\n⚠️ No auto-fixable issues found"))
|
295
|
+
return
|
296
|
+
|
297
|
+
# Create safety manager and fixer
|
298
|
+
safety = SafetyManager(workspace)
|
299
|
+
fixer = SafeFixer(safety)
|
300
|
+
|
301
|
+
# Apply fixes
|
302
|
+
dry_run = options["dry_run"]
|
303
|
+
confirm = not options["no_confirm"]
|
304
|
+
|
305
|
+
self.stdout.write(self.style.SUCCESS("\n🔧 Applying fixes...\n"))
|
306
|
+
|
307
|
+
results = fixer.fix_issues(
|
308
|
+
fixable,
|
309
|
+
dry_run=dry_run,
|
310
|
+
confirm=confirm,
|
311
|
+
verbose=options["verbose"]
|
312
|
+
)
|
313
|
+
|
314
|
+
# Show results
|
315
|
+
if dry_run:
|
316
|
+
self.stdout.write(
|
317
|
+
self.style.WARNING(
|
318
|
+
f"\n🔍 Dry run completed - would fix {results['skipped']} issue(s)"
|
319
|
+
)
|
320
|
+
)
|
321
|
+
else:
|
322
|
+
if results['fixed'] > 0:
|
323
|
+
self.stdout.write(
|
324
|
+
self.style.SUCCESS(
|
325
|
+
f"\n✅ Successfully fixed {results['fixed']} issue(s)!"
|
326
|
+
)
|
327
|
+
)
|
328
|
+
|
329
|
+
if results['failed'] > 0:
|
330
|
+
self.stdout.write(
|
331
|
+
self.style.ERROR(
|
332
|
+
f"\n❌ Failed to fix {results['failed']} issue(s)"
|
333
|
+
)
|
334
|
+
)
|
335
|
+
for error in results['errors']:
|
336
|
+
self.stdout.write(f" - {error}")
|
337
|
+
|
338
|
+
if results['skipped'] > 0:
|
339
|
+
self.stdout.write(
|
340
|
+
self.style.WARNING(
|
341
|
+
f"\n⏭️ Skipped {results['skipped']} issue(s)"
|
342
|
+
)
|
343
|
+
)
|
@@ -0,0 +1,192 @@
|
|
1
|
+
"""
|
2
|
+
Auto-fix enum naming collisions in OpenAPI schema.
|
3
|
+
|
4
|
+
This postprocessing hook automatically generates unique, descriptive enum names
|
5
|
+
based on model names to avoid collisions like "Status50eEnum", "StatusA98Enum".
|
6
|
+
|
7
|
+
Instead generates: "ProductStatusEnum", "OrderStatusEnum", "PostStatusEnum", etc.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import logging
|
11
|
+
from typing import Dict, Any, Optional
|
12
|
+
import re
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
def auto_fix_enum_names(result: Dict[str, Any], generator, request, public) -> Dict[str, Any]:
|
18
|
+
"""
|
19
|
+
DRF Spectacular postprocessing hook to auto-fix enum naming collisions.
|
20
|
+
|
21
|
+
Automatically detects and fixes enum naming collisions by using model names.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
result: OpenAPI schema dict
|
25
|
+
generator: Schema generator instance
|
26
|
+
request: HTTP request
|
27
|
+
public: Whether schema is public
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
Modified OpenAPI schema with fixed enum names
|
31
|
+
|
32
|
+
Example:
|
33
|
+
Before: Status50eEnum, StatusA98Enum (collision hashes)
|
34
|
+
After: ProductStatusEnum, OrderStatusEnum (descriptive names)
|
35
|
+
"""
|
36
|
+
|
37
|
+
if 'components' not in result or 'schemas' not in result['components']:
|
38
|
+
return result
|
39
|
+
|
40
|
+
schemas = result['components']['schemas']
|
41
|
+
|
42
|
+
# Track enum references and their sources (model + field)
|
43
|
+
enum_sources: Dict[str, list] = {} # enum_name -> [(model_name, field_name, choices)]
|
44
|
+
enum_renames: Dict[str, str] = {} # old_name -> new_name
|
45
|
+
|
46
|
+
# Step 1: Find all enums and their sources
|
47
|
+
for schema_name, schema in schemas.items():
|
48
|
+
if schema.get('type') == 'object' and 'properties' in schema:
|
49
|
+
# This is a model schema
|
50
|
+
model_name = _extract_model_name(schema_name)
|
51
|
+
|
52
|
+
for field_name, field_schema in schema['properties'].items():
|
53
|
+
# Check if field references an enum
|
54
|
+
if '$ref' in field_schema:
|
55
|
+
enum_ref = field_schema['$ref']
|
56
|
+
if '#/components/schemas/' in enum_ref:
|
57
|
+
enum_name = enum_ref.split('/')[-1]
|
58
|
+
|
59
|
+
# Track enum source
|
60
|
+
if enum_name not in enum_sources:
|
61
|
+
enum_sources[enum_name] = []
|
62
|
+
|
63
|
+
enum_sources[enum_name].append((model_name, field_name))
|
64
|
+
|
65
|
+
# Step 2: Detect collisions and generate better names
|
66
|
+
for enum_name, sources in enum_sources.items():
|
67
|
+
# Check if enum looks like a collision (contains hash or generic name)
|
68
|
+
if _is_collision_enum(enum_name):
|
69
|
+
# Multiple models use this enum - need unique names
|
70
|
+
if len(sources) == 1:
|
71
|
+
# Single source - generate descriptive name
|
72
|
+
model_name, field_name = sources[0]
|
73
|
+
new_name = _generate_enum_name(model_name, field_name)
|
74
|
+
enum_renames[enum_name] = new_name
|
75
|
+
|
76
|
+
logger.debug(f" Renaming {enum_name} -> {new_name} (from {model_name}.{field_name})")
|
77
|
+
|
78
|
+
# Step 3: Apply renames to schema
|
79
|
+
if enum_renames:
|
80
|
+
logger.info(f"🔧 Auto-fixed {len(enum_renames)} enum naming collision(s)")
|
81
|
+
_apply_enum_renames(result, enum_renames)
|
82
|
+
|
83
|
+
return result
|
84
|
+
|
85
|
+
|
86
|
+
def _extract_model_name(schema_name: str) -> str:
|
87
|
+
"""
|
88
|
+
Extract model name from schema name.
|
89
|
+
|
90
|
+
Examples:
|
91
|
+
"Product" -> "Product"
|
92
|
+
"ProductDetail" -> "Product"
|
93
|
+
"PaginatedProductList" -> "Product"
|
94
|
+
"""
|
95
|
+
# Remove common prefixes/suffixes
|
96
|
+
name = schema_name
|
97
|
+
|
98
|
+
# Remove pagination wrapper
|
99
|
+
if name.startswith('Paginated') and name.endswith('List'):
|
100
|
+
name = name[9:-4] # Remove "Paginated" and "List"
|
101
|
+
|
102
|
+
# Remove common suffixes
|
103
|
+
for suffix in ['Serializer', 'Detail', 'List', 'Create', 'Update']:
|
104
|
+
if name.endswith(suffix):
|
105
|
+
name = name[:-len(suffix)]
|
106
|
+
break
|
107
|
+
|
108
|
+
return name
|
109
|
+
|
110
|
+
|
111
|
+
def _is_collision_enum(enum_name: str) -> bool:
|
112
|
+
"""
|
113
|
+
Check if enum name looks like a collision (contains hash).
|
114
|
+
|
115
|
+
Examples:
|
116
|
+
"Status50eEnum" -> True (has hash)
|
117
|
+
"StatusA98Enum" -> True (has hash)
|
118
|
+
"ProductStatusEnum" -> False (descriptive)
|
119
|
+
"""
|
120
|
+
# Check if enum contains hash-like patterns (3+ hex chars)
|
121
|
+
if re.search(r'[0-9A-Fa-f]{3,}Enum$', enum_name):
|
122
|
+
return True
|
123
|
+
|
124
|
+
# Check for generic single-word enums that are likely collisions
|
125
|
+
# (e.g., "StatusEnum" without model prefix)
|
126
|
+
if re.match(r'^[A-Z][a-z]+Enum$', enum_name):
|
127
|
+
# Single word + Enum - likely collision
|
128
|
+
return True
|
129
|
+
|
130
|
+
return False
|
131
|
+
|
132
|
+
|
133
|
+
def _generate_enum_name(model_name: str, field_name: str) -> str:
|
134
|
+
"""
|
135
|
+
Generate descriptive enum name from model and field.
|
136
|
+
|
137
|
+
Examples:
|
138
|
+
("Product", "status") -> "ProductStatusEnum"
|
139
|
+
("Order", "status") -> "OrderStatusEnum"
|
140
|
+
("Post", "status") -> "PostStatusEnum"
|
141
|
+
"""
|
142
|
+
# Capitalize field name
|
143
|
+
field_capitalized = field_name.capitalize()
|
144
|
+
|
145
|
+
# Combine: ModelName + FieldName + Enum
|
146
|
+
return f"{model_name}{field_capitalized}Enum"
|
147
|
+
|
148
|
+
|
149
|
+
def _apply_enum_renames(schema: Dict[str, Any], renames: Dict[str, str]) -> None:
|
150
|
+
"""
|
151
|
+
Apply enum renames throughout the schema.
|
152
|
+
|
153
|
+
Renames both:
|
154
|
+
1. Schema component definitions (components/schemas/OldName -> NewName)
|
155
|
+
2. All references to renamed enums ($ref: #/components/schemas/OldName)
|
156
|
+
"""
|
157
|
+
if 'components' not in schema or 'schemas' not in schema['components']:
|
158
|
+
return
|
159
|
+
|
160
|
+
schemas = schema['components']['schemas']
|
161
|
+
|
162
|
+
# Step 1: Rename schema definitions
|
163
|
+
for old_name, new_name in renames.items():
|
164
|
+
if old_name in schemas:
|
165
|
+
schemas[new_name] = schemas.pop(old_name)
|
166
|
+
logger.debug(f" Renamed schema: {old_name} -> {new_name}")
|
167
|
+
|
168
|
+
# Step 2: Update all $ref references
|
169
|
+
_update_refs_recursive(schema, renames)
|
170
|
+
|
171
|
+
|
172
|
+
def _update_refs_recursive(obj: Any, renames: Dict[str, str]) -> None:
|
173
|
+
"""
|
174
|
+
Recursively update all $ref references in schema.
|
175
|
+
"""
|
176
|
+
if isinstance(obj, dict):
|
177
|
+
# Check if this dict contains a $ref
|
178
|
+
if '$ref' in obj:
|
179
|
+
ref = obj['$ref']
|
180
|
+
if '#/components/schemas/' in ref:
|
181
|
+
enum_name = ref.split('/')[-1]
|
182
|
+
if enum_name in renames:
|
183
|
+
obj['$ref'] = f"#/components/schemas/{renames[enum_name]}"
|
184
|
+
|
185
|
+
# Recurse into dict values
|
186
|
+
for value in obj.values():
|
187
|
+
_update_refs_recursive(value, renames)
|
188
|
+
|
189
|
+
elif isinstance(obj, list):
|
190
|
+
# Recurse into list items
|
191
|
+
for item in obj:
|
192
|
+
_update_refs_recursive(item, renames)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
"""
|
2
|
+
Django URL integration.
|
3
|
+
|
4
|
+
Provides URL patterns for OpenAPI schema generation.
|
5
|
+
Each configured group gets its own schema endpoint.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import List, Any
|
9
|
+
|
10
|
+
|
11
|
+
def _is_django_configured() -> bool:
|
12
|
+
"""Check if Django settings are configured."""
|
13
|
+
try:
|
14
|
+
from django.conf import settings
|
15
|
+
return settings.configured
|
16
|
+
except ImportError:
|
17
|
+
return False
|
18
|
+
|
19
|
+
|
20
|
+
def get_openapi_urls() -> List[Any]:
|
21
|
+
"""
|
22
|
+
Get URL patterns for OpenAPI schema generation.
|
23
|
+
|
24
|
+
Creates URLs for each configured group:
|
25
|
+
- /openapi/{group_name}/schema/ - JSON schema
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
List of Django URL patterns
|
29
|
+
"""
|
30
|
+
try:
|
31
|
+
from django.urls import path
|
32
|
+
from drf_spectacular.views import SpectacularAPIView
|
33
|
+
from django_cfg.modules.django_client.core import get_openapi_service
|
34
|
+
except ImportError:
|
35
|
+
return []
|
36
|
+
|
37
|
+
service = get_openapi_service()
|
38
|
+
|
39
|
+
if not service.config or not service.is_enabled():
|
40
|
+
return []
|
41
|
+
|
42
|
+
patterns = []
|
43
|
+
|
44
|
+
for group_name in service.get_group_names():
|
45
|
+
group_config = service.get_group(group_name)
|
46
|
+
if not group_config:
|
47
|
+
continue
|
48
|
+
|
49
|
+
# Schema endpoint for each group
|
50
|
+
patterns.append(
|
51
|
+
path(
|
52
|
+
f'{group_name}/schema/',
|
53
|
+
SpectacularAPIView.as_view(
|
54
|
+
urlconf=f'openapi_group_{group_name}',
|
55
|
+
api_version=group_config.version,
|
56
|
+
),
|
57
|
+
name=f'openapi-schema-{group_name}',
|
58
|
+
)
|
59
|
+
)
|
60
|
+
|
61
|
+
return patterns
|
62
|
+
|
63
|
+
|
64
|
+
# Export urlpatterns for django.urls.include()
|
65
|
+
# Only create urlpatterns if Django is configured
|
66
|
+
if _is_django_configured():
|
67
|
+
urlpatterns = get_openapi_urls()
|
68
|
+
else:
|
69
|
+
urlpatterns = []
|
70
|
+
|
71
|
+
|
72
|
+
__all__ = ["get_openapi_urls", "urlpatterns"]
|
File without changes
|
File without changes
|