graph-dependency-analyzer 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
  2. graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
  3. graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
  4. graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
  5. graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
  6. src/__init__.py +0 -0
  7. src/application/__init__.py +0 -0
  8. src/application/dtos/__init__.py +0 -0
  9. src/application/dtos/affected_component.py +59 -0
  10. src/application/dtos/batch_processing_result.py +17 -0
  11. src/application/dtos/impact_analysis_response.py +56 -0
  12. src/application/dtos/impact_query_request.py +38 -0
  13. src/application/dtos/index_batch_request.py +53 -0
  14. src/application/dtos/index_batch_response.py +72 -0
  15. src/application/dtos/index_result.py +20 -0
  16. src/application/dtos/repository_index_result.py +79 -0
  17. src/application/exceptions.py +41 -0
  18. src/application/services/__init__.py +0 -0
  19. src/application/services/file_scanner_service.py +258 -0
  20. src/application/services/progress_tracker.py +132 -0
  21. src/application/use_cases/__init__.py +0 -0
  22. src/application/use_cases/clear_all_data_use_case.py +180 -0
  23. src/application/use_cases/index_batch_use_case.py +153 -0
  24. src/application/use_cases/index_repository_use_case.py +683 -0
  25. src/application/use_cases/indexing_orchestrator.py +181 -0
  26. src/application/use_cases/list_repositories_use_case.py +84 -0
  27. src/cli.py +512 -0
  28. src/config/__init__.py +5 -0
  29. src/config/container.py +211 -0
  30. src/config/logging_config.py +123 -0
  31. src/config/settings.py +165 -0
  32. src/domain/__init__.py +0 -0
  33. src/domain/entities/__init__.py +20 -0
  34. src/domain/entities/ast_node.py +32 -0
  35. src/domain/entities/code_chunk.py +45 -0
  36. src/domain/entities/code_file.py +44 -0
  37. src/domain/entities/dependency_graph.py +110 -0
  38. src/domain/entities/repository.py +46 -0
  39. src/domain/exceptions.py +36 -0
  40. src/domain/repositories/__init__.py +22 -0
  41. src/domain/repositories/i_code_parser.py +50 -0
  42. src/domain/repositories/i_dependency_extractor.py +35 -0
  43. src/domain/repositories/i_embedding_provider.py +31 -0
  44. src/domain/repositories/i_graph_repository.py +78 -0
  45. src/domain/repositories/i_vector_repository.py +55 -0
  46. src/domain/services/__init__.py +8 -0
  47. src/domain/services/dependency_extractor.py +962 -0
  48. src/domain/services/semantic_chunk_builder.py +719 -0
  49. src/domain/value_objects/__init__.py +12 -0
  50. src/domain/value_objects/chunk_type.py +24 -0
  51. src/domain/value_objects/dependency_type.py +22 -0
  52. src/domain/value_objects/node_type.py +22 -0
  53. src/domain/value_objects/qualified_name.py +78 -0
  54. src/infrastructure/__init__.py +0 -0
  55. src/infrastructure/exceptions.py +41 -0
  56. src/infrastructure/external_apis/__init__.py +0 -0
  57. src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
  58. src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
  59. src/infrastructure/parsing/__init__.py +20 -0
  60. src/infrastructure/parsing/config_file_parser.py +347 -0
  61. src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
  62. src/infrastructure/parsing/groovy_parser.py +198 -0
  63. src/infrastructure/parsing/maven_dependency_parser.py +147 -0
  64. src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
  65. src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
  66. src/infrastructure/parsing/python_parser.py +211 -0
  67. src/infrastructure/parsing/sql_schema_parser.py +658 -0
  68. src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
  69. src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
  70. src/infrastructure/persistence/__init__.py +0 -0
  71. src/infrastructure/persistence/graph_export_repository.py +443 -0
  72. src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
  73. src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
  74. src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
  75. src/presentation/__init__.py +0 -0
  76. src/presentation/api/__init__.py +0 -0
  77. src/presentation/api/rest/__init__.py +0 -0
  78. src/presentation/api/rest/exception_handlers.py +174 -0
  79. src/presentation/api/rest/main.py +80 -0
  80. src/presentation/api/rest/routers/__init__.py +5 -0
  81. src/presentation/api/rest/routers/admin.py +103 -0
  82. src/presentation/api/rest/routers/analysis.py +980 -0
  83. src/presentation/api/rest/routers/export.py +442 -0
  84. src/presentation/api/rest/routers/health.py +117 -0
  85. src/presentation/api/rest/routers/indexing.py +148 -0
  86. src/presentation/api/rest/routers/relationships.py +1089 -0
