Skip to content

type_bridge.generator.render.api_dto

api_dto

Render api_dto.py with schema-driven API Data Transfer Objects.

AttrContext dataclass

AttrContext(name, python_name, python_type, default, required, create_required=None, create_default=None, out_required=None, out_default=None)

Context for rendering an attribute field.

RoleContext dataclass

RoleContext(name, python_name, required)

Context for rendering a role field.

EntityContext dataclass

EntityContext(name, class_name, parent_class, abstract, own_attributes=list())

Context for rendering an entity DTO.

RelationContext dataclass

RelationContext(name, class_name, parent_class, abstract, roles=list(), own_attributes=list())

Context for rendering a relation DTO.

BaseClassContext dataclass

BaseClassContext(base_name, source_entity, fields, extra_fields, has_field_sync, field_sync_a=None, field_sync_b=None)

Context for rendering a custom base class.

ValidatorContext dataclass

ValidatorContext(name, func_name, pattern=None)

Context for rendering a custom validator.

CompositeFieldContext dataclass

CompositeFieldContext(name, type_annotation, default, description=None)

Context for a field in a composite DTO.

CompositeContext dataclass

CompositeContext(name, fields, extra_fields, id_field_name, type_field_name, type_enum_from_registry, included_entity_names, has_field_sync, field_sync_a=None, field_sync_b=None, extra_fields_out=None, skip_variants=set())

Context for rendering a composite entity DTO.

render_api_dto

render_api_dto(schema, config=None)

Render the api_dto.py file from the parsed schema.

Parameters:

Name Type Description Default
schema ParsedSchema

The parsed TypeDB schema

required
config DTOConfig | None

Optional DTO configuration for customization

None

Returns:

Type Description
str

Generated Python code as a string

