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.
Files changed (101) hide show
  1. django_cfg/core/generation/integration_generators/api.py +2 -1
  2. django_cfg/models/django/openapi.py +4 -80
  3. django_cfg/modules/django_client/core/archive/manager.py +2 -2
  4. django_cfg/modules/django_client/core/config/config.py +20 -0
  5. django_cfg/modules/django_client/core/config/service.py +1 -1
  6. django_cfg/modules/django_client/core/generator/__init__.py +4 -4
  7. django_cfg/modules/django_client/core/generator/base.py +71 -0
  8. django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
  9. django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
  10. django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
  11. django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
  12. django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
  13. django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
  14. django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
  15. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/api_wrapper.py.jinja +25 -2
  16. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client.py.jinja +24 -6
  17. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client_file.py.jinja +1 -0
  18. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/operation_method.py.jinja +3 -1
  19. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/sub_client.py.jinja +8 -1
  20. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
  21. django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
  22. django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
  23. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/main_init.py.jinja +2 -0
  24. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enum_class.py.jinja +3 -1
  25. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/schema_class.py.jinja +3 -1
  26. django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
  27. django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
  28. django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
  29. django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
  30. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
  31. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
  32. django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
  33. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
  34. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
  35. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
  36. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
  37. django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
  38. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/app_client.ts.jinja +1 -1
  39. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/client.ts.jinja +77 -1
  40. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/main_client_file.ts.jinja +1 -0
  41. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/sub_client.ts.jinja +3 -3
  42. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
  43. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
  44. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/main_index.ts.jinja +73 -11
  45. django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
  46. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
  47. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
  48. django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
  49. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/errors.ts.jinja +3 -1
  50. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/logger.ts.jinja +9 -1
  51. django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
  52. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/storage.ts.jinja +54 -10
  53. django_cfg/modules/django_client/management/commands/generate_client.py +5 -0
  54. django_cfg/modules/django_client/pytest.ini +30 -0
  55. django_cfg/modules/django_client/spectacular/__init__.py +3 -2
  56. django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
  57. django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
  58. django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
  59. django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
  60. django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
  61. django_cfg/modules/django_unfold/callbacks/main.py +6 -6
  62. django_cfg/pyproject.toml +1 -1
  63. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/METADATA +1 -1
  64. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/RECORD +99 -70
  65. django_cfg/modules/django_client/core/generator/python.py +0 -751
  66. django_cfg/modules/django_client/core/generator/typescript.py +0 -872
  67. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/__init__.py.jinja +0 -0
  68. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/app_init.py.jinja +0 -0
  69. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/app_client.py.jinja +0 -0
  70. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/flat_client.py.jinja +0 -0
  71. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client_file.py.jinja +0 -0
  72. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/app_models.py.jinja +0 -0
  73. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enums.py.jinja +0 -0
  74. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/models.py.jinja +0 -0
  75. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/logger.py.jinja +0 -0
  76. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/schema.py.jinja +0 -0
  77. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/app_index.ts.jinja +0 -0
  78. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/flat_client.ts.jinja +0 -0
  79. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/operation.ts.jinja +0 -0
  80. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client_file.ts.jinja +0 -0
  81. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/index.ts.jinja +0 -0
  82. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/app_models.ts.jinja +0 -0
  83. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/enums.ts.jinja +0 -0
  84. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/models.ts.jinja +0 -0
  85. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/http.ts.jinja +0 -0
  86. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/schema.ts.jinja +0 -0
  87. /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
  88. /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
  89. /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
  90. /django_cfg/{dashboard → modules/django_dashboard}/management/__init__.py +0 -0
  91. /django_cfg/{dashboard → modules/django_dashboard}/management/commands/__init__.py +0 -0
  92. /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
  93. /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
  94. /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
  95. /django_cfg/{dashboard → modules/django_dashboard}/sections/documentation.py +0 -0
  96. /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
  97. /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
  98. /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
  99. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
  100. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
  101. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,751 +0,0 @@
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 __future__ import annotations
15
-
16
- import pathlib
17
- from jinja2 import Environment, FileSystemLoader, select_autoescape
18
-
19
- from .base import BaseGenerator, GeneratedFile
20
- from ..ir import IROperationObject, IRSchemaObject
21
-
22
-
23
- class PythonGenerator(BaseGenerator):
24
- """
25
- Python client generator.
26
-
27
- Generates:
28
- - models.py: Pydantic 2 models (User, UserRequest, PatchedUser)
29
- - enums.py: Enum classes (StatusEnum, RoleEnum)
30
- - client.py: AsyncClient with all operations
31
- - __init__.py: Package exports
32
- """
33
-
34
- def __init__(self, *args, **kwargs):
35
- super().__init__(*args, **kwargs)
36
-
37
- # Setup Jinja2 environment
38
- templates_dir = pathlib.Path(__file__).parent / "templates"
39
- self.jinja_env = Environment(
40
- loader=FileSystemLoader(str(templates_dir)),
41
- autoescape=select_autoescape(['html', 'xml']),
42
- trim_blocks=True,
43
- lstrip_blocks=True,
44
- )
45
-
46
- def generate(self) -> list[GeneratedFile]:
47
- """Generate all Python client files."""
48
- files = []
49
-
50
- if self.client_structure == "namespaced":
51
- # Generate per-app folders
52
- ops_by_tag = self.group_operations_by_tag()
53
-
54
- for tag, operations in sorted(ops_by_tag.items()):
55
- # Generate app folder (models.py, client.py, __init__.py)
56
- files.extend(self._generate_app_folder(tag, operations))
57
-
58
- # Generate shared enums.py (Variant 2: all enums in root)
59
- all_schemas = self.context.schemas
60
- all_enums = self._collect_enums_from_schemas(all_schemas)
61
- if all_enums:
62
- files.append(self._generate_shared_enums_file(all_enums))
63
-
64
- # Generate main client.py
65
- files.append(self._generate_main_client_file(ops_by_tag))
66
-
67
- # Generate main __init__.py
68
- files.append(self._generate_main_init_file())
69
-
70
- # Generate logger.py with Rich
71
- files.append(self._generate_logger_file())
72
-
73
- # Generate schema.py with OpenAPI schema
74
- if self.openapi_schema:
75
- files.append(self._generate_schema_file())
76
- else:
77
- # Flat structure (original logic)
78
- files.append(self._generate_models_file())
79
-
80
- enum_schemas = self.get_enum_schemas()
81
- if enum_schemas:
82
- files.append(self._generate_enums_file())
83
-
84
- files.append(self._generate_client_file())
85
- files.append(self._generate_init_file())
86
-
87
- # Generate logger.py with Rich
88
- files.append(self._generate_logger_file())
89
-
90
- # Generate schema.py with OpenAPI schema
91
- if self.openapi_schema:
92
- files.append(self._generate_schema_file())
93
-
94
- return files
95
-
96
- # ===== Models Generation =====
97
-
98
- def _generate_models_file(self) -> GeneratedFile:
99
- """Generate models.py with all Pydantic models."""
100
- # Generate all schemas
101
- schema_codes = []
102
-
103
- # Response models first
104
- for name, schema in self.get_response_schemas().items():
105
- schema_codes.append(self.generate_schema(schema))
106
-
107
- # Request models
108
- for name, schema in self.get_request_schemas().items():
109
- schema_codes.append(self.generate_schema(schema))
110
-
111
- # Patch models
112
- for name, schema in self.get_patch_schemas().items():
113
- schema_codes.append(self.generate_schema(schema))
114
-
115
- template = self.jinja_env.get_template('python/models/models.py.jinja')
116
- content = template.render(
117
- has_enums=bool(self.get_enum_schemas()),
118
- schemas=schema_codes
119
- )
120
-
121
- return GeneratedFile(
122
- path="models.py",
123
- content=content,
124
- description="Pydantic 2 models (Request/Response/Patch)",
125
- )
126
-
127
- def _generate_enums_file(self) -> GeneratedFile:
128
- """Generate enums.py with all Enum classes (flat structure)."""
129
- # Generate all enums
130
- enum_codes = []
131
- for name, schema in self.get_enum_schemas().items():
132
- enum_codes.append(self.generate_enum(schema))
133
-
134
- template = self.jinja_env.get_template('python/models/enums.py.jinja')
135
- content = template.render(enums=enum_codes)
136
-
137
- return GeneratedFile(
138
- path="enums.py",
139
- content=content,
140
- description="Enum classes from x-enum-varnames",
141
- )
142
-
143
- def _generate_shared_enums_file(self, enums: dict[str, IRSchemaObject]) -> GeneratedFile:
144
- """Generate shared enums.py for namespaced structure (Variant 2)."""
145
- # Generate all enums
146
- enum_codes = []
147
- for name, schema in enums.items():
148
- enum_codes.append(self.generate_enum(schema))
149
-
150
- template = self.jinja_env.get_template('python/models/enums.py.jinja')
151
- content = template.render(enums=enum_codes)
152
-
153
- return GeneratedFile(
154
- path="enums.py",
155
- content=content,
156
- description="Shared enum classes from x-enum-varnames",
157
- )
158
-
159
- # ===== Schema Generation =====
160
-
161
- def generate_schema(self, schema: IRSchemaObject) -> str:
162
- """Generate Pydantic model for schema."""
163
- if schema.type != "object":
164
- # For primitive types, skip (they'll be inlined)
165
- return ""
166
-
167
- # Class docstring
168
- docstring_lines = []
169
- if schema.description:
170
- docstring_lines.extend(self.wrap_comment(schema.description, 76))
171
-
172
- # Add metadata about model type
173
- if schema.is_request_model:
174
- docstring_lines.append("")
175
- docstring_lines.append("Request model (no read-only fields).")
176
- elif schema.is_patch_model:
177
- docstring_lines.append("")
178
- docstring_lines.append("PATCH model (all fields optional).")
179
- elif schema.is_response_model:
180
- docstring_lines.append("")
181
- docstring_lines.append("Response model (includes read-only fields).")
182
-
183
- docstring = "\n".join(docstring_lines) if docstring_lines else None
184
-
185
- # Fields
186
- field_lines = []
187
- for prop_name, prop_schema in schema.properties.items():
188
- field_lines.append(self._generate_field(prop_name, prop_schema, schema.required))
189
-
190
- template = self.jinja_env.get_template('python/models/schema_class.py.jinja')
191
- return template.render(
192
- name=schema.name,
193
- docstring=docstring,
194
- fields=field_lines
195
- )
196
-
197
- def _generate_field(
198
- self,
199
- name: str,
200
- schema: IRSchemaObject,
201
- required_fields: list[str],
202
- ) -> str:
203
- """
204
- Generate Pydantic field definition.
205
-
206
- Examples:
207
- id: int
208
- username: str
209
- email: str | None = None
210
- age: int = Field(..., ge=0, le=150)
211
- status: StatusEnum
212
- """
213
- # Check if this field is an enum
214
- if schema.enum and schema.name:
215
- # Use enum type from shared enums
216
- python_type = schema.name
217
- if schema.nullable:
218
- python_type = f"{python_type} | None"
219
- # Check if this field is a reference to an enum (via $ref)
220
- elif schema.ref and schema.ref in self.context.schemas:
221
- ref_schema = self.context.schemas[schema.ref]
222
- if ref_schema.enum:
223
- # This is a reference to an enum component
224
- python_type = schema.ref
225
- if schema.nullable:
226
- python_type = f"{python_type} | None"
227
- else:
228
- # Regular reference
229
- python_type = schema.python_type
230
- else:
231
- # Get Python type
232
- python_type = schema.python_type
233
-
234
- # Check if required
235
- is_required = name in required_fields
236
-
237
- # Build Field() kwargs
238
- field_kwargs = []
239
-
240
- if schema.description:
241
- field_kwargs.append(f"description={schema.description!r}")
242
-
243
- # Validation constraints
244
- if schema.min_length is not None:
245
- field_kwargs.append(f"min_length={schema.min_length}")
246
- if schema.max_length is not None:
247
- field_kwargs.append(f"max_length={schema.max_length}")
248
- if schema.pattern:
249
- field_kwargs.append(f"pattern={schema.pattern!r}")
250
- if schema.minimum is not None:
251
- field_kwargs.append(f"ge={schema.minimum}")
252
- if schema.maximum is not None:
253
- field_kwargs.append(f"le={schema.maximum}")
254
-
255
- # Example
256
- if schema.example:
257
- field_kwargs.append(f"examples=[{schema.example!r}]")
258
-
259
- # Default value
260
- if is_required:
261
- if field_kwargs:
262
- default = f"Field({', '.join(field_kwargs)})"
263
- else:
264
- default = "..."
265
- else:
266
- if field_kwargs:
267
- default = f"Field(None, {', '.join(field_kwargs)})"
268
- else:
269
- default = "None"
270
-
271
- return f"{name}: {python_type} = {default}"
272
-
273
- def generate_enum(self, schema: IRSchemaObject) -> str:
274
- """Generate Enum class from x-enum-varnames."""
275
- # Determine enum base class
276
- if schema.type == "integer":
277
- base_class = "IntEnum"
278
- else:
279
- base_class = "StrEnum"
280
-
281
- # Class docstring
282
- docstring_lines = []
283
- if schema.description:
284
- docstring_lines.extend(self.wrap_comment(schema.description, 76))
285
-
286
- docstring = "\n".join(docstring_lines) if docstring_lines else None
287
-
288
- # Enum members
289
- member_lines = []
290
- for var_name, value in zip(schema.enum_var_names, schema.enum):
291
- if isinstance(value, str):
292
- member_lines.append(f'{var_name} = "{value}"')
293
- else:
294
- member_lines.append(f"{var_name} = {value}")
295
-
296
- template = self.jinja_env.get_template('python/models/enum_class.py.jinja')
297
- return template.render(
298
- name=schema.name,
299
- base_class=base_class,
300
- docstring=docstring,
301
- members=member_lines
302
- )
303
-
304
- # ===== Client Generation =====
305
-
306
- def _generate_client_file(self) -> GeneratedFile:
307
- """Generate client.py with AsyncClient."""
308
- # Client class
309
- client_code = self._generate_client_class()
310
-
311
- template = self.jinja_env.get_template('python/client_file.py.jinja')
312
- content = template.render(
313
- has_enums=bool(self.get_enum_schemas()),
314
- client_code=client_code
315
- )
316
-
317
- return GeneratedFile(
318
- path="client.py",
319
- content=content,
320
- description="AsyncClient with httpx",
321
- )
322
-
323
- def _generate_client_class(self) -> str:
324
- """Generate APIClient class."""
325
- if self.client_structure == "namespaced":
326
- return self._generate_namespaced_client()
327
- else:
328
- return self._generate_flat_client()
329
-
330
- def _generate_flat_client(self) -> str:
331
- """Generate flat APIClient (all methods in one class)."""
332
- # Generate all operation methods
333
- method_codes = []
334
- for op_id, operation in self.context.operations.items():
335
- method_codes.append(self.generate_operation(operation))
336
-
337
- template = self.jinja_env.get_template('python/client/flat_client.py.jinja')
338
- return template.render(
339
- api_title=self.context.openapi_info.title,
340
- operations=method_codes
341
- )
342
-
343
- def _generate_namespaced_client(self) -> str:
344
- """Generate namespaced APIClient (sub-clients per tag)."""
345
- # Group operations by tag (using base class method)
346
- ops_by_tag = self.group_operations_by_tag()
347
-
348
- # Generate sub-client classes
349
- sub_client_classes = []
350
- for tag, operations in sorted(ops_by_tag.items()):
351
- sub_client_classes.append(self._generate_sub_client_class(tag, operations))
352
-
353
- sub_clients_code = "\n\n\n".join(sub_client_classes)
354
-
355
- # Generate main APIClient
356
- main_client_code = self._generate_main_client_class(ops_by_tag)
357
-
358
- return f"{sub_clients_code}\n\n\n{main_client_code}"
359
-
360
- def _generate_sub_client_class(self, tag: str, operations: list) -> str:
361
- """Generate sub-client class for a specific tag."""
362
- class_name = self.tag_to_class_name(tag)
363
-
364
- # Generate methods for this tag
365
- method_codes = []
366
- for operation in operations:
367
- method_codes.append(self.generate_operation(operation, remove_tag_prefix=True))
368
-
369
- template = self.jinja_env.get_template('python/client/sub_client.py.jinja')
370
- return template.render(
371
- tag=self.tag_to_display_name(tag),
372
- class_name=class_name,
373
- operations=method_codes
374
- )
375
-
376
- def _generate_main_client_class(self, ops_by_tag: dict) -> str:
377
- """Generate main APIClient with sub-clients."""
378
- tags = sorted(ops_by_tag.keys())
379
-
380
- # Prepare tags data for template
381
- tags_data = [
382
- {
383
- "class_name": self.tag_to_class_name(tag),
384
- "property": self.tag_to_property_name(tag),
385
- }
386
- for tag in tags
387
- ]
388
-
389
- template = self.jinja_env.get_template('python/client/main_client.py.jinja')
390
- return template.render(
391
- api_title=self.context.openapi_info.title,
392
- tags=tags_data
393
- )
394
-
395
- def generate_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
396
- """Generate async method for operation."""
397
- # Get method name
398
- method_name = operation.operation_id
399
- if remove_tag_prefix and operation.tags:
400
- # Remove tag prefix using base class method
401
- tag = operation.tags[0]
402
- method_name = self.remove_tag_prefix(method_name, tag)
403
-
404
- # Method signature
405
- params = ["self"]
406
-
407
- # Add path parameters
408
- for param in operation.path_parameters:
409
- param_type = self._map_param_type(param.schema_type)
410
- params.append(f"{param.name}: {param_type}")
411
-
412
- # Add request body parameter
413
- if operation.request_body:
414
- params.append(f"data: {operation.request_body.schema_name}")
415
- elif operation.patch_request_body:
416
- params.append(f"data: {operation.patch_request_body.schema_name} | None = None")
417
-
418
- # Add query parameters
419
- for param in operation.query_parameters:
420
- param_type = self._map_param_type(param.schema_type)
421
- if not param.required:
422
- param_type = f"{param_type} | None = None"
423
- params.append(f"{param.name}: {param_type}")
424
-
425
- # Return type
426
- primary_response = operation.primary_success_response
427
- if primary_response and primary_response.schema_name:
428
- if operation.is_list_operation:
429
- return_type = f"list[{primary_response.schema_name}]"
430
- else:
431
- return_type = primary_response.schema_name
432
- else:
433
- return_type = "None"
434
-
435
- signature = f"async def {method_name}({', '.join(params)}) -> {return_type}:"
436
-
437
- # Docstring
438
- docstring_lines = []
439
- if operation.summary:
440
- docstring_lines.append(operation.summary)
441
- if operation.description:
442
- if docstring_lines:
443
- docstring_lines.append("")
444
- docstring_lines.extend(self.wrap_comment(operation.description, 72))
445
-
446
- docstring = "\n".join(docstring_lines) if docstring_lines else None
447
-
448
- # Method body
449
- body_lines = []
450
-
451
- # Build URL
452
- url_expr = f'"{operation.path}"'
453
- if operation.path_parameters:
454
- # Replace {id} with f-string {id}
455
- url_expr = f'f"{operation.path}"'
456
-
457
- body_lines.append(f"url = {url_expr}")
458
-
459
- # Build request
460
- request_kwargs = []
461
-
462
- # Query params
463
- if operation.query_parameters:
464
- query_items = []
465
- for param in operation.query_parameters:
466
- if param.required:
467
- query_items.append(f'"{param.name}": {param.name}')
468
- else:
469
- query_items.append(f'"{param.name}": {param.name} if {param.name} is not None else None')
470
-
471
- query_dict = "{" + ", ".join(query_items) + "}"
472
- request_kwargs.append(f"params={query_dict}")
473
-
474
- # JSON body
475
- if operation.request_body or operation.patch_request_body:
476
- request_kwargs.append("json=data.model_dump() if data else None")
477
-
478
- # Make request
479
- method_lower = operation.http_method.lower()
480
- request_line = f"response = await self._client.{method_lower}(url"
481
- if request_kwargs:
482
- request_line += ", " + ", ".join(request_kwargs)
483
- request_line += ")"
484
-
485
- body_lines.append(request_line)
486
-
487
- # Handle response
488
- body_lines.append("response.raise_for_status()")
489
-
490
- if return_type != "None":
491
- if operation.is_list_operation:
492
- # Paginated list response - extract results
493
- body_lines.append(f"data = response.json()")
494
- body_lines.append(f'return [{ primary_response.schema_name}.model_validate(item) for item in data.get("results", [])]')
495
- else:
496
- body_lines.append(f"return {primary_response.schema_name}.model_validate(response.json())")
497
- else:
498
- body_lines.append("return None")
499
-
500
- template = self.jinja_env.get_template('python/client/operation_method.py.jinja')
501
- return template.render(
502
- method_name=method_name,
503
- params=params,
504
- return_type=return_type,
505
- docstring=docstring,
506
- body_lines=body_lines
507
- )
508
-
509
- def _map_param_type(self, schema_type: str) -> str:
510
- """Map parameter schema type to Python type."""
511
- type_map = {
512
- "string": "str",
513
- "integer": "int",
514
- "number": "float",
515
- "boolean": "bool",
516
- "array": "list[Any]",
517
- }
518
- return type_map.get(schema_type, "Any")
519
-
520
- # ===== Package Init =====
521
-
522
- def _generate_init_file(self) -> GeneratedFile:
523
- """Generate __init__.py with exports."""
524
- template = self.jinja_env.get_template('python/__init__.py.jinja')
525
- content = template.render(
526
- has_enums=bool(self.get_enum_schemas())
527
- )
528
-
529
- return GeneratedFile(
530
- path="__init__.py",
531
- content=content,
532
- description="Package exports",
533
- )
534
-
535
- # ===== Per-App Folder Generation (Namespaced Structure) =====
536
-
537
- def _generate_app_folder(self, tag: str, operations: list[IROperationObject]) -> list[GeneratedFile]:
538
- """Generate folder for a specific app (tag)."""
539
- files = []
540
-
541
- # Get schemas used by this app
542
- app_schemas = self._get_schemas_for_operations(operations)
543
-
544
- # Generate models.py for this app
545
- files.append(self._generate_app_models_file(tag, app_schemas, operations))
546
-
547
- # Generate client.py for this app
548
- files.append(self._generate_app_client_file(tag, operations))
549
-
550
- # Generate __init__.py for this app
551
- files.append(self._generate_app_init_file(tag, operations))
552
-
553
- return files
554
-
555
- def _get_schemas_for_operations(self, operations: list[IROperationObject]) -> dict[str, IRSchemaObject]:
556
- """Get all schemas used by given operations."""
557
- schemas = {}
558
-
559
- for operation in operations:
560
- # Request body schemas
561
- if operation.request_body and operation.request_body.schema_name:
562
- schema_name = operation.request_body.schema_name
563
- if schema_name in self.context.schemas:
564
- schemas[schema_name] = self.context.schemas[schema_name]
565
-
566
- # Patch request body schemas
567
- if operation.patch_request_body and operation.patch_request_body.schema_name:
568
- schema_name = operation.patch_request_body.schema_name
569
- if schema_name in self.context.schemas:
570
- schemas[schema_name] = self.context.schemas[schema_name]
571
-
572
- # Response schemas
573
- for status_code, response in operation.responses.items():
574
- if response.schema_name:
575
- if response.schema_name in self.context.schemas:
576
- schemas[response.schema_name] = self.context.schemas[response.schema_name]
577
-
578
- return schemas
579
-
580
- def _generate_app_models_file(self, tag: str, schemas: dict[str, IRSchemaObject], operations: list[IROperationObject]) -> GeneratedFile:
581
- """Generate models.py for a specific app."""
582
- # Check if we have enums in schemas
583
- app_enums = self._collect_enums_from_schemas(schemas)
584
- has_enums = len(app_enums) > 0
585
-
586
- # Generate schemas
587
- schema_codes = []
588
- for name, schema in schemas.items():
589
- schema_codes.append(self.generate_schema(schema))
590
-
591
- template = self.jinja_env.get_template('python/models/app_models.py.jinja')
592
- content = template.render(
593
- has_enums=has_enums,
594
- enum_names=sorted(app_enums.keys()) if has_enums else [],
595
- schemas=schema_codes if schema_codes else ["pass"]
596
- )
597
-
598
- folder_name = self.tag_and_app_to_folder_name(tag, operations)
599
- return GeneratedFile(
600
- path=f"{folder_name}/models.py",
601
- content=content,
602
- description=f"Pydantic models for {tag}",
603
- )
604
-
605
- def _generate_app_client_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
606
- """Generate client.py for a specific app."""
607
- class_name = self.tag_to_class_name(tag)
608
-
609
- # Generate methods
610
- method_codes = []
611
- for operation in operations:
612
- method_codes.append(self.generate_operation(operation, remove_tag_prefix=True))
613
-
614
- template = self.jinja_env.get_template('python/client/app_client.py.jinja')
615
- content = template.render(
616
- tag=self.tag_to_display_name(tag),
617
- class_name=class_name,
618
- operations=method_codes
619
- )
620
-
621
- folder_name = self.tag_and_app_to_folder_name(tag, operations)
622
- return GeneratedFile(
623
- path=f"{folder_name}/client.py",
624
- content=content,
625
- description=f"API client for {tag}",
626
- )
627
-
628
- def _generate_app_init_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
629
- """Generate __init__.py for a specific app."""
630
- class_name = self.tag_to_class_name(tag)
631
-
632
- template = self.jinja_env.get_template('python/app_init.py.jinja')
633
- content = template.render(class_name=class_name)
634
-
635
- folder_name = self.tag_and_app_to_folder_name(tag, operations)
636
- return GeneratedFile(
637
- path=f"{folder_name}/__init__.py",
638
- content=content,
639
- description=f"Package exports for {tag}",
640
- )
641
-
642
- def _generate_main_client_file(self, ops_by_tag: dict) -> GeneratedFile:
643
- """Generate main client.py with APIClient."""
644
- tags = sorted(ops_by_tag.keys())
645
-
646
- # Prepare tags data for template
647
- tags_data = [
648
- {
649
- "class_name": self.tag_to_class_name(tag),
650
- "slug": self.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
651
- }
652
- for tag in tags
653
- ]
654
-
655
- # Generate main APIClient class
656
- client_code = self._generate_main_client_class(ops_by_tag)
657
-
658
- template = self.jinja_env.get_template('python/client/main_client_file.py.jinja')
659
- content = template.render(
660
- tags=tags_data,
661
- client_code=client_code
662
- )
663
-
664
- return GeneratedFile(
665
- path="client.py",
666
- content=content,
667
- description="Main API client",
668
- )
669
-
670
- def _generate_main_init_file(self) -> GeneratedFile:
671
- """Generate main __init__.py with API class and JWT management."""
672
- ops_by_tag = self.group_operations_by_tag()
673
- tags = sorted(ops_by_tag.keys())
674
-
675
- # Prepare tags data for template
676
- tags_data = [
677
- {
678
- "class_name": self.tag_to_class_name(tag),
679
- "slug": self.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
680
- }
681
- for tag in tags
682
- ]
683
-
684
- # Check if we have enums
685
- all_schemas = self.context.schemas
686
- all_enums = self._collect_enums_from_schemas(all_schemas)
687
-
688
- # API class
689
- api_class = self._generate_api_wrapper_class_python(tags)
690
-
691
- template = self.jinja_env.get_template('python/main_init.py.jinja')
692
- content = template.render(
693
- api_title=self.context.openapi_info.title,
694
- tags=tags_data,
695
- has_enums=bool(all_enums),
696
- enum_names=sorted(all_enums.keys()) if all_enums else [],
697
- api_class=api_class
698
- )
699
-
700
- return GeneratedFile(
701
- path="__init__.py",
702
- content=content,
703
- description="Package exports with API class and JWT management",
704
- )
705
-
706
- def _generate_api_wrapper_class_python(self, tags: list[str]) -> str:
707
- """Generate API wrapper class with JWT management for Python."""
708
- # Prepare property data
709
- properties_data = []
710
- for tag in tags:
711
- properties_data.append({
712
- "tag": tag,
713
- "class_name": self.tag_to_class_name(tag),
714
- "property": self.tag_to_property_name(tag),
715
- })
716
-
717
- template = self.jinja_env.get_template('python/api_wrapper.py.jinja')
718
- return template.render(properties=properties_data)
719
-
720
- def _generate_logger_file(self) -> GeneratedFile:
721
- """Generate logger.py with Rich integration."""
722
- template = self.jinja_env.get_template('python/utils/logger.py.jinja')
723
- content = template.render()
724
-
725
- return GeneratedFile(
726
- path="logger.py",
727
- content=content,
728
- description="API Logger with Rich",
729
- )
730
-
731
- def _generate_schema_file(self) -> GeneratedFile:
732
- """Generate schema.py with OpenAPI schema as dict."""
733
- import json
734
- import re
735
-
736
- # First, convert to pretty JSON
737
- schema_json = json.dumps(self.openapi_schema, indent=4, ensure_ascii=False)
738
-
739
- # Convert JSON literals to Python literals
740
- schema_json = re.sub(r'\btrue\b', 'True', schema_json)
741
- schema_json = re.sub(r'\bfalse\b', 'False', schema_json)
742
- schema_json = re.sub(r'\bnull\b', 'None', schema_json)
743
-
744
- template = self.jinja_env.get_template('python/utils/schema.py.jinja')
745
- content = template.render(schema_dict=schema_json)
746
-
747
- return GeneratedFile(
748
- path="schema.py",
749
- content=content,
750
- description="OpenAPI Schema",
751
- )