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,318 @@
|
|
1
|
+
"""
|
2
|
+
Models Generator - Generates Pydantic models and enums.
|
3
|
+
|
4
|
+
Handles:
|
5
|
+
- Pydantic 2 model classes (Request/Response/Patch)
|
6
|
+
- Enum classes (StrEnum, IntEnum)
|
7
|
+
- Field validation and constraints
|
8
|
+
"""
|
9
|
+
|
10
|
+
from __future__ import annotations
|
11
|
+
|
12
|
+
from jinja2 import Environment
|
13
|
+
|
14
|
+
from ..base import GeneratedFile
|
15
|
+
from ...ir import IRSchemaObject
|
16
|
+
|
17
|
+
|
18
|
+
class ModelsGenerator:
|
19
|
+
"""Generates Pydantic models and enum classes."""
|
20
|
+
|
21
|
+
def __init__(self, jinja_env: Environment, context, base_generator):
|
22
|
+
"""
|
23
|
+
Initialize models generator.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
jinja_env: Jinja2 environment for templates
|
27
|
+
context: Generation context with schemas
|
28
|
+
base_generator: Reference to base generator for utility methods
|
29
|
+
"""
|
30
|
+
self.jinja_env = jinja_env
|
31
|
+
self.context = context
|
32
|
+
self.base = base_generator
|
33
|
+
|
34
|
+
def generate_models_file(self) -> GeneratedFile:
|
35
|
+
"""Generate models.py with all Pydantic models."""
|
36
|
+
# Generate all schemas
|
37
|
+
schema_codes = []
|
38
|
+
|
39
|
+
# Response models first
|
40
|
+
for name, schema in self.base.get_response_schemas().items():
|
41
|
+
schema_codes.append(self.generate_schema(schema))
|
42
|
+
|
43
|
+
# Request models
|
44
|
+
for name, schema in self.base.get_request_schemas().items():
|
45
|
+
schema_codes.append(self.generate_schema(schema))
|
46
|
+
|
47
|
+
# Patch models
|
48
|
+
for name, schema in self.base.get_patch_schemas().items():
|
49
|
+
schema_codes.append(self.generate_schema(schema))
|
50
|
+
|
51
|
+
template = self.jinja_env.get_template('models/models.py.jinja')
|
52
|
+
content = template.render(
|
53
|
+
has_enums=bool(self.base.get_enum_schemas()),
|
54
|
+
schemas=schema_codes
|
55
|
+
)
|
56
|
+
|
57
|
+
return GeneratedFile(
|
58
|
+
path="models.py",
|
59
|
+
content=content,
|
60
|
+
description="Pydantic 2 models (Request/Response/Patch)",
|
61
|
+
)
|
62
|
+
|
63
|
+
def generate_enums_file(self) -> GeneratedFile:
|
64
|
+
"""Generate enums.py with all Enum classes (flat structure)."""
|
65
|
+
# Generate all enums
|
66
|
+
enum_codes = []
|
67
|
+
for name, schema in self.base.get_enum_schemas().items():
|
68
|
+
enum_codes.append(self.generate_enum(schema))
|
69
|
+
|
70
|
+
template = self.jinja_env.get_template('models/enums.py.jinja')
|
71
|
+
content = template.render(enums=enum_codes)
|
72
|
+
|
73
|
+
return GeneratedFile(
|
74
|
+
path="enums.py",
|
75
|
+
content=content,
|
76
|
+
description="Enum classes from x-enum-varnames",
|
77
|
+
)
|
78
|
+
|
79
|
+
def generate_shared_enums_file(self, enums: dict[str, IRSchemaObject]) -> GeneratedFile:
|
80
|
+
"""Generate shared enums.py for namespaced structure (Variant 2)."""
|
81
|
+
# Generate all enums
|
82
|
+
enum_codes = []
|
83
|
+
for name, schema in enums.items():
|
84
|
+
enum_codes.append(self.generate_enum(schema))
|
85
|
+
|
86
|
+
template = self.jinja_env.get_template('models/enums.py.jinja')
|
87
|
+
content = template.render(enums=enum_codes)
|
88
|
+
|
89
|
+
return GeneratedFile(
|
90
|
+
path="enums.py",
|
91
|
+
content=content,
|
92
|
+
description="Shared enum classes from x-enum-varnames",
|
93
|
+
)
|
94
|
+
|
95
|
+
def generate_schema(self, schema: IRSchemaObject) -> str:
|
96
|
+
"""Generate Pydantic model for schema."""
|
97
|
+
if schema.type != "object":
|
98
|
+
# For primitive types, skip (they'll be inlined)
|
99
|
+
return ""
|
100
|
+
|
101
|
+
# Class docstring
|
102
|
+
docstring_lines = []
|
103
|
+
if schema.description:
|
104
|
+
docstring_lines.extend(self.base.wrap_comment(schema.description, 76))
|
105
|
+
|
106
|
+
# Add metadata about model type
|
107
|
+
if schema.is_request_model:
|
108
|
+
docstring_lines.append("")
|
109
|
+
docstring_lines.append("Request model (no read-only fields).")
|
110
|
+
elif schema.is_patch_model:
|
111
|
+
docstring_lines.append("")
|
112
|
+
docstring_lines.append("PATCH model (all fields optional).")
|
113
|
+
elif schema.is_response_model:
|
114
|
+
docstring_lines.append("")
|
115
|
+
docstring_lines.append("Response model (includes read-only fields).")
|
116
|
+
|
117
|
+
docstring = "\n".join(docstring_lines) if docstring_lines else None
|
118
|
+
|
119
|
+
# Fields
|
120
|
+
field_lines = []
|
121
|
+
for prop_name, prop_schema in schema.properties.items():
|
122
|
+
field_lines.append(self._generate_field(prop_name, prop_schema, schema.required))
|
123
|
+
|
124
|
+
template = self.jinja_env.get_template('models/schema_class.py.jinja')
|
125
|
+
return template.render(
|
126
|
+
name=schema.name,
|
127
|
+
docstring=docstring,
|
128
|
+
fields=field_lines
|
129
|
+
)
|
130
|
+
|
131
|
+
def _generate_field(
|
132
|
+
self,
|
133
|
+
name: str,
|
134
|
+
schema: IRSchemaObject,
|
135
|
+
required_fields: list[str],
|
136
|
+
) -> str:
|
137
|
+
"""
|
138
|
+
Generate Pydantic field definition.
|
139
|
+
|
140
|
+
Examples:
|
141
|
+
id: int
|
142
|
+
username: str
|
143
|
+
email: str | None = None
|
144
|
+
age: int = Field(..., ge=0, le=150)
|
145
|
+
status: StatusEnum
|
146
|
+
"""
|
147
|
+
# Check if this field is an enum
|
148
|
+
if schema.enum and schema.name:
|
149
|
+
# Use enum type from shared enums (sanitized to PascalCase)
|
150
|
+
python_type = self.base.sanitize_enum_name(schema.name)
|
151
|
+
if schema.nullable:
|
152
|
+
python_type = f"{python_type} | None"
|
153
|
+
# Check if this field is a reference to an enum (via $ref)
|
154
|
+
elif schema.ref and schema.ref in self.context.schemas:
|
155
|
+
ref_schema = self.context.schemas[schema.ref]
|
156
|
+
if ref_schema.enum:
|
157
|
+
# This is a reference to an enum component (sanitized to PascalCase)
|
158
|
+
python_type = self.base.sanitize_enum_name(schema.ref)
|
159
|
+
if schema.nullable:
|
160
|
+
python_type = f"{python_type} | None"
|
161
|
+
else:
|
162
|
+
# Regular reference
|
163
|
+
python_type = schema.python_type
|
164
|
+
else:
|
165
|
+
# Get Python type
|
166
|
+
python_type = schema.python_type
|
167
|
+
|
168
|
+
# Check if required
|
169
|
+
is_required = name in required_fields
|
170
|
+
|
171
|
+
# Build Field() kwargs
|
172
|
+
field_kwargs = []
|
173
|
+
|
174
|
+
if schema.description:
|
175
|
+
field_kwargs.append(f"description={schema.description!r}")
|
176
|
+
|
177
|
+
# Validation constraints
|
178
|
+
if schema.min_length is not None:
|
179
|
+
field_kwargs.append(f"min_length={schema.min_length}")
|
180
|
+
if schema.max_length is not None:
|
181
|
+
field_kwargs.append(f"max_length={schema.max_length}")
|
182
|
+
if schema.pattern:
|
183
|
+
field_kwargs.append(f"pattern={schema.pattern!r}")
|
184
|
+
if schema.minimum is not None:
|
185
|
+
field_kwargs.append(f"ge={schema.minimum}")
|
186
|
+
if schema.maximum is not None:
|
187
|
+
field_kwargs.append(f"le={schema.maximum}")
|
188
|
+
|
189
|
+
# Example
|
190
|
+
if schema.example:
|
191
|
+
field_kwargs.append(f"examples=[{schema.example!r}]")
|
192
|
+
|
193
|
+
# Default value
|
194
|
+
if is_required:
|
195
|
+
if field_kwargs:
|
196
|
+
default = f"Field({', '.join(field_kwargs)})"
|
197
|
+
else:
|
198
|
+
default = "..."
|
199
|
+
else:
|
200
|
+
if field_kwargs:
|
201
|
+
default = f"Field(None, {', '.join(field_kwargs)})"
|
202
|
+
else:
|
203
|
+
default = "None"
|
204
|
+
|
205
|
+
return f"{name}: {python_type} = {default}"
|
206
|
+
|
207
|
+
def generate_enum(self, schema: IRSchemaObject) -> str:
|
208
|
+
"""Generate Enum class from x-enum-varnames."""
|
209
|
+
# Determine enum base class
|
210
|
+
if schema.type == "integer":
|
211
|
+
base_class = "IntEnum"
|
212
|
+
else:
|
213
|
+
base_class = "StrEnum"
|
214
|
+
|
215
|
+
# Sanitize enum class name (convert to PascalCase)
|
216
|
+
# "OrderDetail.status" → "OrderDetailStatus"
|
217
|
+
# "Currency.currency_type" → "CurrencyCurrencyType"
|
218
|
+
enum_name = self.base.sanitize_enum_name(schema.name)
|
219
|
+
|
220
|
+
# Class docstring
|
221
|
+
docstring = None
|
222
|
+
if schema.description:
|
223
|
+
# Format enum description to split bullet points
|
224
|
+
docstring = self.base.format_enum_description(schema.description)
|
225
|
+
|
226
|
+
# Enum members
|
227
|
+
member_lines = []
|
228
|
+
for var_name, value in zip(schema.enum_var_names, schema.enum):
|
229
|
+
# Skip empty values (from blank=True in Django)
|
230
|
+
if not var_name or (isinstance(value, str) and value == ''):
|
231
|
+
continue
|
232
|
+
|
233
|
+
if isinstance(value, str):
|
234
|
+
member_lines.append(f'{var_name} = "{value}"')
|
235
|
+
else:
|
236
|
+
member_lines.append(f"{var_name} = {value}")
|
237
|
+
|
238
|
+
template = self.jinja_env.get_template('models/enum_class.py.jinja')
|
239
|
+
return template.render(
|
240
|
+
name=enum_name,
|
241
|
+
base_class=base_class,
|
242
|
+
docstring=docstring,
|
243
|
+
members=member_lines
|
244
|
+
)
|
245
|
+
|
246
|
+
def generate_app_models_file(
|
247
|
+
self,
|
248
|
+
tag: str,
|
249
|
+
schemas: dict[str, IRSchemaObject],
|
250
|
+
operations: list
|
251
|
+
) -> GeneratedFile:
|
252
|
+
"""Generate models.py for a specific app (namespaced structure)."""
|
253
|
+
schema_codes = []
|
254
|
+
|
255
|
+
# Collect enum names used in these schemas
|
256
|
+
enum_names = set()
|
257
|
+
|
258
|
+
# Track seen schema names to avoid duplicates
|
259
|
+
seen_schemas = set()
|
260
|
+
|
261
|
+
# Response models
|
262
|
+
for name, schema in schemas.items():
|
263
|
+
if schema.is_response_model and name not in seen_schemas:
|
264
|
+
schema_codes.append(self.generate_schema(schema))
|
265
|
+
seen_schemas.add(name)
|
266
|
+
# Collect enums from properties
|
267
|
+
if schema.properties:
|
268
|
+
for prop in schema.properties.values():
|
269
|
+
if prop.enum and prop.name:
|
270
|
+
enum_names.add(prop.name.replace(".", ""))
|
271
|
+
elif prop.ref and prop.ref in self.context.schemas:
|
272
|
+
ref_schema = self.context.schemas[prop.ref]
|
273
|
+
if ref_schema.enum:
|
274
|
+
enum_names.add(prop.ref.replace(".", ""))
|
275
|
+
|
276
|
+
# Request models
|
277
|
+
for name, schema in schemas.items():
|
278
|
+
if schema.is_request_model and name not in seen_schemas:
|
279
|
+
schema_codes.append(self.generate_schema(schema))
|
280
|
+
seen_schemas.add(name)
|
281
|
+
# Collect enums from properties
|
282
|
+
if schema.properties:
|
283
|
+
for prop in schema.properties.values():
|
284
|
+
if prop.enum and prop.name:
|
285
|
+
enum_names.add(prop.name.replace(".", ""))
|
286
|
+
elif prop.ref and prop.ref in self.context.schemas:
|
287
|
+
ref_schema = self.context.schemas[prop.ref]
|
288
|
+
if ref_schema.enum:
|
289
|
+
enum_names.add(prop.ref.replace(".", ""))
|
290
|
+
|
291
|
+
# Patch models
|
292
|
+
for name, schema in schemas.items():
|
293
|
+
if schema.is_patch_model and name not in seen_schemas:
|
294
|
+
schema_codes.append(self.generate_schema(schema))
|
295
|
+
seen_schemas.add(name)
|
296
|
+
# Collect enums from properties
|
297
|
+
if schema.properties:
|
298
|
+
for prop in schema.properties.values():
|
299
|
+
if prop.enum and prop.name:
|
300
|
+
enum_names.add(prop.name.replace(".", ""))
|
301
|
+
elif prop.ref and prop.ref in self.context.schemas:
|
302
|
+
ref_schema = self.context.schemas[prop.ref]
|
303
|
+
if ref_schema.enum:
|
304
|
+
enum_names.add(prop.ref.replace(".", ""))
|
305
|
+
|
306
|
+
template = self.jinja_env.get_template('models/app_models.py.jinja')
|
307
|
+
content = template.render(
|
308
|
+
has_enums=len(enum_names) > 0,
|
309
|
+
enum_names=sorted(enum_names), # Sort for consistent output
|
310
|
+
schemas=schema_codes
|
311
|
+
)
|
312
|
+
|
313
|
+
folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
|
314
|
+
return GeneratedFile(
|
315
|
+
path=f"{folder_name}/models.py",
|
316
|
+
content=content,
|
317
|
+
description=f"Models for {tag}",
|
318
|
+
)
|
@@ -0,0 +1,278 @@
|
|
1
|
+
"""
|
2
|
+
Operations Generator - Generates operation methods for async and sync clients.
|
3
|
+
|
4
|
+
Handles:
|
5
|
+
- Async operation methods (async def with await)
|
6
|
+
- Sync operation methods (def without await)
|
7
|
+
- Path parameters, query parameters, request bodies
|
8
|
+
- Response parsing and validation
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
from jinja2 import Environment
|
14
|
+
|
15
|
+
from ...ir import IROperationObject
|
16
|
+
|
17
|
+
|
18
|
+
class OperationsGenerator:
|
19
|
+
"""Generates operation methods for Python clients."""
|
20
|
+
|
21
|
+
def __init__(self, jinja_env: Environment, base_generator):
|
22
|
+
"""
|
23
|
+
Initialize operations generator.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
jinja_env: Jinja2 environment for templates
|
27
|
+
base_generator: Reference to base generator for utility methods
|
28
|
+
"""
|
29
|
+
self.jinja_env = jinja_env
|
30
|
+
self.base = base_generator
|
31
|
+
|
32
|
+
def generate_async_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
|
33
|
+
"""Generate async method for operation."""
|
34
|
+
# Get method name
|
35
|
+
method_name = operation.operation_id
|
36
|
+
if remove_tag_prefix and operation.tags:
|
37
|
+
# Remove tag prefix using base class method
|
38
|
+
tag = operation.tags[0]
|
39
|
+
method_name = self.base.remove_tag_prefix(method_name, tag)
|
40
|
+
|
41
|
+
# Method signature
|
42
|
+
params = ["self"]
|
43
|
+
|
44
|
+
# Add path parameters
|
45
|
+
for param in operation.path_parameters:
|
46
|
+
param_type = self._map_param_type(param.schema_type)
|
47
|
+
params.append(f"{param.name}: {param_type}")
|
48
|
+
|
49
|
+
# Add request body parameter
|
50
|
+
if operation.request_body:
|
51
|
+
params.append(f"data: {operation.request_body.schema_name}")
|
52
|
+
elif operation.patch_request_body:
|
53
|
+
params.append(f"data: {operation.patch_request_body.schema_name} | None = None")
|
54
|
+
|
55
|
+
# Add query parameters
|
56
|
+
for param in operation.query_parameters:
|
57
|
+
param_type = self._map_param_type(param.schema_type)
|
58
|
+
if not param.required:
|
59
|
+
param_type = f"{param_type} | None = None"
|
60
|
+
params.append(f"{param.name}: {param_type}")
|
61
|
+
|
62
|
+
# Return type
|
63
|
+
primary_response = operation.primary_success_response
|
64
|
+
if primary_response and primary_response.schema_name:
|
65
|
+
if operation.is_list_operation:
|
66
|
+
return_type = f"list[{primary_response.schema_name}]"
|
67
|
+
else:
|
68
|
+
return_type = primary_response.schema_name
|
69
|
+
else:
|
70
|
+
return_type = "None"
|
71
|
+
|
72
|
+
signature = f"async def {method_name}({', '.join(params)}) -> {return_type}:"
|
73
|
+
|
74
|
+
# Docstring
|
75
|
+
docstring_lines = []
|
76
|
+
if operation.summary:
|
77
|
+
docstring_lines.append(operation.summary)
|
78
|
+
if operation.description:
|
79
|
+
if docstring_lines:
|
80
|
+
docstring_lines.append("")
|
81
|
+
docstring_lines.extend(self.base.wrap_comment(operation.description, 72))
|
82
|
+
|
83
|
+
docstring = "\n".join(docstring_lines) if docstring_lines else None
|
84
|
+
|
85
|
+
# Method body
|
86
|
+
body_lines = []
|
87
|
+
|
88
|
+
# Build URL
|
89
|
+
url_expr = f'"{operation.path}"'
|
90
|
+
if operation.path_parameters:
|
91
|
+
# Replace {id} with f-string {id}
|
92
|
+
url_expr = f'f"{operation.path}"'
|
93
|
+
|
94
|
+
body_lines.append(f"url = {url_expr}")
|
95
|
+
|
96
|
+
# Build request
|
97
|
+
request_kwargs = []
|
98
|
+
|
99
|
+
# Query params
|
100
|
+
if operation.query_parameters:
|
101
|
+
query_items = []
|
102
|
+
for param in operation.query_parameters:
|
103
|
+
if param.required:
|
104
|
+
query_items.append(f'"{param.name}": {param.name}')
|
105
|
+
else:
|
106
|
+
query_items.append(f'"{param.name}": {param.name} if {param.name} is not None else None')
|
107
|
+
|
108
|
+
query_dict = "{" + ", ".join(query_items) + "}"
|
109
|
+
request_kwargs.append(f"params={query_dict}")
|
110
|
+
|
111
|
+
# JSON body
|
112
|
+
if operation.request_body:
|
113
|
+
# Required body
|
114
|
+
request_kwargs.append("json=data.model_dump()")
|
115
|
+
elif operation.patch_request_body:
|
116
|
+
# Optional PATCH body - check for None
|
117
|
+
request_kwargs.append("json=data.model_dump() if data is not None else None")
|
118
|
+
|
119
|
+
# Make request
|
120
|
+
method_lower = operation.http_method.lower()
|
121
|
+
request_line = f"response = await self._client.{method_lower}(url"
|
122
|
+
if request_kwargs:
|
123
|
+
request_line += ", " + ", ".join(request_kwargs)
|
124
|
+
request_line += ")"
|
125
|
+
|
126
|
+
body_lines.append(request_line)
|
127
|
+
|
128
|
+
# Handle response
|
129
|
+
body_lines.append("response.raise_for_status()")
|
130
|
+
|
131
|
+
if return_type != "None":
|
132
|
+
if operation.is_list_operation:
|
133
|
+
# Paginated list response - extract results
|
134
|
+
body_lines.append(f"data = response.json()")
|
135
|
+
body_lines.append(f'return [{primary_response.schema_name}.model_validate(item) for item in data.get("results", [])]')
|
136
|
+
else:
|
137
|
+
body_lines.append(f"return {primary_response.schema_name}.model_validate(response.json())")
|
138
|
+
else:
|
139
|
+
body_lines.append("return None")
|
140
|
+
|
141
|
+
template = self.jinja_env.get_template('client/operation_method.py.jinja')
|
142
|
+
return template.render(
|
143
|
+
method_name=method_name,
|
144
|
+
params=params,
|
145
|
+
return_type=return_type,
|
146
|
+
docstring=docstring,
|
147
|
+
body_lines=body_lines
|
148
|
+
)
|
149
|
+
|
150
|
+
def generate_sync_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
|
151
|
+
"""Generate sync method for operation (mirrors async generate_operation)."""
|
152
|
+
# Get method name
|
153
|
+
method_name = operation.operation_id
|
154
|
+
if remove_tag_prefix and operation.tags:
|
155
|
+
# Remove tag prefix using base class method
|
156
|
+
tag = operation.tags[0]
|
157
|
+
method_name = self.base.remove_tag_prefix(method_name, tag)
|
158
|
+
|
159
|
+
# Method signature
|
160
|
+
params = ["self"]
|
161
|
+
|
162
|
+
# Add path parameters
|
163
|
+
for param in operation.path_parameters:
|
164
|
+
param_type = self._map_param_type(param.schema_type)
|
165
|
+
params.append(f"{param.name}: {param_type}")
|
166
|
+
|
167
|
+
# Add request body parameter
|
168
|
+
if operation.request_body:
|
169
|
+
params.append(f"data: {operation.request_body.schema_name}")
|
170
|
+
elif operation.patch_request_body:
|
171
|
+
params.append(f"data: {operation.patch_request_body.schema_name} | None = None")
|
172
|
+
|
173
|
+
# Add query parameters
|
174
|
+
for param in operation.query_parameters:
|
175
|
+
param_type = self._map_param_type(param.schema_type)
|
176
|
+
if not param.required:
|
177
|
+
param_type = f"{param_type} | None = None"
|
178
|
+
params.append(f"{param.name}: {param_type}")
|
179
|
+
|
180
|
+
# Return type
|
181
|
+
primary_response = operation.primary_success_response
|
182
|
+
if primary_response and primary_response.schema_name:
|
183
|
+
if operation.is_list_operation:
|
184
|
+
return_type = f"list[{primary_response.schema_name}]"
|
185
|
+
else:
|
186
|
+
return_type = primary_response.schema_name
|
187
|
+
else:
|
188
|
+
return_type = "None"
|
189
|
+
|
190
|
+
# Docstring
|
191
|
+
docstring_lines = []
|
192
|
+
if operation.summary:
|
193
|
+
docstring_lines.append(operation.summary)
|
194
|
+
if operation.description:
|
195
|
+
if docstring_lines:
|
196
|
+
docstring_lines.append("")
|
197
|
+
docstring_lines.extend(self.base.wrap_comment(operation.description, 72))
|
198
|
+
|
199
|
+
docstring = "\n".join(docstring_lines) if docstring_lines else None
|
200
|
+
|
201
|
+
# Method body
|
202
|
+
body_lines = []
|
203
|
+
|
204
|
+
# Build URL
|
205
|
+
url_expr = f'"{operation.path}"'
|
206
|
+
if operation.path_parameters:
|
207
|
+
# Replace {id} with f-string {id}
|
208
|
+
url_expr = f'f"{operation.path}"'
|
209
|
+
|
210
|
+
body_lines.append(f"url = {url_expr}")
|
211
|
+
|
212
|
+
# Build request
|
213
|
+
request_kwargs = []
|
214
|
+
|
215
|
+
# Query params
|
216
|
+
if operation.query_parameters:
|
217
|
+
query_items = []
|
218
|
+
for param in operation.query_parameters:
|
219
|
+
if param.required:
|
220
|
+
query_items.append(f'"{param.name}": {param.name}')
|
221
|
+
else:
|
222
|
+
query_items.append(f'"{param.name}": {param.name} if {param.name} is not None else None')
|
223
|
+
|
224
|
+
query_dict = "{" + ", ".join(query_items) + "}"
|
225
|
+
request_kwargs.append(f"params={query_dict}")
|
226
|
+
|
227
|
+
# JSON body
|
228
|
+
if operation.request_body:
|
229
|
+
# Required body
|
230
|
+
request_kwargs.append("json=data.model_dump(exclude_unset=True)")
|
231
|
+
elif operation.patch_request_body:
|
232
|
+
# Optional PATCH body - check for None
|
233
|
+
request_kwargs.append("json=data.model_dump(exclude_unset=True) if data is not None else None")
|
234
|
+
|
235
|
+
# HTTP method
|
236
|
+
method_lower = operation.http_method.lower()
|
237
|
+
|
238
|
+
# Build request call (sync version - no await)
|
239
|
+
if request_kwargs:
|
240
|
+
request_call = f'self._client.{method_lower}(url, {", ".join(request_kwargs)})'
|
241
|
+
else:
|
242
|
+
request_call = f'self._client.{method_lower}(url)'
|
243
|
+
|
244
|
+
body_lines.append(f"response = {request_call}")
|
245
|
+
body_lines.append("response.raise_for_status()")
|
246
|
+
|
247
|
+
# Parse response
|
248
|
+
if return_type != "None":
|
249
|
+
if operation.is_list_operation:
|
250
|
+
# List response - validate each item (paginated)
|
251
|
+
primary_schema = primary_response.schema_name
|
252
|
+
body_lines.append(f"data = response.json()")
|
253
|
+
body_lines.append(f'return [{primary_schema}.model_validate(item) for item in data.get("results", [])]')
|
254
|
+
else:
|
255
|
+
# Single object response
|
256
|
+
body_lines.append(f"return {return_type}.model_validate(response.json())")
|
257
|
+
|
258
|
+
# Render template
|
259
|
+
template = self.jinja_env.get_template('client/sync_operation_method.py.jinja')
|
260
|
+
return template.render(
|
261
|
+
method_name=method_name,
|
262
|
+
params=params,
|
263
|
+
return_type=return_type,
|
264
|
+
body_lines=body_lines,
|
265
|
+
docstring=docstring
|
266
|
+
)
|
267
|
+
|
268
|
+
def _map_param_type(self, schema_type: str) -> str:
|
269
|
+
"""Map parameter schema type to Python type."""
|
270
|
+
type_map = {
|
271
|
+
"string": "str",
|
272
|
+
"integer": "int",
|
273
|
+
"number": "float",
|
274
|
+
"boolean": "bool",
|
275
|
+
"array": "list",
|
276
|
+
"object": "dict",
|
277
|
+
}
|
278
|
+
return type_map.get(schema_type, "str")
|
@@ -0,0 +1,102 @@
|
|
1
|
+
"""
|
2
|
+
Sync Client Generator - Generates sync Python clients.
|
3
|
+
|
4
|
+
Handles:
|
5
|
+
- Main SyncAPIClient class (httpx.Client)
|
6
|
+
- Sync sub-client classes (per tag/app)
|
7
|
+
- Mirrors async client structure without async/await
|
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 SyncClientGenerator:
|
19
|
+
"""Generates sync Python client files."""
|
20
|
+
|
21
|
+
def __init__(self, jinja_env: Environment, base_generator, operations_gen):
|
22
|
+
"""
|
23
|
+
Initialize sync client generator.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
jinja_env: Jinja2 environment for templates
|
27
|
+
base_generator: Reference to base generator
|
28
|
+
operations_gen: Operations generator instance
|
29
|
+
"""
|
30
|
+
self.jinja_env = jinja_env
|
31
|
+
self.base = base_generator
|
32
|
+
self.operations_gen = operations_gen
|
33
|
+
|
34
|
+
def generate_app_sync_client_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
|
35
|
+
"""Generate sync_client.py for a specific app."""
|
36
|
+
class_name = self.base.tag_to_class_name(tag)
|
37
|
+
|
38
|
+
# Generate sync methods
|
39
|
+
method_codes = []
|
40
|
+
for operation in operations:
|
41
|
+
method_codes.append(self.operations_gen.generate_sync_operation(operation, remove_tag_prefix=True))
|
42
|
+
|
43
|
+
template = self.jinja_env.get_template('client/sync_sub_client.py.jinja')
|
44
|
+
content = template.render(
|
45
|
+
tag=self.base.tag_to_display_name(tag),
|
46
|
+
class_name=class_name,
|
47
|
+
operations=method_codes
|
48
|
+
)
|
49
|
+
|
50
|
+
folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
|
51
|
+
return GeneratedFile(
|
52
|
+
path=f"{folder_name}/sync_client.py",
|
53
|
+
content=content,
|
54
|
+
description=f"Sync API client for {tag}",
|
55
|
+
)
|
56
|
+
|
57
|
+
def generate_sync_main_client_file(self, ops_by_tag: dict) -> GeneratedFile:
|
58
|
+
"""Generate sync_client.py with SyncAPIClient."""
|
59
|
+
tags = sorted(ops_by_tag.keys())
|
60
|
+
|
61
|
+
# Prepare tags data for template (with Sync prefix for imports)
|
62
|
+
tags_data = [
|
63
|
+
{
|
64
|
+
"class_name": f"Sync{self.base.tag_to_class_name(tag)}", # Add Sync prefix
|
65
|
+
"slug": f"{self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag])}.sync_client", # Import from sync_client module
|
66
|
+
}
|
67
|
+
for tag in tags
|
68
|
+
]
|
69
|
+
|
70
|
+
# Generate sync APIClient class
|
71
|
+
sync_client_code = self._generate_sync_main_client_class(ops_by_tag)
|
72
|
+
|
73
|
+
template = self.jinja_env.get_template('client/main_client_file.py.jinja')
|
74
|
+
content = template.render(
|
75
|
+
tags=tags_data,
|
76
|
+
client_code=sync_client_code
|
77
|
+
)
|
78
|
+
|
79
|
+
return GeneratedFile(
|
80
|
+
path="sync_client.py",
|
81
|
+
content=content,
|
82
|
+
description="Main sync API client",
|
83
|
+
)
|
84
|
+
|
85
|
+
def _generate_sync_main_client_class(self, ops_by_tag: dict) -> str:
|
86
|
+
"""Generate main SyncAPIClient with sync sub-clients."""
|
87
|
+
tags = sorted(ops_by_tag.keys())
|
88
|
+
|
89
|
+
# Prepare tags data for template
|
90
|
+
tags_data = [
|
91
|
+
{
|
92
|
+
"class_name": self.base.tag_to_class_name(tag),
|
93
|
+
"property": self.base.tag_to_property_name(tag),
|
94
|
+
}
|
95
|
+
for tag in tags
|
96
|
+
]
|
97
|
+
|
98
|
+
template = self.jinja_env.get_template('client/sync_main_client.py.jinja')
|
99
|
+
return template.render(
|
100
|
+
api_title=self.base.context.openapi_info.title,
|
101
|
+
tags=tags_data
|
102
|
+
)
|