codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.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 (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,285 @@
1
+ """
2
+ Tree-sitter language and parser management module.
3
+
4
+ This module provides thread-safe, cached access to tree-sitter languages and parsers.
5
+ It handles the migration from tree-sitter-languages to tree-sitter-language-pack.
6
+
7
+ Key design principles:
8
+ 1. Cache languages, not parsers (parsers are NOT thread-safe)
9
+ 2. Handle language name aliasing
10
+ 3. Provide clear error messages for missing languages
11
+ 4. Support optional tree-sitter dependency
12
+ """
13
+
14
+ from typing import Dict, Optional
15
+ import threading
16
+
17
+ from tree_sitter import Language, Parser
18
+ from tree_sitter_language_pack import get_language
19
+
20
+
21
+ # Language name aliases for compatibility
22
+ LANGUAGE_ALIASES = {
23
+ # Common aliases
24
+ "py": "python",
25
+ "js": "javascript",
26
+ "ts": "typescript",
27
+ "c++": "cpp",
28
+ "c#": "c_sharp",
29
+ "csharp": "c_sharp",
30
+ "cs": "c_sharp",
31
+ "rb": "ruby",
32
+ "rs": "rust",
33
+ "go": "go",
34
+ "php": "php",
35
+ ".php": "php",
36
+
37
+ # Canonical names (map to themselves for consistency)
38
+ "python": "python",
39
+ "javascript": "javascript",
40
+ "typescript": "typescript",
41
+ "tsx": "tsx",
42
+ "cpp": "cpp",
43
+ "c_sharp": "c_sharp",
44
+ "c": "c",
45
+ "java": "java",
46
+ "haskell": "haskell",
47
+ "ruby": "ruby",
48
+ "rust": "rust",
49
+ "kt": "kotlin",
50
+ "kotlin": "kotlin",
51
+ "scala": "scala",
52
+ ".scala": "scala",
53
+ "swift": "swift",
54
+ ".swift": "swift",
55
+ "dart": "dart",
56
+ "perl": "perl",
57
+ "pl": "perl",
58
+ "pm": "perl",
59
+ "elixir": "elixir",
60
+ "ex": "elixir",
61
+ "exs": "elixir",
62
+ }
63
+
64
+ # Canonical names that differ from tree-sitter-language-pack names
65
+ LANGUAGE_PACK_NAMES = {
66
+ "c_sharp": "csharp",
67
+ }
68
+
69
+
70
+ class TreeSitterManager:
71
+ """
72
+ Manages tree-sitter language loading and parser creation.
73
+
74
+ This class provides:
75
+ - Thread-safe language caching
76
+ - Language name aliasing
77
+ - Parser lifecycle management
78
+ - Clear error handling
79
+ """
80
+
81
+ def __init__(self):
82
+ """Initialize the tree-sitter manager."""
83
+ self._language_cache: Dict[str, Language] = {}
84
+ self._cache_lock = threading.Lock()
85
+
86
+ def _normalize_language_name(self, lang: str) -> str:
87
+ """
88
+ Normalize a language name to its canonical form.
89
+
90
+ Args:
91
+ lang: Language name (e.g., "py", "python", "c++")
92
+
93
+ Returns:
94
+ Canonical language name (e.g., "python", "cpp")
95
+
96
+ Raises:
97
+ ValueError: If language name is not recognized
98
+ """
99
+ normalized = LANGUAGE_ALIASES.get(lang.lower())
100
+ if normalized is None:
101
+ raise ValueError(
102
+ f"Unknown language: {lang}. "
103
+ f"Supported languages: {', '.join(sorted(set(LANGUAGE_ALIASES.values())))}"
104
+ )
105
+ return normalized
106
+
107
+ def get_language_safe(self, lang: str) -> Language:
108
+ """
109
+ Get a cached Language object for the specified language.
110
+
111
+ This method is thread-safe and caches languages to avoid repeated loading.
112
+
113
+ Args:
114
+ lang: Language name (supports aliases like "py", "c++", etc.)
115
+
116
+ Returns:
117
+ Tree-sitter Language object
118
+
119
+ Raises:
120
+ ValueError: If language is not supported
121
+ Exception: If language loading fails
122
+ """
123
+ # Normalize the language name
124
+ canonical_name = self._normalize_language_name(lang)
125
+
126
+ # Check cache first (fast path, no lock needed for reads)
127
+ if canonical_name in self._language_cache:
128
+ return self._language_cache[canonical_name]
129
+
130
+ # Load language with lock (slow path)
131
+ with self._cache_lock:
132
+ # Double-check after acquiring lock
133
+ if canonical_name in self._language_cache:
134
+ return self._language_cache[canonical_name]
135
+
136
+ try:
137
+ # Map canonical name to language-pack name where they differ
138
+ pack_name = LANGUAGE_PACK_NAMES.get(canonical_name, canonical_name)
139
+ language = get_language(pack_name)
140
+
141
+ self._language_cache[canonical_name] = language
142
+ return language
143
+ except (KeyError, ModuleNotFoundError):
144
+ raise ValueError(
145
+ f"Language '{canonical_name}' is not available in tree-sitter-language-pack. "
146
+ f"This may be due to a missing or experimental grammar."
147
+ )
148
+ except Exception as e:
149
+ raise Exception(
150
+ f"Failed to load language '{canonical_name}': {e}"
151
+ )
152
+
153
+ def create_parser(self, lang: str) -> Parser:
154
+ """
155
+ Create a new Parser instance for the specified language.
156
+
157
+ IMPORTANT: Parsers are NOT thread-safe and should not be shared across threads.
158
+ Each thread should create its own parser using this method.
159
+
160
+ Args:
161
+ lang: Language name (supports aliases)
162
+
163
+ Returns:
164
+ A new Parser instance configured for the language
165
+
166
+ Raises:
167
+ ValueError: If language is not supported
168
+ Exception: If parser creation fails
169
+ """
170
+ language = self.get_language_safe(lang)
171
+ # In tree-sitter 0.25+, Parser takes language in constructor
172
+ parser = Parser(language)
173
+ return parser
174
+
175
+ def is_language_available(self, lang: str) -> bool:
176
+ """
177
+ Check if a language is available without raising exceptions.
178
+
179
+ Args:
180
+ lang: Language name
181
+
182
+ Returns:
183
+ True if language is available, False otherwise
184
+ """
185
+ try:
186
+ self.get_language_safe(lang)
187
+ return True
188
+ except (ValueError, Exception):
189
+ return False
190
+
191
+ def get_supported_languages(self) -> list[str]:
192
+ """
193
+ Get a list of all supported language names.
194
+
195
+ Returns:
196
+ Sorted list of canonical language names
197
+ """
198
+ return sorted(set(LANGUAGE_ALIASES.values()))
199
+
200
+
201
+ # Global singleton instance
202
+ _manager_instance: Optional[TreeSitterManager] = None
203
+ _instance_lock = threading.Lock()
204
+
205
+
206
+ def get_tree_sitter_manager() -> TreeSitterManager:
207
+ """
208
+ Get the global TreeSitterManager instance (singleton pattern).
209
+
210
+ Returns:
211
+ The global TreeSitterManager instance
212
+ """
213
+ global _manager_instance
214
+
215
+ if _manager_instance is not None:
216
+ return _manager_instance
217
+
218
+ with _instance_lock:
219
+ if _manager_instance is None:
220
+ _manager_instance = TreeSitterManager()
221
+ return _manager_instance
222
+
223
+
224
+ # Convenience functions for backward compatibility
225
+ def get_language_safe(lang: str) -> Language:
226
+ """Get a cached Language object. Thread-safe."""
227
+ return get_tree_sitter_manager().get_language_safe(lang)
228
+
229
+
230
+ def create_parser(lang: str) -> Parser:
231
+ """Create a new Parser for the language. Each call returns a new parser."""
232
+ return get_tree_sitter_manager().create_parser(lang)
233
+
234
+
235
+ def execute_query(language: Language, query_string: str, node):
236
+ """
237
+ Execute a tree-sitter query and return captures in backward-compatible format.
238
+
239
+ This function provides compatibility with the old tree-sitter 0.20.x API where
240
+ you could call query.captures(node). The new 0.25+ API uses QueryCursor.
241
+
242
+ Args:
243
+ language: Tree-sitter Language object
244
+ query_string: Query string in tree-sitter query syntax
245
+ node: Tree-sitter Node to query
246
+
247
+ Returns:
248
+ List of (node, capture_name) tuples, compatible with old API
249
+
250
+ Example:
251
+ >>> from tree_sitter_language_pack import get_language
252
+ >>> lang = get_language('python')
253
+ >>> parser = Parser(lang)
254
+ >>> tree = parser.parse(b'def hello(): pass')
255
+ >>> captures = execute_query(lang, '(function_definition) @func', tree.root_node)
256
+ >>> for node, name in captures:
257
+ ... print(f'{name}: {node.type}')
258
+ """
259
+ from tree_sitter import Query, QueryCursor
260
+
261
+ try:
262
+ # Create query and cursor
263
+ query = Query(language, query_string)
264
+ cursor = QueryCursor(query)
265
+
266
+ # Execute query and convert to old format
267
+ captures = []
268
+
269
+ # Use matches() which returns (pattern_index, captures_dict) tuples
270
+ for pattern_index, captures_dict in cursor.matches(node):
271
+ # captures_dict is {capture_name: [nodes]}
272
+ for capture_name, nodes in captures_dict.items():
273
+ for captured_node in nodes:
274
+ # Old format: (node, capture_name)
275
+ captures.append((captured_node, capture_name))
276
+
277
+ return captures
278
+
279
+ except Exception as e:
280
+ # Provide helpful error message
281
+ raise Exception(
282
+ f"Failed to execute query: {e}\n"
283
+ f"Query string: {query_string[:100]}..."
284
+ )
285
+
File without changes
@@ -0,0 +1,282 @@
1
+ from fastapi import FastAPI, HTTPException, Query, Request
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.responses import HTMLResponse, FileResponse
5
+ from pathlib import Path
6
+ import uvicorn
7
+ import json
8
+ import os
9
+ import sys
10
+ from typing import Optional, List, Dict, Any
11
+
12
+ from ..core.database import DatabaseManager
13
+ from ..utils.debug_log import debug_log
14
+
15
+ app = FastAPI()
16
+
17
+ # Enable CORS for development
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ # Global database manager (will be initialized when server starts)
26
+ db_manager: Optional[DatabaseManager] = None
27
+ # Path to static directory
28
+ _static_dir: Optional[str] = None
29
+
30
+ def set_db_manager(manager: DatabaseManager):
31
+ global db_manager
32
+ db_manager = manager
33
+
34
+ @app.get("/api/graph")
35
+ async def get_graph(repo_path: Optional[str] = None, cypher_query: Optional[str] = None):
36
+ if not db_manager:
37
+ raise HTTPException(status_code=500, detail="Database not initialized")
38
+
39
+ def get_eid(element):
40
+ if element is None: return None
41
+ if isinstance(element, (int, str)):
42
+ return str(element)
43
+
44
+ # If element is a dict (like Neo4j returned items or KuzuDB node/rel dicts)
45
+ if isinstance(element, dict):
46
+ # KuzuDB _src / _dst are directly {'offset': X, 'table': Y}
47
+ if 'offset' in element and 'table' in element:
48
+ return f"{element.get('table')}_{element.get('offset')}"
49
+
50
+ for key in ['_id', 'id', 'element_id']:
51
+ if key in element:
52
+ val = element[key]
53
+ if val is not None:
54
+ # KuzuDB returns dict IDs like {'offset': 1, 'table': 0} inside nodes
55
+ if isinstance(val, dict):
56
+ return f"{val.get('table', 0)}_{val.get('offset', 0)}"
57
+ return str(val)
58
+ return str(id(element))
59
+
60
+ # Try various ways to get ID (Neo4j, FalkorDB, etc. objects)
61
+ for attr in ['element_id', 'id', '_id']:
62
+ if hasattr(element, attr):
63
+ val = getattr(element, attr)
64
+ if val is not None:
65
+ # KuzuDB objects if any
66
+ if isinstance(val, dict):
67
+ return f"{val.get('table', 0)}_{val.get('offset', 0)}"
68
+ return str(val)
69
+ return str(id(element))
70
+
71
+ try:
72
+ nodes_dict = {}
73
+ edges = []
74
+
75
+ print(f"DEBUG: Starting get_graph with repo_path={repo_path}", flush=True)
76
+
77
+ with db_manager.get_driver().session() as session:
78
+ if cypher_query:
79
+ print(f"DEBUG: Executing custom query: {cypher_query}", flush=True)
80
+ result = session.run(cypher_query)
81
+ elif repo_path:
82
+ repo_path = str(Path(repo_path).resolve())
83
+ print(f"DEBUG: Fetching subgraph for: {repo_path}", flush=True)
84
+ query = """
85
+ MATCH (r:Repository {path: $repo_path})
86
+ OPTIONAL MATCH (r)-[:CONTAINS*0..]->(n)
87
+ WITH DISTINCT n
88
+ WHERE n IS NOT NULL
89
+ OPTIONAL MATCH (n)-[rel]->(m)
90
+ RETURN n, rel, m
91
+ """
92
+ result = session.run(query, repo_path=repo_path)
93
+ else:
94
+ print("DEBUG: Fetching global graph", flush=True)
95
+ query = "MATCH (n) OPTIONAL MATCH (n)-[rel]->(m) RETURN n, rel, m LIMIT 50000"
96
+ result = session.run(query)
97
+
98
+ record_count = 0
99
+ for record in result:
100
+ record_count += 1
101
+ # Use .get() to avoid KeyError if the query doesn't return all fields (n, rel, m)
102
+ for key in ['n', 'm']:
103
+ try:
104
+ node = record.get(key)
105
+ if node:
106
+ eid = get_eid(node)
107
+ if eid and eid not in nodes_dict:
108
+ # Extract labels
109
+ labels = []
110
+ if isinstance(node, dict):
111
+ # KuzuDB node label is under '_label'
112
+ if '_label' in node:
113
+ labels = [node['_label']]
114
+ elif 'label' in node:
115
+ labels = [node['label']]
116
+ else:
117
+ for label_attr in ['_labels', 'labels']:
118
+ if hasattr(node, label_attr):
119
+ attr_val = getattr(node, label_attr)
120
+ if attr_val:
121
+ labels = list(attr_val)
122
+ break
123
+
124
+ # Extract properties
125
+ props = {}
126
+ if isinstance(node, dict):
127
+ props = {k: v for k, v in node.items() if not k.startswith('_')}
128
+ else:
129
+ for prop_attr in ['properties', '_properties']:
130
+ if hasattr(node, prop_attr):
131
+ attr_val = getattr(node, prop_attr)
132
+ if attr_val:
133
+ props = dict(attr_val)
134
+ break
135
+
136
+ # Fallback if props still empty but node acts like dict
137
+ if not props and hasattr(node, 'items'):
138
+ try:
139
+ props = dict(node.items())
140
+ except: pass
141
+
142
+ # Extract name/label for frontend
143
+ # Prefer 'name' property, fallback to 'label', then 'path' or 'Unknown'
144
+ display_name = str(props.get('name', props.get('label', props.get('path', 'Unknown'))))
145
+
146
+ nodes_dict[eid] = {
147
+ "id": eid,
148
+ "name": display_name,
149
+ "label": display_name,
150
+ "type": str(labels[0]).capitalize() if labels else "Other",
151
+ "file": str(props.get('path', props.get('file', ''))),
152
+ "val": 4 if (labels and labels[0] in ['Repository', 'Class', 'Interface', 'Trait']) else 2,
153
+ "properties": props
154
+ }
155
+ except Exception as e:
156
+ print(f"DEBUG: Error parsing node: {e}", file=sys.stderr, flush=True)
157
+ continue
158
+
159
+ try:
160
+ rel = record.get('rel')
161
+ if rel is not None:
162
+ # One-shot debug: print type and keys/attrs of first rel
163
+ if not edges:
164
+ if isinstance(rel, dict):
165
+ print(f"DEBUG rel (dict): keys={list(rel.keys())}", file=sys.stderr, flush=True)
166
+ else:
167
+ print(f"DEBUG rel (obj): type={type(rel).__name__}, attrs={[a for a in dir(rel) if not a.startswith('__')]}", file=sys.stderr, flush=True)
168
+
169
+ # FalkorDB / KuzuDB may return rels as dicts OR objects
170
+ if isinstance(rel, dict):
171
+ rid = get_eid(rel)
172
+ # KuzuDB uses _src and _dst, FalkorDB uses src_node/dest_node
173
+ src = rel.get('_src', rel.get('src_node'))
174
+ dst = rel.get('_dst', rel.get('dest_node'))
175
+
176
+ source = get_eid(src) if src is not None else None
177
+ target = get_eid(dst) if dst is not None else None
178
+ rel_type = str(rel.get('_label', rel.get('relation', rel.get('type', 'RELATED')))).upper()
179
+ else:
180
+ rid = get_eid(rel)
181
+ start_node = None
182
+ end_node = None
183
+ for src_attr in ['start_node', 'src_node', '_src_node']:
184
+ if hasattr(rel, src_attr):
185
+ start_node = getattr(rel, src_attr)
186
+ break
187
+ for dest_attr in ['end_node', 'dest_node', '_dest_node']:
188
+ if hasattr(rel, dest_attr):
189
+ end_node = getattr(rel, dest_attr)
190
+ break
191
+ source = get_eid(start_node) if start_node is not None else None
192
+ target = get_eid(end_node) if end_node is not None else None
193
+ rel_type = "RELATED"
194
+ for rel_attr in ['type', 'relation', '_relation']:
195
+ if hasattr(rel, rel_attr):
196
+ rel_type = str(getattr(rel, rel_attr)).upper()
197
+ break
198
+
199
+ if source and target:
200
+ edges.append({
201
+ "id": rid,
202
+ "source": source,
203
+ "target": target,
204
+ "type": rel_type
205
+ })
206
+ except Exception as e:
207
+ print(f"DEBUG: Error parsing relationship: {e}", file=sys.stderr, flush=True)
208
+ pass
209
+
210
+ print(f"DEBUG: Processed {record_count} records. extracted {len(nodes_dict)} nodes and {len(edges)} edges.", file=sys.stderr, flush=True)
211
+
212
+ # Build a list of unique file paths from File-type nodes for the tree
213
+ file_paths = []
214
+ for n in nodes_dict.values():
215
+ if n.get("file") and str(n.get("type", "")).lower() == "file":
216
+ file_paths.append(str(n["file"]))
217
+ file_paths = sorted(list(set(file_paths)))
218
+
219
+ # Read file contents so the frontend never needs a separate API call
220
+ file_contents: dict[str, str] = {}
221
+ for fp in file_paths:
222
+ try:
223
+ with open(fp, "r", encoding="utf-8", errors="replace") as f:
224
+ file_contents[fp] = f.read()
225
+ except Exception:
226
+ pass
227
+
228
+ response_data = {
229
+ "nodes": list(nodes_dict.values()),
230
+ "links": edges,
231
+ "files": file_paths,
232
+ "fileContents": file_contents,
233
+ }
234
+
235
+ print(f"API SUCCESS: Returning graph with {len(response_data['nodes'])} nodes and {len(response_data['links'])} links.", file=sys.stderr, flush=True)
236
+ return response_data
237
+
238
+ except Exception as e:
239
+ debug_log(f"Error fetching graph: {str(e)}")
240
+ import traceback
241
+ traceback.print_exc()
242
+ # Still return a valid structure so the frontend doesn't crash, but with 500 status if raised
243
+ # Actually, let's just return a 500 error but with JSON body if possible
244
+ raise HTTPException(status_code=500, detail=str(e))
245
+
246
+ @app.get("/api/file")
247
+ async def get_file(path: str):
248
+ file_path = Path(path)
249
+ if not file_path.exists():
250
+ raise HTTPException(status_code=404, detail="File not found")
251
+
252
+ try:
253
+ with open(file_path, "r", encoding="utf-8") as f:
254
+ return {"content": f.read()}
255
+ except Exception as e:
256
+ raise HTTPException(status_code=500, detail=str(e))
257
+
258
+ # SPA fallback handler
259
+ @app.get("/{full_path:path}")
260
+ async def spa_fallback(request: Request, full_path: str):
261
+ global _static_dir
262
+ if not _static_dir:
263
+ return HTMLResponse("Static directory not configured", status_code=500)
264
+
265
+ # Filesystem path
266
+ file_path = Path(_static_dir) / full_path
267
+
268
+ # If the file exists and is a file, serve it normally
269
+ if file_path.exists() and file_path.is_file():
270
+ return FileResponse(file_path)
271
+
272
+ # Otherwise serve index.html (Standard SPA routing)
273
+ index_path = Path(_static_dir) / "index.html"
274
+ if index_path.exists():
275
+ return FileResponse(index_path)
276
+
277
+ return HTMLResponse("Not Found (Built UI not found in viz/dist)", status_code=404)
278
+
279
+ def run_server(host: str = "127.0.0.1", port: int = 8000, static_dir: Optional[str] = None):
280
+ global _static_dir
281
+ _static_dir = static_dir
282
+ uvicorn.run(app, host=host, port=port)