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,36 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/core/_mixins/__init__.py
4
+
5
+ """
6
+ Scholar mixin classes for modular functionality.
7
+
8
+ Each mixin provides a specific set of methods for the Scholar class.
9
+ """
10
+
11
+ from ._enrichers import EnricherMixin
12
+ from ._library_handlers import LibraryHandlerMixin
13
+ from ._loaders import LoaderMixin
14
+ from ._pdf_download import PDFDownloadMixin
15
+ from ._pipeline import PipelineMixin
16
+ from ._project_handlers import ProjectHandlerMixin
17
+ from ._savers import SaverMixin
18
+ from ._search import SearchMixin
19
+ from ._services import ServiceMixin
20
+ from ._url_finding import URLFindingMixin
21
+
22
+ __all__ = [
23
+ "EnricherMixin",
24
+ "URLFindingMixin",
25
+ "PDFDownloadMixin",
26
+ "LoaderMixin",
27
+ "SearchMixin",
28
+ "SaverMixin",
29
+ "ProjectHandlerMixin",
30
+ "LibraryHandlerMixin",
31
+ "PipelineMixin",
32
+ "ServiceMixin",
33
+ ]
34
+
35
+
36
+ # EOF
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/core/_mixins/_enrichers.py
4
+
5
+ """
6
+ Enricher mixin for Scholar class.
7
+
8
+ Provides paper enrichment functionality including metadata enrichment,
9
+ impact factor lookup, and citation count retrieval.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ from copy import deepcopy
16
+ from typing import TYPE_CHECKING, Dict, Optional, Union
17
+
18
+ import nest_asyncio
19
+
20
+ from scitex import logging
21
+ from scitex.scholar.impact_factor.ImpactFactorEngine import ImpactFactorEngine
22
+
23
+ if TYPE_CHECKING:
24
+ from ..Paper import Paper
25
+ from ..Papers import Papers
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class EnricherMixin:
31
+ """Mixin providing paper enrichment methods."""
32
+
33
+ async def enrich_papers_async(self, papers: Papers) -> Papers:
34
+ """Async version of enrich_papers for use in async contexts.
35
+
36
+ Args:
37
+ papers: Papers collection to enrich.
38
+
39
+ Returns
40
+ -------
41
+ Enriched Papers collection
42
+ """
43
+ from ..Papers import Papers
44
+
45
+ enriched_list = []
46
+
47
+ for paper in papers:
48
+ try:
49
+ results = await self._scholar_engine.search_async(
50
+ title=paper.metadata.basic.title,
51
+ year=paper.metadata.basic.year,
52
+ authors=(
53
+ paper.metadata.basic.authors[0]
54
+ if paper.metadata.basic.authors
55
+ else None
56
+ ),
57
+ )
58
+
59
+ enriched_paper = self._merge_enrichment_data(paper, results)
60
+ enriched_list.append(enriched_paper)
61
+ title = paper.metadata.basic.title or "No title"
62
+ logger.info(f"{self.name}: Enriched: {title[:50]}...")
63
+
64
+ except Exception as e:
65
+ title = paper.metadata.basic.title or "No title"
66
+ logger.warning(
67
+ f"{self.name}: Failed to enrich paper '{title[:50]}...': {e}"
68
+ )
69
+ enriched_list.append(paper)
70
+
71
+ enriched_papers = Papers(enriched_list, project=self.project)
72
+
73
+ if self.config.resolve("enrich_impact_factors", None, True):
74
+ enriched_papers = self._enrich_impact_factors(enriched_papers)
75
+
76
+ return enriched_papers
77
+
78
+ def enrich_papers(
79
+ self, papers: Optional[Papers] = None
80
+ ) -> Union[Papers, Dict[str, int]]:
81
+ """Enrich papers with metadata from multiple sources.
82
+
83
+ Args:
84
+ papers: Papers collection to enrich. If None, enriches all papers
85
+ in current project.
86
+
87
+ Returns
88
+ -------
89
+ - If papers provided: Returns enriched Papers collection
90
+ - If no papers: Returns dict with enrichment statistics for project
91
+ """
92
+ from ..Papers import Papers
93
+
94
+ if papers is None:
95
+ return self._enrich_current_project()
96
+
97
+ enriched_list = []
98
+ nest_asyncio.apply()
99
+
100
+ for paper in papers:
101
+ try:
102
+ results = asyncio.run(
103
+ self._scholar_engine.search_async(
104
+ title=paper.metadata.basic.title,
105
+ year=paper.metadata.basic.year,
106
+ authors=(
107
+ paper.metadata.basic.authors[0]
108
+ if paper.metadata.basic.authors
109
+ else None
110
+ ),
111
+ )
112
+ )
113
+
114
+ enriched_paper = self._merge_enrichment_data(paper, results)
115
+ enriched_list.append(enriched_paper)
116
+ title = paper.metadata.basic.title or "No title"
117
+ logger.info(f"{self.name}: Enriched: {title[:50]}...")
118
+
119
+ except Exception as e:
120
+ title = paper.metadata.basic.title or "No title"
121
+ logger.warning(
122
+ f"{self.name}: Failed to enrich paper '{title[:50]}...': {e}"
123
+ )
124
+ enriched_list.append(paper)
125
+
126
+ enriched_papers = Papers(enriched_list, project=self.project)
127
+
128
+ if self.config.resolve("enrich_impact_factors", None, True):
129
+ enriched_papers = self._enrich_impact_factors(enriched_papers)
130
+
131
+ return enriched_papers
132
+
133
+ def _enrich_impact_factors(self, papers: Papers) -> Papers:
134
+ """Add journal impact factors to papers.
135
+
136
+ Args:
137
+ papers: Papers collection to enrich with impact factors
138
+
139
+ Returns
140
+ -------
141
+ Papers collection with impact factors added where available
142
+ """
143
+ try:
144
+ jcr_engine = ImpactFactorEngine()
145
+ papers = jcr_engine.enrich_papers(papers)
146
+ return papers
147
+ except Exception as e:
148
+ logger.debug(
149
+ f"{self.name}: JCR engine unavailable: {e}, "
150
+ "falling back to calculation method"
151
+ )
152
+ return papers
153
+
154
+ def _merge_enrichment_data(self, paper: Paper, results: Dict) -> Paper:
155
+ """Merge enrichment results into paper object.
156
+
157
+ Creates a new Paper object with merged data to avoid modifying the original.
158
+ """
159
+ enriched = deepcopy(paper)
160
+
161
+ if not results:
162
+ return enriched
163
+
164
+ # ID section
165
+ if "id" in results:
166
+ if results["id"].get("doi") and not enriched.metadata.id.doi:
167
+ enriched.metadata.set_doi(results["id"]["doi"])
168
+ if results["id"].get("pmid") and not enriched.metadata.id.pmid:
169
+ enriched.metadata.id.pmid = results["id"]["pmid"]
170
+ if results["id"].get("arxiv_id") and not enriched.metadata.id.arxiv_id:
171
+ enriched.metadata.id.arxiv_id = results["id"]["arxiv_id"]
172
+
173
+ # Basic metadata section
174
+ if "basic" in results:
175
+ if results["basic"].get("abstract"):
176
+ enriched.metadata.basic.abstract = results["basic"]["abstract"]
177
+
178
+ if results["basic"].get("title"):
179
+ new_title = results["basic"]["title"]
180
+ current_title = enriched.metadata.basic.title or ""
181
+ if not current_title or len(new_title) > len(current_title):
182
+ enriched.metadata.basic.title = new_title
183
+
184
+ if results["basic"].get("authors") and not enriched.metadata.basic.authors:
185
+ enriched.metadata.basic.authors = results["basic"]["authors"]
186
+
187
+ if results["basic"].get("year") and not enriched.metadata.basic.year:
188
+ enriched.metadata.basic.year = results["basic"]["year"]
189
+
190
+ if (
191
+ results["basic"].get("keywords")
192
+ and not enriched.metadata.basic.keywords
193
+ ):
194
+ enriched.metadata.basic.keywords = results["basic"]["keywords"]
195
+
196
+ # Publication metadata
197
+ if "publication" in results:
198
+ pub = results["publication"]
199
+ meta_pub = enriched.metadata.publication
200
+ if pub.get("journal") and not meta_pub.journal:
201
+ meta_pub.journal = pub["journal"]
202
+ if pub.get("publisher") and not meta_pub.publisher:
203
+ meta_pub.publisher = pub["publisher"]
204
+ if pub.get("volume") and not meta_pub.volume:
205
+ meta_pub.volume = pub["volume"]
206
+ if pub.get("issue") and not meta_pub.issue:
207
+ meta_pub.issue = pub["issue"]
208
+ if pub.get("pages") and not meta_pub.pages:
209
+ meta_pub.pages = pub["pages"]
210
+
211
+ # Citation metadata
212
+ if "citation_count" in results:
213
+ count = results["citation_count"].get("count") or results[
214
+ "citation_count"
215
+ ].get("total")
216
+ if count:
217
+ current_count = enriched.metadata.citation_count.total or 0
218
+ if not current_count or count > current_count:
219
+ enriched.metadata.citation_count.total = count
220
+
221
+ # URL metadata
222
+ if "url" in results:
223
+ if results["url"].get("pdf"):
224
+ pdf_url = results["url"]["pdf"]
225
+ if not any(p.get("url") == pdf_url for p in enriched.metadata.url.pdfs):
226
+ enriched.metadata.url.pdfs.append(
227
+ {"url": pdf_url, "source": "enrichment"}
228
+ )
229
+ if results["url"].get("url") and not enriched.metadata.url.publisher:
230
+ enriched.metadata.url.publisher = results["url"]["url"]
231
+
232
+ return enriched
233
+
234
+ def _enrich_current_project(self) -> Dict[str, int]:
235
+ """Enrich all papers in the current project.
236
+
237
+ Returns
238
+ -------
239
+ Dictionary with enrichment statistics
240
+ """
241
+ if not self.project:
242
+ raise ValueError(
243
+ "No project specified. Use Scholar(project='name') "
244
+ "or provide papers to enrich()."
245
+ )
246
+
247
+ papers = self.load_project(self.project)
248
+ logger.info(
249
+ f"{self.name}: Enriching {len(papers)} papers in project '{self.project}'"
250
+ )
251
+
252
+ enriched_papers = self.enrich_papers(papers)
253
+
254
+ enriched_count = sum(
255
+ 1
256
+ for i, p in enumerate(enriched_papers)
257
+ if p.abstract and not papers[i].abstract
258
+ )
259
+
260
+ saved_ids = self.save_papers_to_library(enriched_papers)
261
+
262
+ return {
263
+ "enriched": enriched_count,
264
+ "failed": len(papers) - enriched_count,
265
+ "total": len(papers),
266
+ "saved": len(saved_ids),
267
+ }
268
+
269
+
270
+ # EOF
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/core/_mixins/_library_handlers.py
4
+
5
+ """
6
+ Library handler mixin for Scholar class.
7
+
8
+ Provides library-wide statistics and backup 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, Union
18
+
19
+ from scitex import logging
20
+ from scitex.logging import ScholarError
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class LibraryHandlerMixin:
26
+ """Mixin providing library management methods."""
27
+
28
+ def get_library_statistics(self) -> Dict[str, Any]:
29
+ """Get comprehensive statistics for the entire Scholar library.
30
+
31
+ Returns
32
+ -------
33
+ Dictionary with library-wide statistics
34
+ """
35
+ master_dir = self.config.get_library_master_dir()
36
+ projects = self.list_projects()
37
+
38
+ stats = {
39
+ "total_projects": len(projects),
40
+ "total_papers": (
41
+ len(list(master_dir.glob("*"))) if master_dir.exists() else 0
42
+ ),
43
+ "projects": projects,
44
+ "library_path": str(self.config.path_manager.library_dir),
45
+ "master_path": str(master_dir),
46
+ }
47
+
48
+ if master_dir.exists():
49
+ total_size = sum(
50
+ f.stat().st_size for f in master_dir.rglob("*") if f.is_file()
51
+ )
52
+ stats["storage_mb"] = total_size / (1024 * 1024)
53
+ else:
54
+ stats["storage_mb"] = 0
55
+
56
+ return stats
57
+
58
+ def backup_library(self, backup_path: Union[str, Path]) -> Dict[str, Any]:
59
+ """Create a backup of the Scholar library.
60
+
61
+ Args:
62
+ backup_path: Path for the backup
63
+
64
+ Returns
65
+ -------
66
+ Dictionary with backup information
67
+ """
68
+ backup_path = Path(backup_path)
69
+ library_path = self.config.path_manager.library_dir
70
+
71
+ if not library_path.exists():
72
+ raise ScholarError("Library directory does not exist")
73
+
74
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
75
+ backup_dir = backup_path / f"scholar_library_backup_{timestamp}"
76
+
77
+ logger.info(f"{self.name}: Creating library backup at {backup_dir}")
78
+ shutil.copytree(library_path, backup_dir)
79
+
80
+ backup_info = {
81
+ "timestamp": timestamp,
82
+ "source": str(library_path),
83
+ "backup": str(backup_dir),
84
+ "size_mb": sum(
85
+ f.stat().st_size for f in backup_dir.rglob("*") if f.is_file()
86
+ )
87
+ / (1024 * 1024),
88
+ }
89
+
90
+ metadata_file = backup_dir / "backup_metadata.json"
91
+ with open(metadata_file, "w") as f:
92
+ json.dump(backup_info, f, indent=2)
93
+
94
+ logger.info(
95
+ f"{self.name}: Library backup completed: {backup_info['size_mb']:.2f} MB"
96
+ )
97
+ return backup_info
98
+
99
+
100
+ # 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/_loaders.py
4
+
5
+ """
6
+ Loader mixin for Scholar class.
7
+
8
+ Provides methods for loading papers from projects and BibTeX files.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Optional, Union
16
+
17
+ from scitex import logging
18
+
19
+ if TYPE_CHECKING:
20
+ from ..Papers import Papers
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class LoaderMixin:
26
+ """Mixin providing paper loading methods."""
27
+
28
+ def load_project(self, project: Optional[str] = None) -> Papers:
29
+ """Load papers from a project using library manager service.
30
+
31
+ Args:
32
+ project: Project name (uses self.project if None)
33
+
34
+ Returns
35
+ -------
36
+ Papers collection from the project
37
+ """
38
+ from ..Paper import Paper
39
+ from ..Papers import Papers
40
+
41
+ project_name = project or self.project
42
+ if not project_name:
43
+ raise ValueError("No project specified")
44
+
45
+ logger.info(f"{self.name}: Loading papers from project: {project_name}")
46
+
47
+ library_dir = self.config.path_manager.library_dir
48
+ project_dir = library_dir / project_name
49
+
50
+ if not project_dir.exists():
51
+ logger.warning(
52
+ f"{self.name}: Project directory does not exist: {project_dir}"
53
+ )
54
+ return Papers([], project=project_name)
55
+
56
+ papers = []
57
+ for item in project_dir.iterdir():
58
+ if item.name in ["info", "project_metadata.json", "README.md"]:
59
+ continue
60
+
61
+ if item.is_symlink():
62
+ master_path = item.resolve()
63
+ if master_path.exists():
64
+ metadata_file = master_path / "metadata.json"
65
+ if metadata_file.exists():
66
+ try:
67
+ with open(metadata_file) as f:
68
+ metadata = json.load(f)
69
+
70
+ paper = Paper.from_dict(metadata)
71
+ papers.append(paper)
72
+ except Exception as e:
73
+ logger.warning(
74
+ f"{self.name}: Failed to load metadata "
75
+ f"from {metadata_file}: {e}"
76
+ )
77
+
78
+ logger.info(
79
+ f"{self.name}: Loaded {len(papers)} papers from project: {project_name}"
80
+ )
81
+ return Papers(papers, project=project_name)
82
+
83
+ def load_bibtex(self, bibtex_input: Union[str, Path]) -> Papers:
84
+ """Load Papers collection from BibTeX file or content.
85
+
86
+ Args:
87
+ bibtex_input: BibTeX file path or content string
88
+
89
+ Returns
90
+ -------
91
+ Papers collection
92
+ """
93
+ from ..Papers import Papers
94
+
95
+ papers = self._library.papers_from_bibtex(bibtex_input)
96
+
97
+ papers_collection = Papers(papers, config=self.config, project=self.project)
98
+ papers_collection.library = self._library
99
+
100
+ return papers_collection
101
+
102
+
103
+ # EOF