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,288 @@
1
+ """Type hint validation rule for SerializerMethodField."""
2
+
3
+ import ast
4
+ from pathlib import Path
5
+ from typing import List
6
+
7
+ from .base import Issue, Severity, ValidationRule
8
+
9
+
10
+ class TypeHintRule(ValidationRule):
11
+ """
12
+ Add type hints to SerializerMethodField methods.
13
+
14
+ Checks: get_* methods without return type annotations
15
+ Fixes: Adds inferred type hints based on method name patterns
16
+ """
17
+
18
+ @property
19
+ def rule_id(self) -> str:
20
+ return "type-hint-001"
21
+
22
+ @property
23
+ def name(self) -> str:
24
+ return "SerializerMethodField Type Hints"
25
+
26
+ @property
27
+ def description(self) -> str:
28
+ return "Ensures all get_* methods have return type hints"
29
+
30
+ def check(self, file_path: Path) -> List[Issue]:
31
+ """Find methods missing type hints."""
32
+ issues = []
33
+
34
+ try:
35
+ content = file_path.read_text(encoding='utf-8')
36
+ tree = ast.parse(content)
37
+ except (SyntaxError, UnicodeDecodeError):
38
+ # Skip files with syntax errors or encoding issues
39
+ return []
40
+
41
+ for node in ast.walk(tree):
42
+ if not isinstance(node, ast.FunctionDef):
43
+ continue
44
+
45
+ # Only process get_* methods
46
+ if not node.name.startswith('get_'):
47
+ continue
48
+
49
+ # Skip if already has type hint
50
+ if node.returns is not None:
51
+ continue
52
+
53
+ # Skip private methods
54
+ if node.name.startswith('_get_'):
55
+ continue
56
+
57
+ # Infer type hint
58
+ inferred_type = self._infer_type(node)
59
+
60
+ issues.append(Issue(
61
+ rule_id=self.rule_id,
62
+ severity=Severity.WARNING,
63
+ file=file_path,
64
+ line=node.lineno,
65
+ column=node.col_offset,
66
+ message=f"Method {node.name}() missing return type hint",
67
+ suggestion=f"Add: -> {inferred_type}",
68
+ auto_fixable=True,
69
+ context={
70
+ 'method_name': node.name,
71
+ 'inferred_type': inferred_type,
72
+ 'line_number': node.lineno,
73
+ }
74
+ ))
75
+
76
+ return issues
77
+
78
+ def can_fix(self, issue: Issue) -> bool:
79
+ """All type hint issues are auto-fixable."""
80
+ return issue.auto_fixable and issue.rule_id == self.rule_id
81
+
82
+ def fix(self, issue: Issue, skip_imports: bool = False) -> bool:
83
+ """
84
+ Add type hint to method.
85
+
86
+ Args:
87
+ issue: Issue to fix
88
+ skip_imports: If True, skip adding imports (for batch processing)
89
+ """
90
+ file_path = issue.file
91
+ inferred_type = issue.context['inferred_type']
92
+ line_number = issue.context['line_number']
93
+
94
+ try:
95
+ # Read file
96
+ content = file_path.read_text(encoding='utf-8')
97
+ lines = content.splitlines(keepends=True)
98
+
99
+ # Ensure line exists
100
+ if line_number > len(lines):
101
+ return False
102
+
103
+ # Find method definition line
104
+ target_line = lines[line_number - 1]
105
+
106
+ # Add type hint (preserve indentation and formatting)
107
+ if ')' in target_line and ':' in target_line:
108
+ # def method(self, obj): → def method(self, obj) -> Type:
109
+ modified_line = target_line.replace('):', f") -> {inferred_type}:", 1)
110
+ lines[line_number - 1] = modified_line
111
+
112
+ # Add imports if needed (unless skipping for batch mode)
113
+ if not skip_imports:
114
+ imports_needed = self._get_required_imports(inferred_type)
115
+ if imports_needed:
116
+ lines = self._add_imports(lines, imports_needed)
117
+
118
+ # Write back
119
+ file_path.write_text(''.join(lines), encoding='utf-8')
120
+ return True
121
+
122
+ except Exception as e:
123
+ print(f"Error fixing {issue.context['method_name']}: {e}")
124
+ return False
125
+
126
+ return False
127
+
128
+ def fix_batch(self, issues: list[Issue]) -> bool:
129
+ """
130
+ Fix multiple issues in same file at once.
131
+ This prevents line number shifting from import additions.
132
+ """
133
+ if not issues:
134
+ return True
135
+
136
+ file_path = issues[0].file
137
+
138
+ try:
139
+ # Read file once
140
+ content = file_path.read_text(encoding='utf-8')
141
+ lines = content.splitlines(keepends=True)
142
+
143
+ # Sort issues by line number in reverse order
144
+ sorted_issues = sorted(issues, key=lambda i: i.context['line_number'], reverse=True)
145
+
146
+ # Collect all required imports
147
+ all_imports = set()
148
+
149
+ # Fix all methods (from bottom to top)
150
+ for issue in sorted_issues:
151
+ inferred_type = issue.context['inferred_type']
152
+ line_number = issue.context['line_number']
153
+
154
+ if line_number > len(lines):
155
+ continue
156
+
157
+ target_line = lines[line_number - 1]
158
+
159
+ if ')' in target_line and ':' in target_line:
160
+ # Add type hint
161
+ modified_line = target_line.replace('):', f") -> {inferred_type}:", 1)
162
+ lines[line_number - 1] = modified_line
163
+
164
+ # Collect imports
165
+ imports_needed = self._get_required_imports(inferred_type)
166
+ for imp in imports_needed:
167
+ all_imports.add(imp)
168
+
169
+ # Add all imports at once
170
+ if all_imports:
171
+ lines = self._add_imports(lines, list(all_imports))
172
+
173
+ # Write back once
174
+ file_path.write_text(''.join(lines), encoding='utf-8')
175
+ return True
176
+
177
+ except Exception as e:
178
+ print(f"Error in batch fix: {e}")
179
+ return False
180
+
181
+ def _infer_type(self, node: ast.FunctionDef) -> str:
182
+ """Infer return type from method name and body."""
183
+ name = node.name
184
+
185
+ # Pattern matching for common cases
186
+ if any(pattern in name for pattern in ['can_', 'is_', 'has_']):
187
+ return 'bool'
188
+
189
+ if name.endswith('_count') or name.endswith('_total'):
190
+ return 'int'
191
+
192
+ if any(pattern in name for pattern in ['get_children', 'get_items', 'get_replies', 'get_list']):
193
+ return 'List[Dict[str, Any]]'
194
+
195
+ if name.endswith('_display') or name.endswith('_text'):
196
+ return 'str'
197
+
198
+ # Analyze return statements
199
+ for child in ast.walk(node):
200
+ if isinstance(child, ast.Return) and child.value:
201
+ # Constant values
202
+ if isinstance(child.value, ast.Constant):
203
+ if isinstance(child.value.value, bool):
204
+ return 'bool'
205
+ elif isinstance(child.value.value, int):
206
+ return 'int'
207
+ elif isinstance(child.value.value, str):
208
+ return 'str'
209
+ elif child.value.value is None:
210
+ return 'Optional[Any]'
211
+
212
+ # List/Dict literals
213
+ elif isinstance(child.value, ast.List):
214
+ return 'List[Any]'
215
+ elif isinstance(child.value, ast.Dict):
216
+ return 'Dict[str, Any]'
217
+
218
+ # Method calls that suggest serializer
219
+ elif isinstance(child.value, ast.Call):
220
+ # SomeSerializer(...).data pattern
221
+ if hasattr(child.value.func, 'attr') and child.value.func.attr == 'data':
222
+ # Check if many=True in call
223
+ for keyword in getattr(child.value, 'keywords', []):
224
+ if keyword.arg == 'many' and isinstance(keyword.value, ast.Constant):
225
+ if keyword.value.value is True:
226
+ return 'List[Dict[str, Any]]'
227
+ return 'Dict[str, Any]'
228
+
229
+ # Default fallback
230
+ return 'Any'
231
+
232
+ def _get_required_imports(self, type_hint: str) -> List[str]:
233
+ """Get required imports for type hint."""
234
+ imports = set()
235
+
236
+ if 'List' in type_hint:
237
+ imports.add('List')
238
+ if 'Dict' in type_hint:
239
+ imports.add('Dict')
240
+ if 'Optional' in type_hint:
241
+ imports.add('Optional')
242
+ if 'Any' in type_hint or not imports:
243
+ imports.add('Any')
244
+
245
+ if imports:
246
+ return [f"from typing import {', '.join(sorted(imports))}"]
247
+ return []
248
+
249
+ def _add_imports(self, lines: List[str], imports: List[str]) -> List[str]:
250
+ """Add imports to file if not already present."""
251
+ content = ''.join(lines)
252
+
253
+ for import_line in imports:
254
+ # Skip if import already exists
255
+ if import_line in content:
256
+ continue
257
+
258
+ # Check if similar import exists
259
+ import_parts = set(import_line.split('import ')[1].replace(' ', '').split(','))
260
+
261
+ # Try to merge with existing typing imports
262
+ merged = False
263
+ for i, line in enumerate(lines):
264
+ if line.strip().startswith('from typing import'):
265
+ existing_parts = set(line.split('import ')[1].replace(' ', '').replace('\n', '').split(','))
266
+ combined = sorted(existing_parts | import_parts)
267
+ lines[i] = f"from typing import {', '.join(combined)}\n"
268
+ merged = True
269
+ break
270
+
271
+ if merged:
272
+ continue
273
+
274
+ # Find where to insert (after other imports)
275
+ insert_idx = 0
276
+ found_import = False
277
+ for i, line in enumerate(lines):
278
+ stripped = line.strip()
279
+ if stripped.startswith('import ') or stripped.startswith('from '):
280
+ found_import = True
281
+ insert_idx = i + 1
282
+ elif found_import and stripped and not stripped.startswith('#'):
283
+ # Found first non-import, non-comment line
284
+ break
285
+
286
+ lines.insert(insert_idx, import_line + '\n')
287
+
288
+ return lines
@@ -0,0 +1,266 @@
1
+ """
2
+ Safety Manager for Code Modifications.
3
+
4
+ Ensures all code modifications are:
5
+ - Backed up before changes
6
+ - Syntax-validated after changes
7
+ - Revertible via rollback
8
+ - Logged for audit trail
9
+ """
10
+
11
+ import ast
12
+ import shutil
13
+ from datetime import datetime, timedelta
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional
16
+ import logging
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SafetyManager:
22
+ """
23
+ Manages backups and rollbacks for safe code modification.
24
+
25
+ Example:
26
+ >>> safety = SafetyManager(workspace=Path('.'))
27
+ >>> transaction_id = safety.start_transaction()
28
+ >>> safety.backup_file(Path('serializers.py'))
29
+ >>> # ... modify file ...
30
+ >>> if safety.validate_syntax(Path('serializers.py')):
31
+ ... safety.commit_transaction()
32
+ ... else:
33
+ ... safety.rollback_transaction()
34
+ """
35
+
36
+ def __init__(self, workspace: Path):
37
+ """
38
+ Initialize safety manager.
39
+
40
+ Args:
41
+ workspace: Root directory for project (usually project root)
42
+ """
43
+ self.workspace = workspace
44
+ self.backup_dir = workspace / '.validation_backups'
45
+ self.transaction_id: Optional[str] = None
46
+ self.backed_up_files: Dict[Path, Path] = {}
47
+
48
+ # Ensure backup directory exists
49
+ self.backup_dir.mkdir(exist_ok=True)
50
+
51
+ def start_transaction(self) -> str:
52
+ """
53
+ Start a new modification transaction.
54
+
55
+ Returns:
56
+ Transaction ID (timestamp-based)
57
+ """
58
+ self.transaction_id = f"fix_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
59
+ self.backed_up_files = {}
60
+
61
+ logger.info(f"Started transaction: {self.transaction_id}")
62
+ return self.transaction_id
63
+
64
+ def backup_file(self, file_path: Path) -> Path:
65
+ """
66
+ Create backup of file before modification.
67
+
68
+ Args:
69
+ file_path: Path to file to backup
70
+
71
+ Returns:
72
+ Path to backup file
73
+
74
+ Raises:
75
+ RuntimeError: If no active transaction
76
+ FileNotFoundError: If file doesn't exist
77
+ """
78
+ if not self.transaction_id:
79
+ raise RuntimeError("No active transaction. Call start_transaction() first.")
80
+
81
+ if not file_path.exists():
82
+ raise FileNotFoundError(f"File not found: {file_path}")
83
+
84
+ # Create transaction backup directory
85
+ transaction_dir = self.backup_dir / self.transaction_id
86
+ transaction_dir.mkdir(parents=True, exist_ok=True)
87
+
88
+ # Create backup with relative path structure
89
+ relative_path = file_path.relative_to(self.workspace)
90
+ backup_path = transaction_dir / relative_path
91
+ backup_path.parent.mkdir(parents=True, exist_ok=True)
92
+
93
+ # Copy file
94
+ shutil.copy2(file_path, backup_path)
95
+ self.backed_up_files[file_path] = backup_path
96
+
97
+ logger.debug(f"Backed up: {file_path} → {backup_path}")
98
+ return backup_path
99
+
100
+ def validate_syntax(self, file_path: Path) -> bool:
101
+ """
102
+ Validate Python syntax after modification.
103
+
104
+ Args:
105
+ file_path: Path to Python file to validate
106
+
107
+ Returns:
108
+ True if syntax is valid, False otherwise
109
+ """
110
+ try:
111
+ content = file_path.read_text(encoding='utf-8')
112
+ ast.parse(content)
113
+ logger.debug(f"Syntax valid: {file_path}")
114
+ return True
115
+ except SyntaxError as e:
116
+ logger.error(f"Syntax error in {file_path}: {e}")
117
+ return False
118
+ except Exception as e:
119
+ logger.error(f"Error validating {file_path}: {e}")
120
+ return False
121
+
122
+ def rollback_file(self, file_path: Path) -> bool:
123
+ """
124
+ Rollback a single file to its backup.
125
+
126
+ Args:
127
+ file_path: Path to file to rollback
128
+
129
+ Returns:
130
+ True if rollback successful, False otherwise
131
+ """
132
+ backup_path = self.backed_up_files.get(file_path)
133
+ if not backup_path or not backup_path.exists():
134
+ logger.warning(f"No backup found for: {file_path}")
135
+ return False
136
+
137
+ try:
138
+ shutil.copy2(backup_path, file_path)
139
+ logger.info(f"Rolled back: {file_path}")
140
+ return True
141
+ except Exception as e:
142
+ logger.error(f"Error rolling back {file_path}: {e}")
143
+ return False
144
+
145
+ def commit_transaction(self) -> bool:
146
+ """
147
+ Finalize successful transaction.
148
+
149
+ Keeps backups for 7 days then schedules cleanup.
150
+
151
+ Returns:
152
+ True if commit successful
153
+ """
154
+ if not self.transaction_id:
155
+ logger.warning("No active transaction to commit")
156
+ return False
157
+
158
+ logger.info(f"Committed transaction: {self.transaction_id}")
159
+
160
+ # Schedule cleanup (delete backups after 7 days)
161
+ self._schedule_cleanup(days=7)
162
+
163
+ # Clear transaction state
164
+ transaction_id = self.transaction_id
165
+ self.transaction_id = None
166
+ self.backed_up_files = {}
167
+
168
+ return True
169
+
170
+ def rollback_transaction(self) -> bool:
171
+ """
172
+ Revert all changes in current transaction.
173
+
174
+ Returns:
175
+ True if all files rolled back successfully
176
+ """
177
+ if not self.transaction_id:
178
+ logger.warning("No active transaction to rollback")
179
+ return False
180
+
181
+ logger.warning(f"Rolling back transaction: {self.transaction_id}")
182
+
183
+ success = True
184
+ for file_path in self.backed_up_files.keys():
185
+ if not self.rollback_file(file_path):
186
+ success = False
187
+
188
+ # Clear transaction state
189
+ self.transaction_id = None
190
+ self.backed_up_files = {}
191
+
192
+ return success
193
+
194
+ def _schedule_cleanup(self, days: int = 7) -> None:
195
+ """
196
+ Schedule cleanup of old backups.
197
+
198
+ Note: Currently just marks with timestamp.
199
+ Actual cleanup should be done by separate process.
200
+
201
+ Args:
202
+ days: Keep backups for this many days
203
+ """
204
+ cleanup_marker = self.backup_dir / self.transaction_id / '.cleanup_after'
205
+ cleanup_date = datetime.now() + timedelta(days=days)
206
+ cleanup_marker.write_text(cleanup_date.isoformat())
207
+
208
+ def cleanup_old_backups(self, days: int = 7) -> int:
209
+ """
210
+ Remove backups older than specified days.
211
+
212
+ Args:
213
+ days: Remove backups older than this many days
214
+
215
+ Returns:
216
+ Number of backup directories removed
217
+ """
218
+ removed = 0
219
+ cutoff_date = datetime.now() - timedelta(days=days)
220
+
221
+ for transaction_dir in self.backup_dir.iterdir():
222
+ if not transaction_dir.is_dir():
223
+ continue
224
+
225
+ # Check if marked for cleanup
226
+ cleanup_marker = transaction_dir / '.cleanup_after'
227
+ if cleanup_marker.exists():
228
+ cleanup_date = datetime.fromisoformat(cleanup_marker.read_text())
229
+ if datetime.now() >= cleanup_date:
230
+ shutil.rmtree(transaction_dir)
231
+ removed += 1
232
+ logger.info(f"Removed old backup: {transaction_dir.name}")
233
+
234
+ return removed
235
+
236
+ def list_backups(self) -> List[Dict[str, any]]:
237
+ """
238
+ List all available backups.
239
+
240
+ Returns:
241
+ List of backup info dicts
242
+ """
243
+ backups = []
244
+
245
+ for transaction_dir in sorted(self.backup_dir.iterdir()):
246
+ if not transaction_dir.is_dir():
247
+ continue
248
+
249
+ # Parse transaction ID
250
+ timestamp_str = transaction_dir.name.replace('fix_', '')
251
+ try:
252
+ timestamp = datetime.strptime(timestamp_str[:15], '%Y%m%d_%H%M%S')
253
+ except ValueError:
254
+ continue
255
+
256
+ # Count files
257
+ files = list(transaction_dir.rglob('*.py'))
258
+
259
+ backups.append({
260
+ 'transaction_id': transaction_dir.name,
261
+ 'timestamp': timestamp,
262
+ 'file_count': len(files),
263
+ 'path': transaction_dir,
264
+ })
265
+
266
+ return backups
@@ -0,0 +1,3 @@
1
+ """
2
+ Django management commands for django-client.
3
+ """
@@ -0,0 +1,3 @@
1
+ """
2
+ Django management commands.
3
+ """