pmcfetch 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.
- pmcfetch-0.1.0/LICENSE +21 -0
- pmcfetch-0.1.0/MANIFEST.in +2 -0
- pmcfetch-0.1.0/PKG-INFO +186 -0
- pmcfetch-0.1.0/README.md +156 -0
- pmcfetch-0.1.0/pyproject.toml +61 -0
- pmcfetch-0.1.0/setup.cfg +4 -0
- pmcfetch-0.1.0/src/pmcfetch/__init__.py +102 -0
- pmcfetch-0.1.0/src/pmcfetch/batch.py +278 -0
- pmcfetch-0.1.0/src/pmcfetch/cli.py +228 -0
- pmcfetch-0.1.0/src/pmcfetch/errors.py +61 -0
- pmcfetch-0.1.0/src/pmcfetch/ids.py +41 -0
- pmcfetch-0.1.0/src/pmcfetch/manifest.py +160 -0
- pmcfetch-0.1.0/src/pmcfetch/pdf.py +44 -0
- pmcfetch-0.1.0/src/pmcfetch/py.typed +0 -0
- pmcfetch-0.1.0/src/pmcfetch/ratelimit.py +53 -0
- pmcfetch-0.1.0/src/pmcfetch/s3source.py +251 -0
- pmcfetch-0.1.0/src/pmcfetch.egg-info/PKG-INFO +186 -0
- pmcfetch-0.1.0/src/pmcfetch.egg-info/SOURCES.txt +26 -0
- pmcfetch-0.1.0/src/pmcfetch.egg-info/dependency_links.txt +1 -0
- pmcfetch-0.1.0/src/pmcfetch.egg-info/entry_points.txt +2 -0
- pmcfetch-0.1.0/src/pmcfetch.egg-info/requires.txt +3 -0
- pmcfetch-0.1.0/src/pmcfetch.egg-info/top_level.txt +1 -0
- pmcfetch-0.1.0/tests/conftest.py +44 -0
- pmcfetch-0.1.0/tests/test_batch.py +206 -0
- pmcfetch-0.1.0/tests/test_cli.py +112 -0
- pmcfetch-0.1.0/tests/test_ids.py +32 -0
- pmcfetch-0.1.0/tests/test_manifest.py +80 -0
- pmcfetch-0.1.0/tests/test_s3source.py +218 -0
pmcfetch-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kavach Shah
|
|
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.
|
pmcfetch-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pmcfetch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Targeted PDF retrieval from the PMC Open Access Subset on AWS. Zero dependencies.
|
|
5
|
+
Author: Kavach Shah
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kms2229/pmcfetch
|
|
8
|
+
Project-URL: Issues, https://github.com/kms2229/pmcfetch/issues
|
|
9
|
+
Keywords: pubmed-central,pmc,open-access,bulk-download,bioinformatics
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
22
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# pmcfetch
|
|
32
|
+
|
|
33
|
+
Targeted PDF retrieval from the **PMC Open Access Subset**, with resume, retries,
|
|
34
|
+
atomic writes, and namespace-aware batch layout. Zero dependencies.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pmcfetch
|
|
38
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --workers 16 --rps 0
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10+. Nothing outside the standard library.
|
|
42
|
+
|
|
43
|
+
## Why
|
|
44
|
+
|
|
45
|
+
NLM publishes every PMC Open Access article version in the public bucket
|
|
46
|
+
`s3://pmc-oa-opendata` — anonymous HTTPS, no AWS account, no credentials, no rate
|
|
47
|
+
limit, and the access route PMC sanctions for systematic retrieval.
|
|
48
|
+
|
|
49
|
+
But `aws s3 sync` is all-or-nothing. There's no `--from-id-list`, so pulling the
|
|
50
|
+
few thousand articles you care about means pulling all ~8 million. And fetching
|
|
51
|
+
them one by one with `aws s3 cp` requires knowing things that aren't obvious:
|
|
52
|
+
|
|
53
|
+
- **Which version?** Articles live under `PMC10007932.1/`, `.2/`, sometimes `.10/`.
|
|
54
|
+
You can't know without listing first — and the sort must be numeric, not
|
|
55
|
+
lexicographic.
|
|
56
|
+
- **Which PDF?** `PMC10007932.1/` also contains `jgo-14-01-302-coif.pdf`,
|
|
57
|
+
`-dss.pdf`, `-prf.pdf`. Glob `*.pdf` and you get the wrong file.
|
|
58
|
+
- **Not every article has one.** Some licences permit XML and plain-text
|
|
59
|
+
redistribution but not the PDF.
|
|
60
|
+
|
|
61
|
+
`pmcfetch` handles all three, then adds the batch machinery you'd otherwise
|
|
62
|
+
rewrite: resume, atomic writes, concurrency, pacing, and structured failure logs.
|
|
63
|
+
|
|
64
|
+
## Python API
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pmcfetch import S3OpenAccessClient, load_records, download_many, FailureLog
|
|
68
|
+
|
|
69
|
+
records = load_records("corpus.json").for_namespace("cardiology")
|
|
70
|
+
|
|
71
|
+
with S3OpenAccessClient() as client:
|
|
72
|
+
report = download_many(
|
|
73
|
+
records, "pdfs/",
|
|
74
|
+
client=client,
|
|
75
|
+
workers=16,
|
|
76
|
+
failure_log=FailureLog("failed.txt"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
print(report.as_dict()) # {'total': 3139, 'downloaded': 3104, 'skipped': 0, 'failed': 35}
|
|
80
|
+
for r in report.failures:
|
|
81
|
+
print(r.pmcid, r.reason)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Single article, plus metadata:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
with S3OpenAccessClient() as c:
|
|
88
|
+
meta = c.metadata("10007932")
|
|
89
|
+
# {'pmcid': 'PMC10007932', 'doi': '10.21037/jgo-23-76',
|
|
90
|
+
# 'license_code': 'CC BY-NC-ND', 'is_retracted': False, ...}
|
|
91
|
+
if not meta["is_retracted"]:
|
|
92
|
+
pdf = c.fetch_pdf("10007932") # accepts 10007932 / pmc10007932 / PMC10007932
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`license_code` and `is_retracted` come free with every article — filter on licence
|
|
96
|
+
terms and drop retracted papers before they reach your corpus.
|
|
97
|
+
|
|
98
|
+
## CLI
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# what's in a corpus file?
|
|
102
|
+
pmcfetch namespaces --corpus corpus.json
|
|
103
|
+
|
|
104
|
+
# pull one namespace
|
|
105
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16 --rps 0
|
|
106
|
+
|
|
107
|
+
# dry run, no requests
|
|
108
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --limit 20 --dry-run
|
|
109
|
+
|
|
110
|
+
# retry failures — the log doubles as an input list
|
|
111
|
+
pmcfetch download --corpus pdfs/failed.txt --out pdfs/ --flat
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Exit code is `1` when any article failed, `0` otherwise — so a batch job fails
|
|
115
|
+
loudly instead of silently producing a short corpus. Bad arguments or an
|
|
116
|
+
unreadable corpus file exit `2`, and Ctrl-C exits `130`.
|
|
117
|
+
|
|
118
|
+
S3 has no rate limit, so `--rps 0` switches pacing off entirely (`--jitter` then
|
|
119
|
+
has no effect) and `--workers` can go wide; throughput is bound by your bandwidth.
|
|
120
|
+
|
|
121
|
+
## Deployment
|
|
122
|
+
|
|
123
|
+
- **Azure** — VM or Container Instances with Blob persistence, plus cost notes.
|
|
124
|
+
See [`docs/azure.md`](https://github.com/kms2229/pmcfetch/blob/main/docs/azure.md).
|
|
125
|
+
- **HPC / SGE** — `--task-id` (defaulting to `$SGE_TASK_ID`) selects a namespace
|
|
126
|
+
by 1-based index. See
|
|
127
|
+
[`examples/sge/download_array.qsub`](https://github.com/kms2229/pmcfetch/blob/main/examples/sge/download_array.qsub).
|
|
128
|
+
|
|
129
|
+
Nothing in the library is platform-specific; those are examples.
|
|
130
|
+
|
|
131
|
+
## Corpus formats
|
|
132
|
+
|
|
133
|
+
`load_records()` normalises keys (`10007932`, `pmc10007932`, `PMC10007932`) and
|
|
134
|
+
accepts:
|
|
135
|
+
|
|
136
|
+
| Input | Shape |
|
|
137
|
+
|---|---|
|
|
138
|
+
| assignment JSON | `{"<id>": {"metadata": {"namespace": "cardiology"}}}` |
|
|
139
|
+
| full-text dump | `{"<id>": "<full text>"}` |
|
|
140
|
+
| extraction output | `{"pmc10007932": {"sections": [...]}}` |
|
|
141
|
+
| plain array | `["PMC1", "2", ...]` |
|
|
142
|
+
| `.txt` | newline-delimited ids, including pmcfetch's own failure logs |
|
|
143
|
+
|
|
144
|
+
Keys that aren't PMC ids land in `LoadResult.skipped` rather than raising, so one
|
|
145
|
+
bad key can't abort a 20k-article batch.
|
|
146
|
+
|
|
147
|
+
## Behaviour worth knowing
|
|
148
|
+
|
|
149
|
+
- **Resume** checks size *and* `%PDF` magic, so a half-written file from a killed
|
|
150
|
+
job is re-fetched rather than treated as complete. Downloads are held to the
|
|
151
|
+
same check before they are written, so every file on disk is one the next run
|
|
152
|
+
skips.
|
|
153
|
+
- **Writes are atomic** — `.part` + `fsync` + rename; a failed write removes its
|
|
154
|
+
`.part`.
|
|
155
|
+
- **Retries** cover 429/5xx and transport errors with exponential backoff and full
|
|
156
|
+
jitter, stretched to any `Retry-After` the server sends (no wait exceeds
|
|
157
|
+
`backoff_cap`, 15 s by default). 404s and non-PDF bodies are not retried.
|
|
158
|
+
- **Rate limiting** is process-wide, so `--workers` and `--rps` are independent.
|
|
159
|
+
- **Ctrl-C stops promptly** — queued articles are cancelled; only those already
|
|
160
|
+
in flight finish.
|
|
161
|
+
- **Namespaces become one folder name** — path separators are replaced, so a
|
|
162
|
+
corpus file cannot write outside `--out`.
|
|
163
|
+
- **Articles with no PDF** raise `PDFNotAvailableError`; ones outside the subset
|
|
164
|
+
raise `ArticleNotInOASubsetError`. Both land in the failure log with a reason.
|
|
165
|
+
- **Any object implementing `fetch_pdf(pmcid) -> bytes`** satisfies the
|
|
166
|
+
`PdfSource` protocol and works with `download_many`.
|
|
167
|
+
|
|
168
|
+
## Tests
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pip install -e '.[dev]'
|
|
172
|
+
python -m pytest tests/ -q
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
No network required — the S3 layer is exercised against scripted listings and
|
|
176
|
+
responses: multi-version and paginated listings, supplementary-PDF
|
|
177
|
+
disambiguation, 429 storms and `Retry-After`, 404s, truncated resumes, and
|
|
178
|
+
concurrent and interrupted batches.
|
|
179
|
+
|
|
180
|
+
## Attribution
|
|
181
|
+
|
|
182
|
+
Data comes from the
|
|
183
|
+
[NIH NLM NCBI PubMed Central Article Datasets](https://registry.opendata.aws/ncbi-pmc/)
|
|
184
|
+
on AWS Open Data. If you redistribute anything built from it, acknowledge NLM as
|
|
185
|
+
the source and don't imply NLM endorsement — see the
|
|
186
|
+
[bucket README](https://pmc-oa-opendata.s3.amazonaws.com/README.txt).
|
pmcfetch-0.1.0/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# pmcfetch
|
|
2
|
+
|
|
3
|
+
Targeted PDF retrieval from the **PMC Open Access Subset**, with resume, retries,
|
|
4
|
+
atomic writes, and namespace-aware batch layout. Zero dependencies.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install pmcfetch
|
|
8
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --workers 16 --rps 0
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.10+. Nothing outside the standard library.
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
NLM publishes every PMC Open Access article version in the public bucket
|
|
16
|
+
`s3://pmc-oa-opendata` — anonymous HTTPS, no AWS account, no credentials, no rate
|
|
17
|
+
limit, and the access route PMC sanctions for systematic retrieval.
|
|
18
|
+
|
|
19
|
+
But `aws s3 sync` is all-or-nothing. There's no `--from-id-list`, so pulling the
|
|
20
|
+
few thousand articles you care about means pulling all ~8 million. And fetching
|
|
21
|
+
them one by one with `aws s3 cp` requires knowing things that aren't obvious:
|
|
22
|
+
|
|
23
|
+
- **Which version?** Articles live under `PMC10007932.1/`, `.2/`, sometimes `.10/`.
|
|
24
|
+
You can't know without listing first — and the sort must be numeric, not
|
|
25
|
+
lexicographic.
|
|
26
|
+
- **Which PDF?** `PMC10007932.1/` also contains `jgo-14-01-302-coif.pdf`,
|
|
27
|
+
`-dss.pdf`, `-prf.pdf`. Glob `*.pdf` and you get the wrong file.
|
|
28
|
+
- **Not every article has one.** Some licences permit XML and plain-text
|
|
29
|
+
redistribution but not the PDF.
|
|
30
|
+
|
|
31
|
+
`pmcfetch` handles all three, then adds the batch machinery you'd otherwise
|
|
32
|
+
rewrite: resume, atomic writes, concurrency, pacing, and structured failure logs.
|
|
33
|
+
|
|
34
|
+
## Python API
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from pmcfetch import S3OpenAccessClient, load_records, download_many, FailureLog
|
|
38
|
+
|
|
39
|
+
records = load_records("corpus.json").for_namespace("cardiology")
|
|
40
|
+
|
|
41
|
+
with S3OpenAccessClient() as client:
|
|
42
|
+
report = download_many(
|
|
43
|
+
records, "pdfs/",
|
|
44
|
+
client=client,
|
|
45
|
+
workers=16,
|
|
46
|
+
failure_log=FailureLog("failed.txt"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
print(report.as_dict()) # {'total': 3139, 'downloaded': 3104, 'skipped': 0, 'failed': 35}
|
|
50
|
+
for r in report.failures:
|
|
51
|
+
print(r.pmcid, r.reason)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Single article, plus metadata:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
with S3OpenAccessClient() as c:
|
|
58
|
+
meta = c.metadata("10007932")
|
|
59
|
+
# {'pmcid': 'PMC10007932', 'doi': '10.21037/jgo-23-76',
|
|
60
|
+
# 'license_code': 'CC BY-NC-ND', 'is_retracted': False, ...}
|
|
61
|
+
if not meta["is_retracted"]:
|
|
62
|
+
pdf = c.fetch_pdf("10007932") # accepts 10007932 / pmc10007932 / PMC10007932
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`license_code` and `is_retracted` come free with every article — filter on licence
|
|
66
|
+
terms and drop retracted papers before they reach your corpus.
|
|
67
|
+
|
|
68
|
+
## CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# what's in a corpus file?
|
|
72
|
+
pmcfetch namespaces --corpus corpus.json
|
|
73
|
+
|
|
74
|
+
# pull one namespace
|
|
75
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16 --rps 0
|
|
76
|
+
|
|
77
|
+
# dry run, no requests
|
|
78
|
+
pmcfetch download --corpus corpus.json --out pdfs/ --limit 20 --dry-run
|
|
79
|
+
|
|
80
|
+
# retry failures — the log doubles as an input list
|
|
81
|
+
pmcfetch download --corpus pdfs/failed.txt --out pdfs/ --flat
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Exit code is `1` when any article failed, `0` otherwise — so a batch job fails
|
|
85
|
+
loudly instead of silently producing a short corpus. Bad arguments or an
|
|
86
|
+
unreadable corpus file exit `2`, and Ctrl-C exits `130`.
|
|
87
|
+
|
|
88
|
+
S3 has no rate limit, so `--rps 0` switches pacing off entirely (`--jitter` then
|
|
89
|
+
has no effect) and `--workers` can go wide; throughput is bound by your bandwidth.
|
|
90
|
+
|
|
91
|
+
## Deployment
|
|
92
|
+
|
|
93
|
+
- **Azure** — VM or Container Instances with Blob persistence, plus cost notes.
|
|
94
|
+
See [`docs/azure.md`](https://github.com/kms2229/pmcfetch/blob/main/docs/azure.md).
|
|
95
|
+
- **HPC / SGE** — `--task-id` (defaulting to `$SGE_TASK_ID`) selects a namespace
|
|
96
|
+
by 1-based index. See
|
|
97
|
+
[`examples/sge/download_array.qsub`](https://github.com/kms2229/pmcfetch/blob/main/examples/sge/download_array.qsub).
|
|
98
|
+
|
|
99
|
+
Nothing in the library is platform-specific; those are examples.
|
|
100
|
+
|
|
101
|
+
## Corpus formats
|
|
102
|
+
|
|
103
|
+
`load_records()` normalises keys (`10007932`, `pmc10007932`, `PMC10007932`) and
|
|
104
|
+
accepts:
|
|
105
|
+
|
|
106
|
+
| Input | Shape |
|
|
107
|
+
|---|---|
|
|
108
|
+
| assignment JSON | `{"<id>": {"metadata": {"namespace": "cardiology"}}}` |
|
|
109
|
+
| full-text dump | `{"<id>": "<full text>"}` |
|
|
110
|
+
| extraction output | `{"pmc10007932": {"sections": [...]}}` |
|
|
111
|
+
| plain array | `["PMC1", "2", ...]` |
|
|
112
|
+
| `.txt` | newline-delimited ids, including pmcfetch's own failure logs |
|
|
113
|
+
|
|
114
|
+
Keys that aren't PMC ids land in `LoadResult.skipped` rather than raising, so one
|
|
115
|
+
bad key can't abort a 20k-article batch.
|
|
116
|
+
|
|
117
|
+
## Behaviour worth knowing
|
|
118
|
+
|
|
119
|
+
- **Resume** checks size *and* `%PDF` magic, so a half-written file from a killed
|
|
120
|
+
job is re-fetched rather than treated as complete. Downloads are held to the
|
|
121
|
+
same check before they are written, so every file on disk is one the next run
|
|
122
|
+
skips.
|
|
123
|
+
- **Writes are atomic** — `.part` + `fsync` + rename; a failed write removes its
|
|
124
|
+
`.part`.
|
|
125
|
+
- **Retries** cover 429/5xx and transport errors with exponential backoff and full
|
|
126
|
+
jitter, stretched to any `Retry-After` the server sends (no wait exceeds
|
|
127
|
+
`backoff_cap`, 15 s by default). 404s and non-PDF bodies are not retried.
|
|
128
|
+
- **Rate limiting** is process-wide, so `--workers` and `--rps` are independent.
|
|
129
|
+
- **Ctrl-C stops promptly** — queued articles are cancelled; only those already
|
|
130
|
+
in flight finish.
|
|
131
|
+
- **Namespaces become one folder name** — path separators are replaced, so a
|
|
132
|
+
corpus file cannot write outside `--out`.
|
|
133
|
+
- **Articles with no PDF** raise `PDFNotAvailableError`; ones outside the subset
|
|
134
|
+
raise `ArticleNotInOASubsetError`. Both land in the failure log with a reason.
|
|
135
|
+
- **Any object implementing `fetch_pdf(pmcid) -> bytes`** satisfies the
|
|
136
|
+
`PdfSource` protocol and works with `download_many`.
|
|
137
|
+
|
|
138
|
+
## Tests
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pip install -e '.[dev]'
|
|
142
|
+
python -m pytest tests/ -q
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
No network required — the S3 layer is exercised against scripted listings and
|
|
146
|
+
responses: multi-version and paginated listings, supplementary-PDF
|
|
147
|
+
disambiguation, 429 storms and `Retry-After`, 404s, truncated resumes, and
|
|
148
|
+
concurrent and interrupted batches.
|
|
149
|
+
|
|
150
|
+
## Attribution
|
|
151
|
+
|
|
152
|
+
Data comes from the
|
|
153
|
+
[NIH NLM NCBI PubMed Central Article Datasets](https://registry.opendata.aws/ncbi-pmc/)
|
|
154
|
+
on AWS Open Data. If you redistribute anything built from it, acknowledge NLM as
|
|
155
|
+
the source and don't imply NLM endorsement — see the
|
|
156
|
+
[bucket README](https://pmc-oa-opendata.s3.amazonaws.com/README.txt).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pmcfetch"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Targeted PDF retrieval from the PMC Open Access Subset on AWS. Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Kavach Shah" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
keywords = ["pubmed-central", "pmc", "open-access", "bulk-download", "bioinformatics"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
28
|
+
"Topic :: Text Processing :: Indexing",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
# Stdlib only — S3 Open Access is anonymous HTTPS.
|
|
32
|
+
dependencies = []
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/kms2229/pmcfetch"
|
|
41
|
+
Issues = "https://github.com/kms2229/pmcfetch/issues"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
pmcfetch = "pmcfetch.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
package-dir = {"" = "src"}
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
pmcfetch = ["py.typed"]
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.dynamic]
|
|
56
|
+
# Single source of truth: pmcfetch.__version__ (also used in the User-Agent).
|
|
57
|
+
version = { attr = "pmcfetch.__version__" }
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
addopts = ["-q"]
|
pmcfetch-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Targeted PDF retrieval from the PMC Open Access Subset.
|
|
2
|
+
|
|
3
|
+
NLM publishes every PMC Open Access article version in the public, anonymous
|
|
4
|
+
bucket ``s3://pmc-oa-opendata``. No AWS account, no credentials, no rate limit,
|
|
5
|
+
and it is the access route PMC sanctions for systematic retrieval.
|
|
6
|
+
|
|
7
|
+
``aws s3 sync`` is all-or-nothing — there is no way to pull just the articles you
|
|
8
|
+
care about. This package fills that gap: give it a list of PMC ids and it
|
|
9
|
+
resolves each article's version, picks the article PDF (not the publisher's
|
|
10
|
+
supplementary PDFs sitting in the same prefix), and writes the results with
|
|
11
|
+
resume, retries, atomic writes, and per-namespace layout.
|
|
12
|
+
|
|
13
|
+
Zero third-party dependencies.
|
|
14
|
+
|
|
15
|
+
from pmcfetch import S3OpenAccessClient, load_records, download_many
|
|
16
|
+
|
|
17
|
+
records = load_records("corpus.json").for_namespace("cardiology")
|
|
18
|
+
with S3OpenAccessClient() as client:
|
|
19
|
+
report = download_many(records, "pdfs/", client=client, workers=16)
|
|
20
|
+
print(report.as_dict())
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
from pmcfetch.batch import (
|
|
28
|
+
BatchReport,
|
|
29
|
+
DownloadResult,
|
|
30
|
+
FailureLog,
|
|
31
|
+
Outcome,
|
|
32
|
+
download_many,
|
|
33
|
+
download_one,
|
|
34
|
+
target_path,
|
|
35
|
+
write_pdf_atomic,
|
|
36
|
+
)
|
|
37
|
+
from pmcfetch.errors import (
|
|
38
|
+
DownloadError,
|
|
39
|
+
HTTPStatusError,
|
|
40
|
+
InvalidPMCIDError,
|
|
41
|
+
NotAPDFError,
|
|
42
|
+
PMCFetchError,
|
|
43
|
+
TransportError,
|
|
44
|
+
)
|
|
45
|
+
from pmcfetch.ids import normalize_pmcid, try_normalize_pmcid
|
|
46
|
+
from pmcfetch.manifest import CorpusRecord, LoadResult, iter_records, load_records
|
|
47
|
+
from pmcfetch.pdf import DEFAULT_MIN_PDF_BYTES, DEFAULT_TIMEOUT, PdfSource, is_pdf
|
|
48
|
+
from pmcfetch.ratelimit import MinIntervalLimiter, NullLimiter
|
|
49
|
+
from pmcfetch.s3source import (
|
|
50
|
+
OA_BASE_URL,
|
|
51
|
+
OA_BUCKET,
|
|
52
|
+
ArticleNotInOASubsetError,
|
|
53
|
+
ArticleVersion,
|
|
54
|
+
PDFNotAvailableError,
|
|
55
|
+
S3OpenAccessClient,
|
|
56
|
+
list_article_versions,
|
|
57
|
+
resolve_latest_version,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"__version__",
|
|
62
|
+
# source
|
|
63
|
+
"S3OpenAccessClient",
|
|
64
|
+
"OA_BUCKET",
|
|
65
|
+
"OA_BASE_URL",
|
|
66
|
+
"ArticleVersion",
|
|
67
|
+
"list_article_versions",
|
|
68
|
+
"resolve_latest_version",
|
|
69
|
+
# batch
|
|
70
|
+
"download_one",
|
|
71
|
+
"download_many",
|
|
72
|
+
"BatchReport",
|
|
73
|
+
"DownloadResult",
|
|
74
|
+
"Outcome",
|
|
75
|
+
"FailureLog",
|
|
76
|
+
"target_path",
|
|
77
|
+
"write_pdf_atomic",
|
|
78
|
+
# manifest
|
|
79
|
+
"load_records",
|
|
80
|
+
"iter_records",
|
|
81
|
+
"CorpusRecord",
|
|
82
|
+
"LoadResult",
|
|
83
|
+
# ids
|
|
84
|
+
"normalize_pmcid",
|
|
85
|
+
"try_normalize_pmcid",
|
|
86
|
+
# pdf / pacing
|
|
87
|
+
"PdfSource",
|
|
88
|
+
"is_pdf",
|
|
89
|
+
"DEFAULT_MIN_PDF_BYTES",
|
|
90
|
+
"DEFAULT_TIMEOUT",
|
|
91
|
+
"MinIntervalLimiter",
|
|
92
|
+
"NullLimiter",
|
|
93
|
+
# errors
|
|
94
|
+
"PMCFetchError",
|
|
95
|
+
"DownloadError",
|
|
96
|
+
"HTTPStatusError",
|
|
97
|
+
"NotAPDFError",
|
|
98
|
+
"PDFNotAvailableError",
|
|
99
|
+
"ArticleNotInOASubsetError",
|
|
100
|
+
"InvalidPMCIDError",
|
|
101
|
+
"TransportError",
|
|
102
|
+
]
|