paperhound 0.1.0__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.
@@ -0,0 +1,23 @@
1
+ .venv/
2
+ .env
3
+ .env.*
4
+
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ *.pyd
9
+
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .coverage
13
+ .coverage.*
14
+ htmlcov/
15
+ coverage.xml
16
+
17
+ dist/
18
+ build/
19
+ *.egg-info/
20
+
21
+ .vscode/
22
+ .idea/
23
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Fernández Camello
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: paperhound
3
+ Version: 0.1.0
4
+ Summary: CLI to search, download, and convert academic papers (arXiv, Semantic Scholar) into Markdown — built for AI/ML researchers.
5
+ Project-URL: Homepage, https://github.com/alexfdez1010/paperhound
6
+ Project-URL: Repository, https://github.com/alexfdez1010/paperhound
7
+ Project-URL: Issues, https://github.com/alexfdez1010/paperhound/issues
8
+ Author-email: Alejandro Fernández Camello <alejandrofernandezcamello@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Alejandro Fernández Camello
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: academic,arxiv,cli,docling,machine-learning,markdown,papers,phd,research,semantic-scholar
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: Intended Audience :: Science/Research
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
44
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
45
+ Classifier: Topic :: Utilities
46
+ Requires-Python: >=3.10
47
+ Requires-Dist: arxiv>=2.1.3
48
+ Requires-Dist: docling>=2.0.0
49
+ Requires-Dist: httpx>=0.27.0
50
+ Requires-Dist: pydantic>=2.7.0
51
+ Requires-Dist: rich>=13.7.0
52
+ Requires-Dist: typer>=0.12.0
53
+ Provides-Extra: dev
54
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
55
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
56
+ Requires-Dist: respx>=0.21.0; extra == 'dev'
57
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
58
+ Description-Content-Type: text/markdown
59
+
60
+ # paperhound
61
+
62
+ [![PyPI](https://img.shields.io/pypi/v/paperhound.svg)](https://pypi.org/project/paperhound/)
63
+ [![Python versions](https://img.shields.io/pypi/pyversions/paperhound.svg)](https://pypi.org/project/paperhound/)
64
+ [![License](https://img.shields.io/pypi/l/paperhound.svg)](LICENSE)
65
+
66
+ > **paperhound** — sniff out academic papers from the command line.
67
+
68
+ A small, fast CLI for AI/ML researchers who want a single tool to **search**,
69
+ **inspect**, **download**, and **convert to Markdown** papers from arXiv and
70
+ Semantic Scholar. Conversion is powered by [docling](https://github.com/docling-project/docling),
71
+ so the resulting Markdown is good enough to feed straight into an LLM context.
72
+
73
+ ## Features
74
+
75
+ - 🔎 **Unified search** — one query, all backends. arXiv and Semantic Scholar
76
+ are queried in parallel and the results are merged and deduplicated.
77
+ - 📄 **Inspect before downloading** — `paperhound show <id>` prints the
78
+ abstract and metadata so you can decide if it's worth a download.
79
+ - ⬇️ **Download by identifier** — arXiv id, DOI, Semantic Scholar paper id, or
80
+ any paper URL. Open-access PDFs are resolved automatically.
81
+ - 📝 **PDF → Markdown via docling** — `paperhound convert paper.pdf` or
82
+ `paperhound get <id>` for the full pipeline.
83
+ - 🤖 **Agent-ready** — ships with a `SKILL.md` and JSON output mode so any
84
+ Claude / OpenAI / local agent can drive the CLI.
85
+ - 🧪 **Heavily tested** — every module has unit tests; live integration tests
86
+ are gated behind an environment variable.
87
+
88
+ ## Installation
89
+
90
+ ```bash
91
+ pip install paperhound
92
+ ```
93
+
94
+ or with [uv](https://docs.astral.sh/uv/):
95
+
96
+ ```bash
97
+ uv tool install paperhound
98
+ ```
99
+
100
+ Python 3.10+ is required. Docling pulls in PyTorch on first run, so the very
101
+ first conversion may take a moment to download model weights.
102
+
103
+ ## Quick start
104
+
105
+ ```bash
106
+ # Search across all providers
107
+ paperhound search "diffusion transformers" --limit 5
108
+
109
+ # Show the abstract for a specific paper
110
+ paperhound show 2401.12345
111
+ paperhound show 10.1038/s41586-020-2649-2 # DOI works too
112
+ paperhound show https://arxiv.org/abs/1706.03762 # ...and URLs
113
+
114
+ # Download the PDF
115
+ paperhound download 1706.03762 -o ./papers/
116
+
117
+ # Convert a local PDF to Markdown
118
+ paperhound convert ./papers/1706.03762.pdf -o attention.md
119
+
120
+ # Or do it all at once: search-resolve, download, convert, clean up
121
+ paperhound get 1706.03762 -o attention.md
122
+ ```
123
+
124
+ ### JSON output for scripts and agents
125
+
126
+ ```bash
127
+ paperhound search "graph neural networks" --json | jq '.[].title'
128
+ paperhound show 1706.03762 --json
129
+ ```
130
+
131
+ ## Commands
132
+
133
+ | Command | Description |
134
+ |---|---|
135
+ | `paperhound search <query>` | Run a unified search. `--limit`, `--source arxiv\|semantic_scholar`, `--year-min`, `--year-max`, `--json`. |
136
+ | `paperhound show <id>` | Fetch a paper's metadata + abstract. |
137
+ | `paperhound download <id> -o <path>` | Download a paper PDF. |
138
+ | `paperhound convert <pdf> -o <md>` | Convert a PDF (or any docling-supported file/URL) to Markdown. |
139
+ | `paperhound get <id> -o <md>` | Download + convert in one step. `--keep-pdf` to keep the PDF. |
140
+ | `paperhound version` | Print the installed version. |
141
+
142
+ Run `paperhound <command> --help` for full options.
143
+
144
+ ## Identifier formats
145
+
146
+ paperhound accepts whatever you have on hand:
147
+
148
+ - arXiv ids: `2401.12345`, `2401.12345v3`, `cs.AI/0301001`, `arXiv:2401.12345`
149
+ - DOIs: `10.1038/s41586-020-2649-2`, `doi:10.1038/...`
150
+ - Semantic Scholar paper ids: 40-char hex
151
+ - URLs: `arxiv.org/abs/...`, `arxiv.org/pdf/...`, `doi.org/...`,
152
+ `semanticscholar.org/paper/...`
153
+
154
+ ## Configuration
155
+
156
+ | Env var | Purpose |
157
+ |---|---|
158
+ | `SEMANTIC_SCHOLAR_API_KEY` | Optional. Lifts the public rate limit for the Semantic Scholar Graph API. |
159
+ | `PAPERHOUND_RUN_INTEGRATION` | Set to `1` to run live integration tests. |
160
+
161
+ ## Use it from agents
162
+
163
+ paperhound is designed to be driven by AI agents. The repo includes a ready-to-install
164
+ [skill at `skills/paperhound/SKILL.md`](skills/paperhound/SKILL.md) that documents
165
+ every command, recommends the JSON output flag, and gives an end-to-end example.
166
+ Drop the skill into your agent's skill directory (e.g. `~/.claude/skills/`) and
167
+ the agent will know how to search papers, fetch abstracts, and produce Markdown.
168
+
169
+ ## Development
170
+
171
+ ```bash
172
+ make install # uv sync --extra dev
173
+ make test # unit tests
174
+ make test-integration # live API tests (PAPERHOUND_RUN_INTEGRATION=1)
175
+ make check # lint + format check + tests (run before pushing)
176
+ ```
177
+
178
+ The test suite uses `respx` to record/replay HTTP, so unit tests do not touch
179
+ the network. Provider clients are dependency-injected, which makes the
180
+ aggregator and CLI fully unit-testable.
181
+
182
+ ## Releasing to PyPI
183
+
184
+ 1. Bump `version` in `pyproject.toml` and `paperhound/__init__.py`.
185
+ 2. Tag the release: `git tag v0.1.1 && git push --tags`.
186
+ 3. The `Publish to PyPI` GitHub Action builds and publishes via
187
+ [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — no
188
+ API token required, just configure the trusted publisher once on PyPI.
189
+
190
+ ## License
191
+
192
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,133 @@
1
+ # paperhound
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/paperhound.svg)](https://pypi.org/project/paperhound/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/paperhound.svg)](https://pypi.org/project/paperhound/)
5
+ [![License](https://img.shields.io/pypi/l/paperhound.svg)](LICENSE)
6
+
7
+ > **paperhound** — sniff out academic papers from the command line.
8
+
9
+ A small, fast CLI for AI/ML researchers who want a single tool to **search**,
10
+ **inspect**, **download**, and **convert to Markdown** papers from arXiv and
11
+ Semantic Scholar. Conversion is powered by [docling](https://github.com/docling-project/docling),
12
+ so the resulting Markdown is good enough to feed straight into an LLM context.
13
+
14
+ ## Features
15
+
16
+ - 🔎 **Unified search** — one query, all backends. arXiv and Semantic Scholar
17
+ are queried in parallel and the results are merged and deduplicated.
18
+ - 📄 **Inspect before downloading** — `paperhound show <id>` prints the
19
+ abstract and metadata so you can decide if it's worth a download.
20
+ - ⬇️ **Download by identifier** — arXiv id, DOI, Semantic Scholar paper id, or
21
+ any paper URL. Open-access PDFs are resolved automatically.
22
+ - 📝 **PDF → Markdown via docling** — `paperhound convert paper.pdf` or
23
+ `paperhound get <id>` for the full pipeline.
24
+ - 🤖 **Agent-ready** — ships with a `SKILL.md` and JSON output mode so any
25
+ Claude / OpenAI / local agent can drive the CLI.
26
+ - 🧪 **Heavily tested** — every module has unit tests; live integration tests
27
+ are gated behind an environment variable.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install paperhound
33
+ ```
34
+
35
+ or with [uv](https://docs.astral.sh/uv/):
36
+
37
+ ```bash
38
+ uv tool install paperhound
39
+ ```
40
+
41
+ Python 3.10+ is required. Docling pulls in PyTorch on first run, so the very
42
+ first conversion may take a moment to download model weights.
43
+
44
+ ## Quick start
45
+
46
+ ```bash
47
+ # Search across all providers
48
+ paperhound search "diffusion transformers" --limit 5
49
+
50
+ # Show the abstract for a specific paper
51
+ paperhound show 2401.12345
52
+ paperhound show 10.1038/s41586-020-2649-2 # DOI works too
53
+ paperhound show https://arxiv.org/abs/1706.03762 # ...and URLs
54
+
55
+ # Download the PDF
56
+ paperhound download 1706.03762 -o ./papers/
57
+
58
+ # Convert a local PDF to Markdown
59
+ paperhound convert ./papers/1706.03762.pdf -o attention.md
60
+
61
+ # Or do it all at once: search-resolve, download, convert, clean up
62
+ paperhound get 1706.03762 -o attention.md
63
+ ```
64
+
65
+ ### JSON output for scripts and agents
66
+
67
+ ```bash
68
+ paperhound search "graph neural networks" --json | jq '.[].title'
69
+ paperhound show 1706.03762 --json
70
+ ```
71
+
72
+ ## Commands
73
+
74
+ | Command | Description |
75
+ |---|---|
76
+ | `paperhound search <query>` | Run a unified search. `--limit`, `--source arxiv\|semantic_scholar`, `--year-min`, `--year-max`, `--json`. |
77
+ | `paperhound show <id>` | Fetch a paper's metadata + abstract. |
78
+ | `paperhound download <id> -o <path>` | Download a paper PDF. |
79
+ | `paperhound convert <pdf> -o <md>` | Convert a PDF (or any docling-supported file/URL) to Markdown. |
80
+ | `paperhound get <id> -o <md>` | Download + convert in one step. `--keep-pdf` to keep the PDF. |
81
+ | `paperhound version` | Print the installed version. |
82
+
83
+ Run `paperhound <command> --help` for full options.
84
+
85
+ ## Identifier formats
86
+
87
+ paperhound accepts whatever you have on hand:
88
+
89
+ - arXiv ids: `2401.12345`, `2401.12345v3`, `cs.AI/0301001`, `arXiv:2401.12345`
90
+ - DOIs: `10.1038/s41586-020-2649-2`, `doi:10.1038/...`
91
+ - Semantic Scholar paper ids: 40-char hex
92
+ - URLs: `arxiv.org/abs/...`, `arxiv.org/pdf/...`, `doi.org/...`,
93
+ `semanticscholar.org/paper/...`
94
+
95
+ ## Configuration
96
+
97
+ | Env var | Purpose |
98
+ |---|---|
99
+ | `SEMANTIC_SCHOLAR_API_KEY` | Optional. Lifts the public rate limit for the Semantic Scholar Graph API. |
100
+ | `PAPERHOUND_RUN_INTEGRATION` | Set to `1` to run live integration tests. |
101
+
102
+ ## Use it from agents
103
+
104
+ paperhound is designed to be driven by AI agents. The repo includes a ready-to-install
105
+ [skill at `skills/paperhound/SKILL.md`](skills/paperhound/SKILL.md) that documents
106
+ every command, recommends the JSON output flag, and gives an end-to-end example.
107
+ Drop the skill into your agent's skill directory (e.g. `~/.claude/skills/`) and
108
+ the agent will know how to search papers, fetch abstracts, and produce Markdown.
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ make install # uv sync --extra dev
114
+ make test # unit tests
115
+ make test-integration # live API tests (PAPERHOUND_RUN_INTEGRATION=1)
116
+ make check # lint + format check + tests (run before pushing)
117
+ ```
118
+
119
+ The test suite uses `respx` to record/replay HTTP, so unit tests do not touch
120
+ the network. Provider clients are dependency-injected, which makes the
121
+ aggregator and CLI fully unit-testable.
122
+
123
+ ## Releasing to PyPI
124
+
125
+ 1. Bump `version` in `pyproject.toml` and `paperhound/__init__.py`.
126
+ 2. Tag the release: `git tag v0.1.1 && git push --tags`.
127
+ 3. The `Publish to PyPI` GitHub Action builds and publishes via
128
+ [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — no
129
+ API token required, just configure the trusted publisher once on PyPI.
130
+
131
+ ## License
132
+
133
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,95 @@
1
+ [project]
2
+ name = "paperhound"
3
+ version = "0.1.0"
4
+ description = "CLI to search, download, and convert academic papers (arXiv, Semantic Scholar) into Markdown — built for AI/ML researchers."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { file = "LICENSE" }
8
+ authors = [
9
+ { name = "Alejandro Fernández Camello", email = "alejandrofernandezcamello@gmail.com" },
10
+ ]
11
+ keywords = [
12
+ "arxiv",
13
+ "semantic-scholar",
14
+ "papers",
15
+ "research",
16
+ "cli",
17
+ "docling",
18
+ "markdown",
19
+ "academic",
20
+ "machine-learning",
21
+ "phd",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 3 - Alpha",
25
+ "Environment :: Console",
26
+ "Intended Audience :: Science/Research",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
36
+ "Topic :: Text Processing :: Markup :: Markdown",
37
+ "Topic :: Utilities",
38
+ ]
39
+ dependencies = [
40
+ "typer>=0.12.0",
41
+ "rich>=13.7.0",
42
+ "httpx>=0.27.0",
43
+ "arxiv>=2.1.3",
44
+ "docling>=2.0.0",
45
+ "pydantic>=2.7.0",
46
+ ]
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/alexfdez1010/paperhound"
50
+ Repository = "https://github.com/alexfdez1010/paperhound"
51
+ Issues = "https://github.com/alexfdez1010/paperhound/issues"
52
+
53
+ [project.scripts]
54
+ paperhound = "paperhound.cli:app"
55
+
56
+ [project.optional-dependencies]
57
+ dev = [
58
+ "pytest>=8.0.0",
59
+ "pytest-cov>=5.0.0",
60
+ "respx>=0.21.0",
61
+ "ruff>=0.6.0",
62
+ ]
63
+
64
+ [build-system]
65
+ requires = ["hatchling"]
66
+ build-backend = "hatchling.build"
67
+
68
+ [tool.hatch.build.targets.wheel]
69
+ packages = ["src/paperhound"]
70
+
71
+ [tool.hatch.build.targets.sdist]
72
+ include = ["src/paperhound", "README.md", "LICENSE", "pyproject.toml"]
73
+
74
+ [tool.pytest.ini_options]
75
+ addopts = "-v"
76
+ testpaths = ["tests"]
77
+ markers = [
78
+ "integration: tests that hit live APIs (deselect with '-m \"not integration\"')",
79
+ ]
80
+
81
+ [tool.ruff]
82
+ line-length = 100
83
+ target-version = "py310"
84
+
85
+ [tool.ruff.lint]
86
+ select = ["E", "F", "I", "B", "UP", "W"]
87
+ ignore = ["E501"]
88
+
89
+ [tool.ruff.lint.flake8-bugbear]
90
+ # Typer relies on typer.Argument()/typer.Option() in defaults; that is the
91
+ # documented API, not a bug.
92
+ extend-immutable-calls = [
93
+ "typer.Argument",
94
+ "typer.Option",
95
+ ]
@@ -0,0 +1,7 @@
1
+ """paperhound — search, download, and convert academic papers from the command line."""
2
+
3
+ from paperhound.models import Author, Paper, PaperIdentifier
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["Author", "Paper", "PaperIdentifier", "__version__"]
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m paperhound`` to run the CLI."""
2
+
3
+ from paperhound.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,213 @@
1
+ """paperhound command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from paperhound import __version__
13
+ from paperhound.convert import convert_to_markdown
14
+ from paperhound.download import download_pdf, resolve_pdf_url
15
+ from paperhound.errors import PaperhoundError
16
+ from paperhound.identifiers import IdentifierKind, detect
17
+ from paperhound.output import papers_to_json, render_paper_detail, render_table
18
+ from paperhound.search import (
19
+ ArxivProvider,
20
+ SearchAggregator,
21
+ SearchQuery,
22
+ SemanticScholarProvider,
23
+ )
24
+
25
+ app = typer.Typer(
26
+ add_completion=False,
27
+ no_args_is_help=True,
28
+ help="Search, download, and convert academic papers from the command line.",
29
+ )
30
+
31
+ console = Console()
32
+ err_console = Console(stderr=True)
33
+
34
+
35
+ def _build_aggregator(sources: list[str] | None = None) -> SearchAggregator:
36
+ chosen = {s.lower() for s in sources} if sources else {"arxiv", "semantic_scholar"}
37
+ providers = []
38
+ if "arxiv" in chosen:
39
+ providers.append(ArxivProvider())
40
+ if "semantic_scholar" in chosen or "s2" in chosen:
41
+ providers.append(SemanticScholarProvider())
42
+ if not providers:
43
+ raise typer.BadParameter(f"Unknown sources: {sources!r}")
44
+ return SearchAggregator(providers)
45
+
46
+
47
+ def _exit_on_error(exc: Exception) -> None:
48
+ err_console.print(f"[red]error:[/red] {exc}")
49
+ raise typer.Exit(code=1)
50
+
51
+
52
+ @app.callback()
53
+ def _root(
54
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging."),
55
+ ) -> None:
56
+ logging.basicConfig(
57
+ level=logging.DEBUG if verbose else logging.WARNING,
58
+ format="%(levelname)s %(name)s: %(message)s",
59
+ )
60
+
61
+
62
+ @app.command()
63
+ def version() -> None:
64
+ """Print the installed paperhound version."""
65
+ console.print(__version__)
66
+
67
+
68
+ @app.command()
69
+ def search(
70
+ query: str = typer.Argument(..., help="Free-text query."),
71
+ limit: int = typer.Option(10, "--limit", "-n", min=1, max=100),
72
+ source: list[str] | None = typer.Option(
73
+ None,
74
+ "--source",
75
+ "-s",
76
+ help="Restrict to a provider (arxiv, semantic_scholar). Repeatable.",
77
+ ),
78
+ year_min: int | None = typer.Option(None, "--year-min"),
79
+ year_max: int | None = typer.Option(None, "--year-max"),
80
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON instead of a table."),
81
+ ) -> None:
82
+ """Search across all providers and print the merged results."""
83
+ aggregator = _build_aggregator(source)
84
+ try:
85
+ papers = aggregator.search(
86
+ SearchQuery(
87
+ text=query,
88
+ limit=limit,
89
+ year_min=year_min,
90
+ year_max=year_max,
91
+ )
92
+ )
93
+ except PaperhoundError as exc:
94
+ _exit_on_error(exc)
95
+ return
96
+
97
+ if not papers:
98
+ err_console.print("[yellow]No results.[/yellow]")
99
+ raise typer.Exit(code=0)
100
+
101
+ if json_output:
102
+ sys.stdout.write(papers_to_json(papers) + "\n")
103
+ else:
104
+ render_table(papers, console)
105
+
106
+
107
+ @app.command()
108
+ def show(
109
+ identifier: str = typer.Argument(..., help="arXiv id, DOI, S2 id, or paper URL."),
110
+ json_output: bool = typer.Option(False, "--json"),
111
+ ) -> None:
112
+ """Fetch a paper's metadata and abstract."""
113
+ aggregator = _build_aggregator()
114
+ try:
115
+ paper = aggregator.get(identifier)
116
+ except PaperhoundError as exc:
117
+ _exit_on_error(exc)
118
+ return
119
+
120
+ if paper is None:
121
+ err_console.print(f"[yellow]Not found:[/yellow] {identifier}")
122
+ raise typer.Exit(code=1)
123
+
124
+ if json_output:
125
+ sys.stdout.write(paper.model_dump_json(indent=2) + "\n")
126
+ else:
127
+ render_paper_detail(paper, console)
128
+
129
+
130
+ def _lookup_pdf_url(identifier: str) -> str | None:
131
+ """Aggregator-backed callback used by ``download`` for non-arXiv ids."""
132
+ aggregator = _build_aggregator()
133
+ paper = aggregator.get(identifier)
134
+ return paper.pdf_url if paper else None
135
+
136
+
137
+ @app.command()
138
+ def download(
139
+ identifier: str = typer.Argument(..., help="arXiv id, DOI, S2 id, or paper URL."),
140
+ output: Path | None = typer.Option(
141
+ None,
142
+ "--output",
143
+ "-o",
144
+ help="Destination file or directory. Defaults to the current directory.",
145
+ ),
146
+ ) -> Path:
147
+ """Download a paper PDF."""
148
+ destination = output if output is not None else Path.cwd()
149
+ try:
150
+ url = resolve_pdf_url(identifier, lookup_pdf_url=_lookup_pdf_url)
151
+ path = download_pdf(url, destination)
152
+ except PaperhoundError as exc:
153
+ _exit_on_error(exc)
154
+ return Path()
155
+ console.print(f"[green]Saved[/green] {path}")
156
+ return path
157
+
158
+
159
+ @app.command()
160
+ def convert(
161
+ source: str = typer.Argument(..., help="Path or URL to a PDF / supported document."),
162
+ output: Path | None = typer.Option(None, "--output", "-o"),
163
+ ) -> None:
164
+ """Convert a document to Markdown via docling."""
165
+ try:
166
+ markdown = convert_to_markdown(source, output=output)
167
+ except PaperhoundError as exc:
168
+ _exit_on_error(exc)
169
+ return
170
+
171
+ if output is None:
172
+ sys.stdout.write(markdown)
173
+ else:
174
+ console.print(f"[green]Wrote[/green] {output}")
175
+
176
+
177
+ @app.command()
178
+ def get(
179
+ identifier: str = typer.Argument(..., help="arXiv id, DOI, S2 id, or paper URL."),
180
+ output: Path | None = typer.Option(
181
+ None,
182
+ "--output",
183
+ "-o",
184
+ help="Markdown output path. Defaults to <id>.md in the current directory.",
185
+ ),
186
+ keep_pdf: bool = typer.Option(False, "--keep-pdf", help="Keep the downloaded PDF."),
187
+ ) -> None:
188
+ """Download a paper and convert it to Markdown in one step."""
189
+ try:
190
+ kind, value = detect(identifier)
191
+ except PaperhoundError as exc:
192
+ _exit_on_error(exc)
193
+ return
194
+
195
+ md_default_stem = value.replace("/", "_") if kind is IdentifierKind.ARXIV else value
196
+ md_path = output or Path.cwd() / f"{md_default_stem}.md"
197
+ pdf_dest = (md_path.parent if keep_pdf else Path.cwd()) / f"{md_default_stem}.pdf"
198
+
199
+ try:
200
+ url = resolve_pdf_url(identifier, lookup_pdf_url=_lookup_pdf_url)
201
+ pdf_path = download_pdf(url, pdf_dest)
202
+ convert_to_markdown(pdf_path, output=md_path)
203
+ except PaperhoundError as exc:
204
+ _exit_on_error(exc)
205
+ return
206
+ finally:
207
+ if not keep_pdf and pdf_dest.exists():
208
+ try:
209
+ pdf_dest.unlink()
210
+ except OSError:
211
+ pass
212
+
213
+ console.print(f"[green]Wrote[/green] {md_path}")