scitex 2.17.3__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 (57) hide show
  1. scitex/_dev/_dashboard/_routes.py +13 -0
  2. scitex/_dev/_dashboard/_scripts.py +144 -23
  3. scitex/_dev/_dashboard/_styles.py +90 -0
  4. scitex/_dev/_dashboard/_templates.py +14 -1
  5. scitex/_dev/_rtd.py +122 -0
  6. scitex/_dev/_ssh.py +38 -8
  7. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +90 -0
  8. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +1571 -0
  9. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +6262 -0
  10. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +1274 -0
  11. scitex/dev/plt/data/mpl/dir_ax.txt +459 -0
  12. scitex/scholar/_mcp/crossref_handlers.py +45 -7
  13. scitex/scholar/_mcp/openalex_handlers.py +45 -7
  14. scitex/scholar/config/default.yaml +2 -0
  15. scitex/scholar/data/.gitkeep +0 -0
  16. scitex/scholar/data/README.md +44 -0
  17. scitex/scholar/data/bib_files/bibliography.bib +1952 -0
  18. scitex/scholar/data/bib_files/neurovista.bib +277 -0
  19. scitex/scholar/data/bib_files/neurovista_enriched.bib +441 -0
  20. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +441 -0
  21. scitex/scholar/data/bib_files/neurovista_processed.bib +338 -0
  22. scitex/scholar/data/bib_files/openaccess.bib +89 -0
  23. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +2178 -0
  24. scitex/scholar/data/bib_files/pac.bib +698 -0
  25. scitex/scholar/data/bib_files/pac_enriched.bib +1061 -0
  26. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  27. scitex/scholar/data/bib_files/pac_titles.txt +75 -0
  28. scitex/scholar/data/bib_files/paywalled.bib +98 -0
  29. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +58 -0
  30. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +87 -0
  31. scitex/scholar/data/bib_files/seizure_prediction.bib +694 -0
  32. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  33. scitex/scholar/data/bib_files/test_complete_enriched.bib +437 -0
  34. scitex/scholar/data/bib_files/test_final_enriched.bib +437 -0
  35. scitex/scholar/data/bib_files/test_seizure.bib +46 -0
  36. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  37. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  38. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  39. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  40. scitex/scholar/data/impact_factor.db +0 -0
  41. scitex/scholar/local_dbs/__init__.py +5 -1
  42. scitex/scholar/local_dbs/export.py +93 -0
  43. scitex/scholar/local_dbs/unified.py +505 -0
  44. scitex/scholar/metadata_engines/ScholarEngine.py +11 -0
  45. scitex/scholar/metadata_engines/individual/OpenAlexLocalEngine.py +346 -0
  46. scitex/scholar/metadata_engines/individual/__init__.py +1 -0
  47. {scitex-2.17.3.dist-info → scitex-2.17.4.dist-info}/METADATA +1 -1
  48. {scitex-2.17.3.dist-info → scitex-2.17.4.dist-info}/RECORD +51 -22
  49. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +0 -462
  50. scitex/scholar/url_finder/.tmp/open_url/README.md +0 -223
  51. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +0 -694
  52. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +0 -1160
  53. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +0 -344
  54. scitex/scholar/url_finder/.tmp/open_url/__init__.py +0 -24
  55. {scitex-2.17.3.dist-info → scitex-2.17.4.dist-info}/WHEEL +0 -0
  56. {scitex-2.17.3.dist-info → scitex-2.17.4.dist-info}/entry_points.txt +0 -0
  57. {scitex-2.17.3.dist-info → scitex-2.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -117,6 +117,19 @@ def register_routes(app: Flask) -> None:
117
117
  except Exception as e:
118
118
  return jsonify({"error": str(e)}), 500
119
119
 
120
+ @app.route("/api/rtd")
121
+ def api_rtd():
122
+ """Get Read the Docs build status."""
123
+ try:
124
+ packages = request.args.getlist("package") or None
125
+ versions = request.args.getlist("version") or None
126
+ from .._rtd import check_all_rtd
127
+
128
+ data = check_all_rtd(packages=packages, versions=versions)
129
+ return jsonify(data)
130
+ except Exception as e:
131
+ return jsonify({"error": str(e)}), 500
132
+
120
133
 
121
134
  def _get_all_version_data(force_refresh: bool = False) -> dict[str, Any]:
122
135
  """Get all version data from all sources.
@@ -8,19 +8,20 @@
8
8
  def get_javascript() -> str:
9
9
  """Return dashboard JavaScript."""
10
10
  return """
11
- let cachedData = { packages: {}, hosts: {}, remotes: {} };
11
+ let cachedData = { packages: {}, hosts: {}, remotes: {}, rtd: {} };
12
12
 
13
13
  async function fetchVersions() {
14
14
  showLoading(true);
15
- cachedData = { packages: {}, hosts: {}, remotes: {} };
15
+ cachedData = { packages: {}, hosts: {}, remotes: {}, rtd: {} };
16
16
  renderFilters();
17
17
  renderData();
18
18
 
19
19
  // Load packages first (fast)
20
20
  fetchPackages();
21
- // Load hosts and remotes in parallel (slower)
21
+ // Load hosts, remotes, and RTD in parallel (slower)
22
22
  fetchHosts();
23
23
  fetchRemotes();
24
+ fetchRtd();
24
25
  }
25
26
 
26
27
  async function fetchPackages() {
@@ -72,6 +73,22 @@ async function fetchRemotes() {
72
73
  }
73
74
  }
74
75
 
76
+ async function fetchRtd() {
77
+ setSectionLoading('rtd', true);
78
+ try {
79
+ const response = await fetch('/api/rtd');
80
+ cachedData.rtd = await response.json();
81
+ renderFilters();
82
+ renderData();
83
+ setSectionUpdated('rtd');
84
+ } catch (err) {
85
+ console.error('Failed to fetch RTD status:', err);
86
+ cachedData.rtd = { error: err.message };
87
+ } finally {
88
+ setSectionLoading('rtd', false);
89
+ }
90
+ }
91
+
75
92
  function setSectionLoading(section, loading) {
76
93
  const el = document.getElementById(section + 'Filters');
77
94
  if (el) {
@@ -167,6 +184,16 @@ function renderFilters() {
167
184
  remoteFilters.innerHTML = '<span style="color: var(--text-secondary)">No remotes configured</span>';
168
185
  }
169
186
 
187
+ const rtdFilters = document.getElementById('rtdFilters');
188
+ const rtdVersions = Object.keys(cachedData.rtd || {});
189
+ if (rtdVersions.length > 0) {
190
+ rtdFilters.innerHTML = rtdVersions.map(v =>
191
+ `<label><input type="checkbox" value="${v}" checked onchange="renderData()"> ${v}</label>`
192
+ ).join('');
193
+ } else {
194
+ rtdFilters.innerHTML = '<span style="color: var(--text-secondary)">Loading...</span>';
195
+ }
196
+
170
197
  document.querySelectorAll('#statusFilters input').forEach(input => {
171
198
  input.onchange = renderData;
172
199
  });
@@ -183,6 +210,22 @@ function getSelectedFilters() {
183
210
  };
184
211
  }
185
212
 
213
+ function getEffectiveStatus(name, info) {
214
+ // Check RTD status and downgrade if needed
215
+ const rtdData = cachedData.rtd || {};
216
+ let status = info.status;
217
+ if (status === 'ok') {
218
+ const rtdLatest = rtdData['latest'] && rtdData['latest'][name];
219
+ const rtdStable = rtdData['stable'] && rtdData['stable'][name];
220
+ if ((rtdLatest && rtdLatest.status === 'failing') ||
221
+ (rtdStable && rtdStable.status === 'failing') ||
222
+ (rtdLatest && rtdLatest.status === 'not_found')) {
223
+ status = 'mismatch';
224
+ }
225
+ }
226
+ return status;
227
+ }
228
+
186
229
  function renderData() {
187
230
  if (!cachedData) return;
188
231
  const filters = getSelectedFilters();
@@ -191,16 +234,17 @@ function renderData() {
191
234
  const filteredPackages = Object.entries(packages)
192
235
  .filter(([name, info]) => {
193
236
  if (!filters.packages.includes(name)) return false;
194
- if (!filters.statuses.includes(info.status)) return false;
237
+ const effectiveStatus = getEffectiveStatus(name, info);
238
+ if (!filters.statuses.includes(effectiveStatus)) return false;
195
239
  return true;
196
240
  });
197
241
 
198
242
  const summary = {
199
243
  total: filteredPackages.length,
200
- ok: filteredPackages.filter(([, i]) => i.status === 'ok').length,
201
- unreleased: filteredPackages.filter(([, i]) => i.status === 'unreleased').length,
202
- mismatch: filteredPackages.filter(([, i]) => i.status === 'mismatch').length,
203
- outdated: filteredPackages.filter(([, i]) => i.status === 'outdated').length
244
+ ok: filteredPackages.filter(([n, i]) => getEffectiveStatus(n, i) === 'ok').length,
245
+ unreleased: filteredPackages.filter(([n, i]) => getEffectiveStatus(n, i) === 'unreleased').length,
246
+ mismatch: filteredPackages.filter(([n, i]) => getEffectiveStatus(n, i) === 'mismatch').length,
247
+ outdated: filteredPackages.filter(([n, i]) => getEffectiveStatus(n, i) === 'outdated').length
204
248
  };
205
249
 
206
250
  document.getElementById('summary').innerHTML = `
@@ -216,6 +260,7 @@ function renderData() {
216
260
  const remote = info.remote || {};
217
261
  const hostData = cachedData.hosts || {};
218
262
  const remoteData = cachedData.remotes || {};
263
+ const rtdData = cachedData.rtd || {};
219
264
 
220
265
  const hostVersions = Object.entries(hostData)
221
266
  .filter(([h]) => !h.startsWith('_') && filters.hosts.includes(h))
@@ -225,55 +270,111 @@ function renderData() {
225
270
  .filter(([r]) => !r.startsWith('_') && filters.remotes.includes(r))
226
271
  .map(([remoteName, remoteInfo]) => ({ name: remoteName, ...(remoteInfo[name] || {}) }));
227
272
 
228
- return renderPackageCard(name, info, local, git, remote, hostVersions, remoteVersions);
273
+ // Get RTD status for this package (latest and stable)
274
+ const rtdStatus = {};
275
+ Object.entries(rtdData).forEach(([version, pkgData]) => {
276
+ if (pkgData[name]) {
277
+ rtdStatus[version] = pkgData[name];
278
+ }
279
+ });
280
+
281
+ return renderPackageCard(name, info, local, git, remote, hostVersions, remoteVersions, rtdStatus);
229
282
  }).join('');
230
283
  }
231
284
 
232
- function renderPackageCard(name, info, local, git, remote, hostVersions, remoteVersions) {
285
+ function renderPackageCard(name, info, local, git, remote, hostVersions, remoteVersions, rtdStatus) {
286
+ const pypiUrl = `https://pypi.org/project/${name}/`;
287
+ const githubUrl = `https://github.com/ywatanabe1989/${name}`;
288
+ const rtdUrl = `https://${name === 'scitex' ? 'scitex-python' : name}.readthedocs.io/`;
289
+
290
+ // Collect all issues for tooltip
291
+ let allIssues = [...(info.issues || [])];
292
+
293
+ // Re-evaluate status based on RTD
294
+ let effectiveStatus = info.status;
295
+ if (rtdStatus && Object.keys(rtdStatus).length > 0) {
296
+ const rtdLatest = rtdStatus['latest'];
297
+ const rtdStable = rtdStatus['stable'];
298
+ if (rtdLatest && rtdLatest.status === 'failing') {
299
+ allIssues.push('RTD latest build failing');
300
+ if (effectiveStatus === 'ok') effectiveStatus = 'mismatch';
301
+ }
302
+ if (rtdStable && rtdStable.status === 'failing') {
303
+ allIssues.push('RTD stable build failing');
304
+ if (effectiveStatus === 'ok') effectiveStatus = 'mismatch';
305
+ }
306
+ if (rtdLatest && rtdLatest.status === 'not_found') {
307
+ allIssues.push('RTD project not found');
308
+ if (effectiveStatus === 'ok') effectiveStatus = 'mismatch';
309
+ }
310
+ }
311
+
312
+ // Create tooltip text from issues (using &#10; for newlines in title attribute)
313
+ const tooltipText = allIssues.length > 0 ? allIssues.join('&#10;') : '';
314
+ const tooltipAttr = tooltipText ? `title="${tooltipText}"` : '';
315
+
233
316
  let html = `
234
- <div class="package-card">
235
- <div class="package-header">
236
- <span class="package-name">${name}</span>
237
- <span class="status-badge status-${info.status}">${info.status}</span>
317
+ <div class="package-card collapsed">
318
+ <div class="package-header" onclick="toggleCard(this)">
319
+ <span class="fold-icon">▶</span>
320
+ <a href="${githubUrl}" target="_blank" class="package-name" onclick="event.stopPropagation()">${name}</a>
321
+ <span class="status-badge status-${effectiveStatus}" ${tooltipAttr}>${effectiveStatus}</span>
322
+ <span class="quick-links">
323
+ <a href="${pypiUrl}" target="_blank" title="PyPI" onclick="event.stopPropagation()">📦</a>
324
+ <a href="${githubUrl}" target="_blank" title="GitHub" onclick="event.stopPropagation()">🐙</a>
325
+ <a href="${rtdUrl}" target="_blank" title="Docs" onclick="event.stopPropagation()">📖</a>
326
+ </span>
238
327
  </div>
239
328
  <div class="package-body">
240
329
  <div class="version-grid">
241
330
  <div class="version-section">
242
- <h4>Local</h4>
331
+ <h4>LOCAL</h4>
243
332
  <div class="version-item"><span class="key">toml</span><span class="value">${local.pyproject_toml || '-'}</span></div>
244
333
  <div class="version-item"><span class="key">installed</span><span class="value">${local.installed || '-'}</span></div>
245
334
  </div>
246
335
  <div class="version-section">
247
- <h4>Git</h4>
336
+ <h4>GIT</h4>
248
337
  <div class="version-item"><span class="key">tag</span><span class="value">${git.latest_tag || '-'}</span></div>
249
338
  <div class="version-item"><span class="key">branch</span><span class="value">${git.branch || '-'}</span></div>
250
339
  </div>
251
340
  <div class="version-section">
252
- <h4>PyPI</h4>
341
+ <h4><a href="https://pypi.org/project/${name}/" target="_blank">PYPI</a></h4>
253
342
  <div class="version-item"><span class="key">published</span><span class="value">${remote.pypi || '-'}</span></div>
254
343
  </div>`;
255
344
 
256
345
  if (hostVersions.length > 0) {
257
- html += `<div class="version-section"><h4>Hosts</h4>`;
258
346
  hostVersions.forEach(h => {
259
- html += `<div class="version-item"><span class="key">${h.name}</span><span class="value">${h.installed || h.error || '-'}</span></div>`;
347
+ html += `<div class="version-section"><h4>${h.name.toUpperCase()}</h4>`;
348
+ html += `<div class="version-item"><span class="key">toml</span><span class="value">${h.toml || '-'}</span></div>`;
349
+ html += `<div class="version-item"><span class="key">installed</span><span class="value">${h.installed || h.error || '-'}</span></div>`;
350
+ html += `</div>`;
260
351
  });
261
- html += `</div>`;
262
352
  }
263
353
 
264
354
  if (remoteVersions.length > 0) {
265
- html += `<div class="version-section"><h4>GitHub</h4>`;
355
+ html += `<div class="version-section"><h4><a href="${githubUrl}" target="_blank">GITHUB</a></h4>`;
266
356
  remoteVersions.forEach(r => {
267
357
  html += `<div class="version-item"><span class="key">${r.name}</span><span class="value">${r.latest_tag || r.error || '-'}</span></div>`;
268
358
  });
269
359
  html += `</div>`;
270
360
  }
271
361
 
362
+ if (rtdStatus && Object.keys(rtdStatus).length > 0) {
363
+ html += `<div class="version-section"><h4><a href="${rtdUrl}" target="_blank">RTD</a></h4>`;
364
+ Object.entries(rtdStatus).forEach(([version, data]) => {
365
+ const statusClass = data.status === 'passing' ? 'rtd-passing' : (data.status === 'failing' ? 'rtd-failing' : 'rtd-unknown');
366
+ const statusIcon = data.status === 'passing' ? '✓' : (data.status === 'failing' ? '✗' : '?');
367
+ const link = data.url ? `<a href="${data.url}" target="_blank">${statusIcon}</a>` : statusIcon;
368
+ html += `<div class="version-item"><span class="key">${version}</span><span class="value ${statusClass}">${link} ${data.status || '-'}</span></div>`;
369
+ });
370
+ html += `</div>`;
371
+ }
372
+
272
373
  html += `</div>`;
273
374
 
274
- if (info.issues && info.issues.length > 0) {
375
+ if (allIssues.length > 0) {
275
376
  html += `<div class="issues"><h4>Issues</h4><ul>`;
276
- info.issues.forEach(i => { html += `<li>${i}</li>`; });
377
+ allIssues.forEach(i => { html += `<li>${i}</li>`; });
277
378
  html += `</ul></div>`;
278
379
  }
279
380
 
@@ -283,6 +384,26 @@ function renderPackageCard(name, info, local, git, remote, hostVersions, remoteV
283
384
 
284
385
  async function refreshData() { await fetchVersions(); }
285
386
 
387
+ function toggleCard(header) {
388
+ const card = header.parentElement;
389
+ card.classList.toggle('collapsed');
390
+ }
391
+
392
+ function toggleAllCards(expand) {
393
+ document.querySelectorAll('.package-card').forEach(card => {
394
+ if (expand) {
395
+ card.classList.remove('collapsed');
396
+ } else {
397
+ card.classList.add('collapsed');
398
+ }
399
+ });
400
+ }
401
+
402
+ function toggleFilters() {
403
+ const filters = document.querySelector('.filters');
404
+ filters.classList.toggle('collapsed');
405
+ }
406
+
286
407
  function exportJSON() {
287
408
  if (!cachedData) return;
288
409
  const blob = new Blob([JSON.stringify(cachedData, null, 2)], { type: 'application/json' });
@@ -199,6 +199,96 @@ button {
199
199
  0% { background: rgba(74, 222, 128, 0.3); }
200
200
  100% { background: transparent; }
201
201
  }
202
+ .rtd-passing { color: var(--success); }
203
+ .rtd-failing { color: var(--error); }
204
+ .rtd-unknown { color: var(--warning); }
205
+ .rtd-passing a, .rtd-failing a, .rtd-unknown a {
206
+ color: inherit;
207
+ text-decoration: none;
208
+ }
209
+ .rtd-passing a:hover, .rtd-failing a:hover, .rtd-unknown a:hover {
210
+ text-decoration: underline;
211
+ }
212
+ /* Collapsible cards */
213
+ .package-card .package-header {
214
+ cursor: pointer;
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 10px;
218
+ }
219
+ .package-card .fold-icon {
220
+ transition: transform 0.2s ease;
221
+ font-size: 0.8rem;
222
+ color: var(--text-secondary);
223
+ }
224
+ .package-card:not(.collapsed) .fold-icon {
225
+ transform: rotate(90deg);
226
+ }
227
+ .package-card.collapsed .package-body {
228
+ display: none;
229
+ }
230
+ .package-card .package-name {
231
+ color: var(--accent);
232
+ text-decoration: none;
233
+ font-weight: bold;
234
+ }
235
+ .package-card .package-name:hover {
236
+ text-decoration: underline;
237
+ }
238
+ .package-card .quick-links {
239
+ margin-left: auto;
240
+ display: flex;
241
+ gap: 8px;
242
+ font-size: 1rem;
243
+ }
244
+ .package-card .quick-links a {
245
+ text-decoration: none;
246
+ opacity: 0.7;
247
+ transition: opacity 0.2s;
248
+ }
249
+ .package-card .quick-links a:hover {
250
+ opacity: 1;
251
+ }
252
+ /* Collapsible filters */
253
+ .filters.collapsed .filter-group {
254
+ display: none;
255
+ }
256
+ .filters-toggle {
257
+ cursor: pointer;
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 8px;
261
+ padding: 10px;
262
+ margin: -20px -20px 15px -20px;
263
+ background: var(--bg-card);
264
+ border-radius: 10px 10px 0 0;
265
+ color: var(--text-secondary);
266
+ font-size: 0.9rem;
267
+ }
268
+ .filters-toggle .fold-icon {
269
+ transition: transform 0.2s ease;
270
+ }
271
+ .filters:not(.collapsed) .filters-toggle .fold-icon {
272
+ transform: rotate(90deg);
273
+ }
274
+ .expand-controls {
275
+ display: flex;
276
+ gap: 10px;
277
+ margin-bottom: 15px;
278
+ }
279
+ .expand-controls button {
280
+ padding: 5px 12px;
281
+ font-size: 0.8rem;
282
+ }
283
+ /* Section header links */
284
+ .version-section h4 a {
285
+ color: var(--text-secondary);
286
+ text-decoration: none;
287
+ }
288
+ .version-section h4 a:hover {
289
+ color: var(--accent);
290
+ text-decoration: underline;
291
+ }
202
292
  """
203
293
 
204
294
 
@@ -36,7 +36,11 @@ def get_dashboard_html() -> str:
36
36
  </div>
37
37
  </header>
38
38
 
39
- <div class="filters">
39
+ <div class="filters collapsed">
40
+ <div class="filters-toggle" onclick="toggleFilters()">
41
+ <span class="fold-icon">▶</span>
42
+ <span>Filters &amp; Config</span>
43
+ </div>
40
44
  <div class="filter-group">
41
45
  <h3>Packages</h3>
42
46
  <div class="filter-options" id="packageFilters"></div>
@@ -59,6 +63,15 @@ def get_dashboard_html() -> str:
59
63
  <h3>Remotes</h3>
60
64
  <div class="filter-options" id="remoteFilters"></div>
61
65
  </div>
66
+ <div class="filter-group">
67
+ <h3>RTD</h3>
68
+ <div class="filter-options" id="rtdFilters"></div>
69
+ </div>
70
+ </div>
71
+
72
+ <div class="expand-controls">
73
+ <button class="btn-secondary" onclick="toggleAllCards(true)">Expand All</button>
74
+ <button class="btn-secondary" onclick="toggleAllCards(false)">Collapse All</button>
62
75
  </div>
63
76
 
64
77
  <div class="summary" id="summary"></div>
scitex/_dev/_rtd.py ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2026-02-03
3
+ # File: scitex/_dev/_rtd.py
4
+
5
+ """Read the Docs build status checking for scitex ecosystem."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import urllib.request
10
+ from typing import Any
11
+
12
+ from ._ecosystem import ECOSYSTEM
13
+
14
+ # RTD project slugs (if different from package name)
15
+ RTD_SLUGS: dict[str, str] = {
16
+ "scitex": "scitex-python",
17
+ }
18
+
19
+
20
+ def get_rtd_slug(package: str) -> str:
21
+ """Get RTD project slug for a package."""
22
+ return RTD_SLUGS.get(package, package)
23
+
24
+
25
+ def check_rtd_status(package: str, version: str = "latest") -> dict[str, Any]:
26
+ """Check Read the Docs build status for a package.
27
+
28
+ Parameters
29
+ ----------
30
+ package : str
31
+ Package name.
32
+ version : str
33
+ RTD version to check (latest, stable, etc.).
34
+
35
+ Returns
36
+ -------
37
+ dict
38
+ Status info with keys: status, version, url, error (if any).
39
+ """
40
+ slug = get_rtd_slug(package)
41
+ badge_url = f"https://readthedocs.org/projects/{slug}/badge/?version={version}"
42
+ docs_url = f"https://{slug}.readthedocs.io/en/{version}/"
43
+
44
+ try:
45
+ # Fetch the badge SVG content to determine status
46
+ req = urllib.request.Request(badge_url)
47
+ req.add_header("User-Agent", "scitex-dev-tools/1.0")
48
+
49
+ with urllib.request.urlopen(req, timeout=10) as response:
50
+ content = response.read().decode("utf-8")
51
+
52
+ # Parse SVG content for status
53
+ if "passing" in content.lower():
54
+ status = "passing"
55
+ elif "failing" in content.lower():
56
+ status = "failing"
57
+ elif "unknown" in content.lower():
58
+ status = "unknown"
59
+ else:
60
+ status = "unknown"
61
+
62
+ return {
63
+ "status": status,
64
+ "version": version,
65
+ "url": docs_url,
66
+ }
67
+
68
+ except urllib.error.HTTPError as e:
69
+ if e.code == 404:
70
+ return {
71
+ "status": "not_found",
72
+ "version": version,
73
+ "error": f"Project '{slug}' not found on RTD",
74
+ }
75
+ return {
76
+ "status": "error",
77
+ "version": version,
78
+ "error": f"HTTP {e.code}: {e.reason}",
79
+ }
80
+ except Exception as e:
81
+ return {
82
+ "status": "error",
83
+ "version": version,
84
+ "error": str(e),
85
+ }
86
+
87
+
88
+ def check_all_rtd(
89
+ packages: list[str] | None = None,
90
+ versions: list[str] | None = None,
91
+ ) -> dict[str, dict[str, dict[str, Any]]]:
92
+ """Check RTD status for all ecosystem packages.
93
+
94
+ Parameters
95
+ ----------
96
+ packages : list[str] | None
97
+ List of package names. If None, uses ecosystem packages.
98
+ versions : list[str] | None
99
+ List of versions to check. Default: ["latest", "stable"].
100
+
101
+ Returns
102
+ -------
103
+ dict
104
+ Mapping: version -> package_name -> status_info
105
+ """
106
+ if packages is None:
107
+ packages = list(ECOSYSTEM.keys())
108
+
109
+ if versions is None:
110
+ versions = ["latest", "stable"]
111
+
112
+ results: dict[str, dict[str, dict[str, Any]]] = {}
113
+
114
+ for version in versions:
115
+ results[version] = {}
116
+ for package in packages:
117
+ results[version][package] = check_rtd_status(package, version)
118
+
119
+ return results
120
+
121
+
122
+ # EOF
scitex/_dev/_ssh.py CHANGED
@@ -147,20 +147,49 @@ def get_remote_versions(
147
147
  ssh_target = f"{host.user}@{host.hostname}"
148
148
  ssh_args.append(ssh_target)
149
149
 
150
- # Build Python command to check all packages
151
- packages_str = ",".join(f"'{p}'" for p in packages)
152
- python_cmd = f"""python3 -c "
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"""
153
156
  import json
154
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
+
155
177
  results = {{}}
156
- for pkg in [{packages_str}]:
178
+ for pkg in {packages_list}:
179
+ result = {{"installed": None, "toml": None, "status": "not_installed"}}
157
180
  try:
158
- results[pkg] = {{'installed': version(pkg), 'status': 'ok'}}
181
+ result["installed"] = version(pkg)
182
+ result["status"] = "ok"
159
183
  except Exception as e:
160
- results[pkg] = {{'installed': None, 'status': 'not_installed', 'error': str(e)}}
184
+ result["error"] = str(e)
185
+ result["toml"] = get_toml_version(pkg)
186
+ results[pkg] = result
161
187
  print(json.dumps(results))
162
- "
163
188
  """
189
+ encoded = base64.b64encode(python_script.encode()).decode()
190
+ python_cmd = (
191
+ f"python3 -c \"import base64;exec(base64.b64decode('{encoded}').decode())\""
192
+ )
164
193
  ssh_args.append(python_cmd)
165
194
 
166
195
  try:
@@ -179,9 +208,10 @@ print(json.dumps(results))
179
208
  }
180
209
 
181
210
  import json
211
+ from typing import cast
182
212
 
183
213
  try:
184
- return json.loads(result.stdout.strip())
214
+ return cast(dict[str, dict[str, Any]], json.loads(result.stdout.strip()))
185
215
  except json.JSONDecodeError:
186
216
  return {
187
217
  pkg: {