django-cfg 1.4.10__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.
- django_cfg/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/views/api/currencies.py +49 -6
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +72 -49
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/dashboard/sections/documentation.py +391 -0
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -348
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +238 -0
- django_cfg/modules/django_admin/management/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +188 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +767 -0
- django_cfg/modules/django_client/core/generator/python.py +751 -0
- django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
- django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
- django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
- django_cfg/modules/django_client/core/generator/typescript.py +872 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/spectacular/__init__.py +9 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +16 -5
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/RECORD +180 -59
- django_cfg/management/commands/generate.py +0 -107
- /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.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}"
|