fixes and modelgen insert

This commit is contained in:
2026-02-04 09:53:48 -03:00
parent b88f75fce0
commit 30b2e1cf44
52 changed files with 5317 additions and 178 deletions

View File

@@ -0,0 +1,268 @@
"""
Django Generator
Generates Django ORM models from model definitions.
"""
import dataclasses as dc
from enum import Enum
from pathlib import Path
from typing import Any, List, get_type_hints
from ..helpers import format_opts, get_origin_name, get_type_name, unwrap_optional
from ..loader.schema import EnumDefinition, ModelDefinition
from ..types import DJANGO_SPECIAL, DJANGO_TYPES
from .base import BaseGenerator
class DjangoGenerator(BaseGenerator):
"""Generates Django ORM model files."""
def file_extension(self) -> str:
return ".py"
def generate(self, models, output_path: Path) -> None:
"""Generate Django models to output_path."""
output_path.parent.mkdir(parents=True, exist_ok=True)
# Handle different input types
if hasattr(models, "models"):
# SchemaLoader or similar
content = self._generate_from_definitions(
models.models, getattr(models, "enums", [])
)
elif isinstance(models, tuple):
# (models, enums) tuple
content = self._generate_from_definitions(models[0], models[1])
elif isinstance(models, list):
# List of dataclasses (MPR style)
content = self._generate_from_dataclasses(models)
else:
raise ValueError(f"Unsupported input type: {type(models)}")
output_path.write_text(content)
def _generate_from_definitions(
self, models: List[ModelDefinition], enums: List[EnumDefinition]
) -> str:
"""Generate from ModelDefinition objects."""
lines = self._generate_header()
# Generate enums as TextChoices
for enum_def in enums:
lines.extend(self._generate_text_choices(enum_def))
lines.append("")
# Generate models
for model_def in models:
lines.extend(self._generate_model_from_definition(model_def))
lines.extend(["", ""])
return "\n".join(lines)
def _generate_from_dataclasses(self, dataclasses: List[type]) -> str:
"""Generate from Python dataclasses (MPR style)."""
lines = self._generate_header()
for cls in dataclasses:
lines.extend(self._generate_model_from_dataclass(cls))
lines.extend(["", ""])
return "\n".join(lines)
def _generate_header(self) -> List[str]:
"""Generate file header."""
return [
'"""',
"Django ORM Models - GENERATED FILE",
"",
"Do not edit directly. Regenerate using modelgen.",
'"""',
"",
"import uuid",
"from django.db import models",
"",
]
def _generate_text_choices(self, enum_def: EnumDefinition) -> List[str]:
"""Generate Django TextChoices from EnumDefinition."""
lines = [
f"class {enum_def.name}(models.TextChoices):",
]
for name, value in enum_def.values:
label = name.replace("_", " ").title()
lines.append(f' {name} = "{value}", "{label}"')
return lines
def _generate_model_from_definition(self, model_def: ModelDefinition) -> List[str]:
"""Generate Django model from ModelDefinition."""
docstring = model_def.docstring or model_def.name
lines = [
f"class {model_def.name}(models.Model):",
f' """{docstring.strip().split(chr(10))[0]}"""',
"",
]
for field in model_def.fields:
django_field = self._resolve_field_type(
field.name, field.type_hint, field.default, field.optional
)
lines.append(f" {field.name} = {django_field}")
# Add Meta and __str__
lines.extend(
[
"",
" class Meta:",
' ordering = ["-created_at"]'
if any(f.name == "created_at" for f in model_def.fields)
else " pass",
"",
" def __str__(self):",
]
)
# Determine __str__ return
field_names = [f.name for f in model_def.fields]
if "filename" in field_names:
lines.append(" return self.filename")
elif "name" in field_names:
lines.append(" return self.name")
else:
lines.append(" return str(self.id)")
return lines
def _generate_model_from_dataclass(self, cls: type) -> List[str]:
"""Generate Django model from a dataclass (MPR style)."""
docstring = cls.__doc__ or cls.__name__
lines = [
f"class {cls.__name__}(models.Model):",
f' """{docstring.strip().split(chr(10))[0]}"""',
"",
]
hints = get_type_hints(cls)
fields = {f.name: f for f in dc.fields(cls)}
# Check for enums and add Status inner class if needed
for type_hint in hints.values():
base, _ = unwrap_optional(type_hint)
if isinstance(base, type) and issubclass(base, Enum):
lines.append(" class Status(models.TextChoices):")
for member in base:
label = member.name.replace("_", " ").title()
lines.append(f' {member.name} = "{member.value}", "{label}"')
lines.append("")
break
# Generate fields
for name, type_hint in hints.items():
if name.startswith("_"):
continue
field = fields.get(name)
default = dc.MISSING
if field and field.default is not dc.MISSING:
default = field.default
django_field = self._resolve_field_type(name, type_hint, default, False)
lines.append(f" {name} = {django_field}")
# Add Meta and __str__
lines.extend(
[
"",
" class Meta:",
' ordering = ["-created_at"]'
if "created_at" in hints
else " pass",
"",
" def __str__(self):",
]
)
if "filename" in hints:
lines.append(" return self.filename")
elif "name" in hints:
lines.append(" return self.name")
else:
lines.append(" return str(self.id)")
return lines
def _resolve_field_type(
self, name: str, type_hint: Any, default: Any, optional: bool
) -> str:
"""Resolve Python type to Django field."""
# Special fields
if name in DJANGO_SPECIAL:
return DJANGO_SPECIAL[name]
base, is_optional = unwrap_optional(type_hint)
optional = optional or is_optional
origin = get_origin_name(base)
type_name = get_type_name(base)
opts = format_opts(optional)
# Container types
if origin == "dict":
return DJANGO_TYPES["dict"]
if origin == "list":
return DJANGO_TYPES["list"]
# UUID / datetime
if type_name == "UUID":
return DJANGO_TYPES["UUID"].format(opts=opts)
if type_name == "datetime":
return DJANGO_TYPES["datetime"].format(opts=opts)
# Enum
if isinstance(base, type) and issubclass(base, Enum):
extra = []
if optional:
extra.append("null=True, blank=True")
if default is not dc.MISSING and isinstance(default, Enum):
extra.append(f"default=Status.{default.name}")
return DJANGO_TYPES["enum"].format(
opts=", " + ", ".join(extra) if extra else ""
)
# Text fields (based on name heuristics)
if base is str and any(
x in name for x in ("message", "comments", "description")
):
return DJANGO_TYPES["text"]
# BigInt fields
if base is int and name in ("file_size", "bitrate"):
return DJANGO_TYPES["bigint"].format(opts=opts)
# String with max_length
if base is str:
max_length = 1000 if "path" in name else 500 if "filename" in name else 255
return DJANGO_TYPES[str].format(
max_length=max_length, opts=", " + opts if opts else ""
)
# Integer
if base is int:
extra = [opts] if opts else []
if default is not dc.MISSING and not callable(default):
extra.append(f"default={default}")
return DJANGO_TYPES[int].format(opts=", ".join(extra))
# Float
if base is float:
extra = [opts] if opts else []
if default is not dc.MISSING and not callable(default):
extra.append(f"default={default}")
return DJANGO_TYPES[float].format(opts=", ".join(extra))
# Boolean
if base is bool:
default_val = default if default is not dc.MISSING else False
return DJANGO_TYPES[bool].format(default=default_val)
# Fallback to CharField
return DJANGO_TYPES[str].format(
max_length=255, opts=", " + opts if opts else ""
)