CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-erdantic

Entity relationship diagrams for Python data model classes like Pydantic.

Pending
Overview
Eval results
Files

model-info.mddocs/

Model Information Classes

Erdantic uses several internal data structures to represent analyzed models, their fields, and relationships. These classes provide the foundation for diagram generation and can be used for advanced programmatic manipulation of diagram data.

Core Information Classes

class ModelInfo(pydantic.BaseModel):
    """Holds information about an analyzed data model class.

    Attributes:
        full_name (FullyQualifiedName): Fully qualified name of the data model class.
        name (str): Name of the data model class.
        fields (Dict[str, FieldInfo]): A mapping to FieldInfo instances for each field of the data
            model class.
        description (str): Docstring or other description of the data model class.
    """
    
    @classmethod
    def from_raw_model(cls, raw_model: type) -> Self:
        """Constructor method to create a new instance from a raw data model class.

        Args:
            raw_model (type): Data model class.

        Returns:
            Self: New instance of ModelInfo.
        """
        
    @property
    def key(self) -> str:
        """Returns the key used to identify this instance of ModelInfo in the
        EntityRelationshipDiagram.models mapping. This value is the string representation of the
        `full_name` field.
        """
        
    @property
    def raw_model(self) -> type:
        """Returns the raw data model class. This is a cached property. If the raw model is not
        already known, it will attempt to import the data model class.
        """
        
    def to_dot_label(self) -> str:
        """Returns the DOT language "HTML-like" syntax specification of a table for this data
        model. It is used as the `label` attribute of data model's node in the graph's DOT
        representation.

        Returns:
            str: DOT language for table
        """

class FieldInfo(pydantic.BaseModel):
    """Holds information about a field of an analyzed data model class.

    Attributes:
        model_full_name (FullyQualifiedName): Fully qualified name of the data model class that
            the field belongs to.
        name (str): Name of the field.
        type_name (str): String representation of the field's type.
    """
    
    @classmethod
    def from_raw_type(cls, model_full_name: FullyQualifiedName, name: str, raw_type: type) -> Self:
        """Constructor method to create a new instance from a raw type annotation.

        Args:
            model_full_name (FullyQualifiedName): Fully qualified name of the data model class that
                the field belongs to.
            name (str): Name of field.
            raw_type (type): Type annotation.

        Returns:
            Self: New FieldInfo instance.
        """
        
    @property
    def key(self) -> str:
        """Returns the key used to identify this instance of FieldInfo in the ModelInfo.fields
        mapping. This value is the value in the 'name' field.
        """
        
    @property
    def raw_type(self) -> type:
        """Returns the raw type annotation of the field. This is a cached property. If the raw
        type is not already known, it will attempt to import the data model class and reextract
        the field's type annotation.

        Raises:
            FieldNotFoundError: If field name doesn't match any fields returned by field extractor.
            UnknownModelTypeError: If model type is not recognized by any plugin.

        Returns:
            type: Type annotation.
        """
        
    def to_dot_row(self) -> str:
        """Returns the DOT language "HTML-like" syntax specification of a row detailing this field
        that is part of a table describing the field's parent data model. It is used as part the
        `label` attribute of data model's node in the graph's DOT representation.

        Returns:
            str: DOT language for table row
        """