@@ -0,0 +1,255 @@
1
+ """NpmPackageDependencyParser - Parse NPM/Yarn package dependencies.
2
+
3
+ Extracts dependencies from package.json, package-lock.json, and yarn.lock files.
4
+ """
5
+ import json
6
+ import logging
7
+ import re
8
+ from typing import Any
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class NpmPackageDependencyParser:
14
+ """
15
+ NPM/Yarn package dependency parser.
16
+
17
+ Extracts dependencies from package.json, package-lock.json, and yarn.lock.
18
+ """
19
+
20
+ def parse_package_json(self, content: str) -> list[dict]:
21
+ """
22
+ Parse package.json and extract all dependencies.
23
+
24
+ Args:
25
+ content: Raw package.json file content
26
+
27
+ Returns:
28
+ List of dependency dictionaries (empty list if completely malformed)
29
+
30
+ Note:
31
+ Logs warning and returns empty list if JSON is completely malformed.
32
+ Logs warning and extracts valid sections if only some sections are malformed.
33
+ """
34
+ try:
35
+ data = json.loads(content)
36
+ except json.JSONDecodeError as e:
37
+ logger.warning(f"Invalid JSON in package.json: {e}. Returning empty list.")
38
+ return []
39
+
40
+ dependencies = []
41
+
42
+ # Extract production dependencies with error handling
43
+ if "dependencies" in data:
44
+ try:
45
+ if isinstance(data["dependencies"], dict):
46
+ dependencies.extend(
47
+ self._parse_dependency_section(data["dependencies"], "production")
48
+ )
49
+ else:
50
+ logger.warning("dependencies section is not an object, skipping")
51
+ except Exception as e:
52
+ logger.warning(f"Error parsing dependencies section: {e}")
53
+
54
+ # Extract devDependencies with error handling
55
+ if "devDependencies" in data:
56
+ try:
57
+ if isinstance(data["devDependencies"], dict):
58
+ dependencies.extend(
59
+ self._parse_dependency_section(data["devDependencies"], "development")
60
+ )
61
+ else:
62
+ logger.warning("devDependencies section is not an object, skipping")
63
+ except Exception as e:
64
+ logger.warning(f"Error parsing devDependencies section: {e}")
65
+
66
+ # Extract peerDependencies with error handling
67
+ if "peerDependencies" in data:
68
+ try:
69
+ if isinstance(data["peerDependencies"], dict):
70
+ dependencies.extend(
71
+ self._parse_dependency_section(data["peerDependencies"], "peer")
72
+ )
73
+ else:
74
+ logger.warning("peerDependencies section is not an object, skipping")
75
+ except Exception as e:
76
+ logger.warning(f"Error parsing peerDependencies section: {e}")
77
+
78
+ return dependencies
79
+
80
+ def _parse_dependency_section(
81
+ self, deps: dict[str, str], scope: str
82
+ ) -> list[dict]:
83
+ """Parse a dependency section (dependencies, devDependencies, etc.)."""
84
+ result = []
85
+ for name, version in deps.items():
86
+ group_id, artifact_id = self._parse_package_name(name)
87
+ result.append(
88
+ {
89
+ "name": name,
90
+ "version": version,
91
+ "scope": scope,
92
+ "group_id": group_id,
93
+ "artifact_id": artifact_id,
94
+ }
95
+ )
96
+ return result
97
+
98
+ def _parse_package_name(self, name: str) -> tuple[str, str]:
99
+ """
100
+ Parse package name into group_id and artifact_id.
101
+
102
+ Args:
103
+ name: Package name (e.g., "@types/react" or "react")
104
+
105
+ Returns:
106
+ Tuple of (group_id, artifact_id)
107
+ """
108
+ if name.startswith("@"):
109
+ # Scoped package: @scope/name
110
+ parts = name.split("/", 1)
111
+ if len(parts) == 2:
112
+ return parts[0], parts[1]
113
+ return "", name
114
+ else:
115
+ # Regular package
116
+ return "", name
117
+
118
+ def parse_package_lock(self, content: str) -> list[dict]:
119
+ """
120
+ Parse package-lock.json and extract resolved versions.
121
+
122
+ Args:
123
+ content: Raw package-lock.json file content
124
+
125
+ Returns:
126
+ List of dependency dictionaries with resolved versions and transitive flag
127
+ """
128
+ try:
129
+ data = json.loads(content)
130
+ except json.JSONDecodeError:
131
+ return []
132
+
133
+ dependencies = []
134
+ lockfile_version = data.get("lockfileVersion", 1)
135
+
136
+ if lockfile_version >= 2:
137
+ # lockfileVersion 2 and 3 use "packages" object
138
+ packages = data.get("packages", {})
139
+
140
+ # Get direct dependencies from root package
141
+ root_pkg = packages.get("", {})
142
+ direct_deps = set()
143
+ for dep_section in ["dependencies", "devDependencies", "peerDependencies"]:
144
+ if dep_section in root_pkg:
145
+ direct_deps.update(root_pkg[dep_section].keys())
146
+
147
+ for pkg_path, pkg_info in packages.items():
148
+ if pkg_path == "":
149
+ # Root package, skip
150
+ continue
151
+
152
+ # Extract package name from path (e.g., "node_modules/react" -> "react")
153
+ name = pkg_path.replace("node_modules/", "")
154
+ version = pkg_info.get("version")
155
+
156
+ if name and version:
157
+ group_id, artifact_id = self._parse_package_name(name)
158
+
159
+ # Check if this is a transitive dependency
160
+ is_transitive = name not in direct_deps
161
+
162
+ dependencies.append(
163
+ {
164
+ "name": name,
165
+ "version": version,
166
+ "resolved": True,
167
+ "transitive": is_transitive,
168
+ "group_id": group_id,
169
+ "artifact_id": artifact_id,
170
+ }
171
+ )
172
+ else:
173
+ # lockfileVersion 1 uses "dependencies" object
174
+ deps = data.get("dependencies", {})
175
+ dependencies.extend(self._parse_lockfile_v1_dependencies(deps))
176
+
177
+ return dependencies
178
+
179
+ def _parse_lockfile_v1_dependencies(self, deps: dict) -> list[dict]:
180
+ """Parse lockfileVersion 1 dependencies recursively."""
181
+ result = []
182
+ for name, info in deps.items():
183
+ version = info.get("version")
184
+ if version:
185
+ group_id, artifact_id = self._parse_package_name(name)
186
+ result.append(
187
+ {
188
+ "name": name,
189
+ "version": version,
190
+ "resolved": True,
191
+ "group_id": group_id,
192
+ "artifact_id": artifact_id,
193
+ }
194
+ )
195
+
196
+ # Recursively parse nested dependencies
197
+ if "dependencies" in info:
198
+ result.extend(self._parse_lockfile_v1_dependencies(info["dependencies"]))
199
+
200
+ return result
201
+
202
+ def parse_yarn_lock(self, content: str) -> list[dict]:
203
+ """
204
+ Parse yarn.lock and extract resolved versions.
205
+
206
+ Args:
207
+ content: Raw yarn.lock file content
208
+
209
+ Returns:
210
+ List of dependency dictionaries
211
+
212
+ Implementation:
213
+ Uses line-based regex parsing for yarn.lock format
214
+ """
215
+ dependencies = []
216
+ current_package = None
217
+ current_name = None
218
+ current_range = None
219
+
220
+ lines = content.split("\n")
221
+ for line in lines:
222
+ # Package declaration line: react@^18.2.0:
223
+ if line and not line.startswith(" ") and "@" in line and line.endswith(":"):
224
+ # Extract package name and version range
225
+ declaration = line.rstrip(":")
226
+ # Handle quoted names: "@babel/core@^7.22.0"
227
+ declaration = declaration.strip('"')
228
+
229
+ if "@" in declaration:
230
+ # Split by last @ to separate name and version
231
+ parts = declaration.rsplit("@", 1)
232
+ if len(parts) == 2:
233
+ current_name = parts[0]
234
+ current_range = parts[1]
235
+ current_package = {"name": current_name, "version_range": current_range}
236
+
237
+ # Version line: version "18.2.0"
238
+ elif line.strip().startswith("version") and current_package:
239
+ match = re.search(r'version\s+"([^"]+)"', line)
240
+ if match:
241
+ version = match.group(1)
242
+ current_package["version"] = version
243
+
244
+ # Parse name into group_id/artifact_id
245
+ group_id, artifact_id = self._parse_package_name(current_name)
246
+ current_package["group_id"] = group_id
247
+ current_package["artifact_id"] = artifact_id
248
+
249
+ # Add to results
250
+ dependencies.append(current_package.copy())
251
+ current_package = None
252
+ current_name = None
253
+ current_range = None
254
+
255
+ return dependencies
@@ -0,0 +1,211 @@
1
+ """
2
+ Python source code parser — extracts classes, functions, imports, and API calls.
3
+
4
+ Uses regex-based parsing (no AST dependency) to extract:
5
+ - Module-level imports and from-imports
6
+ - Class definitions with methods
7
+ - Function definitions
8
+ - HTTP calls (requests, httpx, aiohttp, urllib)
9
+ - Azure SDK usage (azure.identity, azure.appconfiguration, etc.)
10
+ - URL strings and environment variable references
11
+ - Database connections (SQLAlchemy, psycopg2, pymongo, etc.)
12
+
13
+ Returns ParseResult compatible with the existing indexing pipeline.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from pathlib import Path
19
+
20
+ from src.domain.repositories.i_code_parser import ParseResult
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Regex patterns
25
+ # ---------------------------------------------------------------------------
26
+
27
+ # import os / import sys, json
28
+ _RE_IMPORT = re.compile(r'^import\s+([\w,\s]+)', re.M)
29
+
30
+ # from azure.identity import DefaultAzureCredential
31
+ _RE_FROM_IMPORT = re.compile(r'^from\s+([\w.]+)\s+import\s+(.+)', re.M)
32
+
33
+ # class Config: / class MyClass(Base):
34
+ _RE_CLASS = re.compile(r'^class\s+(\w+)\s*(?:\(([^)]*)\))?:', re.M)
35
+
36
+ # def my_function( / async def handler(
37
+ _RE_DEF = re.compile(r'^(?:async\s+)?def\s+(\w+)\s*\(', re.M)
38
+
39
+ # HTTP calls: requests.get("url"), httpx.post("url"), self.client.get("url")
40
+ _RE_HTTP_CALL = re.compile(
41
+ r'(?:requests|httpx|aiohttp|urllib\.request|self\.\w*(?:client|http|session)\w*)'
42
+ r'\.(get|post|put|patch|delete|request)\s*\(\s*["\']([^"\']{5,200})["\']',
43
+ re.I,
44
+ )
45
+
46
+ # String literals that look like URLs
47
+ _RE_URL_STRING = re.compile(
48
+ r'["\']((https?://|wss?://)[^\s"\'<>]{5,200})["\']',
49
+ re.I,
50
+ )
51
+
52
+ # os.getenv / os.environ references (env var names)
53
+ _RE_ENV_VAR = re.compile(r'os\.(?:getenv|environ\.get)\s*\(\s*["\']([^"\']+)["\']')
54
+
55
+ # Azure SDK imports
56
+ _RE_AZURE = re.compile(r'(?:from|import)\s+(azure\.[.\w]+)', re.I)
57
+
58
+ # Database connection strings / SQLAlchemy engine
59
+ _RE_DB_URL = re.compile(
60
+ r'["\'](?:postgresql|postgres|mysql|sqlite|oracle|mssql|mongodb|redis)'
61
+ r'(?:\+\w+)?://[^\s"\']{3,200}["\']',
62
+ re.I,
63
+ )
64
+
65
+ # Decorators that indicate HTTP endpoints (FastAPI, Flask)
66
+ _RE_ROUTE = re.compile(
67
+ r'@(?:app|router|blueprint)\.(get|post|put|patch|delete)\s*\(\s*["\']([^"\']+)["\']',
68
+ re.I,
69
+ )
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Helper
74
+ # ---------------------------------------------------------------------------
75
+
76
+ def _line_of(content: str, pos: int) -> int:
77
+ return content[:pos].count('\n') + 1
78
+
79
+
80
+ class PythonParser:
81
+ """Parser for Python source files."""
82
+
83
+ PYTHON_EXTENSIONS = {'.py', '.pyw'}
84
+
85
+ def parse_file(self, file_path: Path) -> ParseResult:
86
+ if not file_path.exists():
87
+ return ParseResult(file_path=file_path)
88
+
89
+ try:
90
+ content = file_path.read_text(encoding='utf-8', errors='replace')
91
+ except Exception:
92
+ return ParseResult(file_path=file_path)
93
+
94
+ if file_path.suffix.lower() not in self.PYTHON_EXTENSIONS:
95
+ return ParseResult(file_path=file_path)
96
+
97
+ # ── Derive module qualified name from path ─────────────────────────
98
+ # e.g. shared/config.py → shared.config
99
+ stem_parts = list(file_path.parts)
100
+ # Find 'src' or repo root anchor
101
+ module_name = file_path.stem
102
+ try:
103
+ for anchor in ('src', 'app', 'lib'):
104
+ if anchor in stem_parts:
105
+ idx = stem_parts.index(anchor)
106
+ parts = stem_parts[idx:]
107
+ module_name = '.'.join(
108
+ p for p in parts[:-1] # dirs
109
+ ) + '.' + file_path.stem
110
+ break
111
+ except Exception:
112
+ pass
113
+
114
+ # ── Classes ──────────────────────────────────────────────────────
115
+ classes = []
116
+ for m in _RE_CLASS.finditer(content):
117
+ class_name = m.group(1)
118
+ bases = m.group(2) or ''
119
+ qname = f"{module_name}.{class_name}"
120
+ classes.append({
121
+ 'name': class_name,
122
+ 'qualified_name': qname,
123
+ 'node_type': 'CLASS',
124
+ 'line': _line_of(content, m.start()),
125
+ 'metadata': {
126
+ 'language': 'python',
127
+ 'base_classes': bases.strip(),
128
+ 'file_path': str(file_path),
129
+ }
130
+ })
131
+
132
+ # ── Functions / methods ───────────────────────────────────────────
133
+ methods = []
134
+ for m in _RE_DEF.finditer(content):
135
+ fn_name = m.group(1)
136
+ if fn_name.startswith('_') and fn_name != '__init__':
137
+ continue # skip private helpers
138
+ qname = f"{module_name}.{fn_name}"
139
+ methods.append({
140
+ 'name': fn_name,
141
+ 'qualified_name': qname,
142
+ 'node_type': 'METHOD',
143
+ 'line': _line_of(content, m.start()),
144
+ 'metadata': {
145
+ 'language': 'python',
146
+ 'file_path': str(file_path),
147
+ }
148
+ })
149
+
150
+ # ── Imports ───────────────────────────────────────────────────────
151
+ imports = []
152
+ seen_imports: set[str] = set()
153
+
154
+ for m in _RE_IMPORT.finditer(content):
155
+ for mod in m.group(1).split(','):
156
+ mod = mod.strip().split('\n')[0].strip() # take only first line
157
+ if mod and '.' not in mod or mod.count('.') < 3: # skip very long chains
158
+ if mod and mod not in seen_imports:
159
+ seen_imports.add(mod)
160
+ imports.append({
161
+ 'import_path': mod,
162
+ 'line': _line_of(content, m.start()),
163
+ })
164
+
165
+ for m in _RE_FROM_IMPORT.finditer(content):
166
+ mod = m.group(1).strip()
167
+ if mod and mod not in seen_imports:
168
+ seen_imports.add(mod)
169
+ imports.append({
170
+ 'import_path': mod,
171
+ 'line': _line_of(content, m.start()),
172
+ })
173
+
174
+ # ── Method calls / API refs ───────────────────────────────────────
175
+ method_calls = []
176
+
177
+ # HTTP calls with URL
178
+ for m in _RE_HTTP_CALL.finditer(content):
179
+ method_calls.append({
180
+ 'callee': m.group(0).split('(')[0].strip(),
181
+ 'is_api_call': True,
182
+ 'api_path': m.group(2),
183
+ 'line': _line_of(content, m.start()),
184
+ })
185
+
186
+ # FastAPI / Flask route decorators → ENDPOINT
187
+ for m in _RE_ROUTE.finditer(content):
188
+ method_calls.append({
189
+ '_is_endpoint': True,
190
+ 'http_method': m.group(1).upper(),
191
+ 'path': m.group(2),
192
+ 'line': _line_of(content, m.start()),
193
+ })
194
+
195
+ # ── String literals (URLs + env vars) ─────────────────────────────
196
+ string_literals: list[str] = []
197
+ for m in _RE_URL_STRING.finditer(content):
198
+ string_literals.append(m.group(1))
199
+ for m in _RE_ENV_VAR.finditer(content):
200
+ string_literals.append(f"env:{m.group(1)}")
201
+ for m in _RE_DB_URL.finditer(content):
202
+ string_literals.append(m.group(0).strip('"\''))
203
+
204
+ return ParseResult(
205
+ file_path=file_path,
206
+ classes=classes,
207
+ methods=methods,
208
+ imports=imports,
209
+ method_calls=method_calls,
210
+ string_literals=string_literals,
211
+ )