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.
Files changed (264) hide show
  1. scitex/__init__.py +71 -17
  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 +210 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.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