scitex 2.14.0__py3-none-any.whl → 2.15.2__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 (300) 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 +244 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +21 -204
  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 +76 -27
  68. scitex/cli/capture.py +13 -20
  69. scitex/cli/introspect.py +481 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +357 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +23 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +314 -0
  79. scitex/cli/stats.py +15 -8
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +132 -8
  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} +43 -54
  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/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  178. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  179. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  180. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  181. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  182. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  183. scitex/security/README.md +3 -3
  184. scitex/session/README.md +1 -1
  185. scitex/session/__init__.py +26 -7
  186. scitex/session/_decorator.py +1 -1
  187. scitex/sh/README.md +1 -1
  188. scitex/sh/__init__.py +7 -4
  189. scitex/social/__init__.py +155 -0
  190. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  191. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  192. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  193. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  194. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  195. scitex/stats/_mcp/_handlers/_format.py +94 -0
  196. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  197. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  198. scitex/stats/_mcp/_handlers/_power.py +247 -0
  199. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  200. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  201. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  202. scitex/stats/_mcp/handlers.py +19 -1171
  203. scitex/stats/auto/_stat_style.py +175 -0
  204. scitex/stats/auto/_style_definitions.py +411 -0
  205. scitex/stats/auto/_styles.py +22 -620
  206. scitex/stats/descriptive/__init__.py +11 -8
  207. scitex/stats/descriptive/_ci.py +39 -0
  208. scitex/stats/power/_power.py +15 -4
  209. scitex/str/__init__.py +2 -1
  210. scitex/str/_title_case.py +63 -0
  211. scitex/template/README.md +1 -1
  212. scitex/template/__init__.py +25 -10
  213. scitex/template/_code_templates.py +147 -0
  214. scitex/template/_mcp/handlers.py +81 -0
  215. scitex/template/_mcp/tool_schemas.py +55 -0
  216. scitex/template/_templates/__init__.py +51 -0
  217. scitex/template/_templates/audio.py +233 -0
  218. scitex/template/_templates/canvas.py +312 -0
  219. scitex/template/_templates/capture.py +268 -0
  220. scitex/template/_templates/config.py +43 -0
  221. scitex/template/_templates/diagram.py +294 -0
  222. scitex/template/_templates/io.py +107 -0
  223. scitex/template/_templates/module.py +53 -0
  224. scitex/template/_templates/plt.py +202 -0
  225. scitex/template/_templates/scholar.py +267 -0
  226. scitex/template/_templates/session.py +130 -0
  227. scitex/template/_templates/session_minimal.py +43 -0
  228. scitex/template/_templates/session_plot.py +67 -0
  229. scitex/template/_templates/session_stats.py +77 -0
  230. scitex/template/_templates/stats.py +323 -0
  231. scitex/template/_templates/writer.py +296 -0
  232. scitex/template/clone_writer_directory.py +5 -5
  233. scitex/ui/_backends/_email.py +10 -2
  234. scitex/ui/_backends/_webhook.py +5 -1
  235. scitex/web/_search_pubmed.py +10 -6
  236. scitex/writer/README.md +1 -1
  237. scitex/writer/_mcp/handlers.py +11 -744
  238. scitex/writer/_mcp/tool_schemas.py +5 -335
  239. scitex-2.15.2.dist-info/METADATA +648 -0
  240. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
  241. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  242. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  243. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  244. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  245. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  246. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  247. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  248. scitex/diagram/_compile.py +0 -312
  249. scitex/diagram/_diagram.py +0 -355
  250. scitex/diagram/_mcp/__init__.py +0 -4
  251. scitex/diagram/_mcp/handlers.py +0 -400
  252. scitex/diagram/_mcp/tool_schemas.py +0 -157
  253. scitex/diagram/_presets.py +0 -173
  254. scitex/diagram/_schema.py +0 -182
  255. scitex/diagram/_split.py +0 -278
  256. scitex/gen/_ci.py +0 -12
  257. scitex/gen/_title_case.py +0 -89
  258. scitex/plt/_mcp/__init__.py +0 -4
  259. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  260. scitex/plt/_mcp/_handlers_figure.py +0 -195
  261. scitex/plt/_mcp/_handlers_plot.py +0 -252
  262. scitex/plt/_mcp/_handlers_style.py +0 -219
  263. scitex/plt/_mcp/handlers.py +0 -74
  264. scitex/plt/_mcp/tool_schemas.py +0 -497
  265. scitex/plt/mcp_server.py +0 -231
  266. scitex/scholar/data/.gitkeep +0 -0
  267. scitex/scholar/data/README.md +0 -44
  268. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  269. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  270. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  271. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  272. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  273. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  274. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  275. scitex/scholar/data/bib_files/pac.bib +0 -698
  276. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  277. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  278. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  279. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  280. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  281. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  282. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  283. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  284. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  285. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  286. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  287. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  288. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  289. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  290. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  291. scitex/scholar/data/impact_factor.db +0 -0
  292. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  293. scitex/scholar/examples/dev.py +0 -38
  294. scitex-2.14.0.dist-info/METADATA +0 -1238
  295. /scitex/{gen → context}/_detect_environment.py +0 -0
  296. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  297. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  298. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
  299. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
  300. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/scholar/storage/_mixins/_symlink_handlers.py
