Skip to content

pyMogwai API Documentation

core

hd_index

Created on 2024-11-07

@author: wf

base on A. Harth and S. Decker, "Optimized index structures for querying RDF from the Web," Third Latin American Web Congress (LA-WEB'2005), Buenos Aires, Argentina, 2005, pp. 10 pp.-, doi: 10.1109/LAWEB.2005.25. keywords: {Resource description framework;Data models;Semantic Web;Indexes;Java;Vocabulary;Database systems;Memory;Indexing;Information retrieval},

Index

A Single index in the SPOG matrix as explained in identified by from/to positions

Source code in mogwai/core/hd_index.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
class Index:
    """A Single index in the SPOG matrix as explained in
    identified by from/to positions"""

    def __init__(self, from_pos: str, to_pos: str):
        """
        Args:
            from_pos: First position (S,P,O,G)
            to_pos: Second position (S,P,O,G)
        """
        self.from_pos = from_pos
        self.to_pos = to_pos
        self.lookup = {}

    @property
    def name(self) -> str:
        """Full quad index name based on Harth/Decker SPOG ordering"""
        index_name = f"{self.from_pos}{self.to_pos}"
        return index_name

    def add_quad(self, quad: Quad) -> None:
        """Add a quad to this index's lookup using quad positions"""
        from_val = getattr(quad, self.from_pos.lower())
        to_val = getattr(quad, self.to_pos.lower())

        if from_val not in self.lookup:
            self.lookup[from_val] = set()
        self.lookup[from_val].add(to_val)
name: str property

Full quad index name based on Harth/Decker SPOG ordering

__init__(from_pos, to_pos)

Parameters:

Name Type Description Default
from_pos str

First position (S,P,O,G)

required
to_pos str

Second position (S,P,O,G)

required
Source code in mogwai/core/hd_index.py
78
79
80
81
82
83
84
85
86
def __init__(self, from_pos: str, to_pos: str):
    """
    Args:
        from_pos: First position (S,P,O,G)
        to_pos: Second position (S,P,O,G)
    """
    self.from_pos = from_pos
    self.to_pos = to_pos
    self.lookup = {}
add_quad(quad)

Add a quad to this index's lookup using quad positions

Source code in mogwai/core/hd_index.py
 94
 95
 96
 97
 98
 99
100
101
def add_quad(self, quad: Quad) -> None:
    """Add a quad to this index's lookup using quad positions"""
    from_val = getattr(quad, self.from_pos.lower())
    to_val = getattr(quad, self.to_pos.lower())

    if from_val not in self.lookup:
        self.lookup[from_val] = set()
    self.lookup[from_val].add(to_val)

IndexConfig dataclass

Configuration of which SPOG indices to use

Source code in mogwai/core/hd_index.py
16
17
18
19
20
@dataclass
class IndexConfig:
    """Configuration of which SPOG indices to use"""

    active_indices: Set[str]

IndexConfigs

Bases: Enum

Standard index configurations

Source code in mogwai/core/hd_index.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class IndexConfigs(Enum):
    """Standard index configurations"""

    OFF = "off"  # Use no indices
    ALL = "all"  # Use all 16 indices
    MINIMAL = "minimal"  # Use minimal required set

    def get_config(self) -> IndexConfig:
        """Get the index configuration for this enum value"""
        if self == IndexConfigs.OFF:
            return IndexConfig(set())

        if self == IndexConfigs.ALL:
            positions = ["S", "P", "O", "G"]
            indices = {
                f"{from_pos}{to_pos}"
                for from_pos in positions
                for to_pos in positions
                if from_pos != to_pos
            }
            return IndexConfig(indices)

        if self == IndexConfigs.MINIMAL:
            return IndexConfig(
                {
                    # Core indices for basic node relationships
                    "PS",  # Predicate -> Subject: links predicates to subjects (e.g., labels or properties to nodes)
                    "PO",  # Predicate -> Object: maps predicates to values (e.g., property values)
                    "SO",  # Subject -> Object: links source nodes to target nodes in relationships
                    "OS",  # Object -> Subject: reverse lookup for values back to nodes
                    # Graph-based indices for context-specific associations
                    "PG",  # Predicate -> Graph: associates predicates with graph contexts
                    "SG",  # Subject -> Graph: associates subjects with graph contexts
                    "GO",  # Graph -> Object: maps graph contexts to objects for grouped retrieval
                    "GP",  # Graph -> Predicate: links graph contexts to predicates
                }
            )

        raise ValueError(f"Unknown index configuration: {self}")
get_config()

Get the index configuration for this enum value

Source code in mogwai/core/hd_index.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def get_config(self) -> IndexConfig:
    """Get the index configuration for this enum value"""
    if self == IndexConfigs.OFF:
        return IndexConfig(set())

    if self == IndexConfigs.ALL:
        positions = ["S", "P", "O", "G"]
        indices = {
            f"{from_pos}{to_pos}"
            for from_pos in positions
            for to_pos in positions
            if from_pos != to_pos
        }
        return IndexConfig(indices)

    if self == IndexConfigs.MINIMAL:
        return IndexConfig(
            {
                # Core indices for basic node relationships
                "PS",  # Predicate -> Subject: links predicates to subjects (e.g., labels or properties to nodes)
                "PO",  # Predicate -> Object: maps predicates to values (e.g., property values)
                "SO",  # Subject -> Object: links source nodes to target nodes in relationships
                "OS",  # Object -> Subject: reverse lookup for values back to nodes
                # Graph-based indices for context-specific associations
                "PG",  # Predicate -> Graph: associates predicates with graph contexts
                "SG",  # Subject -> Graph: associates subjects with graph contexts
                "GO",  # Graph -> Object: maps graph contexts to objects for grouped retrieval
                "GP",  # Graph -> Predicate: links graph contexts to predicates
            }
        )

    raise ValueError(f"Unknown index configuration: {self}")

Quad dataclass

A quad of hashable values (Subject-Predicate-Object-Graph)

Source code in mogwai/core/hd_index.py
64
65
66
67
68
69
70
71
@dataclass(frozen=True)
class Quad:
    """A quad of hashable values (Subject-Predicate-Object-Graph)"""

    s: Hashable  # Subject
    p: Hashable  # Predicate
    o: Hashable  # Object
    g: Hashable | None = None  # Graph context

SPOGIndex

all 16 possible indices based on SPOG matrix

see http://harth.org/andreas/ YARS and the paper

Source code in mogwai/core/hd_index.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
class SPOGIndex:
    """
    all 16 possible indices based on SPOG matrix

    see http://harth.org/andreas/ YARS and the paper
    """

    def __init__(self, config: IndexConfig):
        self.config = config
        positions = ["S", "P", "O", "G"]
        self.indices = {}
        self.indices = {}
        for from_pos in positions:
            for to_pos in positions:
                if from_pos != to_pos:
                    index = Index(from_pos, to_pos)
                    self.indices[index.name] = index

    def get_lookup(self, from_pos: str, to_pos: str) -> dict | None:
        """
        Get lookup dict for from->to positions if active

        Args:
            from_pos: From position (S,P,O,G)
            to_pos: To position (S,P,O,G)
        Returns:
            Lookup dict if index active in current config, None otherwise
        """
        index_name = f"{from_pos}{to_pos}"
        if index_name in self.config.active_indices:
            return self.indices[index_name].lookup
        return None

    def add_quad(self, quad: Quad) -> None:
        """Add quad only to configured active indices"""
        for index_name in self.config.active_indices:
            self.indices[index_name].add_quad(quad)
add_quad(quad)

Add quad only to configured active indices

Source code in mogwai/core/hd_index.py
137
138
139
140
def add_quad(self, quad: Quad) -> None:
    """Add quad only to configured active indices"""
    for index_name in self.config.active_indices:
        self.indices[index_name].add_quad(quad)
get_lookup(from_pos, to_pos)

Get lookup dict for from->to positions if active

Parameters:

Name Type Description Default
from_pos str

From position (S,P,O,G)

required
to_pos str

To position (S,P,O,G)

required

Returns: Lookup dict if index active in current config, None otherwise

Source code in mogwai/core/hd_index.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def get_lookup(self, from_pos: str, to_pos: str) -> dict | None:
    """
    Get lookup dict for from->to positions if active

    Args:
        from_pos: From position (S,P,O,G)
        to_pos: To position (S,P,O,G)
    Returns:
        Lookup dict if index active in current config, None otherwise
    """
    index_name = f"{from_pos}{to_pos}"
    if index_name in self.config.active_indices:
        return self.indices[index_name].lookup
    return None

mogwaigraph

MogwaiGraph

Bases: DiGraph

networkx based directed graph see https://networkx.org/documentation/stable/reference/classes/digraph.html

Source code in mogwai/core/mogwaigraph.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
class MogwaiGraph(networkx.DiGraph):
    """
    networkx based directed graph
    see https://networkx.org/documentation/stable/reference/classes/digraph.html
    """

    def __init__(
        self, incoming_graph_data=None, config: MogwaiGraphConfig = None, **attr
    ):
        """Initialize a MogwaiGraph with optional data and configuration.

        Args:
            incoming_graph_data: Graph data in NetworkX compatible format
            config (MogwaiGraphConfig): Configuration for field names and defaults
            **attr: Graph attributes as key=value pairs
        """
        super().__init__(incoming_graph_data, **attr)
        self.counter = 0
        self.config = config or MogwaiGraphConfig()
        # Initialize SPOG index based on config
        index_config = IndexConfigs[self.config.index_config.upper()].get_config()
        self.spog_index = SPOGIndex(index_config)

    def get_next_node_id(self):
        """
        get the next node_id
        """
        node_id = self.counter
        self.counter += 1
        return node_id

    def add_to_index(
        self,
        element_type: str,
        subject_id: Hashable,
        label: set,
        name: str,
        properties: dict,
    ):
        """
        Add labels, name, and properties to the SPOG index for a
        given subject and element_type

        Args:
            element_type: (str): node or edge
            subject_id (Hashable): The ID of the subject (node or edge).
            label (set): Set of labels for the subject.
            name (str): Name of the subject.
            properties (dict): Dictionary of additional properties to index.
        """
        # only index if the config calls for it
        if self.config.index_config == "off":
            return
        # Add quads for each label with g="label"
        for lbl in label:
            label_quad = Quad(s=subject_id, p="label", o=lbl, g=f"{element_type}-label")
            self.spog_index.add_quad(label_quad)

        # Add quad for name with g="name"
        name_quad = Quad(s=subject_id, p="name", o=name, g=f"{element_type}-name")
        self.spog_index.add_quad(name_quad)

        # Add quads for each property with g="property"
        for prop_name, prop_value in properties.items():
            if not isinstance(prop_value, Hashable):
                prop_value = str(prop_value)  # Ensure property value is hashable
            property_quad = Quad(
                s=subject_id, p=prop_name, o=prop_value, g=f"{element_type}-property"
            )
            self.spog_index.add_quad(property_quad)

    def add_labeled_node(
        self,
        label: set | str,
        name: str,
        properties: dict = None,
        node_id: Optional[str] = None,
        **kwargs,
    ) -> Any:
        """
        Add a labeled node to the graph.

        we can only insert a node by hashable value and as names and ids
        might occur multiple times we use incremented node ids if no node_id is provided

        Args:
            label (Union[set, str]): The label or set of labels for the node.
            name (str): The name of the node.
            properties (dict, optional): Additional properties for the node. Defaults to None.
            node_id (Optional[int], optional): The ID for the node. If not provided, a new ID will be generated. Defaults to None.
            kwargs (): further property values
        Returns:
            Any: The ID of the newly added node - will be an integer if node_id was kept as default None

        Raises:
            MogwaiGraphError: If a node with the provided ID already exists in the graph.
        """
        label = (
            label
            if isinstance(label, set)
            else (set(label) if isinstance(label, (list, tuple)) else {label})
        )
        if node_id is None:
            node_id = self.get_next_node_id()
        properties = properties or {}
        properties.update(kwargs)
        if self.config.name_field in properties:
            raise MogwaiGraphError(
                f"The '{self.config.name_field}' property is reserved for the node name."
            )
        elif self.config.label_field in properties:
            raise MogwaiGraphError(
                f"The '{self.config.label_field}' property is reserved for the node labels."
            )
        node_props = {
            self.config.name_field: name,
            self.config.label_field: label,
            **properties,
        }
        super().add_node(node_id, **node_props)
        # Use add_to_index to add label, name, and properties as quads
        self.add_to_index("node", node_id, label, name, properties)
        return node_id

    def add_labeled_edge(
        self, srcId: int, destId: int, edgeLabel: str, properties: dict = None, **kwargs
    ):
        """
        add a labeled edge
        """
        if self.has_node(srcId) and self.has_node(destId):
            properties = properties or {}
            properties.update(kwargs)
            if self.config.edge_label_field in properties:
                raise MogwaiGraphError(
                    f"The '{self.config.edge_label_field}' property is reserved for the edge label."
                )
            elif self.config.label_field in properties:
                raise MogwaiGraphError(
                    f"The '{self.config.label_field}' property is reserved for the node labels."
                )
            edge_props = {self.config.edge_label_field: edgeLabel, **properties}
            super().add_edge(srcId, destId, **edge_props)
            # Add a quad specifically for the edge connection
            edge_quad = Quad(s=srcId, p=edgeLabel, o=destId, g="edge-link")
            self.spog_index.add_quad(edge_quad)

            # Use add_to_index to add label, name, and properties as quads
            self.add_to_index("edge", srcId, {edgeLabel}, edgeLabel, properties)
        else:
            raise MogwaiGraphError(
                f"Node with id {srcId if srcId<0 else destId} is not in the graph."
            )

    def add_node(self, *args, **kwargs):
        """Add a node with default or explicit labels"""
        if len(args) > 0:
            node_id = args[0]
        else:
            node_id = self.get_next_node_id()

        label = kwargs.pop("labels", {self.config.default_node_label})
        name = kwargs.pop("name", str(node_id))
        return self.add_labeled_node(label, name, properties=kwargs, node_id=node_id)

    def add_edge(self, *args, **kwargs):
        """Add an edge with default or explicit label"""
        if len(args) < 2:
            raise MogwaiGraphError("add_edge() requires source and target node ids")
        src, dst = args[0:2]
        label = kwargs.pop(self.config.edge_label_field, self.config.default_edge_label)
        return self.add_labeled_edge(src, dst, label, properties=kwargs)

    def _get_nodes_set(self, label: set, name: str):
        n_none = name is None
        if n_none:
            return [n for n in self.nodes(date=True) if label.issubset(n[1]["labels"])]
        if not n_none:
            return [
                n
                for n in self.nodes(data=True)
                if label.issubset(n[1]["labels"]) and n[1]["name"] == name
            ]
        return self.nodes

    def get_nodes(self, label: str | set, name: str):
        if type(label) is set:  # check if we are looking for multiple labels
            if len(label) == 0:
                label = None
            else:
                return self._get_nodes_set(label, name)

        l_none, n_none = label is None, name is None
        if not l_none and not n_none:
            return [
                n
                for n in self.nodes(data=True)
                if label in n[1]["labels"] and n[1]["name"] == name
            ]
        if l_none and not n_none:
            return [n for n in self.nodes(data=True) if n[1]["name"] == name]
        if not l_none and n_none:
            return [n for n in self.nodes(date=True) if label in n[1]["labels"]]
        return self.nodes

    def merge_subgraph(
        self, other: "MogwaiGraph", srcId: int, targetId: int, edgeLabel: str
    ):
        mapping = {k: self.get_next_node_id() for k in other.nodes}
        relabeled = networkx.relabel_nodes(other, mapping, copy=True)
        self.add_nodes_from(relabeled.nodes(data=True))
        self.add_edges_from(relabeled.edges(data=True))
        self.add_labeled_edge(
            srcId=srcId, destId=mapping[targetId], edgeLabel=edgeLabel
        )

    def draw(self, outputfile, title: str = "MogwaiGraph", **kwargs):
        """
        Draw the graph using graphviz
        Parameters
        ----------
        outputfile : str
            the file to save the graph to
        title : str, default 'MogwaiGraph'
            the title of the graph
        kwargs : dict
            additional parameters used to configure the drawing style.
            For more details see `MogwaiGraphDrawer`
        """
        MogwaiGraphDrawer(self, title=title, **kwargs).draw(outputfile)

    @classmethod
    def modern(cls, index_config="off") -> "MogwaiGraph":
        """
        create the modern graph
        see https://tinkerpop.apache.org/docs/current/tutorials/getting-started/
        """
        config = MogwaiGraphConfig
        config.index_config = index_config
        g = MogwaiGraph(config=config)
        marko = g.add_labeled_node("Person", name="marko", age=29)
        vadas = g.add_labeled_node("Person", name="vadas", age=27)
        lop = g.add_labeled_node("Software", name="lop", lang="java")
        josh = g.add_labeled_node("Person", name="josh", age=32)
        ripple = g.add_labeled_node("Software", name="ripple", lang="java")
        peter = g.add_labeled_node("Person", name="peter", age=35)

        g.add_labeled_edge(marko, vadas, "knows", weight=0.5)
        g.add_labeled_edge(marko, josh, "knows", weight=1.0)
        g.add_labeled_edge(marko, lop, "created", weight=0.4)
        g.add_labeled_edge(josh, ripple, "created", weight=1.0)
        g.add_labeled_edge(josh, lop, "created", weight=0.4)
        g.add_labeled_edge(peter, lop, "created", weight=0.2)
        return g

    @classmethod
    def crew(cls) -> "MogwaiGraph":
        """
        create the TheCrew example graph
        see TinkerFactory.createTheCrew() in https://tinkerpop.apache.org/docs/current/reference/
        """
        g = MogwaiGraph()

        def t(startTime: int, endTime: int = None):
            d = dict()
            d["startTime"] = startTime
            if endTime is not None:
                d["endTime"] = endTime
            return d

        marko = g.add_labeled_node(
            "Person",
            name="marko",
            location={
                "san diego": t(1997, 2001),
                "santa cruz": t(2001, 2004),
                "brussels": t(2004, 2005),
                "santa fe": t(2005),
            },
        )
        stephen = g.add_labeled_node(
            "Person",
            name="stephen",
            location={
                "centreville": t(1990, 2000),
                "dulles": t(2000, 2006),
                "purcellvilee": t(2006),
            },
        )
        matthias = g.add_labeled_node(
            "Person",
            name="matthias",
            location={
                "bremen": t(2004, 2007),
                "baltimore": t(2007, 2011),
                "oakland": t(2011, 2014),
                "seattle": t(2014),
            },
        )
        daniel = g.add_labeled_node(
            "Person",
            name="daniel",
            location={
                "spremberg": t(1982, 2005),
                "kaiserslautern": t(2005, 2009),
                "aachen": t(2009),
            },
        )
        gremlin = g.add_labeled_node("Software", name="gremlin")
        tinkergraph = g.add_labeled_node("Software", name="tinkergraph")

        g.add_labeled_edge(marko, gremlin, "uses", skill=4)
        g.add_labeled_edge(stephen, gremlin, "uses", skill=5)
        g.add_labeled_edge(matthias, gremlin, "uses", skill=3)
        g.add_labeled_edge(daniel, gremlin, "uses", skill=5)
        g.add_labeled_edge(marko, tinkergraph, "uses", skill=5)
        g.add_labeled_edge(stephen, tinkergraph, "uses", skill=4)
        g.add_labeled_edge(matthias, tinkergraph, "uses", skill=3)
        g.add_labeled_edge(daniel, tinkergraph, "uses", skill=3)
        g.add_labeled_edge(gremlin, tinkergraph, "traverses")
        g.add_labeled_edge(marko, tinkergraph, "develops", since=2010)
        g.add_labeled_edge(stephen, tinkergraph, "develops", since=2011)
        g.add_labeled_edge(marko, gremlin, "develops", since=2009)
        g.add_labeled_edge(stephen, gremlin, "develops", since=2010)
        g.add_labeled_edge(matthias, gremlin, "develops", since=2012)
        return g
__init__(incoming_graph_data=None, config=None, **attr)

Initialize a MogwaiGraph with optional data and configuration.

Parameters:

Name Type Description Default
incoming_graph_data

Graph data in NetworkX compatible format

None
config MogwaiGraphConfig

Configuration for field names and defaults

None
**attr

Graph attributes as key=value pairs

{}
Source code in mogwai/core/mogwaigraph.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def __init__(
    self, incoming_graph_data=None, config: MogwaiGraphConfig = None, **attr
):
    """Initialize a MogwaiGraph with optional data and configuration.

    Args:
        incoming_graph_data: Graph data in NetworkX compatible format
        config (MogwaiGraphConfig): Configuration for field names and defaults
        **attr: Graph attributes as key=value pairs
    """
    super().__init__(incoming_graph_data, **attr)
    self.counter = 0
    self.config = config or MogwaiGraphConfig()
    # Initialize SPOG index based on config
    index_config = IndexConfigs[self.config.index_config.upper()].get_config()
    self.spog_index = SPOGIndex(index_config)
add_edge(*args, **kwargs)

Add an edge with default or explicit label

Source code in mogwai/core/mogwaigraph.py
190
191
192
193
194
195
196
def add_edge(self, *args, **kwargs):
    """Add an edge with default or explicit label"""
    if len(args) < 2:
        raise MogwaiGraphError("add_edge() requires source and target node ids")
    src, dst = args[0:2]
    label = kwargs.pop(self.config.edge_label_field, self.config.default_edge_label)
    return self.add_labeled_edge(src, dst, label, properties=kwargs)
add_labeled_edge(srcId, destId, edgeLabel, properties=None, **kwargs)

add a labeled edge

Source code in mogwai/core/mogwaigraph.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def add_labeled_edge(
    self, srcId: int, destId: int, edgeLabel: str, properties: dict = None, **kwargs
):
    """
    add a labeled edge
    """
    if self.has_node(srcId) and self.has_node(destId):
        properties = properties or {}
        properties.update(kwargs)
        if self.config.edge_label_field in properties:
            raise MogwaiGraphError(
                f"The '{self.config.edge_label_field}' property is reserved for the edge label."
            )
        elif self.config.label_field in properties:
            raise MogwaiGraphError(
                f"The '{self.config.label_field}' property is reserved for the node labels."
            )
        edge_props = {self.config.edge_label_field: edgeLabel, **properties}
        super().add_edge(srcId, destId, **edge_props)
        # Add a quad specifically for the edge connection
        edge_quad = Quad(s=srcId, p=edgeLabel, o=destId, g="edge-link")
        self.spog_index.add_quad(edge_quad)

        # Use add_to_index to add label, name, and properties as quads
        self.add_to_index("edge", srcId, {edgeLabel}, edgeLabel, properties)
    else:
        raise MogwaiGraphError(
            f"Node with id {srcId if srcId<0 else destId} is not in the graph."
        )
add_labeled_node(label, name, properties=None, node_id=None, **kwargs)

Add a labeled node to the graph.

we can only insert a node by hashable value and as names and ids might occur multiple times we use incremented node ids if no node_id is provided

Parameters:

Name Type Description Default
label Union[set, str]

The label or set of labels for the node.

required
name str

The name of the node.

required
properties dict

Additional properties for the node. Defaults to None.

None
node_id Optional[int]

The ID for the node. If not provided, a new ID will be generated. Defaults to None.

None
kwargs

further property values

{}

Returns: Any: The ID of the newly added node - will be an integer if node_id was kept as default None

Raises:

Type Description
MogwaiGraphError

If a node with the provided ID already exists in the graph.

Source code in mogwai/core/mogwaigraph.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def add_labeled_node(
    self,
    label: set | str,
    name: str,
    properties: dict = None,
    node_id: Optional[str] = None,
    **kwargs,
) -> Any:
    """
    Add a labeled node to the graph.

    we can only insert a node by hashable value and as names and ids
    might occur multiple times we use incremented node ids if no node_id is provided

    Args:
        label (Union[set, str]): The label or set of labels for the node.
        name (str): The name of the node.
        properties (dict, optional): Additional properties for the node. Defaults to None.
        node_id (Optional[int], optional): The ID for the node. If not provided, a new ID will be generated. Defaults to None.
        kwargs (): further property values
    Returns:
        Any: The ID of the newly added node - will be an integer if node_id was kept as default None

    Raises:
        MogwaiGraphError: If a node with the provided ID already exists in the graph.
    """
    label = (
        label
        if isinstance(label, set)
        else (set(label) if isinstance(label, (list, tuple)) else {label})
    )
    if node_id is None:
        node_id = self.get_next_node_id()
    properties = properties or {}
    properties.update(kwargs)
    if self.config.name_field in properties:
        raise MogwaiGraphError(
            f"The '{self.config.name_field}' property is reserved for the node name."
        )
    elif self.config.label_field in properties:
        raise MogwaiGraphError(
            f"The '{self.config.label_field}' property is reserved for the node labels."
        )
    node_props = {
        self.config.name_field: name,
        self.config.label_field: label,
        **properties,
    }
    super().add_node(node_id, **node_props)
    # Use add_to_index to add label, name, and properties as quads
    self.add_to_index("node", node_id, label, name, properties)
    return node_id
add_node(*args, **kwargs)

Add a node with default or explicit labels

Source code in mogwai/core/mogwaigraph.py
179
180
181
182
183
184
185
186
187
188
def add_node(self, *args, **kwargs):
    """Add a node with default or explicit labels"""
    if len(args) > 0:
        node_id = args[0]
    else:
        node_id = self.get_next_node_id()

    label = kwargs.pop("labels", {self.config.default_node_label})
    name = kwargs.pop("name", str(node_id))
    return self.add_labeled_node(label, name, properties=kwargs, node_id=node_id)
add_to_index(element_type, subject_id, label, name, properties)

Add labels, name, and properties to the SPOG index for a given subject and element_type

Parameters:

Name Type Description Default
element_type str

(str): node or edge

required
subject_id Hashable

The ID of the subject (node or edge).

required
label set

Set of labels for the subject.

required
name str

Name of the subject.

required
properties dict

Dictionary of additional properties to index.

required
Source code in mogwai/core/mogwaigraph.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def add_to_index(
    self,
    element_type: str,
    subject_id: Hashable,
    label: set,
    name: str,
    properties: dict,
):
    """
    Add labels, name, and properties to the SPOG index for a
    given subject and element_type

    Args:
        element_type: (str): node or edge
        subject_id (Hashable): The ID of the subject (node or edge).
        label (set): Set of labels for the subject.
        name (str): Name of the subject.
        properties (dict): Dictionary of additional properties to index.
    """
    # only index if the config calls for it
    if self.config.index_config == "off":
        return
    # Add quads for each label with g="label"
    for lbl in label:
        label_quad = Quad(s=subject_id, p="label", o=lbl, g=f"{element_type}-label")
        self.spog_index.add_quad(label_quad)

    # Add quad for name with g="name"
    name_quad = Quad(s=subject_id, p="name", o=name, g=f"{element_type}-name")
    self.spog_index.add_quad(name_quad)

    # Add quads for each property with g="property"
    for prop_name, prop_value in properties.items():
        if not isinstance(prop_value, Hashable):
            prop_value = str(prop_value)  # Ensure property value is hashable
        property_quad = Quad(
            s=subject_id, p=prop_name, o=prop_value, g=f"{element_type}-property"
        )
        self.spog_index.add_quad(property_quad)
crew() classmethod

create the TheCrew example graph see TinkerFactory.createTheCrew() in https://tinkerpop.apache.org/docs/current/reference/

Source code in mogwai/core/mogwaigraph.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
@classmethod
def crew(cls) -> "MogwaiGraph":
    """
    create the TheCrew example graph
    see TinkerFactory.createTheCrew() in https://tinkerpop.apache.org/docs/current/reference/
    """
    g = MogwaiGraph()

    def t(startTime: int, endTime: int = None):
        d = dict()
        d["startTime"] = startTime
        if endTime is not None:
            d["endTime"] = endTime
        return d

    marko = g.add_labeled_node(
        "Person",
        name="marko",
        location={
            "san diego": t(1997, 2001),
            "santa cruz": t(2001, 2004),
            "brussels": t(2004, 2005),
            "santa fe": t(2005),
        },
    )
    stephen = g.add_labeled_node(
        "Person",
        name="stephen",
        location={
            "centreville": t(1990, 2000),
            "dulles": t(2000, 2006),
            "purcellvilee": t(2006),
        },
    )
    matthias = g.add_labeled_node(
        "Person",
        name="matthias",
        location={
            "bremen": t(2004, 2007),
            "baltimore": t(2007, 2011),
            "oakland": t(2011, 2014),
            "seattle": t(2014),
        },
    )
    daniel = g.add_labeled_node(
        "Person",
        name="daniel",
        location={
            "spremberg": t(1982, 2005),
            "kaiserslautern": t(2005, 2009),
            "aachen": t(2009),
        },
    )
    gremlin = g.add_labeled_node("Software", name="gremlin")
    tinkergraph = g.add_labeled_node("Software", name="tinkergraph")

    g.add_labeled_edge(marko, gremlin, "uses", skill=4)
    g.add_labeled_edge(stephen, gremlin, "uses", skill=5)
    g.add_labeled_edge(matthias, gremlin, "uses", skill=3)
    g.add_labeled_edge(daniel, gremlin, "uses", skill=5)
    g.add_labeled_edge(marko, tinkergraph, "uses", skill=5)
    g.add_labeled_edge(stephen, tinkergraph, "uses", skill=4)
    g.add_labeled_edge(matthias, tinkergraph, "uses", skill=3)
    g.add_labeled_edge(daniel, tinkergraph, "uses", skill=3)
    g.add_labeled_edge(gremlin, tinkergraph, "traverses")
    g.add_labeled_edge(marko, tinkergraph, "develops", since=2010)
    g.add_labeled_edge(stephen, tinkergraph, "develops", since=2011)
    g.add_labeled_edge(marko, gremlin, "develops", since=2009)
    g.add_labeled_edge(stephen, gremlin, "develops", since=2010)
    g.add_labeled_edge(matthias, gremlin, "develops", since=2012)
    return g
draw(outputfile, title='MogwaiGraph', **kwargs)

Draw the graph using graphviz Parameters


outputfile : str the file to save the graph to title : str, default 'MogwaiGraph' the title of the graph kwargs : dict additional parameters used to configure the drawing style. For more details see MogwaiGraphDrawer

Source code in mogwai/core/mogwaigraph.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
def draw(self, outputfile, title: str = "MogwaiGraph", **kwargs):
    """
    Draw the graph using graphviz
    Parameters
    ----------
    outputfile : str
        the file to save the graph to
    title : str, default 'MogwaiGraph'
        the title of the graph
    kwargs : dict
        additional parameters used to configure the drawing style.
        For more details see `MogwaiGraphDrawer`
    """
    MogwaiGraphDrawer(self, title=title, **kwargs).draw(outputfile)
get_next_node_id()

get the next node_id

Source code in mogwai/core/mogwaigraph.py
48
49
50
51
52
53
54
def get_next_node_id(self):
    """
    get the next node_id
    """
    node_id = self.counter
    self.counter += 1
    return node_id
modern(index_config='off') classmethod

create the modern graph see https://tinkerpop.apache.org/docs/current/tutorials/getting-started/

Source code in mogwai/core/mogwaigraph.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
@classmethod
def modern(cls, index_config="off") -> "MogwaiGraph":
    """
    create the modern graph
    see https://tinkerpop.apache.org/docs/current/tutorials/getting-started/
    """
    config = MogwaiGraphConfig
    config.index_config = index_config
    g = MogwaiGraph(config=config)
    marko = g.add_labeled_node("Person", name="marko", age=29)
    vadas = g.add_labeled_node("Person", name="vadas", age=27)
    lop = g.add_labeled_node("Software", name="lop", lang="java")
    josh = g.add_labeled_node("Person", name="josh", age=32)
    ripple = g.add_labeled_node("Software", name="ripple", lang="java")
    peter = g.add_labeled_node("Person", name="peter", age=35)

    g.add_labeled_edge(marko, vadas, "knows", weight=0.5)
    g.add_labeled_edge(marko, josh, "knows", weight=1.0)
    g.add_labeled_edge(marko, lop, "created", weight=0.4)
    g.add_labeled_edge(josh, ripple, "created", weight=1.0)
    g.add_labeled_edge(josh, lop, "created", weight=0.4)
    g.add_labeled_edge(peter, lop, "created", weight=0.2)
    return g

MogwaiGraphConfig dataclass

configuration of a MogwaiGraph

Source code in mogwai/core/mogwaigraph.py
11
12
13
14
15
16
17
18
19
20
21
22
@dataclass
class MogwaiGraphConfig:
    """
    configuration of a MogwaiGraph
    """

    name_field: str = "name"
    label_field: str = "labels"
    edge_label_field: str = "labels"
    default_node_label: str = "Node"
    default_edge_label: str = "Edge"
    index_config: str = "off"

MogwaiGraphDrawer

helper class to draw MogwaiGraphs

Source code in mogwai/core/mogwaigraph.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
class MogwaiGraphDrawer:
    """
    helper class to draw MogwaiGraphs
    """

    def __init__(self, g: MogwaiGraph, title: str, **kwargs):
        """
        Parameters
        ----------
        g : MogwaiGraph
            the graph to draw
        title : str
            the title of the graph
        kwargs : dict
            additional parameters used to configure the drawing style
            * *fontname* : str, default 'arial'
                the font to use
            * *fillcolor* : str, default '#ADE1FE'
                the fill color of the vertices
            * *edge_line_width* : int, default 3
                the width of the edges
            * *dash_width* : int, default 5
                number of dashess in the head/properties delimiter
            * *v_limit* : int, default 10
                the maximum number of vertices to show
            * *e_limit* : int, default 10
                the maximum number of edges to show
            * *vertex_properties* : list, default None
                the properties to display for vertices, if `None` all properties are shown
            * *edge_properties* : list, default None
                the properties to display for edges, if `None` all properties are shown
            * *prog* : str, default 'dot'
                the layout program to use
        """
        self.g = g
        self.title = title
        self.config = kwargs or {}
        self.vertex_keys = self.config.get("vertex_properties", None)
        self.edge_keys = self.config.get("edge_properties", None)

        self.v_drawn = set()
        self.e_drawn = set()

    def _draw_vertex(self, n):
        if len(self.v_drawn) >= self.config.get("v_limit", 10):
            return False
        if n[0] in self.v_drawn:
            return None
        id, properties = n
        head = (
            f"{id:d}, {properties.pop('name')}\n{', '.join(properties.pop('labels'))}"
        )
        if self.vertex_keys:
            properties = {k: v for k, v in properties.items() if k in self.vertex_keys}
        body = "\n".join([f"{k}: {v}" for k, v in properties.items()])
        label = f"{head}\n" + ("-" * self.config.get("dash_width", 5)) + f"\n{body}"

        self.gviz.add_node(
            id,
            label=label,
            fillcolor=self.config.get("fillcolor", "#ADE1FE"),
            style="filled",
            fontname=self.config.get("fontname", "arial"),
        )
        self.v_drawn.add(id)
        return True

    def _draw_edge(self, e, with_vertices: bool = True):
        if len(self.e_drawn) > self.config.get("e_limit", 10):
            return False
        if e[:-1] in self.e_drawn:
            return None
        if with_vertices:
            self._draw_vertex((e[0], self.g.nodes[e[0]]))
            self._draw_vertex((e[1], self.g.nodes[e[1]]))
        head = f"{e[2].pop('labels')}"
        body = "\n".join([f"{k}: {v}" for k, v in e[2].items()])
        label = f"{head}\n" + ("-" * self.config.get("dash_width", 5)) + f"\n{body}"

        self.gviz.add_edge(
            e[0],
            e[1],
            label=label,
            style=f"setlinewidth({self.config.get('edge_line_width', 3)})",
            fontname=self.config.get("fontname", "arial"),
        )
        self.e_drawn.add(e[:-1])

    def draw(self, outputfile: str):
        try:
            import pygraphviz
        except ImportError:
            raise ImportError("Please install pygraphviz to draw graphs.")

        self.gviz: pygraphviz.AGraph = networkx.nx_agraph.to_agraph(self.g)
        for n in self.g.nodes(data=True):
            if self._draw_vertex(n) == False:
                break
        for e in self.g.edges(data=True):
            if self._draw_edge(e) == False:
                break
        self.gviz.layout(prog=self.config.get("prog", "dot"))
        self.gviz.draw(outputfile)
__init__(g, title, **kwargs)
Parameters

g : MogwaiGraph the graph to draw title : str the title of the graph kwargs : dict additional parameters used to configure the drawing style * fontname : str, default 'arial' the font to use * fillcolor : str, default '#ADE1FE' the fill color of the vertices * edge_line_width : int, default 3 the width of the edges * dash_width : int, default 5 number of dashess in the head/properties delimiter * v_limit : int, default 10 the maximum number of vertices to show * e_limit : int, default 10 the maximum number of edges to show * vertex_properties : list, default None the properties to display for vertices, if None all properties are shown * edge_properties : list, default None the properties to display for edges, if None all properties are shown * prog : str, default 'dot' the layout program to use

Source code in mogwai/core/mogwaigraph.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def __init__(self, g: MogwaiGraph, title: str, **kwargs):
    """
    Parameters
    ----------
    g : MogwaiGraph
        the graph to draw
    title : str
        the title of the graph
    kwargs : dict
        additional parameters used to configure the drawing style
        * *fontname* : str, default 'arial'
            the font to use
        * *fillcolor* : str, default '#ADE1FE'
            the fill color of the vertices
        * *edge_line_width* : int, default 3
            the width of the edges
        * *dash_width* : int, default 5
            number of dashess in the head/properties delimiter
        * *v_limit* : int, default 10
            the maximum number of vertices to show
        * *e_limit* : int, default 10
            the maximum number of edges to show
        * *vertex_properties* : list, default None
            the properties to display for vertices, if `None` all properties are shown
        * *edge_properties* : list, default None
            the properties to display for edges, if `None` all properties are shown
        * *prog* : str, default 'dot'
            the layout program to use
    """
    self.g = g
    self.title = title
    self.config = kwargs or {}
    self.vertex_keys = self.config.get("vertex_properties", None)
    self.edge_keys = self.config.get("edge_properties", None)

    self.v_drawn = set()
    self.e_drawn = set()

steps

base_steps

branch_steps

filter_steps

HasWithin

Bases: FilterStep

Similar to Has, but with multiple options for the value

Source code in mogwai/core/steps/filter_steps.py
 94
 95
 96
 97
 98
 99
100
101
102
103
class HasWithin(FilterStep):
    """
    Similar to `Has`, but with multiple options for the value
    """
    def __init__(self, traversal:Traversal, key:str|List[str], valueOptions:List|Tuple):
        super().__init__(traversal)
        self.key = key
        self.valueOptions = valueOptions
        indexer = (lambda t: t.get) if key=="id" else get_dict_indexer(key, _NA)
        self._filter = lambda t: indexer(self.traversal._get_element(t)) in self.valueOptions

flatmap_steps

map_steps

modulation_steps

start_steps

statics

add_camel_case_aliases(module_globals)

Add camelCase aliases for all snake_case callables in the module's globals.

Source code in mogwai/core/steps/statics.py
10
11
12
13
14
15
16
17
18
19
20
21
def add_camel_case_aliases(module_globals):
    """Add camelCase aliases for all snake_case callables in the module's globals."""
    camel_case_aliases = {}
    for name, obj in module_globals.items():
        if callable(obj) and '_' in name:  # Only convert callable objects with underscores
            components = name.split('_')
            camel_case_name = components[0] + ''.join(x.capitalize() for x in components[1:])
            if name.endswith('_'):
                camel_case_name += '_'
            if camel_case_name != name:
                camel_case_aliases[camel_case_name] = obj
    module_globals.update(camel_case_aliases)

terminal_steps

traversal

AnonymousTraversal

Bases: Traversal

specialized Traversal

Source code in mogwai/core/traversal.py
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
class AnonymousTraversal(Traversal):
    """
    specialized Traversal
    """

    def __init__(self, start: "Step" = None):
        self.query_steps = [start] if start else []
        self.graph = None
        self.terminated = False
        self._needs_path = False

    # we need this since anonymous traversals need to check this before they're run.
    @property
    def needs_path(self):
        return self._needs_path or any((s.needs_path for s in self.query_steps))

    @needs_path.setter
    def needs_path(self, value):
        self._needs_path = value

    def run(self):
        raise ValueError("Cannot run anonymous traversals")

    def _build(self, traversal: Traversal):
        # first, set the necessary fields
        self.graph = traversal.graph
        self.eager = traversal.eager
        self.use_mp = traversal.use_mp
        self.verify_query = traversal.verify_query
        self.needs_path = any([s.needs_path for s in self.query_steps])
        self.optimize = traversal.optimize
        if traversal.optimize:
            self._optimize_query()
        if self.verify_query:
            self._verify_query()
        if self.query_steps[0].isstart:
            self.query_steps[0].set_traversal(self)
        super()._build()

    def __call__(self, traversers: Iterable["Traverser"]) -> Iterable["Traverser"]:
        # if this traversal is empty, just reflect back the incoming traversers
        if len(self.query_steps) == 0:
            return traversers
        self.traversers = traversers
        if self.eager:
            try:
                for step in self.query_steps:
                    logger.debug("Running step:" + str(step))
                    self.traversers = step(self.traversers)
                    if not type(self.traversers) is list:
                        self.traversers = list(self.traversers)
            except Exception as e:
                raise GraphTraversalError(
                    f"Something went wrong in step {step.print_query()}"
                )
        else:
            for step in self.query_steps:
                logger.debug("Running step:" + str(step))
                self.traversers = step(self.traversers)
            # TODO: Try to do some fancy error handling
        return self.traversers

Traversal

see https://tinkerpop.apache.org/javadocs/3.7.3/core/org/apache/tinkerpop/gremlin/process/traversal/Traversal.html A Traversal represents a directed walk over a Graph. This is the base interface for all traversal's, where each extending interface is seen as a domain specific language. For example, GraphTraversal is a domain specific language for traversing a graph using "graph concepts" (e.g. vertices, edges).

A Traversal is evaluated in one of two ways: iterator-based OLTP or GraphComputer-based OLAP. OLTP traversals leverage an iterator and are executed within a single execution environment (e.g. JVM) (with data access allowed to be remote).

OLAP traversals leverage GraphComputer and are executed between multiple execution environments (e.g.JVMs) (and/or cores).

Source code in mogwai/core/traversal.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
@add_camel_case_methods
class Traversal:
    """
    see https://tinkerpop.apache.org/javadocs/3.7.3/core/org/apache/tinkerpop/gremlin/process/traversal/Traversal.html
    A Traversal represents a directed walk over a Graph.
    This is the base interface for all traversal's,
    where each extending interface is seen as a domain
    specific language. For example, GraphTraversal
    is a domain specific language for traversing a graph
    using "graph concepts" (e.g. vertices, edges).

    A Traversal is evaluated in one of two ways:
    iterator-based OLTP or GraphComputer-based OLAP.
    OLTP traversals leverage an iterator and are executed
    within a single execution environment (e.g. JVM)
    (with data access allowed to be remote).

    OLAP traversals leverage GraphComputer and are executed
    between multiple execution environments (e.g.JVMs) (and/or cores).
    """

    def __init__(
        self,
        source: "MogwaiGraphTraversalSource",
        start: "Step",
        optimize: bool = True,
        eager: bool = False,
        query_verify: bool = False,
        use_mp: bool = False,
    ):
        if start is None:
            raise QueryError("start step cannot be None")
        self.query_steps = [start]
        if not self.query_steps[0].isstart:
            raise QueryError(
                "The first step should be a start-step, got " + str(self.query_steps[0])
            )
        self.graph = source.connector
        self.terminated = False
        self.eager = eager
        self.use_mp = use_mp
        self.verify_query = query_verify
        self.optimize = optimize
        self.max_iteration_depth = DEFAULT_ITERATION_DEPTH

    def _add_step(self, step: "Step"):
        if self.terminated:
            raise QueryError("Cannot add steps to a terminated traversal.")
        self.query_steps.append(step)
        if step.isterminal:
            self.terminated = True

    ## ===== FILTER STEPS ======
    def filter_(self, condition: "AnonymousTraversal") -> "Traversal":
        from .steps.filter_steps import Filter

        self._add_step(Filter(self, condition))
        return self

    def has(self, *args) -> "Traversal":
        """
        Filter traversers based on whether they have the given properties.
        * If one argument is given, it is assumed to be a key, and the step checks if a property with that key exists, regardless of its value.
        * If two arguments are given, it is assumed to be a key and a value, and the step checks if a property with that key exists and has the given value.
        * If three arguments are given, the first argument is assumed to be a label, and the step checks if a property with the given key and value exists on an element with that label.
        """
        # if `key` is a list, like ['a', 'b'], the value will be compared to data['a']['b']
        from .steps.filter_steps import Has

        if len(args) == 1:
            key, value = args[0], None
            self._add_step(Has(self, key, value))
        elif len(args) == 2:
            key, value = args
            self._add_step(Has(self, key, value))
        elif len(args) == 3:
            label, key, value = args
            self._add_step(Has(self, key, value, label=label))
        else:
            raise QueryError("Invalid number of arguments for `has`")
        return self

    def has_not(self, key: str):
        from .steps.filter_steps import HasNot

        self._add_step(HasNot(self, key))
        return self

    def has_key(self, *keys: str):
        from .steps.filter_steps import HasKey

        self._add_step(HasKey(self, *keys))
        return self

    def has_value(self, *values: Any) -> "Traversal":
        from .steps.filter_steps import HasValue

        self._add_step(HasValue(self, *values))
        return self

    def has_id(self, *ids: int | tuple) -> "Traversal":
        from .steps.filter_steps import HasId

        self._add_step(HasId(self, *ids))
        return self

    def has_name(self, *name: str) -> "Traversal":
        if len(name) == 0:
            raise QueryError("No name provided for `has_name`")
        elif len(name) == 1:
            return self.has("name", name[0])
        elif len(name) > 1:
            from .steps.filter_steps import HasWithin

            self._add_step(HasWithin(self, "name", name))
            return self

    def has_label(self, label: str | Set[str]) -> "Traversal":
        if isinstance(label, set):
            from .steps.filter_steps import ContainsAll

            self._add_step(ContainsAll(self, "labels", label))
        else:
            from .steps.filter_steps import Contains

            self._add_step(Contains(self, "labels", label))
        return self

    def is_(self, condition: Any) -> "Traversal":
        from .steps.filter_steps import Is

        self._add_step(Is(self, condition))
        return self

    def contains(self, key: str | List[str], value: Any) -> "Traversal":
        if isinstance(value, list):
            from .steps.filter_steps import ContainsAll

            self._add_step(ContainsAll(self, key, value))
        else:
            from .steps.filter_steps import Contains

            self._add_step(Contains(self, key, value))
        return self

    def within(self, key: str | List[str], options: List[Any]) -> "Traversal":
        from .steps.filter_steps import Within

        self._add_step(Within(self, key, options))
        return self

    def simple_path(self, by: str | List[str] = None) -> "Traversal":
        from .steps.filter_steps import SimplePath

        self._add_step(SimplePath(self, by=by))
        return self

    def limit(self, n: int) -> "Traversal":
        from .steps.filter_steps import Range

        self._add_step(Range(self, 0, n))
        return self

    def range(self, start: int, end: int) -> "Traversal":
        from .steps.filter_steps import Range

        self._add_step(Range(self, start, end))
        return self

    def skip(self, n: int) -> "Traversal":
        from .steps.filter_steps import Range

        self._add_step(Range(self, n, -1))
        return self

    def dedup(self, by: str | List[str] = None) -> "Traversal":
        from .steps.filter_steps import Dedup

        self._add_step(Dedup(self, by=by))
        return self

    def not_(self, condition: "AnonymousTraversal") -> "Traversal":
        from .steps.filter_steps import Not

        self._add_step(Not(self, condition))
        return self

    def and_(self, A: "AnonymousTraversal", B: "AnonymousTraversal") -> "Traversal":
        from .steps.filter_steps import And

        self._add_step(And(self, A, B))
        return self

    def or_(self, A: "AnonymousTraversal", B: "AnonymousTraversal") -> "Traversal":
        from .steps.filter_steps import Or

        self._add_step(Or(self, A, B))
        return self

    ## ===== MAP STEPS ======
    def identity(self) -> "Traversal":  # required for math reasons
        return self

    # Important: `value` extract values from *Property's*
    # `values` extracts values from *elements*!
    # So, .properties(key).value() is the same as .values(key)
    def value(self) -> "Traversal":
        from .steps.map_steps import Value

        self._add_step(Value(self))
        return self

    def key(self) -> "Traversal":
        from .steps.map_steps import Key

        self._add_step(Key(self))
        return self

    def values(self, *keys: str | List[str]) -> "Traversal":
        from .steps.map_steps import Values

        self._add_step(Values(self, *keys))
        return self

    def name(self) -> "Traversal":
        return self.values("name")

    def label(self) -> "Traversal":
        return self.values("labels")

    def properties(self, *keys: str | List[str]) -> "Traversal":
        from .steps.map_steps import Properties

        self._add_step(Properties(self, *keys))
        return self

    def select(self, *args: str, by: str = None) -> "Traversal":
        from .steps.map_steps import Select

        self._add_step(
            Select(self, keys=args[0] if len(args) == 1 else list(args), by=by)
        )
        return self

    def order(
        self,
        by: str | List[str] | "AnonymousTraversal" = None,
        asc: bool | None = None,
        **kwargs,
    ) -> "Traversal":
        from .steps.map_steps import Order

        self._add_step(Order(self, by, asc, **kwargs))
        return self

    def count(self, scope: Scope = Scope.global_) -> "Traversal":
        from .steps.map_steps import Count

        self._add_step(Count(self, scope))
        return self

    def path(self, by: str | List[str] = None) -> "Traversal":
        from .steps.map_steps import Path

        self._add_step(Path(self, by=by))
        return self

    def max_(self, scope: Scope = Scope.global_) -> "Traversal":
        from .steps.map_steps import Max

        self._add_step(Max(self, scope))
        return self

    def min_(self, scope: Scope = Scope.global_) -> "Traversal":
        from .steps.map_steps import Min

        self._add_step(Min(self, scope))
        return self

    def sum_(self, scope: Scope = Scope.global_) -> "Traversal":
        from .steps.map_steps import Aggregate

        self._add_step(Aggregate(self, "sum", scope))
        return self

    def mean(self, scope: Scope = Scope.global_) -> "Traversal":
        from .steps.map_steps import Aggregate

        self._add_step(Aggregate(self, "mean", scope))
        return self

    def element_map(self, *keys: str) -> "Traversal":
        from .steps.map_steps import ElementMap

        if len(keys) == 1:
            keys = keys[0]
        elif len(keys) == 0:
            keys = None
        self._add_step(ElementMap(self, keys))
        return self

    ## ===== FLATMAP STEPS ======
    def out(self, direction: str = None) -> "Traversal":
        from .steps.flatmap_steps import Out

        self._add_step(Out(self, direction))
        return self

    def outE(self, direction: str = None) -> "Traversal":
        from .steps.flatmap_steps import OutE

        self._add_step(OutE(self, direction))
        return self

    def outV(self) -> "Traversal":
        from .steps.flatmap_steps import OutV

        self._add_step(OutV(self))
        return self

    def in_(self, direction: str = None) -> "Traversal":
        from .steps.flatmap_steps import In

        self._add_step(In(self, direction))
        return self

    def inE(self, direction: str = None) -> "Traversal":
        from .steps.flatmap_steps import InE

        self._add_step(InE(self, direction))
        return self

    def inV(self) -> "Traversal":
        from .steps.flatmap_steps import InV

        self._add_step(InV(self))
        return self

    def both(self, direction: str = None) -> "Traversal":
        from .steps.flatmap_steps import Both

        self._add_step(Both(self, direction))
        return self

    def bothE(self, direction: str = None) -> "Traversal":
        from .steps.flatmap_steps import BothE

        self._add_step(BothE(self, direction))
        return self

    def bothV(self) -> "Traversal":
        from .steps.flatmap_steps import BothV

        self._add_step(BothV(self))
        return self

    ## ===== BRANCH STEPS =====
    @with_call_order
    def repeat(
        self,
        do: "Traversal",
        times: int | None = None,
        until: "AnonymousTraversal|None" = None,
        **kwargs,
    ) -> "Traversal":
        from .steps.branch_steps import Repeat
        from .steps.modulation_steps import Temp

        if until is not None:
            until_do = (
                len(kwargs.get("_order", [])) > 0 and kwargs["_order"][0] == "until"
            )
        else:
            until_do = None

        step = Repeat(self, do, times=times, until=until, until_do=until_do)
        while isinstance((prevstep := self.query_steps[-1]), Temp):
            if prevstep.kwargs["type"] == "emit":
                step.emit = prevstep.kwargs["filter"]
                step.emit_before = True
            elif prevstep.kwargs["type"] == "until":
                if until is not None or times is not None:
                    raise QueryError(
                        "Provided `until` to repeat when `times` or `until` was already set."
                    )
                step.until = prevstep.kwargs["cond"]
                step.until_do = True
            else:
                break
            self.query_steps.pop(-1)  # remove the temporary step
        self._add_step(step)
        return self

    def local(self, localTrav: "AnonymousTraversal") -> "Traversal":
        from .steps.branch_steps import Local

        self._add_step(Local(self, localTrav))
        return self

    def branch(self, branchFunc: "AnonymousTraversal") -> "Traversal":
        from .steps.branch_steps import Branch
        from .steps.map_steps import MapStep

        if len(branchFunc.query_steps) == 0 or not isinstance(
            branchFunc.query_steps[-1], MapStep
        ):
            raise TypeError("Branch is only allowed to be given MapSteps")
        self._add_step(Branch(self, branchFunc))
        return self

    def union(self, *traversals: "AnonymousTraversal") -> "Traversal":
        from .steps.branch_steps import Union

        self._add_step(Union(self, *traversals))
        return self

    ## ===== MODULATION STEPS ======
    def option(self, branchKey, OptionStep: "AnonymousTraversal") -> "Traversal":
        from .steps.branch_steps import Branch

        branchStep = self.query_steps[len(self.query_steps) - 1]
        if type(branchStep) is Branch:
            branchStep.flags |= Step.NEEDS_PATH if OptionStep.needs_path else 0
            if branchKey is not None:
                if branchKey not in branchStep.options:
                    branchStep.options[branchKey] = OptionStep
                    return self
                else:
                    raise QueryError(
                        "Duplicate key " + str(branchKey) + ", please use distinct keys"
                    )
            else:
                if branchStep.defaultStep is None:
                    branchStep.defaultStep = OptionStep
                    return self
                else:
                    raise QueryError(
                        "Provided two default (None) options. This is not allowed"
                    )
        else:
            raise QueryError("Options can only be used after Branch()")

    def until(self, cond: "AnonymousTraversal"):
        from .steps.branch_steps import Repeat

        prevstep = self.query_steps[-1]
        if isinstance(prevstep, Repeat):
            if prevstep.until is None and prevstep.times is None:
                prevstep.until = cond
                prevstep.until_do = False
                if cond.needs_path:
                    prevstep.flags |= Repeat.NEEDS_PATH
            else:
                raise QueryError(
                    "Provided `until` to repeat when `times` or `until` was already set."
                )
        else:
            from .steps.modulation_steps import Temp

            self._add_step(Temp(self, type="until", cond=cond))
        return self

    def times(self, reps: int):
        from .steps.branch_steps import Repeat

        prevstep = self.query_steps[-1]
        if isinstance(prevstep, Repeat):
            if prevstep.times is None and prevstep.until is None:
                prevstep.times = reps
            else:
                raise QueryError(
                    "Provided `times` to repeat when `times` or `until` was already set."
                )
        else:
            raise QueryError(
                f"`times` modulation is not supported by step {prevstep.print_query()}"
            )
        return self

    def emit(self, filter: "AnonymousTraversal|None" = None):
        from .steps.branch_steps import Repeat

        prevstep = self.query_steps[-1]
        if isinstance(prevstep, Repeat):
            if prevstep.emit is None:
                prevstep.emit = filter or True
                prevstep.emit_before = False
                if filter and filter.needs_path:
                    prevstep.flags |= Repeat.NEEDS_PATH
            else:
                raise QueryError(
                    "Provided `emit` to repeat when `emit` was already set."
                )
        else:
            from .steps.modulation_steps import Temp

            self._add_step(Temp(self, type="emit", filter=filter or True))
        return self

    def as_(self, name: str) -> "Traversal":
        from .steps.modulation_steps import As

        self._add_step(As(self, name))
        return self

    def by(self, key: str | List[str] | "AnonymousTraversal") -> "Traversal":
        prev_step = self.query_steps[-1]
        if prev_step.supports_by:
            if isinstance(key, AnonymousTraversal):
                if not prev_step.supports_anonymous_by:
                    raise QueryError(
                        f"Step `{prev_step.print_query()}` does not support anonymous traversals as by-modulations."
                    )
            elif type(key) is not str:
                raise QueryError("Invalid key type for by-modulation")

            if prev_step.supports_multiple_by:
                prev_step.by.append(key)
            elif prev_step.by is None:
                prev_step.by = key
            else:
                raise QueryError(
                    f"Step `{prev_step.print_query()}` does not support multiple by-modulations."
                )
        else:
            raise QueryError(
                f"Step `{prev_step.print_query()}` does not support by-modulation."
            )
        return self

    def from_(self, src: int) -> "Traversal":
        prev_step = self.query_steps[-1]
        if prev_step.supports_fromto:
            if type(src) is not int:
                raise QueryError("Invalid source type for from-modulation")
            if prev_step.from_ is None:
                prev_step.from_ = src
            else:
                raise QueryError(
                    f"Step `{prev_step.print_query()}` does not support multiple from-modulations."
                )
        else:
            raise QueryError(
                f"Step `{prev_step.print_query()}` does not support from-modulation."
            )
        return self

    def to_(self, dest: int) -> "Traversal":
        prev_step = self.query_steps[-1]
        if prev_step.supports_fromto:
            if type(dest) is not int:
                raise QueryError("Invalid source type for to-modulation")
            if prev_step.to_ is None:
                prev_step.to_ = dest
            else:
                raise QueryError(
                    f"Step `{prev_step.print_query()}` does not support multiple to-modulations."
                )
        else:
            raise QueryError(
                f"Step `{prev_step.print_query()}` does not support to-modulation."
            )
        return self

    ## ===== SIDE EFFECT STEPS ======
    def side_effect(
        self, side_effect: "AnonymousTraversal|Callable[[Traverser], None]"
    ) -> "Traversal":
        from .steps.base_steps import SideEffectStep

        self._add_step(SideEffectStep(self, side_effect))
        return self

    def property(self, key: str | List[str], value: Any) -> "Traversal":
        from mogwai.utils import get_dict_indexer

        from .steps.base_steps import SideEffectStep

        # keys = ['properties'] + (key if type(key) is list else [key])
        if isinstance(key, (tuple, list)):
            indexer = get_dict_indexer(key[:-1])
            key = key[-1]
        else:
            indexer = lambda x: x

        def effect(t: "Traverser"):
            indexer(self._get_element(t))[key] = value

        self._add_step(SideEffectStep(self, side_effect=effect))
        return self

    ## ===== TERMINAL STEPS ======
    def to_list(
        self, by: List[str] | str = None, include_data: bool = False
    ) -> "Traversal":
        # terminal step
        from .steps.terminal_steps import ToList

        self._add_step(ToList(self, by=by, include_data=include_data))
        return self

    def as_path(self, by: List[str] | str = None) -> "Traversal":
        # terminal step
        from .steps.terminal_steps import AsPath

        self._add_step(AsPath(self, by=by))
        return self

    def has_next(self) -> "Traversal":
        from .steps.terminal_steps import HasNext

        self._add_step(HasNext(self))
        return self

    def next(self) -> "Traversal":
        from .steps.terminal_steps import Next

        self._add_step(Next(self))
        return self

    def iter(
        self, by: str | List[str] = None, include_data: bool = False
    ) -> "Traversal":
        from .steps.terminal_steps import AsGenerator

        self._add_step(AsGenerator(self, by=by, include_data=include_data))
        return self

    def iterate(self) -> "Traversal":
        from .steps.terminal_steps import Iterate

        self._add_step(Iterate(self))
        return self

    def _optimize_query(self):
        pass

    def _verify_query(self):
        from .steps.modulation_steps import Temp

        for step in self.query_steps:
            if isinstance(step, Temp):
                raise QueryError(f"Remaining modulation step of type `{step['type']}`")
        return True

    def _build(self):
        for step in self.query_steps:
            step.build()

    def run(self) -> Any:
        # first, provide the start step with this traversal
        self.traversers = []
        self.query_steps[0].set_traversal(self)
        self._build()
        self.needs_path = any([s.needs_path for s in self.query_steps])
        if self.optimize:
            self._optimize_query()
        self._verify_query()
        if self.eager:
            try:
                for step in self.query_steps:
                    logger.debug("Running step:" + str(step))
                    self.traversers = step(self.traversers)
                    if not type(self.traversers) is list:
                        self.traversers = list(self.traversers)
            except Exception as e:
                raise GraphTraversalError(
                    f"Something went wrong in step {step.print_query()}"
                )
        else:
            for step in self.query_steps:
                logger.debug("Running step:" + str(step))
                self.traversers = step(self.traversers)
            # TODO: Try to do some fancy error handling
        return self.traversers

    def _get_element(self, traverser: "Traverser", data: bool = False):
        if type(traverser) == Traverser:
            if data:
                return (
                    self.graph.edges[traverser.get]
                    if traverser.is_edge
                    else self.graph.nodes(data=data)[traverser.get]
                )
            return (
                self.graph.edges[traverser.get]
                if traverser.is_edge
                else self.graph.nodes[traverser.get]
            )
        else:
            raise GraphTraversalError(
                "Cannot get element from value or property traverser."
                + " Probably you are performing a step that can only be executed on graph elements on a value or property traverser."
            )

    def _get_element_from_id(self, element_id: int | tuple):
        return (
            self.graph.nodes[element_id]
            if type(element_id) is int
            else self.graph.edges[element_id]
        )

    def print_query(self) -> str:
        return " -> ".join([x.print_query() for x in self.query_steps])

    def __str__(self) -> str:
        return self.print_query()
has(*args)

Filter traversers based on whether they have the given properties. * If one argument is given, it is assumed to be a key, and the step checks if a property with that key exists, regardless of its value. * If two arguments are given, it is assumed to be a key and a value, and the step checks if a property with that key exists and has the given value. * If three arguments are given, the first argument is assumed to be a label, and the step checks if a property with the given key and value exists on an element with that label.

Source code in mogwai/core/traversal.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def has(self, *args) -> "Traversal":
    """
    Filter traversers based on whether they have the given properties.
    * If one argument is given, it is assumed to be a key, and the step checks if a property with that key exists, regardless of its value.
    * If two arguments are given, it is assumed to be a key and a value, and the step checks if a property with that key exists and has the given value.
    * If three arguments are given, the first argument is assumed to be a label, and the step checks if a property with the given key and value exists on an element with that label.
    """
    # if `key` is a list, like ['a', 'b'], the value will be compared to data['a']['b']
    from .steps.filter_steps import Has

    if len(args) == 1:
        key, value = args[0], None
        self._add_step(Has(self, key, value))
    elif len(args) == 2:
        key, value = args
        self._add_step(Has(self, key, value))
    elif len(args) == 3:
        label, key, value = args
        self._add_step(Has(self, key, value, label=label))
    else:
        raise QueryError("Invalid number of arguments for `has`")
    return self

traverser

Traverser

see https://tinkerpop.apache.org/javadocs/3.7.3/core/org/apache/tinkerpop/gremlin/process/traversal/Traverser.html

A Traverser represents the current state of an object flowing through a Traversal.

A traverser maintains a reference to the current object, a traverser-local "sack", a traversal-global sideEffect, a bulk count, and a path history.

Different types of traversers can exist depending on the semantics of the traversal and the desire for space/time optimizations of the developer.

Source code in mogwai/core/traverser.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class Traverser:
    """
    see https://tinkerpop.apache.org/javadocs/3.7.3/core/org/apache/tinkerpop/gremlin/process/traversal/Traverser.html

    A Traverser represents the current state of an object
    flowing through a Traversal.

    A traverser maintains a reference to the current object,
    a traverser-local "sack",
    a traversal-global sideEffect, a bulk count,
    and a path history.

    Different types of traversers can exist
    depending on the semantics of the traversal
    and the desire for space/time optimizations of
    the developer.
    """

    def __init__(
        self, node_id: int, other_node_id: int = None, track_path: bool = True
    ):  # TODO set `track_path` default to False
        self.node_id = node_id
        self.track_path = track_path
        self.target = other_node_id
        self.cache = {"__store__": {}}
        self.path = [self.get] if track_path else None

    def move_to_edge(self, source: int, target: int) -> None:
        self.node_id = source
        self.target = target
        if self.track_path:
            self.path.append((source, target))

    @property
    def get(self) -> int | tuple:
        return (self.node_id, self.target) if self.is_edge else self.node_id

    @property
    def source(self) -> int:
        return self.node_id

    @property
    def is_edge(self) -> bool:
        return self.target is not None

    def save(self, key):
        if self.is_edge:
            to_store = Traverser(*self.get, track_path=self.track_path)
        else:
            to_store = Traverser(self.get, track_path=self.track_path)

        to_store.cache = self.cache.copy()  # no need to deep copies
        self.cache["__store__"][key] = to_store

    def load(self, key):
        # logger.debug(f"Cache: {self.cache['__store__'].keys()}")
        try:
            return self.cache["__store__"][key]
        except KeyError:
            raise ValueError(
                f"No object `{key}` was saved in this traverser. Use .as('{key}') to save traversal steps."
            )

    def get_cache(self, key):
        return self.cache.get(key, None)

    def set(self, key: str, val: Any):
        assert key != "__store__", "`__store__` is a reserved key"
        self.cache[key] = val

    def move_to(self, node_id: int) -> "Traverser":
        # logging.debug("Moving traverser from", self.get, "to", node_id)
        self.node_id = node_id
        self.target = None
        if self.track_path:
            self.path.append(node_id)
        return self

    def copy(self):
        t = Traverser(
            node_id=self.node_id, other_node_id=self.target, track_path=self.track_path
        )
        t.cache = deepcopy(self.cache)
        t.path = deepcopy(self.path)
        return t

    def copy_to(self, node_id: int, other_node_id: int = None) -> "Traverser":
        t = self.copy()
        if other_node_id:
            t.move_to_edge(node_id, other_node_id)
        else:
            t.move_to(node_id)
        return t

    def to_value(self, val, dtype=None):
        val = Value(val, dtype=dtype)
        val.cache = deepcopy(self.cache)
        return val

    def to_property(self, key, val, dtype=None):
        p = Property(key, val, dtype=dtype)
        p.cache = deepcopy(self.cache)
        return p

    def __str__(self):
        return f"<{self.__class__.__name__}[get={self.get}, is_edge={self.is_edge}]>"

decorators

decorators

graph_config

Created on 2024-08-17

@author: wf

GraphConfig

Configuration for a graph in the Examples class

Source code in mogwai/graph_config.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@lod_storable
class GraphConfig:
    """
    Configuration for a graph in the Examples class
    """

    name: str
    file_path: Optional[str] = None
    is_default: bool = False
    node_label_key: str = "labelV"
    edge_label_key: str = "labelE"
    node_name_key: Optional[str] = None
    custom_loader: Optional[str] = None  # Changed to str to store function name

    def get_node_name_key(self) -> Callable[[Dict[str, Any]], Any]:
        if self.node_name_key is None:
            return lambda x: x  # Return identity function if no key specified
        elif isinstance(self.node_name_key, str):
            return lambda x: x.pop(self.node_name_key, None)
        else:
            raise ValueError(f"Invalid node_name_key for graph {self.name}")

GraphConfigs

Manages a collection of GraphConfig instances

Source code in mogwai/graph_config.py
36
37
38
39
40
@lod_storable
class GraphConfigs:
    """Manages a collection of GraphConfig instances"""

    configs: Dict[str, GraphConfig] = field(default_factory=dict)

graphs

Created on 2024-08-17

@author: wf

Graphs

Manage MogwaiGraphs

Source code in mogwai/graphs.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
class Graphs:
    """
    Manage MogwaiGraphs
    """

    def __init__(
        self, config_file: str = None, lazy: bool = False, debug: bool = False
    ):
        self.debug = debug
        self.logger = self.get_logger()
        self.examples_dir = os.path.join(
            os.path.dirname(__file__), "..", "mogwai_examples"
        )
        if config_file is None:
            config_file = os.path.join(self.examples_dir, "example_graph_configs.yaml")
        self.config_file = config_file
        self.graphs: Dict[str, MogwaiGraph] = {}

        self.log(f"Loading configurations from: {self.config_file}")
        self.configs = GraphConfigs.load_from_yaml_file(self.config_file)
        self.log(f"Loaded configurations: {self.configs.configs}")

        if not lazy:
            self.load_examples()

    def get_logger(self):
        return logging.getLogger(self.__class__.__name__)

    def log(self, msg: str):
        if self.debug:
            self.logger.debug(msg)

    def load_examples(self):
        """Load all example graphs based on configurations"""
        self.log("Loading default examples")
        for name, config in self.configs.configs.items():
            if config.is_default:
                self.log(f"Loading default graph: {name}")
                self.get(name)  # This will load the graph using the existing get method

    def _load_graph(self, file_path: str, config: GraphConfig) -> MogwaiGraph:
        """Load a single graph from a .graphml file using the provided configuration"""
        self.log(f"Loading graph from file: {file_path}")
        return graphml_to_mogwaigraph(
            file_path,
            node_label_key=config.node_label_key,
            edge_label_key=config.edge_label_key,
            node_name_key=config.get_node_name_key(),
        )

    def get_names(self) -> List[str]:
        """Get a list of available graph names"""
        names = list(self.configs.configs.keys())
        self.log(f"Available graph names: {names}")
        return names

    def get(self, name: str) -> MogwaiGraph:
        """Get a graph by name, loading it if necessary"""
        if name not in self.configs.configs:
            error_msg = f"Graph '{name}' not found in configurations"
            self.log(error_msg)
            raise ValueError(error_msg)

        if name not in self.graphs:
            config = self.configs.configs[name]
            if config.custom_loader:
                self.log(f"Using custom loader for graph '{name}'")
                # Assuming custom_loader is a string representing a method name in MogwaiGraph
                loader = getattr(MogwaiGraph, config.custom_loader, None)
                if loader and callable(loader):
                    self.graphs[name] = loader()
                else:
                    error_msg = f"Invalid custom loader {config.custom_loader} for graph '{name}'"
                    self.log(error_msg)
                    raise ValueError(error_msg)
            elif config.file_path:
                file_path = os.path.join(self.examples_dir, config.file_path)
                self.log(f"Loading graph '{name}' from file: {file_path}")
                self.graphs[name] = self._load_graph(file_path, config)
            else:
                error_msg = f"No loader or file path specified for graph '{name}'"
                self.log(error_msg)
                raise ValueError(error_msg)

        return self.graphs[name]

get(name)

Get a graph by name, loading it if necessary

Source code in mogwai/graphs.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def get(self, name: str) -> MogwaiGraph:
    """Get a graph by name, loading it if necessary"""
    if name not in self.configs.configs:
        error_msg = f"Graph '{name}' not found in configurations"
        self.log(error_msg)
        raise ValueError(error_msg)

    if name not in self.graphs:
        config = self.configs.configs[name]
        if config.custom_loader:
            self.log(f"Using custom loader for graph '{name}'")
            # Assuming custom_loader is a string representing a method name in MogwaiGraph
            loader = getattr(MogwaiGraph, config.custom_loader, None)
            if loader and callable(loader):
                self.graphs[name] = loader()
            else:
                error_msg = f"Invalid custom loader {config.custom_loader} for graph '{name}'"
                self.log(error_msg)
                raise ValueError(error_msg)
        elif config.file_path:
            file_path = os.path.join(self.examples_dir, config.file_path)
            self.log(f"Loading graph '{name}' from file: {file_path}")
            self.graphs[name] = self._load_graph(file_path, config)
        else:
            error_msg = f"No loader or file path specified for graph '{name}'"
            self.log(error_msg)
            raise ValueError(error_msg)

    return self.graphs[name]

get_names()

Get a list of available graph names

Source code in mogwai/graphs.py
66
67
68
69
70
def get_names(self) -> List[str]:
    """Get a list of available graph names"""
    names = list(self.configs.configs.keys())
    self.log(f"Available graph names: {names}")
    return names

load_examples()

Load all example graphs based on configurations

Source code in mogwai/graphs.py
48
49
50
51
52
53
54
def load_examples(self):
    """Load all example graphs based on configurations"""
    self.log("Loading default examples")
    for name, config in self.configs.configs.items():
        if config.is_default:
            self.log(f"Loading default graph: {name}")
            self.get(name)  # This will load the graph using the existing get method

mogwai_cmd

Created on 2024-08-15

@author: wf

MogwaiCmd

Bases: WebserverCmd

command line handling for nicesprinkler

Source code in mogwai/mogwai_cmd.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MogwaiCmd(WebserverCmd):
    """
    command line handling for nicesprinkler
    """

    def __init__(self):
        """
        constructor
        """
        config = MogwaiWebServer.get_config()
        WebserverCmd.__init__(self, config, MogwaiWebServer, DEBUG)

    def getArgParser(self, description: str, version_msg) -> ArgumentParser:
        """
        override the default argparser call
        """
        parser = super().getArgParser(description, version_msg)

        return parser

__init__()

constructor

Source code in mogwai/mogwai_cmd.py
20
21
22
23
24
25
def __init__(self):
    """
    constructor
    """
    config = MogwaiWebServer.get_config()
    WebserverCmd.__init__(self, config, MogwaiWebServer, DEBUG)

getArgParser(description, version_msg)

override the default argparser call

Source code in mogwai/mogwai_cmd.py
27
28
29
30
31
32
33
def getArgParser(self, description: str, version_msg) -> ArgumentParser:
    """
    override the default argparser call
    """
    parser = super().getArgParser(description, version_msg)

    return parser

main(argv=None)

main call

Source code in mogwai/mogwai_cmd.py
36
37
38
39
40
41
42
def main(argv: list = None):
    """
    main call
    """
    cmd = MogwaiCmd()
    exit_code = cmd.cmd_main(argv)
    return exit_code

parser

excel_converter

filesystem

graphml_converter

graphml_to_mogwaigraph(file, node_label_key, node_name_key, edge_label_key=None, default_node_label='Na', default_edge_label='Na', default_node_name='Na', include_id=False, keep=True)

Converts GraphML file to MogwaiGraph object.

Parameters

file : str Path to the GraphML file. node_label_key : str or Callable[[dict],str] Key to use for the node label. If a string, the value of the key is used as the label. If a function, it should take a dictionary of node data and return a string. node_name_key : str or Callable[[dict],str] Key to use for the node name. If a string, the value of the key is used as the name. If a function, it should take a dictionary of node data and return a string. edge_label_key : str or Callable[[dict],str], optional Key to use for the edge label. If a string, the value of the key is used as the label. If a function, it should take a dictionary of edge data and return a string. If None, the node_label_key is used. default_node_label : str, optional Default label to use for nodes that do not have a property corresponding to node_label_key. default_edge_label : str, optional Default label to use for edges that do not have a property corresponding to edge_label_key. default_node_name : str, optional Default name to use for nodes that do not have a property corresponding to node_name_key. include_id : bool or str, optional If True, the node id is included in the data dictionary of each node. If a string, the node id is included in the data dictionary with the given key. keep : bool, optional If True, the labels and names are kept as properties in the node data dictionary. If False, they are removed.

Returns

MogwaiGraph The graph object

Source code in mogwai/parser/graphml_converter.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def graphml_to_mogwaigraph(file:str, node_label_key:str|Callable[[dict],str], node_name_key:str|Callable[[dict],str],
                           edge_label_key:str|Callable[[dict],str]=None,
                           default_node_label:str='Na', default_edge_label:str='Na', default_node_name:str='Na',
                           include_id:bool|str=False, keep:bool=True)->MogwaiGraph:
    """
    Converts GraphML file to MogwaiGraph object.

    Parameters
    ----------
    file : str
        Path to the GraphML file.
    node_label_key : str or Callable[[dict],str]
        Key to use for the node label. If a string, the value of the key is used as the label.
        If a function, it should take a dictionary of node data and return a string.
    node_name_key : str or Callable[[dict],str]
        Key to use for the node name. If a string, the value of the key is used as the name.
        If a function, it should take a dictionary of node data and return a string.
    edge_label_key : str or Callable[[dict],str], optional
        Key to use for the edge label. If a string, the value of the key is used as the label.
        If a function, it should take a dictionary of edge data and return a string.
        If None, the node_label_key is used.
    default_node_label : str, optional
        Default label to use for nodes that do not have a property corresponding to `node_label_key`.
    default_edge_label : str, optional
        Default label to use for edges that do not have a property corresponding to `edge_label_key`.
    default_node_name : str, optional
        Default name to use for nodes that do not have a property corresponding to `node_name_key`.
    include_id : bool or str, optional
        If True, the node id is included in the data dictionary of each node.
        If a string, the node id is included in the data dictionary with the given key.
    keep : bool, optional
        If True, the labels and names are kept as properties in the node data dictionary. If False, they are removed.

    Returns
    -------
    MogwaiGraph
        The graph object
    """
    gml = nx.read_graphml(file)
    if not gml.is_directed():
        raise MogwaiGraphError("Can not import undirected graphml graph")
    g = MogwaiGraph()
    edge_label_key = edge_label_key or node_label_key
    if(include_id==True): include_id='id' #use 'id' as the default key
    #Note: these function change the node data!
    # However, this is not a problem, since `gml` is discarded anyway.
    missing_label_count = count()
    if type(node_label_key) is str:
        def node_label_func(data:dict):
            if node_label_key in data: return data[node_label_key] if keep else data.pop(node_label_key)
            else:
                next(missing_label_count)
                return default_node_label
    else:
        node_label_func = node_label_key
    missing_name_count = count()
    if type(node_name_key) is str:
        def node_name_func(data:dict):
            if node_name_key in data: return data[node_name_key] if keep else data.pop(node_name_key)
            else:
                next(missing_name_count)
                return default_node_name
    else:
        node_name_func = node_name_key

    missing_edge_count = count()
    if type(edge_label_key) is str:
        def edge_label_func(data:dict):
            if edge_label_key in data: return data[edge_label_key] if keep else data.pop(edge_label_key)
            else:
                next(missing_edge_count)
                return default_edge_label
    else:
        edge_label_func = edge_label_key

    node_to_id_map = {}
    for node, data in gml.nodes(data=True):
        if(include_id):
            data[include_id] = node
        assigned_id = g.add_labeled_node(label=node_label_func(data), name=node_name_func(data), **data)
        node_to_id_map[node] = assigned_id
    for node1, node2, data in gml.edges(data=True):
        g.add_labeled_edge(srcId=node_to_id_map[node1] , destId=node_to_id_map[node2], edgeLabel=edge_label_func(data), **data)

    missing_edge_count = next(missing_edge_count)
    missing_name_count = next(missing_name_count)
    missing_label_count = next(missing_label_count)
    if(missing_edge_count>0): logger.warning(f"Encountered {missing_edge_count} edges without label")
    if(missing_name_count>0): logger.warning(f"Encountered {missing_name_count} nodes without name")
    if(missing_label_count>0): logger.warning(f"Encountered {missing_label_count} nodes without label")
    return g

pdfgraph

powerpoint_converter

version

Created on 2024-08-15

@author: wf

Version dataclass

Bases: object

Version handling for pyMogwai

Source code in mogwai/version.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@dataclass
class Version(object):
    """
    Version handling for pyMogwai
    """

    name = "pymogwai"
    version = mogwai.__version__
    date = "2024-08-15"
    updated = "2024-11-10"
    description = "python native gremlin implementation"

    authors = "Wolfgang Fahl"

    chat_url = "https://github.com/juupje/pyMogwai/discussions"
    doc_url = "https://cr.bitplan.com/index.php/pyMogwai"
    cm_url = "https://github.com/juupje/pyMogwai"

    license = f"""Copyright 2024 contributors. All rights reserved.

  Licensed under the Apache License 2.0
  http://www.apache.org/licenses/LICENSE-2.0

  Distributed on an "AS IS" basis without warranties
  or conditions of any kind, either express or implied."""
    longDescription = f"""{name} version {version}
{description}

  Created by {authors} on {date} last updated {updated}"""

web

server

Created on 2024-08-15

@author: wf

MogwaiSolution

Bases: InputWebSolution

the Mogwai solution

Source code in mogwai/web/server.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
class MogwaiSolution(InputWebSolution):
    """
    the Mogwai solution
    """

    def __init__(self, webserver: MogwaiWebServer, client: Client):
        """
        Initialize the solution

        Args:
            webserver (MogwaiWebServer): The webserver instance associated with this context.
            client (Client): The client instance this context is associated with.
        """
        super().__init__(webserver, client)
        self.examples=webserver.examples
        self.graph=None
        self.graph_label=None
        self.result_html=None
        self.update_graph("modern")

    def authenticated(self) -> bool:
        """
        Check if the user is authenticated.
        Returns:
            True if the user is authenticated, False otherwise.
        """
        return self.webserver.login.authenticated()

    def setup_menu(self, detailed: bool = True):
        """
        setup the menu
        """
        super().setup_menu(detailed=detailed)
        ui.button(icon="menu", on_click=lambda: self.header.toggle())
        with self.header:
            if self.authenticated():
                self.link_button("logout", "/logout", "logout", new_tab=False)
            else:
                self.link_button("login", "/login", "login", new_tab=False)

    async def login_ui(self):
        """
        login ui
        """
        await self.webserver.login.login(self)

    async def home(self):
        """Provide the main content page"""
        await self.query_graph()

    async def on_graph_select(self,vce_args):
        await run.io_bound(self.update_graph,vce_args.value)

    def update_graph(self,graph_name:str):
        try:
            self.graph_name=graph_name
            self.graph = self.load_graph(name=graph_name)
            self.get_graph_label()
            if self.graph_label:
                self.graph_label.update()
        except Exception as ex:
            self.handle_exception(ex)

    def get_graph_label(self)->str:
        self.graph_label_text=f"Query Graph {self.graph.name} {len(self.graph.nodes)} nodes {len(self.graph.edges)} edges"
        return self.graph_label_text

    async def query_graph(self):
        """Graph querying page"""
        def setup_query():
            emphasize="text-h5"
            try:
                with ui.row() as self.header_row:
                    graph_selection=self.examples.get_names()
                    self.graph_selector=self.add_select(
                        "graph",
                        graph_selection,
                        value=self.graph_name,
                        on_change=self.on_graph_select)
                if self.authenticated():
                    with ui.row() as self.upload_row:
                        ui.label("import File").classes(emphasize)
                        file_upload = ui.upload(label="Choose a file", multiple=False, auto_upload=True)
                        file_upload.on('upload', self.handle_upload)

                if self.graph:
                    self.get_graph_label()
                    self.graph_label=ui.label().classes(emphasize)
                    self.graph_label.bind_text_from(self, 'graph_label_text')
                    self.query_text_area = (
                        ui.textarea("Enter Gremlin Query")
                        .props("clearable")
                        .props("rows=5;cols=80")
                        .bind_value_to(self, "query")
                    )
                    ui.button("Run Query", on_click=lambda: self.on_run_query())
                else:
                    ui.label("No graph loaded. Please select a graph first.")
                with ui.row() as self.result_row:
                    self.result_html=ui.html()
            except Exception as ex:
                self.handle_exception(ex)

        await self.setup_content_div(setup_query)

    def load_graph(self,file=None,name:str="modern"):
        if file is None:
            if name in self.examples.get_names():
                graph=self.examples.get(name)
            else:
                raise ValueError(f"invalid graph name {name}")
            graph.name=name
        else:
            if file.name.endswith('.graphml'):
                temp_path = os.path.join(tempfile.gettempdir(), file.name)
                with open(temp_path, 'wb') as f:
                    f.write(file.read())
                graph = graphml_to_mogwaigraph(file=temp_path)
            elif file.name.endswith('.xlsx'):
                graph = EXCELGraph(file)
            elif file.name.endswith('.pdf'):
                graph = PDFGraph(file)
            elif file.name.endswith('.pptx'):
                graph = powerpoint_converter.PPGraph(file=file)
            else:
                raise ValueError(f"invalid file {file.name}")
            graph.name=file.name
        return graph

    def handle_upload(self, e):
        """Handle file upload"""
        file = e.content
        try:
            self.graph=self.load_graph(file)
        except Exception as ex:
            ui.notify(f"Unsupported file: {file.name} {str(ex)}", type="negative")
            return

        if self.graph:
            ui.notify("File parsed successfully", type="positive")

    def on_run_query(self, query:str=None):
        """Run a Gremlin query on the graph"""
        if not self.graph:
            ui.notify("No graph loaded. Please select a graph first.", type="warning")
            return
        try:
            if query is None:
                query=self.query
            query_result=self.run_query(query)
            self.display_result(query_result)
        except Exception as e:
            ui.notify(f"Error executing query: {str(e)}", type="negative")

    def run_query(self,query)->QueryResult:
        g = Trav.MogwaiGraphTraversalSource(self.graph)
        traversal = eval(query, {'g': g})
        if not traversal.terminated:
            traversal=traversal.to_list()
        result = traversal.run()
        qr=QueryResult(traversal=traversal,result=result)
        return qr

    def display_result(self,query_result:QueryResult):
        if self.result_html:
            with self.result_row:
                count=len(query_result.result)
                plural_postfix="s" if count>1 else ""
                markup=f"{count} element{plural_postfix}:<br>"
                for i,traverser in enumerate(query_result.result):
                    markup+=f"{i+1}:{str(traverser)}<br>"
                self.result_html.content=markup
__init__(webserver, client)

Initialize the solution

Parameters:

Name Type Description Default
webserver MogwaiWebServer

The webserver instance associated with this context.

required
client Client

The client instance this context is associated with.

required
Source code in mogwai/web/server.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(self, webserver: MogwaiWebServer, client: Client):
    """
    Initialize the solution

    Args:
        webserver (MogwaiWebServer): The webserver instance associated with this context.
        client (Client): The client instance this context is associated with.
    """
    super().__init__(webserver, client)
    self.examples=webserver.examples
    self.graph=None
    self.graph_label=None
    self.result_html=None
    self.update_graph("modern")
authenticated()

Check if the user is authenticated. Returns: True if the user is authenticated, False otherwise.

Source code in mogwai/web/server.py
89
90
91
92
93
94
95
def authenticated(self) -> bool:
    """
    Check if the user is authenticated.
    Returns:
        True if the user is authenticated, False otherwise.
    """
    return self.webserver.login.authenticated()
handle_upload(e)

Handle file upload

Source code in mogwai/web/server.py
198
199
200
201
202
203
204
205
206
207
208
def handle_upload(self, e):
    """Handle file upload"""
    file = e.content
    try:
        self.graph=self.load_graph(file)
    except Exception as ex:
        ui.notify(f"Unsupported file: {file.name} {str(ex)}", type="negative")
        return

    if self.graph:
        ui.notify("File parsed successfully", type="positive")
home() async

Provide the main content page

Source code in mogwai/web/server.py
115
116
117
async def home(self):
    """Provide the main content page"""
    await self.query_graph()
login_ui() async

login ui

Source code in mogwai/web/server.py
109
110
111
112
113
async def login_ui(self):
    """
    login ui
    """
    await self.webserver.login.login(self)
on_run_query(query=None)

Run a Gremlin query on the graph

Source code in mogwai/web/server.py
210
211
212
213
214
215
216
217
218
219
220
221
def on_run_query(self, query:str=None):
    """Run a Gremlin query on the graph"""
    if not self.graph:
        ui.notify("No graph loaded. Please select a graph first.", type="warning")
        return
    try:
        if query is None:
            query=self.query
        query_result=self.run_query(query)
        self.display_result(query_result)
    except Exception as e:
        ui.notify(f"Error executing query: {str(e)}", type="negative")
query_graph() async

Graph querying page

Source code in mogwai/web/server.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
async def query_graph(self):
    """Graph querying page"""
    def setup_query():
        emphasize="text-h5"
        try:
            with ui.row() as self.header_row:
                graph_selection=self.examples.get_names()
                self.graph_selector=self.add_select(
                    "graph",
                    graph_selection,
                    value=self.graph_name,
                    on_change=self.on_graph_select)
            if self.authenticated():
                with ui.row() as self.upload_row:
                    ui.label("import File").classes(emphasize)
                    file_upload = ui.upload(label="Choose a file", multiple=False, auto_upload=True)
                    file_upload.on('upload', self.handle_upload)

            if self.graph:
                self.get_graph_label()
                self.graph_label=ui.label().classes(emphasize)
                self.graph_label.bind_text_from(self, 'graph_label_text')
                self.query_text_area = (
                    ui.textarea("Enter Gremlin Query")
                    .props("clearable")
                    .props("rows=5;cols=80")
                    .bind_value_to(self, "query")
                )
                ui.button("Run Query", on_click=lambda: self.on_run_query())
            else:
                ui.label("No graph loaded. Please select a graph first.")
            with ui.row() as self.result_row:
                self.result_html=ui.html()
        except Exception as ex:
            self.handle_exception(ex)

    await self.setup_content_div(setup_query)
setup_menu(detailed=True)

setup the menu

Source code in mogwai/web/server.py
 97
 98
 99
100
101
102
103
104
105
106
107
def setup_menu(self, detailed: bool = True):
    """
    setup the menu
    """
    super().setup_menu(detailed=detailed)
    ui.button(icon="menu", on_click=lambda: self.header.toggle())
    with self.header:
        if self.authenticated():
            self.link_button("logout", "/logout", "logout", new_tab=False)
        else:
            self.link_button("login", "/login", "login", new_tab=False)

MogwaiWebServer

Bases: InputWebserver

Mogwai WebServer

Source code in mogwai/web/server.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class MogwaiWebServer(InputWebserver):
    """
    Mogwai WebServer
    """
    @classmethod
    def get_config(cls) -> WebserverConfig:
        copy_right = "(c)2024 Wolfgang Fahl"
        config = WebserverConfig(
            copy_right=copy_right,
            version=Version(),
            default_port=9850,
            short_name="mogwai",
        )
        server_config = WebserverConfig.get(config)
        server_config.solution_class = MogwaiSolution
        return server_config

    def __init__(self):
        """Constructs all the necessary attributes for the WebServer object."""
        InputWebserver.__init__(self, config=MogwaiWebServer.get_config())
        users = Users("~/.solutions/mogwai")
        self.login = Login(self, users)
        self.examples=Graphs()

        @ui.page("/")
        async def home(client: Client):
            return await self.page(client, MogwaiSolution.home)

        @ui.page("/query")
        async def query_graph(client: Client):
            return await self.page(client, MogwaiSolution.query_graph)

        @ui.page("/login")
        async def login(client: Client):
            return await self.page(client, MogwaiSolution.login_ui)

        @ui.page("/logout")
        async def logout(client: Client) -> RedirectResponse:
            if self.login.authenticated():
                await self.login.logout()
            return RedirectResponse("/")
__init__()

Constructs all the necessary attributes for the WebServer object.

Source code in mogwai/web/server.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def __init__(self):
    """Constructs all the necessary attributes for the WebServer object."""
    InputWebserver.__init__(self, config=MogwaiWebServer.get_config())
    users = Users("~/.solutions/mogwai")
    self.login = Login(self, users)
    self.examples=Graphs()

    @ui.page("/")
    async def home(client: Client):
        return await self.page(client, MogwaiSolution.home)

    @ui.page("/query")
    async def query_graph(client: Client):
        return await self.page(client, MogwaiSolution.query_graph)

    @ui.page("/login")
    async def login(client: Client):
        return await self.page(client, MogwaiSolution.login_ui)

    @ui.page("/logout")
    async def logout(client: Client) -> RedirectResponse:
        if self.login.authenticated():
            await self.login.logout()
        return RedirectResponse("/")