scitex 2.14.0__py3-none-any.whl → 2.15.1__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 +47 -0
- 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 +191 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/writer.py +21 -204
- 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/audio/README.md +40 -36
- scitex/audio/__init__.py +127 -59
- 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/engines/elevenlabs_engine.py +6 -1
- 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/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +443 -0
- scitex/cli/main.py +198 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/scholar/__init__.py +8 -0
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/writer.py +117 -0
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +191 -0
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/introspect/__init__.py +75 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +42 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/introspect/_mcp/__init__.py +37 -0
- scitex/introspect/_mcp/handlers.py +208 -0
- scitex/introspect/_members.py +151 -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/bundle/README.md +1 -1
- scitex/mcp_server.py +98 -5
- scitex/plt/__init__.py +248 -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/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/sh/README.md +1 -1
- scitex/social/__init__.py +153 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/template/README.md +1 -1
- scitex/template/clone_writer_directory.py +5 -5
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.1.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- 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/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/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- 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/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/core/_mixins/_project_handlers.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Project handler mixin for Scholar class.
|
|
7
|
+
|
|
8
|
+
Provides project management functionality.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import shutil
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from scitex import logging
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProjectHandlerMixin:
|
|
25
|
+
"""Mixin providing project management methods."""
|
|
26
|
+
|
|
27
|
+
def _ensure_project_exists(
|
|
28
|
+
self, project: str, description: Optional[str] = None
|
|
29
|
+
) -> Path:
|
|
30
|
+
"""Ensure project directory exists, create if needed (PRIVATE).
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
project: Project name
|
|
34
|
+
description: Optional project description
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
Path to the project directory
|
|
39
|
+
"""
|
|
40
|
+
project_dir = self.config.get_library_project_dir(project)
|
|
41
|
+
info_dir = project_dir / "info"
|
|
42
|
+
|
|
43
|
+
if not project_dir.exists():
|
|
44
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
logger.info(f"{self.name}: Auto-created project directory: {project}")
|
|
46
|
+
|
|
47
|
+
info_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
old_metadata_file = project_dir / "project_metadata.json"
|
|
50
|
+
metadata_file = info_dir / "project_metadata.json"
|
|
51
|
+
|
|
52
|
+
if old_metadata_file.exists() and not metadata_file.exists():
|
|
53
|
+
shutil.move(str(old_metadata_file), str(metadata_file))
|
|
54
|
+
logger.info(f"{self.name}: Moved project metadata to info directory")
|
|
55
|
+
|
|
56
|
+
if not metadata_file.exists():
|
|
57
|
+
metadata = {
|
|
58
|
+
"name": project,
|
|
59
|
+
"description": description or f"Papers for {project} project",
|
|
60
|
+
"created": datetime.now().isoformat(),
|
|
61
|
+
"created_by": "SciTeX Scholar",
|
|
62
|
+
"auto_created": True,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
with open(metadata_file, "w") as f:
|
|
66
|
+
json.dump(metadata, f, indent=2)
|
|
67
|
+
|
|
68
|
+
logger.info(
|
|
69
|
+
f"{self.name}: Created project metadata in info directory: {project}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return project_dir
|
|
73
|
+
|
|
74
|
+
def _create_project_metadata(
|
|
75
|
+
self, project: str, description: Optional[str] = None
|
|
76
|
+
) -> Path:
|
|
77
|
+
"""Create project directory and metadata (PRIVATE).
|
|
78
|
+
|
|
79
|
+
DEPRECATED: Use _ensure_project_exists instead.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
project: Project name
|
|
83
|
+
description: Optional project description
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
Path to the created project directory
|
|
88
|
+
"""
|
|
89
|
+
return self._ensure_project_exists(project, description)
|
|
90
|
+
|
|
91
|
+
def list_projects(self) -> List[Dict[str, Any]]:
|
|
92
|
+
"""List all projects in the Scholar library.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
List of project information dictionaries
|
|
97
|
+
"""
|
|
98
|
+
library_dir = self.config.path_manager.library_dir
|
|
99
|
+
projects = []
|
|
100
|
+
|
|
101
|
+
for item in library_dir.iterdir():
|
|
102
|
+
if item.is_dir() and item.name != "MASTER":
|
|
103
|
+
project_info = {
|
|
104
|
+
"name": item.name,
|
|
105
|
+
"path": str(item),
|
|
106
|
+
"papers_count": len(list(item.glob("*"))),
|
|
107
|
+
"created": None,
|
|
108
|
+
"description": None,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
metadata_file = item / "project_metadata.json"
|
|
112
|
+
if metadata_file.exists():
|
|
113
|
+
try:
|
|
114
|
+
with open(metadata_file) as f:
|
|
115
|
+
metadata = json.load(f)
|
|
116
|
+
project_info.update(metadata)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.debug(f"Failed to load metadata for {item.name}: {e}")
|
|
119
|
+
|
|
120
|
+
projects.append(project_info)
|
|
121
|
+
|
|
122
|
+
return sorted(projects, key=lambda x: x["name"])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# EOF
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/core/_mixins/_savers.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Saver mixin for Scholar class.
|
|
7
|
+
|
|
8
|
+
Provides methods for saving papers to library and BibTeX format.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, List, Optional, Union
|
|
15
|
+
|
|
16
|
+
from scitex import logging
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..Papers import Papers
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SaverMixin:
|
|
25
|
+
"""Mixin providing paper saving methods."""
|
|
26
|
+
|
|
27
|
+
def save_papers_to_library(self, papers: Papers) -> List[str]:
|
|
28
|
+
"""Save papers collection to library.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
papers: Papers collection to save
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
List of paper IDs saved
|
|
36
|
+
"""
|
|
37
|
+
saved_ids = []
|
|
38
|
+
for paper in papers:
|
|
39
|
+
try:
|
|
40
|
+
paper_id = self._library.save_paper(paper)
|
|
41
|
+
saved_ids.append(paper_id)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.warning(f"{self.name}: Failed to save paper: {e}")
|
|
44
|
+
|
|
45
|
+
logger.info(
|
|
46
|
+
f"{self.name}: Saved {len(saved_ids)}/{len(papers)} papers to library"
|
|
47
|
+
)
|
|
48
|
+
return saved_ids
|
|
49
|
+
|
|
50
|
+
def save_papers_as_bibtex(
|
|
51
|
+
self, papers: Papers, output_path: Optional[Union[str, Path]] = None
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Save papers to BibTeX format with enrichment metadata.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
papers: Papers collection to save
|
|
57
|
+
output_path: Optional path to save the BibTeX file
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
BibTeX content as string with enrichment metadata included
|
|
62
|
+
"""
|
|
63
|
+
from ..storage.BibTeXHandler import BibTeXHandler
|
|
64
|
+
|
|
65
|
+
bibtex_handler = BibTeXHandler(project=self.project, config=self.config)
|
|
66
|
+
return bibtex_handler.papers_to_bibtex(papers, output_path)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# EOF
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/core/_mixins/_search.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Search mixin for Scholar class.
|
|
7
|
+
|
|
8
|
+
Provides search functionality for local library and across projects.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
14
|
+
|
|
15
|
+
from scitex import logging
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..Papers import Papers
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SearchMixin:
|
|
24
|
+
"""Mixin providing search methods."""
|
|
25
|
+
|
|
26
|
+
def search_library(self, query: str, project: Optional[str] = None) -> Papers:
|
|
27
|
+
"""Search papers in local library.
|
|
28
|
+
|
|
29
|
+
For new literature search (not in library), use AI2 Scholar QA:
|
|
30
|
+
https://scholarqa.allen.ai/chat/ then process with:
|
|
31
|
+
papers = scholar.load_bibtex('file.bib') followed by scholar.enrich(papers)
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
query: Search query
|
|
35
|
+
project: Project filter (uses self.project if None)
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
Papers collection matching the query
|
|
40
|
+
"""
|
|
41
|
+
from ..Papers import Papers
|
|
42
|
+
|
|
43
|
+
logger.info(f"{self.name}: Searching library for: {query}")
|
|
44
|
+
return Papers([], project=project or self.project)
|
|
45
|
+
|
|
46
|
+
def search_across_projects(
|
|
47
|
+
self, query: str, projects: Optional[List[str]] = None
|
|
48
|
+
) -> Papers:
|
|
49
|
+
"""Search for papers across multiple projects or the entire library.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
query: Search query
|
|
53
|
+
projects: List of project names to search (None for all)
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
Papers collection with search results
|
|
58
|
+
"""
|
|
59
|
+
from ..Paper import Paper
|
|
60
|
+
from ..Papers import Papers
|
|
61
|
+
|
|
62
|
+
if projects is None:
|
|
63
|
+
all_projects = [p["name"] for p in self.list_projects()]
|
|
64
|
+
else:
|
|
65
|
+
all_projects = projects
|
|
66
|
+
|
|
67
|
+
all_papers = []
|
|
68
|
+
for project in all_projects:
|
|
69
|
+
try:
|
|
70
|
+
project_dir = self.config.get_library_project_dir(project)
|
|
71
|
+
for item in project_dir.iterdir():
|
|
72
|
+
if item.is_symlink() or item.is_dir():
|
|
73
|
+
paper_dir = item.resolve() if item.is_symlink() else item
|
|
74
|
+
metadata_file = paper_dir / "metadata.json"
|
|
75
|
+
if metadata_file.exists():
|
|
76
|
+
try:
|
|
77
|
+
paper = Paper.model_validate_json(
|
|
78
|
+
metadata_file.read_text()
|
|
79
|
+
)
|
|
80
|
+
query_lower = query.lower()
|
|
81
|
+
title = (paper.metadata.basic.title or "").lower()
|
|
82
|
+
abstract = (paper.metadata.basic.abstract or "").lower()
|
|
83
|
+
authors = paper.metadata.basic.authors or []
|
|
84
|
+
if (
|
|
85
|
+
query_lower in title
|
|
86
|
+
or query_lower in abstract
|
|
87
|
+
or any(
|
|
88
|
+
query_lower in (a or "").lower()
|
|
89
|
+
for a in authors
|
|
90
|
+
)
|
|
91
|
+
):
|
|
92
|
+
all_papers.append(paper)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.debug(
|
|
95
|
+
f"{self.name}: Failed to load {metadata_file}: {e}"
|
|
96
|
+
)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.debug(f"{self.name}: Failed to search project {project}: {e}")
|
|
99
|
+
|
|
100
|
+
return Papers(all_papers, config=self.config, project="search_results")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# EOF
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/core/_mixins/_services.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Services mixin for Scholar class.
|
|
7
|
+
|
|
8
|
+
Provides internal service properties with lazy loading.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from scitex.scholar.auth import ScholarAuthManager
|
|
16
|
+
from scitex.scholar.browser import ScholarBrowserManager
|
|
17
|
+
from scitex.scholar.config import ScholarConfig
|
|
18
|
+
from scitex.scholar.metadata_engines.ScholarEngine import ScholarEngine
|
|
19
|
+
from scitex.scholar.storage import LibraryManager, ScholarLibrary
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ServiceMixin:
|
|
23
|
+
"""Mixin providing internal service properties."""
|
|
24
|
+
|
|
25
|
+
def _init_config(self, config) -> ScholarConfig:
|
|
26
|
+
"""Initialize configuration from various input types."""
|
|
27
|
+
if config is None:
|
|
28
|
+
return ScholarConfig.load()
|
|
29
|
+
elif isinstance(config, (str, Path)):
|
|
30
|
+
return ScholarConfig.from_yaml(config)
|
|
31
|
+
elif isinstance(config, ScholarConfig):
|
|
32
|
+
return config
|
|
33
|
+
else:
|
|
34
|
+
raise TypeError(f"Invalid config type: {type(config)}")
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def _scholar_engine(self) -> ScholarEngine:
|
|
38
|
+
"""Get Scholar engine for search and enrichment (PRIVATE)."""
|
|
39
|
+
if not hasattr(self, "_ServiceMixin__scholar_engine"):
|
|
40
|
+
self.__scholar_engine = None
|
|
41
|
+
if self.__scholar_engine is None:
|
|
42
|
+
self.__scholar_engine = ScholarEngine(config=self.config)
|
|
43
|
+
return self.__scholar_engine
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def _auth_manager(self) -> ScholarAuthManager:
|
|
47
|
+
"""Get authentication manager service (PRIVATE)."""
|
|
48
|
+
if not hasattr(self, "_ServiceMixin__auth_manager"):
|
|
49
|
+
self.__auth_manager = None
|
|
50
|
+
if self.__auth_manager is None:
|
|
51
|
+
self.__auth_manager = ScholarAuthManager()
|
|
52
|
+
return self.__auth_manager
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def _browser_manager(self) -> ScholarBrowserManager:
|
|
56
|
+
"""Get browser manager service (PRIVATE)."""
|
|
57
|
+
if not hasattr(self, "_ServiceMixin__browser_manager"):
|
|
58
|
+
self.__browser_manager = None
|
|
59
|
+
if self.__browser_manager is None:
|
|
60
|
+
self.__browser_manager = ScholarBrowserManager(
|
|
61
|
+
auth_manager=self._auth_manager,
|
|
62
|
+
chrome_profile_name="system",
|
|
63
|
+
browser_mode=self.browser_mode,
|
|
64
|
+
)
|
|
65
|
+
return self.__browser_manager
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def _library_manager(self) -> LibraryManager:
|
|
69
|
+
"""Get library manager service - low-level operations (PRIVATE)."""
|
|
70
|
+
if not hasattr(self, "_ServiceMixin__library_manager"):
|
|
71
|
+
self.__library_manager = None
|
|
72
|
+
if self.__library_manager is None:
|
|
73
|
+
self.__library_manager = LibraryManager(
|
|
74
|
+
project=self.project, config=self.config
|
|
75
|
+
)
|
|
76
|
+
return self.__library_manager
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def _library(self) -> ScholarLibrary:
|
|
80
|
+
"""Get Scholar library service - high-level operations (PRIVATE)."""
|
|
81
|
+
if not hasattr(self, "_ServiceMixin__library"):
|
|
82
|
+
self.__library = None
|
|
83
|
+
if self.__library is None:
|
|
84
|
+
self.__library = ScholarLibrary(project=self.project, config=self.config)
|
|
85
|
+
return self.__library
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# EOF
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/core/_mixins/_url_finding.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
URL finding mixin for Scholar class.
|
|
7
|
+
|
|
8
|
+
Provides URL resolution and PDF URL discovery functionality.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Dict, List
|
|
14
|
+
|
|
15
|
+
from scitex import logging
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class URLFindingMixin:
|
|
21
|
+
"""Mixin providing URL finding methods."""
|
|
22
|
+
|
|
23
|
+
async def _find_urls_for_doi_async(self, doi: str, context) -> Dict[str, Any]:
|
|
24
|
+
"""Find all URLs for a DOI (orchestration layer).
|
|
25
|
+
|
|
26
|
+
Workflow:
|
|
27
|
+
DOI -> Publisher URL -> PDF URLs -> OpenURL (fallback)
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
doi: DOI string
|
|
31
|
+
context: Authenticated browser context
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
Dictionary with URL information: {
|
|
36
|
+
"url_doi": "https://doi.org/...",
|
|
37
|
+
"url_publisher": "https://publisher.com/...",
|
|
38
|
+
"urls_pdf": [{"url": "...", "source": "zotero_translator"}],
|
|
39
|
+
"url_openurl_resolved": "..." (if fallback used)
|
|
40
|
+
}
|
|
41
|
+
"""
|
|
42
|
+
from scitex.scholar.auth.gateway import (
|
|
43
|
+
OpenURLResolver,
|
|
44
|
+
normalize_doi_as_http,
|
|
45
|
+
resolve_publisher_url_by_navigating_to_doi_page,
|
|
46
|
+
)
|
|
47
|
+
from scitex.scholar.url_finder.ScholarURLFinder import ScholarURLFinder
|
|
48
|
+
|
|
49
|
+
urls = {"url_doi": normalize_doi_as_http(doi)}
|
|
50
|
+
|
|
51
|
+
# Step 1: Resolve publisher URL
|
|
52
|
+
page = await context.new_page()
|
|
53
|
+
try:
|
|
54
|
+
url_publisher = await resolve_publisher_url_by_navigating_to_doi_page(
|
|
55
|
+
doi, page
|
|
56
|
+
)
|
|
57
|
+
urls["url_publisher"] = url_publisher
|
|
58
|
+
finally:
|
|
59
|
+
await page.close()
|
|
60
|
+
|
|
61
|
+
# Step 2: Find PDF URLs from publisher URL
|
|
62
|
+
url_finder = ScholarURLFinder(context, config=self.config)
|
|
63
|
+
urls_pdf = []
|
|
64
|
+
|
|
65
|
+
if url_publisher:
|
|
66
|
+
urls_pdf = await url_finder.find_pdf_urls(url_publisher)
|
|
67
|
+
|
|
68
|
+
# Step 3: Try OpenURL fallback if no PDFs found
|
|
69
|
+
if not urls_pdf:
|
|
70
|
+
openurl_resolver = OpenURLResolver(config=self.config)
|
|
71
|
+
page = await context.new_page()
|
|
72
|
+
try:
|
|
73
|
+
url_openurl_resolved = await openurl_resolver.resolve_doi(doi, page)
|
|
74
|
+
urls["url_openurl_resolved"] = url_openurl_resolved
|
|
75
|
+
|
|
76
|
+
if url_openurl_resolved and url_openurl_resolved != "skipped":
|
|
77
|
+
urls_pdf = await url_finder.find_pdf_urls(url_openurl_resolved)
|
|
78
|
+
finally:
|
|
79
|
+
await page.close()
|
|
80
|
+
|
|
81
|
+
urls["urls_pdf"] = self._deduplicate_pdf_urls(urls_pdf) if urls_pdf else []
|
|
82
|
+
|
|
83
|
+
return urls
|
|
84
|
+
|
|
85
|
+
def _deduplicate_pdf_urls(self, urls_pdf: List[Dict]) -> List[Dict]:
|
|
86
|
+
"""Remove duplicate PDF URLs.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
urls_pdf: List of PDF URL dicts
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
Deduplicated list of PDF URL dicts
|
|
94
|
+
"""
|
|
95
|
+
seen = set()
|
|
96
|
+
unique = []
|
|
97
|
+
for pdf in urls_pdf:
|
|
98
|
+
url = pdf.get("url") if isinstance(pdf, dict) else pdf
|
|
99
|
+
if url not in seen:
|
|
100
|
+
seen.add(url)
|
|
101
|
+
unique.append(pdf)
|
|
102
|
+
return unique
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# EOF
|