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.
Files changed (46) hide show
  1. scitex/_dev/__init__.py +122 -0
  2. scitex/_dev/_config.py +391 -0
  3. scitex/_dev/_dashboard/__init__.py +11 -0
  4. scitex/_dev/_dashboard/_app.py +89 -0
  5. scitex/_dev/_dashboard/_routes.py +182 -0
  6. scitex/_dev/_dashboard/_scripts.py +422 -0
  7. scitex/_dev/_dashboard/_styles.py +295 -0
  8. scitex/_dev/_dashboard/_templates.py +130 -0
  9. scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
  10. scitex/_dev/_ecosystem.py +109 -0
  11. scitex/_dev/_github.py +360 -0
  12. scitex/_dev/_mcp/__init__.py +11 -0
  13. scitex/_dev/_mcp/handlers.py +182 -0
  14. scitex/_dev/_rtd.py +122 -0
  15. scitex/_dev/_ssh.py +362 -0
  16. scitex/_dev/_versions.py +272 -0
  17. scitex/_mcp_tools/__init__.py +2 -0
  18. scitex/_mcp_tools/dev.py +186 -0
  19. scitex/audio/_audio_check.py +84 -41
  20. scitex/cli/capture.py +45 -22
  21. scitex/cli/dev.py +494 -0
  22. scitex/cli/main.py +2 -0
  23. scitex/cli/stats.py +48 -20
  24. scitex/cli/verify.py +33 -36
  25. scitex/plt/__init__.py +16 -6
  26. scitex/scholar/_mcp/crossref_handlers.py +45 -7
  27. scitex/scholar/_mcp/openalex_handlers.py +45 -7
  28. scitex/scholar/config/default.yaml +2 -0
  29. scitex/scholar/local_dbs/__init__.py +5 -1
  30. scitex/scholar/local_dbs/export.py +93 -0
  31. scitex/scholar/local_dbs/unified.py +505 -0
  32. scitex/scholar/metadata_engines/ScholarEngine.py +11 -0
  33. scitex/scholar/metadata_engines/individual/OpenAlexLocalEngine.py +346 -0
  34. scitex/scholar/metadata_engines/individual/__init__.py +1 -0
  35. scitex/template/__init__.py +18 -1
  36. scitex/template/clone_research_minimal.py +111 -0
  37. scitex/verify/README.md +0 -12
  38. scitex/verify/__init__.py +0 -4
  39. scitex/verify/_visualize.py +0 -4
  40. scitex/verify/_viz/__init__.py +0 -18
  41. {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/METADATA +2 -1
  42. {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/RECORD +45 -24
  43. scitex/verify/_viz/_plotly.py +0 -193
  44. {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/WHEEL +0 -0
  45. {scitex-2.17.0.dist-info → scitex-2.17.4.dist-info}/entry_points.txt +0 -0
  46. {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
@@ -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
@@ -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)