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.
- scitex/__init__.py +47 -0
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +191 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/writer.py +21 -204
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +127 -59
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/engines/elevenlabs_engine.py +6 -1
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +443 -0
- scitex/cli/main.py +198 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/scholar/__init__.py +8 -0
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/writer.py +117 -0
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +191 -0
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/introspect/__init__.py +75 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +42 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/introspect/_mcp/__init__.py +37 -0
- scitex/introspect/_mcp/handlers.py +208 -0
- scitex/introspect/_members.py +151 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/bundle/README.md +1 -1
- scitex/mcp_server.py +98 -5
- scitex/plt/__init__.py +248 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/sh/README.md +1 -1
- scitex/social/__init__.py +153 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/template/README.md +1 -1
- scitex/template/clone_writer_directory.py +5 -5
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.1.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.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)}")
|