codegraphcontext 0.6.8__py3-none-any.whl → 0.6.9__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.
@@ -26,6 +26,7 @@ from ..core import get_database_manager
26
26
  from ..core.jobs import JobManager
27
27
  from ..tools.code_finder import CodeFinder
28
28
  from ..tools.graph_builder import GraphBuilder
29
+ from ..utils import graph_shapes
29
30
  from ..tools.package_resolver import get_local_package_path
30
31
  from ..utils.debug_log import info_logger, warning_logger
31
32
  from ..core.database import Neo4jConnectionError
@@ -227,7 +228,14 @@ def _initialize_services(
227
228
  _fail_services_init()
228
229
  else:
229
230
  console.print(f"[bold red]Database Connection Error:[/bold red] {e}")
230
- console.print("Please ensure your database is configured correctly or run 'cgc doctor'.")
231
+ if "Could not set lock on file" in str(e):
232
+ console.print(
233
+ "[yellow]Another CGC process already has this embedded database open. "
234
+ "If a CGC Gateway or watcher is running, use its MCP/HTTP interface "
235
+ "or stop it before running database-backed CLI commands.[/yellow]"
236
+ )
237
+ else:
238
+ console.print("Please ensure your database is configured correctly or run 'cgc doctor'.")
231
239
  _fail_services_init()
232
240
 
233
241
  # The GraphBuilder requires an event loop, even for synchronous-style execution
@@ -657,44 +665,13 @@ def _render_offline_visualization(
657
665
  def _ident(value) -> Optional[str]:
658
666
  return None if value is None else str(value)
659
667
 
660
- # Kùzu spells the internal record keys in lowercase (_label/_src/_dst/_id);
661
- # Ladybug returns the same fields uppercased (_LABEL/_SRC/_DST/_ID). Look
662
- # up both spellings so either backend reaches the offline renderer (#1458).
663
- def _meta(d: dict, key: str, default=None):
664
- if key in d:
665
- return d[key]
666
- return d.get(key.upper(), default)
667
-
668
- def _has_meta(d: dict, key: str) -> bool:
669
- return key in d or key.upper() in d
670
-
671
- # Neo4j returns driver objects carrying .labels / .type that support
672
- # dict()/Mapping access. FalkorDB's own driver objects also carry
673
- # .labels on nodes, but are NOT dict-convertible — their properties live
674
- # in a plain `.properties` dict, and its Edge exposes `.relation` /
675
- # `.src_node` / `.dest_node` instead of `.type` / `.start_node` /
676
- # `.end_node`. Kùzu and Ladybug return plain dicts carrying _label plus
677
- # _src/_dst. Check the driver attributes first — a driver object may
678
- # also be dict-like.
679
- def _is_relationship(value) -> bool:
680
- if hasattr(value, "labels"):
681
- return False
682
- if hasattr(value, "type"):
683
- return True
684
- if hasattr(value, "relation") and hasattr(value, "src_node"):
685
- return True
686
- return isinstance(value, dict) and _has_meta(value, "_src") and _has_meta(value, "_dst")
687
-
688
- def _is_node(value) -> bool:
689
- if hasattr(value, "labels"):
690
- return True
691
- if hasattr(value, "type"):
692
- return False
693
- if hasattr(value, "relation") and hasattr(value, "src_node"):
694
- return False
695
- # Kùzu relationships also carry _label, so _src/_dst is what
696
- # distinguishes them from nodes.
697
- return isinstance(value, dict) and _has_meta(value, "_label") and not _has_meta(value, "_src")
668
+ # Backend shape handling — which driver returns what, and why each spelling
669
+ # has to be accepted — lives in utils/graph_shapes, shared with the live
670
+ # web renderer in viz/server.py so the two cannot drift apart again.
671
+ _meta = graph_shapes.meta
672
+ _has_meta = graph_shapes.has_meta
673
+ _is_relationship = graph_shapes.is_relationship
674
+ _is_node = graph_shapes.is_node
698
675
 
699
676
  def _node_payload(value) -> Dict[str, Any]:
700
677
  if hasattr(value, "labels"):
@@ -709,10 +686,19 @@ def _render_offline_visualization(
709
686
  node_id = _meta(value, "_id")
710
687
  if node_id is None:
711
688
  node_id = props.get("path") or props.get("name")
689
+
690
+ name = props.get("name") or props.get("path") or ""
691
+ if name == "<module>":
692
+ _fpath = props.get("path") or props.get("file_path") or ""
693
+ if _fpath:
694
+ name = Path(_fpath).name
695
+ else:
696
+ name = "<module>"
697
+
712
698
  return {
713
699
  "id": _ident(node_id),
714
700
  "label": label,
715
- "name": props.get("name") or props.get("path") or "",
701
+ "name": name,
716
702
  "file_path": props.get("path", ""),
717
703
  "line_number": props.get("line_number"),
718
704
  }
@@ -2546,16 +2546,17 @@ def find_by_decorator_search(
2546
2546
 
2547
2547
  @find_app.command("argument")
2548
2548
  def find_by_argument_search(
2549
- argument: str = typer.Argument(..., help="Argument/parameter name to search for"),
2549
+ argument: str = typer.Argument(..., help="Argument/parameter name or type to search for"),
2550
2550
  file: Optional[str] = typer.Option(None, "--file", "-f", help="Specific file path"),
2551
2551
  context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
2552
2552
  ):
2553
2553
  """
2554
- Find functions that take a specific argument/parameter.
2554
+ Find functions that take a specific argument/parameter name or type.
2555
2555
 
2556
2556
  Examples:
2557
2557
  cgc find argument password
2558
2558
  cgc find argument user_id --file src/auth.py
2559
+ cgc find argument OrderFilter
2559
2560
  """
2560
2561
  _load_credentials()
2561
2562
  services = _initialize_services(context)
@@ -2571,7 +2572,7 @@ def find_by_argument_search(
2571
2572
  results = results[:req_limit]
2572
2573
 
2573
2574
  if not results:
2574
- console.print(f"[yellow]No functions found with argument '{argument}'[/yellow]")
2575
+ console.print(f"[yellow]No functions found with argument name or type '{argument}'[/yellow]")
2575
2576
  return
2576
2577
 
2577
2578
  table = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
@@ -3635,6 +3636,8 @@ def datasource_mysql(
3635
3636
  database: str = typer.Option(..., "--database", "-d", help="Database / schema name"),
3636
3637
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Logical datasource name (default: mysql-<database>)"),
3637
3638
  env: str = typer.Option("production", "--env", "-e", help="Deployment environment label"),
3639
+ ssl_verify: bool = typer.Option(False, "--ssl-verify", help="Connect over TLS and verify the server certificate (#1094)"),
3640
+ ssl_ca_certs: Optional[str] = typer.Option(None, "--ssl-ca-certs", help="CA bundle path for --ssl-verify (default: system trust store)"),
3638
3641
  context: Optional[str] = typer.Option(None, "--context", "-c", help="CGC context to use"),
3639
3642
  ):
3640
3643
  """Ingest Aurora MySQL schema (tables + columns) and write to the code graph.
@@ -3650,7 +3653,8 @@ def datasource_mysql(
3650
3653
  console.print(f"[cyan]Connecting to Aurora MySQL at {host}:{port}/{database}...[/cyan]")
3651
3654
  try:
3652
3655
  result = mysql_ingest(host=host, port=port, user=user, password=password,
3653
- database=database, name=name, env=env)
3656
+ database=database, name=name, env=env,
3657
+ ssl_verify=ssl_verify, ssl_ca_certs=ssl_ca_certs)
3654
3658
  except Exception as exc:
3655
3659
  console.print(f"[red]Failed to connect / ingest:[/red] {exc}")
3656
3660
  raise typer.Exit(1)
@@ -3672,6 +3676,8 @@ def datasource_cassandra(
3672
3676
  password: Optional[str] = typer.Option(None, "--password", "-P", help="Cassandra password", hide_input=True),
3673
3677
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Logical datasource name (default: cassandra-<keyspace>)"),
3674
3678
  env: str = typer.Option("production", "--env", "-e", help="Deployment environment label"),
3679
+ ssl_verify: bool = typer.Option(False, "--ssl-verify", help="Connect over TLS and verify the server certificate (#1094)"),
3680
+ ssl_ca_certs: Optional[str] = typer.Option(None, "--ssl-ca-certs", help="CA bundle path for --ssl-verify (default: system trust store)"),
3675
3681
  context: Optional[str] = typer.Option(None, "--context", "-c", help="CGC context to use"),
3676
3682
  ):
3677
3683
  """Ingest Cassandra keyspace schema (tables + columns) and write to the code graph.
@@ -3688,7 +3694,8 @@ def datasource_cassandra(
3688
3694
  console.print(f"[cyan]Connecting to Cassandra at {hosts}/{keyspace}...[/cyan]")
3689
3695
  try:
3690
3696
  result = cassandra_ingest(hosts=hosts, port=port, keyspace=keyspace,
3691
- username=username, password=password, name=name, env=env)
3697
+ username=username, password=password, name=name, env=env,
3698
+ ssl_verify=ssl_verify, ssl_ca_certs=ssl_ca_certs)
3692
3699
  except Exception as exc:
3693
3700
  console.print(f"[red]Failed to connect / ingest:[/red] {exc}")
3694
3701
  raise typer.Exit(1)
@@ -7,6 +7,7 @@ import os
7
7
  from pathlib import Path
8
8
  import time
9
9
  import json
10
+ import re
10
11
  import sys
11
12
  import shutil
12
13
  import yaml
@@ -15,6 +16,48 @@ from codegraphcontext.cli.config_manager import normalize_config_path
15
16
 
16
17
  console = Console()
17
18
 
19
+ def _strip_jsonc(text: str) -> str:
20
+ """Drop // and /* */ comments and trailing commas from a JSONC document.
21
+
22
+ VS Code and its forks write settings.json as JSONC. json.loads rejects both,
23
+ and treating that rejection as "no settings yet" discards the user's file.
24
+ """
25
+ out = []
26
+ i, n = 0, len(text)
27
+ in_string = escaped = False
28
+ while i < n:
29
+ ch = text[i]
30
+ if in_string:
31
+ out.append(ch)
32
+ if escaped:
33
+ escaped = False
34
+ elif ch == "\\":
35
+ escaped = True
36
+ elif ch == '"':
37
+ in_string = False
38
+ i += 1
39
+ continue
40
+ if ch == '"':
41
+ in_string = True
42
+ out.append(ch)
43
+ i += 1
44
+ continue
45
+ if text.startswith("//", i):
46
+ i = text.find("\n", i)
47
+ if i == -1:
48
+ break
49
+ continue
50
+ if text.startswith("/*", i):
51
+ end = text.find("*/", i + 2)
52
+ i = n if end == -1 else end + 2
53
+ continue
54
+ out.append(ch)
55
+ i += 1
56
+ stripped = "".join(out)
57
+ # A trailing comma before } or ] is legal JSONC, not JSON.
58
+ return re.sub(r",(\s*[}\]])", r"\1", stripped)
59
+
60
+
18
61
  def _check_write_access(path: Path) -> bool:
19
62
  target = path if path.exists() else path.parent
20
63
 
@@ -483,12 +526,26 @@ def _configure_ide(mcp_config):
483
526
 
484
527
  console.print(f"Using configuration file at: {target_path}")
485
528
 
529
+ had_comments = False
486
530
  try:
487
- with open(target_path, "r") as f:
531
+ raw = target_path.read_text()
532
+ try:
533
+ settings = json.loads(raw)
534
+ except json.JSONDecodeError:
488
535
  try:
489
- settings = json.load(f)
536
+ settings = json.loads(_strip_jsonc(raw))
537
+ had_comments = True
490
538
  except json.JSONDecodeError:
491
- settings = {}
539
+ # Never fall back to {}: this file gets written back below, so
540
+ # treating an unreadable file as an empty one destroys it.
541
+ console.print(
542
+ f"[bold red]Could not parse {target_path}.[/bold red] "
543
+ "Leaving it untouched so nothing is lost."
544
+ )
545
+ console.print(
546
+ "Please add the MCP configuration manually from the `mcp.json` file generated above."
547
+ )
548
+ return
492
549
  except FileNotFoundError:
493
550
  settings = {}
494
551
 
@@ -515,6 +572,15 @@ def _configure_ide(mcp_config):
515
572
  )
516
573
  return
517
574
 
575
+ if target_path.exists():
576
+ backup_path = target_path.with_suffix(target_path.suffix + ".cgc-backup")
577
+ shutil.copy2(target_path, backup_path)
578
+ console.print(f"[dim]Backed up existing configuration to {backup_path}[/dim]")
579
+ if had_comments:
580
+ console.print(
581
+ "[yellow]Note: this file contained comments. JSON output cannot keep them, "
582
+ "so they are dropped in the rewritten file (the backup above still has them).[/yellow]"
583
+ )
518
584
  try:
519
585
  with open(target_path, "w") as f:
520
586
  json.dump(settings, f, indent=2)
@@ -64,6 +64,32 @@ def find_cgcignore(ignore_root: Path, explicit_path: Optional[str] = None) -> Op
64
64
  return explicit_candidate
65
65
  return None
66
66
 
67
+ def find_gitignore(ignore_root: Path) -> Optional[Path]:
68
+ """Find a repository-local .gitignore, searched the same way as .cgcignore.
69
+
70
+ Bounded to the git worktree for the same reason find_cgcignore is: an indexed
71
+ path outside a repo must not pick up something like /tmp/.gitignore.
72
+ """
73
+ git_root: Optional[Path] = None
74
+ probe = ignore_root
75
+ while True:
76
+ if (probe / ".git").exists():
77
+ git_root = probe
78
+ break
79
+ if probe.parent == probe:
80
+ break
81
+ probe = probe.parent
82
+
83
+ curr = ignore_root
84
+ while True:
85
+ candidate = curr / ".gitignore"
86
+ if candidate.is_file():
87
+ return candidate
88
+ if git_root is None or curr == git_root:
89
+ return None
90
+ curr = curr.parent
91
+
92
+
67
93
  def ensure_default_cgcignore(path: Path, default_patterns: list[str]) -> None:
68
94
  """Create a .cgcignore file with default patterns when it does not exist."""
69
95
  path.parent.mkdir(parents=True, exist_ok=True)
@@ -265,9 +291,23 @@ def build_ignore_spec(
265
291
  parse_cgcignore_lines(explicit_cgcignore_path.read_text(encoding="utf-8").splitlines())
266
292
  )
267
293
 
294
+ gitignore_patterns: list[str] = []
295
+ gitignore_path = find_gitignore(ignore_root)
296
+ if gitignore_path is not None:
297
+ try:
298
+ gitignore_patterns = parse_cgcignore_lines(
299
+ gitignore_path.read_text(encoding="utf-8").splitlines()
300
+ )
301
+ except OSError as e:
302
+ warning_logger(f"Failed to read {gitignore_path}: {e}")
303
+
268
304
  # Defaults first, user patterns last: matching is last-match-wins (see
269
305
  # CGCIgnoreMatcher.match_file), so appending the defaults would make them
270
306
  # unconditionally override the user's rules and leave `!build/` with no way
271
307
  # to re-include a directory the built-in list ignores.
272
- all_patterns = list(default_patterns) + merged_user_patterns
308
+ #
309
+ # .gitignore sits between the two for the same reason: what git ignores is
310
+ # ignored by default, while a `!pattern` in .cgcignore can still bring a
311
+ # git-ignored path back into the graph.
312
+ all_patterns = list(default_patterns) + gitignore_patterns + merged_user_patterns
273
313
  return CGCIgnoreMatcher(all_patterns, ignore_root), local_cgcignore_path
@@ -174,7 +174,7 @@ class EmbeddedGraphManager(GraphQueryInterface):
174
174
  ("Directory", "path STRING, name STRING, PRIMARY KEY (path)"),
175
175
  ("Module", "name STRING, lang STRING, full_import_name STRING, path STRING, line_number INT64, PRIMARY KEY (name)"),
176
176
  # For types with composite keys (name, path, line_number), we use a 'uid'
177
- ("Function", "uid STRING, name STRING, path STRING, line_number INT64, occurrence_index INT64, end_line INT64, source STRING, docstring STRING, lang STRING, cyclomatic_complexity INT64, context STRING, context_type STRING, class_context STRING, class_context_line INT64, module_context STRING, is_dependency BOOLEAN, decorators STRING[], args STRING[], http_method STRING, http_path STRING, embedding DOUBLE[], visibility STRING, modifiers STRING[], is_composable BOOLEAN, PRIMARY KEY (uid)"),
177
+ ("Function", "uid STRING, name STRING, path STRING, line_number INT64, occurrence_index INT64, end_line INT64, source STRING, docstring STRING, lang STRING, cyclomatic_complexity INT64, context STRING, context_type STRING, class_context STRING, class_context_line INT64, module_context STRING, is_dependency BOOLEAN, decorators STRING[], args STRING[], arg_types STRING[], http_method STRING, http_path STRING, embedding DOUBLE[], visibility STRING, modifiers STRING[], is_composable BOOLEAN, PRIMARY KEY (uid)"),
178
178
  ("Class", "uid STRING, name STRING, path STRING, line_number INT64, occurrence_index INT64, end_line INT64, source STRING, docstring STRING, lang STRING, node_type STRING, is_dependency BOOLEAN, decorators STRING[], visibility STRING, modifiers STRING[], PRIMARY KEY (uid)"),
179
179
  ("Variable", "uid STRING, name STRING, path STRING, line_number INT64, occurrence_index INT64, source STRING, docstring STRING, lang STRING, value STRING, context STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
180
180
  ("Trait", "uid STRING, name STRING, path STRING, line_number INT64, occurrence_index INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
@@ -385,6 +385,7 @@ class EmbeddedGraphManager(GraphQueryInterface):
385
385
  ("Function", "is_composable", "BOOLEAN"),
386
386
  ("Function", "http_method", "STRING"),
387
387
  ("Function", "http_path", "STRING"),
388
+ ("Function", "arg_types", "STRING[]"),
388
389
  # Kotlin/JVM precision improvements
389
390
  ("Function", "class_context_line", "INT64"),
390
391
  ("Function", "module_context", "STRING"),
@@ -915,7 +916,7 @@ class EmbeddedSessionWrapper:
915
916
  'File': {'path', 'name', 'relative_path', 'package_name', 'language', 'is_dependency'},
916
917
  'Directory': {'path', 'name'},
917
918
  'Module': {'name', 'lang', 'full_import_name', 'path', 'line_number'},
918
- 'Function': {'uid', 'name', 'path', 'line_number', 'occurrence_index', 'end_line', 'source', 'docstring', 'lang', 'cyclomatic_complexity', 'context', 'context_type', 'class_context', 'class_context_line', 'module_context', 'is_dependency', 'decorators', 'args', 'http_method', 'http_path', 'visibility', 'modifiers', 'is_composable'},
919
+ 'Function': {'uid', 'name', 'path', 'line_number', 'occurrence_index', 'end_line', 'source', 'docstring', 'lang', 'cyclomatic_complexity', 'context', 'context_type', 'class_context', 'class_context_line', 'module_context', 'is_dependency', 'decorators', 'args', 'arg_types', 'http_method', 'http_path', 'visibility', 'modifiers', 'is_composable'},
919
920
  'Class': {'uid', 'name', 'path', 'line_number', 'occurrence_index', 'end_line', 'source', 'docstring', 'lang', 'node_type', 'is_dependency', 'decorators', 'visibility', 'modifiers'},
920
921
  'Variable': {'uid', 'name', 'path', 'line_number', 'occurrence_index', 'source', 'docstring', 'lang', 'value', 'context', 'is_dependency'},
921
922
  'Trait': {'uid', 'name', 'path', 'line_number', 'occurrence_index', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
@@ -110,7 +110,7 @@ TOOLS = {
110
110
  },
111
111
  "target": {
112
112
  "type": "string",
113
- "description": "The primary target for the query (e.g., function name, class name, or 'start_func->end_func' for call chains)."
113
+ "description": "The primary query target (for example, a function name, class name, or 'start_func->end_func' for call chains). For find_functions_by_argument, use a parameter name or type."
114
114
  },
115
115
  "context": {
116
116
  "type": "string",
@@ -635,35 +635,36 @@ class CodeFinder:
635
635
  return results
636
636
 
637
637
  def find_functions_by_argument(self, argument_name: str, path: Optional[str] = None, repo_path: Optional[str] = None, limit: Optional[int] = None) -> List[Dict]:
638
- """Find functions that take a specific argument name."""
638
+ """Find functions that take a specific argument name or type."""
639
639
  repo_path = self._normalize_repo_path_filter(repo_path)
640
640
  with self.driver.session() as session:
641
+ path_filter = "AND f.path = $path" if path else ""
641
642
  repo_filter = "AND f.path STARTS WITH $repo_path" if repo_path else ""
642
643
  limit_clause = "LIMIT $limit" if limit is not None else ""
643
644
  params = {"argument_name": argument_name, "repo_path": repo_path}
644
- if limit is not None:
645
- params["limit"] = limit
646
645
  if path:
647
646
  params["path"] = path
648
- query = f"""
649
- MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
650
- WHERE p.name = $argument_name AND f.path = $path {repo_filter}
651
- RETURN f.name AS function_name, f.path AS path, f.line_number AS line_number,
652
- f.docstring AS docstring, f.is_dependency AS is_dependency
653
- ORDER BY f.is_dependency ASC, f.path, f.line_number
654
- {limit_clause}
655
- """
656
- result = session.run(query, **params)
657
- else:
658
- query = f"""
659
- MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
660
- WHERE p.name = $argument_name {repo_filter}
661
- RETURN f.name AS function_name, f.path AS path, f.line_number AS line_number,
662
- f.docstring AS docstring, f.is_dependency AS is_dependency
663
- ORDER BY f.is_dependency ASC, f.path, f.line_number
664
- {limit_clause}
665
- """
666
- result = session.run(query, **params)
647
+ if limit is not None:
648
+ params["limit"] = limit
649
+
650
+ # A WHERE attached to an OPTIONAL MATCH only constrains the
651
+ # optional part — it can never filter f, so the original form
652
+ # returned EVERY function for any search term. Filter f itself
653
+ # after materializing the optional parameter match.
654
+ query = f"""
655
+ MATCH (f:Function)
656
+ WHERE 1=1 {path_filter} {repo_filter}
657
+ OPTIONAL MATCH (f)-[:HAS_PARAMETER]->(p:Parameter {{name: $argument_name}})
658
+ WITH DISTINCT f, p
659
+ WHERE p IS NOT NULL
660
+ OR (f.arg_types IS NOT NULL AND $argument_name IN f.arg_types)
661
+ RETURN DISTINCT f.name AS function_name, f.path AS path,
662
+ f.line_number AS line_number, f.docstring AS docstring,
663
+ f.is_dependency AS is_dependency
664
+ ORDER BY is_dependency ASC, path, line_number
665
+ {limit_clause}
666
+ """
667
+ result = session.run(query, **params)
667
668
  return result.data()
668
669
 
669
670
  def find_functions_by_decorator(self, decorator_name: str, path: Optional[str] = None, repo_path: Optional[str] = None, limit: Optional[int] = None) -> List[Dict]:
@@ -1,100 +1,119 @@
1
- # src/codegraphcontext/tools/datasources/cassandra_ingester.py
2
- """Cassandra schema ingester (#843).
3
-
4
- Pulls keyspace + table + column metadata from system_schema and writes
5
- Datasource / DbTable / DbColumn nodes.
6
-
7
- Requires: pip install cassandra-driver
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from typing import Any, Dict, List, Optional
13
-
14
-
15
- def ingest(
16
- hosts: List[str],
17
- port: int,
18
- keyspace: str,
19
- username: Optional[str] = None,
20
- password: Optional[str] = None,
21
- name: Optional[str] = None,
22
- env: str = "production",
23
- ) -> Dict[str, Any]:
24
- """Fetch schema from Cassandra system_schema and return a datasource graph dict.
25
-
26
- Returns:
27
- {
28
- datasource: {name, kind, host, env, keyspace},
29
- tables: [{name, fqn, datasource_name}],
30
- columns: [{name, type, nullable, table_fqn, datasource_name}],
31
- }
32
- """
33
- try:
34
- from cassandra.cluster import Cluster
35
- from cassandra.auth import PlainTextAuthProvider
36
- except ImportError:
37
- raise ImportError(
38
- "Cassandra driver not found. Install with: pip install cassandra-driver"
39
- )
40
-
41
- datasource_name = name or f"cassandra-{keyspace}"
42
-
43
- auth_provider = None
44
- if username and password:
45
- auth_provider = PlainTextAuthProvider(username=username, password=password)
46
-
47
- cluster = Cluster(hosts, port=port, auth_provider=auth_provider)
48
- session = cluster.connect()
49
-
50
- try:
51
- tables: List[Dict[str, Any]] = []
52
- columns: List[Dict[str, Any]] = []
53
-
54
- # Tables from system_schema
55
- rows = session.execute(
56
- "SELECT table_name, comment FROM system_schema.tables WHERE keyspace_name = %s",
57
- (keyspace,),
58
- )
59
- for row in rows:
60
- fqn = f"{keyspace}.{row.table_name}"
61
- tables.append({
62
- "name": row.table_name,
63
- "fqn": fqn,
64
- "datasource_name": datasource_name,
65
- "comment": getattr(row, "comment", ""),
66
- })
67
-
68
- # Columns from system_schema
69
- rows = session.execute(
70
- """SELECT table_name, column_name, type, kind
71
- FROM system_schema.columns
72
- WHERE keyspace_name = %s""",
73
- (keyspace,),
74
- )
75
- for row in rows:
76
- table_fqn = f"{keyspace}.{row.table_name}"
77
- columns.append({
78
- "name": row.column_name,
79
- "type": row.type,
80
- "nullable": True, # Cassandra columns are always nullable except PK
81
- "table_fqn": table_fqn,
82
- "datasource_name": datasource_name,
83
- "is_primary_key": row.kind in ("partition_key", "clustering"),
84
- })
85
-
86
- finally:
87
- session.shutdown()
88
- cluster.shutdown()
89
-
90
- return {
91
- "datasource": {
92
- "name": datasource_name,
93
- "kind": "cassandra",
94
- "host": hosts[0],
95
- "env": env,
96
- "keyspace": keyspace,
97
- },
98
- "tables": tables,
99
- "columns": columns,
100
- }
1
+ # src/codegraphcontext/tools/datasources/cassandra_ingester.py
2
+ """Cassandra schema ingester (#843).
3
+
4
+ Pulls keyspace + table + column metadata from system_schema and writes
5
+ Datasource / DbTable / DbColumn nodes.
6
+
7
+ Requires: pip install cassandra-driver
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict, List, Optional
13
+
14
+
15
+ def ingest(
16
+ hosts: List[str],
17
+ port: int,
18
+ keyspace: str,
19
+ username: Optional[str] = None,
20
+ password: Optional[str] = None,
21
+ name: Optional[str] = None,
22
+ env: str = "production",
23
+ ssl_verify: bool = False,
24
+ ssl_ca_certs: Optional[str] = None,
25
+ ) -> Dict[str, Any]:
26
+ """Fetch schema from Cassandra system_schema and return a datasource graph dict.
27
+
28
+ Returns:
29
+ {
30
+ datasource: {name, kind, host, env, keyspace},
31
+ tables: [{name, fqn, datasource_name}],
32
+ columns: [{name, type, nullable, table_fqn, datasource_name}],
33
+ }
34
+ """
35
+ try:
36
+ from cassandra.cluster import Cluster
37
+ from cassandra.auth import PlainTextAuthProvider
38
+ except ImportError:
39
+ raise ImportError(
40
+ "Cassandra driver not found. Install with: pip install cassandra-driver"
41
+ )
42
+
43
+ datasource_name = name or f"cassandra-{keyspace}"
44
+
45
+ auth_provider = None
46
+ if username and password:
47
+ auth_provider = PlainTextAuthProvider(username=username, password=password)
48
+
49
+ ssl_context = None
50
+ if ssl_verify:
51
+ import ssl
52
+
53
+ ssl_context = ssl.create_default_context(
54
+ cafile=ssl_ca_certs if ssl_ca_certs else None
55
+ )
56
+ if not ssl_ca_certs:
57
+ # No explicit CA bundle: verify against system trust store.
58
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
59
+ ssl_context.check_hostname = True
60
+
61
+ cluster = Cluster(
62
+ hosts,
63
+ port=port,
64
+ auth_provider=auth_provider,
65
+ ssl_context=ssl_context,
66
+ )
67
+ session = cluster.connect()
68
+
69
+ try:
70
+ tables: List[Dict[str, Any]] = []
71
+ columns: List[Dict[str, Any]] = []
72
+
73
+ # Tables from system_schema
74
+ rows = session.execute(
75
+ "SELECT table_name, comment FROM system_schema.tables WHERE keyspace_name = %s",
76
+ (keyspace,),
77
+ )
78
+ for row in rows:
79
+ fqn = f"{keyspace}.{row.table_name}"
80
+ tables.append({
81
+ "name": row.table_name,
82
+ "fqn": fqn,
83
+ "datasource_name": datasource_name,
84
+ "comment": getattr(row, "comment", ""),
85
+ })
86
+
87
+ # Columns from system_schema
88
+ rows = session.execute(
89
+ """SELECT table_name, column_name, type, kind
90
+ FROM system_schema.columns
91
+ WHERE keyspace_name = %s""",
92
+ (keyspace,),
93
+ )
94
+ for row in rows:
95
+ table_fqn = f"{keyspace}.{row.table_name}"
96
+ columns.append({
97
+ "name": row.column_name,
98
+ "type": row.type,
99
+ "nullable": True, # Cassandra columns are always nullable except PK
100
+ "table_fqn": table_fqn,
101
+ "datasource_name": datasource_name,
102
+ "is_primary_key": row.kind in ("partition_key", "clustering"),
103
+ })
104
+
105
+ finally:
106
+ session.shutdown()
107
+ cluster.shutdown()
108
+
109
+ return {
110
+ "datasource": {
111
+ "name": datasource_name,
112
+ "kind": "cassandra",
113
+ "host": hosts[0],
114
+ "env": env,
115
+ "keyspace": keyspace,
116
+ },
117
+ "tables": tables,
118
+ "columns": columns,
119
+ }
@@ -1,121 +1,143 @@
1
- # src/codegraphcontext/tools/datasources/mysql_ingester.py
2
- """Aurora MySQL schema ingester (#843).
3
-
4
- Pulls table + column metadata from information_schema and writes
5
- Datasource / DbTable / DbColumn nodes with HAS_COLUMN and STORED_IN edges.
6
-
7
- Requires: pip install PyMySQL (or mysql-connector-python)
8
- Connection URI: mysql://user:pass@host:3306/database
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- from typing import Any, Dict, List, Optional
14
-
15
-
16
- def _connect(host: str, port: int, user: str, password: str, database: str) -> Any:
17
- """Return a DB-API 2.0 connection. Tries PyMySQL then mysql-connector-python."""
18
- try:
19
- import pymysql
20
- return pymysql.connect(
21
- host=host, port=port, user=user, password=password, database=database,
22
- cursorclass=pymysql.cursors.DictCursor, connect_timeout=10,
23
- )
24
- except ImportError:
25
- pass
26
- try:
27
- import mysql.connector
28
- return mysql.connector.connect(
29
- host=host, port=port, user=user, password=password, database=database,
30
- connection_timeout=10,
31
- )
32
- except ImportError:
33
- raise ImportError(
34
- "MySQL driver not found. Install with: pip install PyMySQL"
35
- )
36
-
37
-
38
- def ingest(
39
- host: str,
40
- port: int,
41
- user: str,
42
- password: str,
43
- database: str,
44
- name: Optional[str] = None,
45
- env: str = "production",
46
- ) -> Dict[str, Any]:
47
- """Fetch schema from Aurora MySQL and return a datasource graph dict.
48
-
49
- Returns:
50
- {
51
- datasource: {name, kind, host, env, database},
52
- tables: [{name, fqn, datasource_name}],
53
- columns: [{name, type, nullable, table_fqn, datasource_name}],
54
- }
55
- """
56
- datasource_name = name or f"mysql-{database}"
57
- conn = _connect(host, port, user, password, database)
58
-
59
- try:
60
- tables: List[Dict[str, Any]] = []
61
- columns: List[Dict[str, Any]] = []
62
-
63
- with conn.cursor() as cur:
64
- # Tables
65
- cur.execute(
66
- """
67
- SELECT TABLE_NAME, TABLE_TYPE, TABLE_ROWS, TABLE_COMMENT
68
- FROM information_schema.TABLES
69
- WHERE TABLE_SCHEMA = %s
70
- ORDER BY TABLE_NAME
71
- """,
72
- (database,),
73
- )
74
- for row in cur.fetchall():
75
- fqn = f"{database}.{row['TABLE_NAME']}"
76
- tables.append({
77
- "name": row["TABLE_NAME"],
78
- "fqn": fqn,
79
- "datasource_name": datasource_name,
80
- "table_type": row.get("TABLE_TYPE", "BASE TABLE"),
81
- "estimated_rows": row.get("TABLE_ROWS"),
82
- "comment": row.get("TABLE_COMMENT", ""),
83
- })
84
-
85
- # Columns
86
- cur.execute(
87
- """
88
- SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE,
89
- COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_KEY
90
- FROM information_schema.COLUMNS
91
- WHERE TABLE_SCHEMA = %s
92
- ORDER BY TABLE_NAME, ORDINAL_POSITION
93
- """,
94
- (database,),
95
- )
96
- for row in cur.fetchall():
97
- table_fqn = f"{database}.{row['TABLE_NAME']}"
98
- columns.append({
99
- "name": row["COLUMN_NAME"],
100
- "type": row["DATA_TYPE"],
101
- "nullable": row["IS_NULLABLE"] == "YES",
102
- "table_fqn": table_fqn,
103
- "datasource_name": datasource_name,
104
- "is_primary_key": row.get("COLUMN_KEY") == "PRI",
105
- "comment": row.get("COLUMN_COMMENT", ""),
106
- })
107
-
108
- finally:
109
- conn.close()
110
-
111
- return {
112
- "datasource": {
113
- "name": datasource_name,
114
- "kind": "mysql",
115
- "host": host,
116
- "env": env,
117
- "database": database,
118
- },
119
- "tables": tables,
120
- "columns": columns,
121
- }
1
+ # src/codegraphcontext/tools/datasources/mysql_ingester.py
2
+ """Aurora MySQL schema ingester (#843).
3
+
4
+ Pulls table + column metadata from information_schema and writes
5
+ Datasource / DbTable / DbColumn nodes with HAS_COLUMN and STORED_IN edges.
6
+
7
+ Requires: pip install PyMySQL (or mysql-connector-python)
8
+ Connection URI: mysql://user:pass@host:3306/database
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ def _connect(
17
+ host: str,
18
+ port: int,
19
+ user: str,
20
+ password: str,
21
+ database: str,
22
+ ssl_verify: bool = False,
23
+ ssl_ca_certs: Optional[str] = None,
24
+ ) -> Any:
25
+ """Return a DB-API 2.0 connection. Tries PyMySQL then mysql-connector-python."""
26
+ ssl_kwargs: Dict[str, Any] = {}
27
+ if ssl_verify:
28
+ ssl_kwargs["ssl"] = {
29
+ "ca": ssl_ca_certs if ssl_ca_certs else None,
30
+ "check_hostname": True,
31
+ "verify_mode": "REQUIRED",
32
+ }
33
+ try:
34
+ import pymysql
35
+ return pymysql.connect(
36
+ host=host, port=port, user=user, password=password, database=database,
37
+ cursorclass=pymysql.cursors.DictCursor, connect_timeout=10,
38
+ **ssl_kwargs,
39
+ )
40
+ except ImportError:
41
+ pass
42
+ try:
43
+ import mysql.connector
44
+ conn_kwargs = dict(
45
+ host=host, port=port, user=user, password=password, database=database,
46
+ connection_timeout=10,
47
+ )
48
+ if ssl_verify:
49
+ conn_kwargs["ssl_ca"] = ssl_ca_certs if ssl_ca_certs else None
50
+ conn_kwargs["ssl_verify_cert"] = True
51
+ return mysql.connector.connect(**conn_kwargs)
52
+ except ImportError:
53
+ raise ImportError(
54
+ "MySQL driver not found. Install with: pip install PyMySQL"
55
+ )
56
+
57
+
58
+ def ingest(
59
+ host: str,
60
+ port: int,
61
+ user: str,
62
+ password: str,
63
+ database: str,
64
+ name: Optional[str] = None,
65
+ env: str = "production",
66
+ ssl_verify: bool = False,
67
+ ssl_ca_certs: Optional[str] = None,
68
+ ) -> Dict[str, Any]:
69
+ """Fetch schema from Aurora MySQL and return a datasource graph dict.
70
+
71
+ Returns:
72
+ {
73
+ datasource: {name, kind, host, env, database},
74
+ tables: [{name, fqn, datasource_name}],
75
+ columns: [{name, type, nullable, table_fqn, datasource_name}],
76
+ }
77
+ """
78
+ datasource_name = name or f"mysql-{database}"
79
+ conn = _connect(host, port, user, password, database, ssl_verify, ssl_ca_certs)
80
+
81
+ try:
82
+ tables: List[Dict[str, Any]] = []
83
+ columns: List[Dict[str, Any]] = []
84
+
85
+ with conn.cursor() as cur:
86
+ # Tables
87
+ cur.execute(
88
+ """
89
+ SELECT TABLE_NAME, TABLE_TYPE, TABLE_ROWS, TABLE_COMMENT
90
+ FROM information_schema.TABLES
91
+ WHERE TABLE_SCHEMA = %s
92
+ ORDER BY TABLE_NAME
93
+ """,
94
+ (database,),
95
+ )
96
+ for row in cur.fetchall():
97
+ fqn = f"{database}.{row['TABLE_NAME']}"
98
+ tables.append({
99
+ "name": row["TABLE_NAME"],
100
+ "fqn": fqn,
101
+ "datasource_name": datasource_name,
102
+ "table_type": row.get("TABLE_TYPE", "BASE TABLE"),
103
+ "estimated_rows": row.get("TABLE_ROWS"),
104
+ "comment": row.get("TABLE_COMMENT", ""),
105
+ })
106
+
107
+ # Columns
108
+ cur.execute(
109
+ """
110
+ SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE,
111
+ COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_KEY
112
+ FROM information_schema.COLUMNS
113
+ WHERE TABLE_SCHEMA = %s
114
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
115
+ """,
116
+ (database,),
117
+ )
118
+ for row in cur.fetchall():
119
+ table_fqn = f"{database}.{row['TABLE_NAME']}"
120
+ columns.append({
121
+ "name": row["COLUMN_NAME"],
122
+ "type": row["DATA_TYPE"],
123
+ "nullable": row["IS_NULLABLE"] == "YES",
124
+ "table_fqn": table_fqn,
125
+ "datasource_name": datasource_name,
126
+ "is_primary_key": row.get("COLUMN_KEY") == "PRI",
127
+ "comment": row.get("COLUMN_COMMENT", ""),
128
+ })
129
+
130
+ finally:
131
+ conn.close()
132
+
133
+ return {
134
+ "datasource": {
135
+ "name": datasource_name,
136
+ "kind": "mysql",
137
+ "host": host,
138
+ "env": env,
139
+ "database": database,
140
+ },
141
+ "tables": tables,
142
+ "columns": columns,
143
+ }
@@ -98,6 +98,8 @@ def check_job_status(job_manager: JobManager, **args) -> Dict[str, Any]:
98
98
  job_dict["start_time"] = job.start_time.strftime("%Y-%m-%d %H:%M:%S")
99
99
  if job.end_time:
100
100
  job_dict["end_time"] = job.end_time.strftime("%Y-%m-%d %H:%M:%S")
101
+ if job.last_update_time:
102
+ job_dict["last_update_time"] = job.last_update_time.strftime("%Y-%m-%d %H:%M:%S")
101
103
 
102
104
  job_dict["status"] = job.status.value
103
105
 
@@ -119,6 +121,8 @@ def list_jobs(job_manager: JobManager) -> Dict[str, Any]:
119
121
  job_dict["start_time"] = job.start_time.strftime("%Y-%m-%d %H:%M:%S")
120
122
  if job.end_time:
121
123
  job_dict["end_time"] = job.end_time.strftime("%Y-%m-%d %H:%M:%S")
124
+ if job.last_update_time:
125
+ job_dict["last_update_time"] = job.last_update_time.strftime("%Y-%m-%d %H:%M:%S")
122
126
  jobs_data.append(job_dict)
123
127
 
124
128
  jobs_data.sort(key=lambda x: x["start_time"], reverse=True)
@@ -0,0 +1,123 @@
1
+ """Backend-agnostic classification of values returned by a graph driver.
2
+
3
+ CodeGraphContext renders the same graph through two independent paths — the
4
+ offline renderer in ``cli/cli_helpers.py`` and the live web server in
5
+ ``viz/server.py``. Each supported backend hands back a different shape, so both
6
+ paths need to answer the same question: *is this value a node or a
7
+ relationship?*
8
+
9
+ Until now each path answered it separately, and they drifted:
10
+
11
+ * ``cli_helpers`` classified by **duck typing** (does the value expose
12
+ ``.labels``? ``.relation``/``.src_node``?), which covers any driver.
13
+ * ``viz/server`` classified by **class name**, matching only
14
+ ``Node``/``KuzuNode`` and ``Relationship``/``KuzuRelationship``.
15
+
16
+ FalkorDB's edge class is named ``Edge``, so it matched neither branch in the
17
+ server path and every FalkorDB relationship was silently discarded — on the
18
+ backend that is CGC's *default* on Unix under Python 3.12+. The server's own
19
+ ``parse_rel`` already knew how to read a FalkorDB edge; it was simply never
20
+ reached.
21
+
22
+ Both paths now share the predicates below, so *classification* cannot drift
23
+ apart again. Id and property extraction are still duplicated between
24
+ ``viz/server.parse_node``/``parse_rel`` and ``cli_helpers``' payload builders —
25
+ consolidating those is deliberately left out of this change.
26
+
27
+ Shapes, confirmed by querying each backend's real Python package:
28
+
29
+ ``neo4j``
30
+ ``Node`` carries ``.labels`` and is dict-convertible via the Mapping
31
+ protocol. ``Relationship`` carries ``.type`` plus ``.start_node`` /
32
+ ``.end_node`` (full ``Node`` objects).
33
+
34
+ ``falkordb``
35
+ ``Node`` carries ``.labels`` but is **not** dict-convertible — properties
36
+ live in a plain ``.properties`` dict. ``Edge`` carries ``.relation`` plus
37
+ ``.src_node`` / ``.dest_node``, which hold the bare integer node ids, and
38
+ exposes no ``.labels`` and no ``.type``.
39
+
40
+ ``kuzu`` / ``ladybug``
41
+ Plain dicts. Nodes carry ``_id`` / ``_label``; relationships carry
42
+ ``_src`` / ``_dst`` / ``_label``. Kùzu spells these keys lowercase and
43
+ Ladybug spells the identical fields uppercase (``_ID`` / ``_LABEL`` /
44
+ ``_SRC`` / ``_DST``), so every lookup here accepts either casing.
45
+ """
46
+
47
+ from typing import Any
48
+
49
+ __all__ = ["has_meta", "meta", "is_node", "is_relationship"]
50
+
51
+
52
+ def has_meta(record: dict, key: str) -> bool:
53
+ """True if ``record`` carries ``key`` in either Kùzu or Ladybug casing."""
54
+ return key in record or key.upper() in record
55
+
56
+
57
+ def meta(record: dict, key: str, default: Any = None) -> Any:
58
+ """Read an internal ``_``-prefixed field regardless of its casing."""
59
+ if key in record:
60
+ return record[key]
61
+ return record.get(key.upper(), default)
62
+
63
+
64
+ def _is_driver_relationship(value: Any) -> bool:
65
+ """True for a driver object that looks like a relationship.
66
+
67
+ Neo4j exposes ``.type``; FalkorDB exposes ``.relation`` alongside
68
+ ``.src_node``. Both are checked because neither driver implements the
69
+ other's attribute.
70
+ """
71
+ if hasattr(value, "type"):
72
+ return True
73
+ return hasattr(value, "relation") and hasattr(value, "src_node")
74
+
75
+
76
+ def is_relationship(value: Any) -> bool:
77
+ """True if ``value`` is a relationship from any supported backend."""
78
+ # ``.labels`` is the unambiguous node marker across every driver, so it
79
+ # settles the question before the relationship checks run.
80
+ if hasattr(value, "labels"):
81
+ return False
82
+ if _is_driver_relationship(value):
83
+ return True
84
+ if isinstance(value, dict):
85
+ # Kùzu and Ladybug relationships also carry ``_label``, so the
86
+ # endpoints are what distinguish them from nodes.
87
+ if has_meta(value, "_src") and has_meta(value, "_dst"):
88
+ return True
89
+ # A FalkorDB result mapped into a plain dict keeps the driver's
90
+ # endpoint names.
91
+ return "src_node" in value and "dest_node" in value
92
+ return False
93
+
94
+
95
+ def _has_endpoint_marker(record: dict) -> bool:
96
+ """True if a dict carries any relationship-endpoint field.
97
+
98
+ A half-formed relationship — say ``_src`` present but ``_dst`` missing —
99
+ is not a usable relationship, but it must not be mistaken for a node
100
+ either: it also carries ``_label``, so a plain label check would turn it
101
+ into a phantom node with a relationship's id. Values like that are
102
+ classified as neither and skipped, which is what the offline renderer has
103
+ always done.
104
+ """
105
+ return (
106
+ has_meta(record, "_src")
107
+ or has_meta(record, "_dst")
108
+ or "src_node" in record
109
+ or "dest_node" in record
110
+ )
111
+
112
+
113
+ def is_node(value: Any) -> bool:
114
+ """True if ``value`` is a node from any supported backend."""
115
+ if hasattr(value, "labels"):
116
+ return True
117
+ if _is_driver_relationship(value):
118
+ return False
119
+ if isinstance(value, dict):
120
+ if _has_endpoint_marker(value):
121
+ return False
122
+ return has_meta(value, "_label") or "labels" in value
123
+ return False
@@ -99,11 +99,15 @@ def build_graph_data(
99
99
  for node in nodes:
100
100
  node_id = node.get("id") or node.get("name", "unknown")
101
101
  label = node.get("label", "Function")
102
+ name = node.get("name", str(node_id))
103
+ file_path = node.get("file_path", "")
104
+ if name == "<module>" and file_path:
105
+ name = Path(file_path).name
102
106
  serialised_nodes.append({
103
107
  "id": str(node_id),
104
108
  "label": label,
105
- "name": node.get("name", str(node_id)),
106
- "file_path": node.get("file_path", ""),
109
+ "name": name,
110
+ "file_path": file_path,
107
111
  "line": node.get("line_number"),
108
112
  "color": node_color(label),
109
113
  })
@@ -362,7 +366,11 @@ def render_html(
362
366
  ctx.fillStyle = '#e2e8f0';
363
367
  ctx.font = '10px monospace';
364
368
  ctx.textAlign = 'center';
365
- ctx.fillText(n.name.length > 16 ? n.name.slice(0,14)+'…' : n.name, p.x, p.y + 24);
369
+ let displayName = n.name;
370
+ if (displayName === '<module>' && n.file_path) {{
371
+ displayName = n.file_path.split('/').pop();
372
+ }}
373
+ ctx.fillText(displayName.length > 16 ? displayName.slice(0,14)+'…' : displayName, p.x, p.y + 24);
366
374
  }});
367
375
  }}
368
376
 
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
20
20
  from ..utils.debug_log import debug_log
21
21
  from ..utils.path_sandbox import is_path_allowed
22
22
  from ..utils.cypher_readonly import is_read_only_cypher as _is_read_only_cypher, read_only_rejection_message
23
+ from ..utils import graph_shapes
23
24
 
24
25
  app = FastAPI()
25
26
 
@@ -236,6 +237,15 @@ async def get_graph(repo_path: Optional[str] = None, cypher_query: Optional[str]
236
237
  # Extract name/label for frontend
237
238
  # Prefer 'name' property, fallback to 'label', then 'path' or 'Unknown'
238
239
  display_name = str(props.get('name', props.get('label', props.get('path', 'Unknown'))))
240
+ if display_name == "<module>":
241
+ _fpath = str(props.get('path', props.get('file', '')))
242
+ if _fpath:
243
+ # Same plain-filename label as the payload builders in
244
+ # cli_helpers/visualize_graph, so one node cannot
245
+ # render two different names in different views.
246
+ display_name = os.path.basename(_fpath)
247
+ props['name'] = display_name
248
+ props['label'] = display_name
239
249
 
240
250
  nodes_dict[eid] = {
241
251
  "id": eid,
@@ -469,6 +479,12 @@ def parse_node(node, nodes_dict):
469
479
  except: pass
470
480
 
471
481
  display_name = str(props.get('name', props.get('label', props.get('path', 'Unknown'))))
482
+ if display_name == "<module>":
483
+ _fpath = str(props.get('path', props.get('file', '')))
484
+ if _fpath:
485
+ display_name = os.path.basename(_fpath)
486
+ props['name'] = display_name
487
+ props['label'] = display_name
472
488
 
473
489
  nodes_dict[eid] = {
474
490
  "id": eid,
@@ -533,18 +549,13 @@ def parse_element(val, nodes_dict, edges):
533
549
  parse_element(r, nodes_dict, edges)
534
550
  return
535
551
 
536
- type_name = type(val).__name__
537
-
538
- is_dict_rel = isinstance(val, dict) and (
539
- any(k in val for k in ('_src', '_dst', '_SRC', '_DST', 'src_node', 'dest_node'))
540
- )
541
- is_dict_node = isinstance(val, dict) and (
542
- '_label' in val or '_LABEL' in val or 'labels' in val
543
- ) and not is_dict_rel
544
-
545
- if type_name in ('Node', 'KuzuNode') or is_dict_node:
552
+ # Classification is duck-typed and shared with the offline renderer (see
553
+ # utils/graph_shapes). Matching on class name used to miss FalkorDB's
554
+ # `Edge`, so every FalkorDB relationship was dropped here even though
555
+ # `parse_rel` below reads it correctly.
556
+ if graph_shapes.is_node(val):
546
557
  parse_node(val, nodes_dict)
547
- elif type_name in ('Relationship', 'KuzuRelationship') or is_dict_rel:
558
+ elif graph_shapes.is_relationship(val):
548
559
  parse_rel(val, edges)
549
560
  elif isinstance(val, (list, tuple)):
550
561
  for item in val:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraphcontext
3
- Version: 0.6.8
3
+ Version: 0.6.9
4
4
  Summary: An MCP server that indexes local code into a graph database to provide context to AI assistants.
5
5
  Author-email: Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
6
6
  License: MIT License
@@ -3,7 +3,7 @@ codegraphcontext/__main__.py,sha256=21QjL_lfd6cy-clJYPBg6xKb_IuNDcvvWdaAJEvZ-HQ,
3
3
  codegraphcontext/prompts.py,sha256=aK_oe7DQwHNYtpjOy5aqi2VTKs4KJihwF6O4E8z3IPA,8603
4
4
  codegraphcontext/server.py,sha256=aaplzU_SlkVsPdwiyf6RvXnGqnkvDilvrbC_euUYcAU,37408
5
5
  codegraphcontext/stdlibs.py,sha256=sWcjgJyGIGewilnBt4qXogw2cH4YWwHWkEXbCSIv5Zc,283
6
- codegraphcontext/tool_definitions.py,sha256=BjUD5PKn7GcmgsPxD1-ba4SUr05iE5ueftqIX4Cwy7s,19938
6
+ codegraphcontext/tool_definitions.py,sha256=11A_9cOo_l9vunBUcM8xcxZFJMBsKx2_U4-bMwcjfH8,20001
7
7
  codegraphcontext/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  codegraphcontext/api/app.py,sha256=pP-OaQUny0dsgfP7zj_ib3GqIfGh3zb7P1Gx6rKh0uw,3844
9
9
  codegraphcontext/api/auth.py,sha256=YnV4lKlBgyIP7HFQKKaD7kJZOXBe3gOoyovbhBWI7g8,4407
@@ -11,22 +11,22 @@ codegraphcontext/api/mcp_sse.py,sha256=cWu_dG4rkH8Teb4_BjKNGhkxXKumGaZdFahlT2uw5
11
11
  codegraphcontext/api/router.py,sha256=9XCm5FBZY4z-k8k9Z1OlE9vMdSK-bh6tC5_Qef1F1eA,4383
12
12
  codegraphcontext/api/schemas.py,sha256=pGLuKtdn55pf8mS8wv8MwehbT_3-4OZuSYjI5LJxzOc,1034
13
13
  codegraphcontext/cli/__init__.py,sha256=2vUlnf7H09M9JbS2gS_LmMUBJWAWcGGHkkl4I2YrdSg,39
14
- codegraphcontext/cli/cli_helpers.py,sha256=yB0N680PS49A6eYCisMJIotQDOpo38-FQ-VFfJQ33OQ,56067
14
+ codegraphcontext/cli/cli_helpers.py,sha256=7fEJK0k85basOphCV9iO6zCh1pIONct3jj5ABE1cd2c,55334
15
15
  codegraphcontext/cli/config_manager.py,sha256=L4-Hun7bluTg5TWTyAUxLqTGTvPI6Z8oYVd2rnKEn8w,56113
16
16
  codegraphcontext/cli/hook_manager.py,sha256=nKr6SiuM6zA3eZv1rZSNPXUyqkha8Ja0VGQ3_145bLE,7615
17
- codegraphcontext/cli/main.py,sha256=rxcXzB9Wb6DDxqTXhyHtCQECIhl0_PTJmQy4xBG_EU4,162703
17
+ codegraphcontext/cli/main.py,sha256=2d2kNSLbBgMFZaegWkT1UbZF68ApLDFPzZXu0FqvM6w,163479
18
18
  codegraphcontext/cli/project_config.py,sha256=Slae6fIojlcqRbFzRkAMAsglamryLCPrjUFG16e165g,7492
19
19
  codegraphcontext/cli/registry_commands.py,sha256=KwLwxKzMWs0tAFd__nPf_gz3kmlPO5b6Zcb_9Qkguf0,18516
20
20
  codegraphcontext/cli/setup_macos.py,sha256=0jcxRsQX7_UcfKrFUnc5Yx3m34w0C3w3x5n7KH6ty6g,3743
21
- codegraphcontext/cli/setup_wizard.py,sha256=NzNoWWeMrS-iSRPlrlCydEERo9YWp_eB1he_DkUGXSw,50566
21
+ codegraphcontext/cli/setup_wizard.py,sha256=N958hdsypaE1gza3wEjJsZ3c-i888oRH5PYQkKD3dP4,53083
22
22
  codegraphcontext/cli/simulator.py,sha256=QSHLIP_iQsaoun52ipUpPl7inqlXKmQs6yddQHom42U,14564
23
23
  codegraphcontext/cli/visualizer.py,sha256=QJbxoIcUBgZ2pQP0tq0pxcM5xgT-CQtHWxD7xludK5k,5464
24
24
  codegraphcontext/core/__init__.py,sha256=jSr7aX-5OvFJGoKdx24bFA5-eMyBKhSrxrvJr32XG3g,14081
25
25
  codegraphcontext/core/bundle_registry.py,sha256=xlVBe-fmvEOLY9xDq7w0vXFlTPzeFVIwO9cqzBtVm4E,7490
26
26
  codegraphcontext/core/cgc_bundle.py,sha256=X2aOji754UyjvCEJc3hOQCP6KF_H4OJNQMfUunrYY5I,74714
27
- codegraphcontext/core/cgcignore.py,sha256=x50FOG04kmR12LqixTlfiHW4ka05jf6_jaRA-5yiqis,10847
27
+ codegraphcontext/core/cgcignore.py,sha256=yJeRaPlDyOaSGGdXB5kwBlSqpEXtrqsSzo0UNMoqef8,12236
28
28
  codegraphcontext/core/database.py,sha256=ZLOHXv46hVxWiPYQala0Co7_Z8vqj5juaenudwq6DTU,14893
29
- codegraphcontext/core/database_embedded_kuzu.py,sha256=85J43enxfPgEAR9lu2HflaD-Pmfcx4Mc5vCka_Vho5o,82482
29
+ codegraphcontext/core/database_embedded_kuzu.py,sha256=0MgfAN-3mH8saXqUysE42Kz30HTmyWGCOI3LwyGF9ww,82566
30
30
  codegraphcontext/core/database_falkordb.py,sha256=8TSFfgKoaNdTzSx7P4qXMMs2kTNBzrcW5tZxR8JV4bY,35834
31
31
  codegraphcontext/core/database_falkordb_remote.py,sha256=HdkcmwgUxPy1bwiFqqcU8yMQYib7B3JarYQ1LlTwfJ8,7957
32
32
  codegraphcontext/core/database_kuzu.py,sha256=5I31dGm7wRo3YjIWeWGRVcX1eYRXYc7fPWnT7zHFjf0,1123
@@ -39,7 +39,7 @@ codegraphcontext/core/simulator.py,sha256=qfjR5_EFd8elZWUZE92mi6F3mzjpBTfdnZMQr1
39
39
  codegraphcontext/core/watcher.py,sha256=aF96BdTQ8ye8L7-wmDC6LP2LMgUT47TOGSuCcNDniLM,20208
40
40
  codegraphcontext/tools/__init__.py,sha256=iqQPEkDlGXO7rAaLNQrS_ZAbLSklBSK-OowpARq0mig,41
41
41
  codegraphcontext/tools/advanced_language_query_tool.py,sha256=8pRVMg-Tbp3JrbN31McyTbum91gJPlrjHFCFVJ78coA,4169
42
- codegraphcontext/tools/code_finder.py,sha256=N67zRAfEswvDg_Bw7dVqv8yNS_VjoDvGNPPrJXPfRAg,93126
42
+ codegraphcontext/tools/code_finder.py,sha256=dSd-k3z4QTyLrSjfHbx8bdmPn5e9tovoB7XXYW9wq2w,93116
43
43
  codegraphcontext/tools/graph_builder.py,sha256=NVbdAwH6mCLop167EFs3qctSw1W_FeFzC1WqAGiR_6I,27558
44
44
  codegraphcontext/tools/package_resolver.py,sha256=j6s4H5VnourUdJ-CkA3kCOc-Vdewnjc-PNDshAPCiaU,7802
45
45
  codegraphcontext/tools/report_generator.py,sha256=3tH83_24nm-pWcY_oXUeLB2jYtGGZmkiBb_oyuhS4WM,16114
@@ -49,12 +49,12 @@ codegraphcontext/tools/system.py,sha256=RCUraHQOT-M146CwLM3W1GcBHsAvuans1puL9SPa
49
49
  codegraphcontext/tools/tree_sitter_parser.py,sha256=xChDPOVhyHwsgwXrlYWy1iuIU1kdL6xxTNIqhgm1GbM,3669
50
50
  codegraphcontext/tools/type_utils.py,sha256=ZCJ9b1BwvL7LKl8Duhx-5uwX3Gemp5EGqLMso7zRY3E,472
51
51
  codegraphcontext/tools/datasources/__init__.py,sha256=EDY0_s7DJPRzJhfmYUz3Pxv0b6UIXP-l9KMp3xgg-WU,132
52
- codegraphcontext/tools/datasources/cassandra_ingester.py,sha256=sgbskSkvE98qtOprgzkbVSEIasZWe4WXOzaCiRR2c_Q,3060
53
- codegraphcontext/tools/datasources/mysql_ingester.py,sha256=uWstOv7mOEws1pmT8uAsEBkoOGM1FXOWt_2VCE-tZeg,3991
52
+ codegraphcontext/tools/datasources/cassandra_ingester.py,sha256=06vvjslLyAwlYhLS5Wp55WY_6r6nFgh7py9dqfGuFT8,3697
53
+ codegraphcontext/tools/datasources/mysql_ingester.py,sha256=JmGLrgMpz7HUUNpM1Np1xPXp9Gnjj8119AAP2Td6qKM,4767
54
54
  codegraphcontext/tools/datasources/redis_ingester.py,sha256=mVUKYmr_v6g7wYaavmQscEHGi_CPZrxYLjfkldc6cWE,3788
55
55
  codegraphcontext/tools/handlers/analysis_handlers.py,sha256=jPow6EEwUEQd3H7XfnU7no9GnZ80sY1E6KQyKv5sl_0,14186
56
56
  codegraphcontext/tools/handlers/indexing_handlers.py,sha256=GNpIR_4vY1DawOGUnU0PY4m_9VSUkWV7eh-J9eY5RIg,8031
57
- codegraphcontext/tools/handlers/management_handlers.py,sha256=19-fCuMuY5oHXQ9O4F1qG4ZsKKsGpSGbxxA1gH4AE-g,17419
57
+ codegraphcontext/tools/handlers/management_handlers.py,sha256=5DyBLCR-a58S_qxki0naiUjQs096us1luWQ342jLpWI,17681
58
58
  codegraphcontext/tools/handlers/query_handlers.py,sha256=mqI7pfuSRc92ChAS5IGtrkPyhpPeh-dg-ewVnysx5nw,4643
59
59
  codegraphcontext/tools/handlers/watcher_handlers.py,sha256=po0NP3WnUqJAiDyZW9pz2g2V-itnazI10cXQJuc1UZ8,3809
60
60
  codegraphcontext/tools/indexing/__init__.py,sha256=exCNeB4UYhKhNSoEBNta_EZ51Kdze4iK4e3oWH8B4X8,126
@@ -128,14 +128,15 @@ codegraphcontext/utils/cypher_readonly.py,sha256=RyFBlAbQsNZtFdCN4jzf_WMNnzLIt2O
128
128
  codegraphcontext/utils/debug_log.py,sha256=eFKQmaB4iTGzcdytfwZszhcMJCK6jIPxPXKsJOncl4A,3023
129
129
  codegraphcontext/utils/gcf_encoder.py,sha256=H6G66BCqbOZpyM_4JOFne5F3YrZRA8fiMnT4S_gRCk8,1806
130
130
  codegraphcontext/utils/git_utils.py,sha256=D7ve2gpZaEHk4y6K4ga-o2ka89DVFX1acafD31YAyJM,1383
131
+ codegraphcontext/utils/graph_shapes.py,sha256=1KkctKUh5OjMwa2hf1tDHx6OAbLMElQnrG-ISdvYfzI,4967
131
132
  codegraphcontext/utils/path_ignore.py,sha256=xaYCSq6nzUVX_mEdn3yrmwM--v7orJImga2XAQeiGB0,2168
132
133
  codegraphcontext/utils/path_sandbox.py,sha256=AemD1DHVMA0lj9BpmRkKwJB2OaE0SewggmJnyi0dr04,2784
133
134
  codegraphcontext/utils/repo_path.py,sha256=BxS3y6RV_WaZhg_WxgJygS4SnQa-Ryth2fsicfnYorY,1121
134
135
  codegraphcontext/utils/secret_scanner.py,sha256=Ml6hfiVN8gP_3W_CJWhBQ728HYv2UbmhDaM03SYbyEo,6384
135
136
  codegraphcontext/utils/tool_limits.py,sha256=3d-QnlzxBNocz58VKNhdsta7ZpWThs1AX46_gYXl6pQ,2751
136
137
  codegraphcontext/utils/tree_sitter_manager.py,sha256=vbbae9T69jUbMUphuBj1bnsT0HQJp5vYxzYFKZqV2HU,14496
137
- codegraphcontext/utils/visualize_graph.py,sha256=me35VuizqLyveT86XdgavrgnB2B4lG7WqFyiMr5ea1w,14644
138
- codegraphcontext/viz/server.py,sha256=l2hM6phhYeMn0vAmMz7wtRGdj7LMkg2BsNMOvS1RUfg,31833
138
+ codegraphcontext/utils/visualize_graph.py,sha256=_KO8oCLooBccJHT99kZbYjwODobuEaGQbR0Rrb7NimQ,14951
139
+ codegraphcontext/viz/server.py,sha256=MmbQ99nhTaKF4wsW8mPz_bUyTt40X9pYyIw3zp9cRJ0,32709
139
140
  codegraphcontext/viz/dist/cgcIcon.png,sha256=lik7hnqvkqLc5uo9w9Q_YJXtRx7cWPrXqSLttGtW1sg,58703
140
141
  codegraphcontext/viz/dist/favicon.ico,sha256=AjhUxD_Eslt5XuSVHIAZ494Fk__rb5GLXR8qm0elfP4,1870
141
142
  codegraphcontext/viz/dist/index.html,sha256=uEaFo3uHWD2KojFiyfWRJoS1_t6zrllOnf_siaB0I5U,1946
@@ -174,9 +175,9 @@ codegraphcontext/viz/dist/wasm/tree-sitter-typescript.wasm,sha256=hRVATc7tOOHthq
174
175
  codegraphcontext/viz/dist/wasm/tree-sitter.wasm,sha256=CCeVuI_hXktkBD-DW01xYPv5XoAYVRIqzJUJI5te-RY,196763
175
176
  codegraphcontext/viz/dist/wasm/web-tree-sitter.js,sha256=DIaCNqRylrT_PBVw8g4ImeSnhP9uXNe_ycOlUiVGPko,153666
176
177
  codegraphcontext/viz/dist/wasm/web-tree-sitter.wasm,sha256=CCeVuI_hXktkBD-DW01xYPv5XoAYVRIqzJUJI5te-RY,196763
177
- codegraphcontext-0.6.8.dist-info/licenses/LICENSE,sha256=rh8M-bJpQYJnw2vtRVgt0t7piMZXh5QzaKeNEI0vqqA,1061
178
- codegraphcontext-0.6.8.dist-info/METADATA,sha256=Xo-Kwgpw1NiWuCNQPJCuO9tB9x4LLo26AC34fhPs6Zs,33788
179
- codegraphcontext-0.6.8.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
180
- codegraphcontext-0.6.8.dist-info/entry_points.txt,sha256=LCxWCWMshdvYGoHBPuQZ8C-e4CiNSHCLXofrNSGHkoE,103
181
- codegraphcontext-0.6.8.dist-info/top_level.txt,sha256=CBgc6LAPZIO5FS0nSYYkylDifHsZTIqw3Gf5UwDxeGI,17
182
- codegraphcontext-0.6.8.dist-info/RECORD,,
178
+ codegraphcontext-0.6.9.dist-info/licenses/LICENSE,sha256=rh8M-bJpQYJnw2vtRVgt0t7piMZXh5QzaKeNEI0vqqA,1061
179
+ codegraphcontext-0.6.9.dist-info/METADATA,sha256=OFpyhssoYQhKZN-n3ScY3PSZdinMOQa7WWU5w8U_QNc,33788
180
+ codegraphcontext-0.6.9.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
181
+ codegraphcontext-0.6.9.dist-info/entry_points.txt,sha256=LCxWCWMshdvYGoHBPuQZ8C-e4CiNSHCLXofrNSGHkoE,103
182
+ codegraphcontext-0.6.9.dist-info/top_level.txt,sha256=CBgc6LAPZIO5FS0nSYYkylDifHsZTIqw3Gf5UwDxeGI,17
183
+ codegraphcontext-0.6.9.dist-info/RECORD,,