paperstack-cli 0.3.1__tar.gz → 0.3.2__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.
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/PKG-INFO +50 -7
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/README.md +46 -5
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/pyproject.toml +14 -4
- paperstack_cli-0.3.2/scripts/build/vendor/marked.LICENSE +36 -0
- paperstack_cli-0.3.2/src/paperstack/arxiv.py +125 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/citations.py +2 -3
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/cli.py +263 -37
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/content/arxiv_pdf.py +2 -2
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/corpora.py +27 -3
- paperstack_cli-0.3.2/src/paperstack/credentials.py +147 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/dblp_build.py +3 -1
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/dblp_index.py +27 -53
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/entrypoint.py +9 -1
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/metadata.py +20 -8
- paperstack_cli-0.3.2/src/paperstack/semantic_scholar.py +175 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/viewer.py +38 -10
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/.gitignore +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/LICENSE +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/scripts/build/site/app.js +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/scripts/build/site/entry.html +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/scripts/build/site/favicon.svg +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/scripts/build/site/index.html +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/scripts/build/site/style.css +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/scripts/build/vendor/marked.min.js +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/__init__.py +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/content/__init__.py +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/content/arxiv_source.py +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/content/vendor/latexpand +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/content/vendor/latexpand.LICENSE +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/dblp_catalog.py +0 -0
- {paperstack_cli-0.3.1 → paperstack_cli-0.3.2}/src/paperstack/entry_types.py +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: paperstack-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Review, inspect, and retrieve research sources from one CLI
|
|
5
5
|
Project-URL: Repository, https://github.com/MilkClouds/paperstack
|
|
6
6
|
Project-URL: Issues, https://github.com/MilkClouds/paperstack/issues
|
|
7
|
+
License-Expression: Apache-2.0
|
|
7
8
|
License-File: LICENSE
|
|
8
|
-
Requires-Python: >=3.11
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: filelock>=3.20
|
|
9
11
|
Requires-Dist: polars>=1.43
|
|
10
12
|
Requires-Dist: python-dotenv>=1.2.2
|
|
11
13
|
Requires-Dist: pyyaml>=6
|
|
@@ -21,7 +23,8 @@ under its maintainer's control.
|
|
|
21
23
|
|
|
22
24
|
It can search typed reviews, initialize and validate entries, inspect external paper metadata, read arXiv sources,
|
|
23
25
|
manage a selected-venue DBLP index, and build a static viewer. A synthetic corpus lives in
|
|
24
|
-
[`examples/corpus`](examples/corpus
|
|
26
|
+
[`examples/corpus`](https://github.com/MilkClouds/paperstack/tree/main/examples/corpus); no assessment of real work is
|
|
27
|
+
bundled with the package.
|
|
25
28
|
|
|
26
29
|
## Install
|
|
27
30
|
|
|
@@ -35,16 +38,33 @@ PDF conversion is optional:
|
|
|
35
38
|
uv tool install 'paperstack-cli[pdf]'
|
|
36
39
|
```
|
|
37
40
|
|
|
41
|
+
## AI agent skill
|
|
42
|
+
|
|
43
|
+
Paperstack ships an [Agent Skills](https://agentskills.io)-compatible skill for Codex, Claude Code, and other
|
|
44
|
+
compatible agents. Install the CLI first, then add the skill globally with the open
|
|
45
|
+
[`skills`](https://github.com/vercel-labs/skills) CLI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx skills add MilkClouds/paperstack --skill paperstack -g
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The installer detects supported agents such as Codex and Claude Code; pass `-a codex -a claude-code` to target them
|
|
52
|
+
explicitly, or omit `-g` for a project-local installation. The shared [`SKILL.md`](skills/paperstack/SKILL.md) keeps
|
|
53
|
+
corpus selection, source inspection, entry authoring, validation, and viewer workflows consistent without exposing a
|
|
54
|
+
private corpus. Use `npx skills update paperstack -g` to update it later.
|
|
55
|
+
|
|
38
56
|
## Choose a corpus
|
|
39
57
|
|
|
40
58
|
Register either a local working tree or a GitHub repository. The first registered corpus becomes active.
|
|
41
59
|
|
|
42
60
|
```bash
|
|
43
61
|
paperstack corpus add work --path ~/research/reviews
|
|
62
|
+
paperstack corpus init new --path ~/research/new-corpus
|
|
44
63
|
paperstack corpus add private --repo OWNER/private-reviews
|
|
45
64
|
paperstack corpus list
|
|
46
65
|
paperstack corpus use private
|
|
47
66
|
paperstack corpus remove work
|
|
67
|
+
paperstack corpus remove private --purge-cache --yes
|
|
48
68
|
```
|
|
49
69
|
|
|
50
70
|
Private GitHub repositories use the existing `gh` login without storing its token:
|
|
@@ -58,6 +78,20 @@ paperstack review sync
|
|
|
58
78
|
then a surrounding corpus working tree, then the active profile. Profiles are stored in
|
|
59
79
|
`${XDG_CONFIG_HOME:-~/.config}/paperstack/config.json`.
|
|
60
80
|
|
|
81
|
+
Provider credentials can be stored once without adding them to a project `.env`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
paperstack config set semantic-scholar.api-key
|
|
85
|
+
paperstack config status
|
|
86
|
+
paperstack config paths
|
|
87
|
+
paperstack config unset semantic-scholar.api-key
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`set` prompts without echoing the value; use `--stdin` to pipe it from a password manager. Credentials are written
|
|
91
|
+
atomically to `${XDG_CONFIG_HOME:-~/.config}/paperstack/credentials.json`, with mode `0600` on POSIX systems.
|
|
92
|
+
Exported environment variables take priority, followed by the nearest `.env`, then the credential store. GitHub
|
|
93
|
+
Actions can therefore keep using repository secrets through environment variables.
|
|
94
|
+
|
|
61
95
|
## Review corpus
|
|
62
96
|
|
|
63
97
|
```bash
|
|
@@ -72,7 +106,8 @@ paperstack review citations --fetch
|
|
|
72
106
|
```
|
|
73
107
|
|
|
74
108
|
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
|
|
109
|
+
writing guide, and source-reading workflow are in
|
|
110
|
+
[Entry authoring](https://github.com/MilkClouds/paperstack/blob/main/docs/ENTRIES.md).
|
|
76
111
|
|
|
77
112
|
## Inspect papers
|
|
78
113
|
|
|
@@ -80,15 +115,22 @@ These commands use external source records and do not select a citation or make
|
|
|
80
115
|
|
|
81
116
|
```bash
|
|
82
117
|
paperstack paper search "Attention Is All You Need" --source dblp
|
|
118
|
+
paperstack paper search "robot learning" --source semantic-scholar --year 2024-2026
|
|
119
|
+
paperstack paper search 'ti:"robot learning"' --source arxiv --category cs.RO --sort date
|
|
83
120
|
paperstack paper metadata arxiv:2106.09685
|
|
84
121
|
paperstack paper metadata doi:10.1109/CVPR.2016.90 --source crossref
|
|
122
|
+
paperstack paper citations arxiv:2106.09685 --limit 50
|
|
123
|
+
paperstack paper references doi:10.48550/arXiv.2106.09685 --limit 50
|
|
85
124
|
paperstack paper read arxiv:2604.23073 --outline
|
|
86
125
|
paperstack paper read arxiv:2604.23073 --section 6
|
|
87
126
|
paperstack paper pdf arxiv:2602.09017
|
|
88
127
|
```
|
|
89
128
|
|
|
90
129
|
`metadata` accepts `arxiv:`, `doi:`, `dblp:`, and `openreview:` references. `read` and `pdf` require an `arxiv:`
|
|
91
|
-
reference.
|
|
130
|
+
reference. `authors`, `citations`, and `references` use Semantic Scholar and also accept its `s2:`, `corpus:`, `acl:`,
|
|
131
|
+
`pmid:`, and `mag:` identifiers. Structured commands expose scoped `--json` flags; networked commands expose scoped
|
|
132
|
+
`--offline` flags. Paperstack reports source records but does not synthesize a citation entry or choose which version
|
|
133
|
+
of a work should be cited.
|
|
92
134
|
|
|
93
135
|
## Build a viewer
|
|
94
136
|
|
|
@@ -110,7 +152,8 @@ paperstack index dblp update
|
|
|
110
152
|
```
|
|
111
153
|
|
|
112
154
|
Installed snapshots are content-addressed and verified by SHA-256, schema, and embedded metadata before an atomic
|
|
113
|
-
pointer switch. See
|
|
155
|
+
pointer switch. See
|
|
156
|
+
[DBLP snapshot releases](https://github.com/MilkClouds/paperstack/blob/main/docs/DBLP_RELEASES.md) for maintenance.
|
|
114
157
|
|
|
115
158
|
## Configuration
|
|
116
159
|
|
|
@@ -122,7 +165,7 @@ pointer switch. See [DBLP snapshot releases](docs/DBLP_RELEASES.md) for release
|
|
|
122
165
|
| `PAPERSTACK_PAPERS_DIR` | arXiv source and PDF cache |
|
|
123
166
|
| `SEMANTIC_SCHOLAR_API_KEY` | Optional higher-rate Semantic Scholar access |
|
|
124
167
|
| `OPENREVIEW_ACCESS_TOKEN` | Optional OpenReview access cookie value |
|
|
125
|
-
| `XDG_CONFIG_HOME` | Corpus profile configuration root |
|
|
168
|
+
| `XDG_CONFIG_HOME` | Corpus profile and credential configuration root |
|
|
126
169
|
| `XDG_CACHE_HOME` | Remote corpus and paper cache root |
|
|
127
170
|
| `XDG_DATA_HOME` | DBLP index root |
|
|
128
171
|
|
|
@@ -6,7 +6,8 @@ under its maintainer's control.
|
|
|
6
6
|
|
|
7
7
|
It can search typed reviews, initialize and validate entries, inspect external paper metadata, read arXiv sources,
|
|
8
8
|
manage a selected-venue DBLP index, and build a static viewer. A synthetic corpus lives in
|
|
9
|
-
[`examples/corpus`](examples/corpus
|
|
9
|
+
[`examples/corpus`](https://github.com/MilkClouds/paperstack/tree/main/examples/corpus); no assessment of real work is
|
|
10
|
+
bundled with the package.
|
|
10
11
|
|
|
11
12
|
## Install
|
|
12
13
|
|
|
@@ -20,16 +21,33 @@ PDF conversion is optional:
|
|
|
20
21
|
uv tool install 'paperstack-cli[pdf]'
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
## AI agent skill
|
|
25
|
+
|
|
26
|
+
Paperstack ships an [Agent Skills](https://agentskills.io)-compatible skill for Codex, Claude Code, and other
|
|
27
|
+
compatible agents. Install the CLI first, then add the skill globally with the open
|
|
28
|
+
[`skills`](https://github.com/vercel-labs/skills) CLI:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx skills add MilkClouds/paperstack --skill paperstack -g
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The installer detects supported agents such as Codex and Claude Code; pass `-a codex -a claude-code` to target them
|
|
35
|
+
explicitly, or omit `-g` for a project-local installation. The shared [`SKILL.md`](skills/paperstack/SKILL.md) keeps
|
|
36
|
+
corpus selection, source inspection, entry authoring, validation, and viewer workflows consistent without exposing a
|
|
37
|
+
private corpus. Use `npx skills update paperstack -g` to update it later.
|
|
38
|
+
|
|
23
39
|
## Choose a corpus
|
|
24
40
|
|
|
25
41
|
Register either a local working tree or a GitHub repository. The first registered corpus becomes active.
|
|
26
42
|
|
|
27
43
|
```bash
|
|
28
44
|
paperstack corpus add work --path ~/research/reviews
|
|
45
|
+
paperstack corpus init new --path ~/research/new-corpus
|
|
29
46
|
paperstack corpus add private --repo OWNER/private-reviews
|
|
30
47
|
paperstack corpus list
|
|
31
48
|
paperstack corpus use private
|
|
32
49
|
paperstack corpus remove work
|
|
50
|
+
paperstack corpus remove private --purge-cache --yes
|
|
33
51
|
```
|
|
34
52
|
|
|
35
53
|
Private GitHub repositories use the existing `gh` login without storing its token:
|
|
@@ -43,6 +61,20 @@ paperstack review sync
|
|
|
43
61
|
then a surrounding corpus working tree, then the active profile. Profiles are stored in
|
|
44
62
|
`${XDG_CONFIG_HOME:-~/.config}/paperstack/config.json`.
|
|
45
63
|
|
|
64
|
+
Provider credentials can be stored once without adding them to a project `.env`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
paperstack config set semantic-scholar.api-key
|
|
68
|
+
paperstack config status
|
|
69
|
+
paperstack config paths
|
|
70
|
+
paperstack config unset semantic-scholar.api-key
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`set` prompts without echoing the value; use `--stdin` to pipe it from a password manager. Credentials are written
|
|
74
|
+
atomically to `${XDG_CONFIG_HOME:-~/.config}/paperstack/credentials.json`, with mode `0600` on POSIX systems.
|
|
75
|
+
Exported environment variables take priority, followed by the nearest `.env`, then the credential store. GitHub
|
|
76
|
+
Actions can therefore keep using repository secrets through environment variables.
|
|
77
|
+
|
|
46
78
|
## Review corpus
|
|
47
79
|
|
|
48
80
|
```bash
|
|
@@ -57,7 +89,8 @@ paperstack review citations --fetch
|
|
|
57
89
|
```
|
|
58
90
|
|
|
59
91
|
Paper entries, talks, and posts use separate contracts under `entries/{papers,talks,posts}/`. The complete schemas,
|
|
60
|
-
writing guide, and source-reading workflow are in
|
|
92
|
+
writing guide, and source-reading workflow are in
|
|
93
|
+
[Entry authoring](https://github.com/MilkClouds/paperstack/blob/main/docs/ENTRIES.md).
|
|
61
94
|
|
|
62
95
|
## Inspect papers
|
|
63
96
|
|
|
@@ -65,15 +98,22 @@ These commands use external source records and do not select a citation or make
|
|
|
65
98
|
|
|
66
99
|
```bash
|
|
67
100
|
paperstack paper search "Attention Is All You Need" --source dblp
|
|
101
|
+
paperstack paper search "robot learning" --source semantic-scholar --year 2024-2026
|
|
102
|
+
paperstack paper search 'ti:"robot learning"' --source arxiv --category cs.RO --sort date
|
|
68
103
|
paperstack paper metadata arxiv:2106.09685
|
|
69
104
|
paperstack paper metadata doi:10.1109/CVPR.2016.90 --source crossref
|
|
105
|
+
paperstack paper citations arxiv:2106.09685 --limit 50
|
|
106
|
+
paperstack paper references doi:10.48550/arXiv.2106.09685 --limit 50
|
|
70
107
|
paperstack paper read arxiv:2604.23073 --outline
|
|
71
108
|
paperstack paper read arxiv:2604.23073 --section 6
|
|
72
109
|
paperstack paper pdf arxiv:2602.09017
|
|
73
110
|
```
|
|
74
111
|
|
|
75
112
|
`metadata` accepts `arxiv:`, `doi:`, `dblp:`, and `openreview:` references. `read` and `pdf` require an `arxiv:`
|
|
76
|
-
reference.
|
|
113
|
+
reference. `authors`, `citations`, and `references` use Semantic Scholar and also accept its `s2:`, `corpus:`, `acl:`,
|
|
114
|
+
`pmid:`, and `mag:` identifiers. Structured commands expose scoped `--json` flags; networked commands expose scoped
|
|
115
|
+
`--offline` flags. Paperstack reports source records but does not synthesize a citation entry or choose which version
|
|
116
|
+
of a work should be cited.
|
|
77
117
|
|
|
78
118
|
## Build a viewer
|
|
79
119
|
|
|
@@ -95,7 +135,8 @@ paperstack index dblp update
|
|
|
95
135
|
```
|
|
96
136
|
|
|
97
137
|
Installed snapshots are content-addressed and verified by SHA-256, schema, and embedded metadata before an atomic
|
|
98
|
-
pointer switch. See
|
|
138
|
+
pointer switch. See
|
|
139
|
+
[DBLP snapshot releases](https://github.com/MilkClouds/paperstack/blob/main/docs/DBLP_RELEASES.md) for maintenance.
|
|
99
140
|
|
|
100
141
|
## Configuration
|
|
101
142
|
|
|
@@ -107,7 +148,7 @@ pointer switch. See [DBLP snapshot releases](docs/DBLP_RELEASES.md) for release
|
|
|
107
148
|
| `PAPERSTACK_PAPERS_DIR` | arXiv source and PDF cache |
|
|
108
149
|
| `SEMANTIC_SCHOLAR_API_KEY` | Optional higher-rate Semantic Scholar access |
|
|
109
150
|
| `OPENREVIEW_ACCESS_TOKEN` | Optional OpenReview access cookie value |
|
|
110
|
-
| `XDG_CONFIG_HOME` | Corpus profile configuration root |
|
|
151
|
+
| `XDG_CONFIG_HOME` | Corpus profile and credential configuration root |
|
|
111
152
|
| `XDG_CACHE_HOME` | Remote corpus and paper cache root |
|
|
112
153
|
| `XDG_DATA_HOME` | DBLP index root |
|
|
113
154
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "paperstack-cli"
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
description = "Review, inspect, and retrieve research sources from one CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.11"
|
|
7
8
|
dependencies = [
|
|
9
|
+
"filelock>=3.20",
|
|
8
10
|
"polars>=1.43",
|
|
9
11
|
"python-dotenv>=1.2.2",
|
|
10
12
|
"pyyaml>=6",
|
|
@@ -34,13 +36,21 @@ packages = ["src/paperstack"]
|
|
|
34
36
|
[tool.hatch.build.targets.wheel.force-include]
|
|
35
37
|
"scripts/build/site" = "paperstack/viewer_assets/site"
|
|
36
38
|
"scripts/build/vendor/marked.min.js" = "paperstack/viewer_assets/vendor/marked.min.js"
|
|
39
|
+
"scripts/build/vendor/marked.LICENSE" = "paperstack/viewer_assets/vendor/marked.LICENSE"
|
|
37
40
|
|
|
38
41
|
[tool.hatch.build.targets.sdist]
|
|
39
|
-
include = ["/src/paperstack", "/scripts/build/site", "/scripts/build/vendor/marked.min.js", "/README.md", "/pyproject.toml", "/LICENSE"]
|
|
42
|
+
include = ["/src/paperstack", "/scripts/build/site", "/scripts/build/vendor/marked.min.js", "/scripts/build/vendor/marked.LICENSE", "/README.md", "/pyproject.toml", "/LICENSE"]
|
|
40
43
|
|
|
41
44
|
[tool.ruff]
|
|
42
45
|
line-length = 119
|
|
43
46
|
|
|
47
|
+
[tool.hatch.version]
|
|
48
|
+
source = "vcs"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.version.raw-options]
|
|
51
|
+
tag_regex = "^v(?P<version>\\d+\\.\\d+\\.\\d+(?:[a-zA-Z0-9.-]*)?)$"
|
|
52
|
+
git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "v[0-9]*"]
|
|
53
|
+
|
|
44
54
|
[build-system]
|
|
45
|
-
requires = ["hatchling"]
|
|
55
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
46
56
|
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 :=
|
|
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):
|