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.
- paperhound-0.1.0/.gitignore +23 -0
- paperhound-0.1.0/LICENSE +21 -0
- paperhound-0.1.0/PKG-INFO +192 -0
- paperhound-0.1.0/README.md +133 -0
- paperhound-0.1.0/pyproject.toml +95 -0
- paperhound-0.1.0/src/paperhound/__init__.py +7 -0
- paperhound-0.1.0/src/paperhound/__main__.py +6 -0
- paperhound-0.1.0/src/paperhound/cli.py +213 -0
- paperhound-0.1.0/src/paperhound/convert.py +52 -0
- paperhound-0.1.0/src/paperhound/download.py +76 -0
- paperhound-0.1.0/src/paperhound/errors.py +21 -0
- paperhound-0.1.0/src/paperhound/identifiers.py +101 -0
- paperhound-0.1.0/src/paperhound/models.py +71 -0
- paperhound-0.1.0/src/paperhound/output.py +67 -0
- paperhound-0.1.0/src/paperhound/search/__init__.py +14 -0
- paperhound-0.1.0/src/paperhound/search/aggregator.py +84 -0
- paperhound-0.1.0/src/paperhound/search/arxiv_provider.py +66 -0
- paperhound-0.1.0/src/paperhound/search/base.py +32 -0
- paperhound-0.1.0/src/paperhound/search/semantic_scholar.py +126 -0
paperhound-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/paperhound/)
|
|
63
|
+
[](https://pypi.org/project/paperhound/)
|
|
64
|
+
[](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
|
+
[](https://pypi.org/project/paperhound/)
|
|
4
|
+
[](https://pypi.org/project/paperhound/)
|
|
5
|
+
[](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,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}")
|