shoehorning graphql, step functions and lamdas. aws deployment scripts
This commit is contained in:
236
modelgen/generator/graphene.py
Normal file
236
modelgen/generator/graphene.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
Graphene Generator
|
||||
|
||||
Generates graphene ObjectType and InputObjectType classes from model definitions.
|
||||
Only generates type definitions — queries, mutations, and resolvers are hand-written.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, List, get_type_hints
|
||||
|
||||
from ..helpers import get_origin_name, get_type_name, unwrap_optional
|
||||
from ..loader.schema import EnumDefinition, FieldDefinition, ModelDefinition
|
||||
from ..types import GRAPHENE_RESOLVERS
|
||||
from .base import BaseGenerator
|
||||
|
||||
|
||||
class GrapheneGenerator(BaseGenerator):
|
||||
"""Generates graphene type definition files."""
|
||||
|
||||
def file_extension(self) -> str:
|
||||
return ".py"
|
||||
|
||||
def generate(self, models, output_path: Path) -> None:
|
||||
"""Generate graphene types to output_path."""
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if hasattr(models, "models"):
|
||||
# SchemaLoader
|
||||
content = self._generate_from_definitions(
|
||||
models.models,
|
||||
getattr(models, "enums", []),
|
||||
getattr(models, "api_models", []),
|
||||
)
|
||||
elif isinstance(models, tuple):
|
||||
content = self._generate_from_definitions(models[0], models[1], [])
|
||||
elif isinstance(models, list):
|
||||
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],
|
||||
api_models: List[ModelDefinition],
|
||||
) -> str:
|
||||
"""Generate from ModelDefinition objects."""
|
||||
lines = self._generate_header()
|
||||
|
||||
# Generate enums as graphene.Enum
|
||||
for enum_def in enums:
|
||||
lines.extend(self._generate_enum(enum_def))
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
|
||||
# Generate domain models as ObjectType
|
||||
for model_def in models:
|
||||
lines.extend(self._generate_object_type(model_def))
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
|
||||
# Generate API models — request types as InputObjectType, others as ObjectType
|
||||
for model_def in api_models:
|
||||
if model_def.name.endswith("Request"):
|
||||
lines.extend(self._generate_input_type(model_def))
|
||||
else:
|
||||
lines.extend(self._generate_object_type(model_def))
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
def _generate_from_dataclasses(self, dataclasses: List[type]) -> str:
|
||||
"""Generate from Python dataclasses."""
|
||||
lines = self._generate_header()
|
||||
|
||||
enums_generated = set()
|
||||
for cls in dataclasses:
|
||||
hints = get_type_hints(cls)
|
||||
for type_hint in hints.values():
|
||||
base, _ = unwrap_optional(type_hint)
|
||||
if isinstance(base, type) and issubclass(base, Enum):
|
||||
if base.__name__ not in enums_generated:
|
||||
lines.extend(self._generate_enum_from_python(base))
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
enums_generated.add(base.__name__)
|
||||
|
||||
for cls in dataclasses:
|
||||
lines.extend(self._generate_object_type_from_dataclass(cls))
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
def _generate_header(self) -> List[str]:
|
||||
return [
|
||||
'"""',
|
||||
"Graphene Types - GENERATED FILE",
|
||||
"",
|
||||
"Do not edit directly. Regenerate using modelgen.",
|
||||
'"""',
|
||||
"",
|
||||
"import graphene",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
|
||||
def _generate_enum(self, enum_def: EnumDefinition) -> List[str]:
|
||||
"""Generate graphene.Enum from EnumDefinition."""
|
||||
lines = [f"class {enum_def.name}(graphene.Enum):"]
|
||||
for name, value in enum_def.values:
|
||||
lines.append(f' {name} = "{value}"')
|
||||
return lines
|
||||
|
||||
def _generate_enum_from_python(self, enum_cls: type) -> List[str]:
|
||||
"""Generate graphene.Enum from Python Enum."""
|
||||
lines = [f"class {enum_cls.__name__}(graphene.Enum):"]
|
||||
for member in enum_cls:
|
||||
lines.append(f' {member.name} = "{member.value}"')
|
||||
return lines
|
||||
|
||||
def _generate_object_type(self, model_def: ModelDefinition) -> List[str]:
|
||||
"""Generate graphene.ObjectType from ModelDefinition."""
|
||||
name = model_def.name
|
||||
# Append Type suffix if not already present
|
||||
type_name = f"{name}Type" if not name.endswith("Type") else name
|
||||
|
||||
lines = [f"class {type_name}(graphene.ObjectType):"]
|
||||
if model_def.docstring:
|
||||
doc = model_def.docstring.strip().split("\n")[0]
|
||||
lines.append(f' """{doc}"""')
|
||||
lines.append("")
|
||||
|
||||
if not model_def.fields:
|
||||
lines.append(" pass")
|
||||
else:
|
||||
for field in model_def.fields:
|
||||
graphene_type = self._resolve_type(field.type_hint, field.optional)
|
||||
lines.append(f" {field.name} = {graphene_type}")
|
||||
|
||||
return lines
|
||||
|
||||
def _generate_input_type(self, model_def: ModelDefinition) -> List[str]:
|
||||
"""Generate graphene.InputObjectType from ModelDefinition."""
|
||||
import dataclasses as dc
|
||||
|
||||
name = model_def.name
|
||||
# Convert FooRequest -> FooInput
|
||||
if name.endswith("Request"):
|
||||
input_name = name[: -len("Request")] + "Input"
|
||||
else:
|
||||
input_name = f"{name}Input"
|
||||
|
||||
lines = [f"class {input_name}(graphene.InputObjectType):"]
|
||||
if model_def.docstring:
|
||||
doc = model_def.docstring.strip().split("\n")[0]
|
||||
lines.append(f' """{doc}"""')
|
||||
lines.append("")
|
||||
|
||||
if not model_def.fields:
|
||||
lines.append(" pass")
|
||||
else:
|
||||
for field in model_def.fields:
|
||||
graphene_type = self._resolve_type(field.type_hint, field.optional)
|
||||
# Required only if not optional AND no default value
|
||||
has_default = field.default is not dc.MISSING
|
||||
if not field.optional and not has_default:
|
||||
graphene_type = self._make_required(graphene_type)
|
||||
elif has_default and not field.optional:
|
||||
graphene_type = self._add_default(graphene_type, field.default)
|
||||
lines.append(f" {field.name} = {graphene_type}")
|
||||
|
||||
return lines
|
||||
|
||||
def _generate_object_type_from_dataclass(self, cls: type) -> List[str]:
|
||||
"""Generate graphene.ObjectType from a dataclass."""
|
||||
import dataclasses as dc
|
||||
|
||||
type_name = f"{cls.__name__}Type"
|
||||
lines = [f"class {type_name}(graphene.ObjectType):"]
|
||||
|
||||
hints = get_type_hints(cls)
|
||||
for name, type_hint in hints.items():
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
graphene_type = self._resolve_type(type_hint, False)
|
||||
lines.append(f" {name} = {graphene_type}")
|
||||
|
||||
return lines
|
||||
|
||||
def _resolve_type(self, type_hint: Any, optional: bool) -> str:
|
||||
"""Resolve Python type to graphene field call string."""
|
||||
base, is_optional = unwrap_optional(type_hint)
|
||||
optional = optional or is_optional
|
||||
origin = get_origin_name(base)
|
||||
type_name = get_type_name(base)
|
||||
|
||||
# Look up resolver
|
||||
resolver = (
|
||||
GRAPHENE_RESOLVERS.get(origin)
|
||||
or GRAPHENE_RESOLVERS.get(type_name)
|
||||
or GRAPHENE_RESOLVERS.get(base)
|
||||
or (
|
||||
GRAPHENE_RESOLVERS["enum"]
|
||||
if isinstance(base, type) and issubclass(base, Enum)
|
||||
else None
|
||||
)
|
||||
)
|
||||
|
||||
result = resolver(base) if resolver else "graphene.String"
|
||||
|
||||
# List types already have () syntax from resolver
|
||||
if result.startswith("graphene.List("):
|
||||
return result
|
||||
|
||||
# Scalar types: add () call
|
||||
return f"{result}()"
|
||||
|
||||
def _make_required(self, field_str: str) -> str:
|
||||
"""Add required=True to a graphene field."""
|
||||
if field_str.endswith("()"):
|
||||
return field_str[:-1] + "required=True)"
|
||||
return field_str
|
||||
|
||||
def _add_default(self, field_str: str, default: Any) -> str:
|
||||
"""Add default_value to a graphene field."""
|
||||
if callable(default):
|
||||
# default_factory — skip, graphene doesn't support factories
|
||||
return field_str
|
||||
if field_str.endswith("()"):
|
||||
return field_str[:-1] + f"default_value={default!r})"
|
||||
return field_str
|
||||
Reference in New Issue
Block a user