pmcfetch 0.1.1__tar.gz → 0.2.1__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. {pmcfetch-0.1.1/src/pmcfetch.egg-info → pmcfetch-0.2.1}/PKG-INFO +83 -11
  2. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/README.md +81 -9
  3. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/pyproject.toml +1 -1
  4. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/__init__.py +12 -3
  5. pmcfetch-0.2.1/src/pmcfetch/cli.py +420 -0
  6. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/errors.py +5 -0
  7. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/manifest.py +10 -0
  8. pmcfetch-0.2.1/src/pmcfetch/search.py +249 -0
  9. {pmcfetch-0.1.1 → pmcfetch-0.2.1/src/pmcfetch.egg-info}/PKG-INFO +83 -11
  10. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch.egg-info/SOURCES.txt +3 -1
  11. pmcfetch-0.2.1/tests/conftest.py +93 -0
  12. pmcfetch-0.2.1/tests/test_cli.py +278 -0
  13. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/tests/test_manifest.py +13 -1
  14. pmcfetch-0.2.1/tests/test_search.py +161 -0
  15. pmcfetch-0.1.1/src/pmcfetch/cli.py +0 -228
  16. pmcfetch-0.1.1/tests/conftest.py +0 -44
  17. pmcfetch-0.1.1/tests/test_cli.py +0 -112
  18. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/LICENSE +0 -0
  19. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/MANIFEST.in +0 -0
  20. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/setup.cfg +0 -0
  21. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/batch.py +0 -0
  22. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/ids.py +0 -0
  23. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/pdf.py +0 -0
  24. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/py.typed +0 -0
  25. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/ratelimit.py +0 -0
  26. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch/s3source.py +0 -0
  27. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch.egg-info/dependency_links.txt +0 -0
  28. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch.egg-info/entry_points.txt +0 -0
  29. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch.egg-info/requires.txt +0 -0
  30. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/src/pmcfetch.egg-info/top_level.txt +0 -0
  31. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/tests/test_batch.py +0 -0
  32. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/tests/test_ids.py +0 -0
  33. {pmcfetch-0.1.1 → pmcfetch-0.2.1}/tests/test_s3source.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pmcfetch
3
- Version: 0.1.1
4
- Summary: Targeted PDF retrieval from the PMC Open Access Subset on AWS. Zero dependencies.
3
+ Version: 0.2.1
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
@@ -32,15 +32,40 @@ Dynamic: license-file
32
32
 
33
33
  # pmcfetch
34
34
 
35
- Targeted PDF retrieval from the **PMC Open Access Subset**, with resume, retries,
36
- atomic writes, and namespace-aware batch layout. Zero dependencies.
35
+ Download open-access research papers as PDFs from **PubMed Central**, straight
36
+ from the PMC Open Access Subset on AWS, with resume, retries and atomic writes.
37
+ Requires Python 3.10+, and nothing outside the standard library.
37
38
 
38
39
  ```bash
39
40
  pip install pmcfetch
40
- pmcfetch download --corpus corpus.json --out pdfs/ --workers 16 --rps 0
41
41
  ```
42
42
 
43
- Requires Python 3.10+. Nothing outside the standard library.
43
+ ## What it does
44
+
45
+ **1. Download papers by id.** You already know which papers you want: list their
46
+ PMC ids in a text file, one per line (like `PMC10007932`).
47
+
48
+ ```bash
49
+ pmcfetch download --corpus ids.txt --out pdfs/
50
+ ```
51
+
52
+ **2. Search by keyword.** See how many open-access papers match before
53
+ downloading anything.
54
+
55
+ ```bash
56
+ pmcfetch search "burn scar"
57
+ # [pmcfetch] 20,349 open-access article(s) match 'burn scar'
58
+ ```
59
+
60
+ **3. Download the matches.** Take all of them, or only as many as you want.
61
+
62
+ ```bash
63
+ pmcfetch search "burn scar" --download 50 # the 50 most relevant
64
+ pmcfetch search "burn scar" --download all # every match
65
+ ```
66
+
67
+ In a terminal, `pmcfetch search` asks how many to download right after showing
68
+ the count, so steps 2 and 3 can happen in one go.
44
69
 
45
70
  ## Why
46
71
 
