Files
mediaproc/modelgen/generator/django.py
2026-02-06 20:18:45 -03:00

271 lines
9.2 KiB
Python

"""
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):
enum_name = base.__name__
extra = []
if optional:
extra.append("null=True, blank=True")
if default is not dc.MISSING and isinstance(default, Enum):
extra.append(f"default={enum_name}.{default.name}")
return DJANGO_TYPES["enum"].format(
enum_name=enum_name,
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 ""
)