django-cfg 1.4.11__py3-none-any.whl → 1.4.14__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 (109) hide show
  1. django_cfg/apps/urls.py +120 -108
  2. django_cfg/core/generation/integration_generators/api.py +2 -1
  3. django_cfg/core/integration/url_integration.py +5 -10
  4. django_cfg/models/django/openapi.py +15 -128
  5. django_cfg/modules/django_client/core/archive/manager.py +2 -2
  6. django_cfg/modules/django_client/core/config/config.py +20 -0
  7. django_cfg/modules/django_client/core/config/service.py +1 -1
  8. django_cfg/modules/django_client/core/generator/__init__.py +4 -4
  9. django_cfg/modules/django_client/core/generator/base.py +71 -0
  10. django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
  11. django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
  12. django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
  13. django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
  14. django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
  15. django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
  16. django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
  17. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/api_wrapper.py.jinja +25 -2
  18. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client.py.jinja +24 -6
  19. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client_file.py.jinja +1 -0
  20. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/operation_method.py.jinja +3 -1
  21. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/sub_client.py.jinja +8 -1
  22. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
  23. django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
  24. django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
  25. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/main_init.py.jinja +2 -0
  26. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enum_class.py.jinja +3 -1
  27. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/schema_class.py.jinja +3 -1
  28. django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
  29. django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
  30. django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
  31. django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
  32. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
  33. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
  34. django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
  35. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +539 -0
  36. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
  37. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
  38. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
  39. django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
  40. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/app_client.ts.jinja +1 -1
  41. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/client.ts.jinja +77 -1
  42. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/main_client_file.ts.jinja +1 -0
  43. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/sub_client.ts.jinja +3 -3
  44. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
  45. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
  46. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/main_index.ts.jinja +73 -11
  47. django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
  48. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
  49. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
  50. django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
  51. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/errors.ts.jinja +3 -1
  52. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/logger.ts.jinja +9 -1
  53. django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
  54. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/storage.ts.jinja +54 -10
  55. django_cfg/modules/django_client/management/commands/generate_client.py +5 -0
  56. django_cfg/modules/django_client/pytest.ini +30 -0
  57. django_cfg/modules/django_client/spectacular/__init__.py +3 -2
  58. django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
  59. django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
  60. django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
  61. django_cfg/modules/django_unfold/callbacks/main.py +6 -6
  62. django_cfg/modules/django_unfold/dashboard.py +6 -6
  63. django_cfg/pyproject.toml +1 -1
  64. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/METADATA +1 -1
  65. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/RECORD +100 -78
  66. django_cfg/dashboard/DEBUG_README.md +0 -105
  67. django_cfg/dashboard/REFACTORING_SUMMARY.md +0 -237
  68. django_cfg/modules/django_client/core/generator/python.py +0 -751
  69. django_cfg/modules/django_client/core/generator/typescript.py +0 -872
  70. django_cfg/modules/django_drf_theme/CHANGELOG.md +0 -210
  71. django_cfg/modules/django_drf_theme/EXAMPLE.md +0 -465
  72. django_cfg/modules/django_drf_theme/IMPLEMENTATION.md +0 -232
  73. django_cfg/modules/django_drf_theme/README.md +0 -207
  74. django_cfg/modules/django_drf_theme/TAILWIND_CDN_GUIDE.md +0 -274
  75. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/__init__.py.jinja +0 -0
  76. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/app_init.py.jinja +0 -0
  77. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/app_client.py.jinja +0 -0
  78. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/flat_client.py.jinja +0 -0
  79. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client_file.py.jinja +0 -0
  80. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/app_models.py.jinja +0 -0
  81. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enums.py.jinja +0 -0
  82. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/models.py.jinja +0 -0
  83. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/logger.py.jinja +0 -0
  84. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/schema.py.jinja +0 -0
  85. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/app_index.ts.jinja +0 -0
  86. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/flat_client.ts.jinja +0 -0
  87. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/operation.ts.jinja +0 -0
  88. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client_file.ts.jinja +0 -0
  89. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/index.ts.jinja +0 -0
  90. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/app_models.ts.jinja +0 -0
  91. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/enums.ts.jinja +0 -0
  92. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/models.ts.jinja +0 -0
  93. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/http.ts.jinja +0 -0
  94. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/schema.ts.jinja +0 -0
  95. /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
  96. /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
  97. /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
  98. /django_cfg/{dashboard → modules/django_dashboard}/management/__init__.py +0 -0
  99. /django_cfg/{dashboard → modules/django_dashboard}/management/commands/__init__.py +0 -0
  100. /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
  101. /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
  102. /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
  103. /django_cfg/{dashboard → modules/django_dashboard}/sections/documentation.py +0 -0
  104. /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
  105. /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
  106. /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
  107. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/WHEEL +0 -0
  108. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/entry_points.txt +0 -0
  109. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,539 @@