@@ -97,14 +122,39 @@ with S3OpenAccessClient() as c:
97
122
  `license_code` and `is_retracted` come free with every article — filter on licence
98
123
  terms and drop retracted papers before they reach your corpus.
99
124
 
125
+ Keyword search — check the count first, then take as many as you want:
126
+
127
+ ```python
128
+ from pmcfetch import PMCSearch, S3OpenAccessClient, download_many
129
+
130
+ search = PMCSearch() # PMCSearch(api_key=...) for NCBI's higher rate limit
131
+ summary = search.summarize("burn scar AND 2020:2025[pdat]")
132
+ print(summary.count, summary.warnings) # 12013 ()
133
+
134
+ records = search.records(summary.query, limit=100, namespace="burns") # limit=None for all
135
+ with S3OpenAccessClient() as client:
136
+ report = download_many(records, "pdfs/", client=client, workers=16)
137
+ ```
138
+
100
139
  ## CLI
101
140
 
102
141
  ```bash
142
+ # how many open-access papers match? (in a terminal, it then asks how many to download)
143
+ pmcfetch search "wound healing AND diabetic foot"
144
+
145
+ # download some or all of them, without the question
146
+ pmcfetch search "burn scar" --download 100 --out pdfs/
147
+ pmcfetch search "burn scar" --download all --namespace burns
148
+
149
+ # or save every match now and download later, e.g. in an HPC array job
150
+ pmcfetch search "burn scar" --namespace burns --save burns.json
151
+ pmcfetch download --corpus burns.json --out pdfs/
152
+
103
153
  # what's in a corpus file?
104
154
  pmcfetch namespaces --corpus corpus.json
105
155
 
106
156
  # pull one namespace
107
- pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16 --rps 0
157
+ pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16
108
158
 
109
159
  # dry run, no requests
110
160
  pmcfetch download --corpus corpus.json --out pdfs/ --limit 20 --dry-run
@@ -115,10 +165,19 @@ pmcfetch download --corpus pdfs/failed.txt --out pdfs/ --flat
115
165
 
116
166
  Exit code is `1` when any article failed, `0` otherwise — so a batch job fails
117
167
  loudly instead of silently producing a short corpus. Bad arguments or an
118
- unreadable corpus file exit `2`, and Ctrl-C exits `130`.
168
+ unreadable corpus file exit `2`, and Ctrl-C exits `130`. With `--json`, stdout
169
+ carries only the JSON summary and progress goes to stderr, so it pipes cleanly.
170
+
171
+ S3 has no rate limit, so downloads are not paced by default and `--workers` can
172
+ go wide; throughput is bound by your bandwidth. `--rps N` caps requests per
173
+ second across all workers if you want to go easier on a shared link.
119
174
 
120
- S3 has no rate limit, so `--rps 0` switches pacing off entirely (`--jitter` then
121
- has no effect) and `--workers` can go wide; throughput is bound by your bandwidth.
175
+ `search` takes PMC's search syntax: field tags (`smith j[author]`), date ranges
176
+ (`2020:2025[pdat]`), `AND`/`OR`/`NOT`. It prints how NCBI read the query and warns
177
+ about words it dropped. `--sort relevance` (the default) or `--sort newest` picks
178
+ which papers come first when you take fewer than all. `--save` writes the chosen
179
+ ids to a corpus file that `pmcfetch download` reads; without `--download`, it
180
+ saves every match and downloads none.
122
181
 
123
182
  ## Deployment
124
183
 
@@ -166,6 +225,18 @@ bad key can't abort a 20k-article batch.
166
225
  raise `ArticleNotInOASubsetError`. Both land in the failure log with a reason.
167
226
  - **Any object implementing `fetch_pdf(pmcid) -> bytes`** satisfies the
168
227
  `PdfSource` protocol and works with `download_many`.
228
+ - **Search counts only open-access articles** (`"open access"[filter]`), the
229
+ ones the bucket holds. In a sample of 800 matches, 98% had an article PDF; the
230
+ rest are licensed for XML and text only, and fail with `PDFNotAvailableError`
231
+ like any other run.
232
+ - **Search has no 10,000 cap.** NCBI returns at most 9,999 ids per query, so
233
+ larger result sets are collected by splitting the query into PMC id ranges.
234
+ NCBI ranks only the first 9,999 by relevance; the rest follow newest first.
235
+ - **Search goes through NCBI E-utilities**, which allow 3 requests per second,
236
+ or 10 with an NCBI API key from your
237
+ [NCBI account settings](https://account.ncbi.nlm.nih.gov/settings/)
238
+ (`--api-key`, or `$NCBI_API_KEY`). Collecting 20,000 ids takes about 15 seconds.
239
+ NCBI's count can drift by a few between calls.
169
240
 
170
241
  ## Tests
171
242
 
@@ -177,7 +248,8 @@ python -m pytest tests/ -q
177
248
  No network required — the S3 layer is exercised against scripted listings and
178
249
  responses: multi-version and paginated listings, supplementary-PDF
179
250
  disambiguation, 429 storms and `Retry-After`, 404s, truncated resumes, and
180
- concurrent and interrupted batches.
251
+ concurrent and interrupted batches. Search runs against a scripted ESearch that
252
+ enforces NCBI's per-query cap, so the id-range splitting is tested too.
181
253
 
182
254
  ## Attribution
183
255
 
@@ -1,14 +1,39 @@
1
1
  # pmcfetch
2
2
 
3
- Targeted PDF retrieval from the **PMC Open Access Subset**, with resume, retries,
4
- atomic writes, and namespace-aware batch layout. Zero dependencies.
3
+ Download open-access research papers as PDFs from **PubMed Central**, straight
4
+ from the PMC Open Access Subset on AWS, with resume, retries and atomic writes.
5
+ Requires Python 3.10+, and nothing outside the standard library.
5
6
 
6
7
  ```bash
