From 1009d5f1d6db6568de6d4d818a495c42edaa9027 Mon Sep 17 00:00:00 2001 From: Omen Apps Date: Sat, 20 Mar 2021 16:04:08 -0400 Subject: [PATCH] Improved docs to flesh out descriptions of various model/manager methods --- django_postgresql_dag/models.py | 127 +++++++++++------ docs/index.rst | 1 + docs/methods.rst | 234 ++++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+), 40 deletions(-) create mode 100644 docs/methods.rst diff --git a/django_postgresql_dag/models.py b/django_postgresql_dag/models.py index 5c05ec6..635f79b 100644 --- a/django_postgresql_dag/models.py +++ b/django_postgresql_dag/models.py @@ -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 @@ -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) @@ -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) @@ -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: @@ -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): @@ -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)] @@ -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): @@ -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) @@ -220,58 +250,75 @@ 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() @@ -279,9 +326,9 @@ def descendants_tree(self): 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() @@ -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: @@ -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: @@ -337,10 +384,9 @@ 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(), @@ -348,10 +394,9 @@ def descendants_edges(self): 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(), @@ -359,7 +404,7 @@ def ancestors_edges(self): 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() @@ -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 diff --git a/docs/index.rst b/docs/index.rst index e7cd5fb..75e0d65 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,7 @@ Contents :maxdepth: 2 quickstart + methods transformations diff --git a/docs/methods.rst b/docs/methods.rst new file mode 100644 index 0000000..06808c7 --- /dev/null +++ b/docs/methods.rst @@ -0,0 +1,234 @@ +Methods on Node and Edge +======================== + +*Listed below are the methods that are useful for building/manipulating/querying the graph. We ignore here the methods used only for internal functionality.* + +Node +^^^^ + + +Manager Methods +""""""""""""""" + + +**roots(self, node=None)** + +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. + +**leaves(self, node=None)** + +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. + + +Model Methods +""""""""""""" + +Methods used for building/manipulating +************************************** + +**add_child(self, child, \*\*kwargs)** + +Provided with a Node instance, attaches that instance as a child to the current Node instance + +**remove_child(self, child, delete_node=False)** + +Removes the edge connecting this node to the provided child Node instance, and optionally deletes the child node as well + +**add_parent(self, parent, \\*args, \*\*kwargs)** + +Provided with a Node instance, attaches the current instance as a child to the provided Node instance + +**remove_parent(self, parent, delete_node=False)** + +Removes the edge connecting this node to parent, and optionally deletes the parent node as well + + + +Methods used for querying +************************* + +**ancestors(self, \*\*kwargs)** + +Returns a QuerySet of all nodes in connected paths in a rootward direction + +**ancestors_count(self)** + +Returns an integer number representing the total number of ancestor nodes + +**self_and_ancestors(self, \*\*kwargs)** + +Returns a QuerySet of all nodes in connected paths in a rootward direction, prepending with self + +**ancestors_and_self(self, \*\*kwargs)** + +Returns a QuerySet of all nodes in connected paths in a rootward direction, appending with self + +**descendants(self, \*\*kwargs)** + +Returns a QuerySet of all nodes in connected paths in a leafward direction + +**descendants_count(self)** + +Returns an integer number representing the total number of descendant nodes + +**self_and_descendants(self, \*\*kwargs)** + +Returns a QuerySet of all nodes in connected paths in a leafward direction, prepending with self + +**descendants_and_self(self, \*\*kwargs)** + +Returns a QuerySet of all nodes in connected paths in a leafward direction, appending with self + +**clan(self, \*\*kwargs)** + +Returns a QuerySet with all ancestors nodes, self, and all descendant nodes + +**clan_count(self)** + +Returns an integer number representing the total number of clan nodes + +**siblings(self)** + +Returns a QuerySet of all nodes that share a parent with this node, excluding self + +**siblings_count(self)** + +Returns count of all nodes that share a parent with this node + +**siblings_with_self(self)** + +Returns a QuerySet of all nodes that share a parent with this node and self + +**partners(self)** + +Returns a QuerySet of all nodes that share a child with this node, excluding self + +**partners_count(self)** + +Returns count of all nodes that share a child with this node + +**partners_with_self(self)** + +Returns a QuerySet of all nodes that share a child with this node and self + +**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 + +**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. + +**distance(self, ending_node, \*\*kwargs)** + +Returns the shortest hops count to the target node + +**is_root(self)** + +Returns True if the current Node instance has children, but no parents + +**is_leaf(self)** + +Returns True if the current Node instance has parents, but no children + +**is_island(self)** + +Returns True if the current Node instance has no parents nor children + +**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 + +**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 + +**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 + +**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 + +**node_depth(self)** + +Returns an integer representing the depth of this Node instance from furthest root + +*Not yet implemented* + +**connected_graph(self, \*\*kwargs)** + +Returns a QuerySet of all nodes connected in any way to the current Node instance + +**descendants_tree(self)** + +Returns a tree-like structure with descendants for the current Node + +**ancestors_tree(self)** + +Returns a tree-like structure with ancestors for the current Node + +**roots(self)** + +Returns a QuerySet of all root nodes, if any, for the current Node + +**leaves(self)** + +Returns a QuerySet of all leaf nodes, if any, for the current Node + +**descendants_edges(self)** + +Returns a QuerySet of descendant Edge instances for the current Node + +**ancestors_edges(self)** + +Returns a QuerySet of ancestor Edge instances for the current Node + +**clan_edges(self)** + +Returns a QuerySet of all Edge instances associated with a given node + + + + + +Edge +^^^^ + + +Manager Methods +""""""""""""""" + + +**from_nodes_queryset(self, nodes_queryset)** + +Provided a QuerySet of nodes, returns a QuerySet of all Edge instances where a parent and child Node are within the QuerySet of nodes + +**descendants(self, node, \*\*kwargs)** + +Returns a QuerySet of all Edge instances descended from the given Node instance + +**ancestors(self, node, \*\*kwargs)** + +Returns a QuerySet of all Edge instances which are ancestors of the given Node instance + +**clan(self, node, \*\*kwargs)** + +Returns a QuerySet of all Edge instances for ancestors, self, and descendants + +**path(self, start_node, end_node, \*\*kwargs)** + +Returns a QuerySet of all Edge instances for the shortest path from start_node to end_node + +**validate_route(self, edges, \*\*kwargs)** + +Given a list or set of Edge instances, verify that they result in a contiguous route + +*Not yet implemented.* + +**sort(self, edges, \*\*kwargs)** + +Given a list or set of Edge instances, sort them from root-side to leaf-side + +*Not yet implemented.*