Skip to content

Commit 458b365

Browse files
committed
unreachable entity discovery
1 parent dd8f33b commit 458b365

File tree

2 files changed

+80
-37
lines changed

2 files changed

+80
-37
lines changed

app.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,6 @@
1919
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
2020
logger = logging.getLogger(__name__)
2121

22-
def extract_org_name_from_url(url: str) -> Optional[tuple[str, str]]:
23-
components = url.split('/')
24-
n = len(components)
25-
26-
# https://github.com/falkordb/falkordb
27-
# Expecting atleast 4 components
28-
if n < 4:
29-
return None
30-
31-
return (components[n-2], components[n-1])
32-
3322
@app.route('/graph_entities', methods=['GET'])
3423
def graph_entities():
3524
"""
@@ -446,6 +435,39 @@ def find_paths():
446435
return jsonify(response), 200
447436

448437

438+
@app.route('/unreachable', methods=['POST'])
439+
def unreachable_entities():
440+
"""
441+
Endpoint to retrieve unreachable entities in the graph.
442+
Expects 'repo', optional 'label', and optional 'relation' as parameters in the POST request.
443+
444+
Returns:
445+
JSON response with unreachable entities or error message.
446+
"""
447+
448+
# Get JSON data from the request
449+
data = request.get_json()
450+
451+
# Validate 'repo' parameter
452+
repo = data.get('repo')
453+
if repo is None:
454+
return jsonify({'status': f'Missing mandatory parameter "repo"'}), 400
455+
456+
# Get optional 'label' and 'relation' parameters
457+
lbl = data.get('label', None)
458+
rel = data.get('relation', None)
459+
460+
# Initialize graph with provided repo and credentials
461+
g = Graph(repo)
462+
463+
# Fetch unreachable entities based on optional label and relation
464+
unreachable_entities = g.unreachable_entities(lbl, rel)
465+
466+
# Create and return a successful response
467+
response = { 'status': 'success', 'unreachables ': unreachable_entities }
468+
469+
return jsonify(response), 200
470+
449471
if __name__ == '__main__':
450472
app.run(debug=True)
451473

code_graph/graph.py

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def delete(self) -> None:
8080
self.g.delete()
8181

8282

83+
def _query(self, q: str, params: dict) -> QueryResult:
84+
return self.g.query(q, params)
85+
8386
def get_sub_graph(self, l: int) -> dict:
8487

8588
q = """MATCH (src)
@@ -89,7 +92,7 @@ def get_sub_graph(self, l: int) -> dict:
8992

9093
sub_graph = {'nodes': [], 'edges': [] }
9194

