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,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,30 @@
|
|
1
|
+
[pytest]
|
2
|
+
# Pytest configuration for django_client tests
|
3
|
+
|
4
|
+
testpaths = tests
|
5
|
+
|
6
|
+
python_files = test_*.py
|
7
|
+
python_classes = Test*
|
8
|
+
python_functions = test_*
|
9
|
+
|
10
|
+
# Markers
|
11
|
+
markers =
|
12
|
+
unit: Unit tests (fast, isolated)
|
13
|
+
integration: Integration tests (slower, may require setup)
|
14
|
+
slow: Slow tests (can be skipped for quick runs)
|
15
|
+
|
16
|
+
# Output options
|
17
|
+
addopts =
|
18
|
+
-v
|
19
|
+
--strict-markers
|
20
|
+
--tb=short
|
21
|
+
--color=yes
|
22
|
+
|
23
|
+
# Coverage options (if pytest-cov installed)
|
24
|
+
# addopts = --cov=django_cfg.modules.django_client --cov-report=html --cov-report=term
|
25
|
+
|
26
|
+
# Asyncio configuration
|
27
|
+
asyncio_mode = auto
|
28
|
+
|
29
|
+
# Ignore patterns
|
30
|
+
norecursedirs = .git .tox dist build *.egg __pycache__
|
@@ -0,0 +1,10 @@
|
|
1
|
+
"""
|
2
|
+
DRF Spectacular postprocessing hooks for django-cfg.
|
3
|
+
|
4
|
+
Auto-fixes and enhancements for OpenAPI schema generation.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .enum_naming import auto_fix_enum_names
|
8
|
+
from .async_detection import mark_async_operations
|
9
|
+
|
10
|
+
__all__ = ['auto_fix_enum_names', 'mark_async_operations']
|
@@ -0,0 +1,187 @@
|
|
1
|
+
"""
|
2
|
+
Async View Detection for OpenAPI Schema.
|
3
|
+
|
4
|
+
Postprocessing hook that detects async-capable Django views and marks
|
5
|
+
operations with x-async-capable extension for dual client generation.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import inspect
|
9
|
+
import logging
|
10
|
+
from typing import Dict, Any, Optional
|
11
|
+
from django.urls import resolve, Resolver404
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
def mark_async_operations(result: Dict[str, Any], generator, request, public) -> Dict[str, Any]:
|
17
|
+
"""
|
18
|
+
DRF Spectacular postprocessing hook to mark async-capable operations.
|
19
|
+
|
20
|
+
Scans Django views and marks operations:
|
21
|
+
- async def → operation['x-async-capable'] = True
|
22
|
+
- def → operation['x-async-capable'] = False
|
23
|
+
|
24
|
+
Args:
|
25
|
+
result: OpenAPI schema dict
|
26
|
+
generator: Schema generator instance
|
27
|
+
request: HTTP request
|
28
|
+
public: Whether schema is public
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
Modified OpenAPI schema with async metadata
|
32
|
+
|
33
|
+
Example:
|
34
|
+
paths:
|
35
|
+
/api/products/:
|
36
|
+
get:
|
37
|
+
operationId: products_list
|
38
|
+
x-async-capable: true # Async view detected
|
39
|
+
"""
|
40
|
+
|
41
|
+
if 'paths' not in result:
|
42
|
+
return result
|
43
|
+
|
44
|
+
async_count = 0
|
45
|
+
sync_count = 0
|
46
|
+
|
47
|
+
for path, methods in result['paths'].items():
|
48
|
+
for method, operation in methods.items():
|
49
|
+
if method.upper() in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']:
|
50
|
+
# Try to resolve view function
|
51
|
+
view_func = _resolve_view_function(path, method, operation)
|
52
|
+
|
53
|
+
if view_func:
|
54
|
+
# Check if view is async
|
55
|
+
is_async = inspect.iscoroutinefunction(view_func)
|
56
|
+
operation['x-async-capable'] = is_async
|
57
|
+
|
58
|
+
if is_async:
|
59
|
+
async_count += 1
|
60
|
+
logger.debug(f"✓ Async view detected: {method.upper()} {path}")
|
61
|
+
else:
|
62
|
+
sync_count += 1
|
63
|
+
else:
|
64
|
+
# Default to sync if cannot resolve
|
65
|
+
operation['x-async-capable'] = False
|
66
|
+
sync_count += 1
|
67
|
+
|
68
|
+
logger.info(f"🔍 Async detection: {async_count} async, {sync_count} sync operations")
|
69
|
+
|
70
|
+
return result
|
71
|
+
|
72
|
+
|
73
|
+
def _resolve_view_function(path: str, method: str, operation: Dict[str, Any]) -> Optional[callable]:
|
74
|
+
"""
|
75
|
+
Resolve view function from OpenAPI operation.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
path: API path (e.g., /api/products/)
|
79
|
+
method: HTTP method (e.g., GET)
|
80
|
+
operation: OpenAPI operation dict
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
View function or None if cannot resolve
|
84
|
+
"""
|
85
|
+
# Try to get view from operationId
|
86
|
+
operation_id = operation.get('operationId')
|
87
|
+
|
88
|
+
if not operation_id:
|
89
|
+
return None
|
90
|
+
|
91
|
+
# Convert path to Django URL format
|
92
|
+
# /api/products/{id}/ → /api/products/1/
|
93
|
+
django_path = _convert_openapi_path_to_django(path)
|
94
|
+
|
95
|
+
try:
|
96
|
+
# Resolve URL to view
|
97
|
+
resolved = resolve(django_path)
|
98
|
+
view_func = resolved.func
|
99
|
+
|
100
|
+
# Handle ViewSets and class-based views
|
101
|
+
if hasattr(view_func, 'cls'):
|
102
|
+
# ViewSet - get specific action method
|
103
|
+
view_class = view_func.cls
|
104
|
+
|
105
|
+
# Extract action from operationId
|
106
|
+
# products_list → list, products_create → create
|
107
|
+
action = _extract_action_from_operation_id(operation_id)
|
108
|
+
|
109
|
+
if hasattr(view_class, action):
|
110
|
+
return getattr(view_class, action)
|
111
|
+
|
112
|
+
# Fallback to view class itself
|
113
|
+
return view_class
|
114
|
+
|
115
|
+
elif hasattr(view_func, 'view_class'):
|
116
|
+
# Class-based view
|
117
|
+
view_class = view_func.view_class
|
118
|
+
|
119
|
+
# Get method handler (get, post, put, etc.)
|
120
|
+
method_lower = method.lower()
|
121
|
+
if hasattr(view_class, method_lower):
|
122
|
+
return getattr(view_class, method_lower)
|
123
|
+
|
124
|
+
return view_class
|
125
|
+
|
126
|
+
else:
|
127
|
+
# Function-based view
|
128
|
+
return view_func
|
129
|
+
|
130
|
+
except Resolver404:
|
131
|
+
logger.debug(f"Cannot resolve path: {django_path}")
|
132
|
+
return None
|
133
|
+
except Exception as e:
|
134
|
+
logger.debug(f"Error resolving view for {path}: {e}")
|
135
|
+
return None
|
136
|
+
|
137
|
+
|
138
|
+
def _convert_openapi_path_to_django(openapi_path: str) -> str:
|
139
|
+
"""
|
140
|
+
Convert OpenAPI path to Django URL format.
|
141
|
+
|
142
|
+
Examples:
|
143
|
+
/api/products/{id}/ → /api/products/1/
|
144
|
+
/api/posts/{post_slug}/comments/ → /api/posts/test-slug/comments/
|
145
|
+
"""
|
146
|
+
import re
|
147
|
+
|
148
|
+
# Replace path parameters with sample values
|
149
|
+
# {id} → 1, {slug} → test-slug, {pk} → 1, {uuid} → sample UUID
|
150
|
+
def replace_param(match):
|
151
|
+
param_name = match.group(1)
|
152
|
+
|
153
|
+
# Check uuid first (before id, since uuid contains 'id')
|
154
|
+
if 'uuid' in param_name.lower():
|
155
|
+
return '00000000-0000-0000-0000-000000000001'
|
156
|
+
elif 'id' in param_name.lower() or 'pk' in param_name.lower():
|
157
|
+
return '1'
|
158
|
+
elif 'slug' in param_name.lower():
|
159
|
+
return 'test-slug'
|
160
|
+
else:
|
161
|
+
return 'test-value'
|
162
|
+
|
163
|
+
django_path = re.sub(r'\{([^}]+)\}', replace_param, openapi_path)
|
164
|
+
return django_path
|
165
|
+
|
166
|
+
|
167
|
+
def _extract_action_from_operation_id(operation_id: str) -> str:
|
168
|
+
"""
|
169
|
+
Extract ViewSet action from operationId.
|
170
|
+
|
171
|
+
Examples:
|
172
|
+
products_list → list
|
173
|
+
products_create → create
|
174
|
+
products_retrieve → retrieve
|
175
|
+
products_partial_update → partial_update
|
176
|
+
"""
|
177
|
+
# Split by underscore and get last part
|
178
|
+
parts = operation_id.split('_')
|
179
|
+
|
180
|
+
if len(parts) >= 2:
|
181
|
+
# Get everything after first underscore
|
182
|
+
# products_list → list
|
183
|
+
# products_partial_update → partial_update
|
184
|
+
action = '_'.join(parts[1:])
|
185
|
+
return action
|
186
|
+
|
187
|
+
return operation_id
|