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.
- django_cfg/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/views/api/currencies.py +49 -6
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +73 -49
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -348
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +162 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +208 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +838 -0
- django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
- django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
- django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
- django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
- django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
- django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
- django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
- django_cfg/modules/django_client/core/generator/python/templates/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +153 -0
- django_cfg/modules/django_client/core/generator/python/templates/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +68 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/main_client_file.py.jinja +14 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +52 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/enum_class.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/schema_class.py.jinja +21 -0
- django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
- django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
- django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/client.ts.jinja +403 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/main_client_file.ts.jinja +10 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +268 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/errors.ts.jinja +116 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/logger.ts.jinja +259 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja +158 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +427 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/pytest.ini +30 -0
- django_cfg/modules/django_client/spectacular/__init__.py +10 -0
- django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
- django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
- django_cfg/modules/django_dashboard/management/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
- django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
- django_cfg/modules/django_dashboard/sections/documentation.py +391 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +21 -10
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/METADATA +2 -2
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/RECORD +224 -74
- django_cfg/management/commands/generate.py +0 -107
- /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
- /django_cfg/{dashboard → modules/django_admin}/management/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_admin}/management/commands/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|