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.
- falkordb-1.0.0a0/PKG-INFO +53 -0
- falkordb-1.0.0a0/README.md +35 -0
- falkordb-1.0.0a0/falkordb/__init__.py +6 -0
- falkordb-1.0.0a0/falkordb/edge.py +127 -0
- falkordb-1.0.0a0/falkordb/exceptions.py +15 -0
- falkordb-1.0.0a0/falkordb/execution_plan.py +324 -0
- falkordb-1.0.0a0/falkordb/falkordb.py +193 -0
- falkordb-1.0.0a0/falkordb/graph.py +525 -0
- falkordb-1.0.0a0/falkordb/graph_schema.py +156 -0
- falkordb-1.0.0a0/falkordb/helpers.py +47 -0
- falkordb-1.0.0a0/falkordb/node.py +105 -0
- falkordb-1.0.0a0/falkordb/path.py +151 -0
- falkordb-1.0.0a0/falkordb/query_result.py +540 -0
- falkordb-1.0.0a0/pyproject.toml +23 -0
|
@@ -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,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]
|