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.
Files changed (218) hide show
  1. scitex/__init__.py +47 -0
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +191 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {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