paperstack-cli 0.3.1__tar.gz → 0.4.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.
Files changed (33) hide show
  1. paperstack_cli-0.4.0/PKG-INFO +203 -0
  2. paperstack_cli-0.3.1/PKG-INFO → paperstack_cli-0.4.0/README.md +60 -20
  3. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/pyproject.toml +22 -5
  4. paperstack_cli-0.4.0/scripts/build/vendor/marked.LICENSE +36 -0
  5. paperstack_cli-0.4.0/src/paperstack/arxiv.py +125 -0
  6. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/citations.py +2 -3
  7. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/cli.py +269 -40
  8. paperstack_cli-0.4.0/src/paperstack/content/arxiv_pdf.py +253 -0
  9. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/corpora.py +27 -3
  10. paperstack_cli-0.4.0/src/paperstack/credentials.py +147 -0
  11. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/dblp_build.py +3 -1
  12. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/dblp_index.py +27 -53
  13. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/entrypoint.py +14 -1
  14. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/metadata.py +20 -8
  15. paperstack_cli-0.4.0/src/paperstack/semantic_scholar.py +175 -0
  16. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/viewer.py +38 -10
  17. paperstack_cli-0.3.1/README.md +0 -126
  18. paperstack_cli-0.3.1/src/paperstack/content/arxiv_pdf.py +0 -103
  19. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/.gitignore +0 -0
  20. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/LICENSE +0 -0
  21. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/scripts/build/site/app.js +0 -0
  22. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/scripts/build/site/entry.html +0 -0
  23. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/scripts/build/site/favicon.svg +0 -0
  24. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/scripts/build/site/index.html +0 -0
  25. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/scripts/build/site/style.css +0 -0
  26. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/scripts/build/vendor/marked.min.js +0 -0
  27. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/__init__.py +0 -0
  28. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/content/__init__.py +0 -0
  29. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/content/arxiv_source.py +0 -0
  30. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/content/vendor/latexpand +0 -0
  31. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/content/vendor/latexpand.LICENSE +0 -0
  32. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/dblp_catalog.py +0 -0
  33. {paperstack_cli-0.3.1 → paperstack_cli-0.4.0}/src/paperstack/entry_types.py +0 -0
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.5
2
+ Name: paperstack-cli
3
+ Version: 0.4.0
4
+ Summary: Review, inspect, and retrieve research sources from one CLI
5
+ Project-URL: Repository, https://github.com/MilkClouds/paperstack
6
+ Project-URL: Issues, https://github.com/MilkClouds/paperstack/issues
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: filelock>=3.20
16
+ Requires-Dist: polars>=1.43
17
+ Requires-Dist: python-dotenv>=1.2.2
18
+ Requires-Dist: pyyaml>=6
19
+ Provides-Extra: pdf
20
+ Requires-Dist: pdf-inspector<2,>=1.17; extra == 'pdf'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Paperstack
24
+
25
+ [![CI](https://github.com/MilkClouds/paperstack/actions/workflows/ci.yml/badge.svg)](https://github.com/MilkClouds/paperstack/actions/workflows/ci.yml) [![PyPI version](https://img.shields.io/pypi/v/paperstack-cli.svg)](https://pypi.org/project/paperstack-cli/) [![Python versions](https://img.shields.io/pypi/pyversions/paperstack-cli.svg)](https://pypi.org/project/paperstack-cli/) [![License](https://img.shields.io/pypi/l/paperstack-cli.svg)](https://github.com/MilkClouds/paperstack/blob/main/LICENSE)
26
+
27
+ Paperstack is an open-source CLI for maintaining, querying, and publishing an independently authored research corpus.
28
+ The software and corpus are separate: Paperstack can be public while each corpus and its editorial judgments remain
29
+ under its maintainer's control.
30
+
31
+ It can search typed reviews, initialize and validate entries, inspect external paper metadata, read arXiv sources,
32
+ manage a selected-venue DBLP index, and build a static viewer. A synthetic corpus lives in
33
+ [`examples/corpus`](https://github.com/MilkClouds/paperstack/tree/main/examples/corpus); no assessment of real work is
34
+ bundled with the package.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ uv tool install paperstack-cli
40
+ ```
41
+
42
+ PDF conversion is optional:
43
+
44
+ ```bash
45
+ uv tool install 'paperstack-cli[pdf]'
46
+ ```
47
+
48
+ The PDF extra uses `pdf-inspector` for native extraction and selective OCR.
49
+ Clean text PDFs need no external runtime, while routed OCR pages require the pinned PDFium and ONNX Runtime libraries described in the [OCR runtime guide](https://github.com/firecrawl/pdf-inspector/blob/main/docs/ocr-runtime.md).
50
+ The first routed page downloads and verifies the pinned OCR model set unless an offline model directory is configured.
51
+ When the shared libraries are not on the platform search path, point `pdf-inspector` at the extracted files:
52
+
53
+ ```bash
54
+ export PDFIUM_LIB_PATH=/absolute/path/to/libpdfium.so
55
+ export ORT_DYLIB_PATH=/absolute/path/to/libonnxruntime.so
56
+ ```
57
+
58
+ The runtime guide lists the matching downloads and filenames for Linux, macOS, and Windows.
59
+
60
+ ## AI agent skill
61
+
62
+ Paperstack ships an [Agent Skills](https://agentskills.io)-compatible skill for Codex, Claude Code, and other
63
+ compatible agents. Install the CLI first, then add the skill globally with the open
64
+ [`skills`](https://github.com/vercel-labs/skills) CLI:
65
+
66
+ ```bash
67
+ npx skills add MilkClouds/paperstack --skill paperstack -g
68
+ ```
69
+
70
+ The installer detects supported agents such as Codex and Claude Code; pass `-a codex -a claude-code` to target them
71
+ explicitly, or omit `-g` for a project-local installation. The shared [`SKILL.md`](skills/paperstack/SKILL.md) keeps
72
+ corpus selection, source inspection, entry authoring, validation, and viewer workflows consistent without exposing a
73
+ private corpus. Use `npx skills update paperstack -g` to update it later.
74
+
75
+ ## Choose a corpus
76
+
77
+ Register either a local working tree or a GitHub repository. The first registered corpus becomes active.
78
+
79
+ ```bash
80
+ paperstack corpus add work --path ~/research/reviews
81
+ paperstack corpus init new --path ~/research/new-corpus
82
+ paperstack corpus add private --repo OWNER/private-reviews
83
+ paperstack corpus list
84
+ paperstack corpus use private
85
+ paperstack corpus remove work
86
+ paperstack corpus remove private --purge-cache --yes
87
+ ```
88
+
89
+ Private GitHub repositories use the existing `gh` login without storing its token:
90
+
91
+ ```bash
92
+ gh auth login
93
+ paperstack review sync
94
+ ```
95
+
96
+ `PAPERSTACK_DIR` and `PAPERSTACK_REPO` remain available for automation. Explicit environment variables take priority,
97
+ then a surrounding corpus working tree, then the active profile. Profiles are stored in
98
+ `${XDG_CONFIG_HOME:-~/.config}/paperstack/config.json`.
99
+
100
+ Provider credentials can be stored once without adding them to a project `.env`:
101
+
102
+ ```bash
103
+ paperstack config set semantic-scholar.api-key
104
+ paperstack config status
105
+ paperstack config paths
106
+ paperstack config unset semantic-scholar.api-key
107
+ ```
108
+
109
+ `set` prompts without echoing the value; use `--stdin` to pipe it from a password manager. Credentials are written
110
+ atomically to `${XDG_CONFIG_HOME:-~/.config}/paperstack/credentials.json`, with mode `0600` on POSIX systems.
111
+ Exported environment variables take priority, followed by the nearest `.env`, then the credential store. GitHub
112
+ Actions can therefore keep using repository secrets through environment variables.
113
+
114
+ ## Review corpus
115
+
116
+ ```bash
117
+ paperstack review show example2026paperstack --brief
118
+ paperstack review show arxiv:2410.24164 --json
119
+ paperstack review list --quality good --tag vla
120
+ paperstack review search "flow matching"
121
+ paperstack review init <key> --id arxiv:NNNN.NNNNN --title "Verbatim title" --editor <name>
122
+ paperstack review check --style
123
+ paperstack review audit
124
+ paperstack review citations --fetch
125
+ ```
126
+
127
+ Paper entries, talks, and posts use separate contracts under `entries/{papers,talks,posts}/`. The complete schemas,
128
+ writing guide, and source-reading workflow are in
129
+ [Entry authoring](https://github.com/MilkClouds/paperstack/blob/main/docs/ENTRIES.md).
130
+
131
+ ## Inspect papers
132
+
133
+ These commands use external source records and do not select a citation or make an editorial judgment:
134
+
135
+ ```bash
136
+ paperstack paper search "Attention Is All You Need" --source dblp
137
+ paperstack paper search "robot learning" --source semantic-scholar --year 2024-2026
138
+ paperstack paper search 'ti:"robot learning"' --source arxiv --category cs.RO --sort date
139
+ paperstack paper metadata arxiv:2106.09685
140
+ paperstack paper metadata doi:10.1109/CVPR.2016.90 --source crossref
141
+ paperstack paper citations arxiv:2106.09685 --limit 50
142
+ paperstack paper references doi:10.48550/arXiv.2106.09685 --limit 50
143
+ paperstack paper read arxiv:2604.23073 --outline
144
+ paperstack paper read arxiv:2604.23073 --section 6
145
+ paperstack paper pdf arxiv:2602.09017
146
+ ```
147
+
148
+ `metadata` accepts `arxiv:`, `doi:`, `dblp:`, and `openreview:` references. `read` and `pdf` require an `arxiv:`
149
+ reference. `authors`, `citations`, and `references` use Semantic Scholar and also accept its `s2:`, `corpus:`, `acl:`,
150
+ `pmid:`, and `mag:` identifiers. Structured commands expose scoped `--json` flags; networked commands expose scoped
151
+ `--offline` flags. Paperstack reports source records but does not synthesize a citation entry or choose which version
152
+ of a work should be cited.
153
+
154
+ ## Build a viewer
155
+
156
+ ```bash
157
+ paperstack viewer build
158
+ paperstack viewer serve --port 8000
159
+ ```
160
+
161
+ The viewer is generated from the selected corpus. Source Markdown remains available alongside the rendered pages.
162
+
163
+ ## DBLP index
164
+
165
+ The optional selected-venue index accelerates DBLP search and is required by `review audit`:
166
+
167
+ ```bash
168
+ paperstack index dblp status
169
+ paperstack index dblp install
170
+ paperstack index dblp update
171
+ ```
172
+
173
+ Installed snapshots are content-addressed and verified by SHA-256, schema, and embedded metadata before an atomic
174
+ pointer switch. See
175
+ [DBLP snapshot releases](https://github.com/MilkClouds/paperstack/blob/main/docs/DBLP_RELEASES.md) for maintenance.
176
+
177
+ ## Configuration
178
+
179
+ | Variable | Purpose |
180
+ |---|---|
181
+ | `PAPERSTACK_DIR` | Explicit local corpus working tree |
182
+ | `PAPERSTACK_REPO` | Explicit GitHub corpus in `OWNER/REPO` form |
183
+ | `PAPERSTACK_TTL` | Remote corpus cache refresh interval; default `3600` seconds |
184
+ | `PAPERSTACK_PAPERS_DIR` | arXiv source and PDF cache |
185
+ | `SEMANTIC_SCHOLAR_API_KEY` | Optional higher-rate Semantic Scholar access |
186
+ | `OPENREVIEW_ACCESS_TOKEN` | Optional OpenReview access cookie value |
187
+ | `XDG_CONFIG_HOME` | Corpus profile and credential configuration root |
188
+ | `XDG_CACHE_HOME` | Remote corpus and paper cache root |
189
+ | `XDG_DATA_HOME` | DBLP index root |
190
+
191
+ The console entry point also loads the nearest `.env` without overriding exported variables. Review exit codes are
192
+ `0` for hits, `1` for no match, `2` for ambiguity, and `3` for unavailable data or invalid configuration.
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ uv run --group lint ruff check .
198
+ uv run --group lint ruff format --check .
199
+ uv run --group test pytest -q
200
+ PAPERSTACK_DIR=examples/corpus uv run paperstack viewer build --output /tmp/paperstack-site
201
+ ```
202
+
203
+ Paperstack is licensed under Apache-2.0. Corpora are separate works and may use their own terms.
@@ -1,27 +1,15 @@
1
- Metadata-Version: 2.5
2
- Name: paperstack-cli
3
- Version: 0.3.1
4
- Summary: Review, inspect, and retrieve research sources from one CLI
5
- Project-URL: Repository, https://github.com/MilkClouds/paperstack
6
- Project-URL: Issues, https://github.com/MilkClouds/paperstack/issues
7
- License-File: LICENSE
8
- Requires-Python: >=3.11.4
9
- Requires-Dist: polars>=1.43
10
- Requires-Dist: python-dotenv>=1.2.2
11
- Requires-Dist: pyyaml>=6
12
- Provides-Extra: pdf
13
- Requires-Dist: pymupdf4llm>=0.0.17; extra == 'pdf'
14
- Description-Content-Type: text/markdown
15
-
16
1
  # Paperstack
17
2
 
3
+ [![CI](https://github.com/MilkClouds/paperstack/actions/workflows/ci.yml/badge.svg)](https://github.com/MilkClouds/paperstack/actions/workflows/ci.yml) [![PyPI version](https://img.shields.io/pypi/v/paperstack-cli.svg)](https://pypi.org/project/paperstack-cli/) [![Python versions](https://img.shields.io/pypi/pyversions/paperstack-cli.svg)](https://pypi.org/project/paperstack-cli/) [![License](https://img.shields.io/pypi/l/paperstack-cli.svg)](https://github.com/MilkClouds/paperstack/blob/main/LICENSE)
4
+
18
5
  Paperstack is an open-source CLI for maintaining, querying, and publishing an independently authored research corpus.
19
6
  The software and corpus are separate: Paperstack can be public while each corpus and its editorial judgments remain
20
7
  under its maintainer's control.
21
8
 
22
9
  It can search typed reviews, initialize and validate entries, inspect external paper metadata, read arXiv sources,
23
10
  manage a selected-venue DBLP index, and build a static viewer. A synthetic corpus lives in
24
- [`examples/corpus`](examples/corpus/README.md); no assessment of real work is bundled with the package.
11
+ [`examples/corpus`](https://github.com/MilkClouds/paperstack/tree/main/examples/corpus); no assessment of real work is
12
+ bundled with the package.
25
13
 
26
14
  ## Install
27
15
 
@@ -35,16 +23,45 @@ PDF conversion is optional:
35
23
  uv tool install 'paperstack-cli[pdf]'
36
24
  ```
37
25
 
26
+ The PDF extra uses `pdf-inspector` for native extraction and selective OCR.
27
+ Clean text PDFs need no external runtime, while routed OCR pages require the pinned PDFium and ONNX Runtime libraries described in the [OCR runtime guide](https://github.com/firecrawl/pdf-inspector/blob/main/docs/ocr-runtime.md).
28
+ The first routed page downloads and verifies the pinned OCR model set unless an offline model directory is configured.
29
+ When the shared libraries are not on the platform search path, point `pdf-inspector` at the extracted files:
30
+
31
+ ```bash
32
+ export PDFIUM_LIB_PATH=/absolute/path/to/libpdfium.so
33
+ export ORT_DYLIB_PATH=/absolute/path/to/libonnxruntime.so
34
+ ```
35
+
36
+ The runtime guide lists the matching downloads and filenames for Linux, macOS, and Windows.
37
+
38
+ ## AI agent skill
39
+
40
+ Paperstack ships an [Agent Skills](https://agentskills.io)-compatible skill for Codex, Claude Code, and other
41
+ compatible agents. Install the CLI first, then add the skill globally with the open
42
+ [`skills`](https://github.com/vercel-labs/skills) CLI:
43
+
44
+ ```bash
45
+ npx skills add MilkClouds/paperstack --skill paperstack -g
46
+ ```
47
+
48
+ The installer detects supported agents such as Codex and Claude Code; pass `-a codex -a claude-code` to target them
49
+ explicitly, or omit `-g` for a project-local installation. The shared [`SKILL.md`](skills/paperstack/SKILL.md) keeps
50
+ corpus selection, source inspection, entry authoring, validation, and viewer workflows consistent without exposing a
51
+ private corpus. Use `npx skills update paperstack -g` to update it later.
52
+
38
53
  ## Choose a corpus
39
54
 
40
55
  Register either a local working tree or a GitHub repository. The first registered corpus becomes active.
41
56
 
42
57
  ```bash
43
58
  paperstack corpus add work --path ~/research/reviews
59
+ paperstack corpus init new --path ~/research/new-corpus
44
60
  paperstack corpus add private --repo OWNER/private-reviews
45
61
  paperstack corpus list
46
62
  paperstack corpus use private
47
63
  paperstack corpus remove work
64
+ paperstack corpus remove private --purge-cache --yes
48
65
  ```
49
66
 
50
67
  Private GitHub repositories use the existing `gh` login without storing its token:
@@ -58,6 +75,20 @@ paperstack review sync
58
75
  then a surrounding corpus working tree, then the active profile. Profiles are stored in
59
76
  `${XDG_CONFIG_HOME:-~/.config}/paperstack/config.json`.
60
77
 
78
+ Provider credentials can be stored once without adding them to a project `.env`:
79
+
80
+ ```bash
81
+ paperstack config set semantic-scholar.api-key
82
+ paperstack config status
83
+ paperstack config paths
84
+ paperstack config unset semantic-scholar.api-key
85
+ ```
86
+
87
+ `set` prompts without echoing the value; use `--stdin` to pipe it from a password manager. Credentials are written
88
+ atomically to `${XDG_CONFIG_HOME:-~/.config}/paperstack/credentials.json`, with mode `0600` on POSIX systems.
89
+ Exported environment variables take priority, followed by the nearest `.env`, then the credential store. GitHub
90
+ Actions can therefore keep using repository secrets through environment variables.
91
+
61
92
  ## Review corpus
62
93
 
63
94
  ```bash
@@ -72,7 +103,8 @@ paperstack review citations --fetch
72
103
  ```
73
104
 
74
105
  Paper entries, talks, and posts use separate contracts under `entries/{papers,talks,posts}/`. The complete schemas,
75
- writing guide, and source-reading workflow are in [Entry authoring](docs/ENTRIES.md).
106
+ writing guide, and source-reading workflow are in
107
+ [Entry authoring](https://github.com/MilkClouds/paperstack/blob/main/docs/ENTRIES.md).
76
108
 
77
109
  ## Inspect papers
78
110
 
@@ -80,15 +112,22 @@ These commands use external source records and do not select a citation or make
80
112
 
81
113
  ```bash
82
114
  paperstack paper search "Attention Is All You Need" --source dblp
115
+ paperstack paper search "robot learning" --source semantic-scholar --year 2024-2026
116
+ paperstack paper search 'ti:"robot learning"' --source arxiv --category cs.RO --sort date
83
117
  paperstack paper metadata arxiv:2106.09685
84
118
  paperstack paper metadata doi:10.1109/CVPR.2016.90 --source crossref
119
+ paperstack paper citations arxiv:2106.09685 --limit 50
120
+ paperstack paper references doi:10.48550/arXiv.2106.09685 --limit 50
85
121
  paperstack paper read arxiv:2604.23073 --outline
86
122
  paperstack paper read arxiv:2604.23073 --section 6
87
123
  paperstack paper pdf arxiv:2602.09017
88
124
  ```
89
125
 
90
126
  `metadata` accepts `arxiv:`, `doi:`, `dblp:`, and `openreview:` references. `read` and `pdf` require an `arxiv:`
91
- reference. Structured commands expose scoped `--json` flags; networked commands expose scoped `--offline` flags.
127
+ reference. `authors`, `citations`, and `references` use Semantic Scholar and also accept its `s2:`, `corpus:`, `acl:`,
128
+ `pmid:`, and `mag:` identifiers. Structured commands expose scoped `--json` flags; networked commands expose scoped
129
+ `--offline` flags. Paperstack reports source records but does not synthesize a citation entry or choose which version
130
+ of a work should be cited.
92
131
 
93
132
  ## Build a viewer
94
133
 
@@ -110,7 +149,8 @@ paperstack index dblp update
110
149
  ```
111
150
 
112
151
  Installed snapshots are content-addressed and verified by SHA-256, schema, and embedded metadata before an atomic
113
- pointer switch. See [DBLP snapshot releases](docs/DBLP_RELEASES.md) for release maintenance.
152
+ pointer switch. See
153
+ [DBLP snapshot releases](https://github.com/MilkClouds/paperstack/blob/main/docs/DBLP_RELEASES.md) for maintenance.
114
154
 
115
155
  ## Configuration
116
156
 
@@ -122,7 +162,7 @@ pointer switch. See [DBLP snapshot releases](docs/DBLP_RELEASES.md) for release
122
162
  | `PAPERSTACK_PAPERS_DIR` | arXiv source and PDF cache |
123
163
  | `SEMANTIC_SCHOLAR_API_KEY` | Optional higher-rate Semantic Scholar access |
124
164
  | `OPENREVIEW_ACCESS_TOKEN` | Optional OpenReview access cookie value |
125
- | `XDG_CONFIG_HOME` | Corpus profile configuration root |
165
+ | `XDG_CONFIG_HOME` | Corpus profile and credential configuration root |
126
166
  | `XDG_CACHE_HOME` | Remote corpus and paper cache root |
127
167
  | `XDG_DATA_HOME` | DBLP index root |
128
168
 
@@ -1,17 +1,26 @@
1
1
  [project]
2
2
  name = "paperstack-cli"
3
- version = "0.3.1"
3
+ dynamic = ["version"]
4
4
  description = "Review, inspect, and retrieve research sources from one CLI"
5
5
  readme = "README.md"
6
- requires-python = ">=3.11.4" # First 3.11 release with tarfile.extractall(filter=).
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.11"
8
+ classifiers = [
9
+ "Programming Language :: Python :: 3 :: Only",
10
+ "Programming Language :: Python :: 3.11",
11
+ "Programming Language :: Python :: 3.12",
12
+ "Programming Language :: Python :: 3.13",
13
+ "Programming Language :: Python :: 3.14",
14
+ ]
7
15
  dependencies = [
16
+ "filelock>=3.20",
8
17
  "polars>=1.43",
9
18
  "python-dotenv>=1.2.2",
10
19
  "pyyaml>=6",
11
20
  ]
12
21
 
13
22
  [project.optional-dependencies]
14
- pdf = ["pymupdf4llm>=0.0.17"]
23
+ pdf = ["pdf-inspector>=1.17,<2"]
15
24
 
16
25
  [dependency-groups]
17
26
  lint = ["ruff>=0.12"]
@@ -34,13 +43,21 @@ packages = ["src/paperstack"]
34
43
  [tool.hatch.build.targets.wheel.force-include]
35
44
  "scripts/build/site" = "paperstack/viewer_assets/site"
36
45
  "scripts/build/vendor/marked.min.js" = "paperstack/viewer_assets/vendor/marked.min.js"
46
+ "scripts/build/vendor/marked.LICENSE" = "paperstack/viewer_assets/vendor/marked.LICENSE"
37
47
 
38
48
  [tool.hatch.build.targets.sdist]
39
- include = ["/src/paperstack", "/scripts/build/site", "/scripts/build/vendor/marked.min.js", "/README.md", "/pyproject.toml", "/LICENSE"]
49
+ include = ["/src/paperstack", "/scripts/build/site", "/scripts/build/vendor/marked.min.js", "/scripts/build/vendor/marked.LICENSE", "/README.md", "/pyproject.toml", "/LICENSE"]
40
50
 
41
51
  [tool.ruff]
42
52
  line-length = 119
43
53
 
54
+ [tool.hatch.version]
55
+ source = "vcs"
56
+
57
+ [tool.hatch.version.raw-options]
58
+ tag_regex = "^v(?P<version>\\d+\\.\\d+\\.\\d+(?:[a-zA-Z0-9.-]*)?)$"
59
+ git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "v[0-9]*"]
60
+
44
61
  [build-system]
45
- requires = ["hatchling"]
62
+ requires = ["hatchling", "hatch-vcs"]
46
63
  build-backend = "hatchling.build"
@@ -0,0 +1,36 @@
1
+ ## Marked
2
+
3
+ Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/)
4
+ Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
23
+
24
+ ## Markdown
25
+
26
+ Copyright © 2004, John Gruber
27
+ http://daringfireball.net/
28
+ All rights reserved.
29
+
30
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
31
+
32
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
33
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
34
+ * Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
35
+
36
+ This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
@@ -0,0 +1,125 @@
1
+ """Bounded arXiv discovery with source-backed metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import urllib.parse
7
+ import xml.etree.ElementTree as ET
8
+ from datetime import UTC, datetime
9
+
10
+ from . import metadata
11
+
12
+ ATOM = "http://www.w3.org/2005/Atom"
13
+ ARXIV = "http://arxiv.org/schemas/atom"
14
+ NS = {"atom": ATOM, "arxiv": ARXIV}
15
+ MAX_RESULTS = 100
16
+ _CATEGORY = re.compile(r"^[a-z-]+(?:\.[A-Za-z-]+)?$")
17
+
18
+
19
+ def _date(value: str, *, end: bool = False) -> str:
20
+ try:
21
+ parsed = datetime.fromisoformat(value)
22
+ except ValueError as exc:
23
+ raise ValueError(f"invalid date {value!r}; use YYYY-MM-DD or ISO 8601") from exc
24
+ if len(value) == 10:
25
+ parsed = parsed.replace(hour=23 if end else 0, minute=59 if end else 0)
26
+ elif parsed.tzinfo is not None:
27
+ parsed = parsed.astimezone(UTC)
28
+ return parsed.strftime("%Y%m%d%H%M")
29
+
30
+
31
+ def _query(
32
+ query: str,
33
+ *,
34
+ categories: list[str] | None = None,
35
+ date_from: str | None = None,
36
+ date_to: str | None = None,
37
+ ) -> str:
38
+ parts = []
39
+ if query.strip():
40
+ parts.append(f"({query.strip()})")
41
+ if categories:
42
+ invalid = [item for item in categories if not _CATEGORY.fullmatch(item)]
43
+ if invalid:
44
+ raise ValueError(f"invalid arXiv category: {invalid[0]}")
45
+ parts.append("(" + " OR ".join(f"cat:{item}" for item in categories) + ")")
46
+ if date_from or date_to:
47
+ start = _date(date_from) if date_from else "199107010000"
48
+ end = _date(date_to, end=True) if date_to else datetime.now(UTC).strftime("%Y%m%d%H%M")
49
+ if start > end:
50
+ raise ValueError("date-from must not be after date-to")
51
+ parts.append(f"submittedDate:[{start}+TO+{end}]")
52
+ if not parts:
53
+ raise ValueError("arXiv search needs a query, category, or date range")
54
+ return " AND ".join(parts)
55
+
56
+
57
+ def _text(entry: ET.Element, name: str) -> str | None:
58
+ value = entry.findtext(name, None, NS)
59
+ return " ".join(value.split()) if value else None
60
+
61
+
62
+ def _entry(entry: ET.Element) -> dict:
63
+ raw_id = _text(entry, "atom:id") or ""
64
+ versioned = raw_id.rsplit("/abs/", 1)[-1]
65
+ arxiv_id = re.sub(r"v\d+$", "", versioned)
66
+ links = {node.get("title") or node.get("rel"): node.get("href") for node in entry.findall("atom:link", NS)}
67
+ categories = [node.get("term") for node in entry.findall("atom:category", NS) if node.get("term")]
68
+ primary = entry.find("arxiv:primary_category", NS)
69
+ authors = [_text(node, "atom:name") for node in entry.findall("atom:author", NS)]
70
+ return {
71
+ "id": arxiv_id,
72
+ "versioned_id": versioned,
73
+ "title": _text(entry, "atom:title"),
74
+ "authors": [author for author in authors if author],
75
+ "abstract": _text(entry, "atom:summary"),
76
+ "categories": categories,
77
+ "primary_category": primary.get("term") if primary is not None else (categories[0] if categories else None),
78
+ "published": _text(entry, "atom:published"),
79
+ "updated": _text(entry, "atom:updated"),
80
+ "comment": _text(entry, "arxiv:comment"),
81
+ "journal_ref": _text(entry, "arxiv:journal_ref"),
82
+ "doi": _text(entry, "arxiv:doi"),
83
+ "url": f"https://arxiv.org/abs/{versioned}",
84
+ "pdf_url": links.get("pdf") or f"https://arxiv.org/pdf/{versioned}",
85
+ }
86
+
87
+
88
+ def parse_feed(raw: bytes | str) -> list[dict]:
89
+ root = ET.fromstring(raw)
90
+ return [_entry(entry) for entry in root.findall("atom:entry", NS)]
91
+
92
+
93
+ def search(
94
+ query: str,
95
+ *,
96
+ categories: list[str] | None = None,
97
+ date_from: str | None = None,
98
+ date_to: str | None = None,
99
+ limit: int = 10,
100
+ sort: str = "relevance",
101
+ ) -> dict:
102
+ if not 1 <= limit <= MAX_RESULTS:
103
+ raise ValueError(f"limit must be between 1 and {MAX_RESULTS}")
104
+ if sort not in ("relevance", "date"):
105
+ raise ValueError("sort must be relevance or date")
106
+ built = _query(query, categories=categories, date_from=date_from, date_to=date_to)
107
+ params = urllib.parse.urlencode(
108
+ {
109
+ "search_query": built,
110
+ "max_results": limit,
111
+ "sortBy": "submittedDate" if sort == "date" else "relevance",
112
+ "sortOrder": "descending",
113
+ }
114
+ )
115
+ params = params.replace("%2BTO%2B", "+TO+")
116
+ url = f"https://export.arxiv.org/api/query?{params}"
117
+ return metadata._safe(
118
+ lambda: metadata._result(
119
+ "arxiv",
120
+ url,
121
+ {"query": built, "matches": parse_feed(metadata.request(url))},
122
+ ),
123
+ "arxiv",
124
+ url,
125
+ )
@@ -3,13 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- import os
7
6
  import re
8
7
  import urllib.parse
9
8
  from datetime import UTC, datetime
10
9
  from pathlib import Path
11
10
 
12
- from . import metadata
11
+ from . import credentials, metadata
13
12
 
14
13
  S2_BATCH_API = "https://api.semanticscholar.org/graph/v1/paper/batch"
15
14
  BATCH_SIZE = 500
@@ -44,7 +43,7 @@ def fetch(arxiv_ids: list[str]) -> dict[str, int]:
44
43
  """Fetch citation counts in aligned Semantic Scholar batches."""
45
44
  counts: dict[str, int] = {}
46
45
  headers = {"Content-Type": "application/json"}
47
- if api_key := os.environ.get("SEMANTIC_SCHOLAR_API_KEY"):
46
+ if api_key := credentials.get(credentials.SEMANTIC_SCHOLAR_API_KEY):
48
47
  headers["x-api-key"] = api_key
49
48
 
50
49
  for start in range(0, len(arxiv_ids), BATCH_SIZE):