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.
Files changed (225) 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/views/api/currencies.py +49 -6
  8. django_cfg/apps/payments/views/api/webhooks.py +72 -7
  9. django_cfg/apps/payments/views/overview/serializers.py +34 -1
  10. django_cfg/apps/payments/views/overview/views.py +2 -1
  11. django_cfg/apps/payments/views/serializers/payments.py +6 -6
  12. django_cfg/apps/urls.py +106 -45
  13. django_cfg/core/base/config_model.py +2 -2
  14. django_cfg/core/constants.py +1 -1
  15. django_cfg/core/generation/integration_generators/__init__.py +1 -1
  16. django_cfg/core/generation/integration_generators/api.py +73 -49
  17. django_cfg/core/integration/display/startup.py +30 -22
  18. django_cfg/core/integration/url_integration.py +15 -16
  19. django_cfg/management/commands/check_endpoints.py +11 -160
  20. django_cfg/management/commands/check_settings.py +13 -348
  21. django_cfg/management/commands/clear_constance.py +13 -201
  22. django_cfg/management/commands/create_token.py +13 -321
  23. django_cfg/management/commands/generate_clients.py +23 -0
  24. django_cfg/management/commands/list_urls.py +13 -306
  25. django_cfg/management/commands/migrate_all.py +13 -126
  26. django_cfg/management/commands/migrator.py +13 -396
  27. django_cfg/management/commands/rundramatiq.py +15 -247
  28. django_cfg/management/commands/rundramatiq_simulator.py +12 -429
  29. django_cfg/management/commands/runserver_ngrok.py +15 -160
  30. django_cfg/management/commands/script.py +12 -488
  31. django_cfg/management/commands/show_config.py +12 -215
  32. django_cfg/management/commands/show_urls.py +12 -342
  33. django_cfg/management/commands/superuser.py +15 -295
  34. django_cfg/management/commands/task_clear.py +14 -217
  35. django_cfg/management/commands/task_status.py +13 -248
  36. django_cfg/management/commands/test_email.py +15 -86
  37. django_cfg/management/commands/test_telegram.py +14 -61
  38. django_cfg/management/commands/test_twilio.py +15 -105
  39. django_cfg/management/commands/tree.py +13 -383
  40. django_cfg/management/commands/validate_openapi.py +10 -0
  41. django_cfg/middleware/README.md +1 -1
  42. django_cfg/middleware/user_activity.py +3 -3
  43. django_cfg/models/__init__.py +2 -2
  44. django_cfg/models/api/drf/spectacular.py +6 -6
  45. django_cfg/models/django/__init__.py +2 -2
  46. django_cfg/models/django/openapi.py +162 -0
  47. django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
  48. django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
  49. django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
  50. django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
  51. django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
  52. django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
  53. django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
  54. django_cfg/modules/django_admin/management/commands/script.py +496 -0
  55. django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
  56. django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
  57. django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
  58. django_cfg/modules/django_admin/management/commands/tree.py +390 -0
  59. django_cfg/modules/django_client/__init__.py +20 -0
  60. django_cfg/modules/django_client/apps.py +35 -0
  61. django_cfg/modules/django_client/core/__init__.py +56 -0
  62. django_cfg/modules/django_client/core/archive/__init__.py +11 -0
  63. django_cfg/modules/django_client/core/archive/manager.py +134 -0
  64. django_cfg/modules/django_client/core/cli/__init__.py +12 -0
  65. django_cfg/modules/django_client/core/cli/main.py +235 -0
  66. django_cfg/modules/django_client/core/config/__init__.py +18 -0
  67. django_cfg/modules/django_client/core/config/config.py +208 -0
  68. django_cfg/modules/django_client/core/config/group.py +101 -0
  69. django_cfg/modules/django_client/core/config/service.py +209 -0
  70. django_cfg/modules/django_client/core/generator/__init__.py +115 -0
  71. django_cfg/modules/django_client/core/generator/base.py +838 -0
  72. django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
  73. django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
  74. django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
  75. django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
  76. django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
  77. django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
  78. django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
  79. django_cfg/modules/django_client/core/generator/python/templates/__init__.py.jinja +9 -0
  80. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +153 -0
  81. django_cfg/modules/django_client/core/generator/python/templates/app_init.py.jinja +6 -0
  82. django_cfg/modules/django_client/core/generator/python/templates/client/app_client.py.jinja +18 -0
  83. django_cfg/modules/django_client/core/generator/python/templates/client/flat_client.py.jinja +38 -0
  84. django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +68 -0
  85. django_cfg/modules/django_client/core/generator/python/templates/client/main_client_file.py.jinja +14 -0
  86. django_cfg/modules/django_client/core/generator/python/templates/client/operation_method.py.jinja +9 -0
  87. django_cfg/modules/django_client/core/generator/python/templates/client/sub_client.py.jinja +18 -0
  88. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
  89. django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
  90. django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
  91. django_cfg/modules/django_client/core/generator/python/templates/client_file.py.jinja +13 -0
  92. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +52 -0
  93. django_cfg/modules/django_client/core/generator/python/templates/models/app_models.py.jinja +17 -0
  94. django_cfg/modules/django_client/core/generator/python/templates/models/enum_class.py.jinja +17 -0
  95. django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +8 -0
  96. django_cfg/modules/django_client/core/generator/python/templates/models/models.py.jinja +17 -0
  97. django_cfg/modules/django_client/core/generator/python/templates/models/schema_class.py.jinja +21 -0
  98. django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
  99. django_cfg/modules/django_client/core/generator/python/templates/utils/logger.py.jinja +255 -0
  100. django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
  101. django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py.jinja +12 -0
  102. django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
  103. django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
  104. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
  105. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
  106. django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
  107. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
  108. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
  109. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
  110. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
  111. django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
  112. django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja +2 -0
  113. django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja +18 -0
  114. django_cfg/modules/django_client/core/generator/typescript/templates/client/client.ts.jinja +403 -0
  115. django_cfg/modules/django_client/core/generator/typescript/templates/client/flat_client.ts.jinja +109 -0
  116. django_cfg/modules/django_client/core/generator/typescript/templates/client/main_client_file.ts.jinja +10 -0
  117. django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja +61 -0
  118. django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja +15 -0
  119. django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja +9 -0
  120. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
  121. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
  122. django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja +5 -0
  123. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +268 -0
  124. django_cfg/modules/django_client/core/generator/typescript/templates/models/app_models.ts.jinja +8 -0
  125. django_cfg/modules/django_client/core/generator/typescript/templates/models/enums.ts.jinja +4 -0
  126. django_cfg/modules/django_client/core/generator/typescript/templates/models/models.ts.jinja +8 -0
  127. django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
  128. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
  129. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
  130. django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
  131. django_cfg/modules/django_client/core/generator/typescript/templates/utils/errors.ts.jinja +116 -0
  132. django_cfg/modules/django_client/core/generator/typescript/templates/utils/http.ts.jinja +98 -0
  133. django_cfg/modules/django_client/core/generator/typescript/templates/utils/logger.ts.jinja +259 -0
  134. django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
  135. django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja +7 -0
  136. django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja +158 -0
  137. django_cfg/modules/django_client/core/groups/__init__.py +13 -0
  138. django_cfg/modules/django_client/core/groups/detector.py +178 -0
  139. django_cfg/modules/django_client/core/groups/manager.py +314 -0
  140. django_cfg/modules/django_client/core/ir/__init__.py +57 -0
  141. django_cfg/modules/django_client/core/ir/context.py +387 -0
  142. django_cfg/modules/django_client/core/ir/operation.py +518 -0
  143. django_cfg/modules/django_client/core/ir/schema.py +353 -0
  144. django_cfg/modules/django_client/core/parser/__init__.py +74 -0
  145. django_cfg/modules/django_client/core/parser/base.py +648 -0
  146. django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
  147. django_cfg/modules/django_client/core/parser/models/base.py +212 -0
  148. django_cfg/modules/django_client/core/parser/models/components.py +160 -0
  149. django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
  150. django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
  151. django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
  152. django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
  153. django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
  154. django_cfg/modules/django_client/core/validation/__init__.py +22 -0
  155. django_cfg/modules/django_client/core/validation/checker.py +134 -0
  156. django_cfg/modules/django_client/core/validation/fixer.py +216 -0
  157. django_cfg/modules/django_client/core/validation/reporter.py +480 -0
  158. django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
  159. django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
  160. django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
  161. django_cfg/modules/django_client/core/validation/safety.py +266 -0
  162. django_cfg/modules/django_client/management/__init__.py +3 -0
  163. django_cfg/modules/django_client/management/commands/__init__.py +3 -0
  164. django_cfg/modules/django_client/management/commands/generate_client.py +427 -0
  165. django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
  166. django_cfg/modules/django_client/pytest.ini +30 -0
  167. django_cfg/modules/django_client/spectacular/__init__.py +10 -0
  168. django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
  169. django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
  170. django_cfg/modules/django_client/urls.py +72 -0
  171. django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
  172. django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
  173. django_cfg/modules/django_dashboard/management/__init__.py +0 -0
  174. django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
  175. django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
  176. django_cfg/modules/django_dashboard/sections/documentation.py +391 -0
  177. django_cfg/modules/django_email/management/__init__.py +0 -0
  178. django_cfg/modules/django_email/management/commands/__init__.py +0 -0
  179. django_cfg/modules/django_email/management/commands/test_email.py +93 -0
  180. django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
  181. django_cfg/modules/django_logging/django_logger.py +6 -6
  182. django_cfg/modules/django_ngrok/management/__init__.py +0 -0
  183. django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
  184. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
  185. django_cfg/modules/django_tasks/management/__init__.py +0 -0
  186. django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
  187. django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
  188. django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
  189. django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
  190. django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
  191. django_cfg/modules/django_telegram/management/__init__.py +0 -0
  192. django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
  193. django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
  194. django_cfg/modules/django_twilio/management/__init__.py +0 -0
  195. django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
  196. django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
  197. django_cfg/modules/django_unfold/callbacks/main.py +21 -10
  198. django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
  199. django_cfg/pyproject.toml +2 -6
  200. django_cfg/registry/third_party.py +5 -7
  201. django_cfg/routing/callbacks.py +1 -1
  202. django_cfg/static/admin/css/prose-unfold.css +666 -0
  203. django_cfg/templates/admin/index.html +8 -0
  204. django_cfg/templates/admin/index_new.html +13 -0
  205. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
  206. django_cfg/templates/admin/sections/documentation_section.html +172 -0
  207. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
  208. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/METADATA +2 -2
  209. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/RECORD +224 -74
  210. django_cfg/management/commands/generate.py +0 -107
  211. /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
  212. /django_cfg/{dashboard → modules/django_admin}/management/__init__.py +0 -0
  213. /django_cfg/{dashboard → modules/django_admin}/management/commands/__init__.py +0 -0
  214. /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
  215. /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
  216. /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
  217. /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
  218. /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
  219. /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
  220. /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
  221. /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
  222. /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
  223. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
  224. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
  225. {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