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.
- 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/middleware/api_access.py +6 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +2 -1
- django_cfg/apps/payments/middleware/usage_tracking.py +5 -1
- django_cfg/apps/payments/models/managers/api_key_managers.py +0 -1
- django_cfg/apps/payments/models/managers/subscription_managers.py +0 -1
- django_cfg/apps/payments/services/core/balance_service.py +5 -5
- django_cfg/apps/payments/services/core/subscription_service.py +1 -2
- django_cfg/apps/payments/views/api/balances.py +8 -7
- django_cfg/apps/payments/views/api/base.py +10 -6
- django_cfg/apps/payments/views/api/currencies.py +53 -10
- django_cfg/apps/payments/views/api/payments.py +3 -1
- django_cfg/apps/payments/views/api/subscriptions.py +2 -5
- 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 +82 -41
- 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 -265
- 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/models/django/{revolution.py → revolution_legacy.py} +8 -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/modules/django_unfold/dashboard.py +1 -1
- 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.9.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/RECORD +192 -71
- django_cfg/management/commands/generate.py +0 -107
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.9.dist-info → django_cfg-1.4.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,387 @@
|
|
1
|
+
"""
|
2
|
+
IR Context - Root model for unified API representation.
|
3
|
+
|
4
|
+
This module defines the root IRContext model that contains:
|
5
|
+
- All schemas (User, UserRequest, PatchedUser, etc.)
|
6
|
+
- All operations (endpoints)
|
7
|
+
- Django global metadata (COMPONENT_SPLIT_REQUEST, auth, etc.)
|
8
|
+
- OpenAPI metadata (version, title, servers)
|
9
|
+
|
10
|
+
Key Features:
|
11
|
+
- Single source of truth for code generation
|
12
|
+
- Version-agnostic (normalized from 3.0.3 and 3.1.0)
|
13
|
+
- Django-aware (validates COMPONENT_SPLIT_REQUEST)
|
14
|
+
"""
|
15
|
+
|
16
|
+
from __future__ import annotations
|
17
|
+
|
18
|
+
from typing import Literal
|
19
|
+
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
21
|
+
|
22
|
+
from .operation import IROperationObject
|
23
|
+
from .schema import IRSchemaObject
|
24
|
+
|
25
|
+
|
26
|
+
class OpenAPIInfo(BaseModel):
|
27
|
+
"""
|
28
|
+
OpenAPI specification metadata.
|
29
|
+
|
30
|
+
Examples:
|
31
|
+
>>> info = OpenAPIInfo(
|
32
|
+
... version="3.1.0",
|
33
|
+
... title="My Django API",
|
34
|
+
... description="RESTful API for my Django app",
|
35
|
+
... api_version="1.0.0",
|
36
|
+
... )
|
37
|
+
"""
|
38
|
+
|
39
|
+
model_config = ConfigDict(
|
40
|
+
validate_assignment=True,
|
41
|
+
extra="forbid",
|
42
|
+
frozen=False,
|
43
|
+
validate_default=True,
|
44
|
+
str_strip_whitespace=True,
|
45
|
+
)
|
46
|
+
|
47
|
+
version: Literal["3.0.3", "3.1.0"] = Field(
|
48
|
+
..., description="OpenAPI version (3.0.3 or 3.1.0)"
|
49
|
+
)
|
50
|
+
title: str = Field(..., description="API title")
|
51
|
+
description: str | None = Field(None, description="API description")
|
52
|
+
api_version: str = Field("1.0.0", description="API version (e.g., '1.0.0')")
|
53
|
+
|
54
|
+
# Servers
|
55
|
+
servers: list[str] = Field(
|
56
|
+
default_factory=list,
|
57
|
+
description="Server URLs (e.g., ['https://api.example.com', 'http://localhost:8000'])",
|
58
|
+
)
|
59
|
+
|
60
|
+
# Contact and license
|
61
|
+
contact_name: str | None = Field(None, description="Contact name")
|
62
|
+
contact_email: str | None = Field(None, description="Contact email")
|
63
|
+
license_name: str | None = Field(None, description="License name (e.g., 'MIT')")
|
64
|
+
license_url: str | None = Field(None, description="License URL")
|
65
|
+
|
66
|
+
@property
|
67
|
+
def is_openapi_31(self) -> bool:
|
68
|
+
"""Check if OpenAPI 3.1.0."""
|
69
|
+
return self.version == "3.1.0"
|
70
|
+
|
71
|
+
@property
|
72
|
+
def is_openapi_30(self) -> bool:
|
73
|
+
"""Check if OpenAPI 3.0.3."""
|
74
|
+
return self.version == "3.0.3"
|
75
|
+
|
76
|
+
def __repr__(self) -> str:
|
77
|
+
"""String representation for debugging."""
|
78
|
+
return f"OpenAPIInfo(version={self.version!r}, title={self.title!r}, api_version={self.api_version!r})"
|
79
|
+
|
80
|
+
|
81
|
+
class DjangoGlobalMetadata(BaseModel):
|
82
|
+
"""
|
83
|
+
Django global metadata from drf-spectacular settings.
|
84
|
+
|
85
|
+
This model captures critical Django/DRF configuration that affects
|
86
|
+
code generation. Most importantly, it validates that COMPONENT_SPLIT_REQUEST
|
87
|
+
is enabled (mandatory for correct Request/Response split).
|
88
|
+
|
89
|
+
Examples:
|
90
|
+
>>> # Correct configuration
|
91
|
+
>>> metadata = DjangoGlobalMetadata(
|
92
|
+
... component_split_request=True,
|
93
|
+
... component_split_patch=True,
|
94
|
+
... oas_version="3.1.0",
|
95
|
+
... )
|
96
|
+
|
97
|
+
>>> # Missing COMPONENT_SPLIT_REQUEST (will raise validation error)
|
98
|
+
>>> bad_metadata = DjangoGlobalMetadata(
|
99
|
+
... component_split_request=False, # ❌ FORBIDDEN!
|
100
|
+
... )
|
101
|
+
Traceback (most recent call last):
|
102
|
+
...
|
103
|
+
ValueError: COMPONENT_SPLIT_REQUEST must be True
|
104
|
+
"""
|
105
|
+
|
106
|
+
model_config = ConfigDict(
|
107
|
+
validate_assignment=True,
|
108
|
+
extra="forbid",
|
109
|
+
frozen=False,
|
110
|
+
validate_default=True,
|
111
|
+
str_strip_whitespace=True,
|
112
|
+
)
|
113
|
+
|
114
|
+
# ===== CRITICAL: drf-spectacular Settings =====
|
115
|
+
component_split_request: bool = Field(
|
116
|
+
...,
|
117
|
+
description="COMPONENT_SPLIT_REQUEST setting (MUST be True for correct code generation)",
|
118
|
+
)
|
119
|
+
component_split_patch: bool = Field(
|
120
|
+
True,
|
121
|
+
description="COMPONENT_SPLIT_PATCH setting (separate PatchedUser models)",
|
122
|
+
)
|
123
|
+
oas_version: Literal["3.0.3", "3.1.0"] = Field(
|
124
|
+
"3.1.0",
|
125
|
+
description="OAS_VERSION setting (3.1.0 recommended)",
|
126
|
+
)
|
127
|
+
|
128
|
+
# ===== Default Authentication/Permissions =====
|
129
|
+
default_authentication_classes: list[str] = Field(
|
130
|
+
default_factory=list,
|
131
|
+
description="Default authentication classes (e.g., ['SessionAuthentication'])",
|
132
|
+
)
|
133
|
+
default_permission_classes: list[str] = Field(
|
134
|
+
default_factory=list,
|
135
|
+
description="Default permission classes (e.g., ['IsAuthenticated'])",
|
136
|
+
)
|
137
|
+
|
138
|
+
# ===== CSRF =====
|
139
|
+
csrf_cookie_name: str = Field(
|
140
|
+
"csrftoken",
|
141
|
+
description="Django CSRF cookie name (default: 'csrftoken')",
|
142
|
+
)
|
143
|
+
csrf_header_name: str = Field(
|
144
|
+
"X-CSRFToken",
|
145
|
+
description="Django CSRF header name (default: 'X-CSRFToken')",
|
146
|
+
)
|
147
|
+
|
148
|
+
# ===== Session =====
|
149
|
+
session_cookie_name: str = Field(
|
150
|
+
"sessionid",
|
151
|
+
description="Django session cookie name (default: 'sessionid')",
|
152
|
+
)
|
153
|
+
|
154
|
+
# ===== Validation =====
|
155
|
+
|
156
|
+
@field_validator("component_split_request")
|
157
|
+
@classmethod
|
158
|
+
def validate_component_split_request(cls, v: bool) -> bool:
|
159
|
+
"""
|
160
|
+
Validate that COMPONENT_SPLIT_REQUEST is True.
|
161
|
+
|
162
|
+
This is MANDATORY for correct Request/Response split.
|
163
|
+
Without it, generators will create broken models.
|
164
|
+
|
165
|
+
Raises:
|
166
|
+
ValueError: If COMPONENT_SPLIT_REQUEST is False.
|
167
|
+
"""
|
168
|
+
if not v:
|
169
|
+
raise ValueError(
|
170
|
+
"COMPONENT_SPLIT_REQUEST must be True! "
|
171
|
+
"This is mandatory for correct Request/Response model generation. "
|
172
|
+
"Add this to your Django settings:\n\n"
|
173
|
+
"SPECTACULAR_SETTINGS = {\n"
|
174
|
+
" 'COMPONENT_SPLIT_REQUEST': True,\n"
|
175
|
+
" 'COMPONENT_SPLIT_PATCH': True,\n"
|
176
|
+
"}\n\n"
|
177
|
+
"See: https://drf-spectacular.readthedocs.io/en/latest/settings.html#component-split-request"
|
178
|
+
)
|
179
|
+
return v
|
180
|
+
|
181
|
+
def __repr__(self) -> str:
|
182
|
+
"""String representation for debugging."""
|
183
|
+
return (
|
184
|
+
f"DjangoGlobalMetadata("
|
185
|
+
f"component_split_request={self.component_split_request}, "
|
186
|
+
f"oas_version={self.oas_version!r})"
|
187
|
+
)
|
188
|
+
|
189
|
+
|
190
|
+
class IRContext(BaseModel):
|
191
|
+
"""
|
192
|
+
Root IR model - single source of truth for code generation.
|
193
|
+
|
194
|
+
This model contains everything needed to generate clients:
|
195
|
+
- All schemas (User, UserRequest, PatchedUser, ValidationError, etc.)
|
196
|
+
- All operations (users_list, users_create, users_retrieve, etc.)
|
197
|
+
- Django metadata (COMPONENT_SPLIT_REQUEST validation)
|
198
|
+
- OpenAPI metadata (version, title, servers)
|
199
|
+
|
200
|
+
Examples:
|
201
|
+
>>> # Complete API representation
|
202
|
+
>>> context = IRContext(
|
203
|
+
... openapi_info=OpenAPIInfo(
|
204
|
+
... version="3.1.0",
|
205
|
+
... title="My Django API",
|
206
|
+
... api_version="1.0.0",
|
207
|
+
... ),
|
208
|
+
... django_metadata=DjangoGlobalMetadata(
|
209
|
+
... component_split_request=True,
|
210
|
+
... component_split_patch=True,
|
211
|
+
... ),
|
212
|
+
... schemas={
|
213
|
+
... "User": IRSchemaObject(name="User", type="object", is_response_model=True),
|
214
|
+
... "UserRequest": IRSchemaObject(name="UserRequest", type="object", is_request_model=True),
|
215
|
+
... "PatchedUser": IRSchemaObject(name="PatchedUser", type="object", is_patch_model=True),
|
216
|
+
... },
|
217
|
+
... operations={
|
218
|
+
... "users_list": IROperationObject(
|
219
|
+
... operation_id="users_list",
|
220
|
+
... http_method="GET",
|
221
|
+
... path="/api/users/",
|
222
|
+
... responses={200: IRResponseObject(status_code=200, schema_name="User")},
|
223
|
+
... ),
|
224
|
+
... "users_create": IROperationObject(
|
225
|
+
... operation_id="users_create",
|
226
|
+
... http_method="POST",
|
227
|
+
... path="/api/users/",
|
228
|
+
... request_body=IRRequestBodyObject(schema_name="UserRequest"),
|
229
|
+
... responses={201: IRResponseObject(status_code=201, schema_name="User")},
|
230
|
+
... ),
|
231
|
+
... },
|
232
|
+
... )
|
233
|
+
>>> assert context.has_request_response_split
|
234
|
+
>>> assert len(context.request_models) == 1
|
235
|
+
>>> assert len(context.response_models) == 1
|
236
|
+
"""
|
237
|
+
|
238
|
+
model_config = ConfigDict(
|
239
|
+
validate_assignment=True,
|
240
|
+
extra="forbid",
|
241
|
+
frozen=False,
|
242
|
+
validate_default=True,
|
243
|
+
str_strip_whitespace=True,
|
244
|
+
)
|
245
|
+
|
246
|
+
# ===== Metadata =====
|
247
|
+
openapi_info: OpenAPIInfo = Field(..., description="OpenAPI spec metadata")
|
248
|
+
django_metadata: DjangoGlobalMetadata = Field(
|
249
|
+
..., description="Django/drf-spectacular metadata"
|
250
|
+
)
|
251
|
+
|
252
|
+
# ===== Schemas =====
|
253
|
+
schemas: dict[str, IRSchemaObject] = Field(
|
254
|
+
default_factory=dict,
|
255
|
+
description="All schemas by name (User, UserRequest, PatchedUser, etc.)",
|
256
|
+
)
|
257
|
+
|
258
|
+
# ===== Operations =====
|
259
|
+
operations: dict[str, IROperationObject] = Field(
|
260
|
+
default_factory=dict,
|
261
|
+
description="All operations by operation_id (users_list, users_create, etc.)",
|
262
|
+
)
|
263
|
+
|
264
|
+
# ===== Computed Properties =====
|
265
|
+
|
266
|
+
@property
|
267
|
+
def has_request_response_split(self) -> bool:
|
268
|
+
"""
|
269
|
+
Check if API uses Request/Response split (COMPONENT_SPLIT_REQUEST: True).
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
True if any schema is marked as request model.
|
273
|
+
"""
|
274
|
+
return any(schema.is_request_model for schema in self.schemas.values())
|
275
|
+
|
276
|
+
@property
|
277
|
+
def request_models(self) -> dict[str, IRSchemaObject]:
|
278
|
+
"""Get all request models (UserRequest, TaskRequest, etc.)."""
|
279
|
+
return {
|
280
|
+
name: schema
|
281
|
+
for name, schema in self.schemas.items()
|
282
|
+
if schema.is_request_model
|
283
|
+
}
|
284
|
+
|
285
|
+
@property
|
286
|
+
def response_models(self) -> dict[str, IRSchemaObject]:
|
287
|
+
"""Get all response models (User, Task, etc.)."""
|
288
|
+
return {
|
289
|
+
name: schema
|
290
|
+
for name, schema in self.schemas.items()
|
291
|
+
if schema.is_response_model and not schema.is_patch_model and not schema.is_request_model
|
292
|
+
}
|
293
|
+
|
294
|
+
@property
|
295
|
+
def patch_models(self) -> dict[str, IRSchemaObject]:
|
296
|
+
"""Get all PATCH models (PatchedUser, PatchedTask, etc.)."""
|
297
|
+
return {
|
298
|
+
name: schema
|
299
|
+
for name, schema in self.schemas.items()
|
300
|
+
if schema.is_patch_model
|
301
|
+
}
|
302
|
+
|
303
|
+
@property
|
304
|
+
def enum_schemas(self) -> dict[str, IRSchemaObject]:
|
305
|
+
"""Get all schemas with x-enum-varnames support."""
|
306
|
+
return {
|
307
|
+
name: schema
|
308
|
+
for name, schema in self.schemas.items()
|
309
|
+
if schema.has_enum
|
310
|
+
}
|
311
|
+
|
312
|
+
@property
|
313
|
+
def operations_by_tag(self) -> dict[str, list[IROperationObject]]:
|
314
|
+
"""Group operations by tags."""
|
315
|
+
result: dict[str, list[IROperationObject]] = {}
|
316
|
+
for operation in self.operations.values():
|
317
|
+
for tag in operation.tags:
|
318
|
+
if tag not in result:
|
319
|
+
result[tag] = []
|
320
|
+
result[tag].append(operation)
|
321
|
+
return result
|
322
|
+
|
323
|
+
@property
|
324
|
+
def list_operations(self) -> dict[str, IROperationObject]:
|
325
|
+
"""Get all list operations (GET endpoints with pagination)."""
|
326
|
+
return {
|
327
|
+
op_id: op
|
328
|
+
for op_id, op in self.operations.items()
|
329
|
+
if op.is_list_operation
|
330
|
+
}
|
331
|
+
|
332
|
+
@property
|
333
|
+
def create_operations(self) -> dict[str, IROperationObject]:
|
334
|
+
"""Get all create operations (POST endpoints)."""
|
335
|
+
return {
|
336
|
+
op_id: op for op_id, op in self.operations.items() if op.is_create_operation
|
337
|
+
}
|
338
|
+
|
339
|
+
@property
|
340
|
+
def retrieve_operations(self) -> dict[str, IROperationObject]:
|
341
|
+
"""Get all retrieve operations (GET /{id}/ endpoints)."""
|
342
|
+
return {
|
343
|
+
op_id: op
|
344
|
+
for op_id, op in self.operations.items()
|
345
|
+
if op.is_retrieve_operation
|
346
|
+
}
|
347
|
+
|
348
|
+
@property
|
349
|
+
def update_operations(self) -> dict[str, IROperationObject]:
|
350
|
+
"""Get all update operations (PUT endpoints)."""
|
351
|
+
return {
|
352
|
+
op_id: op for op_id, op in self.operations.items() if op.is_update_operation
|
353
|
+
}
|
354
|
+
|
355
|
+
@property
|
356
|
+
def partial_update_operations(self) -> dict[str, IROperationObject]:
|
357
|
+
"""Get all partial update operations (PATCH endpoints)."""
|
358
|
+
return {
|
359
|
+
op_id: op
|
360
|
+
for op_id, op in self.operations.items()
|
361
|
+
if op.is_partial_update_operation
|
362
|
+
}
|
363
|
+
|
364
|
+
@property
|
365
|
+
def delete_operations(self) -> dict[str, IROperationObject]:
|
366
|
+
"""Get all delete operations (DELETE endpoints)."""
|
367
|
+
return {
|
368
|
+
op_id: op for op_id, op in self.operations.items() if op.is_delete_operation
|
369
|
+
}
|
370
|
+
|
371
|
+
def get_schema(self, name: str) -> IRSchemaObject | None:
|
372
|
+
"""Get schema by name."""
|
373
|
+
return self.schemas.get(name)
|
374
|
+
|
375
|
+
def get_operation(self, operation_id: str) -> IROperationObject | None:
|
376
|
+
"""Get operation by operation_id."""
|
377
|
+
return self.operations.get(operation_id)
|
378
|
+
|
379
|
+
def __repr__(self) -> str:
|
380
|
+
"""String representation for debugging."""
|
381
|
+
return (
|
382
|
+
f"IRContext("
|
383
|
+
f"schemas={len(self.schemas)}, "
|
384
|
+
f"operations={len(self.operations)}, "
|
385
|
+
f"request_models={len(self.request_models)}, "
|
386
|
+
f"response_models={len(self.response_models)})"
|
387
|
+
)
|