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.
Files changed (181) hide show
  1. django_cfg/apps/agents/management/commands/create_agent.py +1 -1
  2. django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
  3. django_cfg/apps/newsletter/serializers.py +40 -3
  4. django_cfg/apps/newsletter/views/campaigns.py +12 -3
  5. django_cfg/apps/newsletter/views/emails.py +14 -3
  6. django_cfg/apps/newsletter/views/subscriptions.py +12 -2
  7. django_cfg/apps/payments/views/api/currencies.py +49 -6
  8. django_cfg/apps/payments/views/api/webhooks.py +72 -7
  9. django_cfg/apps/payments/views/overview/serializers.py +34 -1
  10. django_cfg/apps/payments/views/overview/views.py +2 -1
  11. django_cfg/apps/payments/views/serializers/payments.py +6 -6
  12. django_cfg/apps/urls.py +106 -45
  13. django_cfg/core/base/config_model.py +2 -2
  14. django_cfg/core/constants.py +1 -1
  15. django_cfg/core/generation/integration_generators/__init__.py +1 -1
  16. django_cfg/core/generation/integration_generators/api.py +72 -49
  17. django_cfg/core/integration/display/startup.py +30 -22
  18. django_cfg/core/integration/url_integration.py +15 -16
  19. django_cfg/dashboard/sections/documentation.py +391 -0
  20. django_cfg/management/commands/check_endpoints.py +11 -160
  21. django_cfg/management/commands/check_settings.py +13 -348
  22. django_cfg/management/commands/clear_constance.py +13 -201
  23. django_cfg/management/commands/create_token.py +13 -321
  24. django_cfg/management/commands/generate_clients.py +23 -0
  25. django_cfg/management/commands/list_urls.py +13 -306
  26. django_cfg/management/commands/migrate_all.py +13 -126
  27. django_cfg/management/commands/migrator.py +13 -396
  28. django_cfg/management/commands/rundramatiq.py +15 -247
  29. django_cfg/management/commands/rundramatiq_simulator.py +12 -429
  30. django_cfg/management/commands/runserver_ngrok.py +15 -160
  31. django_cfg/management/commands/script.py +12 -488
  32. django_cfg/management/commands/show_config.py +12 -215
  33. django_cfg/management/commands/show_urls.py +12 -342
  34. django_cfg/management/commands/superuser.py +15 -295
  35. django_cfg/management/commands/task_clear.py +14 -217
  36. django_cfg/management/commands/task_status.py +13 -248
  37. django_cfg/management/commands/test_email.py +15 -86
  38. django_cfg/management/commands/test_telegram.py +14 -61
  39. django_cfg/management/commands/test_twilio.py +15 -105
  40. django_cfg/management/commands/tree.py +13 -383
  41. django_cfg/management/commands/validate_openapi.py +10 -0
  42. django_cfg/middleware/README.md +1 -1
  43. django_cfg/middleware/user_activity.py +3 -3
  44. django_cfg/models/__init__.py +2 -2
  45. django_cfg/models/api/drf/spectacular.py +6 -6
  46. django_cfg/models/django/__init__.py +2 -2
  47. django_cfg/models/django/openapi.py +238 -0
  48. django_cfg/modules/django_admin/management/__init__.py +0 -0
  49. django_cfg/modules/django_admin/management/commands/__init__.py +0 -0
  50. django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
  51. django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
  52. django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
  53. django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
  54. django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
  55. django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
  56. django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
  57. django_cfg/modules/django_admin/management/commands/script.py +496 -0
  58. django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
  59. django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
  60. django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
  61. django_cfg/modules/django_admin/management/commands/tree.py +390 -0
  62. django_cfg/modules/django_client/__init__.py +20 -0
  63. django_cfg/modules/django_client/apps.py +35 -0
  64. django_cfg/modules/django_client/core/__init__.py +56 -0
  65. django_cfg/modules/django_client/core/archive/__init__.py +11 -0
  66. django_cfg/modules/django_client/core/archive/manager.py +134 -0
  67. django_cfg/modules/django_client/core/cli/__init__.py +12 -0
  68. django_cfg/modules/django_client/core/cli/main.py +235 -0
  69. django_cfg/modules/django_client/core/config/__init__.py +18 -0
  70. django_cfg/modules/django_client/core/config/config.py +188 -0
  71. django_cfg/modules/django_client/core/config/group.py +101 -0
  72. django_cfg/modules/django_client/core/config/service.py +209 -0
  73. django_cfg/modules/django_client/core/generator/__init__.py +115 -0
  74. django_cfg/modules/django_client/core/generator/base.py +767 -0
  75. django_cfg/modules/django_client/core/generator/python.py +751 -0
  76. django_cfg/modules/django_client/core/generator/templates/python/__init__.py.jinja +9 -0
  77. django_cfg/modules/django_client/core/generator/templates/python/api_wrapper.py.jinja +130 -0
  78. django_cfg/modules/django_client/core/generator/templates/python/app_init.py.jinja +6 -0
  79. django_cfg/modules/django_client/core/generator/templates/python/client/app_client.py.jinja +18 -0
  80. django_cfg/modules/django_client/core/generator/templates/python/client/flat_client.py.jinja +38 -0
  81. django_cfg/modules/django_client/core/generator/templates/python/client/main_client.py.jinja +50 -0
  82. django_cfg/modules/django_client/core/generator/templates/python/client/main_client_file.py.jinja +13 -0
  83. django_cfg/modules/django_client/core/generator/templates/python/client/operation_method.py.jinja +7 -0
  84. django_cfg/modules/django_client/core/generator/templates/python/client/sub_client.py.jinja +11 -0
  85. django_cfg/modules/django_client/core/generator/templates/python/client_file.py.jinja +13 -0
  86. django_cfg/modules/django_client/core/generator/templates/python/main_init.py.jinja +50 -0
  87. django_cfg/modules/django_client/core/generator/templates/python/models/app_models.py.jinja +17 -0
  88. django_cfg/modules/django_client/core/generator/templates/python/models/enum_class.py.jinja +15 -0
  89. django_cfg/modules/django_client/core/generator/templates/python/models/enums.py.jinja +8 -0
  90. django_cfg/modules/django_client/core/generator/templates/python/models/models.py.jinja +17 -0
  91. django_cfg/modules/django_client/core/generator/templates/python/models/schema_class.py.jinja +19 -0
  92. django_cfg/modules/django_client/core/generator/templates/python/utils/logger.py.jinja +255 -0
  93. django_cfg/modules/django_client/core/generator/templates/python/utils/schema.py.jinja +12 -0
  94. django_cfg/modules/django_client/core/generator/templates/typescript/app_index.ts.jinja +2 -0
  95. django_cfg/modules/django_client/core/generator/templates/typescript/client/app_client.ts.jinja +18 -0
  96. django_cfg/modules/django_client/core/generator/templates/typescript/client/client.ts.jinja +327 -0
  97. django_cfg/modules/django_client/core/generator/templates/typescript/client/flat_client.ts.jinja +109 -0
  98. django_cfg/modules/django_client/core/generator/templates/typescript/client/main_client_file.ts.jinja +9 -0
  99. django_cfg/modules/django_client/core/generator/templates/typescript/client/operation.ts.jinja +61 -0
  100. django_cfg/modules/django_client/core/generator/templates/typescript/client/sub_client.ts.jinja +15 -0
  101. django_cfg/modules/django_client/core/generator/templates/typescript/client_file.ts.jinja +9 -0
  102. django_cfg/modules/django_client/core/generator/templates/typescript/index.ts.jinja +5 -0
  103. django_cfg/modules/django_client/core/generator/templates/typescript/main_index.ts.jinja +206 -0
  104. django_cfg/modules/django_client/core/generator/templates/typescript/models/app_models.ts.jinja +8 -0
  105. django_cfg/modules/django_client/core/generator/templates/typescript/models/enums.ts.jinja +4 -0
  106. django_cfg/modules/django_client/core/generator/templates/typescript/models/models.ts.jinja +8 -0
  107. django_cfg/modules/django_client/core/generator/templates/typescript/utils/errors.ts.jinja +114 -0
  108. django_cfg/modules/django_client/core/generator/templates/typescript/utils/http.ts.jinja +98 -0
  109. django_cfg/modules/django_client/core/generator/templates/typescript/utils/logger.ts.jinja +251 -0
  110. django_cfg/modules/django_client/core/generator/templates/typescript/utils/schema.ts.jinja +7 -0
  111. django_cfg/modules/django_client/core/generator/templates/typescript/utils/storage.ts.jinja +114 -0
  112. django_cfg/modules/django_client/core/generator/typescript.py +872 -0
  113. django_cfg/modules/django_client/core/groups/__init__.py +13 -0
  114. django_cfg/modules/django_client/core/groups/detector.py +178 -0
  115. django_cfg/modules/django_client/core/groups/manager.py +314 -0
  116. django_cfg/modules/django_client/core/ir/__init__.py +57 -0
  117. django_cfg/modules/django_client/core/ir/context.py +387 -0
  118. django_cfg/modules/django_client/core/ir/operation.py +518 -0
  119. django_cfg/modules/django_client/core/ir/schema.py +353 -0
  120. django_cfg/modules/django_client/core/parser/__init__.py +74 -0
  121. django_cfg/modules/django_client/core/parser/base.py +648 -0
  122. django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
  123. django_cfg/modules/django_client/core/parser/models/base.py +212 -0
  124. django_cfg/modules/django_client/core/parser/models/components.py +160 -0
  125. django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
  126. django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
  127. django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
  128. django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
  129. django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
  130. django_cfg/modules/django_client/core/validation/__init__.py +22 -0
  131. django_cfg/modules/django_client/core/validation/checker.py +134 -0
  132. django_cfg/modules/django_client/core/validation/fixer.py +216 -0
  133. django_cfg/modules/django_client/core/validation/reporter.py +480 -0
  134. django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
  135. django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
  136. django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
  137. django_cfg/modules/django_client/core/validation/safety.py +266 -0
  138. django_cfg/modules/django_client/management/__init__.py +3 -0
  139. django_cfg/modules/django_client/management/commands/__init__.py +3 -0
  140. django_cfg/modules/django_client/management/commands/generate_client.py +422 -0
  141. django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
  142. django_cfg/modules/django_client/spectacular/__init__.py +9 -0
  143. django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
  144. django_cfg/modules/django_client/urls.py +72 -0
  145. django_cfg/modules/django_email/management/__init__.py +0 -0
  146. django_cfg/modules/django_email/management/commands/__init__.py +0 -0
  147. django_cfg/modules/django_email/management/commands/test_email.py +93 -0
  148. django_cfg/modules/django_logging/django_logger.py +6 -6
  149. django_cfg/modules/django_ngrok/management/__init__.py +0 -0
  150. django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
  151. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
  152. django_cfg/modules/django_tasks/management/__init__.py +0 -0
  153. django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
  154. django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
  155. django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
  156. django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
  157. django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
  158. django_cfg/modules/django_telegram/management/__init__.py +0 -0
  159. django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
  160. django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
  161. django_cfg/modules/django_twilio/management/__init__.py +0 -0
  162. django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
  163. django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
  164. django_cfg/modules/django_unfold/callbacks/main.py +16 -5
  165. django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
  166. django_cfg/pyproject.toml +2 -6
  167. django_cfg/registry/third_party.py +5 -7
  168. django_cfg/routing/callbacks.py +1 -1
  169. django_cfg/static/admin/css/prose-unfold.css +666 -0
  170. django_cfg/templates/admin/index.html +8 -0
  171. django_cfg/templates/admin/index_new.html +13 -0
  172. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
  173. django_cfg/templates/admin/sections/documentation_section.html +172 -0
  174. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
  175. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/METADATA +2 -2
  176. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/RECORD +180 -59
  177. django_cfg/management/commands/generate.py +0 -107
  178. /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
  179. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/WHEEL +0 -0
  180. {django_cfg-1.4.10.dist-info → django_cfg-1.4.11.dist-info}/entry_points.txt +0 -0
  181. {django_cfg-1.4.10.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
+ )