pmcfetch 0.1.0__py3-none-any.whl

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.
@@ -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).
@@ -0,0 +1,16 @@
1
+ pmcfetch/__init__.py,sha256=iXn10-NB8MHoi5nXbmNcnLqwV26oHGB23PRt7mFOU5g,2743
2
+ pmcfetch/batch.py,sha256=vM5NqtnPvr3yKxPKG7rRvn9NoLEyiCgqNITDWcIbZxo,8934
3
+ pmcfetch/cli.py,sha256=JxTfix3EFzboZPITdfxwV8OZ7WrIH1FAojlTdq6JPvI,8414
4
+ pmcfetch/errors.py,sha256=dp580Az12sCKHMn91PauBnwLAyHsayP9vMEeUIsobbU,1700
5
+ pmcfetch/ids.py,sha256=Kd5LxgAT1PgEr1-ag2LKeaEWASpahYdoVFfzFKJVIZE,1330
6
+ pmcfetch/manifest.py,sha256=YQ9mCYbj2SxOBQylTWyhHnHwR-rVOweP6BYfUUg2YDg,4856
7
+ pmcfetch/pdf.py,sha256=u6Mx1HBuY63uMBSBq94-dPIuvN6jz9JVG2yT8DMu7Xk,1408
8
+ pmcfetch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ pmcfetch/ratelimit.py,sha256=MfkMqkcC6NJmvemrdR9Liw6myZ9ed4OKfc8Ym3bkgcM,1751
10
+ pmcfetch/s3source.py,sha256=NJO0VYV2F-ST2hTojAuI8pL6nDA2SxO_LbNwy7h1wAQ,9331
11
+ pmcfetch-0.1.0.dist-info/licenses/LICENSE,sha256=jQlZX-s8ad_E1M6EP_SAWrOMqVVCKIg1SJnove7OBpM,1068
12
+ pmcfetch-0.1.0.dist-info/METADATA,sha256=UNZrQPMbkfV2lQjYAZjN9I8bnFGILl_eAZO3bHejw9o,7342
13
+ pmcfetch-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
14
+ pmcfetch-0.1.0.dist-info/entry_points.txt,sha256=OP-uK5C4btBKS-S5OVJ4dB94BPfC8OzeXTIKEeFCt00,47
15
+ pmcfetch-0.1.0.dist-info/top_level.txt,sha256=ISYBRCuQLP7ke6rQsK7Xlt5u79NBlkK30ls1C8DHKyA,9
16
+ pmcfetch-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pmcfetch = pmcfetch.cli:main
@@ -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.
@@ -0,0 +1 @@
1
+ pmcfetch