Skip to content

Commit

Permalink
Improved docs to flesh out descriptions of various model/manager methods
Browse files Browse the repository at this point in the history
  • Loading branch information
JackAtOmenApps committed Mar 20, 2021
1 parent 36cb662 commit 1009d5f
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 40 deletions.
127 changes: 87 additions & 40 deletions django_postgresql_dag/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""
A class to model hierarchies of objects following Directed Acyclic Graph structure.
A set of model classes to model hierarchies of objects following Directed Acyclic Graph structure.
The graph traversal queries use Postgresql's recursive CTEs to fetch an entire tree
of related node ids in a single query. These queries also topologically sort the ids
by generation.
The graph traversal queries use Postgresql's recursive CTEs to fetch an entire tree of related node ids in a single
query. These queries also topologically sort the ids by generation.
"""

from django.apps import apps
Expand All @@ -18,13 +17,19 @@

class NodeManager(models.Manager):
def roots(self, node=None):
"""Returns a Queryset of all root Nodes, or optionally, the roots of a select node"""
"""
Returns a Queryset of all root nodes (nodes with no parents) in the Node model. If a node instance is specified,
returns only the roots for that node.
"""
if node is not None:
return node.roots()
return self.filter(parents__isnull=True)

def leaves(self, node=None):
"""Returns a Queryset of all leaf Nodes, or optionally, the leaves of a select node"""
"""
Returns a Queryset of all leaf nodes (nodes with no children) in the Node model. If a node instance is
specified, returns only the leaves for that node.
"""
if node is not None:
return node.leaves()
return self.filter(children__isnull=True)
Expand Down Expand Up @@ -72,6 +77,7 @@ def ordered_queryset_from_pks(self, pks):
return _ordered_filter(self.__class__.objects, "pk", pks)

def add_child(self, child, **kwargs):
"""Provided with a Node instance, attaches that instance as a child to the current Node instance"""
kwargs.update({"parent": self, "child": child})

disable_circular_check = kwargs.pop("disable_circular_check", False)
Expand All @@ -81,7 +87,10 @@ def add_child(self, child, **kwargs):
return cls.save(disable_circular_check=disable_circular_check, allow_duplicate_edges=allow_duplicate_edges)

def remove_child(self, child, delete_node=False):
"""Removes the edge connecting this node to child, and optionally deletes the child node as well"""
"""
Removes the edge connecting this node to the provided child Node instance, and optionally deletes the child
node as well
"""
if child in self.children.all():
self.children.through.objects.filter(parent=self, child=child).delete()
if delete_node:
Expand All @@ -92,6 +101,7 @@ def remove_child(self, child, delete_node=False):
child.delete()

def add_parent(self, parent, *args, **kwargs):
"""Provided with a Node instance, attaches the current instance as a child to the provided Node instance"""
return parent.add_child(self, **kwargs)

def remove_parent(self, parent, delete_node=False):
Expand All @@ -106,44 +116,54 @@ def remove_parent(self, parent, delete_node=False):
parent.delete()

def ancestors_raw(self, **kwargs):
"""Returns a raw QuerySet of all nodes in connected paths in a rootward direction"""
return AncestorQuery(instance=self, **kwargs).raw_queryset()

def ancestors(self, **kwargs):
"""Returns a QuerySet of all nodes in connected paths in a rootward direction"""
pks = [item.pk for item in self.ancestors_raw(**kwargs)]
return self.ordered_queryset_from_pks(pks)

def ancestors_count(self):
"""Returns an integer number representing the total number of ancestor nodes"""
return self.ancestors().count()

def self_and_ancestors(self, **kwargs):
"""Returns a QuerySet of all nodes in connected paths in a rootward direction, prepending with self"""
pks = [self.pk] + [item.pk for item in self.ancestors_raw(**kwargs)][::-1]
return self.ordered_queryset_from_pks(pks)

def ancestors_and_self(self, **kwargs):
"""Returns a QuerySet of all nodes in connected paths in a rootward direction, appending with self"""
pks = [item.pk for item in self.ancestors_raw(**kwargs)] + [self.pk]
return self.ordered_queryset_from_pks(pks)

def descendants_raw(self, **kwargs):
"""Returns a raw QuerySet of all nodes in connected paths in a leafward direction"""
return DescendantQuery(instance=self, **kwargs).raw_queryset()

def descendants(self, **kwargs):
"""Returns a QuerySet of all nodes in connected paths in a leafward direction"""
pks = [item.pk for item in self.descendants_raw(**kwargs)]
return self.ordered_queryset_from_pks(pks)

def descendants_count(self):
"""Returns an integer number representing the total number of descendant nodes"""
return self.descendants().count()

def self_and_descendants(self, **kwargs):
"""Returns a QuerySet of all nodes in connected paths in a leafward direction, prepending with self"""
pks = [self.pk] + [item.pk for item in self.descendants_raw(**kwargs)]
return self.ordered_queryset_from_pks(pks)

def descendants_and_self(self, **kwargs):
"""Returns a QuerySet of all nodes in connected paths in a leafward direction, appending with self"""
pks = [item.pk for item in self.descendants_raw(**kwargs)] + [self.pk]
return self.ordered_queryset_from_pks(pks)

def clan(self, **kwargs):
"""
Returns a queryset with all ancestors, self, and all descendants
Returns a QuerySet with all ancestors nodes, self, and all descendant nodes
"""
pks = (
[item.pk for item in self.ancestors_raw(**kwargs)]
Expand All @@ -153,22 +173,23 @@ def clan(self, **kwargs):
return self.ordered_queryset_from_pks(pks)

def clan_count(self):
"""Returns an integer number representing the total number of clan nodes"""
return self.clan().count()

def siblings(self):
# Returns all nodes that share a parent with this node
"""Returns a QuerySet of all nodes that share a parent with this node, excluding self"""
return self.siblings_with_self().exclude(pk=self.pk)

def siblings_count(self):
# Returns count of all nodes that share a parent with this node
"""Returns count of all nodes that share a parent with this node"""
return self.siblings().count()

def siblings_with_self(self):
# Returns all nodes that share a parent with this node and self
"""Returns a QuerySet of all nodes that share a parent with this node and self"""
return self.__class__.objects.filter(parents__in=self.parents.all()).distinct()

def partners(self):
# Returns all nodes that share a child with this node
"""Returns a QuerySet of all nodes that share a child with this node"""
return self.partners_with_self().exclude(pk=self.pk)

def partners_count(self):
Expand Down Expand Up @@ -200,12 +221,21 @@ def path_raw(self, ending_node, directional=True, **kwargs):
return path

def path_exists(self, ending_node, **kwargs):
"""
Given an ending Node instance, returns a boolean value determining whether there is a path from the current
Node instance to the ending Node instance
"""
try:
return len(list(self.path_raw(ending_node, **kwargs))) >= 1
except NodeNotReachableException:
return False

def path(self, ending_node, **kwargs):
"""
Returns a QuerySet of the shortest path from self to ending node, optionally in either direction.
The resulting Queryset is sorted from root-side, toward leaf-side, regardless of the relative position of
starting and ending nodes.
"""
pks = [item.pk for item in self.path_raw(ending_node, **kwargs)]
return self.ordered_queryset_from_pks(pks)

Expand All @@ -220,68 +250,85 @@ def distance(self, ending_node, **kwargs):

def is_root(self):
"""
Check if has children and not ancestors
Returns True if the current Node instance has children, but no parents
"""
return bool(self.children.exists() and not self.parents.exists())

def is_leaf(self):
"""
Check if has ancestors and not children
Returns True if the current Node instance has parents, but no children
"""
return bool(self.parents.exists() and not self.children.exists())

def is_island(self):
"""
Check if has no ancestors nor children
Returns True if the current Node instance has no parents nor children
"""
return bool(not self.children.exists() and not self.parents.exists())

def is_ancestor_of(self, ending_node, **kwargs):
"""
Provided an ending_node Node instance, returns True if the current Node instance and is an ancestor of the
provided Node instance
"""
try:
return len(self.path_raw(ending_node, **kwargs)) >= 1
except NodeNotReachableException:
return False

def is_descendant_of(self, ending_node, **kwargs):
"""
Provided an ending_node Node instance, returns True if the current Node instance and is a descendant of the
provided Node instance
"""
return (
not self.is_ancestor_of(ending_node, **kwargs)
and len(self.path_raw(ending_node, directional=False, **kwargs)) >= 1
)

def is_sibling_of(self, ending_node):
"""
Provided an ending_node Node instance, returns True if the provided Node instance and the current Node
instance share a parent Node
"""
return ending_node in self.siblings()

def is_partner_of(self, ending_node):
"""
Provided an ending_node Node instance, returns True if the provided Node instance and the current Node
instance share a child Node
"""
return ending_node in self.partners()

def node_depth(self):
# Depth from furthest root
"""Returns an integer representing the depth of this Node instance from furthest root"""
# ToDo: Implement
pass

def connected_graph_raw(self, **kwargs):
# Gets all nodes connected in any way to this node
"""Returns a raw QuerySet of all nodes connected in any way to the current Node instance"""
return ConnectedGraphQuery(instance=self, **kwargs).raw_queryset()

def connected_graph(self, **kwargs):
"""Returns a QuerySet of all nodes connected in any way to the current Node instance"""
pks = [item.pk for item in self.connected_graph_raw(**kwargs)]
return self.ordered_queryset_from_pks(pks)

def descendants_tree(self):
"""
Returns a tree-like structure with descendants
# ToDo: Modify to use CTE
Returns a tree-like structure with descendants for the current Node
"""
# ToDo: Modify to use CTE
tree = {}
for child in self.children.all():
tree[child] = child.descendants_tree()
return tree

def ancestors_tree(self):
"""
Returns a tree-like structure with ancestors
# ToDo: Modify to use CTE
Returns a tree-like structure with ancestors for the current Node
"""
# ToDo: Modify to use CTE
tree = {}
for parent in self.parents.all():
tree[parent] = parent.ancestors_tree()
Expand All @@ -300,9 +347,9 @@ def _roots(self, ancestors_tree):

def roots(self):
"""
Returns roots nodes, if any
# ToDo: Modify to use CTE
Returns a QuerySet of all root nodes, if any, for the current Node
"""
# ToDo: Modify to use CTE
ancestors_tree = self.ancestors_tree()
roots = set()
for ancestor in ancestors_tree:
Expand All @@ -324,9 +371,9 @@ def _leaves(self, descendants_tree):

def leaves(self):
"""
Returns leaves nodes, if any
# ToDo: Modify to use CTE
Returns a QuerySet of all leaf nodes, if any, for the current Node
"""
# ToDo: Modify to use CTE
descendants_tree = self.descendants_tree()
leaves = set()
for descendant in descendants_tree:
Expand All @@ -337,29 +384,27 @@ def leaves(self):

def descendants_edges(self):
"""
Returns a queryset of descendants edges
ToDo: Perform topological sort
Returns a QuerySet of descendant Edge instances for the current Node
"""
# ToDo: Perform topological sort
return edge_model.objects.filter(
parent__in=self.self_and_descendants(),
child__in=self.self_and_descendants(),
)

def ancestors_edges(self):
"""
Returns a queryset of ancestors edges
ToDo: Perform topological sort
Returns a QuerySet of ancestor Edge instances for the current Node
"""
# ToDo: Perform topological sort
return edge_model.objects.filter(
parent__in=self.self_and_ancestors(),
child__in=self.self_and_ancestors(),
)

def clan_edges(self):
"""
Returns a queryset of all edges associated with a given node
Returns a QuerySet of all Edge instances associated with a given node
"""
return self.ancestors_edges() | self.descendants_edges()

Expand All @@ -378,44 +423,46 @@ def duplicate_edge_checker(parent, child):

class EdgeManager(models.Manager):
def from_nodes_queryset(self, nodes_queryset):
"""Provided a queryset of nodes, returns all edges where a parent and child
node are within the queryset of nodes."""
"""
Provided a QuerySet of nodes, returns a QuerySet of all Edge instances where a parent and child Node are within
the QuerySet of nodes
"""
return _ordered_filter(self.model.objects, ["parent", "child"], nodes_queryset)

def descendants(self, node, **kwargs):
"""
Returns a queryset of all edges descended from the given node
Returns a QuerySet of all Edge instances descended from the given Node instance
"""
return _ordered_filter(self.model.objects, "parent", node.self_and_descendants(**kwargs))

def ancestors(self, node, **kwargs):
"""
Returns a queryset of all edges which are ancestors of the given node
Returns a QuerySet of all Edge instances which are ancestors of the given Node instance
"""
return _ordered_filter(self.model.objects, "child", node.ancestors_and_self(**kwargs))

def clan(self, node, **kwargs):
"""
Returns a queryset of all edges for ancestors, self, and descendants
Returns a QuerySet of all Edge instances for ancestors, self, and descendants
"""
return self.from_nodes_queryset(node.clan(**kwargs))

def path(self, start_node, end_node, **kwargs):
"""
Returns a queryset of all edges for the shortest path from start_node to end_node
Returns a QuerySet of all Edge instances for the shortest path from start_node to end_node
"""
return self.from_nodes_queryset(start_node.path(end_node, **kwargs))

def validate_route(self, edges, **kwargs):
"""
Given a list or set of edges, verify that they result in a contiguous route
Given a list or set of Edge instances, verify that they result in a contiguous route
"""
# ToDo: Implement
pass

def sort(self, edges, **kwargs):
"""
Given a list or set of edges, sort them from root-side to leaf-side
Given a list or set of Edge instances, sort them from root-side to leaf-side
"""
# ToDo: Implement
pass
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Contents
:maxdepth: 2

quickstart
methods
transformations


Expand Down
Loading

0 comments on commit 1009d5f

Please sign in to comment.