scitex 2.17.0__py3-none-any.whl → 2.17.4__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.
- scitex/_dev/__init__.py +122 -0
- scitex/_dev/_config.py +391 -0
- scitex/_dev/_dashboard/__init__.py +11 -0
- scitex/_dev/_dashboard/_app.py +89 -0
- scitex/_dev/_dashboard/_routes.py +182 -0
- scitex/_dev/_dashboard/_scripts.py +422 -0
- scitex/_dev/_dashboard/_styles.py +295 -0
- scitex/_dev/_dashboard/_templates.py +130 -0
- scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
- scitex/_dev/_ecosystem.py +109 -0
- scitex/_dev/_github.py +360 -0
- scitex/_dev/_mcp/__init__.py +11 -0
- scitex/_dev/_mcp/handlers.py +182 -0
- scitex/_dev/_rtd.py +122 -0
- scitex/_dev/_ssh.py +362 -0
- scitex/_dev/_versions.py +272 -0
- scitex/_mcp_tools/__init__.py +2 -0
- scitex/_mcp_tools/dev.py +186 -0
- scitex/audio/_audio_check.py +84 -41
- scitex/cli/capture.py +45 -22
- scitex/cli/dev.py +494 -0
- scitex/cli/main.py +2 -0
- scitex/cli/stats.py +48 -20
- scitex/cli/verify.py +33 -36
- scitex/plt/__init__.py +16 -6
- scitex/scholar/_mcp/crossref_handlers.py +45 -7
- scitex/scholar/_mcp/openalex_handlers.py +45 -7
- scitex/scholar/config/default.yaml +2 -0
- scitex/scholar/local_dbs/__init__.py +5 -1
- scitex/scholar/local_dbs/export.py +93 -0
- scitex/scholar/local_dbs/unified.py +505 -0
- scitex/scholar/metadata_engines/ScholarEngine.py +11 -0
- scitex/scholar/metadata_engines/individual/OpenAlexLocalEngine.py +346 -0
- scitex/scholar/metadata_engines/individual/__init__.py +1 -0
- scitex/template/__init__.py +18 -1
- scitex/template/clone_research_minimal.py +111 -0
- scitex/verify/README.md +0 -12
- scitex/verify/__init__.py +0 -4
- scitex/verify/_visualize.py +0 -4
- scitex/verify/_viz/__init__.py +0 -18
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/METADATA +2 -1
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/RECORD +45 -24
- scitex/verify/_viz/_plotly.py +0 -193
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/WHEEL +0 -0
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/entry_points.txt +0 -0
- {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/licenses/LICENSE +0 -0
scitex/_dev/_ssh.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-02-02
|
|
3
|
+
# File: scitex/_dev/_ssh.py
|
|
4
|
+
|
|
5
|
+
"""SSH-based remote version checking for scitex ecosystem."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ._config import DevConfig, HostConfig, get_enabled_hosts, load_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_remote_version(host: HostConfig, package: str) -> dict[str, Any]:
|
|
16
|
+
"""Get version of a package on a remote host via SSH.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
host : HostConfig
|
|
21
|
+
Host configuration.
|
|
22
|
+
package : str
|
|
23
|
+
Package name (PyPI name).
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
dict
|
|
28
|
+
Version info with keys: installed, status, error (if any).
|
|
29
|
+
"""
|
|
30
|
+
# Build SSH command
|
|
31
|
+
ssh_args = ["ssh"]
|
|
32
|
+
|
|
33
|
+
if host.ssh_key:
|
|
34
|
+
ssh_args.extend(["-i", host.ssh_key])
|
|
35
|
+
|
|
36
|
+
if host.port != 22:
|
|
37
|
+
ssh_args.extend(["-p", str(host.port)])
|
|
38
|
+
|
|
39
|
+
# Add connection options for non-interactive use
|
|
40
|
+
ssh_args.extend(
|
|
41
|
+
[
|
|
42
|
+
"-o",
|
|
43
|
+
"BatchMode=yes",
|
|
44
|
+
"-o",
|
|
45
|
+
"StrictHostKeyChecking=accept-new",
|
|
46
|
+
"-o",
|
|
47
|
+
"ConnectTimeout=5",
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
ssh_target = f"{host.user}@{host.hostname}"
|
|
52
|
+
ssh_args.append(ssh_target)
|
|
53
|
+
|
|
54
|
+
# Python command to get version
|
|
55
|
+
python_cmd = f"""python3 -c "
|
|
56
|
+
try:
|
|
57
|
+
from importlib.metadata import version
|
|
58
|
+
print(version('{package}'))
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print('ERROR:' + str(e))
|
|
61
|
+
"
|
|
62
|
+
"""
|
|
63
|
+
ssh_args.append(python_cmd)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
ssh_args,
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
timeout=15,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
output = result.stdout.strip()
|
|
74
|
+
|
|
75
|
+
if result.returncode != 0:
|
|
76
|
+
error = result.stderr.strip() or "SSH connection failed"
|
|
77
|
+
return {
|
|
78
|
+
"installed": None,
|
|
79
|
+
"status": "error",
|
|
80
|
+
"error": error,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if output.startswith("ERROR:"):
|
|
84
|
+
return {
|
|
85
|
+
"installed": None,
|
|
86
|
+
"status": "not_installed",
|
|
87
|
+
"error": output[6:],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"installed": output,
|
|
92
|
+
"status": "ok",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
except subprocess.TimeoutExpired:
|
|
96
|
+
return {
|
|
97
|
+
"installed": None,
|
|
98
|
+
"status": "timeout",
|
|
99
|
+
"error": "SSH connection timed out",
|
|
100
|
+
}
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return {
|
|
103
|
+
"installed": None,
|
|
104
|
+
"status": "error",
|
|
105
|
+
"error": str(e),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_remote_versions(
|
|
110
|
+
host: HostConfig,
|
|
111
|
+
packages: list[str],
|
|
112
|
+
) -> dict[str, dict[str, Any]]:
|
|
113
|
+
"""Get versions of multiple packages on a remote host.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
host : HostConfig
|
|
118
|
+
Host configuration.
|
|
119
|
+
packages : list[str]
|
|
120
|
+
List of package names.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
dict
|
|
125
|
+
Package name -> version info mapping.
|
|
126
|
+
"""
|
|
127
|
+
# Build SSH command that checks all packages at once
|
|
128
|
+
ssh_args = ["ssh"]
|
|
129
|
+
|
|
130
|
+
if host.ssh_key:
|
|
131
|
+
ssh_args.extend(["-i", host.ssh_key])
|
|
132
|
+
|
|
133
|
+
if host.port != 22:
|
|
134
|
+
ssh_args.extend(["-p", str(host.port)])
|
|
135
|
+
|
|
136
|
+
ssh_args.extend(
|
|
137
|
+
[
|
|
138
|
+
"-o",
|
|
139
|
+
"BatchMode=yes",
|
|
140
|
+
"-o",
|
|
141
|
+
"StrictHostKeyChecking=accept-new",
|
|
142
|
+
"-o",
|
|
143
|
+
"ConnectTimeout=5",
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
ssh_target = f"{host.user}@{host.hostname}"
|
|
148
|
+
ssh_args.append(ssh_target)
|
|
149
|
+
|
|
150
|
+
# Build Python command to check all packages (installed + toml)
|
|
151
|
+
# Use base64 encoding to avoid shell escaping issues
|
|
152
|
+
import base64
|
|
153
|
+
|
|
154
|
+
packages_list = repr(packages)
|
|
155
|
+
python_script = f"""
|
|
156
|
+
import json
|
|
157
|
+
from importlib.metadata import version
|
|
158
|
+
from pathlib import Path
|
|
159
|
+
import re
|
|
160
|
+
|
|
161
|
+
def get_toml_version(pkg):
|
|
162
|
+
pkg_dir_names = [pkg, pkg.replace("-", "_"), pkg.replace("_", "-")]
|
|
163
|
+
if pkg == "scitex":
|
|
164
|
+
pkg_dir_names.append("scitex-python")
|
|
165
|
+
for dir_name in pkg_dir_names:
|
|
166
|
+
toml_path = Path.home() / "proj" / dir_name / "pyproject.toml"
|
|
167
|
+
if toml_path.exists():
|
|
168
|
+
try:
|
|
169
|
+
content = toml_path.read_text()
|
|
170
|
+
match = re.search(r'^version\\s*=\\s*["\\'](.*?)["\\']\\s*$', content, re.MULTILINE)
|
|
171
|
+
if match:
|
|
172
|
+
return match.group(1)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
results = {{}}
|
|
178
|
+
for pkg in {packages_list}:
|
|
179
|
+
result = {{"installed": None, "toml": None, "status": "not_installed"}}
|
|
180
|
+
try:
|
|
181
|
+
result["installed"] = version(pkg)
|
|
182
|
+
result["status"] = "ok"
|
|
183
|
+
except Exception as e:
|
|
184
|
+
result["error"] = str(e)
|
|
185
|
+
result["toml"] = get_toml_version(pkg)
|
|
186
|
+
results[pkg] = result
|
|
187
|
+
print(json.dumps(results))
|
|
188
|
+
"""
|
|
189
|
+
encoded = base64.b64encode(python_script.encode()).decode()
|
|
190
|
+
python_cmd = (
|
|
191
|
+
f"python3 -c \"import base64;exec(base64.b64decode('{encoded}').decode())\""
|
|
192
|
+
)
|
|
193
|
+
ssh_args.append(python_cmd)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
result = subprocess.run(
|
|
197
|
+
ssh_args,
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
timeout=30,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if result.returncode != 0:
|
|
204
|
+
error = result.stderr.strip() or "SSH connection failed"
|
|
205
|
+
return {
|
|
206
|
+
pkg: {"installed": None, "status": "error", "error": error}
|
|
207
|
+
for pkg in packages
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
import json
|
|
211
|
+
from typing import cast
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
return cast(dict[str, dict[str, Any]], json.loads(result.stdout.strip()))
|
|
215
|
+
except json.JSONDecodeError:
|
|
216
|
+
return {
|
|
217
|
+
pkg: {
|
|
218
|
+
"installed": None,
|
|
219
|
+
"status": "error",
|
|
220
|
+
"error": f"Invalid response: {result.stdout[:100]}",
|
|
221
|
+
}
|
|
222
|
+
for pkg in packages
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
except subprocess.TimeoutExpired:
|
|
226
|
+
return {
|
|
227
|
+
pkg: {"installed": None, "status": "timeout", "error": "SSH timed out"}
|
|
228
|
+
for pkg in packages
|
|
229
|
+
}
|
|
230
|
+
except Exception as e:
|
|
231
|
+
return {
|
|
232
|
+
pkg: {"installed": None, "status": "error", "error": str(e)}
|
|
233
|
+
for pkg in packages
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def check_all_hosts(
|
|
238
|
+
packages: list[str] | None = None,
|
|
239
|
+
hosts: list[str] | None = None,
|
|
240
|
+
config: DevConfig | None = None,
|
|
241
|
+
) -> dict[str, dict[str, dict[str, Any]]]:
|
|
242
|
+
"""Check versions on all enabled hosts.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
packages : list[str] | None
|
|
247
|
+
List of package names. If None, uses ecosystem packages.
|
|
248
|
+
hosts : list[str] | None
|
|
249
|
+
List of host names to check. If None, checks all enabled hosts.
|
|
250
|
+
config : DevConfig | None
|
|
251
|
+
Configuration to use. If None, loads default config.
|
|
252
|
+
|
|
253
|
+
Returns
|
|
254
|
+
-------
|
|
255
|
+
dict
|
|
256
|
+
Mapping: host_name -> package_name -> version_info
|
|
257
|
+
"""
|
|
258
|
+
if config is None:
|
|
259
|
+
config = load_config()
|
|
260
|
+
|
|
261
|
+
if packages is None:
|
|
262
|
+
from ._ecosystem import get_all_packages
|
|
263
|
+
|
|
264
|
+
packages = get_all_packages()
|
|
265
|
+
|
|
266
|
+
# Get pypi names for packages
|
|
267
|
+
from ._ecosystem import ECOSYSTEM
|
|
268
|
+
|
|
269
|
+
pypi_names = []
|
|
270
|
+
name_map = {} # pypi_name -> package_name
|
|
271
|
+
for pkg in packages:
|
|
272
|
+
if pkg in ECOSYSTEM:
|
|
273
|
+
pypi_name = ECOSYSTEM[pkg].get("pypi_name", pkg)
|
|
274
|
+
else:
|
|
275
|
+
pypi_name = pkg
|
|
276
|
+
pypi_names.append(pypi_name)
|
|
277
|
+
name_map[pypi_name] = pkg
|
|
278
|
+
|
|
279
|
+
# Get enabled hosts
|
|
280
|
+
enabled_hosts = get_enabled_hosts(config)
|
|
281
|
+
if hosts:
|
|
282
|
+
enabled_hosts = [h for h in enabled_hosts if h.name in hosts]
|
|
283
|
+
|
|
284
|
+
results: dict[str, dict[str, dict[str, Any]]] = {}
|
|
285
|
+
|
|
286
|
+
for host in enabled_hosts:
|
|
287
|
+
host_versions = get_remote_versions(host, pypi_names)
|
|
288
|
+
# Map back to package names
|
|
289
|
+
results[host.name] = {
|
|
290
|
+
name_map.get(pypi, pypi): info for pypi, info in host_versions.items()
|
|
291
|
+
}
|
|
292
|
+
# Add host metadata
|
|
293
|
+
results[host.name]["_host"] = {
|
|
294
|
+
"hostname": host.hostname,
|
|
295
|
+
"role": host.role,
|
|
296
|
+
"user": host.user,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return results
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_host_connection(host: HostConfig) -> dict[str, Any]:
|
|
303
|
+
"""Test SSH connection to a host.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
host : HostConfig
|
|
308
|
+
Host to test.
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
dict
|
|
313
|
+
Connection status with keys: connected, error, python_version.
|
|
314
|
+
"""
|
|
315
|
+
ssh_args = ["ssh"]
|
|
316
|
+
|
|
317
|
+
if host.ssh_key:
|
|
318
|
+
ssh_args.extend(["-i", host.ssh_key])
|
|
319
|
+
|
|
320
|
+
if host.port != 22:
|
|
321
|
+
ssh_args.extend(["-p", str(host.port)])
|
|
322
|
+
|
|
323
|
+
ssh_args.extend(
|
|
324
|
+
[
|
|
325
|
+
"-o",
|
|
326
|
+
"BatchMode=yes",
|
|
327
|
+
"-o",
|
|
328
|
+
"StrictHostKeyChecking=accept-new",
|
|
329
|
+
"-o",
|
|
330
|
+
"ConnectTimeout=5",
|
|
331
|
+
]
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
ssh_target = f"{host.user}@{host.hostname}"
|
|
335
|
+
ssh_args.append(ssh_target)
|
|
336
|
+
ssh_args.append("python3 --version")
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
result = subprocess.run(
|
|
340
|
+
ssh_args,
|
|
341
|
+
capture_output=True,
|
|
342
|
+
text=True,
|
|
343
|
+
timeout=10,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if result.returncode == 0:
|
|
347
|
+
return {
|
|
348
|
+
"connected": True,
|
|
349
|
+
"python_version": result.stdout.strip(),
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
"connected": False,
|
|
353
|
+
"error": result.stderr.strip() or "Connection failed",
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
except subprocess.TimeoutExpired:
|
|
357
|
+
return {"connected": False, "error": "Connection timed out"}
|
|
358
|
+
except Exception as e:
|
|
359
|
+
return {"connected": False, "error": str(e)}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# EOF
|
scitex/_dev/_versions.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2026-02-02
|
|
3
|
+
# File: scitex/_dev/_versions.py
|
|
4
|
+
|
|
5
|
+
"""Core version checking logic for the scitex ecosystem."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ._ecosystem import ECOSYSTEM, get_all_packages, get_local_path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_version_from_toml(path: Path) -> str | None:
|
|
18
|
+
"""Read version from pyproject.toml."""
|
|
19
|
+
toml_path = path / "pyproject.toml"
|
|
20
|
+
if not toml_path.exists():
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# Python 3.11+
|
|
25
|
+
import tomllib
|
|
26
|
+
|
|
27
|
+
with open(toml_path, "rb") as f:
|
|
28
|
+
data = tomllib.load(f)
|
|
29
|
+
except ImportError:
|
|
30
|
+
try:
|
|
31
|
+
import tomli
|
|
32
|
+
|
|
33
|
+
with open(toml_path, "rb") as f:
|
|
34
|
+
data = tomli.load(f)
|
|
35
|
+
except ImportError:
|
|
36
|
+
# Fallback: regex parse
|
|
37
|
+
content = toml_path.read_text()
|
|
38
|
+
match = re.search(
|
|
39
|
+
r'^version\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE
|
|
40
|
+
)
|
|
41
|
+
return match.group(1) if match else None
|
|
42
|
+
|
|
43
|
+
return data.get("project", {}).get("version")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_version_installed(package: str) -> str | None:
|
|
47
|
+
"""Get version from importlib.metadata."""
|
|
48
|
+
try:
|
|
49
|
+
from importlib.metadata import version
|
|
50
|
+
|
|
51
|
+
return version(package)
|
|
52
|
+
except Exception:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_git_latest_tag(path: Path) -> str | None:
|
|
57
|
+
"""Get latest git tag (version tags only)."""
|
|
58
|
+
if not path.exists():
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
["git", "describe", "--tags", "--abbrev=0", "--match", "v*"],
|
|
64
|
+
cwd=path,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
timeout=5,
|
|
68
|
+
)
|
|
69
|
+
if result.returncode == 0:
|
|
70
|
+
return result.stdout.strip()
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
# Fallback: list all tags
|
|
75
|
+
try:
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["git", "tag", "-l", "v*", "--sort=-v:refname"],
|
|
78
|
+
cwd=path,
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
timeout=5,
|
|
82
|
+
)
|
|
83
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
84
|
+
return result.stdout.strip().split("\n")[0]
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_git_branch(path: Path) -> str | None:
|
|
92
|
+
"""Get current git branch."""
|
|
93
|
+
if not path.exists():
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
99
|
+
cwd=path,
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
timeout=5,
|
|
103
|
+
)
|
|
104
|
+
if result.returncode == 0:
|
|
105
|
+
return result.stdout.strip()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_pypi_version(package: str) -> str | None:
|
|
113
|
+
"""Fetch latest version from PyPI API."""
|
|
114
|
+
try:
|
|
115
|
+
import urllib.request
|
|
116
|
+
|
|
117
|
+
url = f"https://pypi.org/pypi/{package}/json"
|
|
118
|
+
with urllib.request.urlopen(url, timeout=5) as response:
|
|
119
|
+
import json
|
|
120
|
+
|
|
121
|
+
data = json.loads(response.read().decode())
|
|
122
|
+
return data.get("info", {}).get("version")
|
|
123
|
+
except Exception:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _normalize_version(v: str | None) -> str | None:
|
|
128
|
+
"""Normalize version string (strip v prefix)."""
|
|
129
|
+
if v is None:
|
|
130
|
+
return None
|
|
131
|
+
return v.lstrip("v")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _compare_versions(v1: str | None, v2: str | None) -> int:
|
|
135
|
+
"""Compare two version strings. Returns -1, 0, or 1."""
|
|
136
|
+
if v1 is None or v2 is None:
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
from packaging.version import Version
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
ver1 = Version(_normalize_version(v1))
|
|
143
|
+
ver2 = Version(_normalize_version(v2))
|
|
144
|
+
if ver1 < ver2:
|
|
145
|
+
return -1
|
|
146
|
+
if ver1 > ver2:
|
|
147
|
+
return 1
|
|
148
|
+
return 0
|
|
149
|
+
except Exception:
|
|
150
|
+
# Fallback: string comparison
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _determine_status(info: dict[str, Any]) -> tuple[str, list[str]]:
|
|
155
|
+
"""Determine version status and issues."""
|
|
156
|
+
issues = []
|
|
157
|
+
|
|
158
|
+
toml_ver = info.get("local", {}).get("pyproject_toml")
|
|
159
|
+
installed_ver = info.get("local", {}).get("installed")
|
|
160
|
+
tag_ver = _normalize_version(info.get("git", {}).get("latest_tag"))
|
|
161
|
+
pypi_ver = info.get("remote", {}).get("pypi")
|
|
162
|
+
|
|
163
|
+
# Check local consistency
|
|
164
|
+
if toml_ver and installed_ver and toml_ver != installed_ver:
|
|
165
|
+
issues.append(f"pyproject.toml ({toml_ver}) != installed ({installed_ver})")
|
|
166
|
+
|
|
167
|
+
# Check if toml matches tag
|
|
168
|
+
if toml_ver and tag_ver and toml_ver != tag_ver:
|
|
169
|
+
issues.append(f"pyproject.toml ({toml_ver}) != git tag ({tag_ver})")
|
|
170
|
+
|
|
171
|
+
# Check pypi status
|
|
172
|
+
if toml_ver and pypi_ver:
|
|
173
|
+
cmp = _compare_versions(toml_ver, pypi_ver)
|
|
174
|
+
if cmp > 0:
|
|
175
|
+
issues.append(f"local ({toml_ver}) > pypi ({pypi_ver}) - ready to release")
|
|
176
|
+
return "unreleased", issues
|
|
177
|
+
if cmp < 0:
|
|
178
|
+
issues.append(f"local ({toml_ver}) < pypi ({pypi_ver}) - outdated")
|
|
179
|
+
return "outdated", issues
|
|
180
|
+
|
|
181
|
+
if issues:
|
|
182
|
+
return "mismatch", issues
|
|
183
|
+
|
|
184
|
+
if not toml_ver:
|
|
185
|
+
return "unavailable", ["package not found locally"]
|
|
186
|
+
|
|
187
|
+
return "ok", []
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def list_versions(packages: list[str] | None = None) -> dict[str, Any]:
|
|
191
|
+
"""List versions for all ecosystem packages.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
packages : list[str] | None
|
|
196
|
+
List of package names to check. If None, checks all ecosystem packages.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
dict
|
|
201
|
+
Version information for each package.
|
|
202
|
+
"""
|
|
203
|
+
if packages is None:
|
|
204
|
+
packages = get_all_packages()
|
|
205
|
+
|
|
206
|
+
result = {}
|
|
207
|
+
for pkg in packages:
|
|
208
|
+
if pkg not in ECOSYSTEM:
|
|
209
|
+
result[pkg] = {"status": "unknown", "issues": [f"'{pkg}' not in ecosystem"]}
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
info: dict[str, Any] = {"local": {}, "git": {}, "remote": {}}
|
|
213
|
+
local_path = get_local_path(pkg)
|
|
214
|
+
pypi_name = ECOSYSTEM[pkg].get("pypi_name", pkg)
|
|
215
|
+
|
|
216
|
+
# Local sources
|
|
217
|
+
if local_path and local_path.exists():
|
|
218
|
+
info["local"]["pyproject_toml"] = get_version_from_toml(local_path)
|
|
219
|
+
info["local"]["installed"] = get_version_installed(pypi_name)
|
|
220
|
+
|
|
221
|
+
# Git sources
|
|
222
|
+
if local_path and local_path.exists():
|
|
223
|
+
info["git"]["latest_tag"] = get_git_latest_tag(local_path)
|
|
224
|
+
info["git"]["branch"] = get_git_branch(local_path)
|
|
225
|
+
|
|
226
|
+
# Remote sources
|
|
227
|
+
info["remote"]["pypi"] = get_pypi_version(pypi_name)
|
|
228
|
+
|
|
229
|
+
# Determine status
|
|
230
|
+
status, issues = _determine_status(info)
|
|
231
|
+
info["status"] = status
|
|
232
|
+
info["issues"] = issues
|
|
233
|
+
|
|
234
|
+
result[pkg] = info
|
|
235
|
+
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def check_versions(packages: list[str] | None = None) -> dict[str, Any]:
|
|
240
|
+
"""Check version consistency and return detailed status.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
packages : list[str] | None
|
|
245
|
+
List of package names to check. If None, checks all ecosystem packages.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
dict
|
|
250
|
+
Detailed version check results with overall summary.
|
|
251
|
+
"""
|
|
252
|
+
versions = list_versions(packages)
|
|
253
|
+
|
|
254
|
+
summary = {
|
|
255
|
+
"total": len(versions),
|
|
256
|
+
"ok": 0,
|
|
257
|
+
"mismatch": 0,
|
|
258
|
+
"unreleased": 0,
|
|
259
|
+
"outdated": 0,
|
|
260
|
+
"unavailable": 0,
|
|
261
|
+
"unknown": 0,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for _pkg, info in versions.items():
|
|
265
|
+
status = info.get("status", "unknown")
|
|
266
|
+
if status in summary:
|
|
267
|
+
summary[status] += 1
|
|
268
|
+
|
|
269
|
+
return {"packages": versions, "summary": summary}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# EOF
|
scitex/_mcp_tools/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .audio import register_audio_tools
|
|
|
9
9
|
from .canvas import register_canvas_tools
|
|
10
10
|
from .capture import register_capture_tools
|
|
11
11
|
from .dataset import register_dataset_tools
|
|
12
|
+
from .dev import register_dev_tools
|
|
12
13
|
from .diagram import register_diagram_tools
|
|
13
14
|
from .introspect import register_introspect_tools
|
|
14
15
|
from .plt import register_plt_tools
|
|
@@ -29,6 +30,7 @@ def register_all_tools(mcp) -> None:
|
|
|
29
30
|
register_canvas_tools(mcp)
|
|
30
31
|
register_capture_tools(mcp)
|
|
31
32
|
register_dataset_tools(mcp)
|
|
33
|
+
register_dev_tools(mcp)
|
|
32
34
|
register_diagram_tools(mcp)
|
|
33
35
|
register_introspect_tools(mcp)
|
|
34
36
|
register_plt_tools(mcp)
|