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.
- graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
- graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
- graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
- graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
- graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/application/__init__.py +0 -0
- src/application/dtos/__init__.py +0 -0
- src/application/dtos/affected_component.py +59 -0
- src/application/dtos/batch_processing_result.py +17 -0
- src/application/dtos/impact_analysis_response.py +56 -0
- src/application/dtos/impact_query_request.py +38 -0
- src/application/dtos/index_batch_request.py +53 -0
- src/application/dtos/index_batch_response.py +72 -0
- src/application/dtos/index_result.py +20 -0
- src/application/dtos/repository_index_result.py +79 -0
- src/application/exceptions.py +41 -0
- src/application/services/__init__.py +0 -0
- src/application/services/file_scanner_service.py +258 -0
- src/application/services/progress_tracker.py +132 -0
- src/application/use_cases/__init__.py +0 -0
- src/application/use_cases/clear_all_data_use_case.py +180 -0
- src/application/use_cases/index_batch_use_case.py +153 -0
- src/application/use_cases/index_repository_use_case.py +683 -0
- src/application/use_cases/indexing_orchestrator.py +181 -0
- src/application/use_cases/list_repositories_use_case.py +84 -0
- src/cli.py +512 -0
- src/config/__init__.py +5 -0
- src/config/container.py +211 -0
- src/config/logging_config.py +123 -0
- src/config/settings.py +165 -0
- src/domain/__init__.py +0 -0
- src/domain/entities/__init__.py +20 -0
- src/domain/entities/ast_node.py +32 -0
- src/domain/entities/code_chunk.py +45 -0
- src/domain/entities/code_file.py +44 -0
- src/domain/entities/dependency_graph.py +110 -0
- src/domain/entities/repository.py +46 -0
- src/domain/exceptions.py +36 -0
- src/domain/repositories/__init__.py +22 -0
- src/domain/repositories/i_code_parser.py +50 -0
- src/domain/repositories/i_dependency_extractor.py +35 -0
- src/domain/repositories/i_embedding_provider.py +31 -0
- src/domain/repositories/i_graph_repository.py +78 -0
- src/domain/repositories/i_vector_repository.py +55 -0
- src/domain/services/__init__.py +8 -0
- src/domain/services/dependency_extractor.py +962 -0
- src/domain/services/semantic_chunk_builder.py +719 -0
- src/domain/value_objects/__init__.py +12 -0
- src/domain/value_objects/chunk_type.py +24 -0
- src/domain/value_objects/dependency_type.py +22 -0
- src/domain/value_objects/node_type.py +22 -0
- src/domain/value_objects/qualified_name.py +78 -0
- src/infrastructure/__init__.py +0 -0
- src/infrastructure/exceptions.py +41 -0
- src/infrastructure/external_apis/__init__.py +0 -0
- src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
- src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
- src/infrastructure/parsing/__init__.py +20 -0
- src/infrastructure/parsing/config_file_parser.py +347 -0
- src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
- src/infrastructure/parsing/groovy_parser.py +198 -0
- src/infrastructure/parsing/maven_dependency_parser.py +147 -0
- src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
- src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
- src/infrastructure/parsing/python_parser.py +211 -0
- src/infrastructure/parsing/sql_schema_parser.py +658 -0
- src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
- src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
- src/infrastructure/persistence/__init__.py +0 -0
- src/infrastructure/persistence/graph_export_repository.py +443 -0
- src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
- src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
- src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
- src/presentation/__init__.py +0 -0
- src/presentation/api/__init__.py +0 -0
- src/presentation/api/rest/__init__.py +0 -0
- src/presentation/api/rest/exception_handlers.py +174 -0
- src/presentation/api/rest/main.py +80 -0
- src/presentation/api/rest/routers/__init__.py +5 -0
- src/presentation/api/rest/routers/admin.py +103 -0
- src/presentation/api/rest/routers/analysis.py +980 -0
- src/presentation/api/rest/routers/export.py +442 -0
- src/presentation/api/rest/routers/health.py +117 -0
- src/presentation/api/rest/routers/indexing.py +148 -0
- 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
|
+
)
|