7
8
  pip install pmcfetch
8
- pmcfetch download --corpus corpus.json --out pdfs/ --workers 16 --rps 0
9
9
  ```
10
10
 
11
- Requires Python 3.10+. Nothing outside the standard library.
11
+ ## What it does
12
+
13
+ **1. Download papers by id.** You already know which papers you want: list their
14
+ PMC ids in a text file, one per line (like `PMC10007932`).
15
+
16
+ ```bash
17
+ pmcfetch download --corpus ids.txt --out pdfs/
18
+ ```
19
+
20
+ **2. Search by keyword.** See how many open-access papers match before
21
+ downloading anything.
22
+
23
+ ```bash
24
+ pmcfetch search "burn scar"
25
+ # [pmcfetch] 20,349 open-access article(s) match 'burn scar'
26
+ ```
27
+
28
+ **3. Download the matches.** Take all of them, or only as many as you want.
29
+
30
+ ```bash
31
+ pmcfetch search "burn scar" --download 50 # the 50 most relevant
32
+ pmcfetch search "burn scar" --download all # every match
33
+ ```
34
+
35
+ In a terminal, `pmcfetch search` asks how many to download right after showing
36
+ the count, so steps 2 and 3 can happen in one go.
12
37
 
13
38
  ## Why
14
39
 
@@ -65,14 +90,39 @@ with S3OpenAccessClient() as c:
65
90
  `license_code` and `is_retracted` come free with every article — filter on licence
66
91
  terms and drop retracted papers before they reach your corpus.
67
92
 
93
+ Keyword search — check the count first, then take as many as you want:
94
+
95
+ ```python
96
+ from pmcfetch import PMCSearch, S3OpenAccessClient, download_many
97
+
98
+ search = PMCSearch() # PMCSearch(api_key=...) for NCBI's higher rate limit
99
+ summary = search.summarize("burn scar AND 2020:2025[pdat]")
100
+ print(summary.count, summary.warnings) # 12013 ()
101
+
102
+ records = search.records(summary.query, limit=100, namespace="burns") # limit=None for all
103
+ with S3OpenAccessClient() as client:
104
+ report = download_many(records, "pdfs/", client=client, workers=16)
105
+ ```
106
+
68
107
  ## CLI
69
108
 
70
109
  ```bash
110
+ # how many open-access papers match? (in a terminal, it then asks how many to download)
111
+ pmcfetch search "wound healing AND diabetic foot"
112
+
113
+ # download some or all of them, without the question
114
+ pmcfetch search "burn scar" --download 100 --out pdfs/
115
+ pmcfetch search "burn scar" --download all --namespace burns
116
+
117
+ # or save every match now and download later, e.g. in an HPC array job
118
+ pmcfetch search "burn scar" --namespace burns --save burns.json
119
+ pmcfetch download --corpus burns.json --out pdfs/
120
+
71
121
  # what's in a corpus file?
