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,480 @@
1
+ """Issue reporter for formatting validation results."""
2
+
3
+ import json
4
+ from collections import defaultdict
5
+ from pathlib import Path
6
+ from typing import Dict, List, Any
7
+
8
+ from .rules import Issue, Severity
9
+
10
+
11
+ class IssueReporter:
12
+ """
13
+ Formats and displays validation issues in various formats.
14
+
15
+ Example:
16
+ >>> reporter = IssueReporter()
17
+ >>> reporter.display_console(issues)
18
+ >>> reporter.save_json(issues, Path('report.json'))
19
+ >>> reporter.save_html(issues, Path('report.html'))
20
+ """
21
+
22
+ # ANSI color codes for terminal
23
+ COLORS = {
24
+ 'reset': '\033[0m',
25
+ 'red': '\033[91m',
26
+ 'yellow': '\033[93m',
27
+ 'blue': '\033[94m',
28
+ 'green': '\033[92m',
29
+ 'gray': '\033[90m',
30
+ 'bold': '\033[1m',
31
+ }
32
+
33
+ # Severity symbols and colors
34
+ SEVERITY_CONFIG = {
35
+ Severity.ERROR: {'symbol': '❌', 'color': 'red', 'label': 'ERROR'},
36
+ Severity.WARNING: {'symbol': '⚠️ ', 'color': 'yellow', 'label': 'WARNING'},
37
+ Severity.INFO: {'symbol': 'ℹ️ ', 'color': 'blue', 'label': 'INFO'},
38
+ }
39
+
40
+ def __init__(self, use_colors: bool = True):
41
+ """
42
+ Initialize reporter.
43
+
44
+ Args:
45
+ use_colors: If True, use ANSI colors in console output
46
+ """
47
+ self.use_colors = use_colors
48
+
49
+ def display_console(
50
+ self,
51
+ issues: List[Issue],
52
+ show_suggestions: bool = True,
53
+ group_by_file: bool = True,
54
+ verbose: bool = False
55
+ ) -> None:
56
+ """
57
+ Display issues in console with colors and formatting.
58
+
59
+ Args:
60
+ issues: List of issues to display
61
+ show_suggestions: If True, show fix suggestions
62
+ group_by_file: If True, group issues by file
63
+ verbose: If True, show additional context
64
+ """
65
+ if not issues:
66
+ self._print("✅ No issues found!", 'green', bold=True)
67
+ return
68
+
69
+ # Statistics
70
+ stats = self._get_statistics(issues)
71
+ self._print_header(stats)
72
+
73
+ if group_by_file:
74
+ self._display_by_file(issues, show_suggestions, verbose)
75
+ else:
76
+ self._display_flat(issues, show_suggestions, verbose)
77
+
78
+ self._print_footer(stats)
79
+
80
+ def display_summary(self, issues: List[Issue]) -> None:
81
+ """
82
+ Display compact summary of issues.
83
+
84
+ Args:
85
+ issues: List of issues to summarize
86
+ """
87
+ if not issues:
88
+ self._print("✅ No issues found!", 'green', bold=True)
89
+ return
90
+
91
+ stats = self._get_statistics(issues)
92
+
93
+ print(f"\n📊 Validation Summary")
94
+ print(f" Total: {stats['total']} issue(s)")
95
+ print(f" Errors: {stats['by_severity']['error']} | "
96
+ f"Warnings: {stats['by_severity']['warning']} | "
97
+ f"Info: {stats['by_severity']['info']}")
98
+ print(f" Auto-fixable: {stats['fixable']} ({stats['fixable_percent']:.1f}%)")
99
+ print(f" Files affected: {stats['file_count']}")
100
+ print()
101
+
102
+ def save_json(
103
+ self,
104
+ issues: List[Issue],
105
+ output_path: Path,
106
+ include_stats: bool = True
107
+ ) -> None:
108
+ """
109
+ Save issues as JSON report.
110
+
111
+ Args:
112
+ output_path: Path to save JSON file
113
+ issues: List of issues
114
+ include_stats: If True, include statistics
115
+ """
116
+ data = {
117
+ 'issues': [self._issue_to_dict(issue) for issue in issues]
118
+ }
119
+
120
+ if include_stats:
121
+ data['statistics'] = self._get_statistics(issues)
122
+
123
+ output_path.parent.mkdir(parents=True, exist_ok=True)
124
+ output_path.write_text(json.dumps(data, indent=2), encoding='utf-8')
125
+ print(f"📄 JSON report saved to: {output_path}")
126
+
127
+ def save_html(
128
+ self,
129
+ issues: List[Issue],
130
+ output_path: Path,
131
+ title: str = "Validation Report"
132
+ ) -> None:
133
+ """
134
+ Save issues as HTML report.
135
+
136
+ Args:
137
+ output_path: Path to save HTML file
138
+ issues: List of issues
139
+ title: Report title
140
+ """
141
+ stats = self._get_statistics(issues)
142
+ by_file = self._group_by_file(issues)
143
+
144
+ html = self._generate_html(title, stats, by_file, issues)
145
+
146
+ output_path.parent.mkdir(parents=True, exist_ok=True)
147
+ output_path.write_text(html, encoding='utf-8')
148
+ print(f"📄 HTML report saved to: {output_path}")
149
+
150
+ # Private methods
151
+
152
+ def _get_statistics(self, issues: List[Issue]) -> Dict[str, Any]:
153
+ """Calculate statistics about issues."""
154
+ by_severity = defaultdict(int)
155
+ by_rule = defaultdict(int)
156
+ files = set()
157
+ fixable = 0
158
+
159
+ for issue in issues:
160
+ by_severity[issue.severity.value] += 1
161
+ by_rule[issue.rule_id] += 1
162
+ files.add(str(issue.file))
163
+ if issue.auto_fixable:
164
+ fixable += 1
165
+
166
+ total = len(issues)
167
+ fixable_percent = (fixable / total * 100) if total > 0 else 0
168
+
169
+ return {
170
+ 'total': total,
171
+ 'fixable': fixable,
172
+ 'fixable_percent': fixable_percent,
173
+ 'file_count': len(files),
174
+ 'by_severity': {
175
+ 'error': by_severity.get('error', 0),
176
+ 'warning': by_severity.get('warning', 0),
177
+ 'info': by_severity.get('info', 0),
178
+ },
179
+ 'by_rule': dict(by_rule),
180
+ }
181
+
182
+ def _group_by_file(self, issues: List[Issue]) -> Dict[Path, List[Issue]]:
183
+ """Group issues by file path."""
184
+ by_file = defaultdict(list)
185
+ for issue in issues:
186
+ by_file[issue.file].append(issue)
187
+ return dict(by_file)
188
+
189
+ def _print_header(self, stats: Dict[str, Any]) -> None:
190
+ """Print report header."""
191
+ total = stats['total']
192
+ errors = stats['by_severity']['error']
193
+ warnings = stats['by_severity']['warning']
194
+ infos = stats['by_severity']['info']
195
+
196
+ self._print("\n" + "=" * 80, 'gray')
197
+ self._print("🔍 Validation Report", 'bold')
198
+ self._print("=" * 80, 'gray')
199
+
200
+ msg = f"Found {total} issue(s): "
201
+ if errors > 0:
202
+ msg += f"{errors} error(s), "
203
+ if warnings > 0:
204
+ msg += f"{warnings} warning(s), "
205
+ if infos > 0:
206
+ msg += f"{infos} info"
207
+
208
+ color = 'red' if errors > 0 else 'yellow' if warnings > 0 else 'blue'
209
+ self._print(msg, color)
210
+
211
+ if stats['fixable'] > 0:
212
+ self._print(
213
+ f"✨ {stats['fixable']} issue(s) can be auto-fixed "
214
+ f"({stats['fixable_percent']:.1f}%)",
215
+ 'green'
216
+ )
217
+ print()
218
+
219
+ def _print_footer(self, stats: Dict[str, Any]) -> None:
220
+ """Print report footer."""
221
+ self._print("=" * 80, 'gray')
222
+ print(f"Total: {stats['total']} issue(s) in {stats['file_count']} file(s)")
223
+ self._print("=" * 80 + "\n", 'gray')
224
+
225
+ def _display_by_file(
226
+ self,
227
+ issues: List[Issue],
228
+ show_suggestions: bool,
229
+ verbose: bool
230
+ ) -> None:
231
+ """Display issues grouped by file."""
232
+ by_file = self._group_by_file(issues)
233
+
234
+ for file_path, file_issues in sorted(by_file.items()):
235
+ # File header
236
+ self._print(f"\n📝 {file_path.name}", 'bold')
237
+ self._print(f" {file_path}", 'gray')
238
+
239
+ # Issues for this file
240
+ for issue in sorted(file_issues, key=lambda i: i.line):
241
+ self._display_issue(issue, show_suggestions, verbose, indent=3)
242
+
243
+ def _display_flat(
244
+ self,
245
+ issues: List[Issue],
246
+ show_suggestions: bool,
247
+ verbose: bool
248
+ ) -> None:
249
+ """Display issues in flat list."""
250
+ for issue in sorted(issues, key=lambda i: (str(i.file), i.line)):
251
+ self._display_issue(issue, show_suggestions, verbose, indent=0)
252
+
253
+ def _display_issue(
254
+ self,
255
+ issue: Issue,
256
+ show_suggestions: bool,
257
+ verbose: bool,
258
+ indent: int = 0
259
+ ) -> None:
260
+ """Display single issue."""
261
+ prefix = " " * indent
262
+
263
+ # Severity symbol and location
264
+ config = self.SEVERITY_CONFIG[issue.severity]
265
+ symbol = config['symbol']
266
+ color = config['color']
267
+
268
+ location = f"{issue.file.name}:{issue.line}:{issue.column}"
269
+ self._print(f"{prefix}{symbol} {location}", color)
270
+
271
+ # Message
272
+ print(f"{prefix} {issue.message}")
273
+
274
+ # Rule ID
275
+ if verbose:
276
+ self._print(f"{prefix} [{issue.rule_id}]", 'gray')
277
+
278
+ # Suggestion
279
+ if show_suggestions and issue.suggestion:
280
+ auto_fix = " (auto-fixable)" if issue.auto_fixable else ""
281
+ self._print(f"{prefix} 💡 {issue.suggestion}{auto_fix}", 'blue')
282
+
283
+ # Context
284
+ if verbose and issue.context:
285
+ print(f"{prefix} Context: {issue.context}")
286
+
287
+ print()
288
+
289
+ def _issue_to_dict(self, issue: Issue) -> Dict[str, Any]:
290
+ """Convert issue to dictionary."""
291
+ return {
292
+ 'rule_id': issue.rule_id,
293
+ 'severity': issue.severity.value,
294
+ 'file': str(issue.file),
295
+ 'line': issue.line,
296
+ 'column': issue.column,
297
+ 'message': issue.message,
298
+ 'suggestion': issue.suggestion,
299
+ 'auto_fixable': issue.auto_fixable,
300
+ 'context': issue.context,
301
+ }
302
+
303
+ def _generate_html(
304
+ self,
305
+ title: str,
306
+ stats: Dict[str, Any],
307
+ by_file: Dict[Path, List[Issue]],
308
+ all_issues: List[Issue]
309
+ ) -> str:
310
+ """Generate HTML report."""
311
+ # Simple HTML template
312
+ html = f"""<!DOCTYPE html>
313
+ <html lang="en">
314
+ <head>
315
+ <meta charset="UTF-8">
316
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
317
+ <title>{title}</title>
318
+ <style>
319
+ body {{
320
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
321
+ max-width: 1200px;
322
+ margin: 40px auto;
323
+ padding: 20px;
324
+ background: #f5f5f5;
325
+ }}
326
+ .header {{
327
+ background: white;
328
+ padding: 30px;
329
+ border-radius: 8px;
330
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
331
+ margin-bottom: 20px;
332
+ }}
333
+ h1 {{ margin: 0 0 20px 0; color: #333; }}
334
+ .stats {{
335
+ display: grid;
336
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
337
+ gap: 15px;
338
+ margin-top: 20px;
339
+ }}
340
+ .stat {{
341
+ background: #f8f9fa;
342
+ padding: 15px;
343
+ border-radius: 5px;
344
+ text-align: center;
345
+ }}
346
+ .stat-value {{
347
+ font-size: 32px;
348
+ font-weight: bold;
349
+ margin-bottom: 5px;
350
+ }}
351
+ .stat-label {{
352
+ color: #666;
353
+ font-size: 14px;
354
+ }}
355
+ .file-section {{
356
+ background: white;
357
+ padding: 20px;
358
+ border-radius: 8px;
359
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
360
+ margin-bottom: 15px;
361
+ }}
362
+ .file-header {{
363
+ font-size: 18px;
364
+ font-weight: bold;
365
+ margin-bottom: 15px;
366
+ color: #333;
367
+ }}
368
+ .issue {{
369
+ padding: 15px;
370
+ border-left: 4px solid;
371
+ margin-bottom: 10px;
372
+ background: #f8f9fa;
373
+ }}
374
+ .issue.error {{ border-color: #dc3545; }}
375
+ .issue.warning {{ border-color: #ffc107; }}
376
+ .issue.info {{ border-color: #17a2b8; }}
377
+ .issue-location {{
378
+ font-family: monospace;
379
+ font-size: 13px;
380
+ color: #666;
381
+ margin-bottom: 5px;
382
+ }}
383
+ .issue-message {{
384
+ margin-bottom: 8px;
385
+ }}
386
+ .issue-suggestion {{
387
+ color: #0066cc;
388
+ font-size: 14px;
389
+ }}
390
+ .auto-fixable {{
391
+ display: inline-block;
392
+ background: #28a745;
393
+ color: white;
394
+ padding: 2px 8px;
395
+ border-radius: 3px;
396
+ font-size: 12px;
397
+ margin-left: 10px;
398
+ }}
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <div class="header">
403
+ <h1>🔍 {title}</h1>
404
+ <div class="stats">
405
+ <div class="stat">
406
+ <div class="stat-value">{stats['total']}</div>
407
+ <div class="stat-label">Total Issues</div>
408
+ </div>
409
+ <div class="stat">
410
+ <div class="stat-value">{stats['by_severity']['error']}</div>
411
+ <div class="stat-label">Errors</div>
412
+ </div>
413
+ <div class="stat">
414
+ <div class="stat-value">{stats['by_severity']['warning']}</div>
415
+ <div class="stat-label">Warnings</div>
416
+ </div>
417
+ <div class="stat">
418
+ <div class="stat-value">{stats['fixable']}</div>
419
+ <div class="stat-label">Auto-fixable</div>
420
+ </div>
421
+ <div class="stat">
422
+ <div class="stat-value">{stats['file_count']}</div>
423
+ <div class="stat-label">Files</div>
424
+ </div>
425
+ </div>
426
+ </div>
427
+ """
428
+
429
+ # Group by file
430
+ for file_path, file_issues in sorted(by_file.items()):
431
+ html += f"""
432
+ <div class="file-section">
433
+ <div class="file-header">📝 {file_path.name}</div>
434
+ <div style="color: #666; font-size: 14px; margin-bottom: 15px;">{file_path}</div>
435
+ """
436
+
437
+ for issue in sorted(file_issues, key=lambda i: i.line):
438
+ severity_class = issue.severity.value
439
+ auto_fix_badge = '<span class="auto-fixable">auto-fixable</span>' if issue.auto_fixable else ''
440
+
441
+ html += f"""
442
+ <div class="issue {severity_class}">
443
+ <div class="issue-location">Line {issue.line}:{issue.column} [{issue.rule_id}]</div>
444
+ <div class="issue-message">{issue.message} {auto_fix_badge}</div>
445
+ """
446
+ if issue.suggestion:
447
+ html += f"""
448
+ <div class="issue-suggestion">💡 {issue.suggestion}</div>
449
+ """
450
+ html += """
451
+ </div>
452
+ """
453
+
454
+ html += """
455
+ </div>
456
+ """
457
+
458
+ html += """
459
+ </body>
460
+ </html>
461
+ """
462
+ return html
463
+
464
+ def _print(self, text: str, color: str = '', bold: bool = False) -> None:
465
+ """Print with optional color."""
466
+ if not self.use_colors:
467
+ print(text)
468
+ return
469
+
470
+ codes = []
471
+ if bold:
472
+ codes.append(self.COLORS['bold'])
473
+ if color and color in self.COLORS:
474
+ codes.append(self.COLORS[color])
475
+
476
+ if codes:
477
+ reset = self.COLORS['reset']
478
+ print(f"{''.join(codes)}{text}{reset}")
479
+ else:
480
+ print(text)
@@ -0,0 +1,11 @@
1
+ """Validation rules for OpenAPI schema quality."""
2
+
3
+ from .base import Issue, Severity, ValidationRule
4
+ from .type_hints import TypeHintRule
5
+
6
+ __all__ = [
7
+ 'Issue',
8
+ 'Severity',
9
+ 'ValidationRule',
10
+ 'TypeHintRule',
11
+ ]
@@ -0,0 +1,96 @@
1
+ """Base classes for validation rules."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+
10
+ class Severity(Enum):
11
+ """Issue severity levels."""
12
+ ERROR = "error"
13
+ WARNING = "warning"
14
+ INFO = "info"
15
+
16
+
17
+ @dataclass
18
+ class Issue:
19
+ """Represents a validation issue."""
20
+ rule_id: str
21
+ severity: Severity
22
+ file: Path
23
+ line: int
24
+ column: int
25
+ message: str
26
+ suggestion: str
27
+ auto_fixable: bool
28
+ context: dict # Additional context for fixing
29
+
30
+ def __str__(self) -> str:
31
+ return f"{self.file}:{self.line}:{self.column} [{self.severity.value}] {self.message}"
32
+
33
+
34
+ class ValidationRule(ABC):
35
+ """Base class for all validation rules."""
36
+
37
+ @property
38
+ @abstractmethod
39
+ def rule_id(self) -> str:
40
+ """Unique rule identifier (e.g., 'type-hint-001')."""
41
+ pass
42
+
43
+ @property
44
+ @abstractmethod
45
+ def name(self) -> str:
46
+ """Human-readable rule name."""
47
+ pass
48
+
49
+ @property
50
+ @abstractmethod
51
+ def description(self) -> str:
52
+ """What this rule checks."""
53
+ pass
54
+
55
+ @abstractmethod
56
+ def check(self, file_path: Path) -> List[Issue]:
57
+ """
58
+ Check file for issues.
59
+
60
+ Args:
61
+ file_path: Path to Python file to check
62
+
63
+ Returns:
64
+ List of found issues
65
+ """
66
+ pass
67
+
68
+ @abstractmethod
69
+ def can_fix(self, issue: Issue) -> bool:
70
+ """
71
+ Check if this issue can be auto-fixed.
72
+
73
+ Args:
74
+ issue: Issue to check
75
+
76
+ Returns:
77
+ True if auto-fixable
78
+ """
79
+ pass
80
+
81
+ @abstractmethod
82
+ def fix(self, issue: Issue) -> bool:
83
+ """
84
+ Apply fix for this issue.
85
+
86
+ Args:
87
+ issue: Issue to fix
88
+
89
+ Returns:
90
+ True if fix was successful
91
+
92
+ Note:
93
+ This method modifies files directly!
94
+ Caller is responsible for backups/rollback.
95
+ """
96
+ pass