coffy 0.1.4__py3-none-any.whl → 0.1.6__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.
- coffy/__init__.py +1 -1
- coffy/__pycache__/__init__.cpython-312.pyc +0 -0
- coffy/graph/__init__.py +3 -1
- coffy/graph/__pycache__/__init__.cpython-312.pyc +0 -0
- coffy/graph/__pycache__/graphdb_nx.cpython-312.pyc +0 -0
- coffy/graph/graphdb_nx.py +335 -51
- coffy/nosql/__init__.py +4 -0
- coffy/nosql/__pycache__/__init__.cpython-312.pyc +0 -0
- coffy/nosql/__pycache__/engine.cpython-312.pyc +0 -0
- coffy/nosql/engine.py +396 -33
- coffy/sql/__init__.py +7 -0
- coffy/sql/__pycache__/__init__.cpython-312.pyc +0 -0
- coffy/sql/__pycache__/engine.cpython-312.pyc +0 -0
- coffy/sql/__pycache__/sqldict.cpython-312.pyc +0 -0
- coffy/sql/engine.py +30 -3
- coffy/sql/sqldict.py +53 -13
- {coffy-0.1.4.dist-info → coffy-0.1.6.dist-info}/METADATA +6 -4
- coffy-0.1.6.dist-info/RECORD +28 -0
- coffy/graph/graph_tests.py +0 -137
- coffy/nosql/nosql_tests.py +0 -104
- coffy-0.1.4.dist-info/RECORD +0 -30
- {coffy-0.1.4.dist-info → coffy-0.1.6.dist-info}/WHEEL +0 -0
- {coffy-0.1.4.dist-info → coffy-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {coffy-0.1.4.dist-info → coffy-0.1.6.dist-info}/top_level.txt +0 -0
coffy/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
# coffy/__init__.py
|
2
|
-
# author: nsarathy
|
2
|
+
# author: nsarathy
|
Binary file
|
coffy/graph/__init__.py
CHANGED
Binary file
|
Binary file
|
coffy/graph/graphdb_nx.py
CHANGED
@@ -1,13 +1,28 @@
|
|
1
1
|
# coffy/graph/graphdb_nx.py
|
2
2
|
# author: nsarathy
|
3
3
|
|
4
|
-
|
4
|
+
"""
|
5
|
+
A simple graph database using NetworkX.
|
6
|
+
"""
|
7
|
+
|
5
8
|
import json
|
9
|
+
import networkx as nx
|
6
10
|
import os
|
7
11
|
|
12
|
+
|
8
13
|
class GraphDB:
|
14
|
+
"""
|
15
|
+
A class to represent a graph database.
|
16
|
+
"""
|
9
17
|
|
10
18
|
def __init__(self, directed=False, path=None):
|
19
|
+
"""
|
20
|
+
Initialize a GraphDB instance.
|
21
|
+
directed -- Whether the graph is directed or not.
|
22
|
+
path -- Path to the JSON file where the graph will be stored.
|
23
|
+
If path is ":memory:" or None, the graph will be in-memory only.
|
24
|
+
If path is provided, it must end with ".json".
|
25
|
+
"""
|
11
26
|
self.g = nx.DiGraph() if directed else nx.Graph()
|
12
27
|
self.directed = directed
|
13
28
|
self.in_memory = path == ":memory:"
|
@@ -24,89 +39,186 @@ class GraphDB:
|
|
24
39
|
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
25
40
|
self.save(self.path)
|
26
41
|
|
27
|
-
|
28
42
|
# Node operations
|
43
|
+
|
29
44
|
def add_node(self, node_id, labels=None, **attrs):
|
45
|
+
"""
|
46
|
+
Add a node to the graph.
|
47
|
+
node_id -- Unique identifier for the node.
|
48
|
+
labels -- Optional list of labels for the node.
|
49
|
+
attrs -- Additional attributes for the node.
|
50
|
+
"""
|
51
|
+
if self.has_node(node_id):
|
52
|
+
raise KeyError(
|
53
|
+
f"Node '{node_id}' already exists. Use update_node to modify it."
|
54
|
+
)
|
30
55
|
if labels is not None:
|
31
56
|
attrs["_labels"] = labels if isinstance(labels, list) else [labels]
|
32
57
|
self.g.add_node(node_id, **attrs)
|
33
58
|
self._persist()
|
34
59
|
|
35
60
|
def add_nodes(self, nodes):
|
61
|
+
"""
|
62
|
+
Add multiple nodes to the graph.
|
63
|
+
nodes -- List of dictionaries, each representing a node.
|
64
|
+
"""
|
36
65
|
for node in nodes:
|
37
66
|
node_id = node["id"]
|
38
67
|
labels = node.get("labels") or node.get("_labels") # Accept either form
|
39
|
-
attrs = {
|
68
|
+
attrs = {
|
69
|
+
k: v for k, v in node.items() if k not in ["id", "labels", "_labels"]
|
70
|
+
}
|
40
71
|
self.add_node(node_id, labels=labels, **attrs)
|
41
72
|
|
42
73
|
def get_node(self, node_id):
|
74
|
+
"""
|
75
|
+
Get a node from the graph.
|
76
|
+
node_id -- Unique identifier for the node.
|
77
|
+
Returns the node's attributes as a dictionary.
|
78
|
+
"""
|
43
79
|
return self.g.nodes[node_id]
|
44
80
|
|
45
81
|
def _get_neighbors(self, node_id, direction):
|
82
|
+
"""
|
83
|
+
Get the neighbors of a node in the graph.
|
84
|
+
node_id -- Unique identifier for the node.
|
85
|
+
direction -- Direction of the neighbors to retrieve ('in', 'out', or 'any').
|
86
|
+
Returns a set of neighbor node IDs.
|
87
|
+
"""
|
46
88
|
if self.directed:
|
47
89
|
if direction == "out":
|
48
90
|
return self.g.successors(node_id)
|
49
91
|
elif direction == "in":
|
50
92
|
return self.g.predecessors(node_id)
|
51
93
|
elif direction == "any":
|
52
|
-
return set(self.g.successors(node_id)).union(
|
94
|
+
return set(self.g.successors(node_id)).union(
|
95
|
+
self.g.predecessors(node_id)
|
96
|
+
)
|
53
97
|
else:
|
54
98
|
raise ValueError("Direction must be 'in', 'out', or 'any'")
|
55
99
|
else:
|
56
100
|
return self.g.neighbors(node_id)
|
57
101
|
|
58
102
|
def remove_node(self, node_id):
|
103
|
+
"""
|
104
|
+
Remove a node from the graph.
|
105
|
+
node_id -- Unique identifier for the node.
|
106
|
+
"""
|
59
107
|
self.g.remove_node(node_id)
|
60
108
|
self._persist()
|
61
109
|
|
62
110
|
# Relationship (edge) operations
|
63
111
|
def add_relationship(self, source, target, rel_type=None, **attrs):
|
112
|
+
"""
|
113
|
+
Add a relationship (edge) to the graph.
|
114
|
+
source -- Unique identifier for the source node.
|
115
|
+
target -- Unique identifier for the target node.
|
116
|
+
rel_type -- Optional type of the relationship.
|
117
|
+
attrs -- Additional attributes for the relationship.
|
118
|
+
"""
|
64
119
|
if rel_type:
|
65
120
|
attrs["_type"] = rel_type
|
66
121
|
self.g.add_edge(source, target, **attrs)
|
67
122
|
self._persist()
|
68
123
|
|
69
124
|
def add_relationships(self, relationships):
|
125
|
+
"""
|
126
|
+
Add multiple relationships to the graph.
|
127
|
+
relationships -- List of dictionaries, each representing a relationship.
|
128
|
+
"""
|
70
129
|
for rel in relationships:
|
71
130
|
source = rel["source"]
|
72
131
|
target = rel["target"]
|
73
132
|
rel_type = rel.get("type") or rel.get("_type")
|
74
|
-
attrs = {
|
133
|
+
attrs = {
|
134
|
+
k: v
|
135
|
+
for k, v in rel.items()
|
136
|
+
if k not in ["source", "target", "type", "_type"]
|
137
|
+
}
|
75
138
|
self.add_relationship(source, target, rel_type=rel_type, **attrs)
|
76
139
|
|
77
140
|
def get_relationship(self, source, target):
|
141
|
+
"""
|
142
|
+
Get a relationship (edge) from the graph.
|
143
|
+
source -- Unique identifier for the source node.
|
144
|
+
target -- Unique identifier for the target node.
|
145
|
+
Returns the relationship's attributes as a dictionary.
|
146
|
+
"""
|
78
147
|
return self.g.get_edge_data(source, target)
|
79
148
|
|
80
149
|
def remove_relationship(self, source, target):
|
150
|
+
"""
|
151
|
+
Remove a relationship (edge) from the graph.
|
152
|
+
source -- Unique identifier for the source node.
|
153
|
+
target -- Unique identifier for the target node.
|
154
|
+
"""
|
81
155
|
self.g.remove_edge(source, target)
|
82
156
|
self._persist()
|
83
157
|
|
84
158
|
# Basic queries
|
85
159
|
def neighbors(self, node_id):
|
160
|
+
"""
|
161
|
+
Get the neighbors of a node.
|
162
|
+
node_id -- Unique identifier for the node.
|
163
|
+
Returns a list of neighbor node IDs.
|
164
|
+
"""
|
86
165
|
return list(self.g.neighbors(node_id))
|
87
166
|
|
88
167
|
def degree(self, node_id):
|
168
|
+
"""
|
169
|
+
Get the degree of a node.
|
170
|
+
node_id -- Unique identifier for the node.
|
171
|
+
Returns the degree of the node (number of edges connected to it).
|
172
|
+
"""
|
89
173
|
return self.g.degree[node_id]
|
90
174
|
|
91
175
|
def has_node(self, node_id):
|
176
|
+
"""
|
177
|
+
Check if a node exists in the graph.
|
178
|
+
node_id -- Unique identifier for the node.
|
179
|
+
Returns True if the node exists, False otherwise.
|
180
|
+
"""
|
92
181
|
return self.g.has_node(node_id)
|
93
182
|
|
94
183
|
def has_relationship(self, u, v):
|
184
|
+
"""
|
185
|
+
Check if a relationship (edge) exists in the graph.
|
186
|
+
u -- Unique identifier for the source node.
|
187
|
+
v -- Unique identifier for the target node.
|
188
|
+
Returns True if the relationship exists, False otherwise.
|
189
|
+
"""
|
95
190
|
return self.g.has_edge(u, v)
|
96
|
-
|
191
|
+
|
97
192
|
def update_node(self, node_id, **attrs):
|
193
|
+
"""
|
194
|
+
Update attributes of a node.
|
195
|
+
node_id -- Unique identifier for the node.
|
196
|
+
attrs -- Attributes to update.
|
197
|
+
"""
|
98
198
|
if not self.has_node(node_id):
|
99
199
|
raise KeyError(f"Node '{node_id}' does not exist.")
|
100
200
|
self.g.nodes[node_id].update(attrs)
|
101
201
|
self._persist()
|
102
202
|
|
103
203
|
def update_relationship(self, source, target, **attrs):
|
204
|
+
"""
|
205
|
+
Update attributes of a relationship (edge).
|
206
|
+
source -- Unique identifier for the source node.
|
207
|
+
target -- Unique identifier for the target node.
|
208
|
+
attrs -- Attributes to update.
|
209
|
+
"""
|
104
210
|
if not self.has_relationship(source, target):
|
105
211
|
raise KeyError(f"Relationship '{source}->{target}' does not exist.")
|
106
212
|
self.g.edges[source, target].update(attrs)
|
107
213
|
self._persist()
|
108
214
|
|
109
215
|
def set_node(self, node_id, labels=None, **attrs):
|
216
|
+
"""
|
217
|
+
Set or update a node in the graph.
|
218
|
+
node_id -- Unique identifier for the node.
|
219
|
+
labels -- Optional list of labels for the node.
|
220
|
+
attrs -- Attributes to set or update.
|
221
|
+
"""
|
110
222
|
if self.has_node(node_id):
|
111
223
|
self.update_node(node_id, **attrs)
|
112
224
|
else:
|
@@ -115,6 +227,13 @@ class GraphDB:
|
|
115
227
|
|
116
228
|
# Advanced node search
|
117
229
|
def project_node(self, node_id, fields=None):
|
230
|
+
"""
|
231
|
+
Project a node's attributes.
|
232
|
+
node_id -- Unique identifier for the node.
|
233
|
+
fields -- Optional list of fields to include in the projection.
|
234
|
+
Returns the node's attributes as a dictionary.
|
235
|
+
If fields is None, all attributes are included.
|
236
|
+
"""
|
118
237
|
if not self.has_node(node_id):
|
119
238
|
return None
|
120
239
|
node = self.get_node(node_id).copy()
|
@@ -124,6 +243,14 @@ class GraphDB:
|
|
124
243
|
return {k: node[k] for k in fields if k in node}
|
125
244
|
|
126
245
|
def project_relationship(self, source, target, fields=None):
|
246
|
+
"""
|
247
|
+
Project a relationship's attributes.
|
248
|
+
source -- Unique identifier for the source node.
|
249
|
+
target -- Unique identifier for the target node.
|
250
|
+
fields -- Optional list of fields to include in the projection.
|
251
|
+
Returns the relationship's attributes as a dictionary.
|
252
|
+
If fields is None, all attributes are included.
|
253
|
+
"""
|
127
254
|
if not self.has_relationship(source, target):
|
128
255
|
return None
|
129
256
|
rel = self.get_relationship(source, target).copy()
|
@@ -133,30 +260,72 @@ class GraphDB:
|
|
133
260
|
return {k: rel[k] for k in fields if k in rel}
|
134
261
|
|
135
262
|
def find_nodes(self, label=None, fields=None, **conditions):
|
263
|
+
"""
|
264
|
+
Find nodes in the graph based on conditions.
|
265
|
+
label -- Optional label to filter nodes by.
|
266
|
+
fields -- Optional list of fields to include in the projection.
|
267
|
+
conditions -- Conditions to filter nodes by.
|
268
|
+
Returns a list of nodes that match the conditions.
|
269
|
+
Each node is projected using the specified fields.
|
270
|
+
"""
|
136
271
|
return [
|
137
|
-
self.project_node(n, fields)
|
138
|
-
|
272
|
+
self.project_node(n, fields)
|
273
|
+
for n, a in self.g.nodes(data=True)
|
274
|
+
if (label is None or label in a.get("_labels", []))
|
275
|
+
and self._match_conditions(a, conditions)
|
139
276
|
]
|
140
277
|
|
141
278
|
def find_by_label(self, label, fields=None):
|
279
|
+
"""
|
280
|
+
Find nodes by label.
|
281
|
+
label -- Label to filter nodes by.
|
282
|
+
fields -- Optional list of fields to include in the projection.
|
283
|
+
Returns a list of nodes that have the specified label.
|
284
|
+
Each node is projected using the specified fields.
|
285
|
+
"""
|
142
286
|
return [
|
143
|
-
self.project_node(n, fields)
|
287
|
+
self.project_node(n, fields)
|
288
|
+
for n, a in self.g.nodes(data=True)
|
144
289
|
if label in a.get("_labels", [])
|
145
290
|
]
|
146
291
|
|
147
292
|
def find_relationships(self, rel_type=None, fields=None, **conditions):
|
293
|
+
"""
|
294
|
+
Find relationships in the graph based on conditions.
|
295
|
+
rel_type -- Optional type of the relationship to filter by.
|
296
|
+
fields -- Optional list of fields to include in the projection.
|
297
|
+
conditions -- Conditions to filter relationships by.
|
298
|
+
Returns a list of relationships that match the conditions.
|
299
|
+
Each relationship is projected using the specified fields.
|
300
|
+
"""
|
148
301
|
return [
|
149
|
-
self.project_relationship(u, v, fields)
|
150
|
-
|
302
|
+
self.project_relationship(u, v, fields)
|
303
|
+
for u, v, a in self.g.edges(data=True)
|
304
|
+
if (rel_type is None or a.get("_type") == rel_type)
|
305
|
+
and self._match_conditions(a, conditions)
|
151
306
|
]
|
152
307
|
|
153
308
|
def find_by_relationship_type(self, rel_type, fields=None):
|
309
|
+
"""
|
310
|
+
Find relationships by type.
|
311
|
+
rel_type -- Type of the relationship to filter by.
|
312
|
+
fields -- Optional list of fields to include in the projection.
|
313
|
+
Returns a list of relationships that have the specified type.
|
314
|
+
Each relationship is projected using the specified fields.
|
315
|
+
"""
|
154
316
|
return [
|
155
|
-
self.project_relationship(u, v, fields)
|
317
|
+
self.project_relationship(u, v, fields)
|
318
|
+
for u, v, a in self.g.edges(data=True)
|
156
319
|
if a.get("_type") == rel_type
|
157
320
|
]
|
158
321
|
|
159
322
|
def _match_conditions(self, attrs, conditions):
|
323
|
+
"""
|
324
|
+
Check if the attributes match the given conditions.
|
325
|
+
attrs -- Attributes of the node or relationship.
|
326
|
+
conditions -- Conditions to match against.
|
327
|
+
Returns True if all conditions are met, False otherwise.
|
328
|
+
"""
|
160
329
|
if not conditions:
|
161
330
|
return True
|
162
331
|
logic = conditions.get("_logic", "and")
|
@@ -167,13 +336,20 @@ class GraphDB:
|
|
167
336
|
actual = attrs.get(key)
|
168
337
|
if isinstance(expected, dict):
|
169
338
|
for op, val in expected.items():
|
170
|
-
if op == "gt":
|
171
|
-
|
172
|
-
elif op == "
|
173
|
-
|
174
|
-
elif op == "
|
175
|
-
|
176
|
-
|
339
|
+
if op == "gt":
|
340
|
+
results.append(actual > val)
|
341
|
+
elif op == "lt":
|
342
|
+
results.append(actual < val)
|
343
|
+
elif op == "gte":
|
344
|
+
results.append(actual >= val)
|
345
|
+
elif op == "lte":
|
346
|
+
results.append(actual <= val)
|
347
|
+
elif op == "ne":
|
348
|
+
results.append(actual != val)
|
349
|
+
elif op == "eq":
|
350
|
+
results.append(actual == val)
|
351
|
+
else:
|
352
|
+
results.append(False)
|
177
353
|
else:
|
178
354
|
results.append(actual == expected)
|
179
355
|
|
@@ -182,8 +358,19 @@ class GraphDB:
|
|
182
358
|
elif logic == "not":
|
183
359
|
return not all(results)
|
184
360
|
return all(results)
|
185
|
-
|
186
|
-
def match_node_path(
|
361
|
+
|
362
|
+
def match_node_path(
|
363
|
+
self, start, pattern, return_nodes=True, node_fields=None, direction="out"
|
364
|
+
):
|
365
|
+
"""
|
366
|
+
Match a path in the graph starting from a node.
|
367
|
+
start -- Starting node conditions (e.g., {"name": "Alice"}).
|
368
|
+
pattern -- Pattern to match, a list of dictionaries with "rel_type" and "node" keys.
|
369
|
+
return_nodes -- Whether to return the nodes in the path.
|
370
|
+
node_fields -- Optional list of fields to include in the projected nodes.
|
371
|
+
direction -- Direction of the search ('in', 'out', or 'any').
|
372
|
+
Returns a list of paths, where each path is a list of node IDs.
|
373
|
+
"""
|
187
374
|
start_nodes = self.find_nodes(**start)
|
188
375
|
node_paths = []
|
189
376
|
|
@@ -193,7 +380,7 @@ class GraphDB:
|
|
193
380
|
pattern=pattern,
|
194
381
|
node_path=[s["id"]],
|
195
382
|
node_paths=node_paths,
|
196
|
-
direction=direction
|
383
|
+
direction=direction,
|
197
384
|
)
|
198
385
|
|
199
386
|
unique_paths = list({tuple(p) for p in node_paths})
|
@@ -205,8 +392,15 @@ class GraphDB:
|
|
205
392
|
]
|
206
393
|
return unique_paths
|
207
394
|
|
208
|
-
|
209
395
|
def _match_node_path(self, current_id, pattern, node_path, node_paths, direction):
|
396
|
+
"""
|
397
|
+
Recursive helper function to match a node path.
|
398
|
+
current_id -- Current node ID.
|
399
|
+
pattern -- Pattern to match.
|
400
|
+
node_path -- Current path of node IDs.
|
401
|
+
node_paths -- List to collect all matching node paths.
|
402
|
+
direction -- Direction of the search ('in', 'out', or 'any').
|
403
|
+
"""
|
210
404
|
if not pattern:
|
211
405
|
node_paths.append(node_path)
|
212
406
|
return
|
@@ -224,9 +418,21 @@ class GraphDB:
|
|
224
418
|
continue
|
225
419
|
if neighbor in node_path: # avoid cycles
|
226
420
|
continue
|
227
|
-
self._match_node_path(
|
228
|
-
|
229
|
-
|
421
|
+
self._match_node_path(
|
422
|
+
neighbor, pattern[1:], node_path + [neighbor], node_paths, direction
|
423
|
+
)
|
424
|
+
|
425
|
+
def match_full_path(
|
426
|
+
self, start, pattern, node_fields=None, rel_fields=None, direction="out"
|
427
|
+
):
|
428
|
+
"""
|
429
|
+
Match a full path in the graph starting from a node.
|
430
|
+
start -- Starting node conditions (e.g., {"name": "Alice"}).
|
431
|
+
pattern -- Pattern to match, a list of dictionaries with "rel_type" and "node" keys.
|
432
|
+
node_fields -- Optional list of fields to include in the projected nodes.
|
433
|
+
rel_fields -- Optional list of fields to include in the projected relationships.
|
434
|
+
direction -- Direction of the search ('in', 'out', or 'any').
|
435
|
+
"""
|
230
436
|
start_nodes = self.find_nodes(**start)
|
231
437
|
matched_paths = []
|
232
438
|
|
@@ -237,18 +443,37 @@ class GraphDB:
|
|
237
443
|
relationship_path=[],
|
238
444
|
node_path=[s["id"]],
|
239
445
|
matched_paths=matched_paths,
|
240
|
-
direction=direction
|
446
|
+
direction=direction,
|
241
447
|
)
|
242
448
|
|
243
449
|
return [
|
244
450
|
{
|
245
451
|
"nodes": [self.project_node(n, node_fields) for n in nodes],
|
246
|
-
"relationships": [
|
452
|
+
"relationships": [
|
453
|
+
self.project_relationship(u, v, rel_fields) for u, v in path
|
454
|
+
],
|
247
455
|
}
|
248
456
|
for path, nodes in matched_paths
|
249
457
|
]
|
250
458
|
|
251
|
-
def _match_full_path(
|
459
|
+
def _match_full_path(
|
460
|
+
self,
|
461
|
+
current_id,
|
462
|
+
pattern,
|
463
|
+
relationship_path,
|
464
|
+
node_path,
|
465
|
+
matched_paths,
|
466
|
+
direction,
|
467
|
+
):
|
468
|
+
"""
|
469
|
+
Recursive helper function to match a full path.
|
470
|
+
current_id -- Current node ID.
|
471
|
+
pattern -- Pattern to match.
|
472
|
+
relationship_path -- Current path of relationships.
|
473
|
+
node_path -- Current path of node IDs.
|
474
|
+
matched_paths -- List to collect all matching paths.
|
475
|
+
direction -- Direction of the search ('in', 'out', or 'any').
|
476
|
+
"""
|
252
477
|
if not pattern:
|
253
478
|
matched_paths.append((relationship_path, node_path))
|
254
479
|
return
|
@@ -272,10 +497,20 @@ class GraphDB:
|
|
272
497
|
relationship_path + [(current_id, neighbor)],
|
273
498
|
node_path + [neighbor],
|
274
499
|
matched_paths,
|
275
|
-
direction
|
500
|
+
direction,
|
276
501
|
)
|
277
|
-
|
278
|
-
def match_path_structured(
|
502
|
+
|
503
|
+
def match_path_structured(
|
504
|
+
self, start, pattern, node_fields=None, rel_fields=None, direction="out"
|
505
|
+
):
|
506
|
+
"""
|
507
|
+
Match a structured path in the graph starting from a node.
|
508
|
+
start -- Starting node conditions (e.g., {"name": "Alice"}).
|
509
|
+
pattern -- Pattern to match, a list of dictionaries with "rel_type" and "node" keys.
|
510
|
+
node_fields -- Optional list of fields to include in the projected nodes.
|
511
|
+
rel_fields -- Optional list of fields to include in the projected relationships.
|
512
|
+
direction -- Direction of the search ('in', 'out', or 'any').
|
513
|
+
"""
|
279
514
|
start_nodes = self.find_nodes(**start)
|
280
515
|
structured_paths = []
|
281
516
|
|
@@ -285,12 +520,22 @@ class GraphDB:
|
|
285
520
|
pattern=pattern,
|
286
521
|
path=[{"node": self.project_node(s["id"], node_fields)}],
|
287
522
|
structured_paths=structured_paths,
|
288
|
-
direction=direction
|
523
|
+
direction=direction,
|
289
524
|
)
|
290
525
|
|
291
526
|
return structured_paths
|
292
|
-
|
293
|
-
def _match_structured_path(
|
527
|
+
|
528
|
+
def _match_structured_path(
|
529
|
+
self, current_id, pattern, path, structured_paths, direction
|
530
|
+
):
|
531
|
+
"""
|
532
|
+
Recursive helper function to match a structured path.
|
533
|
+
current_id -- Current node ID.
|
534
|
+
pattern -- Pattern to match.
|
535
|
+
path -- Current structured path.
|
536
|
+
structured_paths -- List to collect all matching structured paths.
|
537
|
+
direction -- Direction of the search ('in', 'out', or 'any').
|
538
|
+
"""
|
294
539
|
if not pattern:
|
295
540
|
structured_paths.append({"path": path})
|
296
541
|
return
|
@@ -311,39 +556,58 @@ class GraphDB:
|
|
311
556
|
|
312
557
|
extended_path = path + [
|
313
558
|
{"relationship": self.project_relationship(current_id, neighbor)},
|
314
|
-
{"node": self.project_node(neighbor)}
|
559
|
+
{"node": self.project_node(neighbor)},
|
315
560
|
]
|
316
561
|
|
317
562
|
self._match_structured_path(
|
318
|
-
neighbor,
|
319
|
-
pattern[1:],
|
320
|
-
extended_path,
|
321
|
-
structured_paths,
|
322
|
-
direction
|
563
|
+
neighbor, pattern[1:], extended_path, structured_paths, direction
|
323
564
|
)
|
324
565
|
|
325
566
|
# Export
|
326
567
|
def nodes(self):
|
327
|
-
|
568
|
+
"""
|
569
|
+
Get all nodes in the graph.
|
570
|
+
Returns a list of dictionaries with node IDs, labels, and attributes.
|
571
|
+
"""
|
572
|
+
return [
|
573
|
+
{
|
574
|
+
"id": n,
|
575
|
+
"labels": a.get("_labels", []),
|
576
|
+
**{k: v for k, v in a.items() if k != "_labels"},
|
577
|
+
}
|
578
|
+
for n, a in self.g.nodes(data=True)
|
579
|
+
]
|
328
580
|
|
329
581
|
def relationships(self):
|
582
|
+
"""
|
583
|
+
Get all relationships in the graph.
|
584
|
+
Returns a list of dictionaries with source, target, type, and attributes.
|
585
|
+
"""
|
330
586
|
return [
|
331
587
|
{
|
332
588
|
"source": u,
|
333
589
|
"target": v,
|
334
590
|
"type": a.get("_type"),
|
335
|
-
**{k: v for k, v in a.items() if k != "_type"}
|
591
|
+
**{k: v for k, v in a.items() if k != "_type"},
|
336
592
|
}
|
337
593
|
for u, v, a in self.g.edges(data=True)
|
338
594
|
]
|
339
595
|
|
340
596
|
def to_dict(self):
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
597
|
+
"""
|
598
|
+
Convert the graph to a dictionary representation.
|
599
|
+
Returns a dictionary with "nodes" and "relationships" keys.
|
600
|
+
Each key contains a list of nodes or relationships, respectively.
|
601
|
+
"""
|
602
|
+
return {"nodes": self.nodes(), "relationships": self.relationships()}
|
345
603
|
|
346
604
|
def save(self, path=None):
|
605
|
+
"""
|
606
|
+
Save the graph to a file.
|
607
|
+
path -- Path to the file where the graph will be saved.
|
608
|
+
If path is None, it will use the instance's path.
|
609
|
+
If path is not specified, it will raise a ValueError.
|
610
|
+
"""
|
347
611
|
path = path or self.path
|
348
612
|
if not path:
|
349
613
|
raise ValueError("No path specified to save the graph.")
|
@@ -351,6 +615,12 @@ class GraphDB:
|
|
351
615
|
json.dump(self.to_dict(), f, indent=4)
|
352
616
|
|
353
617
|
def load(self, path=None):
|
618
|
+
"""
|
619
|
+
Load the graph from a file.
|
620
|
+
path -- Path to the file from which the graph will be loaded.
|
621
|
+
If path is None, it will use the instance's path.
|
622
|
+
If path is not specified, it will raise a ValueError.
|
623
|
+
"""
|
354
624
|
path = path or self.path
|
355
625
|
if not path:
|
356
626
|
raise ValueError("No path specified to load the graph.")
|
@@ -363,17 +633,31 @@ class GraphDB:
|
|
363
633
|
self.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
|
364
634
|
for rel in data.get("relationships", []):
|
365
635
|
self.add_relationship(
|
366
|
-
rel["source"],
|
636
|
+
rel["source"],
|
637
|
+
rel["target"],
|
367
638
|
rel_type=rel.get("type") or rel.get("_type"),
|
368
|
-
**{
|
639
|
+
**{
|
640
|
+
k: v
|
641
|
+
for k, v in rel.items()
|
642
|
+
if k not in ["source", "target", "type", "_type"]
|
643
|
+
},
|
369
644
|
)
|
370
|
-
|
645
|
+
|
371
646
|
def save_query_result(self, result, path=None):
|
647
|
+
"""
|
648
|
+
Save the result of a query to a file.
|
649
|
+
result -- Result of the query, typically a list of nodes or relationships.
|
650
|
+
path -- Path to the file where the result will be saved.
|
651
|
+
If path is None, it will raise a ValueError.
|
652
|
+
"""
|
372
653
|
if path is None:
|
373
654
|
raise ValueError("No path specified to save the query result.")
|
374
655
|
with open(path, "w", encoding="utf-8") as f:
|
375
656
|
json.dump(result, f, indent=4)
|
376
|
-
|
657
|
+
|
377
658
|
def _persist(self):
|
659
|
+
"""
|
660
|
+
Persist changes to the graph to the file if not in memory.
|
661
|
+
"""
|
378
662
|
if not self.in_memory:
|
379
663
|
self.save(self.path)
|
coffy/nosql/__init__.py
CHANGED
Binary file
|
Binary file
|