falkordb 1.0.0a0__tar.gz

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.
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.1
2
+ Name: FalkorDB
3
+ Version: 1.0.0a0
4
+ Summary: Python client for interacting with FalkorDB database
5
+ Home-page: http://falkordb-py.readthedocs.io
6
+ Keywords: FalkorDB,GraphDB,Cypher
7
+ Author: FalkorDB inc
8
+ Author-email: info@falkordb.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: redis (>=5.0.1,<6.0.0)
15
+ Project-URL: Repository, http://github.com/falkorDB/falkordb-py
16
+ Description-Content-Type: text/markdown
17
+
18
+ # FalkorDB
19
+
20
+ FalkorDB python client
21
+
22
+ see [docs](http://falkordb-py.readthedocs.io/)
23
+
24
+ ## Installation
25
+ ```sh
26
+ pip install FalkorDB
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Run FalkorDB instance
32
+ Docker:
33
+ ```sh
34
+ docker run --rm -p 6379:6379 falkordb/falkordb
35
+ ```
36
+ Or use our [sandbox](https://cloud.falkordb.com/sandbox)
37
+
38
+ ### Example
39
+ ```python
40
+ from falkordb import FalkorDB
41
+
42
+ # Connect to FalkorDB
43
+ db = FalkorDB(host='localhost', port=6379)
44
+
45
+ # Select the social graph
46
+ g = db.select_graph('social')
47
+
48
+ # Create 100 nodes and return a handful
49
+ nodes = g.query('UNWIND range(0, 100) AS i CREATE (n {v:1}) RETURN n LIMIT 10').result_set
50
+ for n in nodes:
51
+ print(n)
52
+ ```
53
+
@@ -0,0 +1,35 @@
1
+ # FalkorDB
2
+
3
+ FalkorDB python client
4
+
5
+ see [docs](http://falkordb-py.readthedocs.io/)
6
+
7
+ ## Installation
8
+ ```sh
9
+ pip install FalkorDB
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ### Run FalkorDB instance
15
+ Docker:
16
+ ```sh
17
+ docker run --rm -p 6379:6379 falkordb/falkordb
18
+ ```
19
+ Or use our [sandbox](https://cloud.falkordb.com/sandbox)
20
+
21
+ ### Example
22
+ ```python
23
+ from falkordb import FalkorDB
24
+
25
+ # Connect to FalkorDB
26
+ db = FalkorDB(host='localhost', port=6379)
27
+
28
+ # Select the social graph
29
+ g = db.select_graph('social')
30
+
31
+ # Create 100 nodes and return a handful
32
+ nodes = g.query('UNWIND range(0, 100) AS i CREATE (n {v:1}) RETURN n LIMIT 10').result_set
33
+ for n in nodes:
34
+ print(n)
35
+ ```
@@ -0,0 +1,6 @@
1
+ from .falkordb import FalkorDB
2
+ from .node import Node
3
+ from .edge import Edge
4
+ from .path import Path
5
+ from .graph import Graph
6
+ from .execution_plan import ExecutionPlan, Operation
@@ -0,0 +1,127 @@
1
+ from typing import Optional
2
+ from .node import Node
3
+ from .helpers import quote_string
4
+
5
+ class Edge:
6
+ """
7
+ An edge connecting two nodes.
8
+ """
9
+
10
+ def __init__(self, src_node: Node, relation: str, dest_node: Node,
11
+ edge_id: Optional[int] = None, alias: Optional[str] = '',
12
+ properties=None):
13
+ """
14
+ Create a new edge.
15
+
16
+ Args:
17
+ src_node: The source node of the edge.
18
+ relation: The relationship type of the edge.
19
+ dest_node: The destination node of the edge.
20
+ edge_id: The ID of the edge.
21
+ alias: An alias for the edge (default is empty string).
22
+ properties: The properties of the edge.
23
+
24
+ Raises:
25
+ AssertionError: If either src_node or dest_node is not provided.
26
+
27
+ Returns:
28
+ None
29
+ """
30
+ if src_node is None or dest_node is None:
31
+ raise AssertionError("Both src_node & dest_node must be provided")
32
+
33
+ self.id = edge_id
34
+ self.alias = alias
35
+ self.src_node = src_node
36
+ self.dest_node = dest_node
37
+ self.relation = relation
38
+ self.properties = properties or {}
39
+
40
+ def to_string(self) -> str:
41
+ """
42
+ Get a string representation of the edge's properties.
43
+
44
+ Returns:
45
+ str: A string representation of the edge's properties.
46
+ """
47
+ res = ""
48
+ if self.properties:
49
+ props = ",".join(
50
+ key + ":" + str(quote_string(val))
51
+ for key, val in sorted(self.properties.items())
52
+ )
53
+ res += "{" + props + "}"
54
+
55
+ return res
56
+
57
+ def __str__(self) -> str:
58
+ """
59
+ Get a string representation of the edge.
60
+
61
+ Returns:
62
+ str: A string representation of the edge.
63
+ """
64
+ # Source node
65
+ if isinstance(self.src_node, Node):
66
+ res = f"({self.src_node.alias})"
67
+ else:
68
+ res = "()"
69
+
70
+ # Edge
71
+ res += f"-[{self.alias}"
72
+ if self.relation:
73
+ res += ":" + self.relation
74
+ if self.properties:
75
+ props = ",".join(
76
+ key + ":" + str(quote_string(val))
77
+ for key, val in sorted(self.properties.items())
78
+ )
79
+ res += f"{{{props}}}"
80
+ res += "]->"
81
+
82
+ # Dest node
83
+ if isinstance(self.dest_node, Node):
84
+ res += f"({self.dest_node.alias})"
85
+ else:
86
+ res += "()"
87
+
88
+ return res
89
+
90
+ def __eq__(self, rhs) -> bool:
91
+ """
92
+ Check if two edges are equal.
93
+
94
+ Args:
95
+ rhs: The edge to compare.
96
+
97
+ Returns:
98
+ bool: True if the edges are equal, False otherwise.
99
+ """
100
+ # Type checking
101
+ if not isinstance(rhs, Edge):
102
+ return False
103
+
104
+ # Quick positive check, if both IDs are set
105
+ if self.id is not None and rhs.id is not None and self.id == rhs.id:
106
+ return True
107
+
108
+ # Source and destination nodes should match
109
+ if self.src_node != rhs.src_node:
110
+ return False
111
+
112
+ if self.dest_node != rhs.dest_node:
113
+ return False
114
+
115
+ # Relation should match
116
+ if self.relation != rhs.relation:
117
+ return False
118
+
119
+ # Quick check for the number of properties
120
+ if len(self.properties) != len(rhs.properties):
121
+ return False
122
+
123
+ # Compare properties
124
+ if self.properties != rhs.properties:
125
+ return False
126
+
127
+ return True
@@ -0,0 +1,15 @@
1
+ class SchemaVersionMismatchException(Exception):
2
+ """
3
+ Exception raised when the schema version of the database does not match the
4
+ version of the schema that the application expects.
5
+ """
6
+ def __init__(self, version: int):
7
+ """
8
+ Create a new SchemaVersionMismatchException.
9
+
10
+ Args:
11
+ version: The version of the schema that the application expects.
12
+
13
+ """
14
+
15
+ self.version = version
@@ -0,0 +1,324 @@
1
+ import re
2
+
3
+
4
+ class ProfileStats:
5
+ """
6
+ ProfileStats Class for representing runtime execution statistics of an operation.
7
+
8
+ Attributes:
9
+ records_produced (int): The number of records produced.
10
+ execution_time (float): The execution time in milliseconds.
11
+ """
12
+
13
+ def __init__(self, records_produced: int, execution_time: float):
14
+ """
15
+ Initializes a new ProfileStats instance with the given records_produced and execution_time.
16
+
17
+ Args:
18
+ records_produced (int): The number of records produced.
19
+ execution_time (float): The execution time in milliseconds.
20
+ """
21
+ self.execution_time = execution_time
22
+ self.records_produced = records_produced
23
+
24
+
25
+ class Operation:
26
+ """
27
+ Operation Class for representing a single operation within an execution plan.
28
+
29
+ Attributes:
30
+ name (str): The name of the operation.
31
+ args (str): Operation arguments.
32
+ children (list): List of child operations.
33
+ profile_stats (ProfileStats): Profile statistics for the operation.
34
+ """
35
+
36
+ def __init__(self, name: str, args=None, profile_stats: bool = None):
37
+ """
38
+ Creates a new Operation instance.
39
+
40
+ Args:
41
+ name (str): The name of the operation.
42
+ args (str, optional): Operation arguments.
43
+ profile_stats (ProfileStats, optional): Profile statistics for the operation.
44
+ """
45
+ self.name = name
46
+ self.args = args
47
+ self.children = []
48
+ self.profile_stats = profile_stats
49
+
50
+ @property
51
+ def execution_time(self) -> int:
52
+ """
53
+ returns operation's execution time in ms
54
+ """
55
+ return self.profile_stats.execution_time
56
+
57
+ @property
58
+ def records_produced(self) -> int:
59
+ """
60
+ returns number of records produced by operation.
61
+ """
62
+ return self.profile_stats.records_produced
63
+
64
+ def append_child(self, child):
65
+ """
66
+ Appends a child operation to the current operation.
67
+
68
+ Args:
69
+ child (Operation): The child operation to append.
70
+
71
+ Returns:
72
+ Operation: The updated operation instance.
73
+ """
74
+ if not isinstance(child, Operation):
75
+ raise Exception("child must be Operation")
76
+
77
+ self.children.append(child)
78
+ return self
79
+
80
+ def child_count(self) -> int:
81
+ """
82
+ Returns the number of child operations.
83
+
84
+ Returns:
85
+ int: Number of child operations.
86
+ """
87
+ return len(self.children)
88
+
89
+ def __eq__(self, o: object) -> bool:
90
+ """
91
+ Compares two Operation instances for equality based on their name and arguments.
92
+
93
+ Args:
94
+ o (object): Another Operation instance for comparison.
95
+
96
+ Returns:
97
+ bool: True if the operations are equal, False otherwise.
98
+ """
99
+ if not isinstance(o, Operation):
100
+ return False
101
+
102
+ return self.name == o.name and self.args == o.args
103
+
104
+ def __str__(self) -> str:
105
+ """
106
+ Returns a string representation of the operation.
107
+
108
+ Returns:
109
+ str: String representation of the operation.
110
+ """
111
+ args_str = "" if self.args is None else " | " + self.args
112
+ return f"{self.name}{args_str}"
113
+
114
+
115
+ class ExecutionPlan:
116
+ """
117
+ ExecutionPlan Class for representing a collection of operations.
118
+
119
+ Attributes:
120
+ plan (list): List of strings representing the collection of operations.
121
+ structured_plan (Operation): Root of the structured operation tree.
122
+ """
123
+
124
+ def __init__(self, plan):
125
+ """
126
+ Creates a new ExecutionPlan instance.
127
+
128
+ Args:
129
+ plan (list): List of strings representing the collection of operations.
130
+ """
131
+ if not isinstance(plan, list):
132
+ raise Exception("plan must be an array")
133
+
134
+ if isinstance(plan[0], bytes):
135
+ plan = [b.decode() for b in plan]
136
+
137
+ self.plan = plan
138
+ self.operations = {}
139
+ self.structured_plan = self._operation_tree()
140
+ for key in self.operations:
141
+ self.operations[key].reverse()
142
+
143
+ def collect_operations(self, op_name):
144
+ """
145
+ Collects all operations with specified name from plan
146
+
147
+ Args:
148
+ op_name (string): Name of operation to collect
149
+
150
+ Returns:
151
+ List[Operation]: All operations with the specified name
152
+ """
153
+ if op_name in self.operations:
154
+ return self.operations[op_name]
155
+ return []
156
+
157
+ ops = []
158
+
159
+ for op in self.operations:
160
+ if op.name == op_name:
161
+ ops.append(op)
162
+
163
+ return ops
164
+
165
+ def __compare_operations(self, root_a, root_b) -> bool:
166
+ """
167
+ Compares execution plan operation trees.
168
+
169
+ Returns:
170
+ bool: True if operation trees are equal, False otherwise.
171
+ """
172
+ # compare current root
173
+ if root_a != root_b:
174
+ return False
175
+
176
+ # make sure root have the same number of children
177
+ if root_a.child_count() != root_b.child_count():
178
+ return False
179
+
180
+ # recursively compare children
181
+ for i in range(root_a.child_count()):
182
+ if not self.__compare_operations(root_a.children[i], root_b.children[i]):
183
+ return False
184
+
185
+ return True
186
+
187
+ def __str__(self) -> str:
188
+ """
189
+ Returns a string representation of the execution plan.
190
+
191
+ Returns:
192
+ str: String representation of the execution plan.
193
+ """
194
+ def aggregate_str(str_children):
195
+ return "\n".join(
196
+ [
197
+ " " + line
198
+ for str_child in str_children
199
+ for line in str_child.splitlines()
200
+ ]
201
+ )
202
+
203
+ def combine_str(x, y):
204
+ return f"{x}\n{y}"
205
+
206
+ return self._operation_traverse(
207
+ self.structured_plan, str, aggregate_str, combine_str
208
+ )
209
+
210
+ def __eq__(self, o: object) -> bool:
211
+ """
212
+ Compares two execution plans.
213
+
214
+ Returns:
215
+ bool: True if the two plans are equal, False otherwise.
216
+ """
217
+ # make sure 'o' is an execution-plan
218
+ if not isinstance(o, ExecutionPlan):
219
+ return False
220
+
221
+ # get root for both plans
222
+ root_a = self.structured_plan
223
+ root_b = o.structured_plan
224
+
225
+ # compare execution trees
226
+ return self.__compare_operations(root_a, root_b)
227
+
228
+ def __iter__(self):
229
+ return iter(self.operations)
230
+
231
+ def _operation_traverse(self, op, op_f, aggregate_f, combine_f):
232
+ """
233
+ Traverses the operation tree recursively applying functions.
234
+
235
+ Args:
236
+ op: Operation to traverse.
237
+ op_f: Function applied for each operation.
238
+ aggregate_f: Aggregation function applied for all children of a single operation.
239
+ combine_f: Combine function applied for the operation result and the children result.
240
+ """
241
+ # apply op_f for each operation
242
+ op_res = op_f(op)
243
+ if len(op.children) == 0:
244
+ return op_res # no children return
245
+
246
+ # apply _operation_traverse recursively
247
+ children = [
248
+ self._operation_traverse(child, op_f, aggregate_f, combine_f)
249
+ for child in op.children
250
+ ]
251
+ # combine the operation result with the children aggregated result
252
+ return combine_f(op_res, aggregate_f(children))
253
+
254
+ def _operation_tree(self):
255
+ """
256
+ Builds the operation tree from the string representation.
257
+
258
+ Returns:
259
+ Operation: Root of the structured operation tree.
260
+ """
261
+ # initial state
262
+ i = 0
263
+ level = 0
264
+ stack = []
265
+ current = None
266
+
267
+ def create_operation(args):
268
+ profile_stats = None
269
+ name = args[0].strip()
270
+ args.pop(0)
271
+ if len(args) > 0 and "Records produced" in args[-1]:
272
+ records_produced = int(
273
+ re.search("Records produced: (\\d+)", args[-1]).group(1)
274
+ )
275
+ execution_time = float(
276
+ re.search("Execution time: (\\d+.\\d+) ms", args[-1]).group(1)
277
+ )
278
+ profile_stats = ProfileStats(records_produced, execution_time)
279
+ args.pop(-1)
280
+ return Operation(
281
+ name, None if len(args) == 0 else args[0].strip(), profile_stats
282
+ )
283
+
284
+ # iterate plan operations
285
+ while i < len(self.plan):
286
+ current_op = self.plan[i]
287
+ op_level = current_op.count(" ")
288
+ if op_level == level:
289
+ # if the operation level equal to the current level
290
+ # set the current operation and move next
291
+ child = create_operation(current_op.split("|"))
292
+ if child.name not in self.operations:
293
+ self.operations[child.name] = []
294
+ self.operations[child.name].append(child)
295
+
296
+ if current:
297
+ current = stack.pop()
298
+ current.append_child(child)
299
+ current = child
300
+ i += 1
301
+ stack.append(child)
302
+ elif op_level == level + 1:
303
+ # if the operation is child of the current operation
304
+ # add it as child and set as current operation
305
+ child = create_operation(current_op.split("|"))
306
+ if child.name not in self.operations:
307
+ self.operations[child.name] = []
308
+ self.operations[child.name].append(child)
309
+
310
+ current.append_child(child)
311
+ stack.append(current)
312
+ current = child
313
+ level += 1
314
+ i += 1
315
+ elif op_level < level:
316
+ # if the operation is not child of current operation
317
+ # go back to it's parent operation
318
+ levels_back = level - op_level + 1
319
+ for _ in range(levels_back):
320
+ current = stack.pop()
321
+ level -= levels_back
322
+ else:
323
+ raise Exception("corrupted plan")
324
+ return stack[0]