class Edge(pydantic.BaseModel):
    """Hold information about a relationship between two data model classes. These represent
    directed edges in the entity relationship diagram.

    Attributes:
        source_model_full_name (FullyQualifiedName): Fully qualified name of the source model,
            i.e., the model that contains a field that references the target model.
        source_field_name (str): Name of the field on the source model that references the target
            model.
        target_model_full_name (FullyQualifiedName): Fully qualified name of the target model,
            i.e., the model that is referenced by the source model's field.
        target_cardinality (Cardinality): Cardinality of the target model in the relationship,
            e.g., if the relationship is one (source) to many (target), this value will be
            `Cardinality.MANY`.
        target_modality (Modality): Modality of the target model in the relationship, e.g., if the
            relationship is one (source) to zero (target), meaning that the target is optional,
            this value will be `Modality.ZERO`.
        source_cardinality (Cardinality): Cardinality of the source model in the
            relationship. Defaults to Cardinality.UNSPECIFIED. This will never be set for Edges 
            created by erdantic, but you can set it manually to notate an externally known cardinality.
        source_modality (Modality): Modality of the source model in the relationship.
            Defaults to Modality.UNSPECIFIED. This will never be set for Edges created by erdantic, 
            but you can set it manually to notate an externally known modality.
    """
    
    @property
    def key(self) -> str:
        """Returns the key used to identify this instance of Edge in the
        EntityRelationshipDiagram.edges mapping. This value is a hyphenated string of the fields
        `source_model_full_name`, `source_field_name`, and `target_model_full_name`.
        """
        
    @classmethod
    def from_field_info(cls, target_model: type, source_field_info: FieldInfo) -> Self:
        """Constructor method to create a new instance from a target model instance and a source
        model's FieldInfo.

        Args:
            target_model (type): Target model class.
            source_field_info (FieldInfo): FieldInfo instance for the field on the source model
                that references the target model.

        Returns:
            Self: New instance of Edge.
        """
        
    def target_dot_arrow_shape(self) -> str:
        """Arrow shape specification in Graphviz DOT language for this edge's head (the end at the
        target model). See [Graphviz docs](https://graphviz.org/doc/info/arrows.html) as a
        reference. Shape returned is based on
        [crow's foot notation](https://www.gleek.io/blog/crows-foot-notation) for the
        relationship's cardinality and modality.

        Returns:
            str: DOT language specification for arrow shape of this edge's head
        """
        
    def source_dot_arrow_shape(self) -> str:
        """Arrow shape specification in Graphviz DOT language for this edge's tail (the end at the
        source model). See [Graphviz docs](https://graphviz.org/doc/info/arrows.html) as a
        reference. Shape returned is based on
        [crow's foot notation](https://www.gleek.io/blog/crows-foot-notation) for the
        relationship's cardinality and modality.

        Returns:
            str: DOT language specification for arrow shape of this edge's tail
        """

@total_ordering
class FullyQualifiedName(pydantic.BaseModel):
    """Holds the fully qualified name components (module and qualified name) of a Python object.
    This is used to uniquely identify an object, can be used to import it.

    Attributes:
        module (str): Name of the module that the object is defined in.
        qual_name (str): Qualified name of the object.
    """
    
    @classmethod
    def from_object(cls, obj: Any) -> Self:
        """Constructor method to create a new instance from a Python object.

        Args:
            obj (Any): Python object.

        Returns:
            Self: Fully qualified name of the object.
        """
        
    def import_object(self) -> Any:
        """Imports the object from the module and returns it.

        Returns:
            Any: Object referenced by this FullyQualifiedName instance.
        """
        
    def __str__(self) -> str:
        """Returns string representation as 'module.qual_name'."""
        
    def __hash__(self) -> int:
        """Returns hash based on module and qual_name."""
        
    def __lt__(self, other: Self) -> bool:
        """Comparison operator for sorting."""

Enumerations

class Cardinality(Enum):
    """Enumeration of possible cardinality values for a relationship between two data model
    classes. Cardinality measures the maximum number of associations.
    """
    
    UNSPECIFIED = "unspecified"
    ONE = "one"
    MANY = "many"
    
    def to_dot(self) -> str:
        """Returns the DOT language specification for the arrowhead styling associated with the
        cardinality value.
        """

class Modality(Enum):
    """Enumeration of possible modality values for a relationship between two data model
    classes. Modality measures the minimum number of associations.
    """
    
    UNSPECIFIED = "unspecified"
    ZERO = "zero"
    ONE = "one"
    
    def to_dot(self) -> str:
        """Returns the DOT language specification for the arrowhead styling associated with the
        modality value.
        """

Required Imports

from erdantic.core import (
    ModelInfo, FieldInfo, Edge, FullyQualifiedName, 
    Cardinality, Modality
)
from enum import Enum
from typing import Any
from functools import total_ordering
import pydantic

Usage Examples

Accessing Diagram Information

from erdantic import create
from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str

class Post(BaseModel):
    title: str
    author: User

# Create diagram
diagram = create(User, Post)

# Access model information
for model_key, model_info in diagram.models.items():
    print(f"Model: {model_info.name}")
    print(f"  Full name: {model_info.full_name}")
    print(f"  Description: {model_info.description}")
    
    # Access field information
    for field_name, field_info in model_info.fields.items():
        print(f"  Field: {field_info.name} ({field_info.type_name})")

