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,648 @@
1
+ """
2
+ Base Parser - Common parsing logic for OpenAPI → IR.
3
+
4
+ This module defines the abstract BaseParser class that contains shared logic
5
+ for converting OpenAPI specifications to IR, regardless of version.
6
+
7
+ Version-specific logic (nullable handling, etc.) is delegated to subclasses.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from abc import ABC, abstractmethod
14
+ from typing import Any
15
+
16
+ from ..ir import (
17
+ DjangoGlobalMetadata,
18
+ IRContext,
19
+ IROperationObject,
20
+ IRParameterObject,
21
+ IRRequestBodyObject,
22
+ IRResponseObject,
23
+ IRSchemaObject,
24
+ OpenAPIInfo,
25
+ )
26
+ from .models import (
27
+ OpenAPISpec,
28
+ OperationObject,
29
+ ParameterObject,
30
+ PathItemObject,
31
+ ReferenceObject,
32
+ RequestBodyObject,
33
+ ResponseObject,
34
+ SchemaObject,
35
+ )
36
+
37
+
38
+ class BaseParser(ABC):
39
+ """
40
+ Abstract base parser for OpenAPI → IR conversion.
41
+
42
+ Subclasses implement version-specific logic:
43
+ - OpenAPI30Parser: Handles nullable: true
44
+ - OpenAPI31Parser: Handles type: ['string', 'null']
45
+ """
46
+
47
+ def __init__(self, spec: OpenAPISpec):
48
+ """
49
+ Initialize parser with OpenAPI spec.
50
+
51
+ Args:
52
+ spec: Validated OpenAPISpec object
53
+ """
54
+ self.spec = spec
55
+ self._schema_cache: dict[str, IRSchemaObject] = {}
56
+
57
+ # ===== Main Parse Method =====
58
+
59
+ def parse(self) -> IRContext:
60
+ """
61
+ Parse OpenAPI spec to IRContext.
62
+
63
+ Returns:
64
+ IRContext with all schemas and operations
65
+
66
+ Raises:
67
+ ValueError: If COMPONENT_SPLIT_REQUEST is not detected
68
+ """
69
+ # Parse metadata
70
+ openapi_info = self._parse_openapi_info()
71
+ django_metadata = self._parse_django_metadata()
72
+
73
+ # Parse schemas
74
+ schemas = self._parse_all_schemas()
75
+
76
+ # Parse operations
77
+ operations = self._parse_all_operations()
78
+
79
+ return IRContext(
80
+ openapi_info=openapi_info,
81
+ django_metadata=django_metadata,
82
+ schemas=schemas,
83
+ operations=operations,
84
+ )
85
+
86
+ # ===== Metadata Parsing =====
87
+
88
+ def _parse_openapi_info(self) -> OpenAPIInfo:
89
+ """Parse OpenAPI info to IR."""
90
+ info = self.spec.info
91
+ return OpenAPIInfo(
92
+ version=self.spec.normalized_version,
93
+ title=info.title,
94
+ description=info.description,
95
+ api_version=info.version,
96
+ servers=self.spec.server_urls,
97
+ contact_name=info.contact.name if info.contact else None,
98
+ contact_email=info.contact.email if info.contact else None,
99
+ license_name=info.license.name if info.license else None,
100
+ license_url=info.license.url if info.license else None,
101
+ )
102
+
103
+ def _parse_django_metadata(self) -> DjangoGlobalMetadata:
104
+ """
105
+ Parse Django/drf-spectacular metadata.
106
+
107
+ CRITICAL: This method VALIDATES Django settings, not detects from schema.
108
+ For Read-Only APIs (e.g., shop with only GET endpoints), there may be
109
+ no Request models in the schema. This is VALID if COMPONENT_SPLIT_REQUEST
110
+ is True in Django settings.
111
+
112
+ Returns:
113
+ DjangoGlobalMetadata with validated settings
114
+
115
+ Raises:
116
+ ValueError: If COMPONENT_SPLIT_REQUEST is False in Django settings
117
+ """
118
+ # Try to get settings from Django
119
+ has_split = self._get_django_spectacular_setting('COMPONENT_SPLIT_REQUEST')
120
+ has_patch = self._get_django_spectacular_setting('COMPONENT_SPLIT_PATCH')
121
+
122
+ # Fallback to detection if Django settings not available
123
+ if has_split is None:
124
+ has_split = self._detect_component_split_request()
125
+
126
+ if has_patch is None:
127
+ has_patch = self._detect_component_split_patch()
128
+
129
+ return DjangoGlobalMetadata(
130
+ component_split_request=has_split if has_split is not None else False,
131
+ component_split_patch=has_patch if has_patch is not None else False,
132
+ oas_version=self.spec.normalized_version,
133
+ )
134
+
135
+ def _get_django_spectacular_setting(self, setting_name: str) -> bool | None:
136
+ """
137
+ Get drf-spectacular setting from Django settings.
138
+
139
+ Args:
140
+ setting_name: Name of the SPECTACULAR_SETTINGS key
141
+
142
+ Returns:
143
+ Setting value, or None if not available
144
+ """
145
+ try:
146
+ from django.conf import settings
147
+ if not settings.configured:
148
+ return None
149
+
150
+ spectacular_settings = getattr(settings, 'SPECTACULAR_SETTINGS', {})
151
+ return spectacular_settings.get(setting_name)
152
+ except ImportError:
153
+ # Django not available (standalone usage)
154
+ return None
155
+ except Exception:
156
+ # Any other error
157
+ return None
158
+
159
+ def _detect_component_split_request(self) -> bool:
160
+ """
161
+ Detect if COMPONENT_SPLIT_REQUEST: True is used.
162
+
163
+ Detection strategy:
164
+ 1. Look for schema pairs: User + UserRequest
165
+ 2. Look for schema pairs: Task + TaskRequest
166
+ 3. At least one pair must exist
167
+
168
+ Returns:
169
+ True if Request/Response split detected, False otherwise
170
+ """
171
+ if not self.spec.components or not self.spec.components.schemas:
172
+ return False
173
+
174
+ schema_names = set(self.spec.components.schemas.keys())
175
+
176
+ # Check for Request suffix pattern
177
+ for name in schema_names:
178
+ if name.endswith("Request"):
179
+ # Check if response model exists (without Request suffix)
180
+ response_name = name[:-7] # Remove "Request"
181
+ if response_name in schema_names:
182
+ return True
183
+
184
+ return False
185
+
186
+ def _detect_component_split_patch(self) -> bool:
187
+ """
188
+ Detect if COMPONENT_SPLIT_PATCH: True is used.
189
+
190
+ Detection strategy:
191
+ 1. Look for schema pairs: User + PatchedUser
192
+ 2. At least one pair must exist
193
+
194
+ Returns:
195
+ True if PATCH split detected
196
+ """
197
+ if not self.spec.components or not self.spec.components.schemas:
198
+ return False
199
+
200
+ schema_names = set(self.spec.components.schemas.keys())
201
+
202
+ # Check for Patched prefix pattern
203
+ for name in schema_names:
204
+ if name.startswith("Patched"):
205
+ # Check if base model exists (without Patched prefix)
206
+ base_name = name[7:] # Remove "Patched"
207
+ if base_name in schema_names:
208
+ return True
209
+
210
+ return False
211
+
212
+ # ===== Schema Parsing =====
213
+
214
+ def _parse_all_schemas(self) -> dict[str, IRSchemaObject]:
215
+ """Parse all schemas from components."""
216
+ if not self.spec.components or not self.spec.components.schemas:
217
+ return {}
218
+
219
+ schemas = {}
220
+ for name, schema_or_ref in self.spec.components.schemas.items():
221
+ # Skip references for now
222
+ if isinstance(schema_or_ref, ReferenceObject):
223
+ continue
224
+
225
+ schemas[name] = self._parse_schema(name, schema_or_ref)
226
+
227
+ return schemas
228
+
229
+ def _parse_schema(self, name: str, schema: SchemaObject) -> IRSchemaObject:
230
+ """
231
+ Parse SchemaObject to IRSchemaObject.
232
+
233
+ Args:
234
+ name: Schema name (e.g., 'User', 'UserRequest')
235
+ schema: Raw SchemaObject from OpenAPI spec
236
+
237
+ Returns:
238
+ IRSchemaObject with Request/Response split awareness
239
+ """
240
+ # Check cache
241
+ if name in self._schema_cache:
242
+ return self._schema_cache[name]
243
+
244
+ # Detect Request/Response/Patch model type
245
+ is_request = name.endswith("Request")
246
+ is_patch = name.startswith("Patched")
247
+ is_response = not is_request and not is_patch
248
+
249
+ # Determine related models
250
+ related_request = None
251
+ related_response = None
252
+
253
+ if is_response:
254
+ # Check if UserRequest exists
255
+ potential_request = f"{name}Request"
256
+ if self._schema_exists(potential_request):
257
+ related_request = potential_request
258
+
259
+ if is_request:
260
+ # Extract base name (User from UserRequest)
261
+ base_name = name[:-7] # Remove "Request"
262
+ if self._schema_exists(base_name):
263
+ related_response = base_name
264
+
265
+ if is_patch:
266
+ # Extract base name (User from PatchedUser)
267
+ base_name = name[7:] # Remove "Patched"
268
+ if self._schema_exists(base_name):
269
+ related_response = base_name
270
+
271
+ # Parse properties
272
+ properties = {}
273
+ if schema.properties:
274
+ for prop_name, prop_schema_or_ref in schema.properties.items():
275
+ if isinstance(prop_schema_or_ref, ReferenceObject):
276
+ # Resolve reference
277
+ properties[prop_name] = self._resolve_ref(prop_schema_or_ref)
278
+ else:
279
+ # Handle allOf/anyOf/oneOf (common in drf-spectacular for enum fields)
280
+ ref_from_combinator = self._extract_ref_from_combinators(prop_schema_or_ref)
281
+ if ref_from_combinator:
282
+ # Found $ref in allOf/anyOf/oneOf - resolve it
283
+ properties[prop_name] = self._resolve_ref(ref_from_combinator)
284
+ else:
285
+ # No combinator $ref - parse normally
286
+ properties[prop_name] = self._parse_schema(
287
+ f"{name}.{prop_name}", prop_schema_or_ref
288
+ )
289
+
290
+ # Parse array items
291
+ items = None
292
+ if schema.items:
293
+ if isinstance(schema.items, ReferenceObject):
294
+ # Resolve reference
295
+ items = self._resolve_ref(schema.items)
296
+ else:
297
+ items = self._parse_schema(f"{name}.items", schema.items)
298
+
299
+ # Create IR schema
300
+ ir_schema = IRSchemaObject(
301
+ name=name,
302
+ type=self._normalize_type(schema),
303
+ format=schema.format,
304
+ description=schema.description,
305
+ nullable=self._detect_nullable(schema),
306
+ properties=properties,
307
+ required=schema.required or [],
308
+ items=items,
309
+ enum=schema.enum,
310
+ enum_var_names=schema.x_enum_varnames,
311
+ const=schema.const,
312
+ is_request_model=is_request,
313
+ is_response_model=is_response,
314
+ is_patch_model=is_patch,
315
+ related_request=related_request,
316
+ related_response=related_response,
317
+ min_length=schema.minLength,
318
+ max_length=schema.maxLength,
319
+ pattern=schema.pattern,
320
+ minimum=schema.minimum,
321
+ maximum=schema.maximum,
322
+ read_only=schema.readOnly,
323
+ write_only=schema.writeOnly,
324
+ deprecated=schema.deprecated,
325
+ )
326
+
327
+ # Cache
328
+ self._schema_cache[name] = ir_schema
329
+
330
+ return ir_schema
331
+
332
+ def _schema_exists(self, name: str) -> bool:
333
+ """Check if schema exists in components."""
334
+ if not self.spec.components or not self.spec.components.schemas:
335
+ return False
336
+ return name in self.spec.components.schemas
337
+
338
+ def _resolve_ref(self, ref: ReferenceObject) -> IRSchemaObject:
339
+ """
340
+ Resolve $ref to schema.
341
+
342
+ Args:
343
+ ref: ReferenceObject with $ref string
344
+
345
+ Returns:
346
+ IRSchemaObject (creates simple reference object)
347
+
348
+ Example:
349
+ {"$ref": "#/components/schemas/Profile"}
350
+ → IRSchemaObject(name="Profile", type="object", ref="Profile")
351
+ """
352
+ # Extract schema name from $ref
353
+ # Format: #/components/schemas/SchemaName
354
+ if not ref.ref.startswith("#/components/schemas/"):
355
+ raise ValueError(f"Unsupported $ref format: {ref.ref}")
356
+
357
+ schema_name = ref.ref.split("/")[-1]
358
+
359
+ # Create simple reference object
360
+ # Parser will replace this with actual schema in generator
361
+ return IRSchemaObject(
362
+ name=schema_name,
363
+ type="object", # References are typically objects
364
+ ref=schema_name, # Store reference name
365
+ )
366
+
367
+ def _extract_ref_from_combinators(self, schema: SchemaObject) -> ReferenceObject | None:
368
+ """
369
+ Extract $ref from allOf/anyOf/oneOf combinators if present.
370
+
371
+ DRF-spectacular often wraps enum references in allOf:
372
+ "status": {
373
+ "allOf": [{"$ref": "#/components/schemas/StatusEnum"}],
374
+ "description": "...",
375
+ "readOnly": true
376
+ }
377
+
378
+ This method extracts the $ref for resolution.
379
+
380
+ Args:
381
+ schema: SchemaObject that may contain allOf/anyOf/oneOf
382
+
383
+ Returns:
384
+ ReferenceObject if $ref found in combinators, None otherwise
385
+ """
386
+ # Check for allOf (most common in drf-spectacular for enum fields)
387
+ if schema.allOf and len(schema.allOf) > 0:
388
+ for item in schema.allOf:
389
+ if isinstance(item, ReferenceObject):
390
+ return item
391
+
392
+ # Check for anyOf
393
+ if schema.anyOf and len(schema.anyOf) > 0:
394
+ for item in schema.anyOf:
395
+ if isinstance(item, ReferenceObject):
396
+ return item
397
+
398
+ # Check for oneOf
399
+ if schema.oneOf and len(schema.oneOf) > 0:
400
+ for item in schema.oneOf:
401
+ if isinstance(item, ReferenceObject):
402
+ return item
403
+
404
+ # No combinators or no $ref found
405
+ return None
406
+
407
+ @abstractmethod
408
+ def _detect_nullable(self, schema: SchemaObject) -> bool:
409
+ """
410
+ Detect if schema is nullable (version-specific).
411
+
412
+ Subclasses implement:
413
+ - OpenAPI30Parser: Check nullable: true
414
+ - OpenAPI31Parser: Check type: ['string', 'null']
415
+
416
+ Args:
417
+ schema: Raw SchemaObject
418
+
419
+ Returns:
420
+ True if nullable, False otherwise
421
+ """
422
+ pass
423
+
424
+ def _normalize_type(self, schema: SchemaObject) -> str:
425
+ """
426
+ Normalize schema type to single string.
427
+
428
+ For OAS 3.1.0, type: ['string', 'null'] → 'string'
429
+
430
+ Args:
431
+ schema: Raw SchemaObject
432
+
433
+ Returns:
434
+ Normalized type string
435
+ """
436
+ if schema.base_type:
437
+ return schema.base_type
438
+
439
+ # Fallback: infer from other properties
440
+ if schema.properties is not None:
441
+ return "object"
442
+ if schema.items is not None:
443
+ return "array"
444
+
445
+ return "string" # Default
446
+
447
+ # ===== Operation Parsing =====
448
+
449
+ def _parse_all_operations(self) -> dict[str, IROperationObject]:
450
+ """Parse all operations from paths."""
451
+ if not self.spec.paths:
452
+ return {}
453
+
454
+ operations = {}
455
+ for path, path_item in self.spec.paths.items():
456
+ for method, operation in path_item.operations.items():
457
+ if not operation.operationId:
458
+ # Generate operation_id if missing
459
+ operation.operationId = self._generate_operation_id(method, path)
460
+
461
+ op_id = operation.operationId
462
+ operations[op_id] = self._parse_operation(
463
+ operation, method, path, path_item
464
+ )
465
+
466
+ return operations
467
+
468
+ def _parse_operation(
469
+ self,
470
+ operation: OperationObject,
471
+ method: str,
472
+ path: str,
473
+ path_item: PathItemObject,
474
+ ) -> IROperationObject:
475
+ """Parse OperationObject to IROperationObject."""
476
+ # Parse parameters
477
+ parameters = self._parse_parameters(operation, path_item)
478
+
479
+ # Parse request body
480
+ request_body = None
481
+ patch_request_body = None
482
+
483
+ if operation.requestBody:
484
+ if isinstance(operation.requestBody, ReferenceObject):
485
+ # TODO: Resolve reference
486
+ pass
487
+ else:
488
+ body = self._parse_request_body(operation.requestBody)
489
+ if method == "PATCH":
490
+ patch_request_body = body
491
+ else:
492
+ request_body = body
493
+
494
+ # Parse responses
495
+ responses = self._parse_responses(operation.responses)
496
+
497
+ return IROperationObject(
498
+ operation_id=operation.operationId or "",
499
+ http_method=method,
500
+ path=path,
501
+ summary=operation.summary,
502
+ description=operation.description,
503
+ tags=operation.tags or [],
504
+ parameters=parameters,
505
+ request_body=request_body,
506
+ patch_request_body=patch_request_body,
507
+ responses=responses,
508
+ deprecated=operation.deprecated,
509
+ )
510
+
511
+ def _parse_parameters(
512
+ self, operation: OperationObject, path_item: PathItemObject
513
+ ) -> list[IRParameterObject]:
514
+ """Parse parameters from operation and path item."""
515
+ params = []
516
+
517
+ # Path-level parameters
518
+ if path_item.parameters:
519
+ for param_or_ref in path_item.parameters:
520
+ if isinstance(param_or_ref, ReferenceObject):
521
+ continue
522
+ params.append(self._parse_parameter(param_or_ref))
523
+
524
+ # Operation-level parameters
525
+ if operation.parameters:
526
+ for param_or_ref in operation.parameters:
527
+ if isinstance(param_or_ref, ReferenceObject):
528
+ continue
529
+ params.append(self._parse_parameter(param_or_ref))
530
+
531
+ return params
532
+
533
+ def _parse_parameter(self, param: ParameterObject) -> IRParameterObject:
534
+ """Parse ParameterObject to IRParameterObject."""
535
+ schema_type = "string"
536
+ items_type = None
537
+
538
+ if param.schema_:
539
+ if isinstance(param.schema_, SchemaObject):
540
+ schema_type = self._normalize_type(param.schema_)
541
+ if schema_type == "array" and param.schema_.items:
542
+ if isinstance(param.schema_.items, SchemaObject):
543
+ items_type = self._normalize_type(param.schema_.items)
544
+
545
+ return IRParameterObject(
546
+ name=param.name,
547
+ location=param.in_,
548
+ schema_type=schema_type,
549
+ required=param.required,
550
+ description=param.description,
551
+ default=param.example, # Use example as default for now
552
+ items_type=items_type,
553
+ deprecated=param.deprecated,
554
+ )
555
+
556
+ def _parse_request_body(self, body: RequestBodyObject) -> IRRequestBodyObject:
557
+ """Parse RequestBodyObject to IRRequestBodyObject."""
558
+ # Extract schema name from content
559
+ schema_name = None
560
+ content_type = "application/json"
561
+
562
+ if body.content:
563
+ for ct, media_type in body.content.items():
564
+ content_type = ct
565
+ if media_type.schema_:
566
+ if isinstance(media_type.schema_, ReferenceObject):
567
+ schema_name = media_type.schema_.ref_name
568
+ else:
569
+ # Inline schema - use operation ID as name
570
+ # TODO: Generate proper name
571
+ schema_name = "InlineRequestBody"
572
+ break
573
+
574
+ return IRRequestBodyObject(
575
+ schema_name=schema_name or "UnknownRequest",
576
+ content_type=content_type,
577
+ required=body.required,
578
+ description=body.description,
579
+ )
580
+
581
+ def _parse_responses(
582
+ self, responses: dict[str, ResponseObject | ReferenceObject]
583
+ ) -> dict[int, IRResponseObject]:
584
+ """Parse responses to IR."""
585
+ ir_responses = {}
586
+
587
+ for status_code_str, response_or_ref in responses.items():
588
+ # Parse status code
589
+ if status_code_str == "default":
590
+ continue # Skip default for now
591
+
592
+ try:
593
+ status_code = int(status_code_str)
594
+ except ValueError:
595
+ continue
596
+
597
+ if isinstance(response_or_ref, ReferenceObject):
598
+ # TODO: Resolve reference
599
+ continue
600
+
601
+ # Extract schema name from content
602
+ schema_name = None
603
+ is_paginated = False
604
+
605
+ if response_or_ref.content:
606
+ for media_type in response_or_ref.content.values():
607
+ if media_type.schema_:
608
+ if isinstance(media_type.schema_, ReferenceObject):
609
+ schema_name = media_type.schema_.ref_name
610
+ # Detect pagination
611
+ if "Paginated" in schema_name or "List" in schema_name:
612
+ is_paginated = True
613
+ break
614
+
615
+ ir_responses[status_code] = IRResponseObject(
616
+ status_code=status_code,
617
+ schema_name=schema_name,
618
+ description=response_or_ref.description,
619
+ is_paginated=is_paginated,
620
+ )
621
+
622
+ return ir_responses
623
+
624
+ def _generate_operation_id(self, method: str, path: str) -> str:
625
+ """
626
+ Generate operation_id from method and path.
627
+
628
+ Examples:
629
+ GET /api/users/ → users_list
630
+ POST /api/users/ → users_create
631
+ GET /api/users/{id}/ → users_retrieve
632
+ """
633
+ # Extract resource name from path
634
+ parts = [p for p in path.split("/") if p and not p.startswith("{")]
635
+ resource = parts[-1] if parts else "unknown"
636
+
637
+ # Map method to action
638
+ action_map = {
639
+ "GET": "retrieve" if "{" in path else "list",
640
+ "POST": "create",
641
+ "PUT": "update",
642
+ "PATCH": "partial_update",
643
+ "DELETE": "destroy",
644
+ }
645
+
646
+ action = action_map.get(method, method.lower())
647
+
648
+ return f"{resource}_{action}"