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 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.
@@ -0,0 +1,2 @@
1
+ # setuptools ships tests/test_*.py in the sdist by default, but not the conftest they import.
2
+ recursive-include tests *.py
@@ -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,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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]