Tools for building, querying, manipulating, and exporting directed graphs with django
Project description
django-directed
Tools for building, querying, manipulating, and exporting directed graphs with django.
Documentation can be found at https://django-directed.readthedocs.io/en/latest/
This project is very much a Work In Progress, and is not production-ready. Once it is is a more complete state, it will be moved to the github Watervize organization for long-term development and maintenance.
Fundamentals
Graphs in django-directed are constructed with three models (or potentially more in case of extended features).
- Graph: Represents a connected graph of nodes and edges. It makes it easy to associate metadata with a particular graph and to run commands and queries limited to a subset of all the Edges and Nodes in the database.
- Edge: Connects Nodes to one another within a particular Graph instance.
- Node: A node can belong to more than one Graph. This allows us to represent multi-dimensional or multi-layered graphs.
django-directed includes model factories for building various types of directed graphs. As an example, imagine a project in which you display family trees and also provide a searchable interface for research papers about family trees, where papers can be linked to previous papers that they cite. Both of these concepts can be represented by a Directed Acyclic Graph (DAG), and within your project you could create a set of DAG models for the family tree app and another set of DAG models for the academic papers app.
Quickstart
Assuming you have already started a django project and an app named myapp
Install django-directed
pip install django-directed
Create the concrete models
Using the DAG factory, create a set of concrete Graph, Edge, and Node models for your project. Perform the following steps in your app's models.py
Build a configuration object that will be passed into the factory. Here, we are using the simplest configuration which specifies the model (with appname.ModelName
), but uses the default values for all other configuration options.
from django_directed.models import GraphConfig
my_config = GraphConfig(
graph_model_name="myapp.DAGGraph",
edge_model_name="myapp.DAGEdge",
node_model_name="myapp.DAGNode",
)
Create the concrete models from a model factory service. In this example, we are adding some fields as an example of what you might do in your own application.
# Create DAG factory instance
dag = factory.create("DAG", config=my_config)
# Create concrete models
class DAGGraph(dag.graph()):
metadata = models.JSONField(default=str, blank=True)
class DAGEdge(dag.edge()):
name = models.CharField(max_length=101, blank=True)
weight = models.SmallIntegerField(default=1)
def save(self, *args, **kwargs):
self.name = f"{self.parent.name} {self.child.name}"
super().save(*args, **kwargs)
class DAGNode(dag.node()):
name = models.CharField(max_length=50)
weight = models.SmallIntegerField(default=1)
The model names here (DAGGraph, etc) are for example only. You are welcome to use whatever names you like, but the model names should match the names provided in the configuration.
Migrations
As usual when working with models in django, we need to make migrations and then run them.
python manage.py makemigrations
python manage.py migrate
Build out a basic graph
We are using the `graph_context_manager` here, which is provided in django-directed for convenience. If you decide not to use this context manager, you need to provide the graph instance when creating or querying with Nodes and Edges.
from django_directed.context_managers import graph_scope
from myapp.models import DAGGraph, DAGEdge, DAGNode
# Create a graph instance
first_graph = DAGGraph.objects.create()
# Creating a second graph instance, which will share nodes with first_graph
another_graph = DAGGraph.objects.create()
with graph_scope(first_graph):
# Create several nodes (not yet connected)
root = DAGNode.objects.create(name="root")
a1 = DAGNode.objects.create(name="a1")
a2 = DAGNode.objects.create(name="a2")
a3 = DAGNode.objects.create(name="a3")
b1 = DAGNode.objects.create(name="b1")
b2 = DAGNode.objects.create(name="b2")
b3 = DAGNode.objects.create(name="b3")
b4 = DAGNode.objects.create(name="b4")
c1 = DAGNode.objects.create(name="c1")
c2 = DAGNode.objects.create(name="c2")
# Connect nodes with edges
root.add_child(a1)
root.add_child(a2)
# You can add from either side of the relationship
a3.add_parent(root)
b1.add_parent(a1)
a1.add_child(b2)
a2.add_child(b2)
a3.add_child(b3)
a3.add_child(b4)
b3.add_child(c2)
b3.add_child(c1)
b4.add_child(c2)
with graph_scope(another_graph):
# Connect nodes with edges
c1 = DAGNode.objects.get(name="c1")
c2 = DAGNode.objects.get(name="c2")
c1.add_child(c2)
Resulting Model Data
Here is the resulting data in each model (ignoring the custom fields added in the concrete model definitions).
myapp.DAGGraph
id
----
1
2
myapp.DAGNode
id | name | graph
-----+------+------
1 | root | 1
2 | a1 | 1
3 | a2 | 1
4 | a3 | 1
5 | b1 | 1
6 | b2 | 1
7 | b3 | 1
8 | b4 | 1
9 | c1 | 1
10 | c2 | 1
myapp.DAGEdge
id | parent_id | child_id | name | graph
----+-----------+----------+---------+------
1 | 1 | 2 | root a1 | 1
2 | 1 | 3 | root a2 | 1
3 | 1 | 4 | root a3 | 1
4 | 2 | 5 | a1 b1 | 1
5 | 2 | 6 | a1 b2 | 1
6 | 3 | 6 | a2 b2 | 1
7 | 4 | 7 | a3 b3 | 1
8 | 4 | 8 | a3 b4 | 1
9 | 7 | 10 | b3 c2 | 1
10 | 7 | 9 | b3 c1 | 1
11 | 8 | 10 | b4 c2 | 1
12 | 9 | 10 | c1 c2 | 2
Graph visualization
In the visualized graph below, both of the green nodes (c1) refer to the same Node instance, which is associated with two different graph instances. Likewise, both blue nodes (c2) refer to the same Node instance.
The mermaid.js diagrams require different markup for GitHub markdown compared to display within ReadTheDocs. Both versions are included here, but one will likely appear as code depending on where you are viewing this file.
Graph for display on GitHub
graph TD;
root((root));
a1((a1));
a2((a2));
a3((a3));
b1((b1));
b2((b2));
b3((b3));
b4((b4));
c1((c1));
c2((c2));
c1X((c1));
c2X((c2));
root-->a1;
root-->a2;
root-->a3;
a1-->b1;
a1-->b2;
a2-->b2;
a3-->b3;
a3-->b4;
b3-->c1;
b3-->c2;
b4-->c2;
c1X-->c2X;
style c1 fill:#48A127,stroke:#333,stroke-width:4px;
style c1X fill:#48A127,stroke:#333,stroke-width:4px;
style c2 fill:#279BA1,stroke:#333,stroke-width:4px;
style c2X fill:#279BA1,stroke:#333,stroke-width:4px;
linkStyle default fill:none,stroke:gray
Graph for display on ReadTheDocs
graph TD;
root((root));
a1((a1));
a2((a2));
a3((a3));
b1((b1));
b2((b2));
b3((b3));
b4((b4));
c1((c1));
c2((c2));
c1X((c1));
c2X((c2));
root-->a1;
root-->a2;
root-->a3;
a1-->b1;
a1-->b2;
a2-->b2;
a3-->b3;
a3-->b4;
b3-->c1;
b3-->c2;
b4-->c2;
c1X-->c2X;
style c1 fill:#48A127,stroke:#333,stroke-width:4px;
style c1X fill:#48A127,stroke:#333,stroke-width:4px;
style c2 fill:#279BA1,stroke:#333,stroke-width:4px;
style c2X fill:#279BA1,stroke:#333,stroke-width:4px;
linkStyle default fill:none,stroke:gray
Find the shortest path between two nodes
First, let us try to get the shortest path from c1
and c2
on first_graph
, where no path exists:
with graph_scope(first_graph):
c1 = DAGNode.objects.get(name="c1")
c2 = DAGNode.objects.get(name="c2")
print(c1.shortest_path(c2))
Output: django_directed.models.NodeNotReachableException
Next, we will perform the same query on another_graph
, which does have a path from c1
to c2
through a single Edge. The value returned is a QuerySet of the Nodes in the path.
with graph_scope(another_graph):
c1 = DAGNode.objects.get(name="c1")
c2 = DAGNode.objects.get(name="c2")
print(c1.shortest_path(c2))
Output: <QuerySet [<NetworkNode: c1>, <NetworkNode: c2>]>
For additional methods of querying, see the API docs for Graph, Edge, and Node.
Example apps
These are in-progress, and not ready for actual use.
A series of example apps demonstrating vaious aspects and techniques of using django-directed.
- Airports - An app demonstrating one method of working with multidimensional graphs to model airports with a common set of nodes, and edges for each of the connecting airlines.
- Electrical Grids - Demonstrate graphs of neighborhood electrical connections and meters.
- Family Trees - Demonstrates building family trees for multiple mythological families.
- Forums - Forums and threaded comments.
- NetworkX Graphs - Demonstration of using NetworkX alongside django-directed.
See the Example Apps folder.
Why not use a graph database instead?
- Compatibility - Graph databases don't play very nicely with Django and the Django ORM. There are 3rd party packages to shoehorn in the required functionality, but django is designed for relational databases.
- Simplicity - If most of the work you are doing needs a relational database, mixing an additional entirely different kind of database into the project might not be ideal.
- Tradeoffs - Graph databases are not a panacea. They bring their own set of pros and cons. Maybe a graph database is ideal for your project. But maybe you'll do just as well using django-directed. I encourage you to read up on the benefits graph databases bring, the issues they solve, and also the areas where they do not perform as well as a relational database.
History
0.1.0 (2022-02-08)
- Built initial readme entry to start documenting project goals.
0.1.1 (2022-02-17)
- Continue work on boilerplate to flesh out the foundations of the project.
0.1.2 (2022-02-21)
- Add terminology documentation.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file django-directed-0.1.3.tar.gz
.
File metadata
- Download URL: django-directed-0.1.3.tar.gz
- Upload date:
- Size: 23.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.8.2 readme-renderer/33.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.63.0 importlib-metadata/4.11.2 keyring/23.5.0 rfc3986/2.0.0 colorama/0.4.4 CPython/3.9.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | a9b21d32ac50f941e5926f5c6793743ec9f59373c2178148d2c3b7b360e68837 |
|
MD5 | 27478ab8e9d82c431320edcbabb3a5ce |
|
BLAKE2b-256 | ba4a72ae14dc9cef7d9bbe078b829fc25ec379c798412f61a0274e668391e676 |
File details
Details for the file django_directed-0.1.3-py2.py3-none-any.whl
.
File metadata
- Download URL: django_directed-0.1.3-py2.py3-none-any.whl
- Upload date:
- Size: 23.6 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.8.2 readme-renderer/33.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.63.0 importlib-metadata/4.11.2 keyring/23.5.0 rfc3986/2.0.0 colorama/0.4.4 CPython/3.9.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 38e7a9bd2df1813d38a4297bd454870cac2c8557f83a4770ed75faa6eb03bb67 |
|
MD5 | 945449735629075a9483ab04345842f4 |
|
BLAKE2b-256 | e0aadd9a934a8ab4ee3c3095e2e0f13120d6e784999777d386090e2daf6bd40d |