Entity relationship diagrams for Python data model classes like Pydantic.
—
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.
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."""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.
"""from erdantic.core import (
ModelInfo, FieldInfo, Edge, FullyQualifiedName,
Cardinality, Modality
)
from enum import Enum
from typing import Any
from functools import total_ordering
import pydanticfrom 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}")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) # Truefrom 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) # Truefrom 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# 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()}")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}")# 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)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}") # crowodotInstall with Tessl CLI
npx tessl i tessl/pypi-erdantic