4
+
5
+ """
6
+ Symlink handling mixin for LibraryManager.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional
15
+
16
+ from scitex import logging
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SymlinkHandlersMixin:
22
+ """Mixin providing symlink handling methods."""
23
+
24
+ def _generate_readable_name(
25
+ self,
26
+ comprehensive_metadata: Dict,
27
+ master_storage_path: Path,
28
+ authors: Optional[List[str]] = None,
29
+ year: Optional[int] = None,
30
+ journal: Optional[str] = None,
31
+ ) -> str:
32
+ """Generate readable symlink name from metadata."""
33
+ from scitex.dict import DotDict
34
+
35
+ # Extract author
36
+ first_author = "Unknown"
37
+ if authors and len(authors) > 0:
38
+ author_parts = authors[0].split()
39
+ first_author = (
40
+ author_parts[-1] if len(author_parts) > 1 else author_parts[0]
41
+ )
42
+ first_author = "".join(c for c in first_author if c.isalnum() or c == "-")[
43
+ :20
44
+ ]
45
+
46
+ # Format year
47
+ if isinstance(year, DotDict):
48
+ year = None
49
+
50
+ if isinstance(year, str) and year.isdigit():
51
+ year = int(year)
52
+
53
+ year_str = f"{year:04d}" if isinstance(year, int) else "0000"
54
+
55
+ # Clean journal name
56
+ journal_clean = "Unknown"
57
+ if journal:
58
+ journal_clean = self.config.path_manager._sanitize_filename(journal)[:30]
59
+ if not journal_clean:
60
+ journal_clean = "Unknown"
61
+
62
+ # Get citation count and impact factor
63
+ cc, if_val = self._extract_cc_and_if(comprehensive_metadata)
64
+
65
+ # Count PDFs
66
+ pdf_files = list(master_storage_path.glob("*.pdf"))
67
+ n_pdfs = len(pdf_files)
68
+
69
+ readable_name = f"PDF-{n_pdfs:02d}_CC-{cc:06d}_IF-{int(if_val):03d}_{year_str}_{first_author}_{journal_clean}"
70
+ return readable_name
71
+
72
+ def _extract_cc_and_if(self, comprehensive_metadata: Dict) -> tuple:
73
+ """Extract citation count and impact factor from metadata."""
74
+ if "metadata" in comprehensive_metadata:
75
+ metadata_section = comprehensive_metadata.get("metadata", {})
76
+ cc_val = metadata_section.get("citation_count", {})
77
+ if isinstance(cc_val, dict):
78
+ cc = cc_val.get("total", 0) or 0
79
+ else:
80
+ cc = cc_val or 0
81
+
82
+ publication_section = metadata_section.get("publication", {})
83
+ if_val = publication_section.get("impact_factor", 0.0) or 0.0
84
+ else:
85
+ cc_val = comprehensive_metadata.get("citation_count", 0)
86
+ if isinstance(cc_val, dict):
87
+ cc = cc_val.get("total", 0) or 0
88
+ else:
89
+ cc = cc_val or 0
90
+
91
+ if_val = (
92
+ comprehensive_metadata.get("journal_impact_factor")
93
+ or comprehensive_metadata.get("impact_factor")
94
+ or comprehensive_metadata.get("publication", {}).get("impact_factor")
95
+ )
96
+ if isinstance(if_val, dict):
97
+ if_val = if_val.get("value", 0.0) or 0.0
98
+ else:
99
+ if_val = if_val or 0.0
100
+
101
+ return cc, if_val
102
+
103
+ def update_symlink(
104
+ self,
105
+ master_storage_path: Path,
106
+ project: str,
107
+ metadata: Optional[Dict] = None,
108
+ ) -> Optional[Path]:
109
+ """Update project symlink to reflect current paper status."""
110
+ try:
111
+ if metadata is None:
112
+ metadata_file = master_storage_path / "metadata.json"
113
+ if metadata_file.exists():
114
+ with open(metadata_file) as f:
115
+ metadata = json.load(f)
116
+ else:
117
+ logger.warning(f"No metadata found for {master_storage_path.name}")
118
+ return None
119
+
120
+ # Extract metadata from nested structure if needed
121
+ if "metadata" in metadata:
122
+ meta_section = metadata.get("metadata", {})
123
+ basic_section = meta_section.get("basic", {})
124
+ pub_section = meta_section.get("publication", {})
125
+ authors = basic_section.get("authors")
126
+ year = basic_section.get("year")
127
+ journal = pub_section.get("journal")
128
+ else:
129
+ authors = metadata.get("authors")
130
+ year = metadata.get("year")
131
+ journal = metadata.get("journal")
132
+
133
+ readable_name = self._generate_readable_name(
134
+ comprehensive_metadata=metadata,
135
+ master_storage_path=master_storage_path,
136
+ authors=authors,
137
+ year=year,
138
+ journal=journal,
139
+ )
140
+
141
+ return self._create_project_symlink(
142
+ master_storage_path=master_storage_path,
143
+ project=project,
144
+ readable_name=readable_name,
145
+ )
146
+ except Exception as exc_:
147
+ logger.error(
148
+ f"Failed to update symlink for {master_storage_path.name}: {exc_}"
149
+ )
150
+ return None
151
+
152
+ def _create_project_symlink(
153
+ self, master_storage_path: Path, project: str, readable_name: str
154
+ ) -> Optional[Path]:
155
+ """Create symlink in project directory pointing to master storage."""
156
+ try:
157
+ project_dir = self.config.path_manager.get_library_project_dir(project)
158
+ symlink_path = project_dir / readable_name
159
+ master_id = master_storage_path.name
160
+
161
+ # Remove old symlinks pointing to the same master entry
162
+ for existing_link in project_dir.iterdir():
163
+ if not existing_link.is_symlink():
164
+ continue
165
+
166
+ try:
167
+ target = existing_link.resolve()
168
+ if target.name == master_id and existing_link.name != readable_name:
169
+ logger.debug(f"Removing old symlink: {existing_link.name}")
170
+ existing_link.unlink()
171
+ except Exception as e:
172
+ logger.debug(f"Skipping broken symlink {existing_link.name}: {e}")
173
+ continue
174
+
175
+ # Create new symlink
176
+ if not symlink_path.exists():
177
+ relative_path = os.path.relpath(master_storage_path, project_dir)
178
+ symlink_path.symlink_to(relative_path)
179
+ logger.success(
180
+ f"Created project symlink: {symlink_path} -> {relative_path}"
181
+ )
182
+ else:
183
+ logger.debug(f"Project symlink already exists: {symlink_path}")
184
+
185
+ return symlink_path
186
+
187
+ except Exception as exc_:
188
+ logger.warning(f"Failed to create project symlink: {exc_}")
189
+ return None
190
+
191
+ def _ensure_project_symlink(
192
+ self,
193
+ title: str,
194
+ year: Optional[int] = None,
195
+ authors: Optional[List[str]] = None,
196
+ paper_id: str = None,
197
+ master_storage_path: Path = None,
198
+ ) -> None:
199
+ """Ensure project symlink exists for paper in master library."""
200
+ try:
201
+ if not paper_id or not master_storage_path:
202
+ return
203
+
204
+ project_lib_path = (
205
+ self.config.path_manager.get_scholar_library_path() / self.project
206
+ )
207
+ project_lib_path.mkdir(parents=True, exist_ok=True)
208
+
209
+ paper_info = {"title": title, "year": year, "authors": authors or []}
210
+ readable_paths = self._call_path_manager_get_storage_paths(
211
+ paper_info=paper_info, collection_name=self.project
212
+ )
213
+ readable_name = readable_paths["readable_name"]
214
+ symlink_path = project_lib_path / readable_name
215
+ relative_path = f"../MASTER/{paper_id}"
216
+
217
+ if not symlink_path.exists():
218
+ symlink_path.symlink_to(relative_path)
219
+ logger.info(
220
+ f"Created project symlink: {readable_name} -> {relative_path}"
221
+ )
222
+ except Exception as exc_:
223
+ logger.debug(f"Error creating project symlink: {exc_}")
224
+
225
+
226
+ # EOF
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Time-stamp: "2025-08-01 13:15:00"
4
+ # Author: Claude
5
+ # File: KNOWN_RESOLVERS.py
6
+
7
+ """
8
+ Known OpenURL resolvers from various institutions worldwide.
9
+
10
+ This module contains a curated list of OpenURL resolvers used by
11
+ academic institutions for accessing scholarly content.
12
+
13
+ Sources:
14
+ - Zotero OpenURL Resolver Directory: https://www.zotero.org/openurl_resolvers
15
+ - Individual institution library websites
16
+ - Common resolver patterns
17
+ """
18
+
19
+ from typing import Dict, List, Optional
20
+
21
+ # Major OpenURL resolver vendors
22
+ RESOLVER_VENDORS = {
23
+ "ExLibris": {
24
+ "patterns": ["sfx", "exlibrisgroup.com"],
25
+ "description": "Ex Libris SFX resolver (very common)"
26
+ },
27
+ "SerialsSolutions": {
28
+ "patterns": ["serialssolutions.com", "360link"],
29
+ "description": "ProQuest SerialsSolutions 360 Link"
30
+ },
31
+ "EBSCO": {
32
+ "patterns": ["ebscohost.com/openurlresolver", "linkssource.ebsco.com"],
33
+ "description": "EBSCO Full Text Finder"
34
+ },
35
+ "OCLC": {
36
+ "patterns": ["worldcat.org", "oclc.org"],
37
+ "description": "OCLC WorldCat resolver"
38
+ },
39
+ "Ovid": {
40
+ "patterns": ["ovid.com", "linksolver"],
41
+ "description": "Ovid LinkSolver"
42
+ }
43
+ }
44
+
45
+ # Known institutional OpenURL resolvers
46
+ KNOWN_RESOLVERS: Dict[str, Dict[str, str]] = {
47
+ # United States
48
+ "Harvard University": {
49
+ "url": "https://sfx.hul.harvard.edu/sfx_local",
50
+ "country": "US",
51
+ "vendor": "ExLibris"
52
+ },
53
+ "MIT": {
54
+ "url": "https://owens.mit.edu/sfx_local",
55
+ "country": "US",
56
+ "vendor": "ExLibris"
57
+ },
58
+ "Stanford University": {
59
+ "url": "https://stanford.idm.oclc.org/login?url=",
60
+ "country": "US",
61
+ "vendor": "OCLC"
62
+ },
63
+ "Yale University": {
64
+ "url": "https://yale.idm.oclc.org/login?url=",
65
+ "country": "US",
66
+ "vendor": "OCLC"
67
+ },
68
+ "University of California, Berkeley": {
69
+ "url": "https://ucelinks.cdlib.org:8443/sfx_ucb",
70
+ "country": "US",
71
+ "vendor": "ExLibris"
72
+ },
73
+ "UCLA": {
74
+ "url": "https://ucelinks.cdlib.org:8443/sfx_ucla",
75
+ "country": "US",
76
+ "vendor": "ExLibris"
77
+ },
78
+ "Columbia University": {
79
+ "url": "https://resolver.library.columbia.edu/openurl",
80
+ "country": "US",
81
+ "vendor": "SerialsSolutions"
82
+ },
83
+ "Princeton University": {
84
+ "url": "https://princeton.idm.oclc.org/login?url=",
85
+ "country": "US",
86
+ "vendor": "OCLC"
87
+ },
88
+ "University of Chicago": {
89
+ "url": "https://proxy.uchicago.edu/login?url=",
90
+ "country": "US",
91
+ "vendor": "Custom"
92
+ },
93
+ "Johns Hopkins": {
94
+ "url": "https://openurl.library.jhu.edu",
95
+ "country": "US",
96
+ "vendor": "Custom"
97
+ },
98
+
99
+ # United Kingdom
100
+ "University of Oxford": {
101
+ "url": "https://fs.oxfordjournals.org/openurl",
102
+ "country": "UK",
103
+ "vendor": "Custom"
104
+ },
105
+ "University of Cambridge": {
106
+ "url": "https://cambridge.idm.oclc.org/login?url=",
107
+ "country": "UK",
108
+ "vendor": "OCLC"
109
+ },
110
+ "Imperial College London": {
111
+ "url": "https://imperial.idm.oclc.org/login?url=",
112
+ "country": "UK",
113
+ "vendor": "OCLC"
114
+ },
115
+ "UCL": {
116
+ "url": "https://ucl.idm.oclc.org/login?url=",
117
+ "country": "UK",
118
+ "vendor": "OCLC"
119
+ },
120
+ "University of Edinburgh": {
121
+ "url": "https://discovered.ed.ac.uk/openurl",
122
+ "country": "UK",
123
+ "vendor": "Custom"
124
+ },
125
+
126
+ # Canada
127
+ "University of Toronto": {
128
+ "url": "https://myaccess.library.utoronto.ca/login?url=",
129
+ "country": "CA",
130
+ "vendor": "Custom"
131
+ },
132
+ "McGill University": {
133
+ "url": "https://mcgill.on.worldcat.org/atoztitles/link",
134
+ "country": "CA",
135
+ "vendor": "OCLC"
136
+ },
137
+ "University of British Columbia": {
138
+ "url": "https://ubc.summon.serialssolutions.com/link",
139
+ "country": "CA",
140
+ "vendor": "SerialsSolutions"
141
+ },
142
+
143
+ # Australia
144
+ "University of Melbourne": {
145
+ "url": "https://unimelb.hosted.exlibrisgroup.com/sfxlcl41",
146
+ "country": "AU",
147
+ "vendor": "ExLibris"
148
+ },
149
+ "University of Sydney": {
150
+ "url": "https://ap01.alma.exlibrisgroup.com/view/uresolver/61USYD_INST/openurl",
151
+ "country": "AU",
152
+ "vendor": "ExLibris"
153
+ },
154
+ "Australian National University": {
155
+ "url": "https://anu.hosted.exlibrisgroup.com/primo-explore/openurl",
156
+ "country": "AU",
157
+ "vendor": "ExLibris"
158
+ },
159
+ "University of Queensland": {
160
+ "url": "https://uq.summon.serialssolutions.com/link",
161
+ "country": "AU",
162
+ "vendor": "SerialsSolutions"
163
+ },
164
+ "Monash University": {
165
+ "url": "https://monash.hosted.exlibrisgroup.com/sfx_local",
166
+ "country": "AU",
167
+ "vendor": "ExLibris"
168
+ },
169
+
170
+ # Germany
171
+ "Max Planck Society": {
172
+ "url": "http://sfx.mpg.de/sfx_local",
173
+ "country": "DE",
174
+ "vendor": "ExLibris"
175
+ },
176
+ "University of Munich (LMU)": {
177
+ "url": "https://sfx.bib.uni-muenchen.de/sfx_lmu",
178
+ "country": "DE",
179
+ "vendor": "ExLibris"
180
+ },
181
+ "Heidelberg University": {
182
+ "url": "https://sfx.bib.uni-heidelberg.de/sfx_heidelberg",
183
+ "country": "DE",
184
+ "vendor": "ExLibris"
185
+ },
186
+
187
+ # Netherlands
188
+ "University of Amsterdam": {
189
+ "url": "https://vu-nl.idm.oclc.org/login?url=",
190
+ "country": "NL",
191
+ "vendor": "OCLC"
192
+ },
193
+ "Delft University of Technology": {
194
+ "url": "https://tudelft.idm.oclc.org/login?url=",
195
+ "country": "NL",
196
+ "vendor": "OCLC"
197
+ },
198
+
199
+ # France
200
+ "Sorbonne University": {
201
+ "url": "https://accesdistant.sorbonne-universite.fr/login?url=",
202
+ "country": "FR",
203
+ "vendor": "Custom"
204
+ },
205
+ "École Polytechnique": {
206
+ "url": "https://portail.polytechnique.edu/openurl",
207
+ "country": "FR",
208
+ "vendor": "Custom"
209
+ },
210
+
211
+ # Switzerland
212
+ "ETH Zurich": {
213
+ "url": "https://www.library.ethz.ch/openurl",
214
+ "country": "CH",
215
+ "vendor": "Custom"
216
+ },
217
+ "EPFL": {
218
+ "url": "https://sfx.epfl.ch/sfx_local",
219
+ "country": "CH",
220
+ "vendor": "ExLibris"
221
+ },
222
+
223
+ # Japan
224
+ "University of Tokyo": {
225
+ "url": "https://vs2ga4mq9g.search.serialssolutions.com",
226
+ "country": "JP",
227
+ "vendor": "SerialsSolutions"
228
+ },
229
+ "Kyoto University": {
230
+ "url": "https://kuline.kulib.kyoto-u.ac.jp/portal/openurl",
231
+ "country": "JP",
232
+ "vendor": "Custom"
233
+ },
234
+
235
+ # Singapore
236
+ "National University of Singapore": {
237
+ "url": "https://libproxy.nus.edu.sg/login?url=",
238
+ "country": "SG",
239
+ "vendor": "Custom"
240
+ },
241
+ "Nanyang Technological University": {
242
+ "url": "https://ap01.alma.exlibrisgroup.com/view/uresolver/65NTU_INST/openurl",
243
+ "country": "SG",
244
+ "vendor": "ExLibris"
245
+ },
246
+
247
+ # China
248
+ "Tsinghua University": {
249
+ "url": "http://sfx.lib.tsinghua.edu.cn/sfx_local",
250
+ "country": "CN",
251
+ "vendor": "ExLibris"
252
+ },
253
+ "Peking University": {
254
+ "url": "http://sfx.lib.pku.edu.cn/sfx_pku",
255
+ "country": "CN",
256
+ "vendor": "ExLibris"
257
+ },
258
+
259
+ # South Korea
260
+ "Seoul National University": {
261
+ "url": "https://sfx.snu.ac.kr/sfx_local",
262
+ "country": "KR",
263
+ "vendor": "ExLibris"
264
+ },
265
+ "KAIST": {
266
+ "url": "https://library.kaist.ac.kr/openurl",
267
+ "country": "KR",
268
+ "vendor": "Custom"
269
+ },
270
+
271
+ # Brazil
272
+ "University of São Paulo": {
273
+ "url": "http://www.buscaintegrada.usp.br/openurl",
274
+ "country": "BR",
275
+ "vendor": "Custom"
276
+ },
277
+
278
+ # Mexico
279
+ "UNAM": {
280
+ "url": "https://pbidi.unam.mx/login?url=",
281
+ "country": "MX",
282
+ "vendor": "Custom"
283
+ },
284
+
285
+ # India
286
+ "IIT Delhi": {
287
+ "url": "https://libproxy.iitd.ac.in/login?url=",
288
+ "country": "IN",
289
+ "vendor": "Custom"
290
+ },
291
+ "Indian Institute of Science": {
292
+ "url": "https://library.iisc.ac.in/openurl",
293
+ "country": "IN",
294
+ "vendor": "Custom"
295
+ }
296
+ }
297
+
298
+ # Generic OpenURL resolver patterns
299
+ GENERIC_PATTERNS = [
300
+ # ExLibris SFX patterns
301
+ r"https?://[^/]+/sfx[^/]*",
302
+ r"https?://sfx\.[^/]+",
303
+ r"https?://[^/]+\.exlibrisgroup\.com",
304
+
305
+ # SerialsSolutions patterns
306
+ r"https?://[^/]+\.serialssolutions\.com",
307
+ r"https?://[^/]+/360link",
308
+
309
+ # OCLC patterns
310
+ r"https?://[^/]+\.idm\.oclc\.org",
311
+ r"https?://[^/]+\.worldcat\.org",
312
+
313
+ # Common proxy patterns
314
+ r"https?://[^/]+/login\?url=",
315
+ r"https?://libproxy\.[^/]+",
316
+ r"https?://proxy\.[^/]+",
317
+
318
+ # OpenURL patterns
319
+ r"https?://[^/]+/openurl",
320
+ r"https?://[^/]+/openurlresolver",
321
+ ]
322
+
323
+
324
+ def get_resolver_by_institution(institution_name: str) -> Optional[Dict[str, str]]:
325
+ """
326
+ Get OpenURL resolver information by institution name.
327
+
328
+ Args:
329
+ institution_name: Name of the institution
330
+
331
+ Returns:
332
+ Dict with 'url', 'country', and 'vendor' if found, None otherwise
333
+ """
334
+ # Try exact match first
335
+ if institution_name in KNOWN_RESOLVERS:
336
+ return KNOWN_RESOLVERS[institution_name].copy()
337
+
338
+ # Try case-insensitive match
339
+ institution_lower = institution_name.lower()
340
+ for name, info in KNOWN_RESOLVERS.items():
341
+ if name.lower() == institution_lower:
342
+ return info.copy()
343
+
344
+ # Try partial match
345
+ for name, info in KNOWN_RESOLVERS.items():
346
+ if institution_lower in name.lower() or name.lower() in institution_lower:
347
+ return info.copy()
348
+
349
+ return None
350
+
351
+
352
+ def get_resolvers_by_country(country_code: str) -> Dict[str, Dict[str, str]]:
353
+ """
354
+ Get all OpenURL resolvers for a specific country.
355
+
356
+ Args:
357
+ country_code: Two-letter country code (e.g., 'US', 'UK', 'AU')
358
+
359
+ Returns:
360
+ Dict of institution names to resolver info
361
+ """
362
+ country_code = country_code.upper()
363
+ return {
364
+ name: info
365
+ for name, info in KNOWN_RESOLVERS.items()
366
+ if info.get('country') == country_code
367
+ }
368
+
369
+
370
+ def get_resolvers_by_vendor(vendor_name: str) -> Dict[str, Dict[str, str]]:
371
+ """
372
+ Get all OpenURL resolvers using a specific vendor.
373
+
374
+ Args:
375
+ vendor_name: Vendor name (e.g., 'ExLibris', 'OCLC')
376
+
377
+ Returns:
378
+ Dict of institution names to resolver info
379
+ """
380
+ return {
381
+ name: info
382
+ for name, info in KNOWN_RESOLVERS.items()
383
+ if info.get('vendor', '').lower() == vendor_name.lower()
384
+ }
385
+
386
+
387
+ def validate_resolver_url(url: str) -> bool:
388
+ """
389
+ Check if a URL looks like a valid OpenURL resolver.
390
+
391
+ Args:
392
+ url: URL to validate
393
+
394
+ Returns:
395
+ True if URL matches known resolver patterns
396
+ """
397
+ import re
398
+
399
+ # Check against known resolver URLs
400
+ for info in KNOWN_RESOLVERS.values():
401
+ if url.startswith(info['url']):
402
+ return True
403
+
404
+ # Check against generic patterns
405
+ for pattern in GENERIC_PATTERNS:
406
+ if re.match(pattern, url):
407
+ return True
408
+
409
+ return False
410
+
411
+
412
+ def get_all_resolvers() -> List[Dict[str, str]]:
413
+ """
414
+ Get all known resolvers as a list.
415
+
416
+ Returns:
417
+ List of dicts with 'name', 'url', 'country', 'vendor'
418
+ """
419
+ return [
420
+ {
421
+ 'name': name,
422
+ 'url': info['url'],
423
+ 'country': info.get('country', 'Unknown'),
424
+ 'vendor': info.get('vendor', 'Unknown')
425
+ }
426
+ for name, info in KNOWN_RESOLVERS.items()
427
+ ]
428
+
429
+
430
+ # Common test DOIs for different publishers
431
+ TEST_DOIS = {
432
+ "Nature": "10.1038/nature12373",
433
+ "Science": "10.1126/science.1234567",
434
+ "Cell": "10.1016/j.cell.2020.01.001",
435
+ "Elsevier": "10.1016/j.neuroimage.2020.116584",
436
+ "Wiley": "10.1111/jnc.15327",
437
+ "Springer": "10.1007/s00401-021-02283-6",
438
+ "Oxford": "10.1093/brain/awaa123",
439
+ "IEEE": "10.1109/TPAMI.2020.2984611",
440
+ "ACS": "10.1021/acs.jmedchem.0c00606",
441
+ "PNAS": "10.1073/pnas.1921909117"
442
+ }
443
+
444
+
445
+ if __name__ == "__main__":
446
+ # Example usage
447
+ print(f"Total known resolvers: {len(KNOWN_RESOLVERS)}")
448
+ print(f"\nCountries represented: {len(set(info['country'] for info in KNOWN_RESOLVERS.values()))}")
449
+ print(f"Vendors: {set(info.get('vendor', 'Unknown') for info in KNOWN_RESOLVERS.values())}")
450
+
451
+ # Example: Find resolver for an institution
452
+ resolver = get_resolver_by_institution("Harvard")
453
+ if resolver:
454
+ print(f"\nHarvard resolver: {resolver['url']}")
455
+
456
+ # Example: Get all US resolvers
457
+ us_resolvers = get_resolvers_by_country("US")
458
+ print(f"\nUS institutions with resolvers: {len(us_resolvers)}")
459
+
460
+ # Example: Get all ExLibris resolvers
461
+ exlibris = get_resolvers_by_vendor("ExLibris")
462
+ print(f"Institutions using ExLibris SFX: {len(exlibris)}")