coffy 0.1.5__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 CHANGED
@@ -1,2 +1,2 @@
1
1
  # coffy/__init__.py
2
- # author: nsarathy
2
+ # author: nsarathy
Binary file
coffy/graph/__init__.py CHANGED
@@ -1,4 +1,6 @@
1
1
  # coffy/graph/__init__.py
2
2
  # author: nsarathy
3
3
 
4
- from .graphdb_nx import GraphDB
4
+ from .graphdb_nx import GraphDB as GraphDB
5
+
6
+ __all__ = ["GraphDB"]
coffy/graph/graphdb_nx.py CHANGED
@@ -1,13 +1,28 @@
1
1
  # coffy/graph/graphdb_nx.py
2
2
  # author: nsarathy
3
3
 
4
- import networkx as nx
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 = {k: v for k, v in node.items() if k not in ["id", "labels", "_labels"]}
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(self.g.predecessors(node_id))
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 = {k: v for k, v in rel.items() if k not in ["source", "target", "type", "_type"]}
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) for n, a in self.g.nodes(data=True)
138
- if (label is None or label in a.get("_labels", [])) and self._match_conditions(a, conditions)
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) for n, a in self.g.nodes(data=True)
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) for u, v, a in self.g.edges(data=True)
150
- if (rel_type is None or a.get("_type") == rel_type) and self._match_conditions(a, conditions)
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) for u, v, a in self.g.edges(data=True)
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": results.append(actual > val)
171
- elif op == "lt": results.append(actual < val)
172
- elif op == "gte": results.append(actual >= val)
173
- elif op == "lte": results.append(actual <= val)
174
- elif op == "ne": results.append(actual != val)
175
- elif op == "eq": results.append(actual == val)
176
- else: results.append(False)
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(self, start, pattern, return_nodes=True, node_fields=None, direction="out"):
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(neighbor, pattern[1:], node_path + [neighbor], node_paths, direction)
228
-
229
- def match_full_path(self, start, pattern, node_fields=None, rel_fields=None, direction="out"):
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": [self.project_relationship(u, v, rel_fields) for u, v in path]
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(self, current_id, pattern, relationship_path, node_path, matched_paths, direction):
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(self, start, pattern, node_fields=None, rel_fields=None, direction="out"):
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(self, current_id, pattern, path, structured_paths, direction):
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
- return [{"id": n, "labels": a.get("_labels", []), **{k: v for k, v in a.items() if k != "_labels"}} for n, a in self.g.nodes(data=True)]
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
- return {
342
- "nodes": self.nodes(),
343
- "relationships": self.relationships()
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"], rel["target"],
636
+ rel["source"],
637
+ rel["target"],
367
638
  rel_type=rel.get("type") or rel.get("_type"),
368
- **{k: v for k, v in rel.items() if k not in ["source", "target", "type", "_type"]}
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
@@ -3,5 +3,9 @@
3
3
 
4
4
  from .engine import CollectionManager
5
5
 
6
+
6
7
  def db(collection_name: str, path: str = None):
7
8
  return CollectionManager(collection_name, path=path)
9
+
10
+
11
+ __all__ = ["db", "CollectionManager"]