django-cfg 1.4.9__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.
Files changed (193) hide show
  1. django_cfg/apps/agents/management/commands/create_agent.py +1 -1
  2. django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
  3. django_cfg/apps/newsletter/serializers.py +40 -3
  4. django_cfg/apps/newsletter/views/campaigns.py +12 -3
  5. django_cfg/apps/newsletter/views/emails.py +14 -3
  6. django_cfg/apps/newsletter/views/subscriptions.py +12 -2
  7. django_cfg/apps/payments/middleware/api_access.py +6 -2
  8. django_cfg/apps/payments/middleware/rate_limiting.py +2 -1
  9. django_cfg/apps/payments/middleware/usage_tracking.py +5 -1
  10. django_cfg/apps/payments/models/managers/api_key_managers.py +0 -1
  11. django_cfg/apps/payments/models/managers/subscription_managers.py +0 -1
  12. django_cfg/apps/payments/services/core/balance_service.py +5 -5
  13. django_cfg/apps/payments/services/core/subscription_service.py +1 -2
  14. django_cfg/apps/payments/views/api/balances.py +8 -7
  15. django_cfg/apps/payments/views/api/base.py +10 -6
  16. django_cfg/apps/payments/views/api/currencies.py +53 -10
  17. django_cfg/apps/payments/views/api/payments.py +3 -1
  18. django_cfg/apps/payments/views/api/subscriptions.py +2 -5
  19. django_cfg/apps/payments/views/api/webhooks.py +72 -7
  20. django_cfg/apps/payments/views/overview/serializers.py +34 -1
  21. django_cfg/apps/payments/views/overview/views.py +2 -1
  22. django_cfg/apps/payments/views/serializers/payments.py +6 -6
  23. django_cfg/apps/urls.py +106 -45
  24. django_cfg/core/base/config_model.py +2 -2
  25. django_cfg/core/constants.py +1 -1
  26. django_cfg/core/generation/integration_generators/__init__.py +1 -1
  27. django_cfg/core/generation/integration_generators/api.py +82 -41
  28. django_cfg/core/integration/display/startup.py +30 -22
  29. django_cfg/core/integration/url_integration.py +15 -16
  30. django_cfg/dashboard/sections/documentation.py +391 -0
  31. django_cfg/management/commands/check_endpoints.py +11 -160
  32. django_cfg/management/commands/check_settings.py +13 -265
  33. django_cfg/management/commands/clear_constance.py +13 -201
  34. django_cfg/management/commands/create_token.py +13 -321
  35. django_cfg/management/commands/generate_clients.py +23 -0
  36. django_cfg/management/commands/list_urls.py +13 -306
  37. django_cfg/management/commands/migrate_all.py +13 -126
  38. django_cfg/management/commands/migrator.py +13 -396
  39. django_cfg/management/commands/rundramatiq.py +15 -247
  40. django_cfg/management/commands/rundramatiq_simulator.py +12 -429
  41. django_cfg/management/commands/runserver_ngrok.py +15 -160
  42. django_cfg/management/commands/script.py +12 -488
  43. django_cfg/management/commands/show_config.py +12 -215
  44. django_cfg/management/commands/show_urls.py +12 -342
  45. django_cfg/management/commands/superuser.py +15 -295
  46. django_cfg/management/commands/task_clear.py +14 -217
  47. django_cfg/management/commands/task_status.py +13 -248
  48. django_cfg/management/commands/test_email.py +15 -86
  49. django_cfg/management/commands/test_telegram.py +14 -61
  50. django_cfg/management/commands/test_twilio.py +15 -105
  51. django_cfg/management/commands/tree.py +13 -383
  52. django_cfg/management/commands/validate_openapi.py +10 -0
  53. django_cfg/middleware/README.md +1 -1
  54. django_cfg/middleware/user_activity.py +3 -3
  55. django_cfg/models/__init__.py +2 -2
  56. django_cfg/models/api/drf/spectacular.py +6 -6
  57. django_cfg/models/django/__init__.py +2 -2
  58. django_cfg/models/django/openapi.py +238 -0
  59. django_cfg/models/django/{revolution.py → revolution_legacy.py} +8 -0
  60. django_cfg/modules/django_admin/management/__init__.py +0 -0
  61. django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
  62. django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
  63. django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
  64. django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
  65. django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
  66. django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
  67. django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
  68. django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
  69. django_cfg/modules/django_admin/management/commands/script.py +496 -0
  70. django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
  71. django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
  72. django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
  73. django_cfg/modules/django_admin/management/commands/tree.py +390 -0
  74. django_cfg/modules/django_client/__init__.py +20 -0
  75. django_cfg/modules/django_client/apps.py +35 -0
  76. django_cfg/modules/django_client/core/__init__.py +56 -0
  77. django_cfg/modules/django_client/core/archive/__init__.py +11 -0
  78. django_cfg/modules/django_client/core/archive/manager.py +134 -0
  79. django_cfg/modules/django_client/core/cli/__init__.py +12 -0
  80. django_cfg/modules/django_client/core/cli/main.py +235 -0
  81. django_cfg/modules/django_client/core/config/__init__.py +18 -0
  82. django_cfg/modules/django_client/core/config/config.py +188 -0
  83. django_cfg/modules/django_client/core/config/group.py +101 -0
  84. django_cfg/modules/django_client/core/config/service.py +209 -0
  85. django_cfg/modules/django_client/core/generator/__init__.py +115 -0
  86. django_cfg/modules/django_client/core/generator/base.py +767 -0
  87. django_cfg/modules/django_client/core/generator/python.py +751 -0
  88. django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
  89. django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
  90. django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
  91. django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
  92. django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
  93. django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
  94. django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
  95. django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
  96. django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
  97. django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
  98. django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
  99. django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
  100. django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
  101. django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
  102. django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
  103. django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
  104. django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
  105. django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
  106. django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
  107. django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
  108. django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
  109. django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
  110. django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
  111. django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
  112. django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
  113. django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
  114. django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
  115. django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
  116. django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
  117. django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
  118. django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
  119. django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
  120. django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
  121. django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
  122. django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
  123. django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
  124. django_cfg/modules/django_client/core/generator/typescript.py +872 -0
  125. django_cfg/modules/django_client/core/groups/__init__.py +13 -0
  126. django_cfg/modules/django_client/core/groups/detector.py +178 -0
  127. django_cfg/modules/django_client/core/groups/manager.py +314 -0
  128. django_cfg/modules/django_client/core/ir/__init__.py +57 -0
  129. django_cfg/modules/django_client/core/ir/context.py +387 -0
  130. django_cfg/modules/django_client/core/ir/operation.py +518 -0
  131. django_cfg/modules/django_client/core/ir/schema.py +353 -0
  132. django_cfg/modules/django_client/core/parser/__init__.py +74 -0
  133. django_cfg/modules/django_client/core/parser/base.py +648 -0
  134. django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
  135. django_cfg/modules/django_client/core/parser/models/base.py +212 -0
  136. django_cfg/modules/django_client/core/parser/models/components.py +160 -0
  137. django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
  138. django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
  139. django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
  140. django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
  141. django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
  142. django_cfg/modules/django_client/core/validation/__init__.py +22 -0
  143. django_cfg/modules/django_client/core/validation/checker.py +134 -0
  144. django_cfg/modules/django_client/core/validation/fixer.py +216 -0
  145. django_cfg/modules/django_client/core/validation/reporter.py +480 -0
  146. django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
  147. django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
  148. django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
  149. django_cfg/modules/django_client/core/validation/safety.py +266 -0
  150. django_cfg/modules/django_client/management/__init__.py +3 -0
  151. django_cfg/modules/django_client/management/commands/__init__.py +3 -0
  152. django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
  153. django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
  154. django_cfg/modules/django_client/spectacular/__init__.py +9 -0
  155. django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
  156. django_cfg/modules/django_client/urls.py +72 -0
  157. django_cfg/modules/django_email/management/__init__.py +0 -0
  158. django_cfg/modules/django_email/management/commands/__init__.py +0 -0
  159. django_cfg/modules/django_email/management/commands/test_email.py +93 -0
  160. django_cfg/modules/django_logging/django_logger.py +6 -6
  161. django_cfg/modules/django_ngrok/management/__init__.py +0 -0
  162. django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
  163. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
  164. django_cfg/modules/django_tasks/management/__init__.py +0 -0
  165. django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
  166. django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
  167. django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
  168. django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
  169. django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
  170. django_cfg/modules/django_telegram/management/__init__.py +0 -0
  171. django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
  172. django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
  173. django_cfg/modules/django_twilio/management/__init__.py +0 -0
  174. django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
  175. django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
  176. django_cfg/modules/django_unfold/callbacks/main.py +16 -5
  177. django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
  178. django_cfg/modules/django_unfold/dashboard.py +1 -1
  179. django_cfg/pyproject.toml +2 -6
  180. django_cfg/registry/third_party.py +5 -7
  181. django_cfg/routing/callbacks.py +1 -1
  182. django_cfg/static/admin/css/prose-unfold.css +666 -0
  183. django_cfg/templates/admin/index.html +8 -0
  184. django_cfg/templates/admin/index_new.html +13 -0
  185. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
  186. django_cfg/templates/admin/sections/documentation_section.html +172 -0
  187. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
  188. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
  189. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/RECORD +192 -71
  190. django_cfg/management/commands/generate.py +0 -107
  191. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
  192. {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
  193. {django_cfg-1.4.9.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,9 @@
1
+ """
2
+ DRF Spectacular postprocessing hooks for django-cfg.
3
+
4
+ Auto-fixes for OpenAPI schema generation.
5
+ """
6
+
7
+ from .enum_naming import auto_fix_enum_names
8
+
9
+ __all__ = ['auto_fix_enum_names']
@@ -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