scitex 2.14.0__py3-none-any.whl → 2.15.3__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/__init__.py +71 -17
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +210 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +27 -0
- scitex/_mcp_tools/template.py +24 -0
- scitex/_mcp_tools/writer.py +17 -210
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
- scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
- scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +129 -61
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/_tts.py +18 -10
- scitex/audio/engines/base.py +17 -10
- scitex/audio/engines/elevenlabs_engine.py +7 -2
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
- scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
- scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
- scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
- scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
- scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
- scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
- scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
- scitex/canvas/editor/flask_editor/_core.py +25 -1684
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +160 -41
- scitex/cli/capture.py +133 -20
- scitex/cli/introspect.py +488 -0
- scitex/cli/main.py +200 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/plt.py +414 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +154 -8
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +355 -0
- scitex/cli/stats.py +136 -11
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +49 -299
- scitex/cloud/__init__.py +41 -2
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +256 -0
- scitex/context/__init__.py +22 -0
- scitex/dev/__init__.py +20 -1
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/gen/__init__.py +50 -14
- scitex/gen/_list_packages.py +4 -4
- scitex/introspect/__init__.py +82 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +41 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
- scitex/introspect/_mcp/__init__.py +41 -0
- scitex/introspect/_mcp/handlers.py +233 -0
- scitex/introspect/_members.py +155 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/_save.py +1 -2
- scitex/io/bundle/README.md +1 -1
- scitex/logging/_formatters.py +19 -9
- scitex/mcp_server.py +98 -5
- scitex/os/__init__.py +4 -0
- scitex/{gen → os}/_check_host.py +4 -5
- scitex/plt/__init__.py +245 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/session/__init__.py +26 -7
- scitex/session/_decorator.py +1 -1
- scitex/sh/README.md +1 -1
- scitex/sh/__init__.py +7 -4
- scitex/social/__init__.py +155 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/stats/_mcp/_handlers/__init__.py +31 -0
- scitex/stats/_mcp/_handlers/_corrections.py +113 -0
- scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
- scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
- scitex/stats/_mcp/_handlers/_format.py +94 -0
- scitex/stats/_mcp/_handlers/_normality.py +110 -0
- scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
- scitex/stats/_mcp/_handlers/_power.py +247 -0
- scitex/stats/_mcp/_handlers/_recommend.py +102 -0
- scitex/stats/_mcp/_handlers/_run_test.py +279 -0
- scitex/stats/_mcp/_handlers/_stars.py +48 -0
- scitex/stats/_mcp/handlers.py +19 -1171
- scitex/stats/auto/_stat_style.py +175 -0
- scitex/stats/auto/_style_definitions.py +411 -0
- scitex/stats/auto/_styles.py +22 -620
- scitex/stats/descriptive/__init__.py +11 -8
- scitex/stats/descriptive/_ci.py +39 -0
- scitex/stats/power/_power.py +15 -4
- scitex/str/__init__.py +2 -1
- scitex/str/_title_case.py +63 -0
- scitex/template/README.md +1 -1
- scitex/template/__init__.py +25 -10
- scitex/template/_code_templates.py +147 -0
- scitex/template/_mcp/handlers.py +81 -0
- scitex/template/_mcp/tool_schemas.py +55 -0
- scitex/template/_templates/__init__.py +51 -0
- scitex/template/_templates/audio.py +233 -0
- scitex/template/_templates/canvas.py +312 -0
- scitex/template/_templates/capture.py +268 -0
- scitex/template/_templates/config.py +43 -0
- scitex/template/_templates/diagram.py +294 -0
- scitex/template/_templates/io.py +107 -0
- scitex/template/_templates/module.py +53 -0
- scitex/template/_templates/plt.py +202 -0
- scitex/template/_templates/scholar.py +267 -0
- scitex/template/_templates/session.py +130 -0
- scitex/template/_templates/session_minimal.py +43 -0
- scitex/template/_templates/session_plot.py +67 -0
- scitex/template/_templates/session_stats.py +77 -0
- scitex/template/_templates/stats.py +323 -0
- scitex/template/_templates/writer.py +296 -0
- scitex/template/clone_writer_directory.py +5 -5
- scitex/ui/_backends/_email.py +10 -2
- scitex/ui/_backends/_webhook.py +5 -1
- scitex/web/_search_pubmed.py +10 -6
- scitex/writer/README.md +1 -1
- scitex/writer/__init__.py +43 -34
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.3.dist-info/METADATA +667 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/gen/_ci.py +0 -12
- scitex/gen/_title_case.py +0 -89
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- /scitex/{gen → context}/_detect_environment.py +0 -0
- /scitex/{gen → context}/_get_notebook_path.py +0 -0
- /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_detect.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Plot type detection from axes content.
|
|
7
|
+
|
|
8
|
+
Detects the primary plot type by analyzing scitex history and matplotlib
|
|
9
|
+
artist content.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Optional, Tuple
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _detect_plot_type(ax) -> Tuple[Optional[str], Optional[str]]:
|
|
16
|
+
"""
|
|
17
|
+
Detect the primary plot type and method from axes content.
|
|
18
|
+
|
|
19
|
+
Checks for:
|
|
20
|
+
- Lines -> "line"
|
|
21
|
+
- Scatter collections -> "scatter"
|
|
22
|
+
- Bar containers -> "bar"
|
|
23
|
+
- Patches (histogram) -> "hist"
|
|
24
|
+
- Box plot -> "boxplot"
|
|
25
|
+
- Violin plot -> "violin"
|
|
26
|
+
- Image -> "image"
|
|
27
|
+
- Contour -> "contour"
|
|
28
|
+
- KDE -> "kde"
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
ax : matplotlib.axes.Axes
|
|
33
|
+
The axes to analyze
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
tuple
|
|
38
|
+
(plot_type, method) where method is the actual plotting function used,
|
|
39
|
+
or (None, None) if unclear
|
|
40
|
+
"""
|
|
41
|
+
# Check scitex history FIRST (most reliable for scitex plots)
|
|
42
|
+
result = _detect_from_history(ax)
|
|
43
|
+
if result[0] is not None:
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
# Check for images (takes priority)
|
|
47
|
+
if len(ax.images) > 0:
|
|
48
|
+
return "image", "imshow"
|
|
49
|
+
|
|
50
|
+
# Check for 2D density plots (hist2d, hexbin)
|
|
51
|
+
result = _detect_2d_density(ax)
|
|
52
|
+
if result[0] is not None:
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
# Check for contours
|
|
56
|
+
result = _detect_contours(ax)
|
|
57
|
+
if result[0] is not None:
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
# Check for bar plots and containers
|
|
61
|
+
result = _detect_bars(ax)
|
|
62
|
+
if result[0] is not None:
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
# Check for patches (histogram, violin, pie)
|
|
66
|
+
result = _detect_patches(ax)
|
|
67
|
+
if result[0] is not None:
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
# Check for scatter plots (PathCollection)
|
|
71
|
+
result = _detect_scatter(ax)
|
|
72
|
+
if result[0] is not None:
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
# Check for line plots
|
|
76
|
+
result = _detect_lines(ax)
|
|
77
|
+
if result[0] is not None:
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
return None, None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _detect_from_history(ax) -> Tuple[Optional[str], Optional[str]]:
|
|
84
|
+
"""Detect plot type from scitex history."""
|
|
85
|
+
if not hasattr(ax, "history") or len(ax.history) == 0:
|
|
86
|
+
return None, None
|
|
87
|
+
|
|
88
|
+
# Method to (plot_type, method) mapping
|
|
89
|
+
method_map = {
|
|
90
|
+
"stx_heatmap": ("heatmap", "stx_heatmap"),
|
|
91
|
+
"stx_kde": ("kde", "stx_kde"),
|
|
92
|
+
"stx_ecdf": ("ecdf", "stx_ecdf"),
|
|
93
|
+
"stx_violin": ("violin", "stx_violin"),
|
|
94
|
+
"stx_box": ("boxplot", "stx_box"),
|
|
95
|
+
"boxplot": ("boxplot", "boxplot"),
|
|
96
|
+
"stx_line": ("line", "stx_line"),
|
|
97
|
+
"plot_scatter": ("scatter", "plot_scatter"),
|
|
98
|
+
"stx_mean_std": ("line", "stx_mean_std"),
|
|
99
|
+
"stx_mean_ci": ("line", "stx_mean_ci"),
|
|
100
|
+
"stx_median_iqr": ("line", "stx_median_iqr"),
|
|
101
|
+
"stx_shaded_line": ("line", "stx_shaded_line"),
|
|
102
|
+
"sns_boxplot": ("boxplot", "sns_boxplot"),
|
|
103
|
+
"sns_violinplot": ("violin", "sns_violinplot"),
|
|
104
|
+
"sns_scatterplot": ("scatter", "sns_scatterplot"),
|
|
105
|
+
"sns_lineplot": ("line", "sns_lineplot"),
|
|
106
|
+
"sns_histplot": ("hist", "sns_histplot"),
|
|
107
|
+
"sns_barplot": ("bar", "sns_barplot"),
|
|
108
|
+
"sns_stripplot": ("scatter", "sns_stripplot"),
|
|
109
|
+
"sns_kdeplot": ("kde", "sns_kdeplot"),
|
|
110
|
+
"scatter": ("scatter", "scatter"),
|
|
111
|
+
"bar": ("bar", "bar"),
|
|
112
|
+
"barh": ("bar", "barh"),
|
|
113
|
+
"hist": ("hist", "hist"),
|
|
114
|
+
"hist2d": ("hist2d", "hist2d"),
|
|
115
|
+
"hexbin": ("hexbin", "hexbin"),
|
|
116
|
+
"violinplot": ("violin", "violinplot"),
|
|
117
|
+
"errorbar": ("errorbar", "errorbar"),
|
|
118
|
+
"fill_between": ("fill", "fill_between"),
|
|
119
|
+
"fill_betweenx": ("fill", "fill_betweenx"),
|
|
120
|
+
"imshow": ("image", "imshow"),
|
|
121
|
+
"matshow": ("image", "matshow"),
|
|
122
|
+
"contour": ("contour", "contour"),
|
|
123
|
+
"contourf": ("contour", "contourf"),
|
|
124
|
+
"stem": ("stem", "stem"),
|
|
125
|
+
"step": ("step", "step"),
|
|
126
|
+
"pie": ("pie", "pie"),
|
|
127
|
+
"quiver": ("quiver", "quiver"),
|
|
128
|
+
"streamplot": ("stream", "streamplot"),
|
|
129
|
+
"plot": ("line", "plot"),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Get all methods from history
|
|
133
|
+
for record in ax.history.values():
|
|
134
|
+
if isinstance(record, tuple) and len(record) >= 2:
|
|
135
|
+
method = record[1]
|
|
136
|
+
if method in method_map:
|
|
137
|
+
return method_map[method]
|
|
138
|
+
|
|
139
|
+
return None, None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _detect_2d_density(ax) -> Tuple[Optional[str], Optional[str]]:
|
|
143
|
+
"""Detect 2D density plots (hist2d, hexbin)."""
|
|
144
|
+
if not hasattr(ax, "collections"):
|
|
145
|
+
return None, None
|
|
146
|
+
|
|
147
|
+
for coll in ax.collections:
|
|
148
|
+
coll_type = type(coll).__name__
|
|
149
|
+
if "QuadMesh" in coll_type:
|
|
150
|
+
return "hist2d", "hist2d"
|
|
151
|
+
if "PolyCollection" in coll_type and hasattr(coll, "get_array"):
|
|
152
|
+
arr = coll.get_array()
|
|
153
|
+
if arr is not None and len(arr) > 0:
|
|
154
|
+
return "hexbin", "hexbin"
|
|
155
|
+
|
|
156
|
+
return None, None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _detect_contours(ax) -> Tuple[Optional[str], Optional[str]]:
|
|
160
|
+
"""Detect contour plots."""
|
|
161
|
+
if not hasattr(ax, "collections"):
|
|
162
|
+
return None, None
|
|
163
|
+
|
|
164
|
+
for coll in ax.collections:
|
|
165
|
+
if "Contour" in type(coll).__name__:
|
|
166
|
+
return "contour", "contour"
|
|
167
|
+
|
|
168
|
+
return None, None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _detect_bars(ax) -> Tuple[Optional[str], Optional[str]]:
|
|
172
|
+
"""Detect bar and boxplots from containers."""
|
|
173
|
+
if len(ax.containers) == 0:
|
|
174
|
+
return None, None
|
|
175
|
+
|
|
176
|
+
if any("boxplot" in str(type(c)).lower() for c in ax.containers):
|
|
177
|
+
return "boxplot", "boxplot"
|
|
178
|
+
|
|
179
|
+
return "bar", "bar"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _detect_patches(ax) -> Tuple[Optional[str], Optional[str]]:
|
|
183
|
+
"""Detect histogram, violin, pie from patches."""
|
|
184
|
+
if len(ax.patches) == 0:
|
|
185
|
+
return None, None
|
|
186
|
+
|
|
187
|
+
# Check for pie chart (Wedge patches)
|
|
188
|
+
if any("Wedge" in type(p).__name__ for p in ax.patches):
|
|
189
|
+
return "pie", "pie"
|
|
190
|
+
|
|
191
|
+
# If there are many rectangular patches, likely histogram
|
|
192
|
+
if len(ax.patches) > 5:
|
|
193
|
+
return "hist", "hist"
|
|
194
|
+
|
|
195
|
+
# Check for violin plot
|
|
196
|
+
if any("Poly" in type(p).__name__ for p in ax.patches):
|
|
197
|
+
return "violin", "violinplot"
|
|
198
|
+
|
|
199
|
+
return None, None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _detect_scatter(ax) -> Tuple[Optional[str], Optional[str]]:
|
|
203
|
+
"""Detect scatter plots from PathCollection."""
|
|
204
|
+
if not hasattr(ax, "collections") or len(ax.collections) == 0:
|
|
205
|
+
return None, None
|
|
206
|
+
|
|
207
|
+
for coll in ax.collections:
|
|
208
|
+
if "PathCollection" in type(coll).__name__:
|
|
209
|
+
return "scatter", "scatter"
|
|
210
|
+
|
|
211
|
+
return None, None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _detect_lines(ax) -> Tuple[Optional[str], Optional[str]]:
|
|
215
|
+
"""Detect line and errorbar plots."""
|
|
216
|
+
if len(ax.lines) == 0:
|
|
217
|
+
return None, None
|
|
218
|
+
|
|
219
|
+
if any(hasattr(line, "_mpl_error") for line in ax.lines):
|
|
220
|
+
return "errorbar", "errorbar"
|
|
221
|
+
|
|
222
|
+
return "line", "plot"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# EOF
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_legend.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Legend extraction utilities for figure metadata.
|
|
7
|
+
|
|
8
|
+
Extracts legend information including handles and artist references.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _extract_legend_info(ax) -> Optional[dict]:
|
|
15
|
+
"""
|
|
16
|
+
Extract legend information from axes.
|
|
17
|
+
|
|
18
|
+
Uses matplotlib terminology for legend properties.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
ax : matplotlib.axes.Axes
|
|
23
|
+
The axes to extract legend from
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
dict or None
|
|
28
|
+
Legend info dictionary with matplotlib properties, or None if no legend
|
|
29
|
+
"""
|
|
30
|
+
legend = ax.get_legend()
|
|
31
|
+
if legend is None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
legend_info = {
|
|
35
|
+
"visible": legend.get_visible(),
|
|
36
|
+
"loc": legend._loc if hasattr(legend, "_loc") else "best",
|
|
37
|
+
"frameon": legend.get_frame_on() if hasattr(legend, "get_frame_on") else True,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# ncol - number of columns
|
|
41
|
+
if hasattr(legend, "_ncols"):
|
|
42
|
+
legend_info["ncol"] = legend._ncols
|
|
43
|
+
elif hasattr(legend, "_ncol"):
|
|
44
|
+
legend_info["ncol"] = legend._ncol
|
|
45
|
+
|
|
46
|
+
# Extract legend handles with artist references
|
|
47
|
+
handles = _extract_legend_handles(ax, legend)
|
|
48
|
+
if handles:
|
|
49
|
+
legend_info["handles"] = handles
|
|
50
|
+
|
|
51
|
+
return legend_info
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_legend_handles(ax, legend) -> list:
|
|
55
|
+
"""Extract legend handles with artist references."""
|
|
56
|
+
handles = []
|
|
57
|
+
texts = legend.get_texts()
|
|
58
|
+
legend_handles = legend.legend_handles if hasattr(legend, "legend_handles") else []
|
|
59
|
+
|
|
60
|
+
# Get the raw matplotlib axes for accessing lines to match IDs
|
|
61
|
+
mpl_ax = ax._axis_mpl if hasattr(ax, "_axis_mpl") else ax
|
|
62
|
+
|
|
63
|
+
for i, text in enumerate(texts):
|
|
64
|
+
label_text = text.get_text()
|
|
65
|
+
handle_entry = {"label": label_text}
|
|
66
|
+
|
|
67
|
+
# Try to get artist_id from corresponding handle
|
|
68
|
+
artist_id = None
|
|
69
|
+
if i < len(legend_handles):
|
|
70
|
+
handle = legend_handles[i]
|
|
71
|
+
if hasattr(handle, "_scitex_id"):
|
|
72
|
+
artist_id = handle._scitex_id
|
|
73
|
+
|
|
74
|
+
# Fallback: find matching artist by label in axes artists
|
|
75
|
+
if artist_id is None:
|
|
76
|
+
artist_id = _find_artist_id_by_label(mpl_ax, label_text)
|
|
77
|
+
|
|
78
|
+
if artist_id:
|
|
79
|
+
handle_entry["artist_id"] = artist_id
|
|
80
|
+
|
|
81
|
+
handles.append(handle_entry)
|
|
82
|
+
|
|
83
|
+
return handles
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _find_artist_id_by_label(mpl_ax, label_text: str) -> Optional[str]:
|
|
87
|
+
"""Find artist ID by matching label in axes artists."""
|
|
88
|
+
# Check lines
|
|
89
|
+
for line in mpl_ax.lines:
|
|
90
|
+
line_label = line.get_label()
|
|
91
|
+
if line_label == label_text:
|
|
92
|
+
if hasattr(line, "_scitex_id"):
|
|
93
|
+
return line._scitex_id
|
|
94
|
+
elif not line_label.startswith("_"):
|
|
95
|
+
return line_label
|
|
96
|
+
|
|
97
|
+
# Check collections (scatter)
|
|
98
|
+
for coll in mpl_ax.collections:
|
|
99
|
+
coll_label = coll.get_label() if hasattr(coll, "get_label") else ""
|
|
100
|
+
if coll_label == label_text:
|
|
101
|
+
if hasattr(coll, "_scitex_id"):
|
|
102
|
+
return coll._scitex_id
|
|
103
|
+
elif coll_label and not coll_label.startswith("_"):
|
|
104
|
+
return coll_label
|
|
105
|
+
|
|
106
|
+
# Check patches (bar/hist/pie)
|
|
107
|
+
for patch in mpl_ax.patches:
|
|
108
|
+
patch_label = patch.get_label() if hasattr(patch, "get_label") else ""
|
|
109
|
+
if patch_label == label_text:
|
|
110
|
+
if hasattr(patch, "_scitex_id"):
|
|
111
|
+
return patch._scitex_id
|
|
112
|
+
elif patch_label and not patch_label.startswith("_"):
|
|
113
|
+
return patch_label
|
|
114
|
+
|
|
115
|
+
# Check images (imshow)
|
|
116
|
+
for img in mpl_ax.images:
|
|
117
|
+
img_label = img.get_label() if hasattr(img, "get_label") else ""
|
|
118
|
+
if img_label == label_text:
|
|
119
|
+
if hasattr(img, "_scitex_id"):
|
|
120
|
+
return img._scitex_id
|
|
121
|
+
elif img_label and not img_label.startswith("_"):
|
|
122
|
+
return img_label
|
|
123
|
+
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# EOF
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_rounding.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Rounding utilities for figure metadata.
|
|
7
|
+
|
|
8
|
+
Provides precision-controlled rounding for various measurement types
|
|
9
|
+
(mm, inch, position, etc.) to ensure consistent JSON output.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import List, Union
|
|
13
|
+
|
|
14
|
+
# Precision settings for JSON output
|
|
15
|
+
PRECISION = {
|
|
16
|
+
"mm": 2, # Millimeters: 0.01mm precision (10 microns)
|
|
17
|
+
"inch": 3, # Inches: 0.001 inch precision
|
|
18
|
+
"position": 3, # Normalized position: 0.001 precision
|
|
19
|
+
"lim": 2, # Axis limits: 2 decimal places
|
|
20
|
+
"linewidth": 2, # Line widths: 0.01 precision
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FixedFloat:
|
|
25
|
+
"""
|
|
26
|
+
A float wrapper that preserves fixed decimal places in JSON output.
|
|
27
|
+
|
|
28
|
+
Example: FixedFloat(0.25, 3) -> "0.250" in JSON
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, value: float, precision: int):
|
|
32
|
+
self.value = round(value, precision)
|
|
33
|
+
self.precision = precision
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
return f"{self.value:.{self.precision}f}"
|
|
37
|
+
|
|
38
|
+
def __float__(self):
|
|
39
|
+
return self.value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _round_value(
|
|
43
|
+
value: Union[float, int], precision: int, fixed: bool = False
|
|
44
|
+
) -> Union[float, int, "FixedFloat"]:
|
|
45
|
+
"""
|
|
46
|
+
Round a single value to specified precision.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
value : float or int
|
|
51
|
+
Value to round
|
|
52
|
+
precision : int
|
|
53
|
+
Number of decimal places
|
|
54
|
+
fixed : bool
|
|
55
|
+
If True, return FixedFloat with fixed decimal places (e.g., 0.250)
|
|
56
|
+
If False, return float (e.g., 0.25)
|
|
57
|
+
"""
|
|
58
|
+
if isinstance(value, int):
|
|
59
|
+
if fixed:
|
|
60
|
+
return FixedFloat(float(value), precision)
|
|
61
|
+
return value
|
|
62
|
+
if isinstance(value, float):
|
|
63
|
+
if fixed:
|
|
64
|
+
return FixedFloat(value, precision)
|
|
65
|
+
return round(value, precision)
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _round_list(values: List, precision: int, fixed: bool = False) -> List:
|
|
70
|
+
"""Round all values in a list."""
|
|
71
|
+
return [_round_value(v, precision, fixed) for v in values]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _round_dict(d: dict, precision_map: dict = None) -> dict:
|
|
75
|
+
"""
|
|
76
|
+
Round all float values in a dict based on key-specific precision.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
d : dict
|
|
81
|
+
Dictionary to process
|
|
82
|
+
precision_map : dict, optional
|
|
83
|
+
Mapping of key patterns to precision values.
|
|
84
|
+
Default uses PRECISION settings based on key names.
|
|
85
|
+
"""
|
|
86
|
+
if precision_map is None:
|
|
87
|
+
precision_map = {}
|
|
88
|
+
|
|
89
|
+
result = {}
|
|
90
|
+
for key, value in d.items():
|
|
91
|
+
# Determine precision based on key name
|
|
92
|
+
if "mm" in key.lower():
|
|
93
|
+
prec = PRECISION["mm"]
|
|
94
|
+
elif "inch" in key.lower():
|
|
95
|
+
prec = PRECISION["inch"]
|
|
96
|
+
elif "position" in key.lower() or key in ("left", "bottom", "right", "top"):
|
|
97
|
+
prec = PRECISION["position"]
|
|
98
|
+
elif "lim" in key.lower():
|
|
99
|
+
prec = PRECISION["lim"]
|
|
100
|
+
elif "width" in key.lower() and "line" in key.lower():
|
|
101
|
+
prec = PRECISION["linewidth"]
|
|
102
|
+
else:
|
|
103
|
+
prec = precision_map.get(key, 3) # Default 3 decimals
|
|
104
|
+
|
|
105
|
+
if isinstance(value, dict):
|
|
106
|
+
result[key] = _round_dict(value, precision_map)
|
|
107
|
+
elif isinstance(value, list):
|
|
108
|
+
result[key] = _round_list(value, prec)
|
|
109
|
+
elif isinstance(value, float):
|
|
110
|
+
result[key] = _round_value(value, prec)
|
|
111
|
+
else:
|
|
112
|
+
result[key] = value
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# EOF
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_verification.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
CSV/JSON consistency verification for figure metadata.
|
|
7
|
+
|
|
8
|
+
Provides functions to verify that exported CSV data matches
|
|
9
|
+
the JSON metadata declarations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def assert_csv_json_consistency(csv_path: str, json_path: str = None) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Assert that CSV data file and its JSON metadata are consistent.
|
|
17
|
+
|
|
18
|
+
Raises AssertionError if the column names don't match.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
csv_path : str
|
|
23
|
+
Path to the CSV data file
|
|
24
|
+
json_path : str, optional
|
|
25
|
+
Path to the JSON metadata file. If not provided, assumes
|
|
26
|
+
the JSON is at the same location with .json extension.
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
AssertionError
|
|
31
|
+
If CSV and JSON column names don't match
|
|
32
|
+
FileNotFoundError
|
|
33
|
+
If CSV or JSON files don't exist
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
>>> assert_csv_json_consistency('/tmp/plot.csv') # Passes silently if valid
|
|
38
|
+
>>> # Or use in tests:
|
|
39
|
+
>>> try:
|
|
40
|
+
... assert_csv_json_consistency('/tmp/plot.csv')
|
|
41
|
+
... except AssertionError as e:
|
|
42
|
+
... print(f"Validation failed: {e}")
|
|
43
|
+
"""
|
|
44
|
+
result = verify_csv_json_consistency(csv_path, json_path)
|
|
45
|
+
|
|
46
|
+
if result["errors"]:
|
|
47
|
+
raise FileNotFoundError("\n".join(result["errors"]))
|
|
48
|
+
|
|
49
|
+
if not result["valid"]:
|
|
50
|
+
msg_parts = ["CSV/JSON consistency check failed:"]
|
|
51
|
+
if result["missing_in_csv"]:
|
|
52
|
+
msg_parts.append(
|
|
53
|
+
f" columns_actual missing in CSV: {result['missing_in_csv']}"
|
|
54
|
+
)
|
|
55
|
+
if result["extra_in_csv"]:
|
|
56
|
+
msg_parts.append(f" Extra columns in CSV: {result['extra_in_csv']}")
|
|
57
|
+
if result.get("data_ref_missing"):
|
|
58
|
+
msg_parts.append(
|
|
59
|
+
f" data_ref columns missing in CSV: {result['data_ref_missing']}"
|
|
60
|
+
)
|
|
61
|
+
raise AssertionError("\n".join(msg_parts))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def verify_csv_json_consistency(csv_path: str, json_path: str = None) -> dict:
|
|
65
|
+
"""
|
|
66
|
+
Verify consistency between CSV data file and its JSON metadata.
|
|
67
|
+
|
|
68
|
+
This function checks that:
|
|
69
|
+
1. Column names in the CSV file match those declared in JSON's columns_actual
|
|
70
|
+
2. Artist data_ref values in JSON match actual CSV column names
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
csv_path : str
|
|
75
|
+
Path to the CSV data file
|
|
76
|
+
json_path : str, optional
|
|
77
|
+
Path to the JSON metadata file. If not provided, assumes
|
|
78
|
+
the JSON is at the same location with .json extension.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
dict
|
|
83
|
+
Verification result with keys:
|
|
84
|
+
- 'valid': bool - True if CSV and JSON are consistent
|
|
85
|
+
- 'csv_columns': list - Column names found in CSV
|
|
86
|
+
- 'json_columns': list - Column names declared in JSON
|
|
87
|
+
- 'data_ref_columns': list - Column names from artist data_ref
|
|
88
|
+
- 'missing_in_csv': list - Columns in JSON but not in CSV
|
|
89
|
+
- 'extra_in_csv': list - Columns in CSV but not in JSON
|
|
90
|
+
- 'data_ref_missing': list - data_ref columns not found in CSV
|
|
91
|
+
- 'errors': list - Any error messages
|
|
92
|
+
|
|
93
|
+
Examples
|
|
94
|
+
--------
|
|
95
|
+
>>> result = verify_csv_json_consistency('/tmp/plot.csv')
|
|
96
|
+
>>> print(result['valid'])
|
|
97
|
+
True
|
|
98
|
+
>>> print(result['missing_in_csv'])
|
|
99
|
+
[]
|
|
100
|
+
"""
|
|
101
|
+
import json
|
|
102
|
+
import os
|
|
103
|
+
|
|
104
|
+
import pandas as pd
|
|
105
|
+
|
|
106
|
+
result = {
|
|
107
|
+
"valid": False,
|
|
108
|
+
"csv_columns": [],
|
|
109
|
+
"json_columns": [],
|
|
110
|
+
"data_ref_columns": [],
|
|
111
|
+
"missing_in_csv": [],
|
|
112
|
+
"extra_in_csv": [],
|
|
113
|
+
"data_ref_missing": [],
|
|
114
|
+
"errors": [],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Determine JSON path
|
|
118
|
+
if json_path is None:
|
|
119
|
+
base, _ = os.path.splitext(csv_path)
|
|
120
|
+
json_path = base + ".json"
|
|
121
|
+
|
|
122
|
+
# Check files exist
|
|
123
|
+
if not os.path.exists(csv_path):
|
|
124
|
+
result["errors"].append(f"CSV file not found: {csv_path}")
|
|
125
|
+
return result
|
|
126
|
+
if not os.path.exists(json_path):
|
|
127
|
+
result["errors"].append(f"JSON file not found: {json_path}")
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
# Read CSV columns
|
|
132
|
+
df = pd.read_csv(csv_path, nrows=0) # Just read header
|
|
133
|
+
csv_columns = list(df.columns)
|
|
134
|
+
result["csv_columns"] = csv_columns
|
|
135
|
+
except Exception as e:
|
|
136
|
+
result["errors"].append(f"Error reading CSV: {e}")
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Read JSON metadata
|
|
141
|
+
with open(json_path) as f:
|
|
142
|
+
metadata = json.load(f)
|
|
143
|
+
|
|
144
|
+
# Get columns_actual from data section
|
|
145
|
+
json_columns = []
|
|
146
|
+
if "data" in metadata and "columns_actual" in metadata["data"]:
|
|
147
|
+
json_columns = metadata["data"]["columns_actual"]
|
|
148
|
+
result["json_columns"] = json_columns
|
|
149
|
+
|
|
150
|
+
# Extract data_ref columns from artists
|
|
151
|
+
data_ref_columns = _extract_data_ref_columns(metadata)
|
|
152
|
+
result["data_ref_columns"] = data_ref_columns
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
result["errors"].append(f"Error reading JSON: {e}")
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
# Compare columns_actual with CSV
|
|
159
|
+
csv_set = set(csv_columns)
|
|
160
|
+
json_set = set(json_columns)
|
|
161
|
+
|
|
162
|
+
result["missing_in_csv"] = list(json_set - csv_set)
|
|
163
|
+
result["extra_in_csv"] = list(csv_set - json_set)
|
|
164
|
+
|
|
165
|
+
# Check data_ref columns exist in CSV (if there are any)
|
|
166
|
+
if data_ref_columns:
|
|
167
|
+
data_ref_set = set(data_ref_columns)
|
|
168
|
+
result["data_ref_missing"] = list(data_ref_set - csv_set)
|
|
169
|
+
|
|
170
|
+
# Valid only if columns_actual matches AND data_ref columns are found
|
|
171
|
+
result["valid"] = (
|
|
172
|
+
len(result["missing_in_csv"]) == 0
|
|
173
|
+
and len(result["extra_in_csv"]) == 0
|
|
174
|
+
and len(result["data_ref_missing"]) == 0
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _extract_data_ref_columns(metadata: dict) -> list:
|
|
181
|
+
"""
|
|
182
|
+
Extract data_ref column names from metadata.
|
|
183
|
+
|
|
184
|
+
Skip 'derived_from' key as it contains descriptive text, not CSV column names.
|
|
185
|
+
Also skip 'row_index' as it's a numeric index, not a column name.
|
|
186
|
+
"""
|
|
187
|
+
data_ref_columns = []
|
|
188
|
+
skip_keys = {"derived_from", "row_index"}
|
|
189
|
+
|
|
190
|
+
if "axes" in metadata:
|
|
191
|
+
for ax_key, ax_data in metadata["axes"].items():
|
|
192
|
+
if "artists" in ax_data:
|
|
193
|
+
for artist in ax_data["artists"]:
|
|
194
|
+
if "data_ref" in artist:
|
|
195
|
+
for key, val in artist["data_ref"].items():
|
|
196
|
+
if key not in skip_keys and isinstance(val, str):
|
|
197
|
+
data_ref_columns.append(val)
|
|
198
|
+
|
|
199
|
+
return data_ref_columns
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# EOF
|
scitex/schema/README.md
CHANGED
scitex/scholar/__init__.py
CHANGED
|
@@ -71,6 +71,12 @@ try:
|
|
|
71
71
|
except ImportError:
|
|
72
72
|
utils = None
|
|
73
73
|
|
|
74
|
+
# CrossRef integration via crossref-local delegation (branded as crossref-scitex)
|
|
75
|
+
try:
|
|
76
|
+
from . import crossref_scitex
|
|
77
|
+
except ImportError:
|
|
78
|
+
crossref_scitex = None
|
|
79
|
+
|
|
74
80
|
__all__ = [
|
|
75
81
|
"ScholarConfig",
|
|
76
82
|
"ScholarEngine",
|
|
@@ -81,6 +87,8 @@ __all__ = [
|
|
|
81
87
|
"Papers",
|
|
82
88
|
"Scholar",
|
|
83
89
|
"utils",
|
|
90
|
+
# CrossRef integration (167M+ papers via crossref-local)
|
|
91
|
+
"crossref_scitex",
|
|
84
92
|
]
|
|
85
93
|
|
|
86
94
|
# # Import core classes for advanced users
|