graflo 1.3.3__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.
Files changed (64) hide show
  1. graflo/README.md +18 -0
  2. graflo/__init__.py +70 -0
  3. graflo/architecture/__init__.py +38 -0
  4. graflo/architecture/actor.py +1120 -0
  5. graflo/architecture/actor_util.py +450 -0
  6. graflo/architecture/edge.py +297 -0
  7. graflo/architecture/onto.py +374 -0
  8. graflo/architecture/resource.py +161 -0
  9. graflo/architecture/schema.py +136 -0
  10. graflo/architecture/transform.py +292 -0
  11. graflo/architecture/util.py +93 -0
  12. graflo/architecture/vertex.py +586 -0
  13. graflo/caster.py +655 -0
  14. graflo/cli/__init__.py +14 -0
  15. graflo/cli/ingest.py +194 -0
  16. graflo/cli/manage_dbs.py +197 -0
  17. graflo/cli/plot_schema.py +132 -0
  18. graflo/cli/xml2json.py +93 -0
  19. graflo/data_source/__init__.py +48 -0
  20. graflo/data_source/api.py +339 -0
  21. graflo/data_source/base.py +97 -0
  22. graflo/data_source/factory.py +298 -0
  23. graflo/data_source/file.py +133 -0
  24. graflo/data_source/memory.py +72 -0
  25. graflo/data_source/registry.py +82 -0
  26. graflo/data_source/sql.py +185 -0
  27. graflo/db/__init__.py +44 -0
  28. graflo/db/arango/__init__.py +22 -0
  29. graflo/db/arango/conn.py +1026 -0
  30. graflo/db/arango/query.py +180 -0
  31. graflo/db/arango/util.py +88 -0
  32. graflo/db/conn.py +377 -0
  33. graflo/db/connection/__init__.py +6 -0
  34. graflo/db/connection/config_mapping.py +18 -0
  35. graflo/db/connection/onto.py +688 -0
  36. graflo/db/connection/wsgi.py +29 -0
  37. graflo/db/manager.py +119 -0
  38. graflo/db/neo4j/__init__.py +16 -0
  39. graflo/db/neo4j/conn.py +639 -0
  40. graflo/db/postgres/__init__.py +156 -0
  41. graflo/db/postgres/conn.py +425 -0
  42. graflo/db/postgres/resource_mapping.py +139 -0
  43. graflo/db/postgres/schema_inference.py +245 -0
  44. graflo/db/postgres/types.py +148 -0
  45. graflo/db/tigergraph/__init__.py +9 -0
  46. graflo/db/tigergraph/conn.py +2212 -0
  47. graflo/db/util.py +49 -0
  48. graflo/filter/__init__.py +21 -0
  49. graflo/filter/onto.py +525 -0
  50. graflo/logging.conf +22 -0
  51. graflo/onto.py +190 -0
  52. graflo/plot/__init__.py +17 -0
  53. graflo/plot/plotter.py +556 -0
  54. graflo/util/__init__.py +23 -0
  55. graflo/util/chunker.py +751 -0
  56. graflo/util/merge.py +150 -0
  57. graflo/util/misc.py +37 -0
  58. graflo/util/onto.py +332 -0
  59. graflo/util/transform.py +448 -0
  60. graflo-1.3.3.dist-info/METADATA +190 -0
  61. graflo-1.3.3.dist-info/RECORD +64 -0
  62. graflo-1.3.3.dist-info/WHEEL +4 -0
  63. graflo-1.3.3.dist-info/entry_points.txt +5 -0
  64. graflo-1.3.3.dist-info/licenses/LICENSE +126 -0
