django-cfg 1.4.11__py3-none-any.whl → 1.4.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/apps/urls.py +120 -108
- django_cfg/core/generation/integration_generators/api.py +2 -1
- django_cfg/core/integration/url_integration.py +5 -10
- django_cfg/models/django/openapi.py +15 -128
- 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 +539 -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}/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/modules/django_unfold/dashboard.py +6 -6
- django_cfg/pyproject.toml +1 -1
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/METADATA +1 -1
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/RECORD +100 -78
- django_cfg/dashboard/DEBUG_README.md +0 -105
- django_cfg/dashboard/REFACTORING_SUMMARY.md +0 -237
- 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_drf_theme/CHANGELOG.md +0 -210
- django_cfg/modules/django_drf_theme/EXAMPLE.md +0 -465
- django_cfg/modules/django_drf_theme/IMPLEMENTATION.md +0 -232
- django_cfg/modules/django_drf_theme/README.md +0 -207
- django_cfg/modules/django_drf_theme/TAILWIND_CDN_GUIDE.md +0 -274
- /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.14.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
+
)
|
@@ -0,0 +1,182 @@
|
|
1
|
+
"""
|
2
|
+
Python Generator - Main orchestrator for Python client generation.
|
3
|
+
|
4
|
+
Coordinates all sub-generators:
|
5
|
+
- ModelsGenerator: Pydantic models and enums
|
6
|
+
- OperationsGenerator: Operation methods (async/sync)
|
7
|
+
- AsyncClientGenerator: Async client classes
|
8
|
+
- SyncClientGenerator: Sync client classes
|
9
|
+
- FilesGenerator: Auxiliary files (__init__.py, logger, schema)
|
10
|
+
"""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
13
|
+
|
14
|
+
import pathlib
|
15
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
16
|
+
|
17
|
+
from ..base import BaseGenerator, GeneratedFile
|
18
|
+
from ...ir import IROperationObject, IRSchemaObject
|
19
|
+
|
20
|
+
from .models_generator import ModelsGenerator
|
21
|
+
from .operations_generator import OperationsGenerator
|
22
|
+
from .async_client_gen import AsyncClientGenerator
|
23
|
+
from .sync_client_gen import SyncClientGenerator
|
24
|
+
from .files_generator import FilesGenerator
|
25
|
+
|
26
|
+
|
27
|
+
class PythonGenerator(BaseGenerator):
|
28
|
+
"""
|
29
|
+
Python client generator.
|
30
|
+
|
31
|
+
Generates:
|
32
|
+
- models.py: Pydantic 2 models (User, UserRequest, PatchedUser)
|
33
|
+
- enums.py: Enum classes (StatusEnum, RoleEnum)
|
34
|
+
- client.py: AsyncClient with all operations
|
35
|
+
- sync_client.py: SyncClient with all operations
|
36
|
+
- __init__.py: Package exports
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(self, *args, **kwargs):
|
40
|
+
super().__init__(*args, **kwargs)
|
41
|
+
|
42
|
+
# Setup Jinja2 environment
|
43
|
+
templates_dir = pathlib.Path(__file__).parent / "templates"
|
44
|
+
self.jinja_env = Environment(
|
45
|
+
loader=FileSystemLoader(str(templates_dir)),
|
46
|
+
autoescape=select_autoescape(['html', 'xml']),
|
47
|
+
trim_blocks=True,
|
48
|
+
lstrip_blocks=True,
|
49
|
+
)
|
50
|
+
|
51
|
+
# Initialize sub-generators
|
52
|
+
self.models_gen = ModelsGenerator(self.jinja_env, self.context, self)
|
53
|
+
self.operations_gen = OperationsGenerator(self.jinja_env, self)
|
54
|
+
self.async_client_gen = AsyncClientGenerator(self.jinja_env, self.context, self, self.operations_gen)
|
55
|
+
self.sync_client_gen = SyncClientGenerator(self.jinja_env, self, self.operations_gen)
|
56
|
+
self.files_gen = FilesGenerator(self.jinja_env, self.context, self)
|
57
|
+
|
58
|
+
def generate(self) -> list[GeneratedFile]:
|
59
|
+
"""Generate all Python client files."""
|
60
|
+
files = []
|
61
|
+
|
62
|
+
if self.client_structure == "namespaced":
|
63
|
+
# Generate per-app folders
|
64
|
+
ops_by_tag = self.group_operations_by_tag()
|
65
|
+
|
66
|
+
for tag, operations in sorted(ops_by_tag.items()):
|
67
|
+
# Generate app folder (models.py, client.py, sync_client.py, __init__.py)
|
68
|
+
files.extend(self._generate_app_folder(tag, operations))
|
69
|
+
|
70
|
+
# Generate shared enums.py (Variant 2: all enums in root)
|
71
|
+
all_schemas = self.context.schemas
|
72
|
+
all_enums = self._collect_enums_from_schemas(all_schemas)
|
73
|
+
if all_enums:
|
74
|
+
files.append(self.models_gen.generate_shared_enums_file(all_enums))
|
75
|
+
|
76
|
+
# Generate main async client.py
|
77
|
+
files.append(self.async_client_gen.generate_main_client_file(ops_by_tag))
|
78
|
+
|
79
|
+
# Generate main sync client.py
|
80
|
+
files.append(self.sync_client_gen.generate_sync_main_client_file(ops_by_tag))
|
81
|
+
|
82
|
+
# Generate main __init__.py
|
83
|
+
files.append(self.files_gen.generate_main_init_file())
|
84
|
+
|
85
|
+
# Generate logger.py with Rich
|
86
|
+
files.append(self.files_gen.generate_logger_file())
|
87
|
+
|
88
|
+
# Generate retry.py with tenacity
|
89
|
+
files.append(self.files_gen.generate_retry_file())
|
90
|
+
|
91
|
+
# Generate schema.py with OpenAPI schema
|
92
|
+
if self.openapi_schema:
|
93
|
+
files.append(self.files_gen.generate_schema_file(self.openapi_schema))
|
94
|
+
else:
|
95
|
+
# Flat structure (original logic)
|
96
|
+
files.append(self.models_gen.generate_models_file())
|
97
|
+
|
98
|
+
enum_schemas = self.get_enum_schemas()
|
99
|
+
if enum_schemas:
|
100
|
+
files.append(self.models_gen.generate_enums_file())
|
101
|
+
|
102
|
+
files.append(self.async_client_gen.generate_client_file())
|
103
|
+
files.append(self.files_gen.generate_init_file())
|
104
|
+
|
105
|
+
# Generate logger.py with Rich
|
106
|
+
files.append(self.files_gen.generate_logger_file())
|
107
|
+
|
108
|
+
# Generate retry.py with tenacity
|
109
|
+
files.append(self.files_gen.generate_retry_file())
|
110
|
+
|
111
|
+
# Generate schema.py with OpenAPI schema
|
112
|
+
if self.openapi_schema:
|
113
|
+
files.append(self.files_gen.generate_schema_file(self.openapi_schema))
|
114
|
+
|
115
|
+
# Generate package files if requested
|
116
|
+
if self.generate_package_files:
|
117
|
+
files.append(self.files_gen.generate_pyproject_toml_file(self.package_config))
|
118
|
+
|
119
|
+
return files
|
120
|
+
|
121
|
+
def _generate_app_folder(self, tag: str, operations: list[IROperationObject]) -> list[GeneratedFile]:
|
122
|
+
"""Generate folder for a specific app (tag)."""
|
123
|
+
files = []
|
124
|
+
|
125
|
+
# Get schemas used by this app
|
126
|
+
app_schemas = self._get_schemas_for_operations(operations)
|
127
|
+
|
128
|
+
# Generate models.py for this app
|
129
|
+
files.append(self.models_gen.generate_app_models_file(tag, app_schemas, operations))
|
130
|
+
|
131
|
+
# Generate async client.py for this app
|
132
|
+
files.append(self.async_client_gen.generate_app_client_file(tag, operations))
|
133
|
+
|
134
|
+
# Generate sync client.py for this app
|
135
|
+
files.append(self.sync_client_gen.generate_app_sync_client_file(tag, operations))
|
136
|
+
|
137
|
+
# Generate __init__.py for this app
|
138
|
+
files.append(self.files_gen.generate_app_init_file(tag, operations))
|
139
|
+
|
140
|
+
return files
|
141
|
+
|
142
|
+
def _get_schemas_for_operations(self, operations: list[IROperationObject]) -> dict[str, IRSchemaObject]:
|
143
|
+
"""Get all schemas used by given operations."""
|
144
|
+
schemas = {}
|
145
|
+
|
146
|
+
for operation in operations:
|
147
|
+
# Request body schemas
|
148
|
+
if operation.request_body and operation.request_body.schema_name:
|
149
|
+
schema_name = operation.request_body.schema_name
|
150
|
+
if schema_name in self.context.schemas:
|
151
|
+
schemas[schema_name] = self.context.schemas[schema_name]
|
152
|
+
|
153
|
+
# Patch request body schemas
|
154
|
+
if operation.patch_request_body and operation.patch_request_body.schema_name:
|
155
|
+
schema_name = operation.patch_request_body.schema_name
|
156
|
+
if schema_name in self.context.schemas:
|
157
|
+
schemas[schema_name] = self.context.schemas[schema_name]
|
158
|
+
|
159
|
+
# Response schemas
|
160
|
+
for status_code, response in operation.responses.items():
|
161
|
+
if response.schema_name:
|
162
|
+
if response.schema_name in self.context.schemas:
|
163
|
+
schemas[response.schema_name] = self.context.schemas[response.schema_name]
|
164
|
+
|
165
|
+
return schemas
|
166
|
+
|
167
|
+
# Backward compatibility - delegate to sub-generators
|
168
|
+
def generate_schema(self, schema: IRSchemaObject) -> str:
|
169
|
+
"""Generate Pydantic model for schema (delegates to ModelsGenerator)."""
|
170
|
+
return self.models_gen.generate_schema(schema)
|
171
|
+
|
172
|
+
def generate_enum(self, schema: IRSchemaObject) -> str:
|
173
|
+
"""Generate Enum class (delegates to ModelsGenerator)."""
|
174
|
+
return self.models_gen.generate_enum(schema)
|
175
|
+
|
176
|
+
def generate_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
|
177
|
+
"""Generate async method for operation (delegates to OperationsGenerator)."""
|
178
|
+
return self.operations_gen.generate_async_operation(operation, remove_tag_prefix)
|
179
|
+
|
180
|
+
def generate_sync_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
|
181
|
+
"""Generate sync method for operation (delegates to OperationsGenerator)."""
|
182
|
+
return self.operations_gen.generate_sync_operation(operation, remove_tag_prefix)
|