scitex 2.4.2__py3-none-any.whl → 2.5.0__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/__version__.py +1 -1
- scitex/browser/__init__.py +53 -0
- scitex/browser/debugging/__init__.py +56 -0
- scitex/browser/debugging/_failure_capture.py +372 -0
- scitex/browser/debugging/_sync_session.py +259 -0
- scitex/browser/debugging/_test_monitor.py +284 -0
- scitex/browser/debugging/_visual_cursor.py +432 -0
- scitex/io/_load.py +5 -0
- scitex/io/_load_modules/_canvas.py +171 -0
- scitex/io/_save.py +8 -0
- scitex/io/_save_modules/_canvas.py +356 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
- scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
- scitex/plt/utils/__init__.py +10 -0
- scitex/plt/utils/_collect_figure_metadata.py +14 -12
- scitex/plt/utils/_csv_column_naming.py +237 -0
- scitex/scholar/citation_graph/database.py +9 -2
- scitex/scholar/config/ScholarConfig.py +23 -3
- scitex/scholar/config/default.yaml +55 -0
- scitex/scholar/core/Paper.py +102 -0
- scitex/scholar/core/__init__.py +44 -0
- scitex/scholar/core/journal_normalizer.py +524 -0
- scitex/scholar/core/oa_cache.py +285 -0
- scitex/scholar/core/open_access.py +457 -0
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +137 -0
- scitex/scholar/pdf_download/strategies/__init__.py +6 -0
- scitex/scholar/pdf_download/strategies/open_access_download.py +186 -0
- scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +18 -3
- scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +15 -2
- scitex/session/_decorator.py +13 -1
- scitex/vis/README.md +246 -615
- scitex/vis/__init__.py +138 -78
- scitex/vis/canvas.py +423 -0
- scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
- scitex/vis/editor/__init__.py +1 -1
- scitex/vis/editor/_dearpygui_editor.py +1830 -0
- scitex/vis/editor/_defaults.py +40 -1
- scitex/vis/editor/_edit.py +54 -18
- scitex/vis/editor/_flask_editor.py +37 -0
- scitex/vis/editor/_qt_editor.py +865 -0
- scitex/vis/editor/flask_editor/__init__.py +21 -0
- scitex/vis/editor/flask_editor/bbox.py +216 -0
- scitex/vis/editor/flask_editor/core.py +152 -0
- scitex/vis/editor/flask_editor/plotter.py +130 -0
- scitex/vis/editor/flask_editor/renderer.py +184 -0
- scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
- scitex/vis/editor/flask_editor/templates/html.py +295 -0
- scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
- scitex/vis/editor/flask_editor/templates/styles.py +549 -0
- scitex/vis/editor/flask_editor/utils.py +81 -0
- scitex/vis/io/__init__.py +84 -21
- scitex/vis/io/canvas.py +226 -0
- scitex/vis/io/data.py +204 -0
- scitex/vis/io/directory.py +202 -0
- scitex/vis/io/export.py +460 -0
- scitex/vis/io/panel.py +424 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/RECORD +61 -32
- scitex/vis/DJANGO_INTEGRATION.md +0 -677
- scitex/vis/editor/_web_editor.py +0 -1440
- scitex/vis/tmp.txt +0 -239
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: 2025-12-08
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/browser/debugging/_visual_cursor.py
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Visual cursor and click effects for E2E test feedback.
|
|
8
|
+
|
|
9
|
+
Provides visual feedback during browser automation:
|
|
10
|
+
- Visual cursor indicator that follows mouse movements
|
|
11
|
+
- Click ripple effects
|
|
12
|
+
- Drag state visualization
|
|
13
|
+
- Step progress messages
|
|
14
|
+
|
|
15
|
+
Works with both async and sync Playwright APIs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Union
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from playwright.async_api import Page as AsyncPage
|
|
24
|
+
from playwright.sync_api import Page as SyncPage
|
|
25
|
+
|
|
26
|
+
# CSS styles for visual effects
|
|
27
|
+
VISUAL_EFFECTS_CSS = """
|
|
28
|
+
/* Visual cursor indicator */
|
|
29
|
+
#_scitex_cursor {
|
|
30
|
+
position: fixed;
|
|
31
|
+
width: 24px;
|
|
32
|
+
height: 24px;
|
|
33
|
+
border: 3px solid #FF4444;
|
|
34
|
+
border-radius: 50%;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
z-index: 2147483647;
|
|
37
|
+
transform: translate(-50%, -50%);
|
|
38
|
+
transition: all 0.15s ease-out;
|
|
39
|
+
box-shadow: 0 0 15px rgba(255, 68, 68, 0.6);
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
#_scitex_cursor.clicking {
|
|
43
|
+
transform: translate(-50%, -50%) scale(0.6);
|
|
44
|
+
background: rgba(255, 68, 68, 0.4);
|
|
45
|
+
box-shadow: 0 0 25px rgba(255, 68, 68, 0.8);
|
|
46
|
+
}
|
|
47
|
+
#_scitex_cursor.dragging {
|
|
48
|
+
border-color: #28A745;
|
|
49
|
+
box-shadow: 0 0 15px rgba(40, 167, 69, 0.6);
|
|
50
|
+
width: 28px;
|
|
51
|
+
height: 28px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Click ripple effect */
|
|
55
|
+
.scitex-click-ripple {
|
|
56
|
+
position: fixed;
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
border: 3px solid #FF4444;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
z-index: 2147483646;
|
|
61
|
+
animation: clickRipple 0.5s ease-out forwards;
|
|
62
|
+
}
|
|
63
|
+
@keyframes clickRipple {
|
|
64
|
+
0% { width: 0; height: 0; opacity: 1; transform: translate(-50%, -50%); }
|
|
65
|
+
100% { width: 80px; height: 80px; opacity: 0; transform: translate(-50%, -50%); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Step message container */
|
|
69
|
+
#_scitex_step_messages {
|
|
70
|
+
position: fixed;
|
|
71
|
+
top: 10px;
|
|
72
|
+
left: 10px;
|
|
73
|
+
z-index: 2147483647;
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 8px;
|
|
77
|
+
max-width: 600px;
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
}
|
|
80
|
+
.scitex-step-msg {
|
|
81
|
+
background: rgba(0, 0, 0, 0.9);
|
|
82
|
+
color: white;
|
|
83
|
+
padding: 14px 24px;
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
font-family: 'Courier New', monospace;
|
|
87
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
88
|
+
word-wrap: break-word;
|
|
89
|
+
animation: stepSlideIn 0.3s ease-out;
|
|
90
|
+
}
|
|
91
|
+
@keyframes stepSlideIn {
|
|
92
|
+
0% { opacity: 0; transform: translateX(-20px); }
|
|
93
|
+
100% { opacity: 1; transform: translateX(0); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Test result banner */
|
|
97
|
+
#_scitex_result_banner {
|
|
98
|
+
position: fixed;
|
|
99
|
+
top: 50%;
|
|
100
|
+
left: 50%;
|
|
101
|
+
transform: translate(-50%, -50%);
|
|
102
|
+
padding: 40px 80px;
|
|
103
|
+
border-radius: 16px;
|
|
104
|
+
font-size: 48px;
|
|
105
|
+
font-weight: bold;
|
|
106
|
+
font-family: 'Arial', sans-serif;
|
|
107
|
+
z-index: 2147483647;
|
|
108
|
+
pointer-events: none;
|
|
109
|
+
animation: resultPulse 0.5s ease-out;
|
|
110
|
+
}
|
|
111
|
+
#_scitex_result_banner.success {
|
|
112
|
+
background: rgba(40, 167, 69, 0.95);
|
|
113
|
+
color: white;
|
|
114
|
+
box-shadow: 0 0 50px rgba(40, 167, 69, 0.8);
|
|
115
|
+
}
|
|
116
|
+
#_scitex_result_banner.failure {
|
|
117
|
+
background: rgba(220, 53, 69, 0.95);
|
|
118
|
+
color: white;
|
|
119
|
+
box-shadow: 0 0 50px rgba(220, 53, 69, 0.8);
|
|
120
|
+
}
|
|
121
|
+
@keyframes resultPulse {
|
|
122
|
+
0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
|
|
123
|
+
50% { transform: translate(-50%, -50%) scale(1.1); }
|
|
124
|
+
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
|
|
125
|
+
}
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# JavaScript to inject visual effects
|
|
129
|
+
INJECT_EFFECTS_JS = f"""
|
|
130
|
+
() => {{
|
|
131
|
+
if (document.getElementById('_scitex_visual_effects')) return;
|
|
132
|
+
|
|
133
|
+
const style = document.createElement('style');
|
|
134
|
+
style.id = '_scitex_visual_effects';
|
|
135
|
+
style.textContent = `{VISUAL_EFFECTS_CSS}`;
|
|
136
|
+
document.head.appendChild(style);
|
|
137
|
+
|
|
138
|
+
// Create cursor element
|
|
139
|
+
const cursor = document.createElement('div');
|
|
140
|
+
cursor.id = '_scitex_cursor';
|
|
141
|
+
document.body.appendChild(cursor);
|
|
142
|
+
|
|
143
|
+
// Create step messages container
|
|
144
|
+
const msgContainer = document.createElement('div');
|
|
145
|
+
msgContainer.id = '_scitex_step_messages';
|
|
146
|
+
document.body.appendChild(msgContainer);
|
|
147
|
+
}}
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def inject_visual_effects(page: Union["AsyncPage", "SyncPage"]) -> None:
|
|
152
|
+
"""Inject CSS and elements for visual effects (sync version)."""
|
|
153
|
+
page.evaluate(INJECT_EFFECTS_JS)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def inject_visual_effects_async(page: "AsyncPage") -> None:
|
|
157
|
+
"""Inject CSS and elements for visual effects (async version)."""
|
|
158
|
+
await page.evaluate(INJECT_EFFECTS_JS)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def show_cursor_at(
|
|
162
|
+
page: Union["AsyncPage", "SyncPage"],
|
|
163
|
+
x: float,
|
|
164
|
+
y: float,
|
|
165
|
+
state: str = "normal"
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Move visual cursor to position (sync version).
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
page: Playwright page object
|
|
171
|
+
x: X coordinate
|
|
172
|
+
y: Y coordinate
|
|
173
|
+
state: Cursor state - "normal", "clicking", or "dragging"
|
|
174
|
+
"""
|
|
175
|
+
page.evaluate("""
|
|
176
|
+
([x, y, state]) => {
|
|
177
|
+
let cursor = document.getElementById('_scitex_cursor');
|
|
178
|
+
if (!cursor) {
|
|
179
|
+
cursor = document.createElement('div');
|
|
180
|
+
cursor.id = '_scitex_cursor';
|
|
181
|
+
document.body.appendChild(cursor);
|
|
182
|
+
}
|
|
183
|
+
cursor.style.display = 'block';
|
|
184
|
+
cursor.style.left = x + 'px';
|
|
185
|
+
cursor.style.top = y + 'px';
|
|
186
|
+
cursor.className = state === 'clicking' ? 'clicking' :
|
|
187
|
+
state === 'dragging' ? 'dragging' : '';
|
|
188
|
+
if (state === 'dragging') {
|
|
189
|
+
cursor.style.borderColor = '#28A745';
|
|
190
|
+
cursor.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.6)';
|
|
191
|
+
} else {
|
|
192
|
+
cursor.style.borderColor = '#FF4444';
|
|
193
|
+
cursor.style.boxShadow = '0 0 15px rgba(255, 68, 68, 0.6)';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
""", [x, y, state])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def show_cursor_at_async(
|
|
200
|
+
page: "AsyncPage",
|
|
201
|
+
x: float,
|
|
202
|
+
y: float,
|
|
203
|
+
state: str = "normal"
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Move visual cursor to position (async version)."""
|
|
206
|
+
await page.evaluate("""
|
|
207
|
+
([x, y, state]) => {
|
|
208
|
+
let cursor = document.getElementById('_scitex_cursor');
|
|
209
|
+
if (!cursor) {
|
|
210
|
+
cursor = document.createElement('div');
|
|
211
|
+
cursor.id = '_scitex_cursor';
|
|
212
|
+
document.body.appendChild(cursor);
|
|
213
|
+
}
|
|
214
|
+
cursor.style.display = 'block';
|
|
215
|
+
cursor.style.left = x + 'px';
|
|
216
|
+
cursor.style.top = y + 'px';
|
|
217
|
+
cursor.className = state === 'clicking' ? 'clicking' :
|
|
218
|
+
state === 'dragging' ? 'dragging' : '';
|
|
219
|
+
if (state === 'dragging') {
|
|
220
|
+
cursor.style.borderColor = '#28A745';
|
|
221
|
+
cursor.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.6)';
|
|
222
|
+
} else {
|
|
223
|
+
cursor.style.borderColor = '#FF4444';
|
|
224
|
+
cursor.style.boxShadow = '0 0 15px rgba(255, 68, 68, 0.6)';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
""", [x, y, state])
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def show_click_effect(page: Union["AsyncPage", "SyncPage"], x: float, y: float) -> None:
|
|
231
|
+
"""Show click ripple effect at position (sync version)."""
|
|
232
|
+
page.evaluate("""
|
|
233
|
+
([x, y]) => {
|
|
234
|
+
const ripple = document.createElement('div');
|
|
235
|
+
ripple.className = 'scitex-click-ripple';
|
|
236
|
+
ripple.style.left = x + 'px';
|
|
237
|
+
ripple.style.top = y + 'px';
|
|
238
|
+
document.body.appendChild(ripple);
|
|
239
|
+
setTimeout(() => ripple.remove(), 600);
|
|
240
|
+
|
|
241
|
+
const cursor = document.getElementById('_scitex_cursor');
|
|
242
|
+
if (cursor) {
|
|
243
|
+
cursor.classList.add('clicking');
|
|
244
|
+
setTimeout(() => cursor.classList.remove('clicking'), 150);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
""", [x, y])
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def show_click_effect_async(page: "AsyncPage", x: float, y: float) -> None:
|
|
251
|
+
"""Show click ripple effect at position (async version)."""
|
|
252
|
+
await page.evaluate("""
|
|
253
|
+
([x, y]) => {
|
|
254
|
+
const ripple = document.createElement('div');
|
|
255
|
+
ripple.className = 'scitex-click-ripple';
|
|
256
|
+
ripple.style.left = x + 'px';
|
|
257
|
+
ripple.style.top = y + 'px';
|
|
258
|
+
document.body.appendChild(ripple);
|
|
259
|
+
setTimeout(() => ripple.remove(), 600);
|
|
260
|
+
|
|
261
|
+
const cursor = document.getElementById('_scitex_cursor');
|
|
262
|
+
if (cursor) {
|
|
263
|
+
cursor.classList.add('clicking');
|
|
264
|
+
setTimeout(() => cursor.classList.remove('clicking'), 150);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
""", [x, y])
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def show_step(
|
|
271
|
+
page: Union["AsyncPage", "SyncPage"],
|
|
272
|
+
step: int,
|
|
273
|
+
total: int,
|
|
274
|
+
message: str,
|
|
275
|
+
level: str = "info"
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Show numbered step message in browser (sync version).
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
page: Playwright page object
|
|
281
|
+
step: Current step number
|
|
282
|
+
total: Total number of steps
|
|
283
|
+
message: Message to display
|
|
284
|
+
level: Message level - "info", "success", "warning", or "error"
|
|
285
|
+
"""
|
|
286
|
+
color_map = {
|
|
287
|
+
"info": "#17A2B8",
|
|
288
|
+
"success": "#28A745",
|
|
289
|
+
"warning": "#FFC107",
|
|
290
|
+
"error": "#DC3545",
|
|
291
|
+
}
|
|
292
|
+
color = color_map.get(level, color_map["info"])
|
|
293
|
+
|
|
294
|
+
page.evaluate("""
|
|
295
|
+
([step, total, message, color]) => {
|
|
296
|
+
let container = document.getElementById('_scitex_step_messages');
|
|
297
|
+
if (!container) {
|
|
298
|
+
container = document.createElement('div');
|
|
299
|
+
container.id = '_scitex_step_messages';
|
|
300
|
+
container.style.cssText = `
|
|
301
|
+
position: fixed; top: 10px; left: 10px; z-index: 2147483647;
|
|
302
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
303
|
+
max-width: 600px; pointer-events: none;
|
|
304
|
+
`;
|
|
305
|
+
document.body.appendChild(container);
|
|
306
|
+
}
|
|
307
|
+
const popup = document.createElement('div');
|
|
308
|
+
popup.className = 'scitex-step-msg';
|
|
309
|
+
popup.innerHTML = `<strong>[${step}/${total}] ${message}</strong>`;
|
|
310
|
+
popup.style.cssText = `
|
|
311
|
+
background: rgba(0, 0, 0, 0.9); color: white;
|
|
312
|
+
padding: 14px 24px; border-radius: 8px; font-size: 16px;
|
|
313
|
+
font-family: 'Courier New', monospace;
|
|
314
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
315
|
+
border-left: 6px solid ${color}; word-wrap: break-word;
|
|
316
|
+
`;
|
|
317
|
+
container.appendChild(popup);
|
|
318
|
+
while (container.children.length > 5) container.removeChild(container.firstChild);
|
|
319
|
+
setTimeout(() => { if (popup.parentNode) popup.parentNode.removeChild(popup); }, 8000);
|
|
320
|
+
}
|
|
321
|
+
""", [step, total, message, color])
|
|
322
|
+
page.wait_for_timeout(200)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
async def show_step_async(
|
|
326
|
+
page: "AsyncPage",
|
|
327
|
+
step: int,
|
|
328
|
+
total: int,
|
|
329
|
+
message: str,
|
|
330
|
+
level: str = "info"
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Show numbered step message in browser (async version)."""
|
|
333
|
+
color_map = {
|
|
334
|
+
"info": "#17A2B8",
|
|
335
|
+
"success": "#28A745",
|
|
336
|
+
"warning": "#FFC107",
|
|
337
|
+
"error": "#DC3545",
|
|
338
|
+
}
|
|
339
|
+
color = color_map.get(level, color_map["info"])
|
|
340
|
+
|
|
341
|
+
await page.evaluate("""
|
|
342
|
+
([step, total, message, color]) => {
|
|
343
|
+
let container = document.getElementById('_scitex_step_messages');
|
|
344
|
+
if (!container) {
|
|
345
|
+
container = document.createElement('div');
|
|
346
|
+
container.id = '_scitex_step_messages';
|
|
347
|
+
container.style.cssText = `
|
|
348
|
+
position: fixed; top: 10px; left: 10px; z-index: 2147483647;
|
|
349
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
350
|
+
max-width: 600px; pointer-events: none;
|
|
351
|
+
`;
|
|
352
|
+
document.body.appendChild(container);
|
|
353
|
+
}
|
|
354
|
+
const popup = document.createElement('div');
|
|
355
|
+
popup.className = 'scitex-step-msg';
|
|
356
|
+
popup.innerHTML = `<strong>[${step}/${total}] ${message}</strong>`;
|
|
357
|
+
popup.style.cssText = `
|
|
358
|
+
background: rgba(0, 0, 0, 0.9); color: white;
|
|
359
|
+
padding: 14px 24px; border-radius: 8px; font-size: 16px;
|
|
360
|
+
font-family: 'Courier New', monospace;
|
|
361
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
362
|
+
border-left: 6px solid ${color}; word-wrap: break-word;
|
|
363
|
+
`;
|
|
364
|
+
container.appendChild(popup);
|
|
365
|
+
while (container.children.length > 5) container.removeChild(container.firstChild);
|
|
366
|
+
setTimeout(() => { if (popup.parentNode) popup.parentNode.removeChild(popup); }, 8000);
|
|
367
|
+
}
|
|
368
|
+
""", [step, total, message, color])
|
|
369
|
+
await page.wait_for_timeout(200)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def show_test_result(
|
|
373
|
+
page: Union["AsyncPage", "SyncPage"],
|
|
374
|
+
success: bool,
|
|
375
|
+
message: str = "",
|
|
376
|
+
delay_ms: int = 3000
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Show test result banner (PASS/FAIL) and wait (sync version).
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
page: Playwright page object
|
|
382
|
+
success: True for PASS, False for FAIL
|
|
383
|
+
message: Optional message to display
|
|
384
|
+
delay_ms: How long to display before continuing
|
|
385
|
+
"""
|
|
386
|
+
status = "PASS" if success else "FAIL"
|
|
387
|
+
css_class = "success" if success else "failure"
|
|
388
|
+
display_text = f"{status}" + (f": {message}" if message else "")
|
|
389
|
+
|
|
390
|
+
page.evaluate("""
|
|
391
|
+
([displayText, cssClass]) => {
|
|
392
|
+
// Remove existing banner
|
|
393
|
+
const existing = document.getElementById('_scitex_result_banner');
|
|
394
|
+
if (existing) existing.remove();
|
|
395
|
+
|
|
396
|
+
const banner = document.createElement('div');
|
|
397
|
+
banner.id = '_scitex_result_banner';
|
|
398
|
+
banner.className = cssClass;
|
|
399
|
+
banner.textContent = displayText;
|
|
400
|
+
document.body.appendChild(banner);
|
|
401
|
+
}
|
|
402
|
+
""", [display_text, css_class])
|
|
403
|
+
page.wait_for_timeout(delay_ms)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def show_test_result_async(
|
|
407
|
+
page: "AsyncPage",
|
|
408
|
+
success: bool,
|
|
409
|
+
message: str = "",
|
|
410
|
+
delay_ms: int = 3000
|
|
411
|
+
) -> None:
|
|
412
|
+
"""Show test result banner (PASS/FAIL) and wait (async version)."""
|
|
413
|
+
status = "PASS" if success else "FAIL"
|
|
414
|
+
css_class = "success" if success else "failure"
|
|
415
|
+
display_text = f"{status}" + (f": {message}" if message else "")
|
|
416
|
+
|
|
417
|
+
await page.evaluate("""
|
|
418
|
+
([displayText, cssClass]) => {
|
|
419
|
+
const existing = document.getElementById('_scitex_result_banner');
|
|
420
|
+
if (existing) existing.remove();
|
|
421
|
+
|
|
422
|
+
const banner = document.createElement('div');
|
|
423
|
+
banner.id = '_scitex_result_banner';
|
|
424
|
+
banner.className = cssClass;
|
|
425
|
+
banner.textContent = displayText;
|
|
426
|
+
document.body.appendChild(banner);
|
|
427
|
+
}
|
|
428
|
+
""", [display_text, css_class])
|
|
429
|
+
await page.wait_for_timeout(delay_ms)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# EOF
|
scitex/io/_load.py
CHANGED
|
@@ -44,6 +44,7 @@ from ._load_modules._txt import _load_txt
|
|
|
44
44
|
from ._load_modules._xml import _load_xml
|
|
45
45
|
from ._load_modules._yaml import _load_yaml
|
|
46
46
|
from ._load_modules._zarr import _load_zarr
|
|
47
|
+
from ._load_modules._canvas import _load_canvas
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
def load(
|
|
@@ -151,6 +152,10 @@ def load(
|
|
|
151
152
|
if verbose:
|
|
152
153
|
print(f"[DEBUG] After Path conversion: {lpath}")
|
|
153
154
|
|
|
155
|
+
# Handle .canvas directories (special case - directory not file)
|
|
156
|
+
if lpath.endswith(".canvas"):
|
|
157
|
+
return _load_canvas(lpath, verbose=verbose, **kwargs)
|
|
158
|
+
|
|
154
159
|
# Check if it's a glob pattern
|
|
155
160
|
if "*" in lpath or "?" in lpath or "[" in lpath:
|
|
156
161
|
# Handle glob pattern
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: 2025-12-08
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/io/_load_modules/_canvas.py
|
|
5
|
+
"""
|
|
6
|
+
Load canvas directory (.canvas) for scitex.vis.
|
|
7
|
+
|
|
8
|
+
Canvas directories are portable figure bundles containing:
|
|
9
|
+
- canvas.json: Layout, panels, composition settings
|
|
10
|
+
- panels/: Panel directories (scitex or image type)
|
|
11
|
+
- exports/: Composed outputs (PNG, PDF, SVG)
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
>>> import scitex as stx
|
|
15
|
+
>>> # Load canvas from directory
|
|
16
|
+
>>> canvas = stx.io.load("/path/to/fig1_results.canvas")
|
|
17
|
+
>>> # Access canvas properties
|
|
18
|
+
>>> print(canvas["canvas_name"])
|
|
19
|
+
>>> print(canvas["panels"])
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, Union
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_canvas(
|
|
28
|
+
lpath: Union[str, Path],
|
|
29
|
+
verbose: bool = False,
|
|
30
|
+
load_panels: bool = False,
|
|
31
|
+
as_dict: bool = False,
|
|
32
|
+
**kwargs,
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""
|
|
35
|
+
Load a canvas from a .canvas directory.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
lpath : str or Path
|
|
40
|
+
Path to the .canvas directory.
|
|
41
|
+
verbose : bool, optional
|
|
42
|
+
If True, print verbose output. Default is False.
|
|
43
|
+
load_panels : bool, optional
|
|
44
|
+
If True, also load panel images as numpy arrays.
|
|
45
|
+
If False (default), only load canvas.json metadata.
|
|
46
|
+
as_dict : bool, optional
|
|
47
|
+
If True, return raw dict instead of Canvas object.
|
|
48
|
+
Default is False (returns Canvas object).
|
|
49
|
+
**kwargs
|
|
50
|
+
Additional arguments (reserved for future use).
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
Canvas or Dict[str, Any]
|
|
55
|
+
Canvas object (default) or dictionary if as_dict=True.
|
|
56
|
+
Contains:
|
|
57
|
+
- All fields from canvas.json
|
|
58
|
+
- '_canvas_dir': Path to the canvas directory
|
|
59
|
+
- If load_panels=True, panel images are loaded into memory
|
|
60
|
+
|
|
61
|
+
Raises
|
|
62
|
+
------
|
|
63
|
+
FileNotFoundError
|
|
64
|
+
If the .canvas directory or canvas.json doesn't exist.
|
|
65
|
+
ValueError
|
|
66
|
+
If the path doesn't appear to be a valid canvas directory.
|
|
67
|
+
"""
|
|
68
|
+
lpath = Path(lpath)
|
|
69
|
+
|
|
70
|
+
# Validate path
|
|
71
|
+
if not str(lpath).endswith(".canvas"):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Canvas path must end with .canvas extension: {lpath}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not lpath.exists():
|
|
77
|
+
raise FileNotFoundError(f"Canvas directory not found: {lpath}")
|
|
78
|
+
|
|
79
|
+
if not lpath.is_dir():
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Canvas path must be a directory: {lpath}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
json_path = lpath / "canvas.json"
|
|
85
|
+
if not json_path.exists():
|
|
86
|
+
raise FileNotFoundError(
|
|
87
|
+
f"canvas.json not found in canvas directory: {lpath}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Load canvas.json
|
|
91
|
+
with open(json_path, "r") as f:
|
|
92
|
+
canvas_dict = json.load(f)
|
|
93
|
+
|
|
94
|
+
# Add reference to the canvas directory
|
|
95
|
+
canvas_dict["_canvas_dir"] = str(lpath)
|
|
96
|
+
|
|
97
|
+
if verbose:
|
|
98
|
+
print(f"Loaded canvas: {canvas_dict.get('canvas_name', 'unknown')}")
|
|
99
|
+
print(f" Schema version: {canvas_dict.get('schema_version', 'unknown')}")
|
|
100
|
+
print(f" Panels: {len(canvas_dict.get('panels', []))}")
|
|
101
|
+
|
|
102
|
+
# Optionally load panel images
|
|
103
|
+
if load_panels:
|
|
104
|
+
_load_panel_images(lpath, canvas_dict, verbose=verbose)
|
|
105
|
+
|
|
106
|
+
# Return Canvas object by default
|
|
107
|
+
if not as_dict:
|
|
108
|
+
try:
|
|
109
|
+
from scitex.vis.canvas import Canvas
|
|
110
|
+
canvas_obj = Canvas.from_dict(canvas_dict)
|
|
111
|
+
# Store reference to original directory for copying
|
|
112
|
+
canvas_obj._canvas_dir = str(lpath)
|
|
113
|
+
return canvas_obj
|
|
114
|
+
except ImportError:
|
|
115
|
+
# Fall back to dict if Canvas class unavailable
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
return canvas_dict
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _load_panel_images(
|
|
122
|
+
canvas_dir: Path,
|
|
123
|
+
canvas_dict: Dict[str, Any],
|
|
124
|
+
verbose: bool = False,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Load panel images into canvas_dict.
|
|
128
|
+
|
|
129
|
+
Modifies canvas_dict in place, adding '_image' key to each panel
|
|
130
|
+
containing the loaded numpy array.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
from PIL import Image
|
|
134
|
+
import numpy as np
|
|
135
|
+
except ImportError:
|
|
136
|
+
if verbose:
|
|
137
|
+
print("PIL/numpy not available, skipping panel image loading")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
panels_dir = canvas_dir / "panels"
|
|
141
|
+
|
|
142
|
+
for panel in canvas_dict.get("panels", []):
|
|
143
|
+
panel_name = panel.get("name", "")
|
|
144
|
+
if not panel_name:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
panel_dir = panels_dir / panel_name
|
|
148
|
+
if not panel_dir.exists():
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# Try to find panel image
|
|
152
|
+
panel_type = panel.get("type", "image")
|
|
153
|
+
if panel_type == "scitex":
|
|
154
|
+
img_path = panel_dir / "panel.png"
|
|
155
|
+
else:
|
|
156
|
+
# For image type, use source filename
|
|
157
|
+
source = panel.get("source", "panel.png")
|
|
158
|
+
img_path = panel_dir / source
|
|
159
|
+
|
|
160
|
+
if img_path.exists():
|
|
161
|
+
try:
|
|
162
|
+
img = Image.open(img_path)
|
|
163
|
+
panel["_image"] = np.array(img)
|
|
164
|
+
if verbose:
|
|
165
|
+
print(f" Loaded panel image: {panel_name}")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
if verbose:
|
|
168
|
+
print(f" Failed to load panel image {panel_name}: {e}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# EOF
|
scitex/io/_save.py
CHANGED
|
@@ -61,6 +61,7 @@ from ._save_modules import save_torch
|
|
|
61
61
|
from ._save_modules import save_yaml
|
|
62
62
|
from ._save_modules import save_zarr
|
|
63
63
|
from ._save_modules._bibtex import save_bibtex
|
|
64
|
+
from ._save_modules._canvas import save_canvas
|
|
64
65
|
|
|
65
66
|
logger = logging.getLogger()
|
|
66
67
|
|
|
@@ -510,6 +511,11 @@ def _save(
|
|
|
510
511
|
# Get file extension
|
|
511
512
|
ext = _os.path.splitext(spath)[1].lower()
|
|
512
513
|
|
|
514
|
+
# Handle .canvas directories (special case - path ends with .canvas)
|
|
515
|
+
if spath.endswith(".canvas"):
|
|
516
|
+
save_canvas(obj, spath, **kwargs)
|
|
517
|
+
return
|
|
518
|
+
|
|
513
519
|
# Try dispatch dictionary first for O(1) lookup
|
|
514
520
|
if ext in _FILE_HANDLERS:
|
|
515
521
|
# Check if handler needs special parameters
|
|
@@ -1028,6 +1034,8 @@ def _handle_image_with_csv(
|
|
|
1028
1034
|
|
|
1029
1035
|
# Dispatch dictionary for O(1) file format lookup
|
|
1030
1036
|
_FILE_HANDLERS = {
|
|
1037
|
+
# Canvas directory format (scitex.vis)
|
|
1038
|
+
".canvas": save_canvas,
|
|
1031
1039
|
# Excel formats
|
|
1032
1040
|
".xlsx": save_excel,
|
|
1033
1041
|
".xls": save_excel,
|