scholarcli 1.2__tar.gz → 1.3__tar.gz
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.
- {scholarcli-1.2 → scholarcli-1.3}/PKG-INFO +1 -1
- {scholarcli-1.2 → scholarcli-1.3}/pyproject.toml +1 -1
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/cli.py +65 -60
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/providers.py +5 -3
- {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/PKG-INFO +1 -1
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_providers.py +10 -3
- {scholarcli-1.2 → scholarcli-1.3}/LICENSE +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/README.md +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/setup.cfg +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/__main__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/cache.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/enrich.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/llm_review.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/notes.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/pdf.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/review.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/scholar.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/tui.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholar/utils.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/SOURCES.txt +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/dependency_links.txt +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/entry_points.txt +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/requires.txt +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/top_level.txt +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/aggregator.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/arxiv.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/base.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/crossref.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/google_scholar.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/openalex.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/opencitations.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/semantic_scholar.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/cli.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/exporters/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/exporters/bibtex.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/exporters/csv_exporter.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/exporters/tikz.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/filters/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/filters/filter_engine.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/models.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/paper_utils.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/parsers/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/parsers/pdf_parser.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/scoring/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/scoring/base.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/scoring/llm_scorer.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/scoring/tfidf_scorer.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/snowballing.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/storage/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/storage/json_storage.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/tui/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/tui/app.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/snowball/visualization.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/__init__.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/analysis.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/cli.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/clustering.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/database.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/grobid.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/logging.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/models.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/project.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/tui.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_cache.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_cli.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_enrich.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_llm_review.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_notes.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_pdf.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_review.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_scholar.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_tui.py +0 -0
- {scholarcli-1.2 → scholarcli-1.3}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "scholarcli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3"
|
|
4
4
|
description = "A tool for structured literature searches across bibliographic databases"
|
|
5
5
|
authors = [{ name = "Daniel Bosk", email = "dbosk@kth.se" },
|
|
6
6
|
{ name = "Ric Glassey", email = "glassey@kth.se" }]
|
|
@@ -1629,7 +1629,6 @@ def sessions_show(
|
|
|
1629
1629
|
Uses a pager for interactive viewing when output is a terminal.
|
|
1630
1630
|
"""
|
|
1631
1631
|
import sys
|
|
1632
|
-
from rich.pager import Pager
|
|
1633
1632
|
from scholar.review import load_session
|
|
1634
1633
|
|
|
1635
1634
|
console = Console()
|
|
@@ -1659,73 +1658,79 @@ def sessions_show(
|
|
|
1659
1658
|
}
|
|
1660
1659
|
print(json.dumps(output, indent=2))
|
|
1661
1660
|
else:
|
|
1662
|
-
# Build
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
render_console.print(f"[bold]Session: {session.query}[/bold]")
|
|
1671
|
-
render_console.print(
|
|
1672
|
-
f"[dim]Date: {session.timestamp.strftime('%Y-%m-%d %H:%M')}[/dim]"
|
|
1673
|
-
)
|
|
1674
|
-
|
|
1675
|
-
# Show query-provider pairs if multiple, else simple providers list
|
|
1676
|
-
if len(session.query_provider_pairs) > 1:
|
|
1677
|
-
render_console.print("[dim]Queries by provider:[/dim]")
|
|
1678
|
-
for query, provider in session.query_provider_pairs:
|
|
1679
|
-
render_console.print(f"[dim] {provider}: {query}[/dim]")
|
|
1680
|
-
else:
|
|
1681
|
-
render_console.print(
|
|
1682
|
-
f"[dim]Providers: {', '.join(session.providers)}[/dim]"
|
|
1661
|
+
# Build output using Rich markup, render directly to console
|
|
1662
|
+
def print_session_details(con: Console) -> None:
|
|
1663
|
+
"""Print session details with Rich formatting."""
|
|
1664
|
+
con.print(f"[bold]Session: {session.query}[/bold]")
|
|
1665
|
+
con.print(
|
|
1666
|
+
f"[dim]Date: "
|
|
1667
|
+
f"{session.timestamp.strftime('%Y-%m-%d %H:%M')}[/dim]"
|
|
1683
1668
|
)
|
|
1684
|
-
render_console.print()
|
|
1685
1669
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1670
|
+
# Always show query per provider
|
|
1671
|
+
if session.query_provider_pairs:
|
|
1672
|
+
con.print("[dim]Queries by provider:[/dim]")
|
|
1673
|
+
for query, provider in session.query_provider_pairs:
|
|
1674
|
+
con.print(f"[dim] {provider}: {query}[/dim]")
|
|
1675
|
+
else:
|
|
1676
|
+
# Fallback: show providers with session query
|
|
1677
|
+
con.print("[dim]Queries by provider:[/dim]")
|
|
1678
|
+
for provider in session.providers:
|
|
1679
|
+
con.print(f"[dim] {provider}: {session.query}[/dim]")
|
|
1680
|
+
con.print()
|
|
1681
|
+
|
|
1682
|
+
# Show kept papers
|
|
1683
|
+
kept = session.kept_papers
|
|
1684
|
+
if kept:
|
|
1685
|
+
con.print(f"[green bold]Kept ({len(kept)}):[/green bold]")
|
|
1686
|
+
for d in kept:
|
|
1687
|
+
provider = (
|
|
1688
|
+
f" [dim]({d.provider})[/dim]" if d.provider else ""
|
|
1689
|
+
)
|
|
1690
|
+
tags = (
|
|
1691
|
+
f" [dim]\\[{', '.join(d.tags)}][/dim]"
|
|
1692
|
+
if d.tags
|
|
1693
|
+
else ""
|
|
1694
|
+
)
|
|
1695
|
+
con.print(f" • {d.paper.title}{provider}{tags}")
|
|
1696
|
+
con.print()
|
|
1697
|
+
|
|
1698
|
+
# Show discarded papers
|
|
1699
|
+
discarded = session.discarded_papers
|
|
1700
|
+
if discarded:
|
|
1701
|
+
con.print(
|
|
1702
|
+
f"[red bold]Discarded ({len(discarded)}):[/red bold]"
|
|
1695
1703
|
)
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1704
|
+
for d in discarded:
|
|
1705
|
+
provider = (
|
|
1706
|
+
f" [dim]({d.provider})[/dim]" if d.provider else ""
|
|
1707
|
+
)
|
|
1708
|
+
tags = (
|
|
1709
|
+
f" [dim]\\[{', '.join(d.tags)}][/dim]"
|
|
1710
|
+
if d.tags
|
|
1711
|
+
else ""
|
|
1712
|
+
)
|
|
1713
|
+
con.print(f" • {d.paper.title}{provider}{tags}")
|
|
1714
|
+
con.print()
|
|
1715
|
+
|
|
1716
|
+
# Show pending papers
|
|
1717
|
+
pending = session.pending_papers
|
|
1718
|
+
if pending:
|
|
1719
|
+
con.print(
|
|
1720
|
+
f"[yellow bold]Pending ({len(pending)}):[/yellow bold]"
|
|
1708
1721
|
)
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
if pending:
|
|
1715
|
-
render_console.print(
|
|
1716
|
-
f"[yellow bold]Pending ({len(pending)}):[/yellow bold]"
|
|
1717
|
-
)
|
|
1718
|
-
for d in pending:
|
|
1719
|
-
render_console.print(f" • {d.paper.title}")
|
|
1720
|
-
|
|
1721
|
-
output_text = string_io.getvalue()
|
|
1722
|
+
for d in pending:
|
|
1723
|
+
provider = (
|
|
1724
|
+
f" [dim]({d.provider})[/dim]" if d.provider else ""
|
|
1725
|
+
)
|
|
1726
|
+
con.print(f" • {d.paper.title}{provider}")
|
|
1722
1727
|
|
|
1723
|
-
# Use pager if TTY, otherwise
|
|
1728
|
+
# Use pager if TTY, otherwise print directly
|
|
1724
1729
|
if sys.stdout.isatty():
|
|
1725
1730
|
with console.pager(styles=True):
|
|
1726
|
-
console
|
|
1731
|
+
print_session_details(console)
|
|
1727
1732
|
else:
|
|
1728
|
-
|
|
1733
|
+
print_session_details(console)
|
|
1729
1734
|
|
|
1730
1735
|
|
|
1731
1736
|
@sessions_app.command("export")
|
|
@@ -511,7 +511,7 @@ class SemanticScholarProvider:
|
|
|
511
511
|
logger.warning("s2: %s", e)
|
|
512
512
|
return all_papers
|
|
513
513
|
|
|
514
|
-
def get_paper_by_doi(self, doi: str) -> Paper | None:
|
|
514
|
+
def get_paper_by_doi(self, doi: str | None) -> Paper | None:
|
|
515
515
|
"""
|
|
516
516
|
Fetch a single paper by its DOI.
|
|
517
517
|
|
|
@@ -888,7 +888,7 @@ class OpenAlexProvider:
|
|
|
888
888
|
logger.warning("openalex: %s", e)
|
|
889
889
|
return []
|
|
890
890
|
|
|
891
|
-
def get_paper_by_doi(self, doi: str) -> Paper | None:
|
|
891
|
+
def get_paper_by_doi(self, doi: str | None) -> Paper | None:
|
|
892
892
|
"""
|
|
893
893
|
Fetch a single paper by its DOI.
|
|
894
894
|
|
|
@@ -1897,7 +1897,9 @@ class WebOfScienceProvider:
|
|
|
1897
1897
|
logger.warning("wos: search_for_citation_report failed: %s", e)
|
|
1898
1898
|
return None
|
|
1899
1899
|
|
|
1900
|
-
|
|
1900
|
+
# Accept str | None to simplify calling code - callers can pass paper.doi
|
|
1901
|
+
# without checking for None first (see Semantic Scholar implementation notes).
|
|
1902
|
+
def get_paper_by_doi(self, doi: str | None) -> Paper | None:
|
|
1901
1903
|
"""
|
|
1902
1904
|
Look up a paper by its DOI and return it with WOS metadata.
|
|
1903
1905
|
|
|
@@ -112,7 +112,7 @@ class TestDefaultProviders:
|
|
|
112
112
|
# Re-register provider without key
|
|
113
113
|
ieee_provider = IEEEXploreProvider(api_key=None)
|
|
114
114
|
ieee_provider.api_key = None
|
|
115
|
-
register_provider(ieee_provider)
|
|
115
|
+
register_provider(ieee_provider) # type: ignore[arg-type]
|
|
116
116
|
defaults = get_default_providers()
|
|
117
117
|
assert "ieee" not in [p.name for p in defaults]
|
|
118
118
|
class TestSemanticScholarProvider:
|
|
@@ -290,6 +290,8 @@ class TestSemanticScholarGetPaperByDoi:
|
|
|
290
290
|
paper2 = provider.get_paper_by_doi("10.1234/test")
|
|
291
291
|
|
|
292
292
|
assert mock_get.call_count == 1 # Only one API call
|
|
293
|
+
assert paper1 is not None
|
|
294
|
+
assert paper2 is not None
|
|
293
295
|
assert paper1.title == paper2.title
|
|
294
296
|
|
|
295
297
|
|
|
@@ -556,6 +558,8 @@ class TestOpenAlexGetPaperByDoi:
|
|
|
556
558
|
paper2 = provider.get_paper_by_doi("10.1234/test")
|
|
557
559
|
|
|
558
560
|
assert mock_works.return_value.__getitem__.call_count == 1 # Only one API call
|
|
561
|
+
assert paper1 is not None
|
|
562
|
+
assert paper2 is not None
|
|
559
563
|
assert paper1.title == paper2.title
|
|
560
564
|
|
|
561
565
|
|
|
@@ -1599,8 +1603,9 @@ class TestIEEEXploreProvider:
|
|
|
1599
1603
|
provider = IEEEXploreProvider(api_key="test_key")
|
|
1600
1604
|
assert provider.is_available() is True
|
|
1601
1605
|
|
|
1602
|
-
def test_search_returns_empty_without_api_key(self):
|
|
1606
|
+
def test_search_returns_empty_without_api_key(self, monkeypatch):
|
|
1603
1607
|
"""search() returns empty list when no API key is configured."""
|
|
1608
|
+
monkeypatch.delenv("IEEE_API_KEY", raising=False)
|
|
1604
1609
|
provider = IEEEXploreProvider(api_key=None)
|
|
1605
1610
|
results = provider.search("test")
|
|
1606
1611
|
assert results == []
|
|
@@ -1939,7 +1944,9 @@ class TestArxivProvider:
|
|
|
1939
1944
|
nonlocal call_count
|
|
1940
1945
|
call_count += 1
|
|
1941
1946
|
if call_count < 3:
|
|
1942
|
-
|
|
1947
|
+
# arxiv.HTTPError signature is (url: str, retry: int, status: int),
|
|
1948
|
+
# not (url, status, message) as one might expect.
|
|
1949
|
+
raise arxiv.HTTPError("http://example.com", 0, 500)
|
|
1943
1950
|
return iter([mock_result])
|
|
1944
1951
|
|
|
1945
1952
|
with patch.object(provider._client, "results", side_effect=mock_results):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|