Skip to content

Commit c71a95d

Browse files
committed
2 parents 76b6723 + d873a38 commit c71a95d

6 files changed

Lines changed: 462 additions & 71 deletions

File tree

gremlin/draw.py

Lines changed: 134 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,148 @@
33
44
@author: jv
55
'''
6+
from collections.abc import Iterable
7+
from typing import Any, List, Union
8+
69
import graphviz
10+
11+
from gremlin_python.process.anonymous_traversal import GraphTraversalSource
12+
from gremlin_python.process.graph_traversal import GraphTraversal
13+
from gremlin_python.process.traversal import T
14+
from gremlin_python.structure.graph import Vertex, Edge, Path
15+
16+
from aenum import Enum
17+
18+
719
class GremlinDraw:
8-
@classmethod
9-
def show(cls,g,title:str="Gremlin", v_limit:int=10, e_limit:int=10):
10-
G = graphviz.Digraph(title,format="pdf")
20+
@staticmethod
21+
def __draw_vertex(digraph: graphviz.Digraph, g: GraphTraversalSource, vertex: Vertex) -> graphviz.Digraph:
22+
"""
23+
draw a single given vertex
24+
"""
25+
# developer note: see https://github.com/apache/tinkerpop/blob/master/gremlin-python/src/main/python/gremlin_python/structure/graph.py#LL58C23-L58C23
26+
# when gremlin-python 3.7.0 is released, the following code can be improved (get the properties using vertex.properties)
27+
# then, g can also be removed as a parameter
28+
29+
# get the properties of the vertex
30+
kvp_list = list(next(g.V(vertex).element_map()).items())
31+
# non-proerty items are of type aenum
32+
properties = [item for item in kvp_list if not isinstance(item[0], Enum)]
33+
assert len(properties) == len(kvp_list) - 2 # ID and label are not properties
34+
35+
properties_label = "\n".join(f"{key}: {value}" for key, value in properties)
36+
37+
# draw the vertex
38+
digraph.node(
39+
name=str(vertex.id),
40+
label=f"{str(vertex.id)}\n{vertex.label}\n{'─' * 5}\n{properties_label}",
41+
fillcolor = "#ADE1FE",
42+
style = "filled",
43+
fontname = "arial"
44+
)
45+
46+
return digraph
47+
48+
49+
@staticmethod
50+
def __draw_edge(digraph: graphviz.Digraph, g: GraphTraversalSource, edge: Edge) -> graphviz.Digraph:
51+
"""
52+
draw a single given edge
53+
"""
54+
# developer note: see https://github.com/apache/tinkerpop/blob/master/gremlin-python/src/main/python/gremlin_python/structure/graph.py#L66
55+
# when gremlin-python 3.7.0 is released, the following code can be improved (get the properties using edge.properties)
56+
# then, g can also be removed as a parameter
57+
58+
# get the properties of the edge
59+
#kvp_list = list(next(g.E(edge).element_map()).items())
60+
# Workaround, because the above line does not work due to inconsistencies / bugs in the gremlin-python library
61+
kvp_list = [edge_element_map for edge_element_map in g.E().element_map().to_list() if edge_element_map[T.id] == edge.id][0].items()
62+
# non-proerty items are of type aenum
63+
properties = [item for item in kvp_list if not isinstance(item[0], Enum)]
64+
assert len(properties) == len(kvp_list) - 4 # ID, label, in, and out are not properties
65+
66+
properties_label = "\n".join(f"{key}: {value}" for key, value in properties)
67+
68+
# get the image of the edge by id
69+
in_vertex_id = edge.inV.id
70+
out_vertex_id = edge.outV.id
71+
72+
# draw the edge
73+
digraph.edge(
74+
tail_name = str(in_vertex_id),
75+
head_name = str(out_vertex_id),
76+
label = f"{str(edge.id)}\n{edge.label}\n{'─' * 5}\n{properties_label}",
77+
style = "setlinewidth(3)",
78+
fontname = "arial"
79+
)
80+
81+
return digraph
82+
83+
84+
@staticmethod
85+
def show(g: GraphTraversalSource, title:str="Gremlin", v_limit:int=10, e_limit:int=10) -> graphviz.Digraph:
86+
"""
87+
draw the given graph
88+
"""
89+
G: graphviz.Digraph = graphviz.Digraph(title, format="pdf")
1190

1291
# draw vertices
13-
vlist = g.V().toList()
92+
vlist = g.V().to_list()
1493
vlist = vlist[:v_limit]
1594

1695
for v in vlist:
17-
kvp_list = list(g.V(v).elementMap().next().items())
18-
kvp_list.pop(0) # pop ID
19-
kvp_list.pop(0) # pop label
20-
vertex_properties = ""
21-
for key,val in kvp_list:
22-
vertex_properties += "\n " + str(key) + ": " + str(val);
23-
24-
G.node(name=str(v.id), label=str(v.id) + "\n" + v.label + "\n" + '─' * 5 + vertex_properties, fillcolor = "#ADE1FE", style = "filled", fontname = "arial")
25-
96+
G = GremlinDraw.__draw_vertex(G, g, v)
97+
2698
#draw edges
27-
elist = g.E().elementMap().toList()
99+
elist = g.E().to_list()
28100
elist = elist[:e_limit]
29101

30102
for e in elist:
31-
kvp_list = list(e.items())
32-
kvp_list.pop(0) # pop ID
33-
kvp_list.pop(0) # pop label
34-
kvp_list.pop(0) # pop in
35-
kvp_list.pop(0) # pop out
36-
edge_properties = ""
37-
for key,val in kvp_list:
38-
edge_properties += "\n " + str(key) + ": " + str(val);
39-
40-
# list(list(e.values())[2].values())[0]
41-
# str(g.E(e).inV().next().id)
42-
# str(g.E(e).outV().next().id)
43-
# list(e.values())[0]
44-
45-
G.edge(tail_name = str(list(list(e.values())[3].values())[0]), head_name = str(list(list(e.values())[2].values())[0]), style = "setlinewidth(3)", label = str(list(e.values())[0]) + "\n" + str(list(e.values())[1]) + "\n" + '─' * 5 + edge_properties, fontname = "arial")
46-
return G
103+
G = GremlinDraw.__draw_edge(G, g, e)
104+
105+
return G
106+
107+
@staticmethod
108+
def show_graph_traversal(g: GraphTraversalSource, gt: Union[GraphTraversal, Any], title: str="Gremlin") -> graphviz.Digraph:
109+
"""
110+
draw the given graph traversal
111+
"""
112+
# developer note: when moving the minium supported version up to 3.10, the following code can be greatly improved by using match statements
113+
114+
G: graphviz.Digraph = graphviz.Digraph(title, format="pdf")
115+
116+
worklist: List[Any] = gt.to_list() if isinstance(gt, GraphTraversal) else list(gt) if isinstance(gt, Iterable) else [gt]
117+
118+
while len(worklist) > 0:
119+
# move any vertices to the front of the worklist (draw them first)
120+
worklist = [item for item in worklist if not isinstance(item, Vertex)] + [item for item in worklist if isinstance(item, Vertex)]
121+
122+
result = worklist.pop(0)
123+
124+
if isinstance(result, Vertex):
125+
G = GremlinDraw.__draw_vertex(G, g, result)
126+
elif isinstance(result, Edge):
127+
G = GremlinDraw.__draw_edge(G, g, result)
128+
elif isinstance(result, Path):
129+
for item in result.objects:
130+
worklist.append(item)
131+
elif isinstance(result, dict):
132+
if T.id in result:
133+
# check if the id is a vertex or an edge
134+
if g.V(result[T.id]).hasNext():
135+
G = GremlinDraw.__draw_vertex(G, g, next(g.V(result[T.id])))
136+
elif g.E(result[T.id]).hasNext():
137+
G = GremlinDraw.__draw_edge(G, g, g.E(result[T.id]).next())
138+
else:
139+
#raise Exception("id not found")
140+
pass # silent skip
141+
else:
142+
#raise Exception("id not found")
143+
pass # silent skip
144+
else:
145+
#raise Exception(f"unknown type: {type(result)}")
146+
pass # silent skip
147+
148+
return G
149+
150+

gremlin/examples.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from os.path import abspath, dirname
12
import urllib.request
23
import os
34
from pathlib import Path
5+
6+
from gremlin_python.process.anonymous_traversal import GraphTraversalSource
47
from gremlin.remote import RemoteTraversal
58
from dataclasses import dataclass
69

@@ -39,8 +42,8 @@ def remote(self,file_name:str):
3942
return path
4043

4144

42-
@classmethod
43-
def docker(cls)->"Volume":
45+
@staticmethod
46+
def docker()->"Volume":
4447
"""
4548
get the default docker volume mapping
4649
@@ -54,30 +57,46 @@ def docker(cls)->"Volume":
5457
volume=Volume(local_path=local_path,remote_path=remote_path)
5558
return volume
5659

60+
@staticmethod
61+
def local() -> "Volume":
62+
"""
63+
get the default local volume mapping
64+
65+
Returns:
66+
Volume: the local_path/remote_path mapping
67+
"""
68+
home = str(Path.home())
69+
local_path=f"{home}/.gremlin-examples"
70+
os.makedirs(local_path, exist_ok=True)
71+
remote_path=str(abspath(f"{dirname(abspath(__file__))}/data"))
72+
volume=Volume(local_path=local_path,remote_path=remote_path)
73+
return volume
74+
5775
@dataclass
5876
class Example:
5977
name:str
6078
url:str
6179

62-
def __post_init__(self):
63-
"""
64-
"""
65-
66-
def load(self,g,volume:Volume,force:bool=False,debug:bool=False):
80+
def load(
81+
self,
82+
g: GraphTraversalSource,
83+
volume: Volume,
84+
force: bool = False,
85+
debug: bool = False,
86+
) -> None:
6787
"""
6888
download graph from remote_path to local_path depending on force flag
6989
and load graph into g
7090
7191
Args:
72-
g(GraphTraversal): the target
92+
g(GraphTraversalSource): the target graph (inout)
7393
volume:Volume
7494
force(bool): if True download even if local copy already exists
7595
debug(bool): if True show debugging information
7696
"""
7797
self.download(volume.local_path, force=force,debug=debug)
7898
graph_xml=f"{volume.remote_path}/{self.name}.xml"
7999
RemoteTraversal.load(g, graph_xml)
80-
pass
81100

82101
def download(self,path,force:bool=False,debug:bool=False)->str:
83102
"""
@@ -113,7 +132,7 @@ class Examples:
113132
Examples
114133
"""
115134

116-
def __init__(self,volume,debug:bool=False):
135+
def __init__(self,volume:Volume,debug:bool=False):
117136
"""
118137
Constructor
119138
@@ -129,19 +148,23 @@ def __init__(self,volume,debug:bool=False):
129148
Example(name="tinkerpop-modern",url="https://raw.githubusercontent.com/apache/tinkerpop/master/data/tinkerpop-modern.xml"),
130149
Example(name="grateful-dead",url="https://raw.githubusercontent.com/apache/tinkerpop/master/data/grateful-dead.xml"),
131150
Example(name="air-routes-small",url="https://raw.githubusercontent.com/krlawrence/graph/master/sample-data/air-routes-small.graphml"),
132-
Example(name="air-routes-latest",url="https://raw.githubusercontent.com/krlawrence/graph/master/sample-data/air-routes-latest.graphml") ]:
151+
Example(name="air-routes-latest",url="https://raw.githubusercontent.com/krlawrence/graph/master/sample-data/air-routes-latest.graphml")
152+
]:
133153
self.examples_by_name[example.name]=example
134154

135-
def load_by_name(self,g,name:str):
155+
def load_by_name(self, g: GraphTraversalSource, name: str) -> None:
136156
"""
137157
load an example by name to the given graph
138158
139159
Args:
140-
g: the target graph
160+
g(GraphTraversalSource): the target graph (inout)
141161
name(str): the name of the example
162+
163+
Raises:
164+
Exception: if the example does not exist
142165
"""
143166
if name in self.examples_by_name:
144167
example=self.examples_by_name[name]
145168
example.load(g,self.volume,debug=self.debug)
146169
else:
147-
raise Exception(f"invalid example {name}")
170+
raise Exception(f"invalid example {name}")

0 commit comments

Comments
 (0)