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.
Files changed (64) hide show
  1. scitex/__version__.py +1 -1
  2. scitex/browser/__init__.py +53 -0
  3. scitex/browser/debugging/__init__.py +56 -0
  4. scitex/browser/debugging/_failure_capture.py +372 -0
  5. scitex/browser/debugging/_sync_session.py +259 -0
  6. scitex/browser/debugging/_test_monitor.py +284 -0
  7. scitex/browser/debugging/_visual_cursor.py +432 -0
  8. scitex/io/_load.py +5 -0
  9. scitex/io/_load_modules/_canvas.py +171 -0
  10. scitex/io/_save.py +8 -0
  11. scitex/io/_save_modules/_canvas.py +356 -0
  12. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
  13. scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
  14. scitex/plt/utils/__init__.py +10 -0
  15. scitex/plt/utils/_collect_figure_metadata.py +14 -12
  16. scitex/plt/utils/_csv_column_naming.py +237 -0
  17. scitex/scholar/citation_graph/database.py +9 -2
  18. scitex/scholar/config/ScholarConfig.py +23 -3
  19. scitex/scholar/config/default.yaml +55 -0
  20. scitex/scholar/core/Paper.py +102 -0
  21. scitex/scholar/core/__init__.py +44 -0
  22. scitex/scholar/core/journal_normalizer.py +524 -0
  23. scitex/scholar/core/oa_cache.py +285 -0
  24. scitex/scholar/core/open_access.py +457 -0
  25. scitex/scholar/pdf_download/ScholarPDFDownloader.py +137 -0
  26. scitex/scholar/pdf_download/strategies/__init__.py +6 -0
  27. scitex/scholar/pdf_download/strategies/open_access_download.py +186 -0
  28. scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +18 -3
  29. scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +15 -2
  30. scitex/session/_decorator.py +13 -1
  31. scitex/vis/README.md +246 -615
  32. scitex/vis/__init__.py +138 -78
  33. scitex/vis/canvas.py +423 -0
  34. scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
  35. scitex/vis/editor/__init__.py +1 -1
  36. scitex/vis/editor/_dearpygui_editor.py +1830 -0
  37. scitex/vis/editor/_defaults.py +40 -1
  38. scitex/vis/editor/_edit.py +54 -18
  39. scitex/vis/editor/_flask_editor.py +37 -0
  40. scitex/vis/editor/_qt_editor.py +865 -0
  41. scitex/vis/editor/flask_editor/__init__.py +21 -0
  42. scitex/vis/editor/flask_editor/bbox.py +216 -0
  43. scitex/vis/editor/flask_editor/core.py +152 -0
  44. scitex/vis/editor/flask_editor/plotter.py +130 -0
  45. scitex/vis/editor/flask_editor/renderer.py +184 -0
  46. scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
  47. scitex/vis/editor/flask_editor/templates/html.py +295 -0
  48. scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
  49. scitex/vis/editor/flask_editor/templates/styles.py +549 -0
  50. scitex/vis/editor/flask_editor/utils.py +81 -0
  51. scitex/vis/io/__init__.py +84 -21
  52. scitex/vis/io/canvas.py +226 -0
  53. scitex/vis/io/data.py +204 -0
  54. scitex/vis/io/directory.py +202 -0
  55. scitex/vis/io/export.py +460 -0
  56. scitex/vis/io/panel.py +424 -0
  57. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
  58. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/RECORD +61 -32
  59. scitex/vis/DJANGO_INTEGRATION.md +0 -677
  60. scitex/vis/editor/_web_editor.py +0 -1440
  61. scitex/vis/tmp.txt +0 -239
  62. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
  63. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
  64. {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,257 @@
1
+ <!-- ---
2
+ !-- Timestamp: 2025-12-08 16:05:55
3
+ !-- Author: ywatanabe
4
+ !-- File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/docs/FIGURE_ARCHITECTURE.md
5
+ !-- --- -->
6
+
7
+ # Figure Architecture for scitex.plt
8
+
9
+ ## Terminology
10
+
11
+ | Term | Meaning | In Code |
12
+ |------------|------------------------------|--------------------------------|
13
+ | **Figure** | A matplotlib figure object | `fig, ax = stx.plt.subplots()` |
14
+ | **Axes** | A single subplot/axes | `ax.plot(x, y)` |
15
+ | **Panel** | Used in `scitex.vis` context | See CANVAS_ARCHITECTURE.md |
16
+
17
+ ## Output Format
18
+
19
+ `stx.plt` outputs **3 files per figure**:
20
+
21
+ ```
22
+ output_dir/
23
+ ├── 01_plot.png # Rendered image
24
+ ├── 01_plot.json # Metadata (dimensions, axes, traces, styles)
25
+ └── 01_plot.csv # Raw data (columns referenced in JSON)
26
+ ```
27
+
28
+ ### Save Patterns
29
+
30
+ **Flat (default):**
31
+ ```python
32
+ fig.savefig("./01_plot.png")
33
+ # Creates: 01_plot.png, 01_plot.json, 01_plot.csv
34
+ ```
35
+
36
+ **Organized by extension:**
37
+ ```python
38
+ fig.savefig("./png/01_plot.png")
39
+ # Creates: png/01_plot.png, json/01_plot.json, csv/01_plot.csv
40
+ ```
41
+
42
+ ## JSON Schema (panel.json)
43
+
44
+ ```json
45
+ {
46
+ "metadata_version": "1.1.0",
47
+ "scitex": {
48
+ "version": "2.4.3",
49
+ "created_at": "2025-12-08T15:40:58.453762",
50
+ "created_with": "scitex.plt.subplots (mm-control)",
51
+ "mode": "publication",
52
+ "axes_size_mm": [40, 28],
53
+ "position_in_grid": [0, 0],
54
+ "style_mm": {
55
+ "axis_thickness_mm": 0.2,
56
+ "tick_length_mm": 0.8,
57
+ "tick_thickness_mm": 0.2,
58
+ "trace_thickness_mm": 0.2,
59
+ "marker_size_mm": 0.8,
60
+ "axis_font_size_pt": 7,
61
+ "tick_font_size_pt": 7,
62
+ "title_font_size_pt": 8,
63
+ "legend_font_size_pt": 6,
64
+ "font_family": "Arial",
65
+ "n_ticks": 4
66
+ }
67
+ },
68
+ "matplotlib": {
69
+ "version": "3.10.3"
70
+ },
71
+ "id": "01_plot",
72
+ "dimensions": {
73
+ "figure_size_mm": [80.0, 68.0],
74
+ "figure_size_inch": [3.15, 2.68],
75
+ "figure_size_px": [944, 803],
76
+ "axes_size_mm": [40.0, 28.0],
77
+ "axes_size_inch": [1.57, 1.10],
78
+ "axes_size_px": [472, 330],
79
+ "axes_position": [0.25, 0.29, 0.5, 0.41],
80
+ "dpi": 300
81
+ },
82
+ "margins_mm": {
83
+ "left": 20.0,
84
+ "bottom": 20.0,
85
+ "right": 20.0,
86
+ "top": 20.0
87
+ },
88
+ "axes_bbox_px": {
89
+ "x0": 236,
90
+ "y0": 236,
91
+ "x1": 708,
92
+ "y1": 566,
93
+ "width": 472,
94
+ "height": 330
95
+ },
96
+ "axes_bbox_mm": {
97
+ "x0": 20.0,
98
+ "y0": 20.0,
99
+ "x1": 60.0,
100
+ "y1": 48.0,
101
+ "width": 40.0,
102
+ "height": 28.0
103
+ },
104
+ "axes": {
105
+ "x": {
106
+ "label": "Time",
107
+ "unit": "s",
108
+ "scale": "linear",
109
+ "lim": [-0.31, 6.60],
110
+ "n_ticks": 4
111
+ },
112
+ "y": {
113
+ "label": "Amplitude",
114
+ "unit": "a.u.",
115
+ "scale": "linear",
116
+ "lim": [-1.10, 1.10],
117
+ "n_ticks": 4
118
+ }
119
+ },
120
+ "title": "ax.plot(x, y)",
121
+ "plot_type": "line",
122
+ "method": "plot",
123
+ "traces": [
124
+ {
125
+ "id": "sine",
126
+ "label": "sin(x)",
127
+ "color": "#0000ff",
128
+ "linestyle": "-",
129
+ "linewidth": 0.57,
130
+ "csv_columns": {
131
+ "x": "ax_00_sine_plot_x",
132
+ "y": "ax_00_sine_plot_y"
133
+ }
134
+ },
135
+ {
136
+ "id": "cosine",
137
+ "label": "cos(x)",
138
+ "color": "#ff0000",
139
+ "linestyle": "--",
140
+ "linewidth": 0.57,
141
+ "csv_columns": {
142
+ "x": "ax_00_cosine_plot_x",
143
+ "y": "ax_00_cosine_plot_y"
144
+ }
145
+ }
146
+ ],
147
+ "legend": {
148
+ "visible": true,
149
+ "loc": 0,
150
+ "frameon": false,
151
+ "labels": ["sin(x)", "cos(x)"]
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## CSV Format
157
+
158
+ Column naming convention: `ax_{ax_idx}_{trace_id}_{method}_{dim}`
159
+
160
+ ```csv
161
+ ax_00_sine_plot_x,ax_00_sine_plot_y,ax_00_cosine_plot_x,ax_00_cosine_plot_y
162
+ 0.0,0.0,0.0,1.0
163
+ 0.063,0.063,0.063,0.998
164
+ 0.127,0.127,0.127,0.992
165
+ ...
166
+ ```
167
+
168
+ ## Key Features
169
+
170
+ ### Publication Mode
171
+
172
+ ```python
173
+ fig, ax = stx.plt.subplots(
174
+ fig_mm={"width": 80, "height": 68},
175
+ axes_mm={"width": 40, "height": 28},
176
+ mode="publication"
177
+ )
178
+ ```
179
+
180
+ - All dimensions in **millimeters** for publication standards
181
+ - Consistent styling across figures
182
+ - Automatic metadata embedding
183
+
184
+ ### Automatic Export
185
+
186
+ On `fig.savefig()`:
187
+ 1. PNG/PDF/SVG rendered
188
+ 2. JSON metadata exported
189
+ 3. CSV data exported
190
+
191
+ ### Trace Tracking
192
+
193
+ All plotting calls are tracked:
194
+
195
+ ```python
196
+ ax.plot(x, y, id="sine", label="sin(x)") # Tracked
197
+ ax.scatter(x, y, id="points") # Tracked
198
+ ax.stx_line(x, y, id="trace") # Tracked
199
+ ```
200
+
201
+ ## Supported Plot Methods
202
+
203
+ ### Standard Matplotlib
204
+ - `plot`, `scatter`, `bar`, `barh`
205
+ - `hist`, `hist2d`, `hexbin`
206
+ - `boxplot`, `violinplot`
207
+ - `fill_between`, `fill_betweenx`
208
+ - `errorbar`, `contour`, `contourf`
209
+ - `imshow`, `matshow`, `pie`
210
+ - `quiver`, `streamplot`
211
+ - `stem`, `step`, `eventplot`
212
+
213
+ ### SciTeX Extensions
214
+ - `stx_line`, `stx_shaded_line`
215
+ - `stx_mean_std`, `stx_mean_ci`, `stx_median_iqr`
216
+ - `stx_kde`, `stx_ecdf`
217
+ - `stx_box`, `stx_violin`
218
+ - `stx_bar`, `stx_barh`
219
+ - `stx_scatter`, `stx_scatter_hist`
220
+ - `stx_heatmap`, `stx_conf_mat`
221
+ - `stx_image`, `stx_imshow`
222
+ - `stx_fillv`, `stx_contour`
223
+ - `stx_raster`, `stx_joyplot`
224
+ - `stx_rectangle`
225
+
226
+ ### Seaborn Integration
227
+ - `sns_lineplot`, `sns_scatterplot`
228
+ - `sns_barplot`, `sns_boxplot`, `sns_violinplot`
229
+ - `sns_stripplot`, `sns_swarmplot`
230
+ - `sns_histplot`, `sns_kdeplot`
231
+ - `sns_heatmap`, `sns_jointplot`, `sns_pairplot`
232
+
233
+ ## Integration with scitex.vis
234
+
235
+ stx.plt outputs can be used as `scitex` type panels in a canvas:
236
+
237
+ ```
238
+ canvas/panels/panel_a/
239
+ ├── panel.json # Renamed from 01_plot.json
240
+ ├── panel.csv # Renamed from 01_plot.csv
241
+ └── panel.png # Renamed from 01_plot.png
242
+ ```
243
+
244
+ The JSON structure is compatible - canvas.json references the panel data via relative paths with hash verification.
245
+
246
+ ## Summary
247
+
248
+ | Aspect | Description |
249
+ |-------------|------------------------------------|
250
+ | Output | PNG + JSON + CSV per figure |
251
+ | Units | Millimeters for publication |
252
+ | Tracking | All traces tracked with IDs |
253
+ | Metadata | Dimensions, styles, axes info |
254
+ | Data | CSV with column references in JSON |
255
+ | Integration | Direct use as canvas panels |
256
+
257
+ <!-- EOF -->
@@ -22,6 +22,12 @@ from ._figure_from_axes_mm import (
22
22
  )
23
23
  from ._units import inch_to_mm, mm_to_inch, mm_to_pt, pt_to_mm
24
24
  from ._collect_figure_metadata import collect_figure_metadata
25
+ from ._csv_column_naming import (
26
+ get_csv_column_name,
27
+ get_csv_column_prefix,
28
+ parse_csv_column_name,
29
+ sanitize_trace_id,
30
+ )
25
31
 
26
32
  __all__ = [
27
33
  "HistogramBinManager",
@@ -47,6 +53,8 @@ __all__ = [
47
53
  "cross_ref",
48
54
  "enhance_scitex_save_with_captions",
49
55
  "export_captions",
56
+ "get_csv_column_name",
57
+ "get_csv_column_prefix",
50
58
  "get_dimension_info",
51
59
  "get_scitex_config",
52
60
  "histogram_bin_manager",
@@ -57,9 +65,11 @@ __all__ = [
57
65
  "mk_patches",
58
66
  "mm_to_inch",
59
67
  "mm_to_pt",
68
+ "parse_csv_column_name",
60
69
  "print_dimension_info",
61
70
  "pt_to_mm",
62
71
  "quick_caption",
72
+ "sanitize_trace_id",
63
73
  "save_with_caption",
64
74
  "view_dimensions",
65
75
  ]
@@ -349,14 +349,15 @@ def _extract_traces(ax) -> list:
349
349
  and csv_columns mapping
350
350
  """
351
351
  import matplotlib.colors as mcolors
352
+ from ._csv_column_naming import get_csv_column_name, sanitize_trace_id
352
353
 
353
354
  traces = []
354
355
 
355
356
  # Get axes position for CSV column naming
356
- ax_pos = "00" # Default for single axes
357
+ ax_row, ax_col = 0, 0 # Default for single axes
357
358
  if hasattr(ax, '_scitex_metadata') and 'position_in_grid' in ax._scitex_metadata:
358
359
  pos = ax._scitex_metadata['position_in_grid']
359
- ax_pos = f"{pos[0]}{pos[1]}"
360
+ ax_row, ax_col = pos[0], pos[1]
360
361
 
361
362
  for i, line in enumerate(ax.lines):
362
363
  trace = {}
@@ -369,14 +370,16 @@ def _extract_traces(ax) -> list:
369
370
  label = line.get_label()
370
371
 
371
372
  # Determine trace_id for CSV column matching
373
+ # Use index-based ID to match CSV export (single source of truth)
374
+ trace_id_for_csv = None # Will use trace_index in get_csv_column_name
375
+
376
+ # Store display id/label separately
372
377
  if scitex_id:
373
- trace_id = scitex_id
378
+ trace["id"] = scitex_id
374
379
  elif not label.startswith('_'):
375
- trace_id = label
380
+ trace["id"] = label
376
381
  else:
377
- trace_id = f"line_{i}"
378
-
379
- trace["id"] = trace_id
382
+ trace["id"] = f"line_{i}"
380
383
 
381
384
  # Label (for legend) - use label if not internal
382
385
  if not label.startswith('_'):
@@ -404,12 +407,11 @@ def _extract_traces(ax) -> list:
404
407
  trace["marker"] = marker
405
408
  trace["markersize"] = line.get_markersize()
406
409
 
407
- # CSV column mapping - this is how we'll reconstruct from CSV
408
- # Format matches what _export_as_csv generates: ax_{row}{col}_{id}_plot_x/y
409
- # The id should match the id= kwarg passed to ax.plot()
410
+ # CSV column mapping - use single source of truth
411
+ # Uses trace_index to match what _export_as_csv generates
410
412
  trace["csv_columns"] = {
411
- "x": f"ax_{ax_pos}_{trace_id}_plot_x",
412
- "y": f"ax_{ax_pos}_{trace_id}_plot_y",
413
+ "x": get_csv_column_name("plot_x", ax_row, ax_col, trace_index=i),
414
+ "y": get_csv_column_name("plot_y", ax_row, ax_col, trace_index=i),
413
415
  }
414
416
 
415
417
  traces.append(trace)
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: 2025-12-08
4
+ # File: ./src/scitex/plt/utils/_csv_column_naming.py
5
+
6
+ """
7
+ Single source of truth for CSV column naming in scitex.
8
+
9
+ This module ensures consistent column naming between:
10
+ - CSV export (_export_as_csv)
11
+ - JSON metadata (_collect_figure_metadata)
12
+ - GUI editors (reading CSV data back)
13
+
14
+ Column naming convention:
15
+ ax_{row}{col}_{trace_id}_{data_type}
16
+
17
+ Where:
18
+ - row, col: axes position in grid (e.g., "00" for single axes)
19
+ - trace_id: unique identifier for the trace (from label, id kwarg, or index)
20
+ - data_type: type of data (e.g., "plot_x", "plot_y", "hist_bins", etc.)
21
+ """
22
+
23
+ __all__ = [
24
+ 'get_csv_column_name',
25
+ 'get_csv_column_prefix',
26
+ 'parse_csv_column_name',
27
+ 'sanitize_trace_id',
28
+ ]
29
+
30
+
31
+ def sanitize_trace_id(trace_id: str) -> str:
32
+ """Sanitize trace ID for use in CSV column names.
33
+
34
+ Removes or replaces characters that could cause issues in column names.
35
+
36
+ Parameters
37
+ ----------
38
+ trace_id : str
39
+ Raw trace identifier (label, id kwarg, or generated)
40
+
41
+ Returns
42
+ -------
43
+ str
44
+ Sanitized trace ID safe for CSV column names
45
+ """
46
+ if not trace_id:
47
+ return "unnamed"
48
+
49
+ # Replace problematic characters
50
+ sanitized = str(trace_id)
51
+ # Keep alphanumeric, underscore, hyphen; replace others with underscore
52
+ result = []
53
+ for char in sanitized:
54
+ if char.isalnum() or char in ('_', '-'):
55
+ result.append(char)
56
+ elif char in (' ', '(', ')', '[', ']', '{', '}', '/', '\\', '.'):
57
+ result.append('_')
58
+ # Skip other characters
59
+
60
+ sanitized = ''.join(result)
61
+
62
+ # Remove consecutive underscores
63
+ while '__' in sanitized:
64
+ sanitized = sanitized.replace('__', '_')
65
+
66
+ # Remove leading/trailing underscores
67
+ sanitized = sanitized.strip('_')
68
+
69
+ return sanitized if sanitized else "unnamed"
70
+
71
+
72
+ def get_csv_column_prefix(ax_row: int = 0, ax_col: int = 0, trace_id: str = None, trace_index: int = None) -> str:
73
+ """Get CSV column prefix for a trace.
74
+
75
+ Parameters
76
+ ----------
77
+ ax_row : int
78
+ Row position of axes in grid (default: 0)
79
+ ax_col : int
80
+ Column position of axes in grid (default: 0)
81
+ trace_id : str, optional
82
+ Trace identifier (from label or id kwarg). If None, uses trace_index.
83
+ trace_index : int, optional
84
+ Index of trace when no trace_id is provided (default: 0)
85
+
86
+ Returns
87
+ -------
88
+ str
89
+ Column prefix like "ax_00_sin_x_" or "ax_01_plot_0_"
90
+ """
91
+ ax_pos = f"{ax_row}{ax_col}"
92
+
93
+ if trace_id:
94
+ safe_id = sanitize_trace_id(trace_id)
95
+ elif trace_index is not None:
96
+ safe_id = f"plot_{trace_index}"
97
+ else:
98
+ safe_id = "plot_0"
99
+
100
+ return f"ax_{ax_pos}_{safe_id}_"
101
+
102
+
103
+ def get_csv_column_name(
104
+ data_type: str,
105
+ ax_row: int = 0,
106
+ ax_col: int = 0,
107
+ trace_id: str = None,
108
+ trace_index: int = None,
109
+ ) -> str:
110
+ """Get full CSV column name for a data field.
111
+
112
+ Parameters
113
+ ----------
114
+ data_type : str
115
+ Type of data (e.g., "plot_x", "plot_y", "hist_bins", "bar_heights")
116
+ ax_row : int
117
+ Row position of axes in grid (default: 0)
118
+ ax_col : int
119
+ Column position of axes in grid (default: 0)
120
+ trace_id : str, optional
121
+ Trace identifier (from label or id kwarg)
122
+ trace_index : int, optional
123
+ Index of trace when no trace_id is provided
124
+
125
+ Returns
126
+ -------
127
+ str
128
+ Full column name like "ax_00_sin_x_plot_x" or "ax_01_plot_0_plot_y"
129
+
130
+ Examples
131
+ --------
132
+ >>> get_csv_column_name("plot_x", trace_id="sin(x)")
133
+ 'ax_00_sin_x_plot_x'
134
+ >>> get_csv_column_name("plot_y", ax_row=1, ax_col=2, trace_index=0)
135
+ 'ax_12_plot_0_plot_y'
136
+ """
137
+ prefix = get_csv_column_prefix(ax_row, ax_col, trace_id, trace_index)
138
+ return f"{prefix}{data_type}"
139
+
140
+
141
+ def parse_csv_column_name(column_name: str) -> dict:
142
+ """Parse CSV column name to extract components.
143
+
144
+ Parameters
145
+ ----------
146
+ column_name : str
147
+ Full column name (e.g., "ax_00_sin_x_plot_x")
148
+
149
+ Returns
150
+ -------
151
+ dict
152
+ Dictionary with keys:
153
+ - ax_row: int
154
+ - ax_col: int
155
+ - trace_id: str
156
+ - data_type: str
157
+ - valid: bool (True if parsing succeeded)
158
+
159
+ Examples
160
+ --------
161
+ >>> parse_csv_column_name("ax_00_sin_x_plot_x")
162
+ {'ax_row': 0, 'ax_col': 0, 'trace_id': 'sin_x', 'data_type': 'plot_x', 'valid': True}
163
+ """
164
+ result = {
165
+ 'ax_row': 0,
166
+ 'ax_col': 0,
167
+ 'trace_id': '',
168
+ 'data_type': '',
169
+ 'valid': False,
170
+ }
171
+
172
+ if not column_name or not column_name.startswith('ax_'):
173
+ return result
174
+
175
+ parts = column_name.split('_')
176
+ if len(parts) < 4:
177
+ return result
178
+
179
+ try:
180
+ # Parse ax position (e.g., "00" from "ax_00_...")
181
+ ax_pos = parts[1]
182
+ if len(ax_pos) >= 2:
183
+ result['ax_row'] = int(ax_pos[0])
184
+ result['ax_col'] = int(ax_pos[1])
185
+
186
+ # Last two parts are typically data_type (e.g., "plot_x", "hist_bins")
187
+ # Everything in between is the trace_id
188
+ data_type_parts = parts[-2:] # e.g., ["plot", "x"]
189
+ result['data_type'] = '_'.join(data_type_parts)
190
+
191
+ # Trace ID is everything between ax_pos and data_type
192
+ trace_parts = parts[2:-2]
193
+ result['trace_id'] = '_'.join(trace_parts) if trace_parts else 'plot_0'
194
+
195
+ result['valid'] = True
196
+
197
+ except (ValueError, IndexError):
198
+ pass
199
+
200
+ return result
201
+
202
+
203
+ def get_trace_columns_from_df(df, trace_id: str = None, trace_index: int = None, ax_row: int = 0, ax_col: int = 0) -> dict:
204
+ """Find CSV columns for a specific trace in a DataFrame.
205
+
206
+ Parameters
207
+ ----------
208
+ df : pandas.DataFrame
209
+ DataFrame with CSV data
210
+ trace_id : str, optional
211
+ Trace identifier to search for
212
+ trace_index : int, optional
213
+ Trace index to search for (if trace_id not provided)
214
+ ax_row : int
215
+ Row position of axes
216
+ ax_col : int
217
+ Column position of axes
218
+
219
+ Returns
220
+ -------
221
+ dict
222
+ Dictionary mapping data types to column names, e.g.:
223
+ {'plot_x': 'ax_00_sin_x_plot_x', 'plot_y': 'ax_00_sin_x_plot_y'}
224
+ """
225
+ result = {}
226
+ prefix = get_csv_column_prefix(ax_row, ax_col, trace_id, trace_index)
227
+
228
+ for col in df.columns:
229
+ if col.startswith(prefix):
230
+ # Extract data_type from column name
231
+ data_type = col[len(prefix):]
232
+ result[data_type] = col
233
+
234
+ return result
235
+
236
+
237
+ # EOF
@@ -44,9 +44,16 @@ class CitationDatabase:
44
44
  read_only: If True, open in read-only mode (default)
45
45
  """
46
46
  if read_only:
47
- self.conn = sqlite3.connect(f"file:{self.db_path}?mode=ro", uri=True)
47
+ self.conn = sqlite3.connect(
48
+ f"file:{self.db_path}?mode=ro",
49
+ uri=True,
50
+ check_same_thread=False # Allow multi-threaded access (e.g., Django)
51
+ )
48
52
  else:
49
- self.conn = sqlite3.connect(self.db_path)
53
+ self.conn = sqlite3.connect(
54
+ self.db_path,
55
+ check_same_thread=False
56
+ )
50
57
 
51
58
  self.conn.row_factory = sqlite3.Row
52
59
 
@@ -29,8 +29,22 @@ logger = getLogger(__name__)
29
29
 
30
30
 
31
31
  class ScholarConfig:
32
- def __init__(self, config_path: Optional[Union[str, Path]] = None):
32
+ def __init__(
33
+ self,
34
+ config_path: Optional[Union[str, Path]] = None,
35
+ scholar_dir: Optional[Union[str, Path]] = None,
36
+ ):
37
+ """Initialize ScholarConfig.
38
+
39
+ Args:
40
+ config_path: Path to custom config YAML file
41
+ scholar_dir: Direct path to scholar directory (e.g., /data/users/alice/.scitex)
42
+ This bypasses SCITEX_DIR env var for thread-safe multi-user usage.
43
+ Use this in Django/multi-user environments to avoid race conditions.
44
+ """
33
45
  self.name = self.__class__.__name__
46
+ self._explicit_scholar_dir = scholar_dir # Store for thread-safe access
47
+
34
48
  if config_path and Path(config_path).exists():
35
49
  config_data = self.load_yaml(config_path)
36
50
  else:
@@ -114,8 +128,14 @@ class ScholarConfig:
114
128
 
115
129
  # Path Management ----------------------------------------
116
130
  def _setup_path_manager(self, scholar_dir=None):
117
- scholar_dir = self.cascade.resolve("scholar_dir", default="~/.scitex")
118
- base_path = Path(scholar_dir).expanduser() / "scholar"
131
+ # Priority: explicit parameter > env var > config > default
132
+ if self._explicit_scholar_dir:
133
+ # Use explicitly provided path (thread-safe for multi-user)
134
+ base_path = Path(self._explicit_scholar_dir).expanduser() / "scholar"
135
+ else:
136
+ # Fall back to cascade resolution (uses SCITEX_DIR env var)
137
+ scholar_dir = self.cascade.resolve("scholar_dir", default="~/.scitex")
138
+ base_path = Path(scholar_dir).expanduser() / "scholar"
119
139
  self.path_manager = PathManager(scholar_dir=base_path)
120
140
 
121
141
  @property