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,518 @@
|
|
1
|
+
"""
|
2
|
+
IR Operation Models - Type-safe API endpoint representation.
|
3
|
+
|
4
|
+
This module defines operations (endpoints) with Request/Response split awareness,
|
5
|
+
supporting Django-specific patterns from drf-spectacular.
|
6
|
+
|
7
|
+
Key Features:
|
8
|
+
- Separate request_body and patch_request_body (COMPONENT_SPLIT_PATCH)
|
9
|
+
- Multiple response schemas (200, 201, 400, 404, etc.)
|
10
|
+
- Path/query/header/cookie parameters
|
11
|
+
- Django metadata (authentication, permissions, csrf)
|
12
|
+
"""
|
13
|
+
|
14
|
+
from __future__ import annotations
|
15
|
+
|
16
|
+
from typing import Any, Literal
|
17
|
+
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field
|
19
|
+
|
20
|
+
|
21
|
+
class IRParameterObject(BaseModel):
|
22
|
+
"""
|
23
|
+
API parameter (path, query, header, cookie).
|
24
|
+
|
25
|
+
Examples:
|
26
|
+
>>> # Path parameter
|
27
|
+
>>> user_id = IRParameterObject(
|
28
|
+
... name="id",
|
29
|
+
... location="path",
|
30
|
+
... schema_type="integer",
|
31
|
+
... required=True,
|
32
|
+
... description="User ID",
|
33
|
+
... )
|
34
|
+
|
35
|
+
>>> # Query parameter with default
|
36
|
+
>>> page_size = IRParameterObject(
|
37
|
+
... name="page_size",
|
38
|
+
... location="query",
|
39
|
+
... schema_type="integer",
|
40
|
+
... required=False,
|
41
|
+
... default=20,
|
42
|
+
... description="Items per page",
|
43
|
+
... )
|
44
|
+
"""
|
45
|
+
|
46
|
+
model_config = ConfigDict(
|
47
|
+
validate_assignment=True,
|
48
|
+
extra="forbid",
|
49
|
+
frozen=False,
|
50
|
+
validate_default=True,
|
51
|
+
str_strip_whitespace=True,
|
52
|
+
)
|
53
|
+
|
54
|
+
name: str = Field(..., description="Parameter name (e.g., 'id', 'page_size')")
|
55
|
+
location: Literal["path", "query", "header", "cookie"] = Field(
|
56
|
+
..., description="Parameter location"
|
57
|
+
)
|
58
|
+
schema_type: str = Field(
|
59
|
+
...,
|
60
|
+
description="Parameter type: string, integer, number, boolean, array",
|
61
|
+
)
|
62
|
+
required: bool = Field(False, description="Is parameter required")
|
63
|
+
description: str | None = Field(None, description="Parameter description")
|
64
|
+
|
65
|
+
# Validation
|
66
|
+
default: Any | None = Field(None, description="Default value")
|
67
|
+
enum: list[str | int | float] | None = Field(
|
68
|
+
None, description="Allowed values"
|
69
|
+
)
|
70
|
+
pattern: str | None = Field(None, description="Regex pattern (for strings)")
|
71
|
+
min_length: int | None = Field(None, ge=0, description="Minimum string length")
|
72
|
+
max_length: int | None = Field(None, ge=0, description="Maximum string length")
|
73
|
+
minimum: int | float | None = Field(None, description="Minimum numeric value")
|
74
|
+
maximum: int | float | None = Field(None, description="Maximum numeric value")
|
75
|
+
|
76
|
+
# Array support
|
77
|
+
items_type: str | None = Field(
|
78
|
+
None,
|
79
|
+
description="Array item type (for schema_type='array')",
|
80
|
+
)
|
81
|
+
|
82
|
+
deprecated: bool = Field(False, description="Is parameter deprecated")
|
83
|
+
|
84
|
+
@property
|
85
|
+
def python_type(self) -> str:
|
86
|
+
"""
|
87
|
+
Get Python type hint for this parameter.
|
88
|
+
|
89
|
+
Examples:
|
90
|
+
>>> IRParameterObject(name="id", location="path", schema_type="integer", required=True).python_type
|
91
|
+
'int'
|
92
|
+
>>> IRParameterObject(name="tags", location="query", schema_type="array", items_type="string", required=False).python_type
|
93
|
+
'list[str] | None'
|
94
|
+
"""
|
95
|
+
type_map = {
|
96
|
+
"string": "str",
|
97
|
+
"integer": "int",
|
98
|
+
"number": "float",
|
99
|
+
"boolean": "bool",
|
100
|
+
"array": f"list[{self.items_type or 'Any'}]",
|
101
|
+
}
|
102
|
+
|
103
|
+
base_type = type_map.get(self.schema_type, "Any")
|
104
|
+
|
105
|
+
if not self.required:
|
106
|
+
return f"{base_type} | None"
|
107
|
+
|
108
|
+
return base_type
|
109
|
+
|
110
|
+
def __repr__(self) -> str:
|
111
|
+
"""String representation for debugging."""
|
112
|
+
parts = [
|
113
|
+
f"IRParameterObject(name={self.name!r}",
|
114
|
+
f"location={self.location!r}",
|
115
|
+
f"schema_type={self.schema_type!r}",
|
116
|
+
]
|
117
|
+
|
118
|
+
if self.required:
|
119
|
+
parts.append("required=True")
|
120
|
+
|
121
|
+
if self.default is not None:
|
122
|
+
parts.append(f"default={self.default!r}")
|
123
|
+
|
124
|
+
return ", ".join(parts) + ")"
|
125
|
+
|
126
|
+
|
127
|
+
class IRRequestBodyObject(BaseModel):
|
128
|
+
"""
|
129
|
+
Request body schema reference.
|
130
|
+
|
131
|
+
Examples:
|
132
|
+
>>> # POST /users/ (UserRequest)
|
133
|
+
>>> post_body = IRRequestBodyObject(
|
134
|
+
... schema_name="UserRequest",
|
135
|
+
... content_type="application/json",
|
136
|
+
... required=True,
|
137
|
+
... )
|
138
|
+
|
139
|
+
>>> # PATCH /users/{id}/ (PatchedUser)
|
140
|
+
>>> patch_body = IRRequestBodyObject(
|
141
|
+
... schema_name="PatchedUser",
|
142
|
+
... content_type="application/json",
|
143
|
+
... required=False,
|
144
|
+
... )
|
145
|
+
"""
|
146
|
+
|
147
|
+
model_config = ConfigDict(
|
148
|
+
validate_assignment=True,
|
149
|
+
extra="forbid",
|
150
|
+
frozen=False,
|
151
|
+
validate_default=True,
|
152
|
+
str_strip_whitespace=True,
|
153
|
+
)
|
154
|
+
|
155
|
+
schema_name: str = Field(
|
156
|
+
..., description="Schema reference (e.g., 'UserRequest', 'PatchedUser')"
|
157
|
+
)
|
158
|
+
content_type: str = Field(
|
159
|
+
"application/json",
|
160
|
+
description="Content type (application/json, multipart/form-data, etc.)",
|
161
|
+
)
|
162
|
+
required: bool = Field(True, description="Is request body required")
|
163
|
+
description: str | None = Field(None, description="Request body description")
|
164
|
+
|
165
|
+
def __repr__(self) -> str:
|
166
|
+
"""String representation for debugging."""
|
167
|
+
parts = [
|
168
|
+
f"IRRequestBodyObject(schema_name={self.schema_name!r}",
|
169
|
+
f"content_type={self.content_type!r}",
|
170
|
+
]
|
171
|
+
|
172
|
+
if not self.required:
|
173
|
+
parts.append("required=False")
|
174
|
+
|
175
|
+
return ", ".join(parts) + ")"
|
176
|
+
|
177
|
+
|
178
|
+
class IRResponseObject(BaseModel):
|
179
|
+
"""
|
180
|
+
Response schema with status code.
|
181
|
+
|
182
|
+
Examples:
|
183
|
+
>>> # 200 OK (User)
|
184
|
+
>>> success = IRResponseObject(
|
185
|
+
... status_code=200,
|
186
|
+
... schema_name="User",
|
187
|
+
... description="User retrieved successfully",
|
188
|
+
... )
|
189
|
+
|
190
|
+
>>> # 201 Created (User)
|
191
|
+
>>> created = IRResponseObject(
|
192
|
+
... status_code=201,
|
193
|
+
... schema_name="User",
|
194
|
+
... description="User created successfully",
|
195
|
+
... )
|
196
|
+
|
197
|
+
>>> # 400 Bad Request (ValidationError)
|
198
|
+
>>> error = IRResponseObject(
|
199
|
+
... status_code=400,
|
200
|
+
... schema_name="ValidationError",
|
201
|
+
... description="Invalid request data",
|
202
|
+
... )
|
203
|
+
|
204
|
+
>>> # 204 No Content (no schema)
|
205
|
+
>>> no_content = IRResponseObject(
|
206
|
+
... status_code=204,
|
207
|
+
... schema_name=None,
|
208
|
+
... description="Deleted successfully",
|
209
|
+
... )
|
210
|
+
"""
|
211
|
+
|
212
|
+
model_config = ConfigDict(
|
213
|
+
validate_assignment=True,
|
214
|
+
extra="forbid",
|
215
|
+
frozen=False,
|
216
|
+
validate_default=True,
|
217
|
+
str_strip_whitespace=True,
|
218
|
+
)
|
219
|
+
|
220
|
+
status_code: int = Field(
|
221
|
+
..., ge=100, le=599, description="HTTP status code (200, 201, 400, etc.)"
|
222
|
+
)
|
223
|
+
schema_name: str | None = Field(
|
224
|
+
None, description="Response schema name (e.g., 'User', 'ValidationError')"
|
225
|
+
)
|
226
|
+
content_type: str = Field(
|
227
|
+
"application/json",
|
228
|
+
description="Response content type",
|
229
|
+
)
|
230
|
+
description: str | None = Field(None, description="Response description")
|
231
|
+
|
232
|
+
# Pagination (for list endpoints)
|
233
|
+
is_paginated: bool = Field(
|
234
|
+
False, description="Is response paginated (PageNumberPagination)"
|
235
|
+
)
|
236
|
+
|
237
|
+
@property
|
238
|
+
def is_success(self) -> bool:
|
239
|
+
"""Check if response is successful (2xx)."""
|
240
|
+
return 200 <= self.status_code < 300
|
241
|
+
|
242
|
+
@property
|
243
|
+
def is_error(self) -> bool:
|
244
|
+
"""Check if response is error (4xx, 5xx)."""
|
245
|
+
return self.status_code >= 400
|
246
|
+
|
247
|
+
def __repr__(self) -> str:
|
248
|
+
"""String representation for debugging."""
|
249
|
+
parts = [
|
250
|
+
f"IRResponseObject(status_code={self.status_code}",
|
251
|
+
]
|
252
|
+
|
253
|
+
if self.schema_name:
|
254
|
+
parts.append(f"schema_name={self.schema_name!r}")
|
255
|
+
|
256
|
+
if self.is_paginated:
|
257
|
+
parts.append("is_paginated=True")
|
258
|
+
|
259
|
+
return ", ".join(parts) + ")"
|
260
|
+
|
261
|
+
|
262
|
+
class IROperationObject(BaseModel):
|
263
|
+
"""
|
264
|
+
API operation (endpoint) with Request/Response split awareness.
|
265
|
+
|
266
|
+
This model represents a single API endpoint with full metadata:
|
267
|
+
- HTTP method (GET, POST, PUT, PATCH, DELETE)
|
268
|
+
- Path and parameters
|
269
|
+
- Request body (with separate patch_request_body for PATCH)
|
270
|
+
- Multiple responses (200, 201, 400, 404, etc.)
|
271
|
+
- Django metadata (authentication, permissions)
|
272
|
+
|
273
|
+
Examples:
|
274
|
+
>>> # POST /users/ - Create user
|
275
|
+
>>> create_user = IROperationObject(
|
276
|
+
... operation_id="users_create",
|
277
|
+
... http_method="POST",
|
278
|
+
... path="/api/users/",
|
279
|
+
... summary="Create new user",
|
280
|
+
... request_body=IRRequestBodyObject(schema_name="UserRequest"),
|
281
|
+
... responses={
|
282
|
+
... 201: IRResponseObject(status_code=201, schema_name="User"),
|
283
|
+
... 400: IRResponseObject(status_code=400, schema_name="ValidationError"),
|
284
|
+
... },
|
285
|
+
... tags=["users"],
|
286
|
+
... )
|
287
|
+
|
288
|
+
>>> # PATCH /users/{id}/ - Partial update
|
289
|
+
>>> partial_update = IROperationObject(
|
290
|
+
... operation_id="users_partial_update",
|
291
|
+
... http_method="PATCH",
|
292
|
+
... path="/api/users/{id}/",
|
293
|
+
... summary="Partial update user",
|
294
|
+
... parameters=[
|
295
|
+
... IRParameterObject(name="id", location="path", schema_type="integer", required=True),
|
296
|
+
... ],
|
297
|
+
... patch_request_body=IRRequestBodyObject(schema_name="PatchedUser", required=False),
|
298
|
+
... responses={
|
299
|
+
... 200: IRResponseObject(status_code=200, schema_name="User"),
|
300
|
+
... 404: IRResponseObject(status_code=404, schema_name="Error"),
|
301
|
+
... },
|
302
|
+
... tags=["users"],
|
303
|
+
... )
|
304
|
+
|
305
|
+
>>> # GET /users/ - List users (paginated)
|
306
|
+
>>> list_users = IROperationObject(
|
307
|
+
... operation_id="users_list",
|
308
|
+
... http_method="GET",
|
309
|
+
... path="/api/users/",
|
310
|
+
... summary="List users",
|
311
|
+
... parameters=[
|
312
|
+
... IRParameterObject(name="page", location="query", schema_type="integer", required=False),
|
313
|
+
... IRParameterObject(name="page_size", location="query", schema_type="integer", required=False),
|
314
|
+
... ],
|
315
|
+
... responses={
|
316
|
+
... 200: IRResponseObject(status_code=200, schema_name="PaginatedUserList", is_paginated=True),
|
317
|
+
... },
|
318
|
+
... tags=["users"],
|
319
|
+
... )
|
320
|
+
"""
|
321
|
+
|
322
|
+
model_config = ConfigDict(
|
323
|
+
validate_assignment=True,
|
324
|
+
extra="forbid",
|
325
|
+
frozen=False,
|
326
|
+
validate_default=True,
|
327
|
+
str_strip_whitespace=True,
|
328
|
+
)
|
329
|
+
|
330
|
+
# ===== Core Fields =====
|
331
|
+
operation_id: str = Field(
|
332
|
+
...,
|
333
|
+
description="Unique operation ID (e.g., 'users_create', 'tasks_partial_update')",
|
334
|
+
)
|
335
|
+
http_method: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] = (
|
336
|
+
Field(..., description="HTTP method")
|
337
|
+
)
|
338
|
+
path: str = Field(..., description="URL path (e.g., '/api/users/{id}/')")
|
339
|
+
summary: str | None = Field(None, description="Operation summary")
|
340
|
+
description: str | None = Field(None, description="Detailed description")
|
341
|
+
tags: list[str] = Field(
|
342
|
+
default_factory=list, description="OpenAPI tags (e.g., ['users', 'auth'])"
|
343
|
+
)
|
344
|
+
|
345
|
+
# ===== Parameters =====
|
346
|
+
parameters: list[IRParameterObject] = Field(
|
347
|
+
default_factory=list,
|
348
|
+
description="Path/query/header parameters",
|
349
|
+
)
|
350
|
+
|
351
|
+
# ===== Request Bodies (NEW: Separate POST/PUT vs PATCH) =====
|
352
|
+
request_body: IRRequestBodyObject | None = Field(
|
353
|
+
None,
|
354
|
+
description="Request body for POST/PUT (e.g., UserRequest)",
|
355
|
+
)
|
356
|
+
patch_request_body: IRRequestBodyObject | None = Field(
|
357
|
+
None,
|
358
|
+
description="Request body for PATCH (e.g., PatchedUser with all optional fields)",
|
359
|
+
)
|
360
|
+
|
361
|
+
# ===== Responses =====
|
362
|
+
responses: dict[int, IRResponseObject] = Field(
|
363
|
+
...,
|
364
|
+
description="Responses by status code (200, 201, 400, 404, etc.)",
|
365
|
+
)
|
366
|
+
|
367
|
+
# ===== Django Metadata =====
|
368
|
+
authentication_classes: list[str] = Field(
|
369
|
+
default_factory=list,
|
370
|
+
description="Django authentication classes (e.g., ['SessionAuthentication', 'TokenAuthentication'])",
|
371
|
+
)
|
372
|
+
permission_classes: list[str] = Field(
|
373
|
+
default_factory=list,
|
374
|
+
description="Django permission classes (e.g., ['IsAuthenticated', 'IsAdminUser'])",
|
375
|
+
)
|
376
|
+
csrf_exempt: bool = Field(
|
377
|
+
False, description="Is operation CSRF exempt (@csrf_exempt)"
|
378
|
+
)
|
379
|
+
|
380
|
+
# ===== Metadata =====
|
381
|
+
deprecated: bool = Field(False, description="Is operation deprecated")
|
382
|
+
|
383
|
+
# ===== Computed Properties =====
|
384
|
+
|
385
|
+
@property
|
386
|
+
def is_list_operation(self) -> bool:
|
387
|
+
"""Check if operation is list endpoint (GET with pagination)."""
|
388
|
+
if self.http_method != "GET":
|
389
|
+
return False
|
390
|
+
|
391
|
+
# Check if primary success response is paginated
|
392
|
+
success_response = self.responses.get(200)
|
393
|
+
return success_response is not None and success_response.is_paginated
|
394
|
+
|
395
|
+
@property
|
396
|
+
def is_retrieve_operation(self) -> bool:
|
397
|
+
"""Check if operation is retrieve endpoint (GET /{id}/)."""
|
398
|
+
return (
|
399
|
+
self.http_method == "GET"
|
400
|
+
and "{id}" in self.path
|
401
|
+
and not self.is_list_operation
|
402
|
+
)
|
403
|
+
|
404
|
+
@property
|
405
|
+
def is_create_operation(self) -> bool:
|
406
|
+
"""Check if operation is create endpoint (POST)."""
|
407
|
+
return self.http_method == "POST"
|
408
|
+
|
409
|
+
@property
|
410
|
+
def is_update_operation(self) -> bool:
|
411
|
+
"""Check if operation is update endpoint (PUT)."""
|
412
|
+
return self.http_method == "PUT"
|
413
|
+
|
414
|
+
@property
|
415
|
+
def is_partial_update_operation(self) -> bool:
|
416
|
+
"""Check if operation is partial update endpoint (PATCH)."""
|
417
|
+
return self.http_method == "PATCH"
|
418
|
+
|
419
|
+
@property
|
420
|
+
def is_delete_operation(self) -> bool:
|
421
|
+
"""Check if operation is delete endpoint (DELETE)."""
|
422
|
+
return self.http_method == "DELETE"
|
423
|
+
|
424
|
+
@property
|
425
|
+
def requires_authentication(self) -> bool:
|
426
|
+
"""Check if operation requires authentication."""
|
427
|
+
return bool(self.authentication_classes)
|
428
|
+
|
429
|
+
@property
|
430
|
+
def path_parameters(self) -> list[IRParameterObject]:
|
431
|
+
"""Get path parameters only."""
|
432
|
+
return [p for p in self.parameters if p.location == "path"]
|
433
|
+
|
434
|
+
@property
|
435
|
+
def query_parameters(self) -> list[IRParameterObject]:
|
436
|
+
"""Get query parameters only."""
|
437
|
+
return [p for p in self.parameters if p.location == "query"]
|
438
|
+
|
439
|
+
@property
|
440
|
+
def header_parameters(self) -> list[IRParameterObject]:
|
441
|
+
"""Get header parameters only."""
|
442
|
+
return [p for p in self.parameters if p.location == "header"]
|
443
|
+
|
444
|
+
@property
|
445
|
+
def success_responses(self) -> dict[int, IRResponseObject]:
|
446
|
+
"""Get successful responses only (2xx)."""
|
447
|
+
return {
|
448
|
+
status: response
|
449
|
+
for status, response in self.responses.items()
|
450
|
+
if response.is_success
|
451
|
+
}
|
452
|
+
|
453
|
+
@property
|
454
|
+
def error_responses(self) -> dict[int, IRResponseObject]:
|
455
|
+
"""Get error responses only (4xx, 5xx)."""
|
456
|
+
return {
|
457
|
+
status: response
|
458
|
+
for status, response in self.responses.items()
|
459
|
+
if response.is_error
|
460
|
+
}
|
461
|
+
|
462
|
+
@property
|
463
|
+
def primary_success_status(self) -> int:
|
464
|
+
"""
|
465
|
+
Get primary success status code.
|
466
|
+
|
467
|
+
Returns:
|
468
|
+
Primary success status (200 for GET/PUT/PATCH, 201 for POST, 204 for DELETE).
|
469
|
+
|
470
|
+
Examples:
|
471
|
+
>>> get_op = IROperationObject(operation_id="users_list", http_method="GET", path="/api/users/", responses={200: IRResponseObject(status_code=200)})
|
472
|
+
>>> get_op.primary_success_status
|
473
|
+
200
|
474
|
+
|
475
|
+
>>> post_op = IROperationObject(operation_id="users_create", http_method="POST", path="/api/users/", responses={201: IRResponseObject(status_code=201)})
|
476
|
+
>>> post_op.primary_success_status
|
477
|
+
201
|
478
|
+
"""
|
479
|
+
success = self.success_responses
|
480
|
+
if not success:
|
481
|
+
return 200
|
482
|
+
|
483
|
+
# Prefer 201 for POST, 204 for DELETE, otherwise first 2xx
|
484
|
+
if 201 in success:
|
485
|
+
return 201
|
486
|
+
if 204 in success:
|
487
|
+
return 204
|
488
|
+
if 200 in success:
|
489
|
+
return 200
|
490
|
+
|
491
|
+
return min(success.keys())
|
492
|
+
|
493
|
+
@property
|
494
|
+
def primary_success_response(self) -> IRResponseObject | None:
|
495
|
+
"""Get primary success response object."""
|
496
|
+
return self.responses.get(self.primary_success_status)
|
497
|
+
|
498
|
+
def __repr__(self) -> str:
|
499
|
+
"""String representation for debugging."""
|
500
|
+
parts = [
|
501
|
+
f"IROperationObject(operation_id={self.operation_id!r}",
|
502
|
+
f"http_method={self.http_method!r}",
|
503
|
+
f"path={self.path!r}",
|
504
|
+
]
|
505
|
+
|
506
|
+
if self.request_body:
|
507
|
+
parts.append(
|
508
|
+
f"request_body={self.request_body.schema_name!r}"
|
509
|
+
)
|
510
|
+
|
511
|
+
if self.patch_request_body:
|
512
|
+
parts.append(
|
513
|
+
f"patch_request_body={self.patch_request_body.schema_name!r}"
|
514
|
+
)
|
515
|
+
|
516
|
+
parts.append(f"responses=[{', '.join(map(str, self.responses.keys()))}]")
|
517
|
+
|
518
|
+
return ", ".join(parts) + ")"
|