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,165 @@
1
+ """
2
+ TypeScript Client Generator - Generates TypeScript APIClient classes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from jinja2 import Environment
8
+ from ...ir import IROperationObject
9
+ from ..base import GeneratedFile
10
+
11
+ class ClientGenerator:
12
+ """Generates TypeScript APIClient classes (flat and namespaced)."""
13
+
14
+ def __init__(self, jinja_env: Environment, context, base, operations_gen):
15
+ self.jinja_env = jinja_env
16
+ self.context = context
17
+ self.base = base
18
+ self.operations_gen = operations_gen
19
+
20
+ def generate_client_file(self):
21
+ """Generate client.ts with APIClient class."""
22
+
23
+
24
+ # Client class
25
+ client_code = self._generate_client_class()
26
+
27
+ template = self.jinja_env.get_template('client_file.ts.jinja')
28
+ content = template.render(
29
+ has_enums=bool(self.base.get_enum_schemas()),
30
+ client_code=client_code
31
+ )
32
+
33
+ return GeneratedFile(
34
+ path="client.ts",
35
+ content=content,
36
+ description="APIClient with HTTP adapter and error handling",
37
+ )
38
+
39
+ def _generate_client_class(self) -> str:
40
+ """Generate APIClient class."""
41
+ if self.base.client_structure == "namespaced":
42
+ return self._generate_namespaced_client()
43
+ else:
44
+ return self._generate_flat_client()
45
+
46
+ def _generate_flat_client(self) -> str:
47
+ """Generate flat APIClient (all methods in one class)."""
48
+ # Generate all operation methods
49
+ method_codes = []
50
+ for op_id, operation in self.context.operations.items():
51
+ method_codes.append(self.operations_gen.generate_operation(operation))
52
+
53
+ template = self.jinja_env.get_template('client/flat_client.ts.jinja')
54
+ return template.render(
55
+ api_title=self.context.openapi_info.title,
56
+ operations=method_codes
57
+ )
58
+
59
+ def _generate_namespaced_client(self) -> str:
60
+ """Generate namespaced APIClient (sub-clients per tag)."""
61
+ # Group operations by tag (using base class method)
62
+ ops_by_tag = self.base.group_operations_by_tag()
63
+
64
+ # Generate sub-client classes
65
+ sub_client_classes = []
66
+ for tag, operations in sorted(ops_by_tag.items()):
67
+ sub_client_classes.append(self._generate_sub_client_class(tag, operations))
68
+
69
+ sub_clients_code = "\n\n".join(sub_client_classes)
70
+
71
+ # Generate main APIClient
72
+ main_client_code = self._generate_main_client_class(list(ops_by_tag.keys()))
73
+
74
+ return f"{sub_clients_code}\n\n{main_client_code}"
75
+
76
+ def _generate_sub_client_class(self, tag: str, operations: list) -> str:
77
+ """Generate sub-client class for a specific tag."""
78
+ class_name = self.base.tag_to_class_name(tag)
79
+
80
+ # Generate methods for this tag
81
+ method_codes = []
82
+ for operation in operations:
83
+ method_codes.append(self.operations_gen.generate_operation(operation, remove_tag_prefix=True, in_subclient=True))
84
+
85
+ template = self.jinja_env.get_template('client/sub_client.ts.jinja')
86
+ return template.render(
87
+ tag=self.base.tag_to_display_name(tag),
88
+ class_name=class_name,
89
+ operations=method_codes
90
+ )
91
+
92
+ def _generate_main_client_class(self, ops_by_tag: dict) -> str:
93
+ """Generate main APIClient with sub-clients."""
94
+ tags = sorted(ops_by_tag.keys())
95
+
96
+ # Prepare data for template
97
+ tags_data = [
98
+ {
99
+ "class_name": self.base.tag_to_class_name(tag),
100
+ "property": self.base.tag_to_property_name(tag),
101
+ "slug": self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
102
+ }
103
+ for tag in tags
104
+ ]
105
+
106
+ template = self.jinja_env.get_template('client/client.ts.jinja')
107
+ return template.render(
108
+ sub_clients=True,
109
+ include_imports=False, # Imports already in main_client_file.ts.jinja
110
+ tags=tags_data,
111
+ info={"title": self.context.openapi_info.title},
112
+ )
113
+
114
+ def generate_main_client_file(self, ops_by_tag: dict):
115
+ """Generate main client.ts with APIClient."""
116
+
117
+ tags = sorted(ops_by_tag.keys())
118
+
119
+ # Prepare tags data for template
120
+ tags_data = [
121
+ {
122
+ "class_name": self.base.tag_to_class_name(tag),
123
+ "slug": self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
124
+ }
125
+ for tag in tags
126
+ ]
127
+
128
+ # Generate main APIClient class
129
+ client_code = self._generate_main_client_class(ops_by_tag)
130
+
131
+ template = self.jinja_env.get_template('client/main_client_file.ts.jinja')
132
+ content = template.render(
133
+ tags=tags_data,
134
+ client_code=client_code
135
+ )
136
+
137
+ return GeneratedFile(
138
+ path="client.ts",
139
+ content=content,
140
+ description="Main API client with HTTP adapter and error handling",
141
+ )
142
+
143
+ def generate_app_client_file(self, tag: str, operations: list[IROperationObject]):
144
+ """Generate client.ts for a specific app."""
145
+
146
+ class_name = self.base.tag_to_class_name(tag)
147
+
148
+ # Generate methods
149
+ method_codes = []
150
+ for operation in operations:
151
+ method_codes.append(self.operations_gen.generate_operation(operation, remove_tag_prefix=True, in_subclient=True))
152
+
153
+ template = self.jinja_env.get_template('client/app_client.ts.jinja')
154
+ content = template.render(
155
+ tag=self.base.tag_to_display_name(tag),
156
+ class_name=class_name,
157
+ operations=method_codes
158
+ )
159
+
160
+ folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
161
+ return GeneratedFile(
162
+ path=f"{folder_name}/client.ts",
163
+ content=content,
164
+ description=f"API client for {tag}",
165
+ )
@@ -0,0 +1,428 @@
1
+ """
2
+ Fetchers Generator - Generates typed fetcher functions from IR.
3
+
4
+ This generator creates universal TypeScript functions that:
5
+ - Use Zod schemas for runtime validation
6
+ - Work in any environment (Next.js, React Native, Node.js)
7
+ - Are type-safe with proper TypeScript types
8
+ - Can be used with any data-fetching library
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from jinja2 import Environment
14
+ from ..base import GeneratedFile, BaseGenerator
15
+ from ...ir import IRContext, IROperationObject
16
+
17
+
18
+ class FetchersGenerator:
19
+ """
20
+ Generate typed fetcher functions from IR operations.
21
+
22
+ Features:
23
+ - Runtime validation with Zod
24
+ - Type-safe parameters and responses
25
+ - Works with any data-fetching library (SWR, React Query)
26
+ - Server Component compatible
27
+ """
28
+
29
+ def __init__(self, jinja_env: Environment, context: IRContext, base: BaseGenerator):
30
+ self.jinja_env = jinja_env
31
+ self.context = context
32
+ self.base = base
33
+
34
+ def generate_fetcher_function(self, operation: IROperationObject) -> str:
35
+ """
36
+ Generate a single fetcher function for an operation.
37
+
38
+ Args:
39
+ operation: IROperationObject to convert to fetcher
40
+
41
+ Returns:
42
+ TypeScript fetcher function code
43
+
44
+ Examples:
45
+ >>> generate_fetcher_function(users_list)
46
+ export async function getUsers(params?: GetUsersParams): Promise<PaginatedUser> {
47
+ const response = await api.users.list(params)
48
+ return PaginatedUserSchema.parse(response)
49
+ }
50
+ """
51
+ # Get function name (e.g., "getUsers", "createUser")
52
+ func_name = self._operation_to_function_name(operation)
53
+
54
+ # Get parameters structure
55
+ param_info = self._get_param_structure(operation)
56
+
57
+ # Get response type and schema
58
+ response_type, response_schema = self._get_response_info(operation)
59
+
60
+ # Get API client call
61
+ api_call = self._get_api_call(operation)
62
+
63
+ # Build JSDoc comment
64
+ jsdoc = self._generate_jsdoc(operation, func_name)
65
+
66
+ # Build function
67
+ lines = []
68
+
69
+ # JSDoc
70
+ if jsdoc:
71
+ lines.append(jsdoc)
72
+
73
+ # Function signature with optional client parameter
74
+ if param_info['func_params']:
75
+ lines.append(f"export async function {func_name}(")
76
+ lines.append(f" {param_info['func_params']},")
77
+ lines.append(f" client?: API")
78
+ lines.append(f"): Promise<{response_type}> {{")
79
+ else:
80
+ lines.append(f"export async function {func_name}(")
81
+ lines.append(f" client?: API")
82
+ lines.append(f"): Promise<{response_type}> {{")
83
+
84
+ # Get client instance (either passed or global)
85
+ lines.append(" const api = client || getAPIInstance()")
86
+ lines.append("")
87
+
88
+ # Function body - build API call
89
+ api_call_params = param_info['api_call_params']
90
+ # Replace API. with api.
91
+ api_call_instance = api_call.replace("API.", "api.")
92
+
93
+ if api_call_params:
94
+ lines.append(f" const response = await {api_call_instance}({api_call_params})")
95
+ else:
96
+ lines.append(f" const response = await {api_call_instance}()")
97
+
98
+ # Validation with Zod
99
+ if response_schema:
100
+ lines.append(f" return {response_schema}.parse(response)")
101
+ else:
102
+ lines.append(" return response")
103
+
104
+ lines.append("}")
105
+
106
+ return "\n".join(lines)
107
+
108
+ def _operation_to_function_name(self, operation: IROperationObject) -> str:
109
+ """
110
+ Convert operation to function name.
111
+
112
+ Examples:
113
+ users_list (GET) -> getUsers
114
+ users_retrieve (GET) -> getUser
115
+ users_create (POST) -> createUser
116
+ users_update (PUT) -> updateUser
117
+ users_partial_update (PATCH) -> updateUser
118
+ users_destroy (DELETE) -> deleteUser
119
+ """
120
+ # Remove tag prefix from operation_id
121
+ op_id = operation.operation_id
122
+
123
+ # Handle common patterns (remove only suffix, not all occurrences)
124
+ if op_id.endswith("_list"):
125
+ resource = op_id.removesuffix("_list")
126
+ return f"get{self._to_pascal_case(resource)}"
127
+ elif op_id.endswith("_retrieve"):
128
+ resource = op_id.removesuffix("_retrieve")
129
+ # Singular
130
+ return f"get{self._to_pascal_case(resource).rstrip('s')}"
131
+ elif op_id.endswith("_create"):
132
+ resource = op_id.removesuffix("_create")
133
+ return f"create{self._to_pascal_case(resource)}"
134
+ elif op_id.endswith("_partial_update"):
135
+ resource = op_id.removesuffix("_partial_update")
136
+ return f"partialUpdate{self._to_pascal_case(resource)}"
137
+ elif op_id.endswith("_update"):
138
+ resource = op_id.removesuffix("_update")
139
+ return f"update{self._to_pascal_case(resource)}"
140
+ elif op_id.endswith("_destroy"):
141
+ resource = op_id.removesuffix("_destroy")
142
+ return f"delete{self._to_pascal_case(resource)}"
143
+ else:
144
+ # Custom action - use operation_id as is
145
+ return self._to_camel_case(op_id)
146
+
147
+ def _to_pascal_case(self, snake_str: str) -> str:
148
+ """Convert snake_case to PascalCase."""
149
+ return ''.join(word.capitalize() for word in snake_str.split('_'))
150
+
151
+ def _to_camel_case(self, snake_str: str) -> str:
152
+ """Convert snake_case to camelCase."""
153
+ components = snake_str.split('_')
154
+ return components[0] + ''.join(x.capitalize() for x in components[1:])
155
+
156
+ def _get_param_structure(self, operation: IROperationObject) -> dict:
157
+ """
158
+ Get structured parameter information for function generation.
159
+
160
+ Returns dict with:
161
+ - func_params: Function signature params (e.g., "slug: string, params?: { page?: number }")
162
+ - api_call_params: API call params (e.g., "slug, params" or "slug" or "params")
163
+
164
+ Examples:
165
+ GET /users/{id}/ -> {
166
+ func_params: "id: number",
167
+ api_call_params: "id"
168
+ }
169
+
170
+ GET /users/ with query params -> {
171
+ func_params: "params?: { page?: number }",
172
+ api_call_params: "params"
173
+ }
174
+
175
+ GET /users/{id}/ with query params -> {
176
+ func_params: "id: number, params?: { page?: number }",
177
+ api_call_params: "id, params"
178
+ }
179
+
180
+ POST /users/ -> {
181
+ func_params: "data: UserRequest",
182
+ api_call_params: "data"
183
+ }
184
+
185
+ POST /users/{id}/action/ -> {
186
+ func_params: "id: number, data: ActionRequest",
187
+ api_call_params: "id, data"
188
+ }
189
+ """
190
+ func_params = []
191
+ api_call_params = []
192
+
193
+ # Path parameters (always passed individually)
194
+ if operation.path_parameters:
195
+ for param in operation.path_parameters:
196
+ param_type = self._map_param_type(param.schema_type)
197
+ func_params.append(f"{param.name}: {param_type}")
198
+ api_call_params.append(param.name)
199
+
200
+ # Query parameters (passed as params object, but unpacked when calling API)
201
+ if operation.query_parameters:
202
+ query_fields = []
203
+ # params is required only if all parameters are required
204
+ all_required = all(param.required for param in operation.query_parameters)
205
+ params_accessor = "params." if all_required else "params?."
206
+
207
+ for param in operation.query_parameters:
208
+ param_type = self._map_param_type(param.schema_type)
209
+ optional = "?" if not param.required else ""
210
+ query_fields.append(f"{param.name}{optional}: {param_type}")
211
+ # Unpack from params object when calling API
212
+ api_call_params.append(f"{params_accessor}{param.name}")
213
+
214
+ if query_fields:
215
+ params_optional = "" if all_required else "?"
216
+ func_params.append(f"params{params_optional}: {{ {'; '.join(query_fields)} }}")
217
+
218
+ # Request body (passed as data)
219
+ if operation.request_body:
220
+ schema_name = operation.request_body.schema_name
221
+ # Use schema only if it exists as a component (not inline)
222
+ if schema_name and schema_name in self.context.schemas:
223
+ body_type = schema_name
224
+ else:
225
+ body_type = "any"
226
+ func_params.append(f"data: {body_type}")
227
+ api_call_params.append("data")
228
+
229
+ return {
230
+ 'func_params': ", ".join(func_params) if func_params else "",
231
+ 'api_call_params': ", ".join(api_call_params) if api_call_params else ""
232
+ }
233
+
234
+ def _get_params_type(self, operation: IROperationObject) -> tuple[str, bool]:
235
+ """
236
+ Get parameters type definition.
237
+
238
+ Returns:
239
+ (type_definition, has_params)
240
+
241
+ Examples:
242
+ ("params?: { page?: number; page_size?: number }", True)
243
+ ("id: number", True)
244
+ ("", False)
245
+ """
246
+ params = []
247
+
248
+ # Path parameters
249
+ if operation.path_parameters:
250
+ for param in operation.path_parameters:
251
+ param_type = self._map_param_type(param.schema_type)
252
+ params.append(f"{param.name}: {param_type}")
253
+
254
+ # Query parameters
255
+ if operation.query_parameters:
256
+ query_fields = []
257
+ all_required = all(param.required for param in operation.query_parameters)
258
+
259
+ for param in operation.query_parameters:
260
+ param_type = self._map_param_type(param.schema_type)
261
+ optional = "?" if not param.required else ""
262
+ query_fields.append(f"{param.name}{optional}: {param_type}")
263
+
264
+ if query_fields:
265
+ params_optional = "" if all_required else "?"
266
+ params.append(f"params{params_optional}: {{ {'; '.join(query_fields)} }}")
267
+
268
+ # Request body
269
+ if operation.request_body:
270
+ schema_name = operation.request_body.schema_name
271
+ # Use schema only if it exists as a component (not inline)
272
+ if schema_name and schema_name in self.context.schemas:
273
+ body_type = schema_name
274
+ else:
275
+ body_type = "any"
276
+ params.append(f"data: {body_type}")
277
+
278
+ if not params:
279
+ return ("", False)
280
+
281
+ return (", ".join(params), True)
282
+
283
+ def _map_param_type(self, param_type: str) -> str:
284
+ """Map OpenAPI param type to TypeScript type."""
285
+ type_map = {
286
+ "integer": "number",
287
+ "number": "number",
288
+ "string": "string",
289
+ "boolean": "boolean",
290
+ "array": "any[]",
291
+ "object": "any",
292
+ }
293
+ return type_map.get(param_type, "any")
294
+
295
+ def _get_response_info(self, operation: IROperationObject) -> tuple[str, str | None]:
296
+ """
297
+ Get response type and schema name.
298
+
299
+ Returns:
300
+ (response_type, response_schema_name)
301
+
302
+ Examples:
303
+ ("PaginatedUser", "PaginatedUserSchema")
304
+ ("User", "UserSchema")
305
+ ("void", None)
306
+ """
307
+ # Get 2xx response
308
+ for status_code in [200, 201, 202, 204]:
309
+ if status_code in operation.responses:
310
+ response = operation.responses[status_code]
311
+ if response.schema_name:
312
+ schema_name = response.schema_name
313
+ return (schema_name, f"{schema_name}Schema")
314
+
315
+ # No response or void
316
+ if 204 in operation.responses or operation.http_method == "DELETE":
317
+ return ("void", None)
318
+
319
+ return ("any", None)
320
+
321
+ def _get_api_call(self, operation: IROperationObject) -> str:
322
+ """
323
+ Get API client method call path.
324
+
325
+ Examples:
326
+ API.users.list
327
+ API.users.retrieve
328
+ API.posts.create
329
+ """
330
+ # Get tag/resource name
331
+ tag = operation.tags[0] if operation.tags else "default"
332
+ tag_property = self.base.tag_to_property_name(tag)
333
+
334
+ # Get method name from operation_id
335
+ method_name = self.base.remove_tag_prefix(operation.operation_id, tag)
336
+ method_name = self._to_camel_case(method_name)
337
+
338
+ return f"API.{tag_property}.{method_name}"
339
+
340
+ def _generate_jsdoc(self, operation: IROperationObject, func_name: str) -> str:
341
+ """Generate JSDoc comment for function."""
342
+ lines = ["/**"]
343
+
344
+ # Summary
345
+ if operation.summary:
346
+ lines.append(f" * {operation.summary}")
347
+ else:
348
+ lines.append(f" * {func_name}")
349
+
350
+ # Description
351
+ if operation.description:
352
+ lines.append(" *")
353
+ for desc_line in operation.description.split("\n"):
354
+ lines.append(f" * {desc_line}")
355
+
356
+ # HTTP method and path
357
+ lines.append(" *")
358
+ lines.append(f" * @method {operation.http_method}")
359
+ lines.append(f" * @path {operation.path}")
360
+
361
+ lines.append(" */")
362
+ return "\n".join(lines)
363
+
364
+ def generate_tag_fetchers_file(
365
+ self,
366
+ tag: str,
367
+ operations: list[IROperationObject],
368
+ ) -> GeneratedFile:
369
+ """
370
+ Generate fetchers file for a specific tag/resource.
371
+
372
+ Args:
373
+ tag: Tag name (e.g., "users", "posts")
374
+ operations: List of operations for this tag
375
+
376
+ Returns:
377
+ GeneratedFile with fetchers
378
+ """
379
+ # Generate individual fetchers
380
+ fetchers = []
381
+ schema_names = set()
382
+
383
+ for operation in operations:
384
+ fetcher_code = self.generate_fetcher_function(operation)
385
+ fetchers.append(fetcher_code)
386
+
387
+ # Collect schema names
388
+ _, response_schema = self._get_response_info(operation)
389
+ if response_schema:
390
+ schema_name = response_schema.replace("Schema", "")
391
+ schema_names.add(schema_name)
392
+
393
+ # Add request body schemas (only if they exist as components)
394
+ if operation.request_body and operation.request_body.schema_name:
395
+ # Only add if schema exists in components (not inline)
396
+ if operation.request_body.schema_name in self.context.schemas:
397
+ schema_names.add(operation.request_body.schema_name)
398
+
399
+ # Get display name and folder name (use same naming as APIClient)
400
+ tag_display_name = self.base.tag_to_display_name(tag)
401
+ folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
402
+
403
+ # Render template
404
+ template = self.jinja_env.get_template("fetchers/fetchers.ts.jinja")
405
+ content = template.render(
406
+ tag_display_name=tag_display_name,
407
+ fetchers=fetchers,
408
+ has_schemas=bool(schema_names),
409
+ schema_names=sorted(schema_names),
410
+ has_client=True,
411
+ )
412
+
413
+ return GeneratedFile(
414
+ path=f"_utils/fetchers/{folder_name}.ts",
415
+ content=content,
416
+ description=f"Typed fetchers for {tag_display_name}",
417
+ )
418
+
419
+ def generate_fetchers_index_file(self, module_names: list[str]) -> GeneratedFile:
420
+ """Generate index.ts for fetchers folder."""
421
+ template = self.jinja_env.get_template("fetchers/index.ts.jinja")
422
+ content = template.render(modules=sorted(module_names))
423
+
424
+ return GeneratedFile(
425
+ path="_utils/fetchers/index.ts",
426
+ content=content,
427
+ description="Fetchers index",
428
+ )