django-cfg 1.4.11__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/core/generation/integration_generators/api.py +2 -1
- django_cfg/models/django/openapi.py +4 -80
- django_cfg/modules/django_client/core/archive/manager.py +2 -2
- django_cfg/modules/django_client/core/config/config.py +20 -0
- django_cfg/modules/django_client/core/config/service.py +1 -1
- django_cfg/modules/django_client/core/generator/__init__.py +4 -4
- django_cfg/modules/django_client/core/generator/base.py +71 -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/{templates/python → python/templates}/api_wrapper.py.jinja +25 -2
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client.py.jinja +24 -6
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client_file.py.jinja +1 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/operation_method.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/sub_client.py.jinja +8 -1
- 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/{templates/python → python/templates}/main_init.py.jinja +2 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enum_class.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/schema_class.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -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/{templates/typescript → typescript/templates}/client/app_client.ts.jinja +1 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/client.ts.jinja +77 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/main_client_file.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/sub_client.ts.jinja +3 -3
- 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/{templates/typescript → typescript/templates}/main_index.ts.jinja +73 -11
- 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/{templates/typescript → typescript/templates}/utils/errors.ts.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/logger.ts.jinja +9 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/storage.ts.jinja +54 -10
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -0
- django_cfg/modules/django_client/pytest.ini +30 -0
- django_cfg/modules/django_client/spectacular/__init__.py +3 -2
- django_cfg/modules/django_client/spectacular/async_detection.py +187 -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/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
- django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
- django_cfg/modules/django_unfold/callbacks/main.py +6 -6
- django_cfg/pyproject.toml +1 -1
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/METADATA +1 -1
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/RECORD +99 -70
- django_cfg/modules/django_client/core/generator/python.py +0 -751
- django_cfg/modules/django_client/core/generator/typescript.py +0 -872
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/__init__.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/app_init.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/app_client.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/flat_client.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client_file.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/app_models.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enums.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/models.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/logger.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/schema.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/app_index.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/flat_client.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/operation.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client_file.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/index.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/app_models.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/enums.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/models.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/http.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/schema.ts.jinja +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}/management/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/management/commands/__init__.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/documentation.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.11.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -152,9 +152,10 @@ class APIFrameworksGenerator:
|
|
152
152
|
},
|
153
153
|
"COMPONENT_SPLIT_REQUEST": True,
|
154
154
|
"COMPONENT_SPLIT_PATCH": True,
|
155
|
-
#
|
155
|
+
# Postprocessing hooks
|
156
156
|
"POSTPROCESSING_HOOKS": [
|
157
157
|
"django_cfg.modules.django_client.spectacular.auto_fix_enum_names",
|
158
|
+
"django_cfg.modules.django_client.spectacular.mark_async_operations",
|
158
159
|
],
|
159
160
|
}
|
160
161
|
|
@@ -8,11 +8,11 @@ This replaces django-revolution with a cleaner, faster, type-safe implementation
|
|
8
8
|
"""
|
9
9
|
|
10
10
|
from typing import Dict, Any, Optional
|
11
|
-
from pydantic import
|
11
|
+
from pydantic import Field
|
12
12
|
from django_cfg.modules.django_client.core.config import OpenAPIConfig, OpenAPIGroupConfig
|
13
13
|
|
14
14
|
|
15
|
-
class
|
15
|
+
class OpenAPIClientConfig(OpenAPIConfig):
|
16
16
|
"""
|
17
17
|
Extended OpenAPI configuration with DRF parameters for django-cfg integration.
|
18
18
|
|
@@ -21,9 +21,9 @@ class ExtendedOpenAPIConfig(OpenAPIConfig):
|
|
21
21
|
|
22
22
|
Example:
|
23
23
|
```python
|
24
|
-
from django_cfg import
|
24
|
+
from django_cfg import OpenAPIClientConfig, OpenAPIGroupConfig
|
25
25
|
|
26
|
-
config =
|
26
|
+
config = OpenAPIClientConfig(
|
27
27
|
enabled=True,
|
28
28
|
groups=[
|
29
29
|
OpenAPIGroupConfig(
|
@@ -153,86 +153,10 @@ class ExtendedOpenAPIConfig(OpenAPIConfig):
|
|
153
153
|
description="All django-cfg built-in applications",
|
154
154
|
)
|
155
155
|
|
156
|
-
# Skip individual cfg_* groups - use unified 'cfg' instead
|
157
156
|
return groups_dict
|
158
157
|
|
159
|
-
# Add Support group if enabled
|
160
|
-
if support_enabled and 'cfg_support' not in groups_dict:
|
161
|
-
groups_dict['cfg_support'] = OpenAPIGroupConfig(
|
162
|
-
name="cfg_support",
|
163
|
-
apps=["django_cfg.apps.support"],
|
164
|
-
title="Support API",
|
165
|
-
description="Support tickets and messages API",
|
166
|
-
)
|
167
|
-
|
168
|
-
# Add Accounts group if enabled
|
169
|
-
if accounts_enabled and 'cfg_accounts' not in groups_dict:
|
170
|
-
groups_dict['cfg_accounts'] = OpenAPIGroupConfig(
|
171
|
-
name="cfg_accounts",
|
172
|
-
apps=["django_cfg.apps.accounts"],
|
173
|
-
title="Accounts API",
|
174
|
-
description="User management, OTP, profiles, and activity tracking API",
|
175
|
-
)
|
176
|
-
|
177
|
-
# Add Newsletter group if enabled
|
178
|
-
if newsletter_enabled and 'cfg_newsletter' not in groups_dict:
|
179
|
-
groups_dict['cfg_newsletter'] = OpenAPIGroupConfig(
|
180
|
-
name="cfg_newsletter",
|
181
|
-
apps=["django_cfg.apps.newsletter"],
|
182
|
-
title="Newsletter API",
|
183
|
-
description="Email campaigns, subscriptions, and newsletter management API",
|
184
|
-
)
|
185
|
-
|
186
|
-
# Add Leads group if enabled
|
187
|
-
if leads_enabled and 'cfg_leads' not in groups_dict:
|
188
|
-
groups_dict['cfg_leads'] = OpenAPIGroupConfig(
|
189
|
-
name="cfg_leads",
|
190
|
-
apps=["django_cfg.apps.leads"],
|
191
|
-
title="Leads API",
|
192
|
-
description="Lead collection, contact forms, and CRM integration API",
|
193
|
-
)
|
194
|
-
|
195
|
-
# Add Knowbase group if enabled
|
196
|
-
if knowbase_enabled and 'cfg_knowbase' not in groups_dict:
|
197
|
-
groups_dict['cfg_knowbase'] = OpenAPIGroupConfig(
|
198
|
-
name="cfg_knowbase",
|
199
|
-
apps=["django_cfg.apps.knowbase"],
|
200
|
-
title="Knowbase API",
|
201
|
-
description="Knowledge base, AI chat, embeddings, and search API",
|
202
|
-
)
|
203
|
-
|
204
|
-
# Add Agents group if enabled
|
205
|
-
if agents_enabled and 'cfg_agents' not in groups_dict:
|
206
|
-
groups_dict['cfg_agents'] = OpenAPIGroupConfig(
|
207
|
-
name="cfg_agents",
|
208
|
-
apps=["django_cfg.apps.agents"],
|
209
|
-
title="Agents API",
|
210
|
-
description="Agent definitions, executions, workflows, and tools API",
|
211
|
-
)
|
212
|
-
|
213
|
-
# Add Tasks group if enabled
|
214
|
-
if tasks_enabled and 'cfg_tasks' not in groups_dict:
|
215
|
-
groups_dict['cfg_tasks'] = OpenAPIGroupConfig(
|
216
|
-
name="cfg_tasks",
|
217
|
-
apps=["django_cfg.apps.tasks"],
|
218
|
-
title="Tasks API",
|
219
|
-
description="Tasks, workflows, and automation API",
|
220
|
-
)
|
221
|
-
|
222
|
-
# Add Payments group if enabled
|
223
|
-
if payments_enabled and 'cfg_payments' not in groups_dict:
|
224
|
-
groups_dict['cfg_payments'] = OpenAPIGroupConfig(
|
225
|
-
name="cfg_payments",
|
226
|
-
apps=["django_cfg.apps.payments"],
|
227
|
-
title="Payments API",
|
228
|
-
description="Payments, subscriptions, and billing API",
|
229
|
-
)
|
230
|
-
|
231
158
|
except Exception:
|
232
159
|
pass
|
233
160
|
|
234
161
|
return groups_dict
|
235
162
|
|
236
|
-
|
237
|
-
# Alias for easier import
|
238
|
-
OpenAPIClientConfig = ExtendedOpenAPIConfig
|
@@ -57,12 +57,12 @@ class ArchiveManager:
|
|
57
57
|
|
58
58
|
if python_dir and python_dir.exists():
|
59
59
|
dest = archive_path / "python"
|
60
|
-
shutil.copytree(python_dir, dest)
|
60
|
+
shutil.copytree(python_dir, dest, dirs_exist_ok=True)
|
61
61
|
copied["python"] = str(dest)
|
62
62
|
|
63
63
|
if typescript_dir and typescript_dir.exists():
|
64
64
|
dest = archive_path / "typescript"
|
65
|
-
shutil.copytree(typescript_dir, dest)
|
65
|
+
shutil.copytree(typescript_dir, dest, dirs_exist_ok=True)
|
66
66
|
copied["typescript"] = str(dest)
|
67
67
|
|
68
68
|
# Create metadata
|
@@ -76,6 +76,26 @@ class OpenAPIConfig(BaseModel):
|
|
76
76
|
description="Generate TypeScript client",
|
77
77
|
)
|
78
78
|
|
79
|
+
generate_package_files: bool = Field(
|
80
|
+
default=False,
|
81
|
+
description="Generate package.json (TypeScript) and pyproject.toml (Python)",
|
82
|
+
)
|
83
|
+
|
84
|
+
generate_zod_schemas: bool = Field(
|
85
|
+
default=False,
|
86
|
+
description="Generate Zod schemas for runtime validation (TypeScript only)",
|
87
|
+
)
|
88
|
+
|
89
|
+
generate_fetchers: bool = Field(
|
90
|
+
default=False,
|
91
|
+
description="Generate typed fetcher functions (TypeScript only, requires Zod schemas)",
|
92
|
+
)
|
93
|
+
|
94
|
+
generate_swr_hooks: bool = Field(
|
95
|
+
default=False,
|
96
|
+
description="Generate SWR hooks for React (TypeScript only, requires fetchers)",
|
97
|
+
)
|
98
|
+
|
79
99
|
client_structure: Literal["flat", "namespaced"] = Field(
|
80
100
|
default="namespaced",
|
81
101
|
description=(
|
@@ -59,7 +59,7 @@ class DjangoOpenAPI:
|
|
59
59
|
if not self.config:
|
60
60
|
return {}
|
61
61
|
|
62
|
-
# Use get_groups_with_defaults if available (
|
62
|
+
# Use get_groups_with_defaults if available (OpenAPIClientConfig)
|
63
63
|
if hasattr(self.config, 'get_groups_with_defaults'):
|
64
64
|
return self.config.get_groups_with_defaults()
|
65
65
|
|
@@ -22,10 +22,10 @@ Usage:
|
|
22
22
|
from pathlib import Path
|
23
23
|
from typing import Literal
|
24
24
|
|
25
|
-
from
|
26
|
-
from
|
27
|
-
from
|
28
|
-
from
|
25
|
+
from .base import GeneratedFile
|
26
|
+
from .python import PythonGenerator
|
27
|
+
from .typescript import TypeScriptGenerator
|
28
|
+
from ..ir import IRContext
|
29
29
|
|
30
30
|
__all__ = [
|
31
31
|
"PythonGenerator",
|
@@ -48,6 +48,11 @@ class BaseGenerator(ABC):
|
|
48
48
|
client_structure: str = "namespaced",
|
49
49
|
openapi_schema: dict | None = None,
|
50
50
|
tag_prefix: str = "",
|
51
|
+
package_config: dict | None = None,
|
52
|
+
generate_package_files: bool = False,
|
53
|
+
generate_zod_schemas: bool = False,
|
54
|
+
generate_fetchers: bool = False,
|
55
|
+
generate_swr_hooks: bool = False,
|
51
56
|
):
|
52
57
|
"""
|
53
58
|
Initialize generator with IR context.
|
@@ -57,11 +62,21 @@ class BaseGenerator(ABC):
|
|
57
62
|
client_structure: Client structure ("flat" or "namespaced")
|
58
63
|
openapi_schema: OpenAPI schema dict (for embedding in client)
|
59
64
|
tag_prefix: Prefix to add to all tag names (e.g., "cfg_")
|
65
|
+
package_config: Package configuration (name, version, author, etc.)
|
66
|
+
generate_package_files: Whether to generate package.json/pyproject.toml
|
67
|
+
generate_zod_schemas: Whether to generate Zod schemas (TypeScript only)
|
68
|
+
generate_fetchers: Whether to generate typed fetchers (TypeScript only)
|
69
|
+
generate_swr_hooks: Whether to generate SWR hooks (TypeScript only, React)
|
60
70
|
"""
|
61
71
|
self.context = context
|
62
72
|
self.client_structure = client_structure
|
63
73
|
self.openapi_schema = openapi_schema
|
64
74
|
self.tag_prefix = tag_prefix
|
75
|
+
self.package_config = package_config or {}
|
76
|
+
self.generate_package_files = generate_package_files
|
77
|
+
self.generate_zod_schemas = generate_zod_schemas
|
78
|
+
self.generate_fetchers = generate_fetchers
|
79
|
+
self.generate_swr_hooks = generate_swr_hooks
|
65
80
|
|
66
81
|
# ===== Namespaced Structure Helpers =====
|
67
82
|
|
@@ -765,3 +780,59 @@ class BaseGenerator(ABC):
|
|
765
780
|
lines.append(" ".join(current_line))
|
766
781
|
|
767
782
|
return lines
|
783
|
+
|
784
|
+
def format_enum_description(self, text: str) -> str:
|
785
|
+
"""
|
786
|
+
Format enum description by splitting bullet points.
|
787
|
+
|
788
|
+
Enum descriptions from OpenAPI often have the format:
|
789
|
+
"* `value1` - Desc1 * `value2` - Desc2"
|
790
|
+
|
791
|
+
This method splits them into separate lines:
|
792
|
+
"* `value1` - Desc1\n* `value2` - Desc2"
|
793
|
+
|
794
|
+
Args:
|
795
|
+
text: Enum description text
|
796
|
+
|
797
|
+
Returns:
|
798
|
+
Formatted description with proper line breaks
|
799
|
+
"""
|
800
|
+
if not text:
|
801
|
+
return text
|
802
|
+
|
803
|
+
# Split by " * `" pattern (preserving the first *)
|
804
|
+
import re
|
805
|
+
# Replace " * `" with newline + "* `"
|
806
|
+
formatted = re.sub(r'\s+\*\s+`', '\n* `', text.strip())
|
807
|
+
|
808
|
+
return formatted
|
809
|
+
|
810
|
+
def sanitize_enum_name(self, name: str) -> str:
|
811
|
+
"""
|
812
|
+
Sanitize enum name by converting to PascalCase.
|
813
|
+
|
814
|
+
Examples:
|
815
|
+
"OrderDetail.status" -> "OrderDetailStatus"
|
816
|
+
"Currency.currency_type" -> "CurrencyCurrencyType"
|
817
|
+
"CurrencyList.currency_type" -> "CurrencyListCurrencyType"
|
818
|
+
"User.role" -> "UserRole"
|
819
|
+
|
820
|
+
Args:
|
821
|
+
name: Original enum name (may contain dots, underscores)
|
822
|
+
|
823
|
+
Returns:
|
824
|
+
Sanitized PascalCase name
|
825
|
+
"""
|
826
|
+
# Replace dots with underscores, then split and convert to PascalCase
|
827
|
+
parts = name.replace('.', '_').split('_')
|
828
|
+
result = []
|
829
|
+
for word in parts:
|
830
|
+
if not word:
|
831
|
+
continue
|
832
|
+
# If word is already PascalCase/camelCase, keep it as is
|
833
|
+
# Otherwise capitalize first letter only
|
834
|
+
if word[0].isupper():
|
835
|
+
result.append(word)
|
836
|
+
else:
|
837
|
+
result.append(word[0].upper() + word[1:] if len(word) > 1 else word.upper())
|
838
|
+
return ''.join(result)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"""
|
2
|
+
Python Generator - Generates Python client (Pydantic 2 + httpx).
|
3
|
+
|
4
|
+
This generator creates a complete Python API client from IR:
|
5
|
+
- Pydantic 2 models (Request/Response/Patch splits)
|
6
|
+
- Enum classes from x-enum-varnames
|
7
|
+
- httpx.AsyncClient for async HTTP
|
8
|
+
- Django CSRF/session handling
|
9
|
+
- Type-safe (MyPy strict mode compatible)
|
10
|
+
|
11
|
+
Reference: https://docs.pydantic.dev/latest/
|
12
|
+
"""
|
13
|
+
|
14
|
+
from .generator import PythonGenerator
|
15
|
+
|
16
|
+
__all__ = ['PythonGenerator']
|
@@ -0,0 +1,174 @@
|
|
1
|
+
"""
|
2
|
+
Async Client Generator - Generates async Python clients.
|
3
|
+
|
4
|
+
Handles:
|
5
|
+
- Main APIClient class (httpx.AsyncClient)
|
6
|
+
- Sub-client classes (per tag/app)
|
7
|
+
- Flat vs namespaced structures
|
8
|
+
"""
|
9
|
+
|
10
|
+
from __future__ import annotations
|
11
|
+
|
12
|
+
from jinja2 import Environment
|
13
|
+
|
14
|
+
from ..base import GeneratedFile
|
15
|
+
from ...ir import IROperationObject
|
16
|
+
|
17
|
+
|
18
|
+
class AsyncClientGenerator:
|
19
|
+
"""Generates async Python client files."""
|
20
|
+
|
21
|
+
def __init__(self, jinja_env: Environment, context, base_generator, operations_gen):
|
22
|
+
"""
|
23
|
+
Initialize async client generator.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
jinja_env: Jinja2 environment for templates
|
27
|
+
context: Generation context
|
28
|
+
base_generator: Reference to base generator
|
29
|
+
operations_gen: Operations generator instance
|
30
|
+
"""
|
31
|
+
self.jinja_env = jinja_env
|
32
|
+
self.context = context
|
33
|
+
self.base = base_generator
|
34
|
+
self.operations_gen = operations_gen
|
35
|
+
|
36
|
+
def generate_client_file(self) -> GeneratedFile:
|
37
|
+
"""Generate client.py with AsyncClient."""
|
38
|
+
# Client class
|
39
|
+
client_code = self._generate_client_class()
|
40
|
+
|
41
|
+
template = self.jinja_env.get_template('client_file.py.jinja')
|
42
|
+
content = template.render(
|
43
|
+
has_enums=bool(self.base.get_enum_schemas()),
|
44
|
+
client_code=client_code
|
45
|
+
)
|
46
|
+
|
47
|
+
return GeneratedFile(
|
48
|
+
path="client.py",
|
49
|
+
content=content,
|
50
|
+
description="AsyncClient with httpx",
|
51
|
+
)
|
52
|
+
|
53
|
+
def _generate_client_class(self) -> str:
|
54
|
+
"""Generate APIClient class."""
|
55
|
+
if self.base.client_structure == "namespaced":
|
56
|
+
return self._generate_namespaced_client()
|
57
|
+
else:
|
58
|
+
return self._generate_flat_client()
|
59
|
+
|
60
|
+
def _generate_flat_client(self) -> str:
|
61
|
+
"""Generate flat APIClient (all methods in one class)."""
|
62
|
+
# Generate all operation methods
|
63
|
+
method_codes = []
|
64
|
+
for op_id, operation in self.context.operations.items():
|
65
|
+
method_codes.append(self.operations_gen.generate_async_operation(operation))
|
66
|
+
|
67
|
+
template = self.jinja_env.get_template('client/flat_client.py.jinja')
|
68
|
+
return template.render(
|
69
|
+
api_title=self.context.openapi_info.title,
|
70
|
+
operations=method_codes
|
71
|
+
)
|
72
|
+
|
73
|
+
def _generate_namespaced_client(self) -> str:
|
74
|
+
"""Generate namespaced APIClient (sub-clients per tag)."""
|
75
|
+
# Group operations by tag (using base class method)
|
76
|
+
ops_by_tag = self.base.group_operations_by_tag()
|
77
|
+
|
78
|
+
# Generate sub-client classes
|
79
|
+
sub_client_classes = []
|
80
|
+
for tag, operations in sorted(ops_by_tag.items()):
|
81
|
+
sub_client_classes.append(self._generate_sub_client_class(tag, operations))
|
82
|
+
|
83
|
+
sub_clients_code = "\n\n\n".join(sub_client_classes)
|
84
|
+
|
85
|
+
# Generate main APIClient
|
86
|
+
main_client_code = self._generate_main_client_class(ops_by_tag)
|
87
|
+
|
88
|
+
return f"{sub_clients_code}\n\n\n{main_client_code}"
|
89
|
+
|
90
|
+
def _generate_sub_client_class(self, tag: str, operations: list) -> str:
|
91
|
+
"""Generate sub-client class for a specific tag."""
|
92
|
+
class_name = self.base.tag_to_class_name(tag)
|
93
|
+
|
94
|
+
# Generate methods for this tag
|
95
|
+
method_codes = []
|
96
|
+
for operation in operations:
|
97
|
+
method_codes.append(self.operations_gen.generate_async_operation(operation, remove_tag_prefix=True))
|
98
|
+
|
99
|
+
template = self.jinja_env.get_template('client/sub_client.py.jinja')
|
100
|
+
return template.render(
|
101
|
+
tag=self.base.tag_to_display_name(tag),
|
102
|
+
class_name=class_name,
|
103
|
+
operations=method_codes
|
104
|
+
)
|
105
|
+
|
106
|
+
def _generate_main_client_class(self, ops_by_tag: dict) -> str:
|
107
|
+
"""Generate main APIClient with sub-clients."""
|
108
|
+
tags = sorted(ops_by_tag.keys())
|
109
|
+
|
110
|
+
# Prepare tags data for template
|
111
|
+
tags_data = [
|
112
|
+
{
|
113
|
+
"class_name": self.base.tag_to_class_name(tag),
|
114
|
+
"property": self.base.tag_to_property_name(tag),
|
115
|
+
}
|
116
|
+
for tag in tags
|
117
|
+
]
|
118
|
+
|
119
|
+
template = self.jinja_env.get_template('client/main_client.py.jinja')
|
120
|
+
return template.render(
|
121
|
+
api_title=self.context.openapi_info.title,
|
122
|
+
tags=tags_data
|
123
|
+
)
|
124
|
+
|
125
|
+
def generate_app_client_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
|
126
|
+
"""Generate client.py for a specific app."""
|
127
|
+
class_name = self.base.tag_to_class_name(tag)
|
128
|
+
|
129
|
+
# Generate methods
|
130
|
+
method_codes = []
|
131
|
+
for operation in operations:
|
132
|
+
method_codes.append(self.operations_gen.generate_async_operation(operation, remove_tag_prefix=True))
|
133
|
+
|
134
|
+
template = self.jinja_env.get_template('client/app_client.py.jinja')
|
135
|
+
content = template.render(
|
136
|
+
tag=self.base.tag_to_display_name(tag),
|
137
|
+
class_name=class_name,
|
138
|
+
operations=method_codes
|
139
|
+
)
|
140
|
+
|
141
|
+
folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
|
142
|
+
return GeneratedFile(
|
143
|
+
path=f"{folder_name}/client.py",
|
144
|
+
content=content,
|
145
|
+
description=f"API client for {tag}",
|
146
|
+
)
|
147
|
+
|
148
|
+
def generate_main_client_file(self, ops_by_tag: dict) -> GeneratedFile:
|
149
|
+
"""Generate main client.py with APIClient."""
|
150
|
+
tags = sorted(ops_by_tag.keys())
|
151
|
+
|
152
|
+
# Prepare tags data for template
|
153
|
+
tags_data = [
|
154
|
+
{
|
155
|
+
"class_name": self.base.tag_to_class_name(tag),
|
156
|
+
"slug": self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
|
157
|
+
}
|
158
|
+
for tag in tags
|
159
|
+
]
|
160
|
+
|
161
|
+
# Generate main APIClient class
|
162
|
+
client_code = self._generate_main_client_class(ops_by_tag)
|
163
|
+
|
164
|
+
template = self.jinja_env.get_template('client/main_client_file.py.jinja')
|
165
|
+
content = template.render(
|
166
|
+
tags=tags_data,
|
167
|
+
client_code=client_code
|
168
|
+
)
|
169
|
+
|
170
|
+
return GeneratedFile(
|
171
|
+
path="client.py",
|
172
|
+
content=content,
|
173
|
+
description="Main API client",
|
174
|
+
)
|
@@ -0,0 +1,180 @@
|
|
1
|
+
"""
|
2
|
+
Files Generator - Generates auxiliary files (__init__.py, logger.py, schema.py).
|
3
|
+
|
4
|
+
Handles:
|
5
|
+
- Package __init__.py files
|
6
|
+
- Logger configuration
|
7
|
+
- OpenAPI schema embedding
|
8
|
+
- API wrapper classes
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
import json
|
14
|
+
import re
|
15
|
+
|
16
|
+
from jinja2 import Environment
|
17
|
+
|
18
|
+
from ..base import GeneratedFile
|
19
|
+
from ...ir import IROperationObject
|
20
|
+
|
21
|
+
|
22
|
+
class FilesGenerator:
|
23
|
+
"""Generates auxiliary Python files."""
|
24
|
+
|
25
|
+
def __init__(self, jinja_env: Environment, context, base_generator):
|
26
|
+
"""
|
27
|
+
Initialize files generator.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
jinja_env: Jinja2 environment for templates
|
31
|
+
context: Generation context
|
32
|
+
base_generator: Reference to base generator
|
33
|
+
"""
|
34
|
+
self.jinja_env = jinja_env
|
35
|
+
self.context = context
|
36
|
+
self.base = base_generator
|
37
|
+
|
38
|
+
def generate_init_file(self) -> GeneratedFile:
|
39
|
+
"""Generate __init__.py with exports (flat structure)."""
|
40
|
+
template = self.jinja_env.get_template('__init__.py.jinja')
|
41
|
+
content = template.render(
|
42
|
+
has_enums=bool(self.base.get_enum_schemas())
|
43
|
+
)
|
44
|
+
|
45
|
+
return GeneratedFile(
|
46
|
+
path="__init__.py",
|
47
|
+
content=content,
|
48
|
+
description="Package exports",
|
49
|
+
)
|
50
|
+
|
51
|
+
def generate_app_init_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
|
52
|
+
"""Generate __init__.py for a specific app."""
|
53
|
+
class_name = self.base.tag_to_class_name(tag)
|
54
|
+
|
55
|
+
template = self.jinja_env.get_template('app_init.py.jinja')
|
56
|
+
content = template.render(class_name=class_name)
|
57
|
+
|
58
|
+
folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
|
59
|
+
return GeneratedFile(
|
60
|
+
path=f"{folder_name}/__init__.py",
|
61
|
+
content=content,
|
62
|
+
description=f"Package exports for {tag}",
|
63
|
+
)
|
64
|
+
|
65
|
+
def generate_main_init_file(self) -> GeneratedFile:
|
66
|
+
"""Generate main __init__.py with API class and JWT management."""
|
67
|
+
ops_by_tag = self.base.group_operations_by_tag()
|
68
|
+
tags = sorted(ops_by_tag.keys())
|
69
|
+
|
70
|
+
# Prepare tags data for template
|
71
|
+
tags_data = [
|
72
|
+
{
|
73
|
+
"class_name": self.base.tag_to_class_name(tag),
|
74
|
+
"slug": self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
|
75
|
+
}
|
76
|
+
for tag in tags
|
77
|
+
]
|
78
|
+
|
79
|
+
# Check if we have enums
|
80
|
+
all_schemas = self.context.schemas
|
81
|
+
all_enums = self.base._collect_enums_from_schemas(all_schemas)
|
82
|
+
|
83
|
+
# API class
|
84
|
+
api_class = self.generate_api_wrapper_class_python(tags)
|
85
|
+
|
86
|
+
template = self.jinja_env.get_template('main_init.py.jinja')
|
87
|
+
content = template.render(
|
88
|
+
api_title=self.context.openapi_info.title,
|
89
|
+
tags=tags_data,
|
90
|
+
has_enums=bool(all_enums),
|
91
|
+
enum_names=sorted(all_enums.keys()) if all_enums else [],
|
92
|
+
api_class=api_class
|
93
|
+
)
|
94
|
+
|
95
|
+
return GeneratedFile(
|
96
|
+
path="__init__.py",
|
97
|
+
content=content,
|
98
|
+
description="Package exports with API class and JWT management",
|
99
|
+
)
|
100
|
+
|
101
|
+
def generate_api_wrapper_class_python(self, tags: list[str]) -> str:
|
102
|
+
"""Generate API wrapper class with JWT management for Python."""
|
103
|
+
# Prepare property data
|
104
|
+
properties_data = []
|
105
|
+
for tag in tags:
|
106
|
+
properties_data.append({
|
107
|
+
"tag": tag,
|
108
|
+
"class_name": self.base.tag_to_class_name(tag),
|
109
|
+
"property": self.base.tag_to_property_name(tag),
|
110
|
+
})
|
111
|
+
|
112
|
+
template = self.jinja_env.get_template('api_wrapper.py.jinja')
|
113
|
+
return template.render(properties=properties_data)
|
114
|
+
|
115
|
+
def generate_logger_file(self) -> GeneratedFile:
|
116
|
+
"""Generate logger.py with Rich integration."""
|
117
|
+
template = self.jinja_env.get_template('utils/logger.py.jinja')
|
118
|
+
content = template.render()
|
119
|
+
|
120
|
+
return GeneratedFile(
|
121
|
+
path="logger.py",
|
122
|
+
content=content,
|
123
|
+
description="API Logger with Rich",
|
124
|
+
)
|
125
|
+
|
126
|
+
def generate_retry_file(self) -> GeneratedFile:
|
127
|
+
"""Generate retry.py with tenacity integration."""
|
128
|
+
template = self.jinja_env.get_template('utils/retry.py.jinja')
|
129
|
+
content = template.render()
|
130
|
+
|
131
|
+
return GeneratedFile(
|
132
|
+
path="retry.py",
|
133
|
+
content=content,
|
134
|
+
description="Retry utilities with tenacity",
|
135
|
+
)
|
136
|
+
|
137
|
+
def generate_pyproject_toml_file(self, package_config: dict = None) -> GeneratedFile:
|
138
|
+
"""Generate pyproject.toml for Poetry/PyPI publishing."""
|
139
|
+
if package_config is None:
|
140
|
+
package_config = {}
|
141
|
+
|
142
|
+
# Default configuration
|
143
|
+
defaults = {
|
144
|
+
"package_name": package_config.get("name", "api-client"),
|
145
|
+
"version": package_config.get("version", "1.0.0"),
|
146
|
+
"description": package_config.get("description") or f"Auto-generated Python client for {self.context.openapi_info.title}",
|
147
|
+
"authors": package_config.get("authors", ["Author <author@example.com>"]),
|
148
|
+
"license": package_config.get("license", "MIT"),
|
149
|
+
"repository_url": package_config.get("repository_url"),
|
150
|
+
"keywords": package_config.get("keywords", ["api", "client", "python", "openapi"]),
|
151
|
+
"python_version": package_config.get("python_version", "^3.12"),
|
152
|
+
}
|
153
|
+
|
154
|
+
template = self.jinja_env.get_template('pyproject.toml.jinja')
|
155
|
+
content = template.render(**defaults)
|
156
|
+
|
157
|
+
return GeneratedFile(
|
158
|
+
path="pyproject.toml",
|
159
|
+
content=content,
|
160
|
+
description="Poetry package configuration",
|
161
|
+
)
|
162
|
+
|
163
|
+
def generate_schema_file(self, openapi_schema: dict) -> GeneratedFile:
|
164
|
+
"""Generate schema.py with OpenAPI schema as dict."""
|
165
|
+
# First, convert to pretty JSON
|
166
|
+
schema_json = json.dumps(openapi_schema, indent=4, ensure_ascii=False)
|
167
|
+
|
168
|
+
# Convert JSON literals to Python literals
|
169
|
+
schema_json = re.sub(r'\btrue\b', 'True', schema_json)
|
170
|
+
schema_json = re.sub(r'\bfalse\b', 'False', schema_json)
|
171
|
+
schema_json = re.sub(r'\bnull\b', 'None', schema_json)
|
172
|
+
|
173
|
+
template = self.jinja_env.get_template('utils/schema.py.jinja')
|
174
|
+
content = template.render(schema_dict=schema_json)
|
175
|
+
|
176
|
+
return GeneratedFile(
|
177
|
+
path="schema.py",
|
178
|
+
content=content,
|
179
|
+
description="OpenAPI Schema",
|
180
|
+
)
|