# Access relationship information
for edge_key, edge in diagram.edges.items():
    print(f"Relationship: {edge.source_model_full_name}.{edge.source_field_name} -> {edge.target_model_full_name}")
    print(f"  Target cardinality: {edge.target_cardinality.value}")
    print(f"  Target modality: {edge.target_modality.value}")

Working with FullyQualifiedName

from erdantic.core import FullyQualifiedName

# Create from object
fqn = FullyQualifiedName.from_object(User)
print(f"Module: {fqn.module}")        # __main__ (or actual module)
print(f"Qualified name: {fqn.qual_name}")  # User
print(f"Full name: {str(fqn)}")       # __main__.User

# Import object back
imported_class = fqn.import_object()
print(imported_class == User)  # True

Manual ModelInfo Creation

from erdantic.core import ModelInfo, FieldInfo, FullyQualifiedName

# Create model info manually (normally done automatically)
model_fqn = FullyQualifiedName.from_object(User)
model_info = ModelInfo.from_raw_model(User)

print(f"Model name: {model_info.name}")
print(f"Model key: {model_info.key}")
print(f"Fields: {list(model_info.fields.keys())}")

# Access the original model class
original_model = model_info.raw_model
print(original_model == User)  # True

Manual FieldInfo Creation

from erdantic.core import FieldInfo, FullyQualifiedName

# Create field info manually
model_fqn = FullyQualifiedName.from_object(User)
field_info = FieldInfo.from_raw_type(
    model_full_name=model_fqn,
    name="name",
    raw_type=str
)

print(f"Field name: {field_info.name}")
print(f"Field type: {field_info.type_name}")
print(f"Field key: {field_info.key}")

# Access raw type
raw_type = field_info.raw_type
print(raw_type == str)  # True

Understanding Relationships

# Examine edge details
for edge in diagram.edges.values():
    print(f"Source: {edge.source_model_full_name}")
    print(f"Source field: {edge.source_field_name}")
    print(f"Target: {edge.target_model_full_name}")
    
    # Cardinality and modality
    print(f"Target cardinality: {edge.target_cardinality}")  # ONE or MANY
    print(f"Target modality: {edge.target_modality}")        # ZERO, ONE, or UNSPECIFIED
    
    # DOT arrow shapes
    print(f"Target arrow: {edge.target_dot_arrow_shape()}")
    print(f"Source arrow: {edge.source_dot_arrow_shape()}")

Custom Edge Creation

from erdantic.core import Edge, Cardinality, Modality, FullyQualifiedName

# Create custom edge manually
user_fqn = FullyQualifiedName.from_object(User)
post_fqn = FullyQualifiedName.from_object(Post)

custom_edge = Edge(
    source_model_full_name=post_fqn,
    source_field_name="author",
    target_model_full_name=user_fqn,
    target_cardinality=Cardinality.ONE,
    target_modality=Modality.ONE,
    source_cardinality=Cardinality.MANY,  # Optional: many posts per user
    source_modality=Modality.ZERO         # Optional: user might have no posts
)

print(f"Edge key: {custom_edge.key}")

DOT Generation from Components

# Generate DOT representations of individual components
model_info = diagram.models[str(FullyQualifiedName.from_object(User))]
dot_label = model_info.to_dot_label()
print("Model DOT label:", dot_label)

field_info = model_info.fields["name"]
dot_row = field_info.to_dot_row()
print("Field DOT row:", dot_row)

Working with Cardinality and Modality

from erdantic.core import Cardinality, Modality

# All cardinality values
print("Cardinalities:")
for card in Cardinality:
    print(f"  {card.value}: {card.to_dot()}")

# All modality values  
print("Modalities:")
for mod in Modality:
    print(f"  {mod.value}: {mod.to_dot()}")

# Combined arrow shapes (cardinality + modality)
one_required = Cardinality.ONE.to_dot() + Modality.ONE.to_dot()
many_optional = Cardinality.MANY.to_dot() + Modality.ZERO.to_dot()
print(f"One required: {one_required}")    # noneteetee
print(f"Many optional: {many_optional}")  # crowodot

Install with Tessl CLI

npx tessl i tessl/pypi-erdantic

docs

cli.md

convenience-functions.md

diagram-creation.md

exceptions.md

index.md

model-info.md

plugin-system.md

tile.json