1
+ """
2
+ SWR Hooks Generator - Generates React hooks for data fetching.
3
+
4
+ This generator creates SWR-based React hooks from IR:
5
+ - Query hooks (GET operations) using useSWR
6
+ - Mutation hooks (POST/PUT/PATCH/DELETE) using useSWRConfig
7
+ - Automatic key generation
8
+ - Type-safe parameters and responses
9
+ - Optimistic updates support
10
+
11
+ Architecture:
12
+ - Query hooks: useSWR with automatic key management
13
+ - Mutation hooks: Custom hooks with revalidation
14
+ - Works only in React client components
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from jinja2 import Environment
20
+ from ..base import BaseGenerator, GeneratedFile
21
+ from ...ir import IROperationObject, IRContext
22
+
23
+
24
+ class HooksGenerator:
25
+ """
26
+ SWR hooks generator for React.
27
+
28
+ Generates:
29
+ - useResource() hooks for GET operations
30
+ - useCreateResource() hooks for POST
31
+ - useUpdateResource() hooks for PUT/PATCH
32
+ - useDeleteResource() hooks for DELETE
33
+ """
34
+
35
+ def __init__(self, jinja_env: Environment, context: IRContext, base: BaseGenerator):
36
+ self.jinja_env = jinja_env
37
+ self.context = context
38
+ self.base = base
39
+
40
+ def generate_query_hook(self, operation: IROperationObject) -> str:
41
+ """
42
+ Generate useSWR hook for GET operation.
43
+
44
+ Examples:
45
+ >>> generate_query_hook(users_list)
46
+ export function useShopProducts(params?: { page?: number }) {
47
+ return useSWR(
48
+ params ? ['shop-products', params] : 'shop-products',
49
+ () => Fetchers.getShopProducts(params)
50
+ )
51
+ }
52
+ """
53
+ # Get hook name
54
+ hook_name = self._operation_to_hook_name(operation)
55
+
56
+ # Get fetcher function name
57
+ fetcher_name = self._operation_to_fetcher_name(operation)
58
+
59
+ # Get parameters
60
+ param_info = self._get_param_info(operation)
61
+
62
+ # Get response type
63
+ response_type = self._get_response_type(operation)
64
+
65
+ # Get SWR key
66
+ swr_key = self._generate_swr_key(operation)
67
+
68
+ # Build hook
69
+ lines = []
70
+
71
+ # JSDoc
72
+ lines.append("/**")
73
+ if operation.summary:
74
+ lines.append(f" * {operation.summary}")
75
+ lines.append(" *")
76
+ lines.append(f" * @method {operation.http_method}")
77
+ lines.append(f" * @path {operation.path}")
78
+ lines.append(" */")
79
+
80
+ # Hook signature
81
+ if param_info['func_params']:
82
+ lines.append(f"export function {hook_name}({param_info['func_params']}) {{")
83
+ else:
84
+ lines.append(f"export function {hook_name}() {{")
85
+
86
+ # useSWR call
87
+ fetcher_params = param_info['fetcher_params']
88
+ if fetcher_params:
89
+ lines.append(f" return useSWR<{response_type}>(")
90
+ lines.append(f" {swr_key},")
91
+ lines.append(f" () => Fetchers.{fetcher_name}({fetcher_params})")
92
+ lines.append(" )")
93
+ else:
94
+ lines.append(f" return useSWR<{response_type}>(")
95
+ lines.append(f" {swr_key},")
96
+ lines.append(f" () => Fetchers.{fetcher_name}()")
97
+ lines.append(" )")
98
+
99
+ lines.append("}")
100
+
101
+ return "\n".join(lines)
102
+
103
+ def generate_mutation_hook(self, operation: IROperationObject) -> str:
104
+ """
105
+ Generate mutation hook for POST/PUT/PATCH/DELETE.
106
+
107
+ Examples:
108
+ >>> generate_mutation_hook(users_create)
109
+ export function useCreateShopProduct() {
110
+ const { mutate } = useSWRConfig()
111
+
112
+ return async (data: ProductCreateRequest) => {
113
+ const result = await Fetchers.createShopProduct(data)
114
+ mutate('shop-products')
115
+ return result
116
+ }
117
+ }
118
+ """
119
+ # Get hook name
120
+ hook_name = self._operation_to_hook_name(operation)
121
+
122
+ # Get fetcher function name
123
+ fetcher_name = self._operation_to_fetcher_name(operation)
124
+
125
+ # Get parameters
126
+ param_info = self._get_param_info(operation)
127
+
128
+ # Get response type
129
+ response_type = self._get_response_type(operation)
130
+
131
+ # Get revalidation keys
132
+ revalidation_keys = self._get_revalidation_keys(operation)
133
+
134
+ # Build hook
135
+ lines = []
136
+
137
+ # JSDoc
138
+ lines.append("/**")
139
+ if operation.summary:
140
+ lines.append(f" * {operation.summary}")
141
+ lines.append(" *")
142
+ lines.append(f" * @method {operation.http_method}")
143
+ lines.append(f" * @path {operation.path}")
144
+ lines.append(" */")
145
+
146
+ # Hook signature
147
+ lines.append(f"export function {hook_name}() {{")
148
+ lines.append(" const { mutate } = useSWRConfig()")
149
+ lines.append("")
150
+
151
+ # Return async function
152
+ if param_info['func_params']:
153
+ lines.append(f" return async ({param_info['func_params']}): Promise<{response_type}> => {{")
154
+ else:
155
+ lines.append(f" return async (): Promise<{response_type}> => {{")
156
+
157
+ # Call fetcher
158
+ fetcher_params = param_info['fetcher_params']
159
+ if fetcher_params:
160
+ lines.append(f" const result = await Fetchers.{fetcher_name}({fetcher_params})")
161
+ else:
162
+ lines.append(f" const result = await Fetchers.{fetcher_name}()")
163
+
164
+ # Revalidate
165
+ if revalidation_keys:
166
+ lines.append("")
167
+ lines.append(" // Revalidate related queries")
168
+ for key in revalidation_keys:
169
+ lines.append(f" mutate('{key}')")
170
+
171
+ lines.append("")
172
+ lines.append(" return result")
173
+ lines.append(" }")
174
+ lines.append("}")
175
+
176
+ return "\n".join(lines)
177
+
178
+ def _operation_to_hook_name(self, operation: IROperationObject) -> str:
179
+ """
180
+ Convert operation to hook name.
181
+
182
+ Examples:
183
+ users_list (GET) -> useUsersList
184
+ users_retrieve (GET) -> useUsersById
185
+ users_create (POST) -> useCreateUsers
186
+ users_update (PUT) -> useUpdateUsers
187
+ users_partial_update (PATCH) -> usePartialUpdateUsers
188
+ users_destroy (DELETE) -> useDeleteUsers
189
+ """
190
+ op_id = operation.operation_id
191
+
192
+ # Keep full resource name and add suffixes for uniqueness
193
+ if op_id.endswith("_list"):
194
+ resource = op_id.removesuffix("_list")
195
+ return f"use{self._to_pascal_case(resource)}List"
196
+ elif op_id.endswith("_retrieve"):
197
+ resource = op_id.removesuffix("_retrieve")
198
+ # Add ById suffix to distinguish from list
199
+ return f"use{self._to_pascal_case(resource)}ById"
200
+ elif op_id.endswith("_create"):
201
+ resource = op_id.removesuffix("_create")
202
+ return f"useCreate{self._to_pascal_case(resource)}"
203
+ elif op_id.endswith("_partial_update"):
204
+ resource = op_id.removesuffix("_partial_update")
205
+ return f"usePartialUpdate{self._to_pascal_case(resource)}"
206
+ elif op_id.endswith("_update"):
207
+ resource = op_id.removesuffix("_update")
208
+ return f"useUpdate{self._to_pascal_case(resource)}"
209
+ elif op_id.endswith("_destroy"):
210
+ resource = op_id.removesuffix("_destroy")
211
+ return f"useDelete{self._to_pascal_case(resource)}"
212
+ else:
213
+ # Custom action
214
+ return f"use{self._to_pascal_case(op_id)}"
215
+
216
+ def _operation_to_fetcher_name(self, operation: IROperationObject) -> str:
217
+ """Get corresponding fetcher function name (must match fetchers_generator logic)."""
218
+ op_id = operation.operation_id
219
+
220
+ # Must match fetchers_generator._operation_to_function_name() exactly
221
+ if op_id.endswith("_list"):
222
+ resource = op_id.removesuffix("_list")
223
+ return f"get{self._to_pascal_case(resource)}List"
224
+ elif op_id.endswith("_retrieve"):
225
+ resource = op_id.removesuffix("_retrieve")
226
+ # Add ById suffix to match fetchers_generator
227
+ return f"get{self._to_pascal_case(resource)}ById"
228
+ elif op_id.endswith("_create"):
229
+ resource = op_id.removesuffix("_create")
230
+ return f"create{self._to_pascal_case(resource)}"
231
+ elif op_id.endswith("_partial_update"):
232
+ resource = op_id.removesuffix("_partial_update")
233
+ return f"partialUpdate{self._to_pascal_case(resource)}"
234
+ elif op_id.endswith("_update"):
235
+ resource = op_id.removesuffix("_update")
236
+ return f"update{self._to_pascal_case(resource)}"
237
+ elif op_id.endswith("_destroy"):
238
+ resource = op_id.removesuffix("_destroy")
239
+ return f"delete{self._to_pascal_case(resource)}"
240
+ else:
241
+ return self._to_camel_case(op_id)
242
+
243
+ def _get_param_info(self, operation: IROperationObject) -> dict:
244
+ """
245
+ Get parameter info for hook.
246
+
247
+ Returns:
248
+ {
249
+ 'func_params': Function parameters for hook signature
250
+ 'fetcher_params': Parameters to pass to fetcher
251
+ }
252
+ """
253
+ func_params = []
254
+ fetcher_params = []
255
+
256
+ # Path parameters
257
+ if operation.path_parameters:
258
+ for param in operation.path_parameters:
259
+ param_type = self._map_param_type(param.schema_type)
260
+ func_params.append(f"{param.name}: {param_type}")
261
+ fetcher_params.append(param.name)
262
+
263
+ # Query parameters
264
+ if operation.query_parameters:
265
+ query_fields = []
266
+ all_required = all(param.required for param in operation.query_parameters)
267
+
268
+ for param in operation.query_parameters:
269
+ param_type = self._map_param_type(param.schema_type)
270
+ optional = "?" if not param.required else ""
271
+ query_fields.append(f"{param.name}{optional}: {param_type}")
272
+
273
+ if query_fields:
274
+ params_optional = "" if all_required else "?"
275
+ func_params.append(f"params{params_optional}: {{ {'; '.join(query_fields)} }}")
276
+ fetcher_params.append("params")
277
+
278
+ # Request body
279
+ if operation.request_body:
280
+ schema_name = operation.request_body.schema_name
281
+ # Use schema only if it exists as a component (not inline)
282
+ if schema_name and schema_name in self.context.schemas:
283
+ body_type = schema_name
284
+ else:
285
+ body_type = "any"
286
+ func_params.append(f"data: {body_type}")
287
+ fetcher_params.append("data")
288
+
289
+ return {
290
+ 'func_params': ", ".join(func_params) if func_params else "",
291
+ 'fetcher_params': ", ".join(fetcher_params) if fetcher_params else ""
292
+ }
293
+
294
+ def _map_param_type(self, param_type: str) -> str:
295
+ """Map OpenAPI param type to TypeScript type."""
296
+ type_map = {
297
+ "integer": "number",
298
+ "number": "number",
299
+ "string": "string",
300
+ "boolean": "boolean",
301
+ "array": "any[]",
302
+ "object": "any",
303
+ }
304
+ return type_map.get(param_type, "any")
305
+
306
+ def _get_response_type(self, operation: IROperationObject) -> str:
307
+ """Get response type for hook."""
308
+ # Get 2xx response
309
+ for status_code in [200, 201, 202, 204]:
310
+ if status_code in operation.responses:
311
+ response = operation.responses[status_code]
312
+ if response.schema_name:
313
+ return response.schema_name
314
+
315
+ # No response or void
316
+ if 204 in operation.responses or operation.http_method == "DELETE":
317
+ return "void"
318
+
319
+ return "any"
320
+
321
+ def _generate_swr_key(self, operation: IROperationObject) -> str:
322
+ """
323
+ Generate SWR key for query.
324
+
325
+ Examples:
326
+ GET /products/ -> 'shop-products'
327
+ GET /products/{id}/ -> ['shop-product', id]
328
+ GET /products/?category=5 -> ['shop-products', params]
329
+ """
330
+ # Get resource name from operation_id
331
+ op_id = operation.operation_id
332
+
333
+ # Determine if list or detail
334
+ is_list = op_id.endswith("_list")
335
+ is_detail = op_id.endswith("_retrieve")
336
+
337
+ # Remove common suffixes
338
+ resource = op_id.replace("_list", "").replace("_retrieve", "")
339
+
340
+ # For detail views, use singular form
341
+ if is_detail:
342
+ resource = resource.rstrip('s') if resource.endswith('s') and len(resource) > 1 else resource
343
+
344
+ # Convert to kebab-case
345
+ key_base = resource.replace("_", "-")
346
+
347
+ # Check if has path params or query params
348
+ has_path_params = bool(operation.path_parameters)
349
+ has_query_params = bool(operation.query_parameters)
350
+
351
+ if has_path_params:
352
+ # Single resource: ['shop-product', id]
353
+ param_name = operation.path_parameters[0].name
354
+ return f"['{key_base}', {param_name}]"
355
+ elif has_query_params:
356
+ # List with params: params ? ['shop-products', params] : 'shop-products'
357
+ return f"params ? ['{key_base}', params] : '{key_base}'"
358
+ else:
359
+ # Simple key: 'shop-products'
360
+ return f"'{key_base}'"
361
+
362
+ def _get_revalidation_keys(self, operation: IROperationObject) -> list[str]:
363
+ """
364
+ Get SWR keys that should be revalidated after mutation.
365
+
366
+ Examples:
367
+ POST /products/ -> ['shop-products']
368
+ PUT /products/{id}/ -> ['shop-products', 'shop-product']
369
+ DELETE /products/{id}/ -> ['shop-products']
370
+ """
371
+ keys = []
372
+
373
+ op_id = operation.operation_id
374
+ resource = op_id.replace("_create", "").replace("_update", "").replace("_partial_update", "").replace("_destroy", "")
375
+
376
+ # List key (for revalidating lists)
377
+ list_key = f"{resource.replace('_', '-')}"
378
+ keys.append(list_key)
379
+
380
+ # Detail key (for update/delete operations)
381
+ if operation.http_method in ("PUT", "PATCH", "DELETE"):
382
+ detail_key = f"{resource.replace('_', '-').rstrip('s')}"
383
+ if detail_key != list_key:
384
+ keys.append(detail_key)
385
+
386
+ return keys
387
+
388
+ def _to_pascal_case(self, snake_str: str) -> str:
389
+ """Convert snake_case to PascalCase."""
390
+ return ''.join(word.capitalize() for word in snake_str.split('_'))
391
+
392
+ def _to_camel_case(self, snake_str: str) -> str:
393
+ """Convert snake_case to camelCase."""
394
+ components = snake_str.split('_')
395
+ return components[0] + ''.join(x.capitalize() for x in components[1:])
396
+
397
+ def generate_tag_hooks_file(
398
+ self,
399
+ tag: str,
400
+ operations: list[IROperationObject],
401
+ ) -> GeneratedFile:
402
+ """
403
+ Generate hooks file for a specific tag/resource.
404
+
405
+ Args:
406
+ tag: Tag name (e.g., "shop_products")
407
+ operations: List of operations for this tag
408
+
409
+ Returns:
410
+ GeneratedFile with hooks
411
+ """
412
+ # Separate queries and mutations & collect schema names
413
+ query_hooks = []
414
+ mutation_hooks = []
415
+ schema_names = set()
416
+
417
+ for operation in operations:
418
+ # Collect schemas used in this operation (only if they exist as components)
419
+ if operation.request_body and operation.request_body.schema_name:
420
+ if operation.request_body.schema_name in self.context.schemas:
421
+ schema_names.add(operation.request_body.schema_name)
422
+ if operation.patch_request_body and operation.patch_request_body.schema_name:
423
+ if operation.patch_request_body.schema_name in self.context.schemas:
424
+ schema_names.add(operation.patch_request_body.schema_name)
425
+
426
+ # Get response schema
427
+ response = operation.primary_success_response
428
+ if response and response.schema_name:
429
+ schema_names.add(response.schema_name)
430
+
431
+ # Generate hook
432
+ if operation.http_method == "GET":
433
+ query_hooks.append(self.generate_query_hook(operation))
434
+ else:
435
+ mutation_hooks.append(self.generate_mutation_hook(operation))
436
+
437
+ # Get display name for documentation
438
+ tag_display_name = self.base.tag_to_display_name(tag)
439
+
440
+ # Build file content
441
+ lines = []
442
+
443
+ # Header
444
+ lines.append("/**")
445
+ lines.append(f" * SWR Hooks for {tag_display_name}")
446
+ lines.append(" *")
447
+ lines.append(" * Auto-generated React hooks for data fetching with SWR.")
448
+ lines.append(" *")
449
+ lines.append(" * Setup:")
450
+ lines.append(" * ```typescript")
451
+ lines.append(" * // Configure API once (in your app root)")
452
+ lines.append(" * import { configureAPI } from '../../api-instance'")
453
+ lines.append(" * configureAPI({ baseUrl: 'https://api.example.com' })")
454
+ lines.append(" * ```")
455
+ lines.append(" *")
456
+ lines.append(" * Usage:")
457
+ lines.append(" * ```typescript")
458
+ lines.append(" * // Query hook")
459
+ lines.append(" * const { data, error, mutate } = useShopProducts({ page: 1 })")
460
+ lines.append(" *")
461
+ lines.append(" * // Mutation hook")
462
+ lines.append(" * const createProduct = useCreateShopProduct()")
463
+ lines.append(" * await createProduct({ name: 'Product', price: 99 })")
464
+ lines.append(" * ```")
465
+ lines.append(" */")
466
+
467
+ # Import types from schemas
468
+ for schema_name in sorted(schema_names):
469
+ lines.append(f"import type {{ {schema_name} }} from '../schemas/{schema_name}.schema'")
470
+
471
+ lines.append("import useSWR from 'swr'")
472
+ lines.append("import { useSWRConfig } from 'swr'")
473
+ lines.append("import * as Fetchers from '../fetchers'")
474
+ lines.append("")
475
+
476
+ # Query hooks
477
+ if query_hooks:
478
+ lines.append("// ===== Query Hooks (GET) =====")
479
+ lines.append("")
480
+ for hook in query_hooks:
481
+ lines.append(hook)
482
+ lines.append("")
483
+
484
+ # Mutation hooks
485
+ if mutation_hooks:
486
+ lines.append("// ===== Mutation Hooks (POST/PUT/PATCH/DELETE) =====")
487
+ lines.append("")
488
+ for hook in mutation_hooks:
489
+ lines.append(hook)
490
+ lines.append("")
491
+
492
+ content = "\n".join(lines)
493
+
494
+ # Get file path (use same naming as APIClient)
495
+ folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
496
+ file_path = f"_utils/hooks/{folder_name}.ts"
497
+
498
+ return GeneratedFile(
499
+ path=file_path,
500
+ content=content,
501
+ description=f"SWR hooks for {tag_display_name}",
502
+ )
503
+
504
+ def generate_hooks_index_file(self, module_names: list[str]) -> GeneratedFile:
505
+ """Generate index.ts for hooks folder."""
506
+ lines = []
507
+
508
+ lines.append("/**")
509
+ lines.append(" * SWR Hooks - React hooks for data fetching")
510
+ lines.append(" *")
511
+ lines.append(" * Auto-generated from OpenAPI specification.")
512
+ lines.append(" * These hooks use SWR for data fetching and caching.")
513
+ lines.append(" *")
514
+ lines.append(" * Usage:")
515
+ lines.append(" * ```typescript")
516
+ lines.append(" * import { useShopProducts } from './_utils/hooks'")
517
+ lines.append(" *")
518
+ lines.append(" * function ProductsPage() {")
519
+ lines.append(" * const { data, error } = useShopProducts({ page: 1 })")
520
+ lines.append(" * if (error) return <Error />")
521
+ lines.append(" * if (!data) return <Loading />")
522
+ lines.append(" * return <ProductList products={data.results} />")
523
+ lines.append(" * }")
524
+ lines.append(" * ```")
525
+ lines.append(" */")
526
+ lines.append("")
527
+
528
+ for module_name in module_names:
529
+ lines.append(f"export * from './{module_name}'")
530
+
531
+ lines.append("")
532
+
533
+ content = "\n".join(lines)
534
+
535
+ return GeneratedFile(
536
+ path="_utils/hooks/index.ts",
537
+ content=content,
538
+ description="Index file for SWR hooks",
539
+ )