Usage Example: dataclasses¶
This notebook shows an example of using erdantic with the standard library's dataclasses module.
Let's take a look at the dataclasses (from here on in referred to as "models") from the erdantic.examples.dataclasses
module. Here's their source code for clarity.
import inspect
import rich.syntax
import erdantic.examples.dataclasses
rich.syntax.Syntax(
inspect.getsource(erdantic.examples.dataclasses),
"python",
theme="default",
line_numbers=True
)
1 """Example data model classes using standard library's 2 [`dataclasses`](https://docs.python.org/3/library/dataclasses.html) module.""" 3 4 from dataclasses import dataclass, field 5 from datetime import datetime 6 from enum import Enum 7 from typing import List, Optional 8 9 10 class Alignment(str, Enum): 11 LAWFUL_GOOD = "lawful_good" 12 NEUTRAL_GOOD = "neutral_good" 13 CHAOTIC_GOOD = "chaotic_good" 14 LAWFUL_NEUTRAL = "lawful_neutral" 15 TRUE_NEUTRAL = "true_neutral" 16 CHAOTIC_NEUTRAL = "chaotic_neutral" 17 LAWFUL_EVIL = "lawful_evil" 18 NEUTRAL_EVIL = "neutral_evil" 19 CHAOTIC_EVIL = "chaotic_evil" 20 21 22 @dataclass 23 class Adventurer: 24 """A person often late for dinner but with a tale or two to tell. 25 26 Attributes: 27 name (str): Name of this adventurer 28 profession (str): Profession of this adventurer 29 alignment (Alignment): Alignment of this adventurer 30 level (int): Level of this adventurer 31 """ 32 33 name: str 34 profession: str 35 alignment: Alignment 36 level: int = 1 37 38 39 @dataclass 40 class QuestGiver: 41 """A person who offers a task that needs completing. 42 43 Attributes: 44 name (str): Name of this quest giver 45 faction (str): Faction that this quest giver belongs to 46 location (str): Location this quest giver can be found 47 """ 48 49 name: str 50 faction: Optional[str] = None 51 location: str = "Adventurer's Guild" 52 53 54 @dataclass 55 class Quest: 56 """A task to complete, with some monetary reward. 57 58 Attributes: 59 name (str): Name by which this quest is referred to 60 giver (QuestGiver): Person who offered the quest 61 reward_gold (int): Amount of gold to be rewarded for quest completion 62 """ 63 64 name: str 65 giver: QuestGiver 66 reward_gold: int = 100 67 68 69 @dataclass 70 class Party: 71 """A group of adventurers finding themselves doing and saying things altogether unexpected. 72 73 Attributes: 74 name (str): Name that party is known by 75 formed_datetime (datetime): Timestamp of when the party was formed 76 members (List[Adventurer]): Adventurers that belong to this party 77 active_quest (Optional[Quest]): Current quest that party is actively tackling 78 """ 79 80 name: str 81 formed_datetime: datetime 82 members: List[Adventurer] = field(default_factory=list) 83 active_quest: Optional[Quest] = None 84
Using the CLI¶
The fastest way to rendering a diagram is to use the command-line interface. Below we use IPython's !
to run a command in the system shell. We pass the full dotted path to the root class of our composition hierarchy, along with an output file path. erdantic will walk the composition graph to find all child classes.
!erdantic erdantic.examples.dataclasses.Party -o diagram.png
2024-07-16 17:47:35,178 | erdantic.core | INFO | Adding model 'erdantic.examples.dataclasses.Party' to diagram... 2024-07-16 17:47:35,181 | erdantic.core | INFO | Rendering diagram to diagram.png
2024-07-16 17:47:35,371 | erdantic.cli | INFO | Rendered diagram to diagram.png
The format rendered is inferred from the file extension.
Using the Python library¶
You can also use the erdantic Python library, which lets you inspect the diagram object. The diagram object contains all of the data that erdantic extracted about the model you provide, as well as any related models. As demonstrated below, the diagram object automatically pretty-prints in IPython or Jupyter notebooks and even automatically renders in Jupyter notebooks.
import erdantic as erd
from erdantic.examples.dataclasses import Party
diagram = erd.create(Party)
diagram
EntityRelationshipDiagram( models={ 'erdantic.examples.dataclasses.Adventurer': ModelInfo(...), 'erdantic.examples.dataclasses.Party': ModelInfo(...), 'erdantic.examples.dataclasses.Quest': ModelInfo(...), 'erdantic.examples.dataclasses.QuestGiver': ModelInfo(...) }, edges={ 'erdantic.examples.dataclasses.Party-active_quest-erdantic.examples.dataclasses.Quest': Edge(...), 'erdantic.examples.dataclasses.Party-members-erdantic.examples.dataclasses.Adventurer': Edge(...), 'erdantic.examples.dataclasses.Quest-giver-erdantic.examples.dataclasses.QuestGiver': Edge(...) } )
Inspecting the data¶
The models
attribute gives you access to a dictionary of ModelInfo
objects that contain the data for each model in the diagram. All of erdantic's data objects are Pydantic models.
Tip
If you have the rich library installed, the IPython/Jupyter representation of erdantic's data objects will be nicely colored.
list(diagram.models.keys())
['erdantic.examples.dataclasses.Adventurer', 'erdantic.examples.dataclasses.Party', 'erdantic.examples.dataclasses.Quest', 'erdantic.examples.dataclasses.QuestGiver']
diagram.models['erdantic.examples.dataclasses.Party']
ModelInfo( full_name=FullyQualifiedName(module='erdantic.examples.dataclasses', qual_name='Party'), name='Party', fields={ 'name': FieldInfo( model_full_name=FullyQualifiedName(module='erdantic.examples.dataclasses', qual_name='Party'), name='name', type_name='str' ), 'formed_datetime': FieldInfo( model_full_name=FullyQualifiedName(module='erdantic.examples.dataclasses', qual_name='Party'), name='formed_datetime', type_name='datetime' ), 'members': FieldInfo( model_full_name=FullyQualifiedName(module='erdantic.examples.dataclasses', qual_name='Party'), name='members', type_name='List[Adventurer]' ), 'active_quest': FieldInfo( model_full_name=FullyQualifiedName(module='erdantic.examples.dataclasses', qual_name='Party'), name='active_quest', type_name='Optional[Quest]' ) }, description='erdantic.examples.dataclasses.Party\n\nA group of adventurers finding themselves doing and saying things altogether unexpected.\n\nAttributes:\n name (str): Name that party is known by\n formed_datetime (datetime): Timestamp of when the party was formed\n members (List[Adventurer]): Adventurers that belong to this party\n active_quest (Optional[Quest]): Current quest that party is actively tackling\n' )
And the edges
attribute gives you access to a dictionary of Edge
objects that contain the data for each relationship between the models.
list(diagram.edges.keys())
['erdantic.examples.dataclasses.Party-active_quest-erdantic.examples.dataclasses.Quest', 'erdantic.examples.dataclasses.Party-members-erdantic.examples.dataclasses.Adventurer', 'erdantic.examples.dataclasses.Quest-giver-erdantic.examples.dataclasses.QuestGiver']
diagram.edges["erdantic.examples.dataclasses.Party-members-erdantic.examples.dataclasses.Adventurer"]
Edge( source_model_full_name=FullyQualifiedName(module='erdantic.examples.dataclasses', qual_name='Party'), source_field_name='members', target_model_full_name=FullyQualifiedName(module='erdantic.examples.dataclasses', qual_name='Adventurer'), target_cardinality=<Cardinality.MANY: 'many'>, target_modality=<Modality.UNSPECIFIED: 'unspecified'>, source_cardinality=<Cardinality.UNSPECIFIED: 'unspecified'>, source_modality=<Modality.UNSPECIFIED: 'unspecified'> )
Rendering the diagram to an image file¶
You can use the draw
method to render the diagram to disk.
diagram.draw("diagram.svg")
# Equivalently, use erd.draw directly from Party
# erd.draw(Party, out="dataclasses.svg")
erdantic uses Graphviz, a well-established open-source C library, to create the diagram. Graphviz uses the DOT language for describing graphs. You use the to_dot
method to get the DOT representation as a string.
print(diagram.to_dot())
# Equivalently, use erd.to_dot directly from Party
assert diagram.to_dot() == erd.to_dot(Party)
digraph "Entity Relationship Diagram created by erdantic" { graph [fontcolor=gray66, fontname="Times New Roman,Times,Liberation Serif,serif", fontsize=9, label="Created by erdantic v1.0.4 <https://github.com/drivendataorg/erdantic>", nodesep=0.5, rankdir=LR, ranksep=1.5 ]; node [fontname="Times New Roman,Times,Liberation Serif,serif", fontsize=14, label="\N", shape=plain ]; edge [dir=both]; "erdantic.examples.dataclasses.Adventurer" [label=<<table border="0" cellborder="1" cellspacing="0"><tr><td port="_root" colspan="2"><b>Adventurer</b></td></tr><tr><td>name</td><td port="name">str</td></tr><tr><td>profession</td><td port="profession">str</td></tr><tr><td>alignment</td><td port="alignment">Alignment</td></tr><tr><td>level</td><td port="level">int</td></tr></table>>, tooltip="erdantic.examples.dataclasses.Adventurer

A person often late for dinner but with a tale or two to tell.

Attributes:&#\ xA; name (str): Name of this adventurer
 profession (str): Profession of this adventurer
 alignment (Alignment): \ Alignment of this adventurer
 level (int): Level of this adventurer
"]; "erdantic.examples.dataclasses.Party" [label=<<table border="0" cellborder="1" cellspacing="0"><tr><td port="_root" colspan="2"><b>Party</b></td></tr><tr><td>name</td><td port="name">str</td></tr><tr><td>formed_datetime</td><td port="formed_datetime">datetime</td></tr><tr><td>members</td><td port="members">List[Adventurer]</td></tr><tr><td>active_quest</td><td port="active_quest">Optional[Quest]</td></tr></table>>, tooltip="erdantic.examples.dataclasses.Party

A group of adventurers finding themselves doing and saying things altogether unexpected.&#\ xA;
Attributes:
 name (str): Name that party is known by
 formed_datetime (datetime): Timestamp of when the party \ was formed
 members (List[Adventurer]): Adventurers that belong to this party
 active_quest (Optional[Quest]): Current \ quest that party is actively tackling
"]; "erdantic.examples.dataclasses.Party":members:e -> "erdantic.examples.dataclasses.Adventurer":_root:w [arrowhead=crownone, arrowtail=nonenone]; "erdantic.examples.dataclasses.Quest" [label=<<table border="0" cellborder="1" cellspacing="0"><tr><td port="_root" colspan="2"><b>Quest</b></td></tr><tr><td>name</td><td port="name">str</td></tr><tr><td>giver</td><td port="giver">QuestGiver</td></tr><tr><td>reward_gold</td><td port="reward_gold">int</td></tr></table>>, tooltip="erdantic.examples.dataclasses.Quest

A task to complete, with some monetary reward.

Attributes:
 name (str): \ Name by which this quest is referred to
 giver (QuestGiver): Person who offered the quest
 reward_gold (int): Amount \ of gold to be rewarded for quest completion
"]; "erdantic.examples.dataclasses.Party":active_quest:e -> "erdantic.examples.dataclasses.Quest":_root:w [arrowhead=noneteeodot, arrowtail=nonenone]; "erdantic.examples.dataclasses.QuestGiver" [label=<<table border="0" cellborder="1" cellspacing="0"><tr><td port="_root" colspan="2"><b>QuestGiver</b></td></tr><tr><td>name</td><td port="name">str</td></tr><tr><td>faction</td><td port="faction">Optional[str]</td></tr><tr><td>location</td><td port="location">str</td></tr></table>>, tooltip="erdantic.examples.dataclasses.QuestGiver

A person who offers a task that needs completing.

Attributes:
 \ name (str): Name of this quest giver
 faction (str): Faction that this quest giver belongs to
 location (str): Location \ this quest giver can be found
"]; "erdantic.examples.dataclasses.Quest":giver:e -> "erdantic.examples.dataclasses.QuestGiver":_root:w [arrowhead=noneteetee, arrowtail=nonenone]; }
Terminal Models¶
If you have an enormous composition graph and want to chop it up, you can make that work by specifying models to be terminal nodes.
For the CLI, use the -t
option to specify a model to be a terminus. To specify more than one, used repeated -t
options. So, for example, if you want one diagram rooted by Party
that terminates at Quest
, and another diagram that is rooted by Quest
, you can use the following two shell commands.
erdantic erdantic.examples.dataclasses.Party \
-t erdantic erdantic.examples.dataclasses.Quest \
-o party.png
erdantic erdantic.examples.dataclasses.Quest -o quest.png
When using the Python library, pass your terminal node in a list to the terminal_models
keyword argument. Below is the Python code for creating diagrams equivalent to the above shell commands.
from erdantic.examples.dataclasses import Quest
diagram1 = erd.create(Party, terminal_models=[Quest])
diagram1
EntityRelationshipDiagram( models={ 'erdantic.examples.dataclasses.Adventurer': ModelInfo(...), 'erdantic.examples.dataclasses.Party': ModelInfo(...), 'erdantic.examples.dataclasses.Quest': ModelInfo(...) }, edges={ 'erdantic.examples.dataclasses.Party-active_quest-erdantic.examples.dataclasses.Quest': Edge(...), 'erdantic.examples.dataclasses.Party-members-erdantic.examples.dataclasses.Adventurer': Edge(...) } )
diagram2 = erd.create(Quest)
diagram2
EntityRelationshipDiagram( models={ 'erdantic.examples.dataclasses.Quest': ModelInfo(...), 'erdantic.examples.dataclasses.QuestGiver': ModelInfo(...) }, edges={'erdantic.examples.dataclasses.Quest-giver-erdantic.examples.dataclasses.QuestGiver': Edge(...)} )