Source code in type_bridge/generator/render/api_dto.py
def render_api_dto(
    schema: ParsedSchema,
    config: DTOConfig | None = None,
) -> str:
    """Render the api_dto.py file from the parsed schema.

    Args:
        schema: The parsed TypeDB schema
        config: Optional DTO configuration for customization

    Returns:
        Generated Python code as a string
    """
    effective_config = config or DTOConfig()

    # Build class name mappings
    entity_class_names = {name: to_class_name(name) for name in schema.entities}
    relation_class_names = {name: to_class_name(name) for name in schema.relations}

    # Check for name clashes between entity/relation names and union type names
    entity_union = effective_config.entity_union_name
    relation_union = effective_config.relation_union_name

    for name, class_name in entity_class_names.items():
        if class_name == entity_union:
            raise ValueError(
                f"Entity '{name}' generates class name '{class_name}' which clashes "
                f"with entity_union_name='{entity_union}'. Use a different "
                f"entity_union_name in DTOConfig to avoid this conflict."
            )
        if class_name == relation_union:
            raise ValueError(
                f"Entity '{name}' generates class name '{class_name}' which clashes "
                f"with relation_union_name='{relation_union}'. Use a different "
                f"relation_union_name in DTOConfig to avoid this conflict."
            )

    for name, class_name in relation_class_names.items():
        if class_name == entity_union:
            raise ValueError(
                f"Relation '{name}' generates class name '{class_name}' which clashes "
                f"with entity_union_name='{entity_union}'. Use a different "
                f"entity_union_name in DTOConfig to avoid this conflict."
            )
        if class_name == relation_union:
            raise ValueError(
                f"Relation '{name}' generates class name '{class_name}' which clashes "
                f"with relation_union_name='{relation_union}'. Use a different "
                f"relation_union_name in DTOConfig to avoid this conflict."
            )

    # Build custom base class contexts
    base_class_contexts = _build_base_class_contexts(effective_config, schema)

    # Build validator contexts
    validator_contexts = _build_validator_contexts(effective_config)

    # Build composite entity contexts
    composite_contexts = _build_composite_contexts(effective_config, schema)

    # Build set of inherited attrs to skip (from all base classes)
    inherited_attrs_to_skip: dict[str, set[str]] = {}
    for bc in effective_config.base_classes:
        inherited_attrs_to_skip[bc.source_entity] = set(bc.inherited_attrs)

    # Build set of excluded entities
    excluded_entities = set(effective_config.exclude_entities)

    # Build entity contexts
    entities = []
    for name, spec in schema.entities.items():
        # Skip excluded entities
        if name in excluded_entities:
            continue

        parent_class = _get_parent_class(
            name,
            spec.parent,
            entity_class_names,
            "BaseDTO",
            effective_config,
            schema.entities,
        )

        # Find which attrs to skip (inherited from custom base)
        skip_attrs: set[str] = set()
        base_config = effective_config.get_base_class_for_entity(name, schema.entities)
        if base_config and name != base_config.source_entity:
            skip_attrs = inherited_attrs_to_skip.get(base_config.source_entity, set())

        # Get attributes owned by this entity (excluding inherited ones)
        own_attrs = []
        for attr_name in sorted(spec.owns):
            if attr_name in skip_attrs:
                continue
            attr_ctx = _get_attribute_context(attr_name, schema, spec)
            # Apply entity_field_overrides
            for efo in effective_config.entity_field_overrides:
                if efo.entity == name and efo.field == attr_name:
                    if efo.variant == "create":
                        if efo.required is not None:
                            attr_ctx.create_required = efo.required
                        if efo.default is not None:
                            attr_ctx.create_default = efo.default
                    elif efo.variant == "out":
                        if efo.required is not None:
                            attr_ctx.out_required = efo.required
                        if efo.default is not None:
                            attr_ctx.out_default = efo.default
            own_attrs.append(attr_ctx)

        entities.append(
            EntityContext(
                name=name,
                class_name=entity_class_names[name],
                parent_class=parent_class,
                abstract=spec.abstract,
                own_attributes=own_attrs,
            )
        )

    # Sort entities by inheritance (parents before children)
    entities = _sort_by_inheritance(entities, schema.entities)

    # Build relation contexts
    relations = []
    for name, spec in schema.relations.items():
        parent_class = _get_parent_class(
            name,
            spec.parent,
            relation_class_names,
            "BaseRelation",
            effective_config,
            {},  # Relations don't have custom base classes yet
        )

        # Get roles for this relation
        roles = []
        for role_spec in spec.roles:
            # Check cardinality to determine if required
            required = False
            if role_spec.cardinality:
                required = role_spec.cardinality.min >= 1

            roles.append(
                RoleContext(
                    name=role_spec.name,
                    python_name=to_python_name(role_spec.name),
                    required=required,
                )
            )

        # Get attributes owned by this relation
        own_attrs = []
        for attr_name in sorted(spec.owns):
            attr_ctx = _get_attribute_context(attr_name, schema, spec)
            own_attrs.append(attr_ctx)

        relations.append(
            RelationContext(
                name=name,
                class_name=relation_class_names[name],
                parent_class=parent_class,
                abstract=spec.abstract,
                roles=roles,
                own_attributes=own_attrs,
            )
        )

    # Sort relations by inheritance
    relations = _sort_by_inheritance(relations, schema.relations)

    # Filter concrete (non-abstract) types for union types
    concrete_entities = [e for e in entities if not e.abstract]
    concrete_relations = [r for r in relations if not r.abstract]

    template = get_template("api_dto.py.jinja")
    return template.render(
        entities=entities,
        relations=relations,
        concrete_entities=concrete_entities,
        concrete_relations=concrete_relations,
        base_classes=base_class_contexts,
        validators=validator_contexts,
        composites=composite_contexts,
        preamble=effective_config.preamble,
        entity_union_name=effective_config.entity_union_name,
        relation_union_name=effective_config.relation_union_name,
        iid_field_name=effective_config.iid_field_name,
        skip_relation_output=effective_config.skip_relation_output,
        relation_create_base_class=effective_config.relation_create_base_class,
        relation_preamble=effective_config.relation_preamble,
        strict_out_models=effective_config.strict_out_models,
    )