Skip to content

erdantic.erd

Edge

Class for an edge in the entity relationship diagram graph. Represents the composition relationship between a composite model (source via source_field) with a component model (target).

Attributes:

Name Type Description
source Model

Composite data model.

source_field Field

Field on source that has type of `target.

target Model

Component data model.

Source code in erdantic/erd.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Edge:
    """Class for an edge in the entity relationship diagram graph. Represents the composition
    relationship between a composite model (`source` via `source_field`) with a component model
    (`target`).

    Attributes:
        source (Model): Composite data model.
        source_field (Field): Field on `source` that has type of `target.
        target (Model): Component data model.
    """

    source: "Model"
    source_field: "Field"
    target: "Model"

    def __init__(self, source: "Model", source_field: "Field", target: "Model"):
        if source_field not in set(source.fields):
            raise UnknownFieldError(
                f"source_field {source_field} is not a field of source {source}"
            )
        self.source = source
        self.source_field = source_field
        self.target = target

    def dot_arrowhead(self) -> str:
        """Arrow shape specification in Graphviz DOT language for this edge's head. See
        [Graphviz docs](https://graphviz.org/doc/info/arrows.html) as a reference. Shape returned
        is based on [crow's foot notation](https://www.calebcurry.com/cardinality-and-modality/)
        for the relationship's cardinality and modality.

        Returns:
            str: DOT language specification for arrow shape of this edge's head
        """
        cardinality = "crow" if self.source_field.is_many() else "nonetee"
        modality = (
            "odot" if self.source_field.is_nullable() or self.source_field.is_many() else "tee"
        )
        return cardinality + modality

    def __hash__(self) -> int:
        return hash((self.source, self.source_field, self.target))

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, type(self)) and hash(self) == hash(other)

    def __repr__(self) -> str:
        return (
            f"Edge(source={repr(self.source)}, source_field={self.source_field}, "
            f"target={self.target})"
        )

    def __lt__(self, other) -> bool:
        if isinstance(other, Edge):
            self_key = (self.source, self.source.fields.index(self.source_field), self.target)
            other_key = (other.source, other.source.fields.index(other.source_field), other.target)
            return self_key < other_key
        return NotImplemented

dot_arrowhead()

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

Returns:

Name Type Description
str str

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

Source code in erdantic/erd.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def dot_arrowhead(self) -> str:
    """Arrow shape specification in Graphviz DOT language for this edge's head. See
    [Graphviz docs](https://graphviz.org/doc/info/arrows.html) as a reference. Shape returned
    is based on [crow's foot notation](https://www.calebcurry.com/cardinality-and-modality/)
    for the relationship's cardinality and modality.

    Returns:
        str: DOT language specification for arrow shape of this edge's head
    """
    cardinality = "crow" if self.source_field.is_many() else "nonetee"
    modality = (
        "odot" if self.source_field.is_nullable() or self.source_field.is_many() else "tee"
    )
    return cardinality + modality

EntityRelationshipDiagram

Class for entity relationship diagram.

Attributes:

Name Type Description
models List[Model]

Data models (nodes) in diagram.

attr2 List[Edge]

Edges in diagram, representing the composition relationship between models.

Source code in erdantic/erd.py
 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
106
107
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
class EntityRelationshipDiagram:
    """Class for entity relationship diagram.

    Attributes:
        models (List[Model]): Data models (nodes) in diagram.
        attr2 (List[Edge]): Edges in diagram, representing the composition relationship between
            models.
    """

    models: List["Model"]
    edges: List["Edge"]

    def __init__(self, models: Sequence["Model"], edges: Sequence["Edge"]):
        self.models = sorted(models)
        self.edges = sorted(edges)

    def draw(self, out: Union[str, os.PathLike], **kwargs):
        """Render entity relationship diagram for given data model classes to file.

        Args:
            out (Union[str, os.PathLike]): Output file path for rendered diagram.
            **kwargs: Additional keyword arguments to [`pygraphviz.AGraph.draw`](https://pygraphviz.github.io/documentation/latest/reference/agraph.html#pygraphviz.AGraph.draw).
        """
        self.graph().draw(out, prog="dot", **kwargs)

    def graph(self) -> pgv.AGraph:
        """Return [`pygraphviz.AGraph`](https://pygraphviz.github.io/documentation/latest/reference/agraph.html)
        instance for diagram.

        Returns:
            pygraphviz.AGraph: graph object for diagram
        """
        g = pgv.AGraph(
            directed=True,
            strict=False,
            nodesep=0.5,
            ranksep=1.5,
            rankdir="LR",
            name="Entity Relationship Diagram",
            label=f"Created by erdantic v{__version__} <https://github.com/drivendataorg/erdantic>",
            fontsize=9,
            fontcolor="gray66",
        )
        g.node_attr["fontsize"] = 14
        g.node_attr["shape"] = "plain"
        for model in self.models:
            g.add_node(
                model.key,
                label=model.dot_label(),
                tooltip=model.docstring.replace("\n", "&#xA;"),
            )
        for edge in self.edges:
            g.add_edge(
                edge.source.key,
                edge.target.key,
                tailport=f"{edge.source_field.name}:e",
                headport="_root:w",
                arrowhead=edge.dot_arrowhead(),
            )
        return g

    def to_dot(self) -> str:
        """Generate Graphviz [DOT language](https://graphviz.org/doc/info/lang.html) representation
        of entity relationship diagram for given data model classes.

        Returns:
            str: DOT language representation of diagram
        """
        return self.graph().string()

    def __hash__(self) -> int:
        return hash((tuple(self.models), tuple(self.edges)))

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, type(self)) and hash(self) == hash(other)

    def __repr__(self) -> str:
        models = ", ".join(repr(m) for m in self.models)
        edges = ", ".join(repr(e) for e in self.edges)
        return f"EntityRelationshipDiagram(models=[{models}], edges=[{edges}])"

    def _repr_png_(self) -> bytes:
        graph = self.graph()
        return graph.draw(prog="dot", format="png")

    def _repr_svg_(self) -> str:
        graph = self.graph()
        return graph.draw(prog="dot", format="svg").decode(graph.encoding)

draw(out, **kwargs)

Render entity relationship diagram for given data model classes to file.

Parameters:

Name Type Description Default
out Union[str, PathLike]

Output file path for rendered diagram.

required
**kwargs

Additional keyword arguments to pygraphviz.AGraph.draw.

{}
Source code in erdantic/erd.py
 97
 98
 99
100
101
102
103
104
def draw(self, out: Union[str, os.PathLike], **kwargs):
    """Render entity relationship diagram for given data model classes to file.

    Args:
        out (Union[str, os.PathLike]): Output file path for rendered diagram.
        **kwargs: Additional keyword arguments to [`pygraphviz.AGraph.draw`](https://pygraphviz.github.io/documentation/latest/reference/agraph.html#pygraphviz.AGraph.draw).
    """
    self.graph().draw(out, prog="dot", **kwargs)

graph()

Return pygraphviz.AGraph instance for diagram.

Returns:

Type Description
AGraph

pygraphviz.AGraph: graph object for diagram

Source code in erdantic/erd.py
106
107
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
def graph(self) -> pgv.AGraph:
    """Return [`pygraphviz.AGraph`](https://pygraphviz.github.io/documentation/latest/reference/agraph.html)
    instance for diagram.

    Returns:
        pygraphviz.AGraph: graph object for diagram
    """
    g = pgv.AGraph(
        directed=True,
        strict=False,
        nodesep=0.5,
        ranksep=1.5,
        rankdir="LR",
        name="Entity Relationship Diagram",
        label=f"Created by erdantic v{__version__} <https://github.com/drivendataorg/erdantic>",
        fontsize=9,
        fontcolor="gray66",
    )
    g.node_attr["fontsize"] = 14
    g.node_attr["shape"] = "plain"
    for model in self.models:
        g.add_node(
            model.key,
            label=model.dot_label(),
            tooltip=model.docstring.replace("\n", "&#xA;"),
        )
    for edge in self.edges:
        g.add_edge(
            edge.source.key,
            edge.target.key,
            tailport=f"{edge.source_field.name}:e",
            headport="_root:w",
            arrowhead=edge.dot_arrowhead(),
        )
    return g

to_dot()

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

Returns:

Name Type Description
str str

DOT language representation of diagram

Source code in erdantic/erd.py
142
143
144
145
146
147
148
149
def to_dot(self) -> str:
    """Generate Graphviz [DOT language](https://graphviz.org/doc/info/lang.html) representation
    of entity relationship diagram for given data model classes.

    Returns:
        str: DOT language representation of diagram
    """
    return self.graph().string()

adapt_model(obj)

Dispatch object to appropriate concrete Model adapter subclass and return instantiated adapter instance.

Parameters:

Name Type Description Default
obj Any

Data model class to adapt

required

Raises:

Type Description
UnknownModelTypeError

If obj does not match registered Model adapter classes

Returns:

Name Type Description
Model Model

Instantiated concrete Model subclass instance

Source code in erdantic/erd.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def adapt_model(obj: Any) -> Model:
    """Dispatch object to appropriate concrete [`Model`][erdantic.base.Model] adapter subclass and
    return instantiated adapter instance.

    Args:
        obj (Any): Data model class to adapt

    Raises:
        UnknownModelTypeError: If obj does not match registered Model adapter classes

    Returns:
        Model: Instantiated concrete `Model` subclass instance
    """
    for model_adapter in model_adapter_registry.values():
        if model_adapter.is_model_type(obj):
            return model_adapter(obj)
    raise UnknownModelTypeError(model=obj)

create(*models_or_modules, termini=[], limit_search_models_to=None)

Construct EntityRelationshipDiagram from given data model classes.

Parameters:

Name Type Description Default
*models_or_modules type

Data model classes to diagram or modules containing them.

()
termini Sequence[type]

Data model classes to set as terminal nodes. erdantic will stop searching for component classes when it reaches these models

[]
limit_search_models_to Optional[Iterable[sr]]

Iterable of identifiers of data model classes that erdantic supports. If any are specified, when searching a module, limit data model classes to those ones. Defaults to None which will find all data model classes supported by erdantic.

None

Raises: UnknownModelTypeError: If model is not recognized as a supported model type.

Returns:

Name Type Description
EntityRelationshipDiagram EntityRelationshipDiagram

diagram object for given data model.

Source code in erdantic/erd.py
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
208
209
210
def create(
    *models_or_modules: Union[type, ModuleType],
    termini: Sequence[type] = [],
    limit_search_models_to: Optional[Iterable[str]] = None,
) -> EntityRelationshipDiagram:
    """Construct [`EntityRelationshipDiagram`][erdantic.erd.EntityRelationshipDiagram] from given
    data model classes.

    Args:
        *models_or_modules (type): Data model classes to diagram or modules containing them.
        termini (Sequence[type]): Data model classes to set as terminal nodes. erdantic will stop
            searching for component classes when it reaches these models
        limit_search_models_to (Optional[Iterable[sr]], optional): Iterable of identifiers of data
            model classes that erdantic supports. If any are specified, when searching a module,
            limit data model classes to those ones. Defaults to None which will find all data model
            classes supported by erdantic.
    Raises:
        UnknownModelTypeError: If model is not recognized as a supported model type.

    Returns:
        EntityRelationshipDiagram: diagram object for given data model.
    """
    models = []
    for mm in models_or_modules:
        if isinstance(mm, type):
            models.append(mm)
        elif isinstance(mm, ModuleType):
            models.extend(find_models(mm, limit_search_models_to=limit_search_models_to))
        else:
            raise NotATypeError(f"Given model is not a type: {mm}")
    for terminal_model in tuple(termini):
        if not isinstance(terminal_model, type):
            raise NotATypeError(f"Given terminal model is not a type: {terminal_model}")

    seen_models: Set[Model] = {adapt_model(t) for t in termini}
    seen_edges: Set[Edge] = set()
    for raw_model in models:
        model = adapt_model(raw_model)
        search_composition_graph(model=model, seen_models=seen_models, seen_edges=seen_edges)
    return EntityRelationshipDiagram(models=list(seen_models), edges=list(seen_edges))

draw(*models_or_modules, out, termini=[], limit_search_models_to=None, **kwargs)

Render entity relationship diagram for given data model classes to file.

Parameters:

Name Type Description Default
*models_or_modules type

Data model classes to diagram, or modules containing them.

()
out Union[str, PathLike]

Output file path for rendered diagram.

required
termini Sequence[type]

Data model classes to set as terminal nodes. erdantic will stop searching for component classes when it reaches these models

[]
limit_search_models_to Optional[Iterable[sr]]

Iterable of identifiers of data model classes that erdantic supports. If any are specified, when searching a module, limit data model classes to those ones. Defaults to None which will find all data model classes supported by erdantic.

None
**kwargs

Additional keyword arguments to pygraphviz.AGraph.draw.

{}
Source code in erdantic/erd.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
def draw(
    *models_or_modules: Union[type, ModuleType],
    out: Union[str, os.PathLike],
    termini: Sequence[type] = [],
    limit_search_models_to: Optional[Iterable[str]] = None,
    **kwargs,
):
    """Render entity relationship diagram for given data model classes to file.

    Args:
        *models_or_modules (type): Data model classes to diagram, or modules containing them.
        out (Union[str, os.PathLike]): Output file path for rendered diagram.
        termini (Sequence[type]): Data model classes to set as terminal nodes. erdantic will stop
            searching for component classes when it reaches these models
        limit_search_models_to (Optional[Iterable[sr]], optional): Iterable of identifiers of data
            model classes that erdantic supports. If any are specified, when searching a module,
            limit data model classes to those ones. Defaults to None which will find all data model
            classes supported by erdantic.
        **kwargs: Additional keyword arguments to [`pygraphviz.AGraph.draw`](https://pygraphviz.github.io/documentation/latest/reference/agraph.html#pygraphviz.AGraph.draw).
    """
    diagram = create(
        *models_or_modules, termini=termini, limit_search_models_to=limit_search_models_to
    )
    diagram.draw(out=out, **kwargs)

find_models(module, limit_search_models_to=None)

Searches a module and yields all data model classes found.

Parameters:

Name Type Description Default
module ModuleType

Module to search for data model classes

required
limit_search_models_to Optional[Iterable[sr]]

Iterable of identifiers of data model classes that erdantic supports. If any are specified, when searching a module, limit data model classes to those ones. Defaults to None which will find all data model classes supported by erdantic.

None

Yields:

Type Description
type

Iterator[type]: Members of module that are data model classes.

Source code in erdantic/erd.py
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
def find_models(
    module: ModuleType, limit_search_models_to: Optional[Iterable[str]] = None
) -> Iterator[type]:
    """Searches a module and yields all data model classes found.

    Args:
        module (ModuleType): Module to search for data model classes
        limit_search_models_to (Optional[Iterable[sr]], optional): Iterable of identifiers of data
            model classes that erdantic supports. If any are specified, when searching a module,
            limit data model classes to those ones. Defaults to None which will find all data model
            classes supported by erdantic.

    Yields:
        Iterator[type]: Members of module that are data model classes.
    """
    limit_search_models_to_adapters: Iterable[Type[Model]]
    if limit_search_models_to is None:
        limit_search_models_to_adapters = model_adapter_registry.values()
    else:
        limit_search_models_to_adapters = [get_model_adapter(m) for m in limit_search_models_to]

    for _, member in inspect.getmembers(module, inspect.isclass):
        if member.__module__ == module.__name__:
            for model_adapter in limit_search_models_to_adapters:
                if model_adapter.is_model_type(member):
                    yield member

search_composition_graph(model, seen_models, seen_edges)

Recursively search composition graph for a model, where nodes are models and edges are composition relationships between models. Nodes and edges that are discovered will be added to the two respective provided set instances.

Parameters:

Name Type Description Default
model Model

Root node to begin search.

required
seen_models Set[Model]

Set instance that visited nodes will be added to.

required
seen_edges Set[Edge]

Set instance that traversed edges will be added to.

required
Source code in erdantic/erd.py
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
def search_composition_graph(
    model: Model,
    seen_models: Set[Model],
    seen_edges: Set[Edge],
):
    """Recursively search composition graph for a model, where nodes are models and edges are
    composition relationships between models. Nodes and edges that are discovered will be added to
    the two respective provided set instances.

    Args:
        model (Model): Root node to begin search.
        seen_models (Set[Model]): Set instance that visited nodes will be added to.
        seen_edges (Set[Edge]): Set instance that traversed edges will be added to.
    """
    if model not in seen_models:
        seen_models.add(model)
        for field in model.fields:
            try:
                for arg in get_recursive_args(field.type_obj):
                    try:
                        field_model = adapt_model(arg)
                        seen_edges.add(Edge(source=model, source_field=field, target=field_model))
                        search_composition_graph(field_model, seen_models, seen_edges)
                    except UnknownModelTypeError:
                        pass
            except _UnevaluatedForwardRefError as e:
                raise UnevaluatedForwardRefError(
                    model=model, field=field, forward_ref=e.forward_ref
                ) from None
            except _StringForwardRefError as e:
                raise StringForwardRefError(
                    model=model, field=field, forward_ref=e.forward_ref
                ) from None

to_dot(*models_or_modules, termini=[], limit_search_models_to=None)

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

Parameters:

Name Type Description Default
*models_or_modules type

Data model classes to diagram, or modules containing them.

()
termini Sequence[type]

Data model classes to set as terminal nodes. erdantic will stop searching for component classes when it reaches these models

[]
limit_search_models_to Optional[Iterable[sr]]

Iterable of identifiers of data model classes that erdantic supports. If any are specified, when searching a module, limit data model classes to those ones. Defaults to None which will find all data model classes supported by erdantic.

None

Returns:

Name Type Description
str str

DOT language representation of diagram

Source code in erdantic/erd.py
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
def to_dot(
    *models_or_modules: Union[type, ModuleType],
    termini: Sequence[type] = [],
    limit_search_models_to: Optional[Iterable[str]] = None,
) -> str:
    """Generate Graphviz [DOT language](https://graphviz.org/doc/info/lang.html) representation of
    entity relationship diagram for given data model classes.

    Args:
        *models_or_modules (type): Data model classes to diagram, or modules containing them.
        termini (Sequence[type]): Data model classes to set as terminal nodes. erdantic will stop
            searching for component classes when it reaches these models
        limit_search_models_to (Optional[Iterable[sr]], optional): Iterable of identifiers of data
            model classes that erdantic supports. If any are specified, when searching a module,
            limit data model classes to those ones. Defaults to None which will find all data model
            classes supported by erdantic.

    Returns:
        str: DOT language representation of diagram
    """
    diagram = create(
        *models_or_modules, termini=termini, limit_search_models_to=limit_search_models_to
    )
    return diagram.to_dot()