@@ -315,13 +315,15 @@ def query(self, q: str, data_binding=None, single_row=False, single_cell="", sin
315315
316316
317317
318- def query_extended (self , q : str , params = None , flatten = False , fields_to_exclude = None ) -> [dict ]:
318+ def query_extended (self , q : str , data_binding = None , flatten = False , fields_to_exclude = None ) -> [dict ]:
319319 """
320- Extended version of query(), meant to extract additional info for queries that return Graph Data Types,
320+ Extended version of query(), meant to extract additional info
321+ for queries that return Graph Data Types,
321322 i.e. nodes, relationships or paths,
322323 such as "MATCH (n) RETURN n", or "MATCH (n1)-[r]->(n2) RETURN r"
323324
324- For example, useful in scenarios where nodes were returned, and their Neo4j internal IDs and/or labels are desired
325+ For example, useful in scenarios where nodes were returned,
326+ and their Neo4j internal IDs and/or labels are desired
325327 (in addition to all the properties and their values)
326328
327329 Unless the flatten flag is True, individual records are kept as separate lists.
@@ -334,7 +336,7 @@ def query_extended(self, q: str, params = None, flatten = False, fields_to_exclu
334336 Try running with flatten=True "MATCH (b:boat), (c:car) RETURN b, c" on data like "CREATE (b:boat), (c1:car1), (c2:car2)"
335337
336338 :param q: A Cypher query
337- :param params : An optional Cypher dictionary
339+ :param data_binding : An optional Cypher dictionary
338340 EXAMPLE, assuming that the cypher string contains the substring "$age":
339341 {'age': 20}
340342 :param flatten: Flag indicating whether the Graph Data Types need to remain clustered by record,
@@ -345,20 +347,20 @@ def query_extended(self, q: str, params = None, flatten = False, fields_to_exclu
345347 :return: A (possibly empty) list of dictionaries, if flatten is True,
346348 or a list of list, if flatten is False.
347349 Each item in the lists is a dictionary, with details that will depend on which Graph Data Types
348- were returned in the Cypher query.
349- EXAMPLE of individual items - for a returned NODE
350- {'gender': 'M', 'age': 20, 'internal_id': 123, 'neo4j_labels': ['patient']}
351- EXAMPLE of individual items - for a returned RELATIONSHIP
352- {'price': 7500, 'internal_id': 2,
353- 'neo4j_start_node': <Node id=11 labels=frozenset() properties={}>,
354- 'neo4j_end_node': <Node id=14 labels=frozenset() properties={}>,
355- 'neo4j_type': 'bought_by'}]
350+ were returned in the Cypher query.
351+ EXAMPLE of * individual items* - for a returned NODE
352+ {'gender': 'M', 'age': 20, 'internal_id': 123, 'neo4j_labels': ['patient']}
353+ EXAMPLE of * individual items* - for a returned RELATIONSHIP
354+ {'price': 7500, 'internal_id': 2,
355+ 'neo4j_start_node': <Node id=11 labels=frozenset() properties={}>,
356+ 'neo4j_end_node': <Node id=14 labels=frozenset() properties={}>,
357+ 'neo4j_type': 'bought_by'}]
356358 """
357359 # Start a new session, use it, and then immediately close it
358360 with self .driver .session () as new_session :
359- result = new_session .run (q , params )
361+ result = new_session .run (q , data_binding )
360362 if self .profiling :
361- print ("-- query_extended() PROFILING ----------\n " , q , "\n " , params )
363+ print ("-- query_extended() PROFILING ----------\n " , q , "\n " , data_binding )
362364
363365 # Note: A neo4j.Result iterable object (printing it, shows an object of type "neo4j.work.result.Result")
364366 # See https://neo4j.com/docs/api/python-driver/current/api.html#neo4j.Result
@@ -836,8 +838,6 @@ def get_node_internal_id(self, match: dict) -> int:
836838 (node , where , data_binding ) = CypherUtils .unpack_match (match_structure , include_dummy = False )
837839
838840 q = f"MATCH { node } { CypherUtils .prepare_where (where )} RETURN id(n) AS INTERNAL_ID"
839- print (q )
840- print (data_binding )
841841 self .debug_query_print (q , data_binding , "get_node_internal_id" )
842842
843843 result = self .query (q , data_binding , single_column = "INTERNAL_ID" )
@@ -879,11 +879,12 @@ def ________CREATE_NODES________(DIVIDER):
879879
880880 def create_node (self , labels , properties = None ) -> int :
881881 """
882- Create a new node with the given label(s) and with the attributes/values specified in the properties dictionary.
882+ Create a new node with the given label(s),
883+ and with the attributes/values specified in the properties dictionary.
883884 Return the Neo4j internal ID of the node just created.
884885
885886 :param labels: A string, or list/tuple of strings, specifying Neo4j labels (ok to have blank spaces)
886- :param properties: An optional (possibly empty or None) dictionary of properties to set for the new node.
887+ :param properties: OPTIONAL (possibly empty or None) dictionary of properties to set for the new node.
887888 EXAMPLE: {'age': 22, 'gender': 'F'}
888889
889890 :return: An integer with the Neo4j internal ID of the node just created
@@ -1030,7 +1031,7 @@ def create_attached_node(self, labels, properties = None,
10301031 def create_node_with_links (self , labels , properties = None , links = None , merge = False ) -> int :
10311032 """
10321033 Create a new node, with the given labels and optional properties,
1033- and make it a parent of all the EXISTING nodes that are specified
1034+ and link it up to all the EXISTING nodes that are specified
10341035 in the (possibly empty) list of link nodes, identified by their Neo4j internal ID's.
10351036
10361037 The list of link nodes also contains the names to give to each link,
@@ -1729,14 +1730,14 @@ def add_links(self, match_from: Union[int, dict], match_to: Union[int, dict], re
17291730
17301731 def add_links_fast (self , match_from : int , match_to : int , rel_name :str ) -> int :
17311732 """
1732- Experimental first method optimized for speed. Only internal database ID are used
1733+ Method optimized for speed. Only internal database ID are used.
17331734
17341735 Add a links (aka graph edges/relationships), with the specified rel_name,
17351736 originating in the node identified by match_from,
17361737 and terminating in the node identified by match_to
17371738
1738- :param match_from: An integer with a Neo4j node id
1739- :param match_to: An integer with a Neo4j node id
1739+ :param match_from: An integer with an internal Neo4j node id
1740+ :param match_to: An integer with an internal Neo4j node id
17401741 :param rel_name: The name to give to the new relationship between the 2 specified nodes. Blanks allowed
17411742
17421743 :return: The number of links added. If none got added, or in case of error, an Exception is raised
@@ -2131,20 +2132,21 @@ def count_links(self, match: Union[int, dict], rel_name: str, rel_dir="OUT", nei
21312132
21322133
21332134
2134- def get_parents_and_children (self , node_id : int ) -> ():
2135+ def get_parents_and_children (self , internal_id : int ) -> ():
21352136 """
21362137 Fetch all the nodes connected to the given one by INbound relationships to it (its "parents"),
21372138 as well as by OUTbound relationships to it (its "children")
2139+ TODO: allow specifying a relationship name to follow
21382140
2139- :param node_id : An integer with a Neo4j internal node ID
2140- :return: A dictionary with 2 keys: 'parent_list' and 'child_list'
2141- The values are lists of dictionaries with 3 keys: "internal_id", "label", "rel"
2142- EXAMPLE of individual items in either parent_list or child_list:
2143- {'internal_id': 163, 'labels': ['Subject'], 'rel': 'HAS_TREATMENT'}
2141+ :param internal_id : An integer with a Neo4j internal node ID
2142+ :return: A dictionary with 2 keys: 'parent_list' and 'child_list'
2143+ The values are lists of dictionaries with 3 keys: "internal_id", "label", "rel"
2144+ EXAMPLE of individual items in either parent_list or child_list:
2145+ {'internal_id': 163, 'labels': ['Subject'], 'rel': 'HAS_TREATMENT'}
21442146 """
21452147
21462148 # Fetch the parents
2147- cypher = f"MATCH (parent)-[inbound]->(n) WHERE id(n) = { node_id } " \
2149+ cypher = f"MATCH (parent)-[inbound]->(n) WHERE id(n) = { internal_id } " \
21482150 "RETURN id(parent) AS internal_id, labels(parent) AS labels, type(inbound) AS rel"
21492151
21502152 parent_list = self .query (cypher )
@@ -2154,7 +2156,7 @@ def get_parents_and_children(self, node_id: int) -> ():
21542156
21552157
21562158 # Fetch the children
2157- cypher = f"MATCH (n)-[outbound]->(child) WHERE id(n) = { node_id } " \
2159+ cypher = f"MATCH (n)-[outbound]->(child) WHERE id(n) = { internal_id } " \
21582160 "RETURN id(child) AS internal_id, labels(child) AS labels, type(outbound) AS rel"
21592161
21602162 child_list = self .query (cypher )
@@ -2167,6 +2169,56 @@ def get_parents_and_children(self, node_id: int) -> ():
21672169
21682170
21692171
2172+ def get_siblings (self , internal_id : int , rel_name : str , rel_dir = "OUT" ) -> [int ]:
2173+ """
2174+ Return the data of all the "sibling" nodes of the given one.
2175+ "Siblings" is meant as "sharing a link (by default outbound) of the specified name,
2176+ to a common other node".
2177+
2178+ EXAMPLE: 2 nodes, "French" and "German",
2179+ each with a outbound link named "subcategory_of" to a third node,
2180+ will be considered "siblings" under rel_name="subcategory_of" and rel_dir="OUT
2181+
2182+ :param internal_id: Integer with the internal database ID of the node of interest
2183+ :param rel_name: The name of the relationship used to establish a "siblings" connection
2184+ :param rel_dir: Either "OUT" (default) or "IN". The link direction expected from the
2185+ start node to its "parents" - and then IN REVERSE to the parent's children
2186+ :return: A list of dictionaries, with one element for each "sibling";
2187+ each element contains the 'internal_id' and 'neo4j_labels' keys,
2188+ plus whatever attributes are stored on that node.
2189+ EXAMPLE of single element:
2190+ {'name': 'French', 'internal_id': 123, 'neo4j_labels': ['Categories']}
2191+ """
2192+ CypherUtils .assert_valid_internal_id (internal_id )
2193+
2194+ assert type (rel_name ) == str , \
2195+ f"get_siblings(): argument `rel_name` must be a string; " \
2196+ f"the given value ({ rel_name } ) is of type { type (rel_name )} "
2197+
2198+ # Follow the links with the specified name, in the indicated direction from the given link,
2199+ # and then in the reverse direction
2200+ if rel_dir == "OUT" :
2201+ q = f"""
2202+ MATCH (n) - [:{ rel_name } ] -> (parent) <- [:{ rel_name } ] - (sibling)
2203+ WHERE id(n) = $internal_id
2204+ RETURN sibling
2205+ """
2206+ elif rel_dir == "IN" :
2207+ q = f"""
2208+ MATCH (n) <- [:{ rel_name } ] - (parent) - [:{ rel_name } ] -> (sibling)
2209+ WHERE id(n) = $internal_id
2210+ RETURN sibling
2211+ """
2212+ else :
2213+ raise Exception (f"get_siblings(): unknown value for the `rel_dir` argument ({ rel_dir } ); "
2214+ f"allowed values are 'IN' and 'OUT'" )
2215+
2216+ result = self .query_extended (q , data_binding = {"internal_id" : internal_id }, flatten = True )
2217+ return result
2218+
2219+
2220+
2221+
21702222
21712223 #####################################################################################################
21722224
@@ -2206,9 +2258,9 @@ def get_label_properties(self, label:str) -> list:
22062258 RETURN DISTINCT propertyName
22072259 ORDER BY propertyName
22082260 """
2209- params = {'label' : label }
2261+ data_binding = {'label' : label }
22102262
2211- return [res ['propertyName' ] for res in self .query (q , params )]
2263+ return [res ['propertyName' ] for res in self .query (q , data_binding )]
22122264
22132265
22142266
@@ -3044,4 +3096,4 @@ def _debug_local(self) -> str:
30443096
30453097 :return:
30463098 """
3047- return "remote "
3099+ return "local "
0 commit comments