Skip to content

erdantic.core

DEFAULT_EDGE_ATTR module-attribute

DEFAULT_EDGE_ATTR = (('dir', 'both'))

Default edge attributes passed to Graphviz.

DEFAULT_GRAPH_ATTR module-attribute

DEFAULT_GRAPH_ATTR = (
    ("nodesep", "0.5"),
    ("ranksep", "1.5"),
    ("rankdir", "LR"),
    (
        "label",
        f"Created by erdantic v{__version__} <https://github.com/drivendataorg/erdantic>",
    ),
    (
        "fontname",
        "Times New Roman,Times,Liberation Serif,serif",
    ),
    ("fontsize", "9"),
    ("fontcolor", "gray66"),
)

Default graph attributes passed to Graphviz.

DEFAULT_NODE_ATTR module-attribute

DEFAULT_NODE_ATTR = (
    (
        "fontname",
        "Times New Roman,Times,Liberation Serif,serif",
    ),
    ("fontsize", 14),
    ("shape", "plain"),
)

Default node attributes passed to Graphviz.

Cardinality

Bases: Enum

Enumeration of possible cardinality values for a relationship between two data model classes. Cardinality measures the maximum number of associations.

Source code in erdantic/core.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
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.
        """
        return _CARDINALITY_DOT_MAPPING[self]

to_dot

to_dot() -> str

Returns the DOT language specification for the arrowhead styling associated with the cardinality value.

Source code in erdantic/core.py
323
324
325
326
327
def to_dot(self) -> str:
    """Returns the DOT language specification for the arrowhead styling associated with the
    cardinality value.
    """
    return _CARDINALITY_DOT_MAPPING[self]

Edge

Bases: BaseModel

Hold information about a relationship between two data model classes. These represent directed edges in the entity relationship diagram.

Attributes:

Name Type Description
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 Optional[Cardinality]

Cardinality of the source model in the relationship. This will never be set for Edges created by erdantic, but you can set it manually to notate an externally known cardinality.

source_modality Optional[Modality]

Modality of the source model in the relationship. This will never be set for Edges created by erdantic, but you can set it manually to notate an externally known modality.

Source code in erdantic/core.py
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
@add_repr_pretty_to_pydantic
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 (Optional[Cardinality]): Cardinality of the source model in the
            relationship. This will never be set for Edges created by erdantic, but you can set it
            manually to notate an externally known cardinality.
        source_modality (Optional[Modality]): Modality of the source model in the relationship.
            This will never be set for Edges created by erdantic, but you can set it manually to
            notate an externally known modality.
    """

    source_model_full_name: FullyQualifiedName
    source_field_name: str
    target_model_full_name: FullyQualifiedName
    target_cardinality: Cardinality
    target_modality: Modality
    source_cardinality: Cardinality = Cardinality.UNSPECIFIED
    source_modality: Modality = Modality.UNSPECIFIED

    @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`.
        """
        return "-".join(
            str(part)
            for part in (
                self.source_model_full_name,
                self.source_field_name,
                self.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.
        """
        is_collection = is_collection_type_of(source_field_info.raw_type, target_model)
        is_nullable = is_nullable_type(source_field_info.raw_type)
        cardinality = Cardinality.MANY if is_collection else Cardinality.ONE
        if is_nullable:
            modality = Modality.ZERO
        else:
            modality = Modality.UNSPECIFIED if is_collection else Modality.ONE
        return cls(
            source_model_full_name=source_field_info.model_full_name,
            source_field_name=source_field_info.name,
            target_model_full_name=FullyQualifiedName.from_object(target_model),
            target_cardinality=cardinality,
            target_modality=modality,
        )

    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
        """
        return self.target_cardinality.to_dot() + self.target_modality.to_dot()

    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
        """
        return self.source_cardinality.to_dot() + self.source_modality.to_dot()

key property

key: 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.

from_field_info classmethod

from_field_info(
    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.

Parameters:

Name Type Description Default
target_model type

Target model class.

required
source_field_info FieldInfo

FieldInfo instance for the field on the source model that references the target model.

required

Returns:

Type Description
Self

New instance of Edge.

Source code in erdantic/core.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
@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.
    """
    is_collection = is_collection_type_of(source_field_info.raw_type, target_model)
    is_nullable = is_nullable_type(source_field_info.raw_type)
    cardinality = Cardinality.MANY if is_collection else Cardinality.ONE
    if is_nullable:
        modality = Modality.ZERO
    else:
        modality = Modality.UNSPECIFIED if is_collection else Modality.ONE
    return cls(
        source_model_full_name=source_field_info.model_full_name,
        source_field_name=source_field_info.name,
        target_model_full_name=FullyQualifiedName.from_object(target_model),
        target_cardinality=cardinality,
        target_modality=modality,
    )

source_dot_arrow_shape

source_dot_arrow_shape() -> str

Arrow shape specification in Graphviz DOT language for this edge's tail (the end at the source model). See Graphviz docs as a reference. Shape returned is based on crow's foot notation for the relationship's cardinality and modality.

Returns:

Type Description
str

DOT language specification for arrow shape of this edge's tail

Source code in erdantic/core.py
449
450
451
452
453
454
455
456
457
458
459
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
    """
    return self.source_cardinality.to_dot() + self.source_modality.to_dot()

target_dot_arrow_shape

target_dot_arrow_shape() -> str

Arrow shape specification in Graphviz DOT language for this edge's head (the end at the target model). See Graphviz docs as a reference. Shape returned is based on crow's foot notation for the relationship's cardinality and modality.

Returns:

Type Description
str

DOT language specification for arrow shape of this edge's head

Source code in erdantic/core.py
437
438
439
440
441
442
443
444
445
446
447
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
    """
    return self.target_cardinality.to_dot() + self.target_modality.to_dot()

EntityRelationshipDiagram

Bases: BaseModel

Holds information about an entity relationship diagram for a set of data model classes and their relationships, and provides methods to render the diagram.

Attributes:

Name Type Description
models SortedDict[str, ModelInfo]

Mapping of ModelInfo instances for models included in the diagram. Each key is the string representation of the fully qualified name of the model.

edges SortedSet[Edge]

Set of edges representing relationships between the models.

Source code in erdantic/core.py
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
class EntityRelationshipDiagram(pydantic.BaseModel):
    """Holds information about an entity relationship diagram for a set of data model classes and
    their relationships, and provides methods to render the diagram.

    Attributes:
        models (SortedDict[str, ModelInfo]): Mapping of ModelInfo instances for models included
            in the diagram. Each key is the string representation of the fully qualified name of
            the model.
        edges (SortedSet[Edge]): Set of edges representing relationships between the models.
    """

    models: SortedDict[str, ModelInfo] = SortedDict()
    edges: SortedDict[str, Edge] = SortedDict()

    model_config = pydantic.ConfigDict(
        validate_default=True,
    )

    @property
    def _model_info_cls(self) -> Type[ModelInfo]:
        """Returns the model info class used by this diagram class. For the normal
        EntityRelationshipDiagram class, this is erdantic.core.ModelInfo."""
        annotation = self.model_fields["models"].annotation
        args = get_args(annotation)
        return args[1]

    @property
    def _edge_cls(self) -> Type[Edge]:
        """Returns the edge class used by this diagram class. For the normal
        EntityRelationshipDiagram class, this is erdantic.core.Edge."""
        annotation = self.model_fields["edges"].annotation
        args = get_args(annotation)
        return args[1]

    def _add_if_model(self, model: type, recurse: bool) -> bool:
        """Private recursive method to add a model to the diagram."""
        try:
            key = str(FullyQualifiedName.from_object(model))
        except AttributeError as e:
            # May get typing special forms that don't have __qualname__ attribute
            # These are not going to be models
            if "__qualname__" in str(e):
                return False
            raise
        if key not in self.models:
            try:
                model_info = self._model_info_cls.from_raw_model(model)
                self.models[key] = model_info
                logger.debug("Sucessfully added model '%s'.", key)
                if recurse:
                    logger.debug("Searching fields of '%s' for other models...", key)
                    for field_info in model_info.fields.values():
                        logger.debug(
                            "Analyzing model '%s' field '%s' of type '%s'...",
                            key,
                            field_info.name,
                            field_info.type_name,
                        )
                        try:
                            for arg in get_recursive_args(field_info.raw_type):
                                is_model = self._add_if_model(arg, recurse=recurse)
                                if is_model:
                                    edge = self._edge_cls.from_field_info(arg, field_info)
                                    self.edges[edge.key] = edge
                                    logger.debug(
                                        "Added edge from model '%s' field '%s' to model '%s'.",
                                        edge.source_model_full_name,
                                        edge.source_field_name,
                                        edge.target_model_full_name,
                                    )
                        except _UnevaluatedForwardRefError as e:
                            raise UnevaluatedForwardRefError(
                                model_full_name=model_info.full_name,
                                field_name=field_info.name,
                                forward_ref=e.forward_ref,
                            )
            except UnknownModelTypeError:
                return False
        else:
            logger.debug("Model '%s' already exists in diagram.", key)
        return True

    def add_model(self, model: type, recurse=True):
        """Add a data model class to the diagram.

        Args:
            model (type): Data model class to add to the diagram.
            recurse (bool, optional): Whether to recursively add models referenced by fields of
                the given model. Defaults to True.

        Raises:
            UnknownModelTypeError: If the model is not recognized as a data model class type that
                is supported by registered plugins.
            UnresolvableForwardRefError: If the model contains a forward reference that cannot be
                automatically resolved.
        """
        logger.info("Adding model '%s' to diagram...", typenames(model))
        is_model = self._add_if_model(model, recurse=recurse)
        if not is_model:
            raise UnknownModelTypeError(model=model, available_plugins=list_plugins())

    def draw(
        self,
        out: Union[str, os.PathLike],
        graph_attr: Optional[Mapping[str, Any]] = None,
        node_attr: Optional[Mapping[str, Any]] = None,
        edge_attr: Optional[Mapping[str, Any]] = None,
        **kwargs,
    ):
        """Render entity relationship diagram for given data model classes to file. The file format
        can be inferred from the file extension. Typical formats include '.png', '.svg', and
        '.pdf'.

        Args:
            out (str | os.PathLike): Output file path for rendered diagram.
            graph_attr (Mapping[str, Any] | None, optional): Override any graph attributes on
                the `pygraphviz.AGraph` instance. Defaults to None.
            node_attr (Mapping[str, Any] | None, optional): Override any node attributes for all
                nodes on the `pygraphviz.AGraph` instance. Defaults to None.
            edge_attr (Mapping[str, Any] | None, optional): Override any edge attributes for all
                edges on the `pygraphviz.AGraph` instance. Defaults to None.
            **kwargs: Additional keyword arguments to
                [`pygraphviz.AGraph.draw`][pygraphviz.AGraph.draw].
        """
        logger.info("Rendering diagram to %s", out)
        self.to_graphviz(
            graph_attr=graph_attr,
            node_attr=node_attr,
            edge_attr=edge_attr,
        ).draw(out, prog="dot", **kwargs)

    def to_graphviz(
        self,
        graph_attr: Optional[Mapping[str, Any]] = None,
        node_attr: Optional[Mapping[str, Any]] = None,
        edge_attr: Optional[Mapping[str, Any]] = None,
    ) -> pgv.AGraph:
        """Return [`pygraphviz.AGraph`][pygraphviz.agraph.AGraph] instance for diagram.

        Args:
            graph_attr (Mapping[str, Any] | None, optional): Override any graph attributes on
                the `pygraphviz.AGraph` instance. Defaults to None.
            node_attr (Mapping[str, Any] | None, optional): Override any node attributes for all
                nodes on the `pygraphviz.AGraph` instance. Defaults to None.
            edge_attr (Mapping[str, Any] | None, optional): Override any edge attributes for all
                edges on the `pygraphviz.AGraph` instance. Defaults to None.

        Returns:
            pygraphviz.AGraph: graph object for diagram
        """
        g = pgv.AGraph(
            name="Entity Relationship Diagram created by erdantic",
            directed=True,
            strict=False,
        )
        g.graph_attr.update(DEFAULT_GRAPH_ATTR)
        g.graph_attr.update(graph_attr or {})
        g.node_attr.update(DEFAULT_NODE_ATTR)
        g.node_attr.update(node_attr or {})
        g.edge_attr.update(DEFAULT_EDGE_ATTR)
        g.edge_attr.update(edge_attr or {})
        for full_name, model_info in self.models.items():
            g.add_node(
                full_name,
                label=model_info.to_dot_label(),
                tooltip=model_info.description.replace("\n", "&#xA;"),
            )
        for edge in self.edges.values():
            g.add_edge(
                edge.source_model_full_name,
                edge.target_model_full_name,
                tailport=f"{edge.source_field_name}:e",
                headport="_root:w",
                arrowhead=edge.target_dot_arrow_shape(),
                arrowtail=edge.source_dot_arrow_shape(),
            )
        return g

    def to_dot(
        self,
        graph_attr: Optional[Mapping[str, Any]] = None,
        node_attr: Optional[Mapping[str, Any]] = None,
        edge_attr: Optional[Mapping[str, Any]] = None,
    ) -> str:
        """Generate Graphviz [DOT language](https://graphviz.org/doc/info/lang.html) representation
        of entity relationship diagram for given data model classes.

        Args:
            graph_attr (Mapping[str, Any] | None, optional): Override any graph attributes on
                the `pygraphviz.AGraph` instance. Defaults to None.
            node_attr (Mapping[str, Any] | None, optional): Override any node attributes for all
                nodes on the `pygraphviz.AGraph` instance. Defaults to None.
            edge_attr (Mapping[str, Any] | None, optional): Override any edge attributes for all
                edges on the `pygraphviz.AGraph` instance. Defaults to None.

        Returns:
            str: DOT language representation of diagram
        """
        return self.to_graphviz(
            graph_attr=graph_attr,
            node_attr=node_attr,
            edge_attr=edge_attr,
        ).string()

    def _repr_pretty_(self, p, cycle):
        """IPython special method to pretty-print an object."""
        try:
            # Try using rich if it's available, since rich is nicer
            import rich  # type: ignore [import-not-found]

            rich.print(self)
        except ModuleNotFoundError:
            if cycle:
                p.text(repr(self))
            else:
                with p.group(1, f"{self.__class__.__name__}(", ")"):
                    # Render models as a dictionary with ModelInfo objects abbreviated
                    p.text("models=")
                    p.pretty(
                        {k: ellipsis_arg_repr_factory(type(v)) for k, v in self.models.items()}
                    )
                    p.text(",")
                    p.breakable()
                    # Render edges as a dictionary with Edge objects abbreviated
                    p.text("edges=")
                    p.pretty(
                        {k: ellipsis_arg_repr_factory(type(v)) for k, v in self.edges.items()}
                    )
                    p.breakable()

    def _repr_png_(self) -> bytes:
        """IPython special method to display object as a PNG image."""
        graph = self.to_graphviz()
        return graph.draw(prog="dot", format="png")

    def _repr_svg_(self) -> str:
        """IPython special method to display object as an SVG image."""
        graph = self.to_graphviz()
        return graph.draw(prog="dot", format="svg").decode(graph.encoding)

    def __rich_repr__(self):
        """Rich special method to format the representation of an object."""
        yield "models", {k: ellipsis_arg_repr_factory(type(v)) for k, v in self.models.items()}
        yield "edges", {k: ellipsis_arg_repr_factory(type(v)) for k, v in self.edges.items()}

add_model

add_model(model: type, recurse=True)

Add a data model class to the diagram.

Parameters:

Name Type Description Default
model type

Data model class to add to the diagram.

required
recurse bool

Whether to recursively add models referenced by fields of the given model. Defaults to True.

True

Raises:

Type Description
UnknownModelTypeError

If the model is not recognized as a data model class type that is supported by registered plugins.

UnresolvableForwardRefError

If the model contains a forward reference that cannot be automatically resolved.

Source code in erdantic/core.py
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
def add_model(self, model: type, recurse=True):
    """Add a data model class to the diagram.

    Args:
        model (type): Data model class to add to the diagram.
        recurse (bool, optional): Whether to recursively add models referenced by fields of
            the given model. Defaults to True.

    Raises:
        UnknownModelTypeError: If the model is not recognized as a data model class type that
            is supported by registered plugins.
        UnresolvableForwardRefError: If the model contains a forward reference that cannot be
            automatically resolved.
    """
    logger.info("Adding model '%s' to diagram...", typenames(model))
    is_model = self._add_if_model(model, recurse=recurse)
    if not is_model:
        raise UnknownModelTypeError(model=model, available_plugins=list_plugins())

draw

draw(
    out: Union[str, PathLike],
    graph_attr: Optional[Mapping[str, Any]] = None,
    node_attr: Optional[Mapping[str, Any]] = None,
    edge_attr: Optional[Mapping[str, Any]] = None,
    **kwargs
)

Render entity relationship diagram for given data model classes to file. The file format can be inferred from the file extension. Typical formats include '.png', '.svg', and '.pdf'.

Parameters:

Name Type Description Default
out str | PathLike

Output file path for rendered diagram.

required
graph_attr Mapping[str, Any] | None

Override any graph attributes on the pygraphviz.AGraph instance. Defaults to None.

None
node_attr Mapping[str, Any] | None

Override any node attributes for all nodes on the pygraphviz.AGraph instance. Defaults to None.

None
edge_attr Mapping[str, Any] | None

Override any edge attributes for all edges on the pygraphviz.AGraph instance. Defaults to None.

None
**kwargs

Additional keyword arguments to pygraphviz.AGraph.draw.

{}
Source code in erdantic/core.py
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
def draw(
    self,
    out: Union[str, os.PathLike],
    graph_attr: Optional[Mapping[str, Any]] = None,
    node_attr: Optional[Mapping[str, Any]] = None,
    edge_attr: Optional[Mapping[str, Any]] = None,
    **kwargs,
):
    """Render entity relationship diagram for given data model classes to file. The file format
    can be inferred from the file extension. Typical formats include '.png', '.svg', and
    '.pdf'.

    Args:
        out (str | os.PathLike): Output file path for rendered diagram.
        graph_attr (Mapping[str, Any] | None, optional): Override any graph attributes on
            the `pygraphviz.AGraph` instance. Defaults to None.
        node_attr (Mapping[str, Any] | None, optional): Override any node attributes for all
            nodes on the `pygraphviz.AGraph` instance. Defaults to None.
        edge_attr (Mapping[str, Any] | None, optional): Override any edge attributes for all
            edges on the `pygraphviz.AGraph` instance. Defaults to None.
        **kwargs: Additional keyword arguments to
            [`pygraphviz.AGraph.draw`][pygraphviz.AGraph.draw].
    """
    logger.info("Rendering diagram to %s", out)
    self.to_graphviz(
        graph_attr=graph_attr,
        node_attr=node_attr,
        edge_attr=edge_attr,
    ).draw(out, prog="dot", **kwargs)

to_dot

to_dot(
    graph_attr: Optional[Mapping[str, Any]] = None,
    node_attr: Optional[Mapping[str, Any]] = None,
    edge_attr: Optional[Mapping[str, Any]] = None,
) -> str

Generate Graphviz DOT language representation of entity relationship diagram for given data model classes.

Parameters:

Name Type Description Default
graph_attr Mapping[str, Any] | None

Override any graph attributes on the pygraphviz.AGraph instance. Defaults to None.

None
node_attr Mapping[str, Any] | None

Override any node attributes for all nodes on the pygraphviz.AGraph instance. Defaults to None.

None
edge_attr Mapping[str, Any] | None

Override any edge attributes for all edges on the pygraphviz.AGraph instance. Defaults to None.

None

Returns:

Type Description
str

DOT language representation of diagram

Source code in erdantic/core.py
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
def to_dot(
    self,
    graph_attr: Optional[Mapping[str, Any]] = None,
    node_attr: Optional[Mapping[str, Any]] = None,
    edge_attr: Optional[Mapping[str, Any]] = None,
) -> str:
    """Generate Graphviz [DOT language](https://graphviz.org/doc/info/lang.html) representation
    of entity relationship diagram for given data model classes.

    Args:
        graph_attr (Mapping[str, Any] | None, optional): Override any graph attributes on
            the `pygraphviz.AGraph` instance. Defaults to None.
        node_attr (Mapping[str, Any] | None, optional): Override any node attributes for all
            nodes on the `pygraphviz.AGraph` instance. Defaults to None.
        edge_attr (Mapping[str, Any] | None, optional): Override any edge attributes for all
            edges on the `pygraphviz.AGraph` instance. Defaults to None.

    Returns:
        str: DOT language representation of diagram
    """
    return self.to_graphviz(
        graph_attr=graph_attr,
        node_attr=node_attr,
        edge_attr=edge_attr,
    ).string()

to_graphviz

to_graphviz(
    graph_attr: Optional[Mapping[str, Any]] = None,
    node_attr: Optional[Mapping[str, Any]] = None,
    edge_attr: Optional[Mapping[str, Any]] = None,
) -> AGraph

Return pygraphviz.AGraph instance for diagram.

Parameters:

Name Type Description Default
graph_attr Mapping[str, Any] | None

Override any graph attributes on the pygraphviz.AGraph instance. Defaults to None.

None
node_attr Mapping[str, Any] | None

Override any node attributes for all nodes on the pygraphviz.AGraph instance. Defaults to None.

None
edge_attr Mapping[str, Any] | None

Override any edge attributes for all edges on the pygraphviz.AGraph instance. Defaults to None.

None

Returns:

Type Description
AGraph

graph object for diagram

Source code in erdantic/core.py
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
def to_graphviz(
    self,
    graph_attr: Optional[Mapping[str, Any]] = None,
    node_attr: Optional[Mapping[str, Any]] = None,
    edge_attr: Optional[Mapping[str, Any]] = None,
) -> pgv.AGraph:
    """Return [`pygraphviz.AGraph`][pygraphviz.agraph.AGraph] instance for diagram.

    Args:
        graph_attr (Mapping[str, Any] | None, optional): Override any graph attributes on
            the `pygraphviz.AGraph` instance. Defaults to None.
        node_attr (Mapping[str, Any] | None, optional): Override any node attributes for all
            nodes on the `pygraphviz.AGraph` instance. Defaults to None.
        edge_attr (Mapping[str, Any] | None, optional): Override any edge attributes for all
            edges on the `pygraphviz.AGraph` instance. Defaults to None.

    Returns:
        pygraphviz.AGraph: graph object for diagram
    """
    g = pgv.AGraph(
        name="Entity Relationship Diagram created by erdantic",
        directed=True,
        strict=False,
    )
    g.graph_attr.update(DEFAULT_GRAPH_ATTR)
    g.graph_attr.update(graph_attr or {})
    g.node_attr.update(DEFAULT_NODE_ATTR)
    g.node_attr.update(node_attr or {})
    g.edge_attr.update(DEFAULT_EDGE_ATTR)
    g.edge_attr.update(edge_attr or {})
    for full_name, model_info in self.models.items():
        g.add_node(
            full_name,
            label=model_info.to_dot_label(),
            tooltip=model_info.description.replace("\n", "&#xA;"),
        )
    for edge in self.edges.values():
        g.add_edge(
            edge.source_model_full_name,
            edge.target_model_full_name,
            tailport=f"{edge.source_field_name}:e",
            headport="_root:w",
            arrowhead=edge.target_dot_arrow_shape(),
            arrowtail=edge.source_dot_arrow_shape(),
        )
    return g

FieldInfo

Bases: BaseModel

Holds information about a field of an analyzed data model class.

Attributes:

Name Type Description
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.

Source code in erdantic/core.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
@add_repr_pretty_to_pydantic
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.
    """

    model_full_name: FullyQualifiedName
    name: str
    type_name: str

    model_config = pydantic.ConfigDict(
        extra="forbid",
        protected_namespaces=(),
    )

    _dot_row_template = """<tr><td>{name}</td><td port="{name}">{type_name}</td></tr>"""

    _raw_type: Optional[type] = pydantic.PrivateAttr(None)

    @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: _description_
        """
        if get_origin(raw_type) is Annotated:
            # Drop the Annotated extra metadata for the string representation
            type_name = typenames(get_args(raw_type)[0], remove_modules=REMOVE_ALL_MODULES)
        else:
            type_name = typenames(raw_type, remove_modules=REMOVE_ALL_MODULES)
        field_info = cls(
            model_full_name=model_full_name,
            name=name,
            type_name=type_name,
        )
        field_info._raw_type = raw_type
        return field_info

    @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.
        """
        return self.name

    @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: _description_
            UnknownModelTypeError: _description_

        Returns:
            type: Type annotation.
        """
        if self._raw_type is None:
            model = self.model_full_name.import_object()
            get_fields_fn = identify_field_extractor_fn(model)
            if get_fields_fn:
                for field_info in get_fields_fn(model):
                    if field_info.name == self.name:
                        self._raw_type = field_info.raw_type
                        break
                else:
                    raise FieldNotFoundError(
                        name=self.name, obj=model, model_full_name=self.model_full_name
                    )
            else:
                raise UnknownModelTypeError(model=model, available_plugins=list_plugins())
        return self._raw_type

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, FieldInfo):
            return NotImplemented
        return self.model_dump() == other.model_dump()

    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
        """
        return self._dot_row_template.format(name=self.name, type_name=self.type_name)

key property

key: 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.

raw_type property

raw_type: 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:

Type Description
FieldNotFoundError

description

UnknownModelTypeError

description

Returns:

Type Description
type

Type annotation.

from_raw_type classmethod

from_raw_type(
    model_full_name: FullyQualifiedName,
    name: str,
    raw_type: type,
) -> Self

Constructor method to create a new instance from a raw type annotation.

Parameters:

Name Type Description Default
model_full_name FullyQualifiedName

Fully qualified name of the data model class that the field belongs to.

required
name str

Name of field.

required
raw_type type

Type annotation.

required

Returns:

Type Description
Self

description

Source code in erdantic/core.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
@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: _description_
    """
    if get_origin(raw_type) is Annotated:
        # Drop the Annotated extra metadata for the string representation
        type_name = typenames(get_args(raw_type)[0], remove_modules=REMOVE_ALL_MODULES)
    else:
        type_name = typenames(raw_type, remove_modules=REMOVE_ALL_MODULES)
    field_info = cls(
        model_full_name=model_full_name,
        name=name,
        type_name=type_name,
    )
    field_info._raw_type = raw_type
    return field_info

to_dot_row

to_dot_row() -> 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:

Type Description
str

DOT language for table row

Source code in erdantic/core.py
199
200
201
202
203
204
205
206
207
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
    """
    return self._dot_row_template.format(name=self.name, type_name=self.type_name)

FullyQualifiedName

Bases: 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:

Name Type Description
module str

Name of the module that the object is defined in.

qual_name str

Qualified name of the object.

Source code in erdantic/core.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@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.
    """

    module: str
    qual_name: str

    @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.
        """
        return cls(module=obj.__module__, qual_name=obj.__qualname__)

    def __hash__(self) -> int:
        return hash((self.module, self.qual_name))

    def __str__(self) -> str:
        return f"{self.module}.{self.qual_name}"

    def import_object(self) -> Any:
        """Imports the object from the module and returns it.

        Returns:
            Any: Object referenced by this FullyQualifiedName instance.
        """
        module = import_module(self.module)
        obj = module
        for name in self.qual_name.split("."):
            obj = getattr(obj, name)
        return obj

    def __lt__(self, other: Self) -> bool:
        if not isinstance(other, FullyQualifiedName):
            return NotImplemented
        return (self.module, self.qual_name) < (other.module, other.qual_name)

from_object classmethod

from_object(obj: Any) -> Self

Constructor method to create a new instance from a Python object.

Parameters:

Name Type Description Default
obj Any

Python object.

required

Returns:

Type Description
Self

Fully qualified name of the object.

Source code in erdantic/core.py
72
73
74
75
76
77
78
79
80
81
82
@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.
    """
    return cls(module=obj.__module__, qual_name=obj.__qualname__)

import_object

import_object() -> Any

Imports the object from the module and returns it.

Returns:

Type Description
Any

Object referenced by this FullyQualifiedName instance.

Source code in erdantic/core.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def import_object(self) -> Any:
    """Imports the object from the module and returns it.

    Returns:
        Any: Object referenced by this FullyQualifiedName instance.
    """
    module = import_module(self.module)
    obj = module
    for name in self.qual_name.split("."):
        obj = getattr(obj, name)
    return obj

Modality

Bases: Enum

Enumeration of possible modality values for a relationship between two data model classes. Modality measures the minimum number of associations.

Source code in erdantic/core.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
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.
        """
        return _MODALITY_DOT_MAPPING[self]

to_dot

to_dot() -> str

Returns the DOT language specification for the arrowhead styling associated with the modality value.

Source code in erdantic/core.py
346
347
348
349
350
def to_dot(self) -> str:
    """Returns the DOT language specification for the arrowhead styling associated with the
    modality value.
    """
    return _MODALITY_DOT_MAPPING[self]

ModelInfo

Bases: BaseModel, Generic[_ModelType]

Holds information about an analyzed data model class.

Attributes:

Name Type Description
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.

Source code in erdantic/core.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
@add_repr_pretty_to_pydantic
class ModelInfo(pydantic.BaseModel, Generic[_ModelType]):
    """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.
    """

    full_name: FullyQualifiedName
    name: str
    fields: Dict[str, FieldInfo]
    description: str = ""

    model_config = pydantic.ConfigDict(
        extra="forbid",
    )

    _dot_table_template = textwrap.dedent(
        """\
        <<table border="0" cellborder="1" cellspacing="0">
        <tr><td port="_root" colspan="{num_cols}"><b>{name}</b></td></tr>
        {rows}
        </table>>
        """
    )

    _raw_model: Optional[_ModelType] = pydantic.PrivateAttr(None)

    @classmethod
    def from_raw_model(cls, raw_model: _ModelType) -> 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.
        """
        get_fields_fn = identify_field_extractor_fn(raw_model)
        if not get_fields_fn:
            raise UnknownModelTypeError(model=raw_model, available_plugins=list_plugins())

        full_name = FullyQualifiedName.from_object(raw_model)
        description = str(full_name)
        docstring = inspect.getdoc(raw_model)
        if docstring:
            description += "\n\n" + docstring + "\n"

        model_info = cls(
            full_name=full_name,
            name=raw_model.__name__,
            fields={field_info.name: field_info for field_info in get_fields_fn(raw_model)},
            description=description,
        )
        model_info._raw_model = raw_model
        return model_info

    @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.
        """
        return str(self.full_name)

    @property
    def raw_model(self) -> _ModelType:
        """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.
        """
        if self._raw_model is None:
            self._raw_model = self.full_name.import_object()
        return self._raw_model

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, ModelInfo):
            return NotImplemented
        return self.model_dump() == other.model_dump()

    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
        """
        # Get number of columns dynamically from first row
        try:
            num_cols = next(iter(self.fields.values())).to_dot_row().count("<td")
        except StopIteration:
            # No fields, so just use 1 column
            num_cols = 1
        # Concatenate DOT of all rows together
        rows = "\n".join(field_info.to_dot_row() for field_info in self.fields.values()) + "\n"
        return self._dot_table_template.format(
            name=self.name, num_cols=num_cols, rows=rows
        ).replace("\n", "")

key property

key: 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.

raw_model property

raw_model: _ModelType

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.

from_raw_model classmethod

from_raw_model(raw_model: _ModelType) -> Self

Constructor method to create a new instance from a raw data model class.

Parameters:

Name Type Description Default
raw_model type

Data model class.

required

Returns:

Type Description
Self

New instance of ModelInfo.

Source code in erdantic/core.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
@classmethod
def from_raw_model(cls, raw_model: _ModelType) -> 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.
    """
    get_fields_fn = identify_field_extractor_fn(raw_model)
    if not get_fields_fn:
        raise UnknownModelTypeError(model=raw_model, available_plugins=list_plugins())

    full_name = FullyQualifiedName.from_object(raw_model)
    description = str(full_name)
    docstring = inspect.getdoc(raw_model)
    if docstring:
        description += "\n\n" + docstring + "\n"

    model_info = cls(
        full_name=full_name,
        name=raw_model.__name__,
        fields={field_info.name: field_info for field_info in get_fields_fn(raw_model)},
        description=description,
    )
    model_info._raw_model = raw_model
    return model_info

to_dot_label

to_dot_label() -> 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:

Type Description
str

DOT language for table

Source code in erdantic/core.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
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
    """
    # Get number of columns dynamically from first row
    try:
        num_cols = next(iter(self.fields.values())).to_dot_row().count("<td")
    except StopIteration:
        # No fields, so just use 1 column
        num_cols = 1
    # Concatenate DOT of all rows together
    rows = "\n".join(field_info.to_dot_row() for field_info in self.fields.values()) + "\n"
    return self._dot_table_template.format(
        name=self.name, num_cols=num_cols, rows=rows
    ).replace("\n", "")