django-cfg 1.4.11__py3-none-any.whl → 1.4.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/core/generation/integration_generators/api.py +2 -1
- django_cfg/models/django/openapi.py +4 -80
- django_cfg/modules/django_client/core/archive/manager.py +2 -2
- django_cfg/modules/django_client/core/config/config.py +20 -0
- django_cfg/modules/django_client/core/config/service.py +1 -1
- django_cfg/modules/django_client/core/generator/__init__.py +4 -4
- django_cfg/modules/django_client/core/generator/base.py +71 -0
- django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
- django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
- django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
- django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
- django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
- django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
- django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/api_wrapper.py.jinja +25 -2
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client.py.jinja +24 -6
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client_file.py.jinja +1 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/operation_method.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/sub_client.py.jinja +8 -1
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/main_init.py.jinja +2 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enum_class.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/schema_class.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
- django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
- django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
- django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/app_client.ts.jinja +1 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/client.ts.jinja +77 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/main_client_file.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/sub_client.ts.jinja +3 -3
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/main_index.ts.jinja +73 -11
- django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/errors.ts.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/logger.ts.jinja +9 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/storage.ts.jinja +54 -10
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -0
- django_cfg/modules/django_client/pytest.ini +30 -0
- django_cfg/modules/django_client/spectacular/__init__.py +3 -2
- django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
- django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
- django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
- django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
- django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
- django_cfg/modules/django_unfold/callbacks/main.py +6 -6
- django_cfg/pyproject.toml +1 -1
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/METADATA +1 -1
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/RECORD +99 -70
- django_cfg/modules/django_client/core/generator/python.py +0 -751
- django_cfg/modules/django_client/core/generator/typescript.py +0 -872
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/__init__.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/app_init.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/app_client.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/flat_client.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client_file.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/app_models.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enums.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/models.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/logger.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/schema.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/app_index.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/flat_client.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/operation.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client_file.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/index.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/app_models.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/enums.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/models.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/http.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/schema.ts.jinja +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/management/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/management/commands/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/documentation.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -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)
|
@@ -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
|
+
)
|