django-cfg 1.4.10__py3-none-any.whl → 1.4.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. django_cfg/apps/agents/management/commands/create_agent.py +1 -1
  2. django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
  3. django_cfg/apps/newsletter/serializers.py +40 -3
  4. django_cfg/apps/newsletter/views/campaigns.py +12 -3
  5. django_cfg/apps/newsletter/views/emails.py +14 -3
  6. django_cfg/apps/newsletter/views/subscriptions.py +12 -2
  7. django_cfg/apps/payments/views/api/currencies.py +49 -6
  8. django_cfg/apps/payments/views/api/webhooks.py +72 -7
  9. django_cfg/apps/payments/views/overview/serializers.py +34 -1
  10. django_cfg/apps/payments/views/overview/views.py +2 -1
  11. django_cfg/apps/payments/views/serializers/payments.py +6 -6
  12. django_cfg/apps/urls.py +106 -45
  13. django_cfg/core/base/config_model.py +2 -2
  14. django_cfg/core/constants.py +1 -1
  15. django_cfg/core/generation/integration_generators/__init__.py +1 -1
  16. django_cfg/core/generation/integration_generators/api.py +73 -49
  17. django_cfg/core/integration/display/startup.py +30 -22
  18. django_cfg/core/integration/url_integration.py +15 -16
  19. django_cfg/management/commands/check_endpoints.py +11 -160
  20. django_cfg/management/commands/check_settings.py +13 -348
  21. django_cfg/management/commands/clear_constance.py +13 -201
  22. django_cfg/management/commands/create_token.py +13 -321
  23. django_cfg/management/commands/generate_clients.py +23 -0
  24. django_cfg/management/commands/list_urls.py +13 -306
  25. django_cfg/management/commands/migrate_all.py +13 -126
  26. django_cfg/management/commands/migrator.py +13 -396
  27. django_cfg/management/commands/rundramatiq.py +15 -247
  28. django_cfg/management/commands/rundramatiq_simulator.py +12 -429
  29. django_cfg/management/commands/runserver_ngrok.py +15 -160
  30. django_cfg/management/commands/script.py +12 -488
  31. django_cfg/management/commands/show_config.py +12 -215
  32. django_cfg/management/commands/show_urls.py +12 -342
  33. django_cfg/management/commands/superuser.py +15 -295
  34. django_cfg/management/commands/task_clear.py +14 -217
  35. django_cfg/management/commands/task_status.py +13 -248
  36. django_cfg/management/commands/test_email.py +15 -86
  37. django_cfg/management/commands/test_telegram.py +14 -61
  38. django_cfg/management/commands/test_twilio.py +15 -105
  39. django_cfg/management/commands/tree.py +13 -383
  40. django_cfg/management/commands/validate_openapi.py +10 -0
  41. django_cfg/middleware/README.md +1 -1
  42. django_cfg/middleware/user_activity.py +3 -3
  43. django_cfg/models/__init__.py +2 -2
  44. django_cfg/models/api/drf/spectacular.py +6 -6
  45. django_cfg/models/django/__init__.py +2 -2
  46. django_cfg/models/django/openapi.py +162 -0
  47. django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
  48. django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
  49. django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
  50. django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
  51. django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
  52. django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
  53. django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
  54. django_cfg/modules/django_admin/management/commands/script.py +496 -0
  55. django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
  56. django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
  57. django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
  58. django_cfg/modules/django_admin/management/commands/tree.py +390 -0
  59. django_cfg/modules/django_client/__init__.py +20 -0
  60. django_cfg/modules/django_client/apps.py +35 -0
  61. django_cfg/modules/django_client/core/__init__.py +56 -0
  62. django_cfg/modules/django_client/core/archive/__init__.py +11 -0
  63. django_cfg/modules/django_client/core/archive/manager.py +134 -0
  64. django_cfg/modules/django_client/core/cli/__init__.py +12 -0
  65. django_cfg/modules/django_client/core/cli/main.py +235 -0
  66. django_cfg/modules/django_client/core/config/__init__.py +18 -0
  67. django_cfg/modules/django_client/core/config/config.py +208 -0
  68. django_cfg/modules/django_client/core/config/group.py +101 -0
  69. django_cfg/modules/django_client/core/config/service.py +209 -0
  70. django_cfg/modules/django_client/core/generator/__init__.py +115 -0
  71. django_cfg/modules/django_client/core/generator/base.py +838 -0
  72. django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
  73. django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
  74. django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
  75. django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
  76. django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
  77. django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
  78. django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
  79. django_cfg/modules/django_client/core/generator/python/templates/__init__.py.jinja +9 -0
  80. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +153 -0
  81. django_cfg/modules/django_client/core/generator/python/templates/app_init.py.jinja +6 -0
  82. django_cfg/modules/django_client/core/generator/python/templates/client/app_client.py.jinja +18 -0
  83. django_cfg/modules/django_client/core/generator/python/templates/client/flat_client.py.jinja +38 -0
  84. django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +68 -0
  85. django_cfg/modules/django_client/core/generator/python/templates/client/main_client_file.py.jinja +14 -0
  86. django_cfg/modules/django_client/core/generator/python/templates/client/operation_method.py.jinja +9 -0
  87. django_cfg/modules/django_client/core/generator/python/templates/client/sub_client.py.jinja +18 -0
  88. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
  89. django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
  90. django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
  91. django_cfg/modules/django_client/core/generator/python/templates/client_file.py.jinja +13 -0
  92. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +52 -0
  93. django_cfg/modules/django_client/core/generator/python/templates/models/app_models.py.jinja +17 -0
  94. django_cfg/modules/django_client/core/generator/python/templates/models/enum_class.py.jinja +17 -0
  95. django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +8 -0
  96. django_cfg/modules/django_client/core/generator/python/templates/models/models.py.jinja +17 -0
  97. django_cfg/modules/django_client/core/generator/python/templates/models/schema_class.py.jinja +21 -0
  98. django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
  99. django_cfg/modules/django_client/core/generator/python/templates/utils/logger.py.jinja +255 -0
  100. django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
  101. django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py.jinja +12 -0
  102. django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
  103. django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
  104. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
  105. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
  106. django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
  107. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
  108. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
  109. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
  110. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
  111. django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
  112. django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja +2 -0
  113. django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja +18 -0
  114. django_cfg/modules/django_client/core/generator/typescript/templates/client/client.ts.jinja +403 -0
  115. django_cfg/modules/django_client/core/generator/typescript/templates/client/flat_client.ts.jinja +109 -0
  116. django_cfg/modules/django_client/core/generator/typescript/templates/client/main_client_file.ts.jinja +10 -0
  117. django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja +61 -0
  118. django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja +15 -0
  119. django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja +9 -0
  120. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
  121. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
  122. django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja +5 -0
  123. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +268 -0
  124. django_cfg/modules/django_client/core/generator/typescript/templates/models/app_models.ts.jinja +8 -0
  125. django_cfg/modules/django_client/core/generator/typescript/templates/models/enums.ts.jinja +4 -0
  126. django_cfg/modules/django_client/core/generator/typescript/templates/models/models.ts.jinja +8 -0
  127. django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
  128. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
  129. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
  130. django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
  131. django_cfg/modules/django_client/core/generator/typescript/templates/utils/errors.ts.jinja +116 -0
  132. django_cfg/modules/django_client/core/generator/typescript/templates/utils/http.ts.jinja +98 -0
  133. django_cfg/modules/django_client/core/generator/typescript/templates/utils/logger.ts.jinja +259 -0
  134. django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
  135. django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja +7 -0
  136. django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja +158 -0
  137. django_cfg/modules/django_client/core/groups/__init__.py +13 -0
  138. django_cfg/modules/django_client/core/groups/detector.py +178 -0
  139. django_cfg/modules/django_client/core/groups/manager.py +314 -0
  140. django_cfg/modules/django_client/core/ir/__init__.py +57 -0
  141. django_cfg/modules/django_client/core/ir/context.py +387 -0
  142. django_cfg/modules/django_client/core/ir/operation.py +518 -0
  143. django_cfg/modules/django_client/core/ir/schema.py +353 -0
  144. django_cfg/modules/django_client/core/parser/__init__.py +74 -0
  145. django_cfg/modules/django_client/core/parser/base.py +648 -0
  146. django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
  147. django_cfg/modules/django_client/core/parser/models/base.py +212 -0
  148. django_cfg/modules/django_client/core/parser/models/components.py +160 -0
  149. django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
  150. django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
  151. django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
  152. django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
  153. django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
  154. django_cfg/modules/django_client/core/validation/__init__.py +22 -0
  155. django_cfg/modules/django_client/core/validation/checker.py +134 -0
  156. django_cfg/modules/django_client/core/validation/fixer.py +216 -0
  157. django_cfg/modules/django_client/core/validation/reporter.py +480 -0
  158. django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
  159. django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
  160. django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
  161. django_cfg/modules/django_client/core/validation/safety.py +266 -0
  162. django_cfg/modules/django_client/management/__init__.py +3 -0
  163. django_cfg/modules/django_client/management/commands/__init__.py +3 -0
  164. django_cfg/modules/django_client/management/commands/generate_client.py +427 -0
  165. django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
  166. django_cfg/modules/django_client/pytest.ini +30 -0
  167. django_cfg/modules/django_client/spectacular/__init__.py +10 -0
  168. django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
  169. django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
  170. django_cfg/modules/django_client/urls.py +72 -0
  171. django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
  172. django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
  173. django_cfg/modules/django_dashboard/management/__init__.py +0 -0
  174. django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
  175. django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
  176. django_cfg/modules/django_dashboard/sections/documentation.py +391 -0
  177. django_cfg/modules/django_email/management/__init__.py +0 -0
  178. django_cfg/modules/django_email/management/commands/__init__.py +0 -0
  179. django_cfg/modules/django_email/management/commands/test_email.py +93 -0
  180. django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
  181. django_cfg/modules/django_logging/django_logger.py +6 -6
  182. django_cfg/modules/django_ngrok/management/__init__.py +0 -0
  183. django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
  184. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
  185. django_cfg/modules/django_tasks/management/__init__.py +0 -0
  186. django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
  187. django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
  188. django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
  189. django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
  190. django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
  191. django_cfg/modules/django_telegram/management/__init__.py +0 -0
  192. django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
  193. django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
  194. django_cfg/modules/django_twilio/management/__init__.py +0 -0
  195. django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
  196. django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
  197. django_cfg/modules/django_unfold/callbacks/main.py +21 -10
  198. django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
  199. django_cfg/pyproject.toml +2 -6
  200. django_cfg/registry/third_party.py +5 -7
  201. django_cfg/routing/callbacks.py +1 -1
  202. django_cfg/static/admin/css/prose-unfold.css +666 -0
  203. django_cfg/templates/admin/index.html +8 -0
  204. django_cfg/templates/admin/index_new.html +13 -0
  205. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
  206. django_cfg/templates/admin/sections/documentation_section.html +172 -0
  207. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
  208. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/METADATA +2 -2
  209. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/RECORD +224 -74
  210. django_cfg/management/commands/generate.py +0 -107
  211. /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
  212. /django_cfg/{dashboard → modules/django_admin}/management/__init__.py +0 -0
  213. /django_cfg/{dashboard → modules/django_admin}/management/commands/__init__.py +0 -0
  214. /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
  215. /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
  216. /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
  217. /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
  218. /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
  219. /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
  220. /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
  221. /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
  222. /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
  223. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
  224. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
  225. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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) + ")"