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,872 @@
1
+ """
2
+ TypeScript Generator - Generates TypeScript client (Fetch API).
3
+
4
+ This generator creates a complete TypeScript API client from IR:
5
+ - TypeScript interfaces (Request/Response/Patch splits)
6
+ - Enum types from x-enum-varnames
7
+ - Fetch API for HTTP
8
+ - Django CSRF/session handling
9
+ - Type-safe
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import pathlib
15
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
16
+
17
+ from .base import BaseGenerator, GeneratedFile
18
+ from ..ir import IROperationObject, IRSchemaObject
19
+
20
+
21
+ class TypeScriptGenerator(BaseGenerator):
22
+ """
23
+ TypeScript client generator.
24
+
25
+ Generates:
26
+ - models.ts: TypeScript interfaces (User, UserRequest, PatchedUser)
27
+ - enums.ts: Enum types (StatusEnum, RoleEnum)
28
+ - client.ts: APIClient class with all operations
29
+ - index.ts: Module exports
30
+ """
31
+
32
+ def __init__(self, *args, **kwargs):
33
+ super().__init__(*args, **kwargs)
34
+
35
+ # Setup Jinja2 environment
36
+ templates_dir = pathlib.Path(__file__).parent / "templates"
37
+ self.jinja_env = Environment(
38
+ loader=FileSystemLoader(str(templates_dir)),
39
+ autoescape=select_autoescape(['html', 'xml']),
40
+ trim_blocks=True,
41
+ lstrip_blocks=True,
42
+ )
43
+
44
+ def generate(self) -> list[GeneratedFile]:
45
+ """Generate all TypeScript client files."""
46
+ files = []
47
+
48
+ if self.client_structure == "namespaced":
49
+ # Generate per-app folders
50
+ ops_by_tag = self.group_operations_by_tag()
51
+
52
+ for tag, operations in sorted(ops_by_tag.items()):
53
+ # Generate app folder (models.ts, client.ts, index.ts)
54
+ files.extend(self._generate_app_folder(tag, operations))
55
+
56
+ # Generate shared enums.ts (Variant 2: all enums in root)
57
+ all_schemas = self.context.schemas
58
+ all_enums = self._collect_enums_from_schemas(all_schemas)
59
+ if all_enums:
60
+ files.append(self._generate_shared_enums_file(all_enums))
61
+
62
+ # Generate main client.ts
63
+ files.append(self._generate_main_client_file(ops_by_tag))
64
+
65
+ # Generate main index.ts
66
+ files.append(self._generate_main_index_file())
67
+
68
+ # Generate http.ts with HttpClientAdapter
69
+ files.append(self._generate_http_adapter_file())
70
+
71
+ # Generate errors.ts with APIError
72
+ files.append(self._generate_errors_file())
73
+
74
+ # Generate storage.ts with StorageAdapter
75
+ files.append(self._generate_storage_file())
76
+
77
+ # Generate logger.ts with Consola
78
+ files.append(self._generate_logger_file())
79
+
80
+ # Generate schema.ts with OpenAPI schema
81
+ if self.openapi_schema:
82
+ files.append(self._generate_schema_file())
83
+ else:
84
+ # Flat structure (original logic)
85
+ files.append(self._generate_models_file())
86
+
87
+ enum_schemas = self.get_enum_schemas()
88
+ if enum_schemas:
89
+ files.append(self._generate_enums_file())
90
+
91
+ files.append(self._generate_client_file())
92
+ files.append(self._generate_index_file())
93
+
94
+ # Generate storage.ts with StorageAdapter
95
+ files.append(self._generate_storage_file())
96
+
97
+ # Generate logger.ts with Consola
98
+ files.append(self._generate_logger_file())
99
+
100
+ # Generate schema.ts with OpenAPI schema
101
+ if self.openapi_schema:
102
+ files.append(self._generate_schema_file())
103
+
104
+ return files
105
+
106
+ # ===== Models Generation =====
107
+
108
+ def _generate_models_file(self) -> GeneratedFile:
109
+ """Generate models.ts with all TypeScript interfaces."""
110
+ # Generate all schemas
111
+ schema_codes = []
112
+
113
+ # Response models first
114
+ for name, schema in self.get_response_schemas().items():
115
+ schema_codes.append(self.generate_schema(schema))
116
+
117
+ # Request models
118
+ for name, schema in self.get_request_schemas().items():
119
+ schema_codes.append(self.generate_schema(schema))
120
+
121
+ # Patch models
122
+ for name, schema in self.get_patch_schemas().items():
123
+ schema_codes.append(self.generate_schema(schema))
124
+
125
+ template = self.jinja_env.get_template('typescript/models/models.ts.jinja')
126
+ content = template.render(
127
+ has_enums=bool(self.get_enum_schemas()),
128
+ schemas=schema_codes
129
+ )
130
+
131
+ return GeneratedFile(
132
+ path="models.ts",
133
+ content=content,
134
+ description="TypeScript interfaces (Request/Response/Patch)",
135
+ )
136
+
137
+ def _generate_enums_file(self) -> GeneratedFile:
138
+ """Generate enums.ts with all enum types (flat structure)."""
139
+ enum_codes = []
140
+ for name, schema in self.get_enum_schemas().items():
141
+ enum_codes.append(self.generate_enum(schema))
142
+
143
+ template = self.jinja_env.get_template('typescript/models/enums.ts.jinja')
144
+ content = template.render(enums=enum_codes)
145
+
146
+ return GeneratedFile(
147
+ path="enums.ts",
148
+ content=content,
149
+ description="Enum types from x-enum-varnames",
150
+ )
151
+
152
+ def _generate_shared_enums_file(self, enums: dict[str, IRSchemaObject]) -> GeneratedFile:
153
+ """Generate shared enums.ts for namespaced structure (Variant 2)."""
154
+ enum_codes = []
155
+ for name, schema in enums.items():
156
+ enum_codes.append(self.generate_enum(schema))
157
+
158
+ template = self.jinja_env.get_template('typescript/models/enums.ts.jinja')
159
+ content = template.render(enums=enum_codes)
160
+
161
+ return GeneratedFile(
162
+ path="enums.ts",
163
+ content=content,
164
+ description="Shared enum types from x-enum-varnames",
165
+ )
166
+
167
+ # ===== Schema Generation =====
168
+
169
+ def generate_schema(self, schema: IRSchemaObject) -> str:
170
+ """Generate TypeScript interface for schema."""
171
+ if schema.type != "object":
172
+ # For primitive types, skip (they'll be inlined)
173
+ return ""
174
+
175
+ # Interface comment
176
+ comment_lines = []
177
+ if schema.description:
178
+ comment_lines.extend(self.wrap_comment(schema.description, 76))
179
+
180
+ # Add metadata about model type
181
+ if schema.is_request_model:
182
+ comment_lines.append("")
183
+ comment_lines.append("Request model (no read-only fields).")
184
+ elif schema.is_patch_model:
185
+ comment_lines.append("")
186
+ comment_lines.append("PATCH model (all fields optional).")
187
+ elif schema.is_response_model:
188
+ comment_lines.append("")
189
+ comment_lines.append("Response model (includes read-only fields).")
190
+
191
+ comment = "/**\n * " + "\n * ".join(comment_lines) + "\n */" if comment_lines else None
192
+
193
+ # Fields
194
+ field_lines = []
195
+
196
+ for prop_name, prop_schema in schema.properties.items():
197
+ field_lines.append(self._generate_field(prop_name, prop_schema, schema.required))
198
+
199
+ # Build interface
200
+ lines = []
201
+
202
+ if comment:
203
+ lines.append(comment)
204
+
205
+ lines.append(f"export interface {schema.name} {{")
206
+
207
+ if field_lines:
208
+ for field_line in field_lines:
209
+ lines.append(self.indent(field_line, 2))
210
+ else:
211
+ # Empty interface
212
+ pass
213
+
214
+ lines.append("}")
215
+
216
+ return "\n".join(lines)
217
+
218
+ def _generate_field(
219
+ self,
220
+ name: str,
221
+ schema: IRSchemaObject,
222
+ required_fields: list[str],
223
+ ) -> str:
224
+ """
225
+ Generate TypeScript field definition.
226
+
227
+ Examples:
228
+ id: number;
229
+ username: string;
230
+ email?: string | null;
231
+ status: Enums.StatusEnum;
232
+ """
233
+ # Check if this field is an enum
234
+ if schema.enum and schema.name:
235
+ # Use enum type from shared enums
236
+ ts_type = f"Enums.{schema.name}"
237
+ if schema.nullable:
238
+ ts_type = f"{ts_type} | null"
239
+ # Check if this field is a reference to an enum (via $ref)
240
+ elif schema.ref and schema.ref in self.context.schemas:
241
+ ref_schema = self.context.schemas[schema.ref]
242
+ if ref_schema.enum:
243
+ # This is a reference to an enum component
244
+ ts_type = f"Enums.{schema.ref}"
245
+ if schema.nullable:
246
+ ts_type = f"{ts_type} | null"
247
+ else:
248
+ # Regular reference
249
+ ts_type = schema.typescript_type
250
+ else:
251
+ # Get TypeScript type
252
+ ts_type = schema.typescript_type
253
+
254
+ # Check if required
255
+ is_required = name in required_fields
256
+
257
+ # Optional marker
258
+ optional_marker = "" if is_required else "?"
259
+
260
+ # Comment
261
+ if schema.description:
262
+ return f"/** {schema.description} */\n{name}{optional_marker}: {ts_type};"
263
+
264
+ return f"{name}{optional_marker}: {ts_type};"
265
+
266
+ def generate_enum(self, schema: IRSchemaObject) -> str:
267
+ """Generate TypeScript enum from x-enum-varnames."""
268
+ # Enum comment
269
+ comment_lines = []
270
+ if schema.description:
271
+ comment_lines.extend(self.wrap_comment(schema.description, 76))
272
+
273
+ comment = "/**\n * " + "\n * ".join(comment_lines) + "\n */" if comment_lines else None
274
+
275
+ # Enum members
276
+ member_lines = []
277
+ for var_name, value in zip(schema.enum_var_names, schema.enum):
278
+ if isinstance(value, str):
279
+ member_lines.append(f'{var_name} = "{value}",')
280
+ else:
281
+ member_lines.append(f"{var_name} = {value},")
282
+
283
+ # Build enum
284
+ lines = []
285
+
286
+ if comment:
287
+ lines.append(comment)
288
+
289
+ lines.append(f"export enum {schema.name} {{")
290
+
291
+ for member_line in member_lines:
292
+ lines.append(self.indent(member_line, 2))
293
+
294
+ lines.append("}")
295
+
296
+ return "\n".join(lines)
297
+
298
+ # ===== Client Generation =====
299
+
300
+ def _generate_client_file(self) -> GeneratedFile:
301
+ """Generate client.ts with APIClient class."""
302
+ # Client class
303
+ client_code = self._generate_client_class()
304
+
305
+ template = self.jinja_env.get_template('typescript/client_file.ts.jinja')
306
+ content = template.render(
307
+ has_enums=bool(self.get_enum_schemas()),
308
+ client_code=client_code
309
+ )
310
+
311
+ return GeneratedFile(
312
+ path="client.ts",
313
+ content=content,
314
+ description="APIClient with HTTP adapter and error handling",
315
+ )
316
+
317
+ def _generate_client_class(self) -> str:
318
+ """Generate APIClient class."""
319
+ if self.client_structure == "namespaced":
320
+ return self._generate_namespaced_client()
321
+ else:
322
+ return self._generate_flat_client()
323
+
324
+ def _generate_flat_client(self) -> str:
325
+ """Generate flat APIClient (all methods in one class)."""
326
+ # Generate all operation methods
327
+ method_codes = []
328
+ for op_id, operation in self.context.operations.items():
329
+ method_codes.append(self.generate_operation(operation))
330
+
331
+ template = self.jinja_env.get_template('typescript/client/flat_client.ts.jinja')
332
+ return template.render(
333
+ api_title=self.context.openapi_info.title,
334
+ operations=method_codes
335
+ )
336
+
337
+ def _generate_namespaced_client(self) -> str:
338
+ """Generate namespaced APIClient (sub-clients per tag)."""
339
+ # Group operations by tag (using base class method)
340
+ ops_by_tag = self.group_operations_by_tag()
341
+
342
+ # Generate sub-client classes
343
+ sub_client_classes = []
344
+ for tag, operations in sorted(ops_by_tag.items()):
345
+ sub_client_classes.append(self._generate_sub_client_class(tag, operations))
346
+
347
+ sub_clients_code = "\n\n".join(sub_client_classes)
348
+
349
+ # Generate main APIClient
350
+ main_client_code = self._generate_main_client_class(list(ops_by_tag.keys()))
351
+
352
+ return f"{sub_clients_code}\n\n{main_client_code}"
353
+
354
+ def _generate_sub_client_class(self, tag: str, operations: list) -> str:
355
+ """Generate sub-client class for a specific tag."""
356
+ class_name = self.tag_to_class_name(tag)
357
+
358
+ # Generate methods for this tag
359
+ method_codes = []
360
+ for operation in operations:
361
+ method_codes.append(self.generate_operation(operation, remove_tag_prefix=True, in_subclient=True))
362
+
363
+ template = self.jinja_env.get_template('typescript/client/sub_client.ts.jinja')
364
+ return template.render(
365
+ tag=self.tag_to_display_name(tag),
366
+ class_name=class_name,
367
+ operations=method_codes
368
+ )
369
+
370
+ def _generate_main_client_class(self, ops_by_tag: dict) -> str:
371
+ """Generate main APIClient with sub-clients."""
372
+ tags = sorted(ops_by_tag.keys())
373
+
374
+ # Prepare data for template
375
+ tags_data = [
376
+ {
377
+ "class_name": self.tag_to_class_name(tag),
378
+ "property": self.tag_to_property_name(tag),
379
+ "slug": self.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
380
+ }
381
+ for tag in tags
382
+ ]
383
+
384
+ template = self.jinja_env.get_template('typescript/client/client.ts.jinja')
385
+ return template.render(
386
+ sub_clients=True,
387
+ include_imports=False, # Imports already in main_client_file.ts.jinja
388
+ tags=tags_data,
389
+ info={"title": self.context.openapi_info.title},
390
+ )
391
+
392
+ def generate_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False, in_subclient: bool = False) -> str:
393
+ """Generate async method for operation."""
394
+ # Get method name
395
+ operation_id = operation.operation_id
396
+ if remove_tag_prefix and operation.tags:
397
+ # Remove tag prefix using base class method
398
+ tag = operation.tags[0]
399
+ operation_id = self.remove_tag_prefix(operation_id, tag)
400
+
401
+ # Convert snake_case to camelCase
402
+ method_name = self._to_camel_case(operation_id)
403
+
404
+ # Request method prefix
405
+ request_prefix = "this.client" if in_subclient else "this"
406
+
407
+ # Method parameters
408
+ params = []
409
+
410
+ # Add path parameters
411
+ for param in operation.path_parameters:
412
+ param_type = self._map_param_type(param.schema_type)
413
+ params.append(f"{param.name}: {param_type}")
414
+
415
+ # Check if this is a file upload operation
416
+ is_multipart = (
417
+ operation.request_body
418
+ and operation.request_body.content_type == "multipart/form-data"
419
+ )
420
+
421
+ # Add request body parameter
422
+ if operation.request_body:
423
+ if is_multipart:
424
+ # For multipart, get schema properties and add as individual File parameters
425
+ schema_name = operation.request_body.schema_name
426
+ if schema_name in self.context.schemas:
427
+ schema = self.context.schemas[schema_name]
428
+ for prop_name, prop in schema.properties.items():
429
+ # Check if it's a file field (format: binary)
430
+ if prop.format == "binary":
431
+ params.append(f"{prop_name}: File | Blob")
432
+ else:
433
+ # Regular field in multipart
434
+ prop_type = self._map_param_type(prop.type)
435
+ if prop_name in schema.required:
436
+ params.append(f"{prop_name}: {prop_type}")
437
+ else:
438
+ params.append(f"{prop_name}?: {prop_type}")
439
+ else:
440
+ # JSON request body
441
+ params.append(f"data: Models.{operation.request_body.schema_name}")
442
+ elif operation.patch_request_body:
443
+ params.append(f"data?: Models.{operation.patch_request_body.schema_name}")
444
+
445
+ # Add query parameters
446
+ for param in operation.query_parameters:
447
+ param_type = self._map_param_type(param.schema_type)
448
+ if not param.required:
449
+ param_type = f"{param_type} | null"
450
+ params.append(f"{param.name}?: {param_type}")
451
+ else:
452
+ params.append(f"{param.name}: {param_type}")
453
+
454
+ # Return type
455
+ primary_response = operation.primary_success_response
456
+ if primary_response and primary_response.schema_name:
457
+ if operation.is_list_operation:
458
+ return_type = f"Models.{primary_response.schema_name}[]"
459
+ else:
460
+ return_type = f"Models.{primary_response.schema_name}"
461
+ else:
462
+ return_type = "void"
463
+
464
+ signature = f"async {method_name}({', '.join(params)}): Promise<{return_type}> {{"
465
+
466
+ # Comment
467
+ comment_lines = []
468
+ if operation.summary:
469
+ comment_lines.append(operation.summary)
470
+ if operation.description:
471
+ if comment_lines:
472
+ comment_lines.append("")
473
+ comment_lines.extend(self.wrap_comment(operation.description, 72))
474
+
475
+ comment = "/**\n * " + "\n * ".join(comment_lines) + "\n */" if comment_lines else None
476
+
477
+ # Method body
478
+ body_lines = []
479
+
480
+ # Build path
481
+ path_expr = f'"{operation.path}"'
482
+ if operation.path_parameters:
483
+ # Replace {id} with ${id}
484
+ path_with_vars = operation.path
485
+ for param in operation.path_parameters:
486
+ path_with_vars = path_with_vars.replace(f"{{{param.name}}}", f"${{{param.name}}}")
487
+ path_expr = f'`{path_with_vars}`'
488
+
489
+ # Build request options
490
+ request_opts = []
491
+
492
+ # Query params
493
+ if operation.query_parameters:
494
+ query_items = [f"{param.name}" for param in operation.query_parameters]
495
+ query_dict = "{ " + ", ".join(query_items) + " }"
496
+ request_opts.append(f"params: {query_dict}")
497
+
498
+ # Body / FormData
499
+ if operation.request_body or operation.patch_request_body:
500
+ if is_multipart and operation.request_body:
501
+ # Build FormData for multipart upload
502
+ schema_name = operation.request_body.schema_name
503
+ if schema_name in self.context.schemas:
504
+ schema = self.context.schemas[schema_name]
505
+ body_lines.append("const formData = new FormData();")
506
+ for prop_name, prop in schema.properties.items():
507
+ if prop.format == "binary":
508
+ # Append file
509
+ body_lines.append(f"formData.append('{prop_name}', {prop_name});")
510
+ elif prop_name in schema.required or True: # Append all non-undefined fields
511
+ # Append other fields (wrap in if check for optional)
512
+ if prop_name not in schema.required:
513
+ body_lines.append(f"if ({prop_name} !== undefined) formData.append('{prop_name}', String({prop_name}));")
514
+ else:
515
+ body_lines.append(f"formData.append('{prop_name}', String({prop_name}));")
516
+ request_opts.append("formData")
517
+ else:
518
+ # JSON body
519
+ request_opts.append("body: data")
520
+
521
+ # Make request
522
+ if request_opts:
523
+ request_line = f"const response = await {request_prefix}.request<{return_type}>('{operation.http_method}', {path_expr}, {{ {', '.join(request_opts)} }});"
524
+ else:
525
+ request_line = f"const response = await {request_prefix}.request<{return_type}>('{operation.http_method}', {path_expr});"
526
+
527
+ body_lines.append(request_line)
528
+
529
+ # Handle response
530
+ if operation.is_list_operation and primary_response:
531
+ # Extract results from paginated response
532
+ body_lines.append("return (response as any).results || [];")
533
+ elif return_type != "void":
534
+ body_lines.append("return response;")
535
+ else:
536
+ body_lines.append("return;")
537
+
538
+ # Build method
539
+ lines = []
540
+
541
+ if comment:
542
+ lines.append(comment)
543
+
544
+ lines.append(signature)
545
+
546
+ for line in body_lines:
547
+ lines.append(self.indent(line, 2))
548
+
549
+ lines.append("}")
550
+
551
+ return "\n".join(lines)
552
+
553
+ def _map_param_type(self, schema_type: str) -> str:
554
+ """Map parameter schema type to TypeScript type."""
555
+ type_map = {
556
+ "string": "string",
557
+ "integer": "number",
558
+ "number": "number",
559
+ "boolean": "boolean",
560
+ "array": "any[]",
561
+ }
562
+ return type_map.get(schema_type, "any")
563
+
564
+ def _to_camel_case(self, snake_str: str) -> str:
565
+ """
566
+ Convert snake_case to camelCase.
567
+
568
+ Examples:
569
+ >>> self._to_camel_case("users_list")
570
+ 'usersList'
571
+ >>> self._to_camel_case("users_partial_update")
572
+ 'usersPartialUpdate'
573
+ """
574
+ components = snake_str.split("_")
575
+ return components[0] + "".join(x.title() for x in components[1:])
576
+
577
+ # ===== Index File =====
578
+
579
+ def _generate_index_file(self) -> GeneratedFile:
580
+ """Generate index.ts with exports."""
581
+ template = self.jinja_env.get_template('typescript/index.ts.jinja')
582
+ content = template.render(
583
+ has_enums=bool(self.get_enum_schemas())
584
+ )
585
+
586
+ return GeneratedFile(
587
+ path="index.ts",
588
+ content=content,
589
+ description="Module exports",
590
+ )
591
+
592
+ # ===== Per-App Folder Generation (Namespaced Structure) =====
593
+
594
+ def _generate_app_folder(self, tag: str, operations: list[IROperationObject]) -> list[GeneratedFile]:
595
+ """Generate folder for a specific app (tag)."""
596
+ files = []
597
+
598
+ # Get schemas used by this app
599
+ app_schemas = self._get_schemas_for_operations(operations)
600
+
601
+ # Generate models.ts for this app
602
+ files.append(self._generate_app_models_file(tag, app_schemas, operations))
603
+
604
+ # Generate client.ts for this app
605
+ files.append(self._generate_app_client_file(tag, operations))
606
+
607
+ # Generate index.ts for this app
608
+ files.append(self._generate_app_index_file(tag, operations))
609
+
610
+ return files
611
+
612
+ def _get_schemas_for_operations(self, operations: list[IROperationObject]) -> dict[str, IRSchemaObject]:
613
+ """
614
+ Get all schemas used by given operations.
615
+
616
+ This method recursively resolves all schema dependencies ($ref) to ensure
617
+ that nested schemas (e.g., APIKeyList referenced by PaginatedAPIKeyListList)
618
+ are included in the generated models file.
619
+ """
620
+ schemas = {}
621
+
622
+ for operation in operations:
623
+ # Request body schemas
624
+ if operation.request_body and operation.request_body.schema_name:
625
+ schema_name = operation.request_body.schema_name
626
+ if schema_name in self.context.schemas:
627
+ schemas[schema_name] = self.context.schemas[schema_name]
628
+
629
+ # Patch request body schemas
630
+ if operation.patch_request_body and operation.patch_request_body.schema_name:
631
+ schema_name = operation.patch_request_body.schema_name
632
+ if schema_name in self.context.schemas:
633
+ schemas[schema_name] = self.context.schemas[schema_name]
634
+
635
+ # Response schemas
636
+ for status_code, response in operation.responses.items():
637
+ if response.schema_name:
638
+ if response.schema_name in self.context.schemas:
639
+ schemas[response.schema_name] = self.context.schemas[response.schema_name]
640
+
641
+ # Recursively resolve all nested schema dependencies
642
+ schemas = self._resolve_nested_schemas(schemas)
643
+
644
+ return schemas
645
+
646
+ def _resolve_nested_schemas(self, initial_schemas: dict[str, IRSchemaObject]) -> dict[str, IRSchemaObject]:
647
+ """
648
+ Recursively resolve all nested schema dependencies ($ref).
649
+
650
+ This ensures that if SchemaA references SchemaB (e.g., via a property or array items),
651
+ SchemaB is also included in the output, even if it's not directly used in operations.
652
+
653
+ Example:
654
+ PaginatedAPIKeyListList has:
655
+ results: Array<APIKeyList> ← $ref to APIKeyList
656
+
657
+ This method will find APIKeyList and include it.
658
+
659
+ Args:
660
+ initial_schemas: Schemas directly used by operations
661
+
662
+ Returns:
663
+ All schemas including nested dependencies
664
+ """
665
+ resolved = dict(initial_schemas)
666
+ queue = list(initial_schemas.values())
667
+ seen = set(initial_schemas.keys())
668
+
669
+ while queue:
670
+ schema = queue.pop(0)
671
+
672
+ # Check properties for $ref and nested items
673
+ if schema.properties:
674
+ for prop in schema.properties.values():
675
+ # Direct $ref on property
676
+ if prop.ref and prop.ref not in seen:
677
+ if prop.ref in self.context.schemas:
678
+ resolved[prop.ref] = self.context.schemas[prop.ref]
679
+ queue.append(self.context.schemas[prop.ref])
680
+ seen.add(prop.ref)
681
+
682
+ # $ref inside array items (CRITICAL for PaginatedXList patterns!)
683
+ if prop.items and prop.items.ref:
684
+ if prop.items.ref not in seen:
685
+ if prop.items.ref in self.context.schemas:
686
+ resolved[prop.items.ref] = self.context.schemas[prop.items.ref]
687
+ queue.append(self.context.schemas[prop.items.ref])
688
+ seen.add(prop.items.ref)
689
+
690
+ # Check array items for $ref at schema level
691
+ if schema.items and schema.items.ref:
692
+ if schema.items.ref not in seen:
693
+ if schema.items.ref in self.context.schemas:
694
+ resolved[schema.items.ref] = self.context.schemas[schema.items.ref]
695
+ queue.append(self.context.schemas[schema.items.ref])
696
+ seen.add(schema.items.ref)
697
+
698
+ return resolved
699
+
700
+ def _generate_app_models_file(self, tag: str, schemas: dict[str, IRSchemaObject], operations: list[IROperationObject]) -> GeneratedFile:
701
+ """Generate models.ts for a specific app."""
702
+ # Check if we have enums in schemas
703
+ app_enums = self._collect_enums_from_schemas(schemas)
704
+ has_enums = len(app_enums) > 0
705
+
706
+ # Generate schemas
707
+ schema_codes = []
708
+ for name, schema in schemas.items():
709
+ schema_codes.append(self.generate_schema(schema))
710
+
711
+ template = self.jinja_env.get_template('typescript/models/app_models.ts.jinja')
712
+ content = template.render(
713
+ has_enums=has_enums,
714
+ schemas=schema_codes
715
+ )
716
+
717
+ folder_name = self.tag_and_app_to_folder_name(tag, operations)
718
+ return GeneratedFile(
719
+ path=f"{folder_name}/models.ts",
720
+ content=content,
721
+ description=f"TypeScript interfaces for {tag}",
722
+ )
723
+
724
+ def _generate_app_client_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
725
+ """Generate client.ts for a specific app."""
726
+ class_name = self.tag_to_class_name(tag)
727
+
728
+ # Generate methods
729
+ method_codes = []
730
+ for operation in operations:
731
+ method_codes.append(self.generate_operation(operation, remove_tag_prefix=True, in_subclient=True))
732
+
733
+ template = self.jinja_env.get_template('typescript/client/app_client.ts.jinja')
734
+ content = template.render(
735
+ tag=self.tag_to_display_name(tag),
736
+ class_name=class_name,
737
+ operations=method_codes
738
+ )
739
+
740
+ folder_name = self.tag_and_app_to_folder_name(tag, operations)
741
+ return GeneratedFile(
742
+ path=f"{folder_name}/client.ts",
743
+ content=content,
744
+ description=f"API client for {tag}",
745
+ )
746
+
747
+ def _generate_app_index_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
748
+ """Generate index.ts for a specific app."""
749
+ template = self.jinja_env.get_template('typescript/app_index.ts.jinja')
750
+ content = template.render()
751
+
752
+ folder_name = self.tag_and_app_to_folder_name(tag, operations)
753
+ return GeneratedFile(
754
+ path=f"{folder_name}/index.ts",
755
+ content=content,
756
+ description=f"Module exports for {tag}",
757
+ )
758
+
759
+ def _generate_main_client_file(self, ops_by_tag: dict) -> GeneratedFile:
760
+ """Generate main client.ts with APIClient."""
761
+ tags = sorted(ops_by_tag.keys())
762
+
763
+ # Prepare tags data for template
764
+ tags_data = [
765
+ {
766
+ "class_name": self.tag_to_class_name(tag),
767
+ "slug": self.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
768
+ }
769
+ for tag in tags
770
+ ]
771
+
772
+ # Generate main APIClient class
773
+ client_code = self._generate_main_client_class(ops_by_tag)
774
+
775
+ template = self.jinja_env.get_template('typescript/client/main_client_file.ts.jinja')
776
+ content = template.render(
777
+ tags=tags_data,
778
+ client_code=client_code
779
+ )
780
+
781
+ return GeneratedFile(
782
+ path="client.ts",
783
+ content=content,
784
+ description="Main API client with HTTP adapter and error handling",
785
+ )
786
+
787
+ def _generate_main_index_file(self) -> GeneratedFile:
788
+ """Generate main index.ts with API class and JWT management."""
789
+ ops_by_tag = self.group_operations_by_tag()
790
+ tags = sorted(ops_by_tag.keys())
791
+
792
+ # Prepare tags data for template
793
+ tags_data = [
794
+ {
795
+ "class_name": self.tag_to_class_name(tag, suffix=""),
796
+ "property": self.tag_to_property_name(tag),
797
+ "slug": self.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
798
+ }
799
+ for tag in tags
800
+ ]
801
+
802
+ # Check if we have enums
803
+ all_schemas = self.context.schemas
804
+ all_enums = self._collect_enums_from_schemas(all_schemas)
805
+
806
+ template = self.jinja_env.get_template('typescript/main_index.ts.jinja')
807
+ content = template.render(
808
+ api_title=self.context.openapi_info.title,
809
+ tags=tags_data,
810
+ has_enums=bool(all_enums)
811
+ )
812
+
813
+ return GeneratedFile(
814
+ path="index.ts",
815
+ content=content,
816
+ description="Main index with API class and JWT management",
817
+ )
818
+
819
+ def _generate_http_adapter_file(self) -> GeneratedFile:
820
+ """Generate http.ts with HttpClient adapter interface."""
821
+ template = self.jinja_env.get_template('typescript/utils/http.ts.jinja')
822
+ content = template.render()
823
+
824
+ return GeneratedFile(
825
+ path="http.ts",
826
+ content=content,
827
+ description="HTTP client adapter interface and implementations",
828
+ )
829
+
830
+ def _generate_errors_file(self) -> GeneratedFile:
831
+ """Generate errors.ts with APIError class."""
832
+ template = self.jinja_env.get_template('typescript/utils/errors.ts.jinja')
833
+ content = template.render()
834
+
835
+ return GeneratedFile(
836
+ path="errors.ts",
837
+ content=content,
838
+ description="API error classes",
839
+ )
840
+
841
+ def _generate_storage_file(self) -> GeneratedFile:
842
+ """Generate storage.ts with StorageAdapter implementations."""
843
+ template = self.jinja_env.get_template('typescript/utils/storage.ts.jinja')
844
+ content = template.render()
845
+
846
+ return GeneratedFile(
847
+ path="storage.ts",
848
+ content=content,
849
+ description="Storage adapters for cross-platform support",
850
+ )
851
+
852
+ def _generate_logger_file(self) -> GeneratedFile:
853
+ """Generate logger.ts with Consola integration."""
854
+ template = self.jinja_env.get_template('typescript/utils/logger.ts.jinja')
855
+ content = template.render()
856
+
857
+ return GeneratedFile(
858
+ path="logger.ts",
859
+ content=content,
860
+ description="API Logger with Consola",
861
+ )
862
+
863
+ def _generate_schema_file(self) -> GeneratedFile:
864
+ """Generate schema.ts with OpenAPI schema as const."""
865
+ template = self.jinja_env.get_template('typescript/utils/schema.ts.jinja')
866
+ content = template.render(schema=self.openapi_schema)
867
+
868
+ return GeneratedFile(
869
+ path="schema.ts",
870
+ content=content,
871
+ description="OpenAPI Schema",
872
+ )