runtime-deps 0.1.0__tar.gz → 0.1.2__tar.gz
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.
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/PKG-INFO +4 -1
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/auto_req/cli.py +0 -55
- runtime_deps-0.1.2/auto_req/installer.py +219 -0
- runtime_deps-0.1.2/auto_req/mapper.py +567 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/pyproject.toml +6 -1
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/runtime_deps.egg-info/PKG-INFO +4 -1
- runtime_deps-0.1.0/auto_req/installer.py +0 -548
- runtime_deps-0.1.0/auto_req/mapper.py +0 -166
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/README.md +0 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/auto_req/__init__.py +0 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/auto_req/analyzer.py +0 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/auto_req/pytest_plugin.py +0 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/runtime_deps.egg-info/SOURCES.txt +0 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/runtime_deps.egg-info/dependency_links.txt +0 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/runtime_deps.egg-info/entry_points.txt +0 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/runtime_deps.egg-info/top_level.txt +0 -0
- {runtime_deps-0.1.0 → runtime_deps-0.1.2}/setup.cfg +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runtime-deps
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: AST-driven dynamic dependency resolution and runtime provisioning engine.
|
|
5
5
|
Author: Pavan Kumar Korrapati
|
|
6
|
+
Project-URL: Homepage, https://github.com/pavankorrapati/TraceReq
|
|
7
|
+
Project-URL: Repository, https://github.com/pavankorrapati/TraceReq.git
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/pavankorrapati/TraceReq/issues
|
|
6
9
|
Requires-Python: >=3.10
|
|
7
10
|
Description-Content-Type: text/markdown
|
|
8
11
|
|
|
@@ -1,58 +1,3 @@
|
|
|
1
|
-
# import argparse
|
|
2
|
-
# import subprocess
|
|
3
|
-
# import sys
|
|
4
|
-
# from pathlib import Path
|
|
5
|
-
# from auto_req.analyzer import CodeAnalyzer
|
|
6
|
-
# from auto_req.installer import PackageInstaller
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# def main():
|
|
10
|
-
# parser = argparse.ArgumentParser(
|
|
11
|
-
# description="TraceReq: Auto-detect and install missing Python requirements by analyzing code."
|
|
12
|
-
# )
|
|
13
|
-
# parser.add_argument(
|
|
14
|
-
# "target",
|
|
15
|
-
# nargs="?",
|
|
16
|
-
# default=".",
|
|
17
|
-
# help="Target directory or Python file to analyze (default: current directory)",
|
|
18
|
-
# )
|
|
19
|
-
# args = parser.parse_args()
|
|
20
|
-
|
|
21
|
-
# target_path = Path(args.target).resolve()
|
|
22
|
-
# analyzer = CodeAnalyzer()
|
|
23
|
-
|
|
24
|
-
# if target_path.is_file():
|
|
25
|
-
# required_imports = analyzer.extract_imports_from_file(target_path)
|
|
26
|
-
# elif target_path.is_dir():
|
|
27
|
-
# required_imports = analyzer.scan_directory(target_path)
|
|
28
|
-
# else:
|
|
29
|
-
# print(f"Error: Target path '{target_path}' does not exist.")
|
|
30
|
-
# sys.exit(1)
|
|
31
|
-
|
|
32
|
-
# installer = PackageInstaller(project_root=target_path.parent if target_path.is_file() else target_path)
|
|
33
|
-
# missing = installer.resolve_missing(required_imports)
|
|
34
|
-
|
|
35
|
-
# if not missing:
|
|
36
|
-
# print("[TraceReq] All required dependencies are already installed.")
|
|
37
|
-
# else:
|
|
38
|
-
# success = installer.install_packages(missing)
|
|
39
|
-
# if not success:
|
|
40
|
-
# sys.exit(1)
|
|
41
|
-
|
|
42
|
-
# # Automatically execute the file if target is a single Python script
|
|
43
|
-
# if target_path.is_file():
|
|
44
|
-
# python_exec = installer.get_target_python()
|
|
45
|
-
# print(f"[TraceReq] Running {target_path.name} using {python_exec.name}...\n")
|
|
46
|
-
|
|
47
|
-
# try:
|
|
48
|
-
# subprocess.run([str(python_exec), str(target_path)], check=True)
|
|
49
|
-
# except subprocess.CalledProcessError as e:
|
|
50
|
-
# print(f"\n[TraceReq] Execution failed with exit code {e.returncode}")
|
|
51
|
-
# sys.exit(e.returncode)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# if __name__ == "__main__":
|
|
55
|
-
# main()
|
|
56
1
|
import argparse
|
|
57
2
|
import subprocess
|
|
58
3
|
import sys
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional, Set
|
|
8
|
+
from auto_req.mapper import PyPIMapper
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PackageInstaller:
|
|
12
|
+
"""Detects missing dependencies and installs them into a project's local virtual environment."""
|
|
13
|
+
|
|
14
|
+
COMMON_VENV_NAMES = [".venv", "venv", "env", ".env", "virtualenv"]
|
|
15
|
+
|
|
16
|
+
# Special packages requiring additional commands after initial pip install
|
|
17
|
+
POST_INSTALL_HOOKS = {
|
|
18
|
+
"playwright": ["-m", "playwright", "install","chromium"]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Regex patterns mapped to repair actions when execution fails
|
|
22
|
+
ERROR_REMEDIATION_RULES = [
|
|
23
|
+
{
|
|
24
|
+
"pattern": r"Executable doesn't exist at .*ms-playwright",
|
|
25
|
+
"description": "Missing Playwright browser binaries",
|
|
26
|
+
"args": ["-m", "playwright", "install"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"pattern": r"playwright install",
|
|
30
|
+
"description": "Playwright post-install requirement detected",
|
|
31
|
+
"args": ["-m", "playwright", "install"],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"pattern": r"spacy\.util\.load_model.*Can't find model",
|
|
35
|
+
"description": "Missing spaCy language model",
|
|
36
|
+
"args": ["-m", "spacy", "download", "en_core_web_sm"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"pattern": r"Resource .* not found\. Please use the NLTK Downloader",
|
|
40
|
+
"description": "Missing NLTK data package",
|
|
41
|
+
"args": ["-c", "import nltk; nltk.download('all')"],
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
46
|
+
self.mapper = PyPIMapper()
|
|
47
|
+
self.stdlib_modules = set(sys.stdlib_module_names)
|
|
48
|
+
self.project_root = Path(project_root or Path.cwd()).resolve()
|
|
49
|
+
self.venv_path = self._detect_virtual_env()
|
|
50
|
+
|
|
51
|
+
def _detect_virtual_env(self) -> Optional[Path]:
|
|
52
|
+
"""Dynamically locates any virtual environment directory inside or around the project root."""
|
|
53
|
+
if "VIRTUAL_ENV" in os.environ:
|
|
54
|
+
active_env = Path(os.environ["VIRTUAL_ENV"])
|
|
55
|
+
if self._is_valid_venv(active_env):
|
|
56
|
+
return active_env
|
|
57
|
+
|
|
58
|
+
if sys.prefix != sys.base_prefix:
|
|
59
|
+
active_sys_venv = Path(sys.prefix)
|
|
60
|
+
if self._is_valid_venv(active_sys_venv):
|
|
61
|
+
return active_sys_venv
|
|
62
|
+
|
|
63
|
+
for name in self.COMMON_VENV_NAMES:
|
|
64
|
+
candidate = self.project_root / name
|
|
65
|
+
if self._is_valid_venv(candidate):
|
|
66
|
+
return candidate
|
|
67
|
+
|
|
68
|
+
for name in self.COMMON_VENV_NAMES:
|
|
69
|
+
candidate = self.project_root.parent / name
|
|
70
|
+
if self._is_valid_venv(candidate):
|
|
71
|
+
return candidate
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
for item in self.project_root.iterdir():
|
|
75
|
+
if item.is_dir() and item.name not in self.COMMON_VENV_NAMES:
|
|
76
|
+
if self._is_valid_venv(item):
|
|
77
|
+
return item
|
|
78
|
+
except PermissionError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def _is_valid_venv(self, path: Path) -> bool:
|
|
84
|
+
"""Checks if a directory contains a valid virtual environment Python binary."""
|
|
85
|
+
if os.name == "nt": # Windows
|
|
86
|
+
py_exec = path / "Scripts" / "python.exe"
|
|
87
|
+
else: # macOS / Linux
|
|
88
|
+
py_exec = path / "bin" / "python"
|
|
89
|
+
return py_exec.exists()
|
|
90
|
+
|
|
91
|
+
def get_target_python(self) -> Path:
|
|
92
|
+
"""Returns the Python binary path of the detected venv, or system Python fallback."""
|
|
93
|
+
if self.venv_path:
|
|
94
|
+
if os.name == "nt":
|
|
95
|
+
py_exec = self.venv_path / "Scripts" / "python.exe"
|
|
96
|
+
else:
|
|
97
|
+
py_exec = self.venv_path / "bin" / "python"
|
|
98
|
+
|
|
99
|
+
if py_exec.exists():
|
|
100
|
+
return py_exec
|
|
101
|
+
|
|
102
|
+
return Path(sys.executable)
|
|
103
|
+
|
|
104
|
+
def get_installed_packages(self) -> Set[str]:
|
|
105
|
+
"""Returns normalized names of installed packages in the target venv/environment."""
|
|
106
|
+
search_paths = []
|
|
107
|
+
|
|
108
|
+
if self.venv_path:
|
|
109
|
+
if os.name == "nt":
|
|
110
|
+
venv_site = self.venv_path / "Lib" / "site-packages"
|
|
111
|
+
else:
|
|
112
|
+
venv_site = (
|
|
113
|
+
self.venv_path
|
|
114
|
+
/ "lib"
|
|
115
|
+
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
116
|
+
/ "site-packages"
|
|
117
|
+
)
|
|
118
|
+
if venv_site.exists():
|
|
119
|
+
search_paths = [str(venv_site)]
|
|
120
|
+
|
|
121
|
+
installed = set()
|
|
122
|
+
dists = (
|
|
123
|
+
importlib.metadata.distributions(path=search_paths)
|
|
124
|
+
if search_paths
|
|
125
|
+
else importlib.metadata.distributions()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
for dist in dists:
|
|
129
|
+
name = dist.metadata.get("Name")
|
|
130
|
+
if name:
|
|
131
|
+
installed.add(name.lower().replace("_", "-"))
|
|
132
|
+
return installed
|
|
133
|
+
|
|
134
|
+
def resolve_missing(self, required_imports: Set[str]) -> List[str]:
|
|
135
|
+
"""Identifies imports that are not installed in the target environment."""
|
|
136
|
+
installed = self.get_installed_packages()
|
|
137
|
+
missing_pypi_packages = []
|
|
138
|
+
|
|
139
|
+
# 1. Ignore Python built-in standard library modules (os, sys, math, etc.)
|
|
140
|
+
third_party_imports = required_imports - self.stdlib_modules
|
|
141
|
+
|
|
142
|
+
# 2. Ignore Local Project Modules/Folders (e.g., auto_req, utils, tests)
|
|
143
|
+
external_imports = set()
|
|
144
|
+
for imp in third_party_imports:
|
|
145
|
+
local_folder = self.project_root / imp
|
|
146
|
+
local_file = self.project_root / f"{imp}.py"
|
|
147
|
+
|
|
148
|
+
# If the folder or file exists locally in your project, skip it!
|
|
149
|
+
if local_folder.exists() or local_file.exists():
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
external_imports.add(imp)
|
|
153
|
+
|
|
154
|
+
# 3. Only attempt to download actual third-party packages from PyPI
|
|
155
|
+
for imp in external_imports:
|
|
156
|
+
pypi_name = self.mapper.map_import_to_pypi(imp)
|
|
157
|
+
normalized_pypi = pypi_name.lower().replace("_", "-")
|
|
158
|
+
|
|
159
|
+
if normalized_pypi not in installed:
|
|
160
|
+
missing_pypi_packages.append(pypi_name)
|
|
161
|
+
|
|
162
|
+
return missing_pypi_packages
|
|
163
|
+
|
|
164
|
+
def run_post_install_hooks(self, installed_packages: List[str], python_exec: Path):
|
|
165
|
+
"""Runs additional setup commands required by specific packages after pip install."""
|
|
166
|
+
for pkg in installed_packages:
|
|
167
|
+
normalized_pkg = pkg.lower().replace("_", "-")
|
|
168
|
+
if normalized_pkg in self.POST_INSTALL_HOOKS:
|
|
169
|
+
hook_args = self.POST_INSTALL_HOOKS[normalized_pkg]
|
|
170
|
+
cmd = [str(python_exec), *hook_args]
|
|
171
|
+
print(f"[TraceReq] Running post-installation setup for '{pkg}': {' '.join(cmd)}")
|
|
172
|
+
subprocess.run(cmd, check=False)
|
|
173
|
+
|
|
174
|
+
def install_packages(self, packages: List[str]) -> bool:
|
|
175
|
+
"""Executes pip install targeting the detected virtual environment and triggers post-install hooks."""
|
|
176
|
+
if not packages:
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
python_exec = self.get_target_python()
|
|
180
|
+
|
|
181
|
+
if self.venv_path:
|
|
182
|
+
print(f"\n[TraceReq] Target Virtual Environment Found: {self.venv_path.name}")
|
|
183
|
+
else:
|
|
184
|
+
print("\n[TraceReq] No local venv found. Target: Global Environment")
|
|
185
|
+
|
|
186
|
+
print(f"[TraceReq] Target Python: {python_exec}")
|
|
187
|
+
print(f"[TraceReq] Installing dependencies: {', '.join(packages)}...")
|
|
188
|
+
|
|
189
|
+
cmd = [str(python_exec), "-m", "pip", "install", *packages]
|
|
190
|
+
result = subprocess.run(cmd)
|
|
191
|
+
|
|
192
|
+
if result.returncode == 0:
|
|
193
|
+
print("[TraceReq] Installation completed successfully!")
|
|
194
|
+
self.run_post_install_hooks(packages, python_exec)
|
|
195
|
+
print()
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
print("[TraceReq] Error occurred during pip installation.\n")
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
def attempt_error_remediation(self, error_output: str) -> bool:
|
|
202
|
+
"""Parses stdout/stderr tracebacks for known dependency errors and attempts repair."""
|
|
203
|
+
python_exec = self.get_target_python()
|
|
204
|
+
|
|
205
|
+
for rule in self.ERROR_REMEDIATION_RULES:
|
|
206
|
+
if re.search(rule["pattern"], error_output, re.IGNORECASE):
|
|
207
|
+
print(f"\n[TraceReq] Detected execution error: {rule['description']}")
|
|
208
|
+
cmd = [str(python_exec), *rule["args"]]
|
|
209
|
+
print(f"[TraceReq] Auto-remediating: {' '.join(cmd)}")
|
|
210
|
+
|
|
211
|
+
result = subprocess.run(cmd)
|
|
212
|
+
if result.returncode == 0:
|
|
213
|
+
print("[TraceReq] Auto-remediation succeeded!\n")
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
print("[TraceReq] Auto-remediation failed.\n")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
return False
|