72
122
  pmcfetch namespaces --corpus corpus.json
73
123
 
74
124
  # pull one namespace
75
- pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16 --rps 0
125
+ pmcfetch download --corpus corpus.json --out pdfs/ --namespace cardiology --workers 16
76
126
 
77
127
  # dry run, no requests
78
128
  pmcfetch download --corpus corpus.json --out pdfs/ --limit 20 --dry-run
@@ -83,10 +133,19 @@ pmcfetch download --corpus pdfs/failed.txt --out pdfs/ --flat
83
133
 
84
134
  Exit code is `1` when any article failed, `0` otherwise — so a batch job fails
85
135
  loudly instead of silently producing a short corpus. Bad arguments or an
86
- unreadable corpus file exit `2`, and Ctrl-C exits `130`.
136
+ unreadable corpus file exit `2`, and Ctrl-C exits `130`. With `--json`, stdout
137
+ carries only the JSON summary and progress goes to stderr, so it pipes cleanly.
138
+
139
+ S3 has no rate limit, so downloads are not paced by default and `--workers` can
140
+ go wide; throughput is bound by your bandwidth. `--rps N` caps requests per
141
+ second across all workers if you want to go easier on a shared link.
87
142
 
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.
143
+ `search` takes PMC's search syntax: field tags (`smith j[author]`), date ranges
144
+ (`2020:2025[pdat]`), `AND`/`OR`/`NOT`. It prints how NCBI read the query and warns
145
+ about words it dropped. `--sort relevance` (the default) or `--sort newest` picks
146
+ which papers come first when you take fewer than all. `--save` writes the chosen
147
+ ids to a corpus file that `pmcfetch download` reads; without `--download`, it
148
+ saves every match and downloads none.
90
149
 
91
150
  ## Deployment
92
151
 
@@ -134,6 +193,18 @@ bad key can't abort a 20k-article batch.
134
193
  raise `ArticleNotInOASubsetError`. Both land in the failure log with a reason.
135
194
  - **Any object implementing `fetch_pdf(pmcid) -> bytes`** satisfies the
136
195
  `PdfSource` protocol and works with `download_many`.
196
+ - **Search counts only open-access articles** (`"open access"[filter]`), the
197
+ ones the bucket holds. In a sample of 800 matches, 98% had an article PDF; the
198
+ rest are licensed for XML and text only, and fail with `PDFNotAvailableError`
199
+ like any other run.
200
+ - **Search has no 10,000 cap.** NCBI returns at most 9,999 ids per query, so
201
+ larger result sets are collected by splitting the query into PMC id ranges.
202
+ NCBI ranks only the first 9,999 by relevance; the rest follow newest first.
203
+ - **Search goes through NCBI E-utilities**, which allow 3 requests per second,
204
+ or 10 with an NCBI API key from your
205
+ [NCBI account settings](https://account.ncbi.nlm.nih.gov/settings/)
206
+ (`--api-key`, or `$NCBI_API_KEY`). Collecting 20,000 ids takes about 15 seconds.
207
+ NCBI's count can drift by a few between calls.
137
208
 
138
209
  ## Tests
139
210
 
@@ -145,7 +216,8 @@ python -m pytest tests/ -q
145
216
  No network required — the S3 layer is exercised against scripted listings and
146
217
  responses: multi-version and paginated listings, supplementary-PDF
147
218
  disambiguation, 429 storms and `Retry-After`, 404s, truncated resumes, and
148
- concurrent and interrupted batches.
219
+ concurrent and interrupted batches. Search runs against a scripted ESearch that
220
+ enforces NCBI's per-query cap, so the id-range splitting is tested too.
149
221
 
150
222
  ## Attribution
151
223
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "pmcfetch"
7
7
  dynamic = ["version"]
8
- description = "Targeted PDF retrieval from the PMC Open Access Subset on AWS. Zero dependencies."
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"]
@@ -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.1.1"
27
+ __version__ = "0.2.1"
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
  ]