@@ -0,0 +1,639 @@
1
+ """Neo4j connection implementation for graph database operations.
2
+
3
+ This module implements the Connection interface for Neo4j, providing
4
+ specific functionality for graph operations in Neo4j. It handles:
5
+ - Node and relationship management
6
+ - Cypher query execution
7
+ - Index creation and management
8
+ - Batch operations
9
+ - Graph traversal and pattern matching
10
+
11
+ Key Features:
12
+ - Label-based node organization
13
+ - Relationship type management
14
+ - Property indices
15
+ - Cypher query execution
16
+ - Batch node and relationship operations
17
+
18
+ Example:
19
+ >>> conn = Neo4jConnection(config)
20
+ >>> conn.init_db(schema, clean_start=True)
21
+ >>> conn.upsert_docs_batch(docs, "User", match_keys=["email"])
22
+ """
23
+
24
+ import logging
25
+
26
+ from neo4j import GraphDatabase
27
+
28
+ from graflo.architecture.edge import Edge
29
+ from graflo.architecture.onto import Index
30
+ from graflo.architecture.schema import Schema
31
+ from graflo.architecture.vertex import VertexConfig
32
+ from graflo.db.conn import Connection
33
+ from graflo.filter.onto import Expression
34
+ from graflo.onto import AggregationType, DBFlavor, ExpressionFlavor
35
+
36
+ from ..connection.onto import Neo4jConfig
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class Neo4jConnection(Connection):
42
+ """Neo4j-specific implementation of the Connection interface.
43
+
44
+ This class provides Neo4j-specific implementations for all database
45
+ operations, including node management, relationship operations, and
46
+ Cypher query execution. It uses the Neo4j Python driver for all operations.
47
+
48
+ Attributes:
49
+ flavor: Database flavor identifier (NEO4J)
50
+ conn: Neo4j session instance
51
+ """
52
+
53
+ flavor = DBFlavor.NEO4J
54
+
55
+ def __init__(self, config: Neo4jConfig):
56
+ """Initialize Neo4j connection.
57
+
58
+ Args:
59
+ config: Neo4j connection configuration containing URL and credentials
60
+ """
61
+ super().__init__()
62
+ # Store config for later use
63
+ self.config = config
64
+ # Ensure url is not None - GraphDatabase.driver requires a non-None URI
65
+ if config.url is None:
66
+ raise ValueError("Neo4j connection requires a URL to be configured")
67
+ self._driver = GraphDatabase.driver(
68
+ uri=config.url, auth=(config.username, config.password)
69
+ )
70
+ self.conn = self._driver.session()
71
+
72
+ def execute(self, query, **kwargs):
73
+ """Execute a Cypher query.
74
+
75
+ Args:
76
+ query: Cypher query string to execute
77
+ **kwargs: Additional query parameters
78
+
79
+ Returns:
80
+ Result: Neo4j query result
81
+ """
82
+ cursor = self.conn.run(query, **kwargs)
83
+ return cursor
84
+
85
+ def close(self):
86
+ """Close the Neo4j connection and session."""
87
+ # Close session first, then the underlying driver
88
+ try:
89
+ self.conn.close()
90
+ finally:
91
+ # Ensure the driver is also closed to release resources
92
+ self._driver.close()
93
+
94
+ def create_database(self, name: str):
95
+ """Create a new Neo4j database.
96
+
97
+ Note: This operation is only supported in Neo4j Enterprise Edition.
98
+ Community Edition only supports one database per instance.
99
+
100
+ Args:
101
+ name: Name of the database to create
102
+ """
103
+ try:
104
+ self.execute(f"CREATE DATABASE {name}")
105
+ logger.info(f"Successfully created Neo4j database '{name}'")
106
+ except Exception as e:
107
+ raise e
108
+
109
+ def delete_database(self, name: str):
110
+ """Delete a Neo4j database.
111
+
112
+ Note: This operation is only supported in Neo4j Enterprise Edition.
113
+ As a fallback, it deletes all nodes and relationships.
114
+
115
+ Args:
116
+ name: Name of the database to delete (unused, deletes all data)
117
+ """
118
+ try:
119
+ self.execute("MATCH (n) DETACH DELETE n")
120
+ logger.info("Successfully cleaned Neo4j database")
121
+ except Exception as e:
122
+ logger.error(
123
+ f"Failed to clean Neo4j database: {e}",
124
+ exc_info=True,
125
+ )
126
+ raise
127
+
128
+ def define_vertex_indices(self, vertex_config: VertexConfig):
129
+ """Define indices for vertex labels.
130
+
131
+ Creates indices for each vertex label based on the configuration.
132
+
133
+ Args:
134
+ vertex_config: Vertex configuration containing index definitions
135
+ """
136
+ for c in vertex_config.vertex_set:
137
+ for index_obj in vertex_config.indexes(c):
138
+ self._add_index(c, index_obj)
139
+
140
+ def define_edge_indices(self, edges: list[Edge]):
141
+ """Define indices for relationship types.
142
+
143
+ Creates indices for each relationship type based on the configuration.
144
+
145
+ Args:
146
+ edges: List of edge configurations containing index definitions
147
+ """
148
+ for edge in edges:
149
+ for index_obj in edge.indexes:
150
+ if edge.relation is not None:
151
+ self._add_index(edge.relation, index_obj, is_vertex_index=False)
152
+
153
+ def _add_index(self, obj_name, index: Index, is_vertex_index=True):
154
+ """Add an index to a label or relationship type.
155
+
156
+ Args:
157
+ obj_name: Label or relationship type name
158
+ index: Index configuration to create
159
+ is_vertex_index: If True, create index on nodes, otherwise on relationships
160
+ """
161
+ fields_str = ", ".join([f"x.{f}" for f in index.fields])
162
+ fields_str2 = "_".join(index.fields)
163
+ index_name = f"{obj_name}_{fields_str2}"
164
+ if is_vertex_index:
165
+ formula = f"(x:{obj_name})"
166
+ else:
167
+ formula = f"()-[x:{obj_name}]-()"
168
+
169
+ q = f"CREATE INDEX {index_name} IF NOT EXISTS FOR {formula} ON ({fields_str});"
170
+
171
+ self.execute(q)
172
+
173
+ def define_schema(self, schema: Schema):
174
+ """Define collections based on schema.
175
+
176
+ Note: This is a no-op in Neo4j as collections are implicit.
177
+
178
+ Args:
179
+ schema: Schema containing collection definitions
180
+ """
181
+ pass
182
+
183
+ def define_vertex_collections(self, schema: Schema):
184
+ """Define vertex collections based on schema.
185
+
186
+ Note: This is a no-op in Neo4j as vertex collections are implicit.
187
+
188
+ Args:
189
+ schema: Schema containing vertex definitions
190
+ """
191
+ pass
192
+
193
+ def define_edge_collections(self, edges: list[Edge]):
194
+ """Define edge collections based on schema.
195
+
196
+ Note: This is a no-op in Neo4j as edge collections are implicit.
197
+
198
+ Args:
199
+ edges: List of edge configurations
200
+ """
201
+ pass
202
+
203
+ def delete_graph_structure(self, vertex_types=(), graph_names=(), delete_all=False):
204
+ """Delete graph structure (nodes and relationships) from Neo4j.
205
+
206
+ In Neo4j:
207
+ - Labels: Categories for nodes (equivalent to vertex types)
208
+ - Relationship Types: Types of relationships (equivalent to edge types)
209
+ - No explicit "graph" concept - all nodes/relationships are in the database
210
+
211
+ Args:
212
+ vertex_types: Label names to delete nodes for
213
+ graph_names: Unused in Neo4j (no explicit graph concept)
214
+ delete_all: If True, delete all nodes and relationships
215
+ """
216
+ cnames = vertex_types
217
+ if cnames:
218
+ for c in cnames:
219
+ q = f"MATCH (n:{c}) DELETE n"
220
+ self.execute(q)
221
+ else:
222
+ q = "MATCH (n) DELETE n"
223
+ self.execute(q)
224
+
225
+ def init_db(self, schema: Schema, clean_start):
226
+ """Initialize Neo4j with the given schema.
227
+
228
+ Checks if the database exists and creates it if it doesn't.
229
+ Uses schema.general.name if database is not set in config.
230
+ Note: Database creation is only supported in Neo4j Enterprise Edition.
231
+
232
+ Args:
233
+ schema: Schema containing graph structure definitions
234
+ clean_start: If True, delete all existing data before initialization
235
+ """
236
+ # Determine database name: use config.database if set, otherwise use schema.general.name
237
+ db_name = self.config.database
238
+ if not db_name:
239
+ db_name = schema.general.name
240
+ # Update config for subsequent operations
241
+ self.config.database = db_name
242
+
243
+ # Check if database exists and create it if it doesn't
244
+ # Note: This only works in Neo4j Enterprise Edition
245
+ # For Community Edition, we'll try to create it but it may fail gracefully
246
+ # Community Edition only allows one database per instance
247
+ try:
248
+ # Try to check if database exists (Enterprise feature)
249
+ try:
250
+ result = self.execute("SHOW DATABASES")
251
+ # Neo4j result is a cursor-like object, iterate to get records
252
+ databases = []
253
+ for record in result:
254
+ # Record structure may vary, try common field names
255
+ if hasattr(record, "get"):
256
+ db_name_field = (
257
+ record.get("name")
258
+ or record.get("database")
259
+ or record.get("db")
260
+ )
261
+ else:
262
+ # If record is a dict-like object, try direct access
263
+ db_name_field = getattr(record, "name", None) or getattr(
264
+ record, "database", None
265
+ )
266
+ if db_name_field:
267
+ databases.append(db_name_field)
268
+
269
+ if db_name not in databases:
270
+ logger.info(
271
+ f"Database '{db_name}' does not exist, attempting to create it..."
272
+ )
273
+ try:
274
+ self.create_database(db_name)
275
+ logger.info(f"Successfully created database '{db_name}'")
276
+ except Exception as create_error:
277
+ logger.info(
278
+ f"Neo4j Community Edition? Could not create database '{db_name}': {create_error}. "
279
+ f"This may be Neo4j Community Edition which only supports one database per instance.",
280
+ exc_info=True,
281
+ )
282
+ # Continue with default database for Community Edition
283
+ except Exception as show_error:
284
+ # If SHOW DATABASES fails (Community Edition or older versions), try to create anyway
285
+ logger.debug(
286
+ f"Could not check database existence (may be Community Edition): {show_error}"
287
+ )
288
+ try:
289
+ self.create_database(db_name)
290
+ logger.info(f"Successfully created database '{db_name}'")
291
+ except Exception as create_error:
292
+ logger.info(
293
+ f"Neo4j Community Edition? Could not create database '{db_name}': {create_error}. "
294
+ f"This may be Neo4j Community Edition which only supports one database per instance. "
295
+ f"Continuing with default database.",
296
+ exc_info=True,
297
+ )
298
+ # Continue with default database for Community Edition
299
+ except Exception as e:
300
+ logger.error(
301
+ f"Error during database initialization for '{db_name}': {e}",
302
+ exc_info=True,
303
+ )
304
+ # Don't raise - allow operation to continue with default database
305
+ logger.warning(
306
+ "Continuing with default database due to initialization error"
307
+ )
308
+
309
+ try:
310
+ if clean_start:
311
+ try:
312
+ self.delete_database("")
313
+ logger.debug(f"Cleaned database '{db_name}' for fresh start")
314
+ except Exception as clean_error:
315
+ logger.warning(
316
+ f"Error during clean_start for database '{db_name}': {clean_error}",
317
+ exc_info=True,
318
+ )
319
+ # Continue - may be first run or already clean
320
+
321
+ try:
322
+ self.define_indexes(schema)
323
+ logger.debug(f"Defined indexes for database '{db_name}'")
324
+ except Exception as index_error:
325
+ logger.error(
326
+ f"Failed to define indexes for database '{db_name}': {index_error}",
327
+ exc_info=True,
328
+ )
329
+ raise
330
+ except Exception as e:
331
+ logger.error(
332
+ f"Error during database schema initialization for '{db_name}': {e}",
333
+ exc_info=True,
334
+ )
335
+ raise
336
+
337
+ def upsert_docs_batch(self, docs, class_name, match_keys, **kwargs):
338
+ """Upsert a batch of nodes using Cypher.
339
+
340
+ Performs an upsert operation on a batch of nodes, using the specified
341
+ match keys to determine whether to update existing nodes or create new ones.
342
+
343
+ Args:
344
+ docs: List of node documents to upsert
345
+ class_name: Label to upsert into
346
+ match_keys: Keys to match for upsert operation
347
+ **kwargs: Additional options:
348
+ - dry: If True, don't execute the query
349
+ """
350
+ dry = kwargs.pop("dry", False)
351
+
352
+ index_str = ", ".join([f"{k}: row.{k}" for k in match_keys])
353
+ q = f"""
354
+ WITH $batch AS batch
355
+ UNWIND batch as row
356
+ MERGE (n:{class_name} {{ {index_str} }})
357
+ ON MATCH set n += row
358
+ ON CREATE set n += row
359
+ """
360
+ if not dry:
361
+ self.execute(q, batch=docs)
362
+
363
+ def insert_edges_batch(
364
+ self,
365
+ docs_edges,
366
+ source_class,
367
+ target_class,
368
+ relation_name,
369
+ collection_name=None,
370
+ match_keys_source=("_key",),
371
+ match_keys_target=("_key",),
372
+ filter_uniques=True,
373
+ uniq_weight_fields=None,
374
+ uniq_weight_collections=None,
375
+ upsert_option=False,
376
+ head=None,
377
+ **kwargs,
378
+ ):
379
+ """Insert a batch of relationships using Cypher.
380
+
381
+ Creates relationships between source and target nodes, with support for
382
+ property matching and unique constraints.
383
+
384
+ Args:
385
+ docs_edges: List of edge documents in format [{__source: source_doc, __target: target_doc}]
386
+ source_class: Source node label
387
+ target_class: Target node label
388
+ relation_name: Relationship type name
389
+ collection_name: Unused in Neo4j
390
+ match_keys_source: Keys to match source nodes
391
+ match_keys_target: Keys to match target nodes
392
+ filter_uniques: Unused in Neo4j
393
+ uniq_weight_fields: Unused in Neo4j
394
+ uniq_weight_collections: Unused in Neo4j
395
+ upsert_option: Unused in Neo4j
396
+ head: Optional limit on number of relationships to insert
397
+ **kwargs: Additional options:
398
+ - dry: If True, don't execute the query
399
+ """
400
+ dry = kwargs.pop("dry", False)
401
+
402
+ source_match_str = [f"source.{key} = row[0].{key}" for key in match_keys_source]
403
+ target_match_str = [f"target.{key} = row[1].{key}" for key in match_keys_target]
404
+
405
+ match_clause = "WHERE " + " AND ".join(source_match_str + target_match_str)
406
+
407
+ q = f"""
408
+ WITH $batch AS batch
409
+ UNWIND batch as row
410
+ MATCH (source:{source_class}),
411
+ (target:{target_class}) {match_clause}
412
+ MERGE (source)-[r:{relation_name}]->(target)
413
+ SET r += row[2]
414
+
415
+ """
416
+ if not dry:
417
+ self.execute(q, batch=docs_edges)
418
+
419
+ def insert_return_batch(self, docs, class_name):
420
+ """Insert nodes and return their properties.
421
+
422
+ Note: Not implemented in Neo4j.
423
+
424
+ Args:
425
+ docs: Documents to insert
426
+ class_name: Label to insert into
427
+
428
+ Raises:
429
+ NotImplementedError: This method is not implemented for Neo4j
430
+ """
431
+ raise NotImplementedError()
432
+
433
+ def fetch_docs(
434
+ self,
435
+ class_name,
436
+ filters: list | dict | None = None,
437
+ limit: int | None = None,
438
+ return_keys: list | None = None,
439
+ unset_keys: list | None = None,
440
+ **kwargs,
441
+ ):
442
+ """Fetch nodes from a label.
443
+
444
+ Args:
445
+ class_name: Label to fetch from
446
+ filters: Query filters
447
+ limit: Maximum number of nodes to return
448
+ return_keys: Keys to return
449
+ unset_keys: Unused in Neo4j
450
+
451
+ Returns:
452
+ list: Fetched nodes
453
+ """
454
+ if filters is not None:
455
+ ff = Expression.from_dict(filters)
456
+ filter_clause = f"WHERE {ff(doc_name='n', kind=DBFlavor.NEO4J)}"
457
+ else:
458
+ filter_clause = ""
459
+
460
+ if return_keys is not None:
461
+ keep_clause_ = ", ".join([f".{item}" for item in return_keys])
462
+ keep_clause = f"{{ {keep_clause_} }}"
463
+ else:
464
+ keep_clause = ""
465
+
466
+ if limit is not None and isinstance(limit, int):
467
+ limit_clause = f"LIMIT {limit}"
468
+ else:
469
+ limit_clause = ""
470
+
471
+ q = (
472
+ f"MATCH (n:{class_name})"
473
+ f" {filter_clause}"
474
+ f" RETURN n {keep_clause}"
475
+ f" {limit_clause}"
476
+ )
477
+ cursor = self.execute(q)
478
+ r = [item["n"] for item in cursor.data()]
479
+ return r
480
+
481
+ # TODO test
482
+ def fetch_edges(
483
+ self,
484
+ from_type: str,
485
+ from_id: str,
486
+ edge_type: str | None = None,
487
+ to_type: str | None = None,
488
+ to_id: str | None = None,
489
+ filters: list | dict | None = None,
490
+ limit: int | None = None,
491
+ return_keys: list | None = None,
492
+ unset_keys: list | None = None,
493
+ **kwargs,
494
+ ):
495
+ """Fetch edges from Neo4j using Cypher.
496
+
497
+ Args:
498
+ from_type: Source node label
499
+ from_id: Source node ID (property name depends on match_keys used)
500
+ edge_type: Optional relationship type to filter by
501
+ to_type: Optional target node label to filter by
502
+ to_id: Optional target node ID to filter by
503
+ filters: Additional query filters
504
+ limit: Maximum number of edges to return
505
+ return_keys: Keys to return (projection)
506
+ unset_keys: Keys to exclude (projection) - not supported in Neo4j
507
+ **kwargs: Additional parameters
508
+
509
+ Returns:
510
+ list: List of fetched edges
511
+ """
512
+ # Build Cypher query to fetch edges
513
+ # Match source node first
514
+ source_match = f"(source:{from_type} {{id: '{from_id}'}})"
515
+
516
+ # Build relationship pattern
517
+ if edge_type:
518
+ rel_pattern = f"-[r:{edge_type}]->"
519
+ else:
520
+ rel_pattern = "-[r]->"
521
+
522
+ # Build target node match
523
+ if to_type:
524
+ target_match = f"(target:{to_type})"
525
+ else:
526
+ target_match = "(target)"
527
+
528
+ # Add target ID filter if provided
529
+ where_clauses = []
530
+ if to_id:
531
+ where_clauses.append(f"target.id = '{to_id}'")
532
+
533
+ # Add additional filters if provided
534
+ if filters is not None:
535
+ from graflo.filter.onto import Expression
536
+
537
+ ff = Expression.from_dict(filters)
538
+ filter_clause = ff(doc_name="r", kind=ExpressionFlavor.NEO4J)
539
+ where_clauses.append(filter_clause)
540
+
541
+ where_clause = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
542
+
543
+ # Build return clause
544
+ if return_keys is not None:
545
+ return_clause = ", ".join([f"r.{key} as {key}" for key in return_keys])
546
+ return_clause = f"RETURN {return_clause}"
547
+ else:
548
+ return_clause = "RETURN r"
549
+
550
+ limit_clause = f"LIMIT {limit}" if limit else ""
551
+
552
+ query = f"""
553
+ MATCH {source_match}{rel_pattern}{target_match}
554
+ {where_clause}
555
+ {return_clause}
556
+ {limit_clause}
557
+ """
558
+
559
+ cursor = self.execute(query)
560
+ result = [item["r"] for item in cursor.data()]
561
+
562
+ # Note: unset_keys is not supported in Neo4j as we can't modify the result structure
563
+ # after the query
564
+
565
+ return result
566
+
567
+ def fetch_present_documents(
568
+ self,
569
+ batch,
570
+ class_name,
571
+ match_keys,
572
+ keep_keys,
573
+ flatten=False,
574
+ filters: list | dict | None = None,
575
+ ):
576
+ """Fetch nodes that exist in the database.
577
+
578
+ Note: Not implemented in Neo4j.
579
+
580
+ Args:
581
+ batch: Batch of documents to check
582
+ class_name: Label to check in
583
+ match_keys: Keys to match nodes
584
+ keep_keys: Keys to keep in result
585
+ flatten: Unused in Neo4j
586
+ filters: Additional query filters
587
+
588
+ Raises:
589
+ NotImplementedError: This method is not implemented for Neo4j
590
+ """
591
+ raise NotImplementedError
592
+
593
+ def aggregate(
594
+ self,
595
+ class_name,
596
+ aggregation_function: AggregationType,
597
+ discriminant: str | None = None,
598
+ aggregated_field: str | None = None,
599
+ filters: list | dict | None = None,
600
+ ):
601
+ """Perform aggregation on nodes.
602
+
603
+ Note: Not implemented in Neo4j.
604
+
605
+ Args:
606
+ class_name: Label to aggregate
607
+ aggregation_function: Type of aggregation to perform
608
+ discriminant: Field to group by
609
+ aggregated_field: Field to aggregate
610
+ filters: Query filters
611
+
612
+ Raises:
613
+ NotImplementedError: This method is not implemented for Neo4j
614
+ """
615
+ raise NotImplementedError
616
+
617
+ def keep_absent_documents(
618
+ self,
619
+ batch,
620
+ class_name,
621
+ match_keys,
622
+ keep_keys,
623
+ filters: list | dict | None = None,
624
+ ):
625
+ """Keep nodes that don't exist in the database.
626
+
627
+ Note: Not implemented in Neo4j.
628
+
629
+ Args:
630
+ batch: Batch of documents to check
631
+ class_name: Label to check in
632
+ match_keys: Keys to match nodes
633
+ keep_keys: Keys to keep in result
634
+ filters: Additional query filters
635
+
636
+ Raises:
637
+ NotImplementedError: This method is not implemented for Neo4j
638
+ """
639
+ raise NotImplementedError