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
@@ -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
+ )