fromscratch-pygraphdb 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pygraphdb/node.py ADDED
@@ -0,0 +1,149 @@
1
+ """Node -- a single entity in a graph database.
2
+
3
+ Imagine a party where every guest wears a name tag. Each person (node)
4
+ has a unique ID (like their phone number), a label (like "Student" or
5
+ "Teacher"), and properties (like their name, age, or favorite color).
6
+
7
+ A node is the building block of a graph. By itself it's just a dot on
8
+ a whiteboard -- but connect it to other nodes with edges, and suddenly
9
+ you have a map of relationships!
10
+
11
+ Example:
12
+ >>> from pygraphdb.node import Node
13
+ >>> alice = Node(node_id="1", label="Person", properties={"name": "Alice", "age": 13})
14
+ >>> alice.label
15
+ 'Person'
16
+ >>> alice.properties["name"]
17
+ 'Alice'
18
+
19
+ """
20
+
21
+ from dataclasses import dataclass, field
22
+
23
+
24
+ @dataclass
25
+ class Node:
26
+ """A single entity in the graph with an ID, label, and properties.
27
+
28
+ Think of a node like a person at a party:
29
+ - ``node_id`` is their unique wristband number (no two people share one,
30
+ and it can't be changed once it's on)
31
+ - ``label`` is the group they belong to ("Student", "Teacher", "Parent")
32
+ - ``properties`` is everything about them (name, age, hobbies)
33
+
34
+ Args:
35
+ node_id: A unique identifier for this node.
36
+ label: A category or type label (e.g. "Person", "City").
37
+ properties: A dictionary of key-value attributes.
38
+
39
+ """
40
+
41
+ node_id: str
42
+ label: str = ""
43
+ properties: dict[str, object] = field(default_factory=lambda: {})
44
+
45
+ def __setattr__(self, name: str, value: object) -> None:
46
+ """Set an attribute, except ``node_id`` once it has been set.
47
+
48
+ The graph files every node under its id. Changing the id afterwards
49
+ would leave the node filed under the old one, like swapping your
50
+ wristband after the guest list was written -- so it isn't allowed.
51
+
52
+ Raises:
53
+ AttributeError: If ``node_id`` is set a second time.
54
+
55
+ """
56
+ if name == "node_id" and "node_id" in self.__dict__:
57
+ msg = "a node's node_id can't be changed: it files the node in the graph"
58
+ raise AttributeError(msg)
59
+ super().__setattr__(name, value)
60
+
61
+ def has_property(self, key: str) -> bool:
62
+ """Check whether this node has a given property key.
63
+
64
+ Args:
65
+ key: The property name to look for.
66
+
67
+ Returns:
68
+ True if the property exists, False otherwise.
69
+
70
+ """
71
+ return key in self.properties
72
+
73
+ def get_property(self, key: str, default: object = None) -> object:
74
+ """Get a property value by key, with an optional default.
75
+
76
+ Args:
77
+ key: The property name to retrieve.
78
+ default: Value to return if the key is missing.
79
+
80
+ Returns:
81
+ The property value, or the default.
82
+
83
+ """
84
+ return self.properties.get(key, default)
85
+
86
+ def set_property(self, key: str, value: object) -> None:
87
+ """Set a property on this node.
88
+
89
+ Args:
90
+ key: The property name.
91
+ value: The property value.
92
+
93
+ """
94
+ self.properties[key] = value
95
+
96
+ def remove_property(self, key: str) -> object:
97
+ """Remove and return a property from this node.
98
+
99
+ Args:
100
+ key: The property name to remove.
101
+
102
+ Returns:
103
+ The removed value.
104
+
105
+ Raises:
106
+ KeyError: If the property does not exist.
107
+
108
+ """
109
+ return self.properties.pop(key)
110
+
111
+ def to_dict(self) -> dict[str, object]:
112
+ """Write everything about this node onto an index card.
113
+
114
+ Like copying a party guest's wristband number, group, and details
115
+ onto a card you can file away -- turns the node into a plain
116
+ dictionary you can save to a file.
117
+
118
+ Returns:
119
+ A dictionary representation of this node.
120
+
121
+ """
122
+ return {
123
+ "node_id": self.node_id,
124
+ "label": self.label,
125
+ "properties": dict(self.properties),
126
+ }
127
+
128
+ @classmethod
129
+ def from_dict(cls, data: dict[str, object]) -> Node:
130
+ """Rebuild a node from the index card that to_dict wrote.
131
+
132
+ Like reading a filed-away card and bringing that party guest back --
133
+ turns a plain dictionary back into a real Node.
134
+
135
+ Args:
136
+ data: A dictionary with node_id, label, and properties keys.
137
+
138
+ Returns:
139
+ A new Node instance.
140
+
141
+ """
142
+ node_id = str(data.get("node_id", ""))
143
+ label = str(data.get("label", ""))
144
+ raw_props = data.get("properties")
145
+ props: dict[str, object] = {}
146
+ if isinstance(raw_props, dict):
147
+ for k, v in raw_props.items(): # pyright: ignore[reportUnknownVariableType]
148
+ props[str(k)] = v # pyright: ignore[reportUnknownArgumentType]
149
+ return cls(node_id=node_id, label=label, properties=props)
@@ -0,0 +1,147 @@
1
+ """Shortest path finding -- like GPS directions for your graph.
2
+
3
+ When you ask Google Maps for directions, it finds the route with the
4
+ fewest turns (or shortest distance). Our pathfinding does the same
5
+ thing for graphs -- it finds the path with the fewest edges between
6
+ two nodes.
7
+
8
+ We use BFS (breadth-first search) because it naturally finds the
9
+ shortest path in an unweighted graph. It explores nodes layer by
10
+ layer, so the first time it reaches the destination, that path is
11
+ guaranteed to be the shortest!
12
+
13
+ Example:
14
+ >>> from pygraphdb.graph import Graph
15
+ >>> from pygraphdb.pathfinding import shortest_path
16
+ >>> g = Graph()
17
+ >>> for nid in ["a", "b", "c", "d"]:
18
+ ... g.add_node(nid)
19
+ >>> g.add_edge("e1", "a", "b")
20
+ >>> g.add_edge("e2", "b", "c")
21
+ >>> g.add_edge("e3", "a", "d")
22
+ >>> g.add_edge("e4", "d", "c")
23
+ >>> path = shortest_path(g, "a", "c")
24
+ >>> [n.node_id for n in path]
25
+ ['a', 'b', 'c']
26
+
27
+ """
28
+
29
+ from collections import deque
30
+
31
+ from pygraphdb.graph import Graph
32
+ from pygraphdb.node import Node
33
+
34
+
35
+ def shortest_path(graph: Graph, start_id: str, end_id: str) -> list[Node]:
36
+ """Find the shortest path between two nodes using BFS.
37
+
38
+ Like asking for GPS directions: find the route with the fewest
39
+ stops between your starting point and your destination.
40
+
41
+ Args:
42
+ graph: The graph to search.
43
+ start_id: The ID of the starting node.
44
+ end_id: The ID of the destination node.
45
+
46
+ Returns:
47
+ A list of nodes forming the shortest path (including start and end).
48
+ An empty list if no path exists.
49
+
50
+ Raises:
51
+ NodeNotFoundError: If start or end node does not exist.
52
+
53
+ """
54
+ graph.get_node(start_id) # Validate existence
55
+ graph.get_node(end_id) # Validate existence
56
+
57
+ if start_id == end_id:
58
+ return [graph.get_node(start_id)]
59
+
60
+ visited: set[str] = {start_id}
61
+ # Each queue entry is (current_node_id, path_so_far)
62
+ queue: deque[tuple[str, list[str]]] = deque([(start_id, [start_id])])
63
+
64
+ while queue:
65
+ current_id, path = queue.popleft()
66
+ for edge in graph.get_outgoing_edges(current_id):
67
+ neighbor_id = edge.target_id
68
+ if neighbor_id == end_id:
69
+ full_path = [*path, neighbor_id]
70
+ return [graph.get_node(nid) for nid in full_path]
71
+ if neighbor_id not in visited:
72
+ visited.add(neighbor_id)
73
+ queue.append((neighbor_id, [*path, neighbor_id]))
74
+
75
+ return []
76
+
77
+
78
+ def all_paths(
79
+ graph: Graph,
80
+ start_id: str,
81
+ end_id: str,
82
+ *,
83
+ max_depth: int = 10,
84
+ ) -> list[list[Node]]:
85
+ """Find all paths between two nodes up to a maximum depth.
86
+
87
+ Like finding every possible route between your house and school,
88
+ not just the shortest one.
89
+
90
+ Args:
91
+ graph: The graph to search.
92
+ start_id: The ID of the starting node.
93
+ end_id: The ID of the destination node.
94
+ max_depth: Maximum path length to consider.
95
+
96
+ Returns:
97
+ A list of paths, where each path is a list of nodes.
98
+
99
+ Raises:
100
+ NodeNotFoundError: If start or end node does not exist.
101
+
102
+ """
103
+ graph.get_node(start_id) # Validate existence
104
+ graph.get_node(end_id) # Validate existence
105
+
106
+ results: list[list[str]] = []
107
+ _all_paths_helper(graph, start_id, end_id, [start_id], set(), results, max_depth)
108
+ return [[graph.get_node(nid) for nid in path] for path in results]
109
+
110
+
111
+ def _all_paths_helper(
112
+ graph: Graph,
113
+ current_id: str,
114
+ end_id: str,
115
+ current_path: list[str],
116
+ visited: set[str],
117
+ results: list[list[str]],
118
+ max_depth: int,
119
+ ) -> None:
120
+ """Recursive helper for finding all paths.
121
+
122
+ Args:
123
+ graph: The graph to search.
124
+ current_id: Current position in the traversal.
125
+ end_id: The destination node ID.
126
+ current_path: Path taken so far.
127
+ visited: Set of visited node IDs.
128
+ results: Accumulator for found paths.
129
+ max_depth: Maximum remaining depth.
130
+
131
+ """
132
+ if current_id == end_id:
133
+ results.append(list(current_path))
134
+ return
135
+
136
+ if max_depth <= 0:
137
+ return
138
+
139
+ visited.add(current_id)
140
+ for edge in graph.get_outgoing_edges(current_id):
141
+ if edge.target_id not in visited:
142
+ current_path.append(edge.target_id)
143
+ _all_paths_helper(
144
+ graph, edge.target_id, end_id, current_path, visited, results, max_depth - 1
145
+ )
146
+ current_path.pop()
147
+ visited.discard(current_id)
@@ -0,0 +1,129 @@
1
+ """Persistence -- save and load graphs to/from JSON files.
2
+
3
+ Think of persistence like saving a video game. When you're done
4
+ playing, you save your progress to a file. Next time you start
5
+ the game, you load that file and pick up exactly where you left off.
6
+
7
+ Our graph database works the same way: you can save your entire
8
+ graph (all nodes and edges) to a JSON file, and later load it
9
+ back into memory to continue working with it.
10
+
11
+ JSON is a text format that both humans and computers can read --
12
+ it looks like a nicely organized list with curly braces and colons.
13
+
14
+ Example:
15
+ >>> from pygraphdb.graph import Graph
16
+ >>> from pygraphdb.persistence import save_graph, load_graph
17
+ >>> g = Graph()
18
+ >>> g.add_node("alice", label="Person")
19
+ >>> save_graph(g, "/tmp/my_graph.json")
20
+ >>> g2 = load_graph("/tmp/my_graph.json")
21
+ >>> g2.get_node("alice").label
22
+ 'Person'
23
+
24
+ """
25
+
26
+ import json
27
+ from pathlib import Path
28
+ from typing import cast
29
+
30
+ from pygraphdb.atomic import atomic_write_text
31
+ from pygraphdb.errors import GraphError, PersistenceError
32
+ from pygraphdb.graph import Graph
33
+
34
+ # Every real save file has these top-level keys; anything else isn't one of ours.
35
+ _REQUIRED_KEYS = frozenset({"nodes", "edges"})
36
+
37
+
38
+ def save_graph(graph: Graph, file_path: str | Path) -> None:
39
+ """Save a graph to a JSON file.
40
+
41
+ Like saving your game progress -- writes all nodes and edges
42
+ to a file so you can load them later.
43
+
44
+ Args:
45
+ graph: The graph to save.
46
+ file_path: The path to write the JSON file to.
47
+
48
+ """
49
+ path = Path(file_path)
50
+ data = graph.to_dict()
51
+ atomic_write_text(path, json.dumps(data, indent=2, default=str))
52
+
53
+
54
+ def load_graph(file_path: str | Path) -> Graph:
55
+ """Load a graph from a JSON file.
56
+
57
+ Like loading a saved game -- reads nodes and edges from a file
58
+ and rebuilds the graph in memory.
59
+
60
+ Args:
61
+ file_path: The path to read the JSON file from.
62
+
63
+ Returns:
64
+ A new Graph populated with the saved data.
65
+
66
+ Raises:
67
+ PersistenceError: If the file is missing, unreadable, not valid JSON,
68
+ or not a valid save file.
69
+
70
+ """
71
+ path = Path(file_path)
72
+ try:
73
+ raw = path.read_text(encoding="utf-8")
74
+ # A file that isn't UTF-8 text (an image, a zip, a corrupted save) fails
75
+ # while decoding, not while reading, so it needs catching too.
76
+ except (OSError, UnicodeDecodeError) as exc:
77
+ msg = f"Could not read {path}: {exc}"
78
+ raise PersistenceError(msg) from exc
79
+ try:
80
+ data: object = json.loads(raw)
81
+ # JSONDecodeError is a kind of ValueError; a plain ValueError also means a
82
+ # number too long for Python to read, and RecursionError means lists or
83
+ # objects nested too deep. Each one is a file we can't load.
84
+ except (ValueError, RecursionError) as exc:
85
+ msg = f"Invalid JSON in {path}: {exc}"
86
+ raise PersistenceError(msg) from exc
87
+ if not isinstance(data, dict):
88
+ msg = f"{path} is not a valid save file: the top level must be an object"
89
+ raise PersistenceError(msg)
90
+ saved: dict[str, object] = {str(k): v for k, v in cast("dict[object, object]", data).items()}
91
+ missing = _REQUIRED_KEYS - saved.keys()
92
+ if missing:
93
+ msg = f"{path} is not a valid save file: missing {sorted(missing)}"
94
+ raise PersistenceError(msg)
95
+ try:
96
+ return Graph.from_dict(saved)
97
+ # GraphError covers an edge whose end isn't one of the saved nodes, and two
98
+ # nodes or edges sharing an id: files save_graph never writes, so they're
99
+ # refused like any other bad file.
100
+ except (GraphError, KeyError, TypeError, ValueError, AttributeError) as exc:
101
+ msg = f"{path} is not a valid save file: {exc}"
102
+ raise PersistenceError(msg) from exc
103
+
104
+
105
+ def graph_to_json(graph: Graph) -> str:
106
+ """Serialize a graph to a JSON string (without writing to disk).
107
+
108
+ Args:
109
+ graph: The graph to serialize.
110
+
111
+ Returns:
112
+ A JSON string representation of the graph.
113
+
114
+ """
115
+ return json.dumps(graph.to_dict(), indent=2, default=str)
116
+
117
+
118
+ def graph_from_json(json_string: str) -> Graph:
119
+ """Deserialize a graph from a JSON string.
120
+
121
+ Args:
122
+ json_string: A JSON string representing a graph.
123
+
124
+ Returns:
125
+ A new Graph populated from the JSON data.
126
+
127
+ """
128
+ data: dict[str, object] = json.loads(json_string)
129
+ return Graph.from_dict(data)
pygraphdb/py.typed ADDED
File without changes
pygraphdb/query.py ADDED
@@ -0,0 +1,241 @@
1
+ """Simple Cypher-like query language parser and executor.
2
+
3
+ Real graph databases like Neo4j use a language called Cypher to ask
4
+ questions about the graph. It's like SQL but for graphs! Our version
5
+ is simplified but follows the same idea.
6
+
7
+ Think of it like asking questions in a specific format:
8
+ - "MATCH" means "find me..."
9
+ - "(n:Person)" means "a node labeled Person, call it n"
10
+ - "-[:KNOWS]->" means "connected by a KNOWS relationship"
11
+ - "WHERE n.name = 'Alice'" means "but only if the name is Alice"
12
+ - "RETURN m" means "give me back the thing called m"
13
+
14
+ Supported query format:
15
+ MATCH (n:Label)-[:REL_TYPE]->(m) WHERE n.prop = "value" RETURN m
16
+ MATCH (n:Label) RETURN n
17
+ MATCH (n:Label) WHERE n.prop = "value" RETURN n
18
+
19
+ Example:
20
+ >>> from pygraphdb.graph import Graph
21
+ >>> from pygraphdb.query import execute_query
22
+ >>> g = Graph()
23
+ >>> g.add_node("1", label="Person", properties={"name": "Alice"})
24
+ >>> g.add_node("2", label="Person", properties={"name": "Bob"})
25
+ >>> g.add_edge("e1", "1", "2", rel_type="KNOWS")
26
+ >>> results = execute_query(g, 'MATCH (n:Person) WHERE n.name = "Alice" RETURN n')
27
+ >>> len(results)
28
+ 1
29
+
30
+ """
31
+
32
+ import re
33
+
34
+ from pygraphdb.errors import QuerySyntaxError
35
+ from pygraphdb.graph import Graph
36
+ from pygraphdb.node import Node
37
+
38
+ # Pattern for: MATCH (var:Label)-[:REL]->(var2) WHERE var.prop = "val" RETURN var2
39
+ _PATTERN_WITH_EDGE = re.compile(
40
+ r"MATCH\s+\((\w+):(\w+)\)" # (n:Label)
41
+ r"\s*-\[:(\w+)\]->\s*" # -[:REL]->
42
+ r"\((\w+)\)" # (m)
43
+ r"(?:\s+WHERE\s+(.+?))?" # optional WHERE clause
44
+ r"\s+RETURN\s+(\w+)", # RETURN var
45
+ re.IGNORECASE,
46
+ )
47
+
48
+ # Pattern for: MATCH (var:Label) WHERE var.prop = "val" RETURN var
49
+ _PATTERN_NODE_ONLY = re.compile(
50
+ r"MATCH\s+\((\w+):(\w+)\)" # (n:Label)
51
+ r"(?:\s+WHERE\s+(.+?))?" # optional WHERE clause
52
+ r"\s+RETURN\s+(\w+)", # RETURN var
53
+ re.IGNORECASE,
54
+ )
55
+
56
+ # Pattern for WHERE conditions: var.prop = "value" or var.prop = 'value'
57
+ _WHERE_CONDITION = re.compile(
58
+ r"(\w+)\.(\w+)\s*=\s*[\"']([^\"']*)[\"']",
59
+ )
60
+
61
+
62
+ def execute_query(graph: Graph, query: str) -> list[Node]:
63
+ """Execute a simple Cypher-like query against the graph.
64
+
65
+ Parse and run a query string to find matching nodes.
66
+
67
+ Supported patterns:
68
+ - ``MATCH (n:Label) RETURN n``
69
+ - ``MATCH (n:Label) WHERE n.prop = "value" RETURN n``
70
+ - ``MATCH (n:Label)-[:REL]->(m) RETURN m``
71
+ - ``MATCH (n:Label)-[:REL]->(m) WHERE n.prop = "value" RETURN m``
72
+
73
+ Args:
74
+ graph: The graph to query.
75
+ query: A Cypher-like query string.
76
+
77
+ Returns:
78
+ A list of nodes matching the query.
79
+
80
+ Raises:
81
+ QuerySyntaxError: If the query cannot be parsed.
82
+
83
+ """
84
+ query = query.strip()
85
+
86
+ # Try edge pattern first (more specific)
87
+ match = _PATTERN_WITH_EDGE.match(query)
88
+ if match:
89
+ return _execute_edge_query(graph, match)
90
+
91
+ # Try node-only pattern
92
+ match = _PATTERN_NODE_ONLY.match(query)
93
+ if match:
94
+ return _execute_node_query(graph, match)
95
+
96
+ msg = f"Cannot parse query: {query}"
97
+ raise QuerySyntaxError(msg)
98
+
99
+
100
+ def _execute_node_query(graph: Graph, match: re.Match[str]) -> list[Node]:
101
+ """Execute a node-only query (no edge pattern).
102
+
103
+ Args:
104
+ graph: The graph to query.
105
+ match: The regex match object.
106
+
107
+ Returns:
108
+ A list of matching nodes.
109
+
110
+ """
111
+ var_name = match.group(1)
112
+ label = match.group(2)
113
+ where_clause = match.group(3)
114
+ return_var = match.group(4)
115
+
116
+ if return_var != var_name:
117
+ msg = f"RETURN variable '{return_var}' not defined in MATCH"
118
+ raise QuerySyntaxError(msg)
119
+
120
+ conditions = _parse_conditions(where_clause, {var_name})
121
+
122
+ results: list[Node] = []
123
+ for node in graph.get_all_nodes():
124
+ if node.label != label:
125
+ continue
126
+ if not _node_matches(node, var_name, conditions):
127
+ continue
128
+ results.append(node)
129
+ return results
130
+
131
+
132
+ def _execute_edge_query(graph: Graph, match: re.Match[str]) -> list[Node]:
133
+ """Execute a query with an edge pattern.
134
+
135
+ Args:
136
+ graph: The graph to query.
137
+ match: The regex match object.
138
+
139
+ Returns:
140
+ A list of matching nodes.
141
+
142
+ """
143
+ source_var = match.group(1)
144
+ source_label = match.group(2)
145
+ rel_type = match.group(3)
146
+ target_var = match.group(4)
147
+ where_clause = match.group(5)
148
+ return_var = match.group(6)
149
+
150
+ if return_var not in (source_var, target_var):
151
+ msg = f"RETURN variable '{return_var}' not defined in MATCH"
152
+ raise QuerySyntaxError(msg)
153
+
154
+ # WHERE conditions may reference either the source or the target variable.
155
+ conditions = _parse_conditions(where_clause, {source_var, target_var})
156
+
157
+ results: list[Node] = []
158
+ seen_ids: set[str] = set()
159
+
160
+ for node in graph.get_all_nodes():
161
+ if node.label != source_label:
162
+ continue
163
+ if not _node_matches(node, source_var, conditions):
164
+ continue
165
+
166
+ for edge in graph.get_outgoing_edges(node.node_id):
167
+ if edge.rel_type != rel_type:
168
+ continue
169
+ target = graph.get_node(edge.target_id)
170
+ if not _node_matches(target, target_var, conditions):
171
+ continue
172
+
173
+ if return_var == source_var:
174
+ # One matching edge is enough to keep the source node.
175
+ if node.node_id not in seen_ids:
176
+ seen_ids.add(node.node_id)
177
+ results.append(node)
178
+ break
179
+ if target.node_id not in seen_ids:
180
+ seen_ids.add(target.node_id)
181
+ results.append(target)
182
+
183
+ return results
184
+
185
+
186
+ def _parse_conditions(
187
+ where_clause: str | None,
188
+ valid_vars: set[str],
189
+ ) -> list[tuple[str, str, str]]:
190
+ """Turn a raw WHERE clause into a list of (var, prop, value) conditions.
191
+
192
+ Args:
193
+ where_clause: The raw WHERE clause string, or None if there was no WHERE.
194
+ valid_vars: The variable names the query actually defines.
195
+
196
+ Returns:
197
+ A list of parsed conditions (empty if there was no WHERE clause).
198
+
199
+ Raises:
200
+ QuerySyntaxError: If a WHERE clause is present but nothing parses
201
+ (for example an unquoted value), or a condition refers to a
202
+ variable the query never defined.
203
+
204
+ """
205
+ if where_clause is None:
206
+ return []
207
+ conditions: list[tuple[str, str, str]] = _WHERE_CONDITION.findall(where_clause)
208
+ if not conditions:
209
+ msg = (
210
+ f"Cannot understand WHERE clause: {where_clause!r} "
211
+ '(values must be quoted, e.g. n.name = "Alice")'
212
+ )
213
+ raise QuerySyntaxError(msg)
214
+ for cond_var, _prop_name, _prop_value in conditions:
215
+ if cond_var not in valid_vars:
216
+ msg = f"WHERE refers to unknown variable '{cond_var}'"
217
+ raise QuerySyntaxError(msg)
218
+ return conditions
219
+
220
+
221
+ def _node_matches(node: Node, var_name: str, conditions: list[tuple[str, str, str]]) -> bool:
222
+ """Check whether a node satisfies the conditions aimed at its variable.
223
+
224
+ Only conditions whose variable equals ``var_name`` apply here; conditions
225
+ for other variables are checked against their own nodes elsewhere.
226
+
227
+ Args:
228
+ node: The node to test.
229
+ var_name: The variable this node is bound to.
230
+ conditions: All parsed WHERE conditions.
231
+
232
+ Returns:
233
+ True if the node satisfies every condition aimed at ``var_name``.
234
+
235
+ """
236
+ for cond_var, prop_name, prop_value in conditions:
237
+ if cond_var != var_name:
238
+ continue
239
+ if str(node.get_property(prop_name)) != prop_value:
240
+ return False
241
+ return True