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,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