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.
- scitex/_dev/_dashboard/_routes.py +13 -0
- scitex/_dev/_dashboard/_scripts.py +144 -23
- scitex/_dev/_dashboard/_styles.py +90 -0
- scitex/_dev/_dashboard/_templates.py +14 -1
- scitex/_dev/_rtd.py +122 -0
- scitex/_dev/_ssh.py +38 -8
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +90 -0
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +1571 -0
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +6262 -0
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +1274 -0
- scitex/dev/plt/data/mpl/dir_ax.txt +459 -0
- 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/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +44 -0
- scitex/scholar/data/bib_files/bibliography.bib +1952 -0
- scitex/scholar/data/bib_files/neurovista.bib +277 -0
- scitex/scholar/data/bib_files/neurovista_enriched.bib +441 -0
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +441 -0
- scitex/scholar/data/bib_files/neurovista_processed.bib +338 -0
- scitex/scholar/data/bib_files/openaccess.bib +89 -0
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +2178 -0
- scitex/scholar/data/bib_files/pac.bib +698 -0
- scitex/scholar/data/bib_files/pac_enriched.bib +1061 -0
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +75 -0
- scitex/scholar/data/bib_files/paywalled.bib +98 -0
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +58 -0
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +87 -0
- scitex/scholar/data/bib_files/seizure_prediction.bib +694 -0
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +437 -0
- scitex/scholar/data/bib_files/test_final_enriched.bib +437 -0
- scitex/scholar/data/bib_files/test_seizure.bib +46 -0
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -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-2.17.3.dist-info → scitex-2.17.4.dist-info}/METADATA +1 -1
- {scitex-2.17.3.dist-info → scitex-2.17.4.dist-info}/RECORD +51 -22
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +0 -462
- scitex/scholar/url_finder/.tmp/open_url/README.md +0 -223
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +0 -694
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +0 -1160
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +0 -344
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +0 -24
- {scitex-2.17.3.dist-info → scitex-2.17.4.dist-info}/WHEEL +0 -0
- {scitex-2.17.3.dist-info → scitex-2.17.4.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
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
|
|
201
|
-
unreleased: filteredPackages.filter(([, i]) => i
|
|
202
|
-
mismatch: filteredPackages.filter(([, i]) => i
|
|
203
|
-
outdated: filteredPackages.filter(([, i]) => i
|
|
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
|
-
|
|
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 for newlines in title attribute)
|
|
313
|
+
const tooltipText = allIssues.length > 0 ? allIssues.join(' ') : '';
|
|
314
|
+
const tooltipAttr = tooltipText ? `title="${tooltipText}"` : '';
|
|
315
|
+
|
|
233
316
|
let html = `
|
|
234
|
-
<div class="package-card">
|
|
235
|
-
<div class="package-header">
|
|
236
|
-
<span class="
|
|
237
|
-
<
|
|
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>
|
|
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>
|
|
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>
|
|
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-
|
|
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>
|
|
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 (
|
|
375
|
+
if (allIssues.length > 0) {
|
|
275
376
|
html += `<div class="issues"><h4>Issues</h4><ul>`;
|
|
276
|
-
|
|
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 & 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
|
-
|
|
152
|
-
|
|
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
|
|
178
|
+
for pkg in {packages_list}:
|
|
179
|
+
result = {{"installed": None, "toml": None, "status": "not_installed"}}
|
|
157
180
|
try:
|
|
158
|
-
|
|
181
|
+
result["installed"] = version(pkg)
|
|
182
|
+
result["status"] = "ok"
|
|
159
183
|
except Exception as e:
|
|
160
|
-
|
|
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: {
|