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.
Files changed (33) hide show
  1. {pmcfetch-0.1.0/src/pmcfetch.egg-info → pmcfetch-0.2.0}/PKG-INFO +61 -9
  2. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/README.md +57 -7
  3. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/pyproject.toml +3 -1
  4. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/__init__.py +12 -3
  5. pmcfetch-0.2.0/src/pmcfetch/cli.py +420 -0
  6. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/errors.py +5 -0
  7. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/manifest.py +10 -0
  8. pmcfetch-0.2.0/src/pmcfetch/search.py +249 -0
  9. {pmcfetch-0.1.0 → pmcfetch-0.2.0/src/pmcfetch.egg-info}/PKG-INFO +61 -9
  10. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/SOURCES.txt +3 -1
  11. pmcfetch-0.2.0/tests/conftest.py +93 -0
  12. pmcfetch-0.2.0/tests/test_cli.py +278 -0
  13. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/tests/test_manifest.py +13 -1
  14. pmcfetch-0.2.0/tests/test_search.py +161 -0
  15. pmcfetch-0.1.0/src/pmcfetch/cli.py +0 -228
  16. pmcfetch-0.1.0/tests/conftest.py +0 -44
  17. pmcfetch-0.1.0/tests/test_cli.py +0 -112
  18. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/LICENSE +0 -0
  19. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/MANIFEST.in +0 -0
  20. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/setup.cfg +0 -0
  21. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/batch.py +0 -0
  22. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/ids.py +0 -0
  23. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/pdf.py +0 -0
  24. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/py.typed +0 -0
  25. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/ratelimit.py +0 -0
  26. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch/s3source.py +0 -0
  27. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/dependency_links.txt +0 -0
  28. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/entry_points.txt +0 -0
  29. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/requires.txt +0 -0
  30. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/src/pmcfetch.egg-info/top_level.txt +0 -0
  31. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/tests/test_batch.py +0 -0
  32. {pmcfetch-0.1.0 → pmcfetch-0.2.0}/tests/test_ids.py +0 -0
  33. {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.1.0
4
- Summary: Targeted PDF retrieval from the PMC Open Access Subset on AWS. Zero dependencies.
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. Zero dependencies.
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 download --corpus corpus.json --out pdfs/ --workers 16 --rps 0
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 --rps 0
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
- 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.
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. Zero dependencies.
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 download --corpus corpus.json --out pdfs/ --workers 16 --rps 0
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 --rps 0
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
- 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.
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 = "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"]
@@ -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.1.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
  ]