pmcfetch 0.1.0__tar.gz → 0.2.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.
- {pmcfetch-0.1.0/src/pmcfetch.egg-info → pmcfetch-0.2.0}/PKG-INFO +61 -9
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/README.md +57 -7
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/pyproject.toml +3 -1
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/__init__.py +12 -3
- pmcfetch-0.2.0/src/pmcfetch/cli.py +420 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/errors.py +5 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/manifest.py +10 -0
- pmcfetch-0.2.0/src/pmcfetch/search.py +249 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0/src/pmcfetch.egg-info}/PKG-INFO +61 -9
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/SOURCES.txt +3 -1
- pmcfetch-0.2.0/tests/conftest.py +93 -0
- pmcfetch-0.2.0/tests/test_cli.py +278 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/tests/test_manifest.py +13 -1
- pmcfetch-0.2.0/tests/test_search.py +161 -0
- pmcfetch-0.1.0/src/pmcfetch/cli.py +0 -228
- pmcfetch-0.1.0/tests/conftest.py +0 -44
- pmcfetch-0.1.0/tests/test_cli.py +0 -112
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/LICENSE +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/MANIFEST.in +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/setup.cfg +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/batch.py +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/ids.py +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/pdf.py +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/py.typed +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/ratelimit.py +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/s3source.py +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/dependency_links.txt +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/entry_points.txt +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/requires.txt +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/top_level.txt +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/tests/test_batch.py +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/tests/test_ids.py +0 -0
- {pmcfetch-0.1.0 → pmcfetch-0.2.0}/tests/test_s3source.py +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pmcfetch
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Search the PMC Open Access Subset by keyword and download article PDFs from AWS. Zero dependencies.
|
|
5
5
|
Author: Kavach Shah
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/kms2229/pmcfetch
|
|
8
|
+
Project-URL: Source, https://github.com/kms2229/pmcfetch
|
|
8
9
|
Project-URL: Issues, https://github.com/kms2229/pmcfetch/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/kms2229/pmcfetch/blob/main/CHANGELOG.md
|
|
9
11
|
Keywords: pubmed-central,pmc,open-access,bulk-download,bioinformatics
|
|
10
12
|
Classifier: Development Status :: 4 - Beta
|
|
11
13
|
Classifier: Environment :: Console
|
|
@@ -31,11 +33,14 @@ Dynamic: license-file
|
|
|
31
33
|
# pmcfetch
|
|
32
34
|
|
|
33
35
|
Targeted PDF retrieval from the **PMC Open Access Subset**, with resume, retries,
|
|
34
|
-
atomic writes, and namespace-aware batch layout.
|
|
36
|
+
atomic writes, and namespace-aware batch layout. Start from a list of PMC ids,
|
|
37
|
+
or from a keyword search. Zero dependencies.
|
|
35
38
|
|
|
36
39
|
```bash
|
|
37
40
|
pip install pmcfetch
|
|
38
|
-
pmcfetch
|
|
41
|
+
pmcfetch search "burn scar" # how many open-access papers match?
|
|
42
|
+
pmcfetch search "burn scar" --download 50 # fetch the 50 most relevant
|
|
43
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --workers 16
|
|
39
44
|
```
|
|
40
45
|
|
|
41
46
|
Requires Python 3.10+. Nothing outside the standard library.
|
|
@@ -95,14 +100,39 @@ with S3OpenAccessClient() as c:
|
|
|
95
100
|
`license_code` and `is_retracted` come free with every article — filter on licence
|
|
96
101
|
terms and drop retracted papers before they reach your corpus.
|
|
97
102
|
|
|
103
|
+
Keyword search — check the count first, then take as many as you want:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from pmcfetch import PMCSearch, S3OpenAccessClient, download_many
|
|
107
|
+
|
|
108
|
+
search = PMCSearch() # PMCSearch(api_key=...) for NCBI's higher rate limit
|
|
109
|
+
summary = search.summarize("burn scar AND 2020:2025[pdat]")
|
|
110
|
+
print(summary.count, summary.warnings) # 12013 ()
|
|
111
|
+
|
|
112
|
+
records = search.records(summary.query, limit=100, namespace="burns") # limit=None for all
|
|
113
|
+
with S3OpenAccessClient() as client:
|
|
114
|
+
report = download_many(records, "pdfs/", client=client, workers=16)
|
|
115
|
+
```
|
|
116
|
+
|
|
98
117
|
## CLI
|
|
99
118
|
|
|
100
119
|
```bash
|
|
120
|
+
# how many open-access papers match? (in a terminal, it then asks how many to download)
|
|
121
|
+
pmcfetch search "wound healing AND diabetic foot"
|
|
122
|
+
|
|
123
|
+
# download some or all of them, without the question
|
|
124
|
+
pmcfetch search "burn scar" --download 100 --out pdfs/
|
|
125
|
+
pmcfetch search "burn scar" --download all --namespace burns
|
|
126
|
+
|
|
127
|
+
# or save every match now and download later, e.g. in an HPC array job
|
|
128
|
+
pmcfetch search "burn scar" --namespace burns --save burns.json
|
|
129
|
+
pmcfetch download --corpus burns.json --out pdfs/
|
|
130
|
+
|
|
101
131
|
# what's in a corpus file?
|
|
102
132
|
pmcfetch namespaces --corpus corpus.json
|
|
103
133
|
|
|
104
134
|
# pull one namespace
|
|
105
|
-
pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16
|
|
135
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16
|
|
106
136
|
|
|
107
137
|
# dry run, no requests
|
|
108
138
|
pmcfetch download --corpus corpus.json --out pdfs/ --limit 20 --dry-run
|
|
@@ -113,10 +143,19 @@ pmcfetch download --corpus pdfs/failed.txt --out pdfs/ --flat
|
|
|
113
143
|
|
|
114
144
|
Exit code is `1` when any article failed, `0` otherwise — so a batch job fails
|
|
115
145
|
loudly instead of silently producing a short corpus. Bad arguments or an
|
|
116
|
-
unreadable corpus file exit `2`, and Ctrl-C exits `130`.
|
|
146
|
+
unreadable corpus file exit `2`, and Ctrl-C exits `130`. With `--json`, stdout
|
|
147
|
+
carries only the JSON summary and progress goes to stderr, so it pipes cleanly.
|
|
148
|
+
|
|
149
|
+
S3 has no rate limit, so downloads are not paced by default and `--workers` can
|
|
150
|
+
go wide; throughput is bound by your bandwidth. `--rps N` caps requests per
|
|
151
|
+
second across all workers if you want to go easier on a shared link.
|
|
117
152
|
|
|
118
|
-
|
|
119
|
-
|
|
153
|
+
`search` takes PMC's search syntax: field tags (`smith j[author]`), date ranges
|
|
154
|
+
(`2020:2025[pdat]`), `AND`/`OR`/`NOT`. It prints how NCBI read the query and warns
|
|
155
|
+
about words it dropped. `--sort relevance` (the default) or `--sort newest` picks
|
|
156
|
+
which papers come first when you take fewer than all. `--save` writes the chosen
|
|
157
|
+
ids to a corpus file that `pmcfetch download` reads; without `--download`, it
|
|
158
|
+
saves every match and downloads none.
|
|
120
159
|
|
|
121
160
|
## Deployment
|
|
122
161
|
|
|
@@ -164,6 +203,18 @@ bad key can't abort a 20k-article batch.
|
|
|
164
203
|
raise `ArticleNotInOASubsetError`. Both land in the failure log with a reason.
|
|
165
204
|
- **Any object implementing `fetch_pdf(pmcid) -> bytes`** satisfies the
|
|
166
205
|
`PdfSource` protocol and works with `download_many`.
|
|
206
|
+
- **Search counts only open-access articles** (`"open access"[filter]`), the
|
|
207
|
+
ones the bucket holds. In a sample of 800 matches, 98% had an article PDF; the
|
|
208
|
+
rest are licensed for XML and text only, and fail with `PDFNotAvailableError`
|
|
209
|
+
like any other run.
|
|
210
|
+
- **Search has no 10,000 cap.** NCBI returns at most 9,999 ids per query, so
|
|
211
|
+
larger result sets are collected by splitting the query into PMC id ranges.
|
|
212
|
+
NCBI ranks only the first 9,999 by relevance; the rest follow newest first.
|
|
213
|
+
- **Search goes through NCBI E-utilities**, which allow 3 requests per second,
|
|
214
|
+
or 10 with an NCBI API key from your
|
|
215
|
+
[NCBI account settings](https://account.ncbi.nlm.nih.gov/settings/)
|
|
216
|
+
(`--api-key`, or `$NCBI_API_KEY`). Collecting 20,000 ids takes about 15 seconds.
|
|
217
|
+
NCBI's count can drift by a few between calls.
|
|
167
218
|
|
|
168
219
|
## Tests
|
|
169
220
|
|
|
@@ -175,7 +226,8 @@ python -m pytest tests/ -q
|
|
|
175
226
|
No network required — the S3 layer is exercised against scripted listings and
|
|
176
227
|
responses: multi-version and paginated listings, supplementary-PDF
|
|
177
228
|
disambiguation, 429 storms and `Retry-After`, 404s, truncated resumes, and
|
|
178
|
-
concurrent and interrupted batches.
|
|
229
|
+
concurrent and interrupted batches. Search runs against a scripted ESearch that
|
|
230
|
+
enforces NCBI's per-query cap, so the id-range splitting is tested too.
|
|
179
231
|
|
|
180
232
|
## Attribution
|
|
181
233
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# pmcfetch
|
|
2
2
|
|
|
3
3
|
Targeted PDF retrieval from the **PMC Open Access Subset**, with resume, retries,
|
|
4
|
-
atomic writes, and namespace-aware batch layout.
|
|
4
|
+
atomic writes, and namespace-aware batch layout. Start from a list of PMC ids,
|
|
5
|
+
or from a keyword search. Zero dependencies.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
pip install pmcfetch
|
|
8
|
-
pmcfetch
|
|
9
|
+
pmcfetch search "burn scar" # how many open-access papers match?
|
|
10
|
+
pmcfetch search "burn scar" --download 50 # fetch the 50 most relevant
|
|
11
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --workers 16
|
|
9
12
|
```
|
|
10
13
|
|
|
11
14
|
Requires Python 3.10+. Nothing outside the standard library.
|
|
@@ -65,14 +68,39 @@ with S3OpenAccessClient() as c:
|
|
|
65
68
|
`license_code` and `is_retracted` come free with every article — filter on licence
|
|
66
69
|
terms and drop retracted papers before they reach your corpus.
|
|
67
70
|
|
|
71
|
+
Keyword search — check the count first, then take as many as you want:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pmcfetch import PMCSearch, S3OpenAccessClient, download_many
|
|
75
|
+
|
|
76
|
+
search = PMCSearch() # PMCSearch(api_key=...) for NCBI's higher rate limit
|
|
77
|
+
summary = search.summarize("burn scar AND 2020:2025[pdat]")
|
|
78
|
+
print(summary.count, summary.warnings) # 12013 ()
|
|
79
|
+
|
|
80
|
+
records = search.records(summary.query, limit=100, namespace="burns") # limit=None for all
|
|
81
|
+
with S3OpenAccessClient() as client:
|
|
82
|
+
report = download_many(records, "pdfs/", client=client, workers=16)
|
|
83
|
+
```
|
|
84
|
+
|
|
68
85
|
## CLI
|
|
69
86
|
|
|
70
87
|
```bash
|
|
88
|
+
# how many open-access papers match? (in a terminal, it then asks how many to download)
|
|
89
|
+
pmcfetch search "wound healing AND diabetic foot"
|
|
90
|
+
|
|
91
|
+
# download some or all of them, without the question
|
|
92
|
+
pmcfetch search "burn scar" --download 100 --out pdfs/
|
|
93
|
+
pmcfetch search "burn scar" --download all --namespace burns
|
|
94
|
+
|
|
95
|
+
# or save every match now and download later, e.g. in an HPC array job
|
|
96
|
+
pmcfetch search "burn scar" --namespace burns --save burns.json
|
|
97
|
+
pmcfetch download --corpus burns.json --out pdfs/
|
|
98
|
+
|
|
71
99
|
# what's in a corpus file?
|
|
72
100
|
pmcfetch namespaces --corpus corpus.json
|
|
73
101
|
|
|
74
102
|
# pull one namespace
|
|
75
|
-
pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16
|
|
103
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16
|
|
76
104
|
|
|
77
105
|
# dry run, no requests
|
|
78
106
|
pmcfetch download --corpus corpus.json --out pdfs/ --limit 20 --dry-run
|
|
@@ -83,10 +111,19 @@ pmcfetch download --corpus pdfs/failed.txt --out pdfs/ --flat
|
|
|
83
111
|
|
|
84
112
|
Exit code is `1` when any article failed, `0` otherwise — so a batch job fails
|
|
85
113
|
loudly instead of silently producing a short corpus. Bad arguments or an
|
|
86
|
-
unreadable corpus file exit `2`, and Ctrl-C exits `130`.
|
|
114
|
+
unreadable corpus file exit `2`, and Ctrl-C exits `130`. With `--json`, stdout
|
|
115
|
+
carries only the JSON summary and progress goes to stderr, so it pipes cleanly.
|
|
116
|
+
|
|
117
|
+
S3 has no rate limit, so downloads are not paced by default and `--workers` can
|
|
118
|
+
go wide; throughput is bound by your bandwidth. `--rps N` caps requests per
|
|
119
|
+
second across all workers if you want to go easier on a shared link.
|
|
87
120
|
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
`search` takes PMC's search syntax: field tags (`smith j[author]`), date ranges
|
|
122
|
+
(`2020:2025[pdat]`), `AND`/`OR`/`NOT`. It prints how NCBI read the query and warns
|
|
123
|
+
about words it dropped. `--sort relevance` (the default) or `--sort newest` picks
|
|
124
|
+
which papers come first when you take fewer than all. `--save` writes the chosen
|
|
125
|
+
ids to a corpus file that `pmcfetch download` reads; without `--download`, it
|
|
126
|
+
saves every match and downloads none.
|
|
90
127
|
|
|
91
128
|
## Deployment
|
|
92
129
|
|
|
@@ -134,6 +171,18 @@ bad key can't abort a 20k-article batch.
|
|
|
134
171
|
raise `ArticleNotInOASubsetError`. Both land in the failure log with a reason.
|
|
135
172
|
- **Any object implementing `fetch_pdf(pmcid) -> bytes`** satisfies the
|
|
136
173
|
`PdfSource` protocol and works with `download_many`.
|
|
174
|
+
- **Search counts only open-access articles** (`"open access"[filter]`), the
|
|
175
|
+
ones the bucket holds. In a sample of 800 matches, 98% had an article PDF; the
|
|
176
|
+
rest are licensed for XML and text only, and fail with `PDFNotAvailableError`
|
|
177
|
+
like any other run.
|
|
178
|
+
- **Search has no 10,000 cap.** NCBI returns at most 9,999 ids per query, so
|
|
179
|
+
larger result sets are collected by splitting the query into PMC id ranges.
|
|
180
|
+
NCBI ranks only the first 9,999 by relevance; the rest follow newest first.
|
|
181
|
+
- **Search goes through NCBI E-utilities**, which allow 3 requests per second,
|
|
182
|
+
or 10 with an NCBI API key from your
|
|
183
|
+
[NCBI account settings](https://account.ncbi.nlm.nih.gov/settings/)
|
|
184
|
+
(`--api-key`, or `$NCBI_API_KEY`). Collecting 20,000 ids takes about 15 seconds.
|
|
185
|
+
NCBI's count can drift by a few between calls.
|
|
137
186
|
|
|
138
187
|
## Tests
|
|
139
188
|
|
|
@@ -145,7 +194,8 @@ python -m pytest tests/ -q
|
|
|
145
194
|
No network required — the S3 layer is exercised against scripted listings and
|
|
146
195
|
responses: multi-version and paginated listings, supplementary-PDF
|
|
147
196
|
disambiguation, 429 storms and `Retry-After`, 404s, truncated resumes, and
|
|
148
|
-
concurrent and interrupted batches.
|
|
197
|
+
concurrent and interrupted batches. Search runs against a scripted ESearch that
|
|
198
|
+
enforces NCBI's per-query cap, so the id-range splitting is tested too.
|
|
149
199
|
|
|
150
200
|
## Attribution
|
|
151
201
|
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pmcfetch"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "
|
|
8
|
+
description = "Search the PMC Open Access Subset by keyword and download article PDFs from AWS. Zero dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
@@ -38,7 +38,9 @@ dev = [
|
|
|
38
38
|
|
|
39
39
|
[project.urls]
|
|
40
40
|
Homepage = "https://github.com/kms2229/pmcfetch"
|
|
41
|
+
Source = "https://github.com/kms2229/pmcfetch"
|
|
41
42
|
Issues = "https://github.com/kms2229/pmcfetch/issues"
|
|
43
|
+
Changelog = "https://github.com/kms2229/pmcfetch/blob/main/CHANGELOG.md"
|
|
42
44
|
|
|
43
45
|
[project.scripts]
|
|
44
46
|
pmcfetch = "pmcfetch.cli:main"
|
|
@@ -8,7 +8,9 @@ and it is the access route PMC sanctions for systematic retrieval.
|
|
|
8
8
|
care about. This package fills that gap: give it a list of PMC ids and it
|
|
9
9
|
resolves each article's version, picks the article PDF (not the publisher's
|
|
10
10
|
supplementary PDFs sitting in the same prefix), and writes the results with
|
|
11
|
-
resume, retries, atomic writes, and per-namespace layout.
|
|
11
|
+
resume, retries, atomic writes, and per-namespace layout. Or start from a
|
|
12
|
+
keyword search: :class:`PMCSearch` counts the matching open-access articles
|
|
13
|
+
before anything is downloaded.
|
|
12
14
|
|
|
13
15
|
Zero third-party dependencies.
|
|
14
16
|
|
|
@@ -22,7 +24,7 @@ Zero third-party dependencies.
|
|
|
22
24
|
|
|
23
25
|
from __future__ import annotations
|
|
24
26
|
|
|
25
|
-
__version__ = "0.
|
|
27
|
+
__version__ = "0.2.0"
|
|
26
28
|
|
|
27
29
|
from pmcfetch.batch import (
|
|
28
30
|
BatchReport,
|
|
@@ -40,10 +42,11 @@ from pmcfetch.errors import (
|
|
|
40
42
|
InvalidPMCIDError,
|
|
41
43
|
NotAPDFError,
|
|
42
44
|
PMCFetchError,
|
|
45
|
+
SearchError,
|
|
43
46
|
TransportError,
|
|
44
47
|
)
|
|
45
48
|
from pmcfetch.ids import normalize_pmcid, try_normalize_pmcid
|
|
46
|
-
from pmcfetch.manifest import CorpusRecord, LoadResult, iter_records, load_records
|
|
49
|
+
from pmcfetch.manifest import CorpusRecord, LoadResult, iter_records, load_records, save_records
|
|
47
50
|
from pmcfetch.pdf import DEFAULT_MIN_PDF_BYTES, DEFAULT_TIMEOUT, PdfSource, is_pdf
|
|
48
51
|
from pmcfetch.ratelimit import MinIntervalLimiter, NullLimiter
|
|
49
52
|
from pmcfetch.s3source import (
|
|
@@ -56,9 +59,13 @@ from pmcfetch.s3source import (
|
|
|
56
59
|
list_article_versions,
|
|
57
60
|
resolve_latest_version,
|
|
58
61
|
)
|
|
62
|
+
from pmcfetch.search import PMCSearch, SearchSummary
|
|
59
63
|
|
|
60
64
|
__all__ = [
|
|
61
65
|
"__version__",
|
|
66
|
+
# search
|
|
67
|
+
"PMCSearch",
|
|
68
|
+
"SearchSummary",
|
|
62
69
|
# source
|
|
63
70
|
"S3OpenAccessClient",
|
|
64
71
|
"OA_BUCKET",
|
|
@@ -77,6 +84,7 @@ __all__ = [
|
|
|
77
84
|
"write_pdf_atomic",
|
|
78
85
|
# manifest
|
|
79
86
|
"load_records",
|
|
87
|
+
"save_records",
|
|
80
88
|
"iter_records",
|
|
81
89
|
"CorpusRecord",
|
|
82
90
|
"LoadResult",
|
|
@@ -99,4 +107,5 @@ __all__ = [
|
|
|
99
107
|
"ArticleNotInOASubsetError",
|
|
100
108
|
"InvalidPMCIDError",
|
|
101
109
|
"TransportError",
|
|
110
|
+
"SearchError",
|
|
102
111
|
]
|