92-
result_set = self.g.query(q, {'limit': l}).result_set
95+
result_set = self._query(q, {'limit': l}).result_set
9396
for row in result_set:
9497
src = row[0]
9598
e = row[1]
@@ -137,7 +140,7 @@ def get_neighbors(self, node_id: int, rel: Optional[str] = None, lbl: Optional[s
137140

138141
try:
139142
# Execute the graph query with node_id parameter
140-
result_set = self.g.query(query, {'node_id': node_id}).result_set
143+
result_set = self._query(query, {'node_id': node_id}).result_set
141144

142145
# Iterate over the result set and process nodes and edges
143146
for edge, destination_node in result_set:
@@ -172,7 +175,7 @@ def add_class(self, c: Class) -> None:
172175
'src_end': c.src_end,
173176
}
174177

175-
res = self.g.query(q, params)
178+
res = self._query(q, params)
176179
c.id = res.result_set[0][0]
177180

178181
def _class_from_node(self, n: Node) -> Class:
@@ -193,7 +196,7 @@ def _class_from_node(self, n: Node) -> Class:
193196

194197
def get_class_by_name(self, class_name: str) -> Optional[Class]:
195198
q = "MATCH (c:Class) WHERE c.name = $name RETURN c LIMIT 1"
196-
res = self.g.query(q, {'name': class_name}).result_set
199+
res = self._query(q, {'name': class_name}).result_set
197200

198201
if len(res) == 0:
199202
return None
@@ -205,7 +208,7 @@ def get_class(self, class_id: int) -> Optional[Class]:
205208
WHERE ID(c) = $class_id
206209
RETURN c"""
207210

208-
res = self.g.query(q, {'class_id': class_id})
211+
res = self._query(q, {'class_id': class_id})
209212

210213
if len(res.result_set) == 0:
211214
return None
@@ -239,7 +242,7 @@ def add_function(self, func: Function) -> None:
239242
'ret_type': func.ret_type
240243
}
241244

242-
res = self.g.query(q, params)
245+
res = self._query(q, params)
243246
func.id = res.result_set[0][0]
244247

245248
def _function_from_node(self, n: Node) -> Function:
@@ -280,7 +283,7 @@ def set_functions_metadata(self, ids: List[int], metadata: List[dict]) -> None:
280283

281284
params = {'ids': ids, 'values': metadata}
282285

283-
self.g.query(q, params)
286+
self._query(q, params)
284287

285288
# get all functions defined by file
286289
def get_functions_in_file(self, path: str, name: str, ext: str) -> List[Function]:
@@ -289,13 +292,13 @@ def get_functions_in_file(self, path: str, name: str, ext: str) -> List[Function
289292
RETURN collect(func)"""
290293

291294
params = {'path': path, 'name': name, 'ext': ext}
292-
funcs = self.g.query(q, params).result_set[0][0]
295+
funcs = self._query(q, params).result_set[0][0]
293296

294297
return [self._function_from_node(n) for n in funcs]
295298

296299
def get_function_by_name(self, name: str) -> Optional[Function]:
297300
q = "MATCH (f:Function) WHERE f.name = $name RETURN f LIMIT 1"
298-
res = self.g.query(q, {'name': name}).result_set
301+
res = self._query(q, {'name': name}).result_set
299302

300303
if len(res) == 0:
301304
return None
@@ -332,7 +335,7 @@ def prefix_search(self, prefix: str) -> str:
332335

333336
try:
334337
# Execute the query using the provided graph database connection.
335-
completions = self.g.query(query, {'prefix': search_prefix}).result_set[0][0]
338+
completions = self._query(query, {'prefix': search_prefix}).result_set[0][0]
336339

337340
# Remove label Searchable from each node
338341
for node in completions:
@@ -354,7 +357,7 @@ def get_function(self, func_id: int) -> Optional[Function]:
354357
WHERE ID(f) = $func_id
355358
RETURN f"""
356359

357-
res = self.g.query(q, {'func_id': func_id})
360+
res = self._query(q, {'func_id': func_id})
358361

359362
if len(res.result_set) == 0:
360363
return None
@@ -369,7 +372,7 @@ def function_calls(self, func_id: int) -> List[Function]:
369372
MATCH (f)-[:CALLS]->(callee)
370373
RETURN callee"""
371374

372-
res = self.g.query(q, {'func_id': func_id})
375+
res = self._query(q, {'func_id': func_id})
373376

374377
callees = []
375378
for row in res.result_set:
@@ -384,7 +387,7 @@ def function_called_by(self, func_id: int) -> List[Function]:
384387
MATCH (caller)-[:CALLS]->(f)
385388
RETURN caller"""
386389

387-
res = self.g.query(q, {'func_id': func_id})
390+
res = self._query(q, {'func_id': func_id})
388391

389392
callers = []
390393
for row in res.result_set:
@@ -407,7 +410,7 @@ def add_file(self, file: File) -> None:
407410
RETURN ID(f)"""
408411
params = {'path': file.path, 'name': file.name, 'ext': file.ext}
409412

410-
res = self.g.query(q, params)
413+
res = self._query(q, params)
411414
file.id = res.result_set[0][0]
412415

413416
def delete_files(self, files: List[dict], log: bool = False) -> tuple[str, dict, List[int]]:
@@ -432,7 +435,7 @@ def delete_files(self, files: List[dict], log: bool = False) -> tuple[str, dict,
432435
"""
433436

434437
params = {'files': files}
435-
res = self.g.query(q, params)
438+
res = self._query(q, params)
436439

437440
if log and (res.relationships_deleted > 0 or res.nodes_deleted > 0):
438441
return (q, params)
@@ -464,7 +467,7 @@ def get_file(self, path: str, name: str, ext: str) -> Optional[File]:
464467
RETURN f"""
465468
params = {'path': path, 'name': name, 'ext': ext}
466469

467-
res = self.g.query(q, params)
470+
res = self._query(q, params)
468471
if(len(res.result_set) == 0):
469472
return None
470473

@@ -491,7 +494,7 @@ def set_file_coverage(self, path: str, name: str, ext: str, coverage: float) ->
491494

492495
params = {'path': path, 'name': name, 'ext': ext, 'coverage': coverage}
493496

494-
res = self.g.query(q, params)
497+
res = self._query(q, params)
495498

496499
def connect_entities(self, relation: str, src_id: int, dest_id: int) -> None:
497500
"""
@@ -507,7 +510,7 @@ def connect_entities(self, relation: str, src_id: int, dest_id: int) -> None:
507510
MERGE (src)-[:{relation}]->(dest)"""
508511

509512
params = {'src_id': src_id, 'dest_id': dest_id}
510-
self.g.query(q, params)
513+
self._query(q, params)
511514

512515
def function_calls_function(self, caller_id: int, callee_id: int, pos: int) -> None:
513516
"""
@@ -524,7 +527,7 @@ def function_calls_function(self, caller_id: int, callee_id: int, pos: int) -> N
524527
MERGE (caller)-[e:CALLS {pos:$pos}]->(callee)"""
525528

526529
params = {'caller_id': caller_id, 'callee_id': callee_id, 'pos': pos}
527-
self.g.query(q, params)
530+
self._query(q, params)
528531

529532
def add_struct(self, s: Struct) -> None:
530533
"""
@@ -548,7 +551,7 @@ def add_struct(self, s: Struct) -> None:
548551
'fields': s.fields
549552
}
550553

551-
res = self.g.query(q, params)
554+
res = self._query(q, params)
552555
s.id = res.result_set[0][0]
553556

554557
def _struct_from_node(self, n: Node) -> Struct:
@@ -578,7 +581,7 @@ def _struct_from_node(self, n: Node) -> Struct:
578581

579582
def get_struct_by_name(self, struct_name: str) -> Optional[Struct]:
580583
q = "MATCH (s:Struct) WHERE s.name = $name RETURN s LIMIT 1"
581-
res = self.g.query(q, {'name': struct_name}).result_set
584+
res = self._query(q, {'name': struct_name}).result_set
582585

583586
if len(res) == 0:
584587
return None
@@ -590,7 +593,7 @@ def get_struct(self, struct_id: int) -> Optional[Struct]:
590593
WHERE ID(s) = $struct_id
591594
RETURN s"""
592595

593-
res = self.g.query(q, {'struct_id': struct_id})
596+
res = self._query(q, {'struct_id': struct_id})
594597

595598
if len(res.result_set) == 0:
596599
return None
@@ -617,7 +620,7 @@ def rerun_query(self, q: str, params: dict) -> QueryResult:
617620
Re-run a query to transition the graph from one state to another
618621
"""
619622

620-
return self.g.query(q, params)
623+
return self._query(q, params)
621624

622625
def find_paths(self, src: int, dest: int) -> List[Path]:
623626
"""
@@ -644,7 +647,7 @@ def find_paths(self, src: int, dest: int) -> List[Path]:
644647
"""
645648

646649
# Perform the query with the source and destination node IDs.
647-
result_set = self.g.query(q, {'src_id': src, 'dest_id': dest}).result_set
650+
result_set = self._query(q, {'src_id': src, 'dest_id': dest}).result_set
648651

649652
paths = []
650653

@@ -665,11 +668,29 @@ def stats(self) -> dict:
665668
"""
666669

667670
q = "MATCH (n) RETURN count(n)"
668-
node_count = self.g.query(q).result_set[0][0]
671+
node_count = self._query(q).result_set[0][0]
669672

670673
q = "MATCH ()-[e]->() RETURN count(e)"
671-
edge_count = self.g.query(q).result_set[0][0]
674+
edge_count = self._query(q).result_set[0][0]
672675

673676
# Return the statistics
674677
return {'node_count': node_count, 'edge_count': edge_count}
675678

679+
def unreachable_entities(self, lbl: Optional[str], rel: Optional[str]) -> List[dict]:
680+
lbl = f": {lbl}" if lbl else ""
681+
rel = f": {rel}" if rel else ""
682+
683+
q = f""" MATCH (n {lbl})
684+
WHERE not ()-[{rel}]->(n)
685+
RETURN n
686+
"""
687+
688+
result_set = self._query(q).result_set
689+
690+
unreachables = []
691+
for row in result_set:
692+
node = row[0]
693+
unreachables.append(encode_node(node))
694+
695+
return unreachables
696+

0 commit comments

Comments
 (0)