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.
Files changed (76) hide show
  1. {scholarcli-1.2 → scholarcli-1.3}/PKG-INFO +1 -1
  2. {scholarcli-1.2 → scholarcli-1.3}/pyproject.toml +1 -1
  3. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/cli.py +65 -60
  4. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/providers.py +5 -3
  5. {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/PKG-INFO +1 -1
  6. {scholarcli-1.2 → scholarcli-1.3}/tests/test_providers.py +10 -3
  7. {scholarcli-1.2 → scholarcli-1.3}/LICENSE +0 -0
  8. {scholarcli-1.2 → scholarcli-1.3}/README.md +0 -0
  9. {scholarcli-1.2 → scholarcli-1.3}/setup.cfg +0 -0
  10. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/__init__.py +0 -0
  11. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/__main__.py +0 -0
  12. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/cache.py +0 -0
  13. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/enrich.py +0 -0
  14. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/llm_review.py +0 -0
  15. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/notes.py +0 -0
  16. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/pdf.py +0 -0
  17. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/review.py +0 -0
  18. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/scholar.py +0 -0
  19. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/tui.py +0 -0
  20. {scholarcli-1.2 → scholarcli-1.3}/src/scholar/utils.py +0 -0
  21. {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/SOURCES.txt +0 -0
  22. {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/dependency_links.txt +0 -0
  23. {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/entry_points.txt +0 -0
  24. {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/requires.txt +0 -0
  25. {scholarcli-1.2 → scholarcli-1.3}/src/scholarcli.egg-info/top_level.txt +0 -0
  26. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/__init__.py +0 -0
  27. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/__init__.py +0 -0
  28. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/aggregator.py +0 -0
  29. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/arxiv.py +0 -0
  30. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/base.py +0 -0
  31. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/crossref.py +0 -0
  32. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/google_scholar.py +0 -0
  33. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/openalex.py +0 -0
  34. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/opencitations.py +0 -0
  35. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/apis/semantic_scholar.py +0 -0
  36. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/cli.py +0 -0
  37. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/exporters/__init__.py +0 -0
  38. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/exporters/bibtex.py +0 -0
  39. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/exporters/csv_exporter.py +0 -0
  40. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/exporters/tikz.py +0 -0
  41. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/filters/__init__.py +0 -0
  42. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/filters/filter_engine.py +0 -0
  43. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/models.py +0 -0
  44. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/paper_utils.py +0 -0
  45. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/parsers/__init__.py +0 -0
  46. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/parsers/pdf_parser.py +0 -0
  47. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/scoring/__init__.py +0 -0
  48. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/scoring/base.py +0 -0
  49. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/scoring/llm_scorer.py +0 -0
  50. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/scoring/tfidf_scorer.py +0 -0
  51. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/snowballing.py +0 -0
  52. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/storage/__init__.py +0 -0
  53. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/storage/json_storage.py +0 -0
  54. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/tui/__init__.py +0 -0
  55. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/tui/app.py +0 -0
  56. {scholarcli-1.2 → scholarcli-1.3}/src/snowball/visualization.py +0 -0
  57. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/__init__.py +0 -0
  58. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/analysis.py +0 -0
  59. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/cli.py +0 -0
  60. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/clustering.py +0 -0
  61. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/database.py +0 -0
  62. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/grobid.py +0 -0
  63. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/logging.py +0 -0
  64. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/models.py +0 -0
  65. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/project.py +0 -0
  66. {scholarcli-1.2 → scholarcli-1.3}/src/tuxedo/tui.py +0 -0
  67. {scholarcli-1.2 → scholarcli-1.3}/tests/test_cache.py +0 -0
  68. {scholarcli-1.2 → scholarcli-1.3}/tests/test_cli.py +0 -0
  69. {scholarcli-1.2 → scholarcli-1.3}/tests/test_enrich.py +0 -0
  70. {scholarcli-1.2 → scholarcli-1.3}/tests/test_llm_review.py +0 -0
  71. {scholarcli-1.2 → scholarcli-1.3}/tests/test_notes.py +0 -0
  72. {scholarcli-1.2 → scholarcli-1.3}/tests/test_pdf.py +0 -0
  73. {scholarcli-1.2 → scholarcli-1.3}/tests/test_review.py +0 -0
  74. {scholarcli-1.2 → scholarcli-1.3}/tests/test_scholar.py +0 -0
  75. {scholarcli-1.2 → scholarcli-1.3}/tests/test_tui.py +0 -0
  76. {scholarcli-1.2 → scholarcli-1.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scholarcli
3
- Version: 1.2
3
+ Version: 1.3
4
4
  Summary: A tool for structured literature searches across bibliographic databases
5
5
  Author-email: Daniel Bosk <dbosk@kth.se>, Ric Glassey <glassey@kth.se>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scholarcli"
3
- version = "1.2"
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 rich output, use pager if TTY
1663
- from io import StringIO
1664
- from rich.console import Console as RichConsole
1665
-
1666
- # Render to string first to check if pager needed
1667
- string_io = StringIO()
1668
- render_console = RichConsole(file=string_io, force_terminal=True)
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
- # Show kept papers
1687
- kept = session.kept_papers
1688
- if kept:
1689
- render_console.print(
1690
- f"[green bold]Kept ({len(kept)}):[/green bold]"
1691
- )
1692
- for d in kept:
1693
- tags = (
1694
- f" [dim]\\[{', '.join(d.tags)}][/dim]" if d.tags else ""
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
- render_console.print(f" • {d.paper.title}{tags}")
1697
- render_console.print()
1698
-
1699
- # Show discarded papers
1700
- discarded = session.discarded_papers
1701
- if discarded:
1702
- render_console.print(
1703
- f"[red bold]Discarded ({len(discarded)}):[/red bold]"
1704
- )
1705
- for d in discarded:
1706
- tags = (
1707
- f" [dim]\\[{', '.join(d.tags)}][/dim]" if d.tags else ""
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
- render_console.print(f" • {d.paper.title}{tags}")
1710
- render_console.print()
1711
-
1712
- # Show pending papers
1713
- pending = session.pending_papers
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 just print
1728
+ # Use pager if TTY, otherwise print directly
1724
1729
  if sys.stdout.isatty():
1725
1730
  with console.pager(styles=True):
1726
- console.print(output_text, end="")
1731
+ print_session_details(console)
1727
1732
  else:
1728
- print(output_text, end="")
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
- def get_paper_by_doi(self, doi: str) -> Paper | None:
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scholarcli
3
- Version: 1.2
3
+ Version: 1.3
4
4
  Summary: A tool for structured literature searches across bibliographic databases
5
5
  Author-email: Daniel Bosk <dbosk@kth.se>, Ric Glassey <glassey@kth.se>
6
6
  License-Expression: MIT
@@ -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
- raise arxiv.HTTPError("http://example.com", 500, "Internal Server Error")
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