pyPaperFlow 0.6.0__tar.gz → 0.6.2__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.
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/PKG-INFO +57 -1
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/README.md +56 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/README_zh.md +59 -0
- pypaperflow-0.6.2/src/pyPaperFlow/__init__.py +1 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/cli.py +18 -8
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/preprint/arxiv_fetcher.py +7 -32
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/preprint/biorxiv_fetcher.py +159 -9
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/preprint/chemrxiv_fetcher.py +9 -5
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/preprint/europepmc_fetcher.py +34 -9
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/preprint/source_utils.py +61 -1
- pypaperflow-0.6.0/.claude/settings.local.json +0 -8
- pypaperflow-0.6.0/src/pyPaperFlow/__init__.py +0 -1
- pypaperflow-0.6.0/tests/test_arxiv_fulltext.py +0 -47
- pypaperflow-0.6.0/tests/test_biorxiv_fulltext.py +0 -101
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/.github/workflows/docs.yml +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/.gitignore +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/LICENSE +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/mkdoc_site/index.md +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/mkdocs.yml +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/pyproject.toml +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/requirements-docs.txt +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/scripts/sync_docs.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/integrations/cloak_fallback.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/integrations/cloak_pdf.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/integrations/github_export.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/integrations/mineru_parser.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/integrations/pdf_fetch.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/integrations/undetected_fallback.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/integrations/undetected_pdf.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/preprint/source_merge.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/preprint/source_models.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/pubmed/__init__.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/pubmed/pubmed_fetcher.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/pubmed/pubmed_merger.py +0 -0
- {pypaperflow-0.6.0 → pypaperflow-0.6.2}/src/pyPaperFlow/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyPaperFlow
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Automated paper fetching and analysis platform.
|
|
5
5
|
Project-URL: Homepage, https://github.com/MaybeBio/pyPaperFlow
|
|
6
6
|
Project-URL: Issues, https://github.com/MaybeBio/pyPaperFlow/issues
|
|
@@ -112,6 +112,7 @@ This tool is designed to `complement rather than replace` reference management s
|
|
|
112
112
|
|
|
113
113
|
- **Automated Retrieval from Multiple Sources**: Automatically search and retrieve paper metadata and full-text records from `PubMed/Medline, arXiv, medRxiv, chemRxiv and bioRxiv`. The repository focuses primarily on biomedical research and computational interdisciplinary fields (`Biomedicine + Computational Biology`).
|
|
114
114
|
- **Full-Text Access**: Enable automatic downloading of open-access full texts in XML/Text format from `PMC`. For preprints and other publications without accessible PMC full texts, alternative acquisition modules are integrated to fetch `original PDFs`, with `Sci-Hub` set as the fallback provider.
|
|
115
|
+
- **Preprint Full-Text Fetch (no PDF parsing)**: For preprints without an OA PDF, dedicated methods return clean section-headed text directly — `ArxivFetcher.fetch_full_text(arxiv_id)` reads ar5iv-rendered HTML (arXiv LaTeX → HTML), and `BioRxivFetcher.fetch_full_text(doi)` reads the native bioRxiv/medRxiv full-text HTML first (browser User-Agent + rate-limit handling), then falls back to Europe PMC fullTextXML for preprints already published into PMC.
|
|
115
116
|
- **Structured Storage**:
|
|
116
117
|
- **Metadata**: Preserved in well-structured detailed JSON files.
|
|
117
118
|
- **Full Text**: Stored in multiple formats including parsed JSON and Markdown for versatile downstream usage — JSON for programmatic data analysis, and Markdown optimized for LLM comprehension and processing.
|
|
@@ -360,6 +361,8 @@ Our literature database primarily covers biomedical research and computational i
|
|
|
360
361
|
>
|
|
361
362
|
> **Difference vs. exhaustive full-corpus enumeration:** a relevance search is a heuristic over the deposited metadata. The "no-omission-by-construction" alternative is to list the *whole* platform corpus (`filter=prefix…` with no `query`, cursor-paging every record — ≈ 55k ChemRxiv / 436k openRxiv) and run the boolean AND locally, with no relevance engine in the loop; recall is then exactly "all records whose metadata fully matches the query" (a `--start/--end-date` window shrinks the pull). The cost is downloading the full corpus per search, and it still inherits the source-level boundaries above (deposit lag, metadata-only, version duplication). This tool's `search()` path is relevance-based today; the exhaustive mode is not currently exposed as a flag.
|
|
362
363
|
|
|
364
|
+
**Retries & backoff (all preprint commands).** Every preprint fetcher retries failed HTTP requests with exponential backoff — the delay doubles per attempt (`1.5s → 3s → 6s → 12s → …`, capped at 30s) and honors the server's `Retry-After` header when present. The default budget is **3 retries (≈4.5s of backoff)**, tuned to fail fast for interactive use so you get a quick answer rather than a ~22s silent stall. Override it per command with the `--max-retries` flag (e.g. `biorxiv-search ... --max-retries 5`); unattended jobs (e.g. `monitor.py`) pass a larger value explicitly. For **bioRxiv / medRxiv**, when the Europe PMC full-text leg is unreachable (e.g. a transient upstream outage) the search degrades to Crossref-only metadata matching and prints a `Warning: ... degraded ...` notice to stderr — this is a fallback, not a failure, but it drops body-only-term matches, so watch for the warning on unattended runs.
|
|
365
|
+
|
|
363
366
|
We recommend that you proactively learn and master the search syntax of these databases, as our built‑in search module functions similarly to the search bar on official web portals.
|
|
364
367
|
|
|
365
368
|
For instance, here is a typical complex query example tailored for PubMed:
|
|
@@ -1450,6 +1453,50 @@ In theory, all DOI‑driven literature workflows can be standardised following t
|
|
|
1450
1453
|
|
|
1451
1454
|
> Modules dedicated to the aforementioned preprint platforms are still under development and refinement. Preprint‑related subcommands are provided for testing purposes only. For detailed test cases, refer to [Cases](./docs/Cases.md)
|
|
1452
1455
|
|
|
1456
|
+
#### Preprint full-text fetch (Python API)
|
|
1457
|
+
|
|
1458
|
+
For preprints without an open-access PDF, each fetcher exposes a `fetch_full_text()` method that returns clean section-headed text without any PDF parsing:
|
|
1459
|
+
|
|
1460
|
+
```python
|
|
1461
|
+
from pyPaperFlow.preprint.arxiv_fetcher import ArxivFetcher
|
|
1462
|
+
from pyPaperFlow.preprint.biorxiv_fetcher import BioRxivFetcher
|
|
1463
|
+
from pyPaperFlow.preprint.europepmc_fetcher import EuropePMCFullText
|
|
1464
|
+
|
|
1465
|
+
# arXiv → ar5iv rendered HTML (LaTeX → HTML)
|
|
1466
|
+
arxiv = ArxivFetcher(root_dir="./papers")
|
|
1467
|
+
text = arxiv.fetch_full_text("1706.03762") # "" on failure
|
|
1468
|
+
|
|
1469
|
+
# bioRxiv / medRxiv → native full-text HTML first, then Europe PMC fullTextXML
|
|
1470
|
+
biorxiv = BioRxivFetcher(root_dir="./papers", platform="biorxiv")
|
|
1471
|
+
text = biorxiv.fetch_full_text("10.1101/2023.06.22.546069")
|
|
1472
|
+
|
|
1473
|
+
# Any DOI-indexed preprint → Europe PMC fullTextXML directly
|
|
1474
|
+
epmc = EuropePMCFullText()
|
|
1475
|
+
xml = epmc.full_text_xml("10.1101/2023.06.22.546069")
|
|
1476
|
+
epmc.close()
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1479
|
+
All three return an empty string `""` on failure, so callers can gracefully fall back to the abstract. The returned text is section-headed (`## Section`) plain text ready for LLM input.
|
|
1480
|
+
|
|
1481
|
+
**bioRxiv / medRxiv full-text route & rate-limit handling**
|
|
1482
|
+
|
|
1483
|
+
`BioRxivFetcher.fetch_full_text(doi)` resolves full text in this order:
|
|
1484
|
+
|
|
1485
|
+
1. **Native full-text HTML** (`{landing_base}/{doi}.full-text`) — the preprint's own rendered page, parsed into `## Section` text (stops at References, skips figure/table captions). Requires a browser `User-Agent`.
|
|
1486
|
+
2. **Europe PMC `fullTextXML`** — only present once the preprint has been published into PMC; preprints still at the PPR (preprint) stage return 404 here.
|
|
1487
|
+
3. Otherwise `""` (caller falls back to the abstract).
|
|
1488
|
+
|
|
1489
|
+
Because bioRxiv/medRxiv sit behind a Cloudflare wall that returns `429` on rapid successive requests, the HTML route applies several safeguards so a batch fetch does not get silently rate-limited into `""`:
|
|
1490
|
+
|
|
1491
|
+
- **Browser User-Agent** — non-browser UAs are always 429 on `.full-text`.
|
|
1492
|
+
- **Connection reuse + `trust_env=False`** — fewer handshakes, avoids local-proxy timeouts.
|
|
1493
|
+
- **Inter-request throttle** (2 s, shared across instances) — requests are never fired back-to-back.
|
|
1494
|
+
- **Status-code branching** — `404` returns immediately (genuinely no full text); only `429/403/5xx` retry.
|
|
1495
|
+
- **Exponential backoff** (3/6/12 s, capped at 20 s, jittered) — retried up to `max_retries` times.
|
|
1496
|
+
- **Global cooldown** (30 s) — after any `429/403`, the whole batch pauses before the next request instead of hammering the wall.
|
|
1497
|
+
|
|
1498
|
+
Typical cost: ~2–4 s per successful paper, ~30–90 s when a rate limit is hit — a small fraction of any downstream LLM step, so it is not the pipeline bottleneck.
|
|
1499
|
+
|
|
1453
1500
|
### 6. Critical Reading and Knowledge Graph Analysis: Downstream End‑Use
|
|
1454
1501
|
|
|
1455
1502
|
Upon completing literature retrieval, parsing, and structured processing as outlined above, users obtain chapter‑organised Markdown files and structured JSON files, which serve as the fundamental inputs for subsequent critical reading and knowledge graph analysis.
|
|
@@ -1483,6 +1530,8 @@ You may directly run the test scripts to verify the correctness and completeness
|
|
|
1483
1530
|
|
|
1484
1531
|
> - [ ] Supplement query syntax for various literature databases and implement skill‑based support. Currently only partial MeSH‑aware syntax priors for PubMed are integrated.
|
|
1485
1532
|
> - [ ] Maintain and update the BioPython library (E‑utilities API) for PubMed parsing from this stage onward. Current version: BioPython 1.87; see [biopython Repository](https://github.com/biopython/biopython) for details.
|
|
1533
|
+
> - [x] Europe PMC can return errors inside an HTTP 200 body (e.g. `{"errCode":404,...}` or a bare `{"version":"6.9"}` with no `resultList`); `EuropePMCSearch` now raises on `errCode` / a missing `resultList` so `last_search_degraded` is set instead of silently returning an empty result set.
|
|
1534
|
+
> - [x] `SourcePaper.version` is populated for arXiv (from the `vN` suffix) and was hardcoded to `""` for bioRxiv/medRxiv/chemRxiv. Now derived from the DOI version suffix (e.g. `.../v2`) via `extract_version_from_doi`, so version dedup (# ③ above) works uniformly across sources.
|
|
1486
1535
|
|
|
1487
1536
|
</details>
|
|
1488
1537
|
|
|
@@ -1524,5 +1573,12 @@ You may directly run the test scripts to verify the correctness and completeness
|
|
|
1524
1573
|
|
|
1525
1574
|
</details>
|
|
1526
1575
|
|
|
1576
|
+
<details>
|
|
1577
|
+
<summary><b>7. Testing & CI</b></summary>
|
|
1578
|
+
|
|
1579
|
+
> - [ ] Add a pytest suite for pyPaperFlow itself. Regression coverage for the retry/backoff + degradation changes currently lives only in the monitor repo's `tests/test_backfill.py`; `test/` here holds sample output data, and CI (`docs.yml`) builds docs only.
|
|
1580
|
+
|
|
1581
|
+
</details>
|
|
1582
|
+
|
|
1527
1583
|
|
|
1528
1584
|
|
|
@@ -83,6 +83,7 @@ This tool is designed to `complement rather than replace` reference management s
|
|
|
83
83
|
|
|
84
84
|
- **Automated Retrieval from Multiple Sources**: Automatically search and retrieve paper metadata and full-text records from `PubMed/Medline, arXiv, medRxiv, chemRxiv and bioRxiv`. The repository focuses primarily on biomedical research and computational interdisciplinary fields (`Biomedicine + Computational Biology`).
|
|
85
85
|
- **Full-Text Access**: Enable automatic downloading of open-access full texts in XML/Text format from `PMC`. For preprints and other publications without accessible PMC full texts, alternative acquisition modules are integrated to fetch `original PDFs`, with `Sci-Hub` set as the fallback provider.
|
|
86
|
+
- **Preprint Full-Text Fetch (no PDF parsing)**: For preprints without an OA PDF, dedicated methods return clean section-headed text directly — `ArxivFetcher.fetch_full_text(arxiv_id)` reads ar5iv-rendered HTML (arXiv LaTeX → HTML), and `BioRxivFetcher.fetch_full_text(doi)` reads the native bioRxiv/medRxiv full-text HTML first (browser User-Agent + rate-limit handling), then falls back to Europe PMC fullTextXML for preprints already published into PMC.
|
|
86
87
|
- **Structured Storage**:
|
|
87
88
|
- **Metadata**: Preserved in well-structured detailed JSON files.
|
|
88
89
|
- **Full Text**: Stored in multiple formats including parsed JSON and Markdown for versatile downstream usage — JSON for programmatic data analysis, and Markdown optimized for LLM comprehension and processing.
|
|
@@ -331,6 +332,8 @@ Our literature database primarily covers biomedical research and computational i
|
|
|
331
332
|
>
|
|
332
333
|
> **Difference vs. exhaustive full-corpus enumeration:** a relevance search is a heuristic over the deposited metadata. The "no-omission-by-construction" alternative is to list the *whole* platform corpus (`filter=prefix…` with no `query`, cursor-paging every record — ≈ 55k ChemRxiv / 436k openRxiv) and run the boolean AND locally, with no relevance engine in the loop; recall is then exactly "all records whose metadata fully matches the query" (a `--start/--end-date` window shrinks the pull). The cost is downloading the full corpus per search, and it still inherits the source-level boundaries above (deposit lag, metadata-only, version duplication). This tool's `search()` path is relevance-based today; the exhaustive mode is not currently exposed as a flag.
|
|
333
334
|
|
|
335
|
+
**Retries & backoff (all preprint commands).** Every preprint fetcher retries failed HTTP requests with exponential backoff — the delay doubles per attempt (`1.5s → 3s → 6s → 12s → …`, capped at 30s) and honors the server's `Retry-After` header when present. The default budget is **3 retries (≈4.5s of backoff)**, tuned to fail fast for interactive use so you get a quick answer rather than a ~22s silent stall. Override it per command with the `--max-retries` flag (e.g. `biorxiv-search ... --max-retries 5`); unattended jobs (e.g. `monitor.py`) pass a larger value explicitly. For **bioRxiv / medRxiv**, when the Europe PMC full-text leg is unreachable (e.g. a transient upstream outage) the search degrades to Crossref-only metadata matching and prints a `Warning: ... degraded ...` notice to stderr — this is a fallback, not a failure, but it drops body-only-term matches, so watch for the warning on unattended runs.
|
|
336
|
+
|
|
334
337
|
We recommend that you proactively learn and master the search syntax of these databases, as our built‑in search module functions similarly to the search bar on official web portals.
|
|
335
338
|
|
|
336
339
|
For instance, here is a typical complex query example tailored for PubMed:
|
|
@@ -1421,6 +1424,50 @@ In theory, all DOI‑driven literature workflows can be standardised following t
|
|
|
1421
1424
|
|
|
1422
1425
|
> Modules dedicated to the aforementioned preprint platforms are still under development and refinement. Preprint‑related subcommands are provided for testing purposes only. For detailed test cases, refer to [Cases](./docs/Cases.md)
|
|
1423
1426
|
|
|
1427
|
+
#### Preprint full-text fetch (Python API)
|
|
1428
|
+
|
|
1429
|
+
For preprints without an open-access PDF, each fetcher exposes a `fetch_full_text()` method that returns clean section-headed text without any PDF parsing:
|
|
1430
|
+
|
|
1431
|
+
```python
|
|
1432
|
+
from pyPaperFlow.preprint.arxiv_fetcher import ArxivFetcher
|
|
1433
|
+
from pyPaperFlow.preprint.biorxiv_fetcher import BioRxivFetcher
|
|
1434
|
+
from pyPaperFlow.preprint.europepmc_fetcher import EuropePMCFullText
|
|
1435
|
+
|
|
1436
|
+
# arXiv → ar5iv rendered HTML (LaTeX → HTML)
|
|
1437
|
+
arxiv = ArxivFetcher(root_dir="./papers")
|
|
1438
|
+
text = arxiv.fetch_full_text("1706.03762") # "" on failure
|
|
1439
|
+
|
|
1440
|
+
# bioRxiv / medRxiv → native full-text HTML first, then Europe PMC fullTextXML
|
|
1441
|
+
biorxiv = BioRxivFetcher(root_dir="./papers", platform="biorxiv")
|
|
1442
|
+
text = biorxiv.fetch_full_text("10.1101/2023.06.22.546069")
|
|
1443
|
+
|
|
1444
|
+
# Any DOI-indexed preprint → Europe PMC fullTextXML directly
|
|
1445
|
+
epmc = EuropePMCFullText()
|
|
1446
|
+
xml = epmc.full_text_xml("10.1101/2023.06.22.546069")
|
|
1447
|
+
epmc.close()
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
All three return an empty string `""` on failure, so callers can gracefully fall back to the abstract. The returned text is section-headed (`## Section`) plain text ready for LLM input.
|
|
1451
|
+
|
|
1452
|
+
**bioRxiv / medRxiv full-text route & rate-limit handling**
|
|
1453
|
+
|
|
1454
|
+
`BioRxivFetcher.fetch_full_text(doi)` resolves full text in this order:
|
|
1455
|
+
|
|
1456
|
+
1. **Native full-text HTML** (`{landing_base}/{doi}.full-text`) — the preprint's own rendered page, parsed into `## Section` text (stops at References, skips figure/table captions). Requires a browser `User-Agent`.
|
|
1457
|
+
2. **Europe PMC `fullTextXML`** — only present once the preprint has been published into PMC; preprints still at the PPR (preprint) stage return 404 here.
|
|
1458
|
+
3. Otherwise `""` (caller falls back to the abstract).
|
|
1459
|
+
|
|
1460
|
+
Because bioRxiv/medRxiv sit behind a Cloudflare wall that returns `429` on rapid successive requests, the HTML route applies several safeguards so a batch fetch does not get silently rate-limited into `""`:
|
|
1461
|
+
|
|
1462
|
+
- **Browser User-Agent** — non-browser UAs are always 429 on `.full-text`.
|
|
1463
|
+
- **Connection reuse + `trust_env=False`** — fewer handshakes, avoids local-proxy timeouts.
|
|
1464
|
+
- **Inter-request throttle** (2 s, shared across instances) — requests are never fired back-to-back.
|
|
1465
|
+
- **Status-code branching** — `404` returns immediately (genuinely no full text); only `429/403/5xx` retry.
|
|
1466
|
+
- **Exponential backoff** (3/6/12 s, capped at 20 s, jittered) — retried up to `max_retries` times.
|
|
1467
|
+
- **Global cooldown** (30 s) — after any `429/403`, the whole batch pauses before the next request instead of hammering the wall.
|
|
1468
|
+
|
|
1469
|
+
Typical cost: ~2–4 s per successful paper, ~30–90 s when a rate limit is hit — a small fraction of any downstream LLM step, so it is not the pipeline bottleneck.
|
|
1470
|
+
|
|
1424
1471
|
### 6. Critical Reading and Knowledge Graph Analysis: Downstream End‑Use
|
|
1425
1472
|
|
|
1426
1473
|
Upon completing literature retrieval, parsing, and structured processing as outlined above, users obtain chapter‑organised Markdown files and structured JSON files, which serve as the fundamental inputs for subsequent critical reading and knowledge graph analysis.
|
|
@@ -1454,6 +1501,8 @@ You may directly run the test scripts to verify the correctness and completeness
|
|
|
1454
1501
|
|
|
1455
1502
|
> - [ ] Supplement query syntax for various literature databases and implement skill‑based support. Currently only partial MeSH‑aware syntax priors for PubMed are integrated.
|
|
1456
1503
|
> - [ ] Maintain and update the BioPython library (E‑utilities API) for PubMed parsing from this stage onward. Current version: BioPython 1.87; see [biopython Repository](https://github.com/biopython/biopython) for details.
|
|
1504
|
+
> - [x] Europe PMC can return errors inside an HTTP 200 body (e.g. `{"errCode":404,...}` or a bare `{"version":"6.9"}` with no `resultList`); `EuropePMCSearch` now raises on `errCode` / a missing `resultList` so `last_search_degraded` is set instead of silently returning an empty result set.
|
|
1505
|
+
> - [x] `SourcePaper.version` is populated for arXiv (from the `vN` suffix) and was hardcoded to `""` for bioRxiv/medRxiv/chemRxiv. Now derived from the DOI version suffix (e.g. `.../v2`) via `extract_version_from_doi`, so version dedup (# ③ above) works uniformly across sources.
|
|
1457
1506
|
|
|
1458
1507
|
</details>
|
|
1459
1508
|
|
|
@@ -1495,5 +1544,12 @@ You may directly run the test scripts to verify the correctness and completeness
|
|
|
1495
1544
|
|
|
1496
1545
|
</details>
|
|
1497
1546
|
|
|
1547
|
+
<details>
|
|
1548
|
+
<summary><b>7. Testing & CI</b></summary>
|
|
1549
|
+
|
|
1550
|
+
> - [ ] Add a pytest suite for pyPaperFlow itself. Regression coverage for the retry/backoff + degradation changes currently lives only in the monitor repo's `tests/test_backfill.py`; `test/` here holds sample output data, and CI (`docs.yml`) builds docs only.
|
|
1551
|
+
|
|
1552
|
+
</details>
|
|
1553
|
+
|
|
1498
1554
|
|
|
1499
1555
|
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
|
|
83
83
|
- **多来源自动检索**:自动从 `PubMed/Medline`、`arXiv`、`medRxiv`、`chemRxiv` 和 `bioRxiv` 搜索并获取论文元数据与全文记录。项目主要聚焦于生物医学与计算交叉领域(`Biomedicine + Computational Biology`)。
|
|
84
84
|
- **全文获取**:支持自动从 `PMC` 下载开放获取的 XML/Text 全文。对于预印本及其他没有 PMC 全文的文献,集成了额外的获取模块以下载 `原始 PDF`,并将 `Sci-Hub` 作为兜底来源。
|
|
85
|
+
- **预印本全文获取(免 PDF 解析)**:对于没有开放获取 PDF 的预印本,提供专用方法直接返回带章节标题的纯文本——`ArxivFetcher.fetch_full_text(arxiv_id)` 读取 ar5iv 渲染 HTML(arXiv LaTeX→HTML),`BioRxivFetcher.fetch_full_text(doi)` 优先读取 bioRxiv / medRxiv 原生全文 HTML(浏览器 User-Agent + 限流处理),再回退到 Europe PMC fullTextXML(已正式收录进 PMC 的预印本);`EuropePMCFullText.full_text_xml(doi)` 从 Europe PMC 读取 JATS 全文 XML。
|
|
85
86
|
- **结构化存储**:
|
|
86
87
|
- **元数据**:保存为结构清晰的详细 JSON 文件。
|
|
87
88
|
- **全文**:保存为多种格式,包括解析后的 JSON 和 Markdown,方便下游使用。其中 JSON 适合程序化分析,Markdown 更适合 LLM 理解与处理。
|
|
@@ -335,6 +336,8 @@ ChemRxiv 相关模块:
|
|
|
335
336
|
>
|
|
336
337
|
> 全库拉取对于轻量级的文献调研并不适用,除非你有明确的理由需要获取某一特定数据库的全部文献,而且每年每月更新的文献本身就具有一定的冗余性,所以从效率+数量上考虑,单纯相关性检索应该能够满足绝大多数科研工作者的文献调研需求(`因为真正重要的内容一定会反复出现,往往不需要担心全量遗漏`)。当然,对于全量拉取,可以参考其他开源工具如 [paperscraper](https://github.com/jannisborn/paperscraper) 等的实现。
|
|
337
338
|
|
|
339
|
+
**重试与退避(所有预印本命令)。** 每个预印本 fetcher 在 HTTP 请求失败时都会按指数退避重试——延迟逐次翻倍(`1.5s → 3s → 6s → 12s → …`,上限 30s),并在服务器返回 `Retry-After` 头时优先遵循该头。默认预算是 **3 次重试(约 4.5s 退避)**,为交互式使用而调成"快速失败",让你尽快得到结果,而不是静默卡住约 22s。可用每条命令的 `--max-retries` 参数覆盖(例如 `biorxiv-search ... --max-retries 5`);无人值守任务(如 `monitor.py`)会显式传入更大的值。对 **bioRxiv / medRxiv** 而言,当 Europe PMC 全文支路不可达时(如上游临时故障),检索会降级为 Crossref 纯元数据匹配,并向 stderr 打印 `Warning: ... degraded ...` 提示——这是降级而非失败,但会丢失仅出现在正文中的词项命中,因此无人值守运行时务必留意该警告。
|
|
340
|
+
|
|
338
341
|
建议用户提前学习并熟练掌握上述数据库的检索语法,本工具内置搜索模块的运行逻辑与数据库网页端搜索框基本一致。
|
|
339
342
|
|
|
340
343
|
> ✨ 这里我们为你提供了几个特定文献数据库构建搜索query的skill,[paper query skill](./docs/Skills.md)
|
|
@@ -1440,6 +1443,50 @@ mineru_config.yaml mineru_export_config.yaml
|
|
|
1440
1443
|
|
|
1441
1444
|
> ⚠️ `针对上述预印本平台的模块目前基本已经开发完毕,后续只对相关功能进行维护和优化`, 测试细节与pubmed合并,详情见[Cases](./docs/Cases.md)
|
|
1442
1445
|
|
|
1446
|
+
#### 预印本全文获取(Python API)
|
|
1447
|
+
|
|
1448
|
+
对于没有开放获取 PDF 的预印本,各 fetcher 提供 `fetch_full_text()` 方法,免 PDF 解析直接返回带章节标题的纯文本:
|
|
1449
|
+
|
|
1450
|
+
```python
|
|
1451
|
+
from pyPaperFlow.preprint.arxiv_fetcher import ArxivFetcher
|
|
1452
|
+
from pyPaperFlow.preprint.biorxiv_fetcher import BioRxivFetcher
|
|
1453
|
+
from pyPaperFlow.preprint.europepmc_fetcher import EuropePMCFullText
|
|
1454
|
+
|
|
1455
|
+
# arXiv → ar5iv 渲染 HTML(LaTeX → HTML)
|
|
1456
|
+
arxiv = ArxivFetcher(root_dir="./papers")
|
|
1457
|
+
text = arxiv.fetch_full_text("1706.03762") # 失败时返回 ""
|
|
1458
|
+
|
|
1459
|
+
# bioRxiv / medRxiv → 原生全文 HTML 优先,Europe PMC fullTextXML 兜底
|
|
1460
|
+
biorxiv = BioRxivFetcher(root_dir="./papers", platform="biorxiv")
|
|
1461
|
+
text = biorxiv.fetch_full_text("10.1101/2023.06.22.546069")
|
|
1462
|
+
|
|
1463
|
+
# 任意 DOI 收录预印本 → 直接取 Europe PMC fullTextXML
|
|
1464
|
+
epmc = EuropePMCFullText()
|
|
1465
|
+
xml = epmc.full_text_xml("10.1101/2023.06.22.546069")
|
|
1466
|
+
epmc.close()
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
三者失败时均返回空字符串 `""`,调用方可优雅回退到摘要。返回文本为带章节标题(`## Section`)的纯文本,可直接作为 LLM 输入。
|
|
1470
|
+
|
|
1471
|
+
**bioRxiv / medRxiv 全文获取路径与限流处理**
|
|
1472
|
+
|
|
1473
|
+
`BioRxivFetcher.fetch_full_text(doi)` 按以下顺序解析全文:
|
|
1474
|
+
|
|
1475
|
+
1. **原生全文 HTML**(`{landing_base}/{doi}.full-text`)——预印本自身的渲染页面,解析为 `## Section` 纯文本(遇 References 停止,跳过图/表标题)。需要浏览器 `User-Agent`。
|
|
1476
|
+
2. **Europe PMC `fullTextXML`**——仅在预印本已正式收录进 PMC 后才存在;仍处于 PPR(预印本)阶段的记录在此返回 404。
|
|
1477
|
+
3. 否则返回 `""`(调用方回退到摘要)。
|
|
1478
|
+
|
|
1479
|
+
由于 bioRxiv/medRxiv 位于 Cloudflare 防火墙之后,连续快速请求会触发 `429`,HTML 路径应用了多重保护,避免批量抓取被静默限流成 `""`:
|
|
1480
|
+
|
|
1481
|
+
- **浏览器 User-Agent**——非浏览器 UA 在 `.full-text` 上恒为 429。
|
|
1482
|
+
- **连接复用 + `trust_env=False`**——减少握手、避免本地代理超时。
|
|
1483
|
+
- **请求间隔节流**(2 s,跨实例共享)——请求绝不背靠背连发。
|
|
1484
|
+
- **状态码分支**——`404` 立即返回(确实无全文);仅 `429/403/5xx` 才重试。
|
|
1485
|
+
- **指数退避**(3/6/12 s,封顶 20 s,加抖动)——最多重试 `max_retries` 次。
|
|
1486
|
+
- **全局冷却**(30 s)——任一次 `429/403` 后,整批在下一次请求前暂停,而非继续撞击防火墙。
|
|
1487
|
+
|
|
1488
|
+
典型耗时:成功约 2–4 s/篇,触发限流约 30–90 s——相对任何下游 LLM 步骤都只是零头,不构成流水线瓶颈。
|
|
1489
|
+
|
|
1443
1490
|
|
|
1444
1491
|
#### 1. 命令速查 (TL;DR)
|
|
1445
1492
|
|
|
@@ -1544,6 +1591,8 @@ Europe PMC 走的是预印本全文,能补上 Crossref 只看标题摘要而漏
|
|
|
1544
1591
|
5. **ChemRxiv 检索走 Crossref,不用官方 API**:ChemRxiv 的公开 API(`chemrxiv.org/engage/chemrxiv/public-api/v1`)对非浏览器客户端(httpx/curl)返回 Cloudflare 403,而 Crossref 侧(prefix `10.26434`)是稳定、最全的元数据通道,故 `chemrxiv-*` 只查 Crossref(也不并入 Europe PMC)。⚠️ 代价见下(版本重复 / 新贴有入库延迟 / 只看标题摘要)。完整讨论见 README「注意点:为什么预印本检索走 Crossref 元数据」。
|
|
1545
1592
|
6. **ChemRxiv PDF 直连可下**:PDF 端点固定为 `https://chemrxiv.org/doi/pdf/{doi}`,本网络实测经 httpx 直连即返回 `%PDF` 字节,**不需要** CloakBrowser / undetected_chromedriver 回退(与 bioRxiv/medRxiv 的 Cloudflare 403 相反)。万一某篇直连失败,`--download-pdf` 仍会自动走浏览器回退链。
|
|
1546
1593
|
7. **版本重复(去重要手动)**:Crossref 把 ChemRxiv 每次改版都单独注册成一个 DOI work——`10.26434/chemrxiv-2025-tj4pr-v2` 与 `chemrxiv-2025-tj4pr`、`10.26434/chemrxiv.15007500/v2` 与 `/v1` 都会作为独立结果同时命中(见下方实测,3 条 DOI 实为 2 篇论文)。`chemrxiv-*` 不去重,用 `--file` 清单抓取前可自行剔除旧版 DOI。
|
|
1594
|
+
8. **重试与退避**:所有预印本命令(`arxiv-*` / `biorxiv-*` / `medrxiv-*` / `chemrxiv-*`)的 HTTP 请求失败都会按指数退避重试(延迟逐次翻倍 `1.5s→3s→6s→12s→…`,封顶 30s,并优先遵循 `Retry-After` 头)。默认 **3 次重试(约 4.5s)**,面向交互式使用快速失败;可用 `--max-retries` 覆盖(如 `biorxiv-search ... --max-retries 5`),无人值守任务(如 `monitor.py`)会显式传更大值。
|
|
1595
|
+
9. **bioRxiv/medRxiv 的 Europe PMC 降级**:当 Europe PMC 全文支路不可达(如上游临时 503)时,`biorxiv-*` / `medrxiv-*` 的搜索会自动降级为纯 Crossref 元数据匹配,并向 stderr 打印 `Warning: ... degraded ...`——这是降级而非失败,但会丢失仅出现在正文中的词项命中(如基因缩写),无人值守运行时务必留意该警告。
|
|
1547
1596
|
|
|
1548
1597
|
#### 2. 搜索并获取 arXiv 论文
|
|
1549
1598
|
如果你只想先拿到 ID,可以先搜索;如果想同时获取元数据和 PDF,可以直接 fetch。
|
|
@@ -2302,6 +2351,8 @@ paperflow pubmed-export-md -i IDR_all_20260520_2026-05-20_18-33-54.json -o ./IDR
|
|
|
2302
2351
|
|
|
2303
2352
|
> - [ ] 各文献数据库Query搜索语法的补充,尝试skill化,目前仅实现pubmed mesh部分语法先验结合
|
|
2304
2353
|
> - [ ] 从这一步开始,关于pubmed数据库解析部分,考虑BioPython库的更新与维护(E-utility的接口)。目前biopython version 1.87,详情参考[biopython仓库](https://github.com/biopython/biopython)
|
|
2354
|
+
> - [x] Europe PMC 可能在 HTTP 200 响应体内返回错误(如 `{"errCode":404,...}` 或缺少 `resultList` 的裸 `{"version":"6.9"}`);`EuropePMCSearch` 现对 `errCode` / 缺失 `resultList` 抛异常,从而设置 `last_search_degraded`,不再静默返回空结果集。
|
|
2355
|
+
> - [x] `SourcePaper.version` 对 arXiv 有值(来自 `vN` 后缀),但此前对 bioRxiv/medRxiv/chemRxiv 硬编码为 `""`。现通过 `extract_version_from_doi` 从 DOI 版本后缀(如 `.../v2`)推导,使版本去重(上文 ③)在各源间一致生效。
|
|
2305
2356
|
|
|
2306
2357
|
</details>
|
|
2307
2358
|
|
|
@@ -2350,6 +2401,14 @@ paperflow pubmed-export-md -i IDR_all_20260520_2026-05-20_18-33-54.json -o ./IDR
|
|
|
2350
2401
|
</details>
|
|
2351
2402
|
|
|
2352
2403
|
|
|
2404
|
+
<details markdown="1">
|
|
2405
|
+
<summary><b>7. 测试与 CI</b></summary>
|
|
2406
|
+
|
|
2407
|
+
> - [ ] 为 pyPaperFlow 本身补充 pytest 测试套件。目前退避/降级改动的回归覆盖只存在于 monitor 仓库的 `tests/test_backfill.py`;本仓库 `test/` 只有样例输出数据,CI(`docs.yml`)仅构建文档。
|
|
2408
|
+
|
|
2409
|
+
</details>
|
|
2410
|
+
|
|
2411
|
+
|
|
2353
2412
|
|
|
2354
2413
|
|
|
2355
2414
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.2"
|
|
@@ -8,6 +8,7 @@ from .pubmed.pubmed_fetcher import PubmedFetcher
|
|
|
8
8
|
from .preprint.arxiv_fetcher import ArxivFetcher
|
|
9
9
|
from .preprint.biorxiv_fetcher import BioRxivFetcher
|
|
10
10
|
from .preprint.chemrxiv_fetcher import ChemRxivFetcher
|
|
11
|
+
from .preprint.source_utils import DEFAULT_MAX_RETRIES
|
|
11
12
|
from .pubmed.pubmed_merger import PubmedMerger
|
|
12
13
|
from .integrations import pdf_fetch
|
|
13
14
|
from .integrations.github_export import run_github_export
|
|
@@ -20,6 +21,7 @@ opt_storage = typer.Option("./Papers", "--storage-dir", "-s", help="Directory in
|
|
|
20
21
|
opt_email = typer.Option(..., "--email", help="Entrez Email.")
|
|
21
22
|
opt_api_key = typer.Option(None, "--api-key", help="NCBI API Key (recommended).")
|
|
22
23
|
opt_max_retries = typer.Option(3, "--max-retries", help="Maximum number of retries for Entrez API calls.")
|
|
24
|
+
opt_preprint_max_retries = typer.Option(DEFAULT_MAX_RETRIES, "--max-retries", help="Maximum number of retries per request. Backoff is exponential, capped at 30s.")
|
|
23
25
|
opt_batch_size = typer.Option(50, "--batch-size", "-b", help="Batch size for fetching.")
|
|
24
26
|
opt_arxiv_backend = typer.Option("native", "--backend", help="arXiv backend: 'native' or 'paperscraper'.")
|
|
25
27
|
|
|
@@ -338,9 +340,10 @@ def arxiv_search_cmd(
|
|
|
338
340
|
start_date: Optional[str] = typer.Option(None, "--start-date", help="Optional start date in YYYY-MM-DD."),
|
|
339
341
|
end_date: Optional[str] = typer.Option(None, "--end-date", help="Optional end date in YYYY-MM-DD."),
|
|
340
342
|
backend: str = opt_arxiv_backend,
|
|
343
|
+
max_retries: int = opt_preprint_max_retries,
|
|
341
344
|
):
|
|
342
345
|
"""Search arXiv and write matching IDs to a text file."""
|
|
343
|
-
fetcher = ArxivFetcher(root_dir=storage_dir, backend=backend)
|
|
346
|
+
fetcher = ArxivFetcher(root_dir=storage_dir, backend=backend, max_retries=max_retries)
|
|
344
347
|
records = fetcher.search(query=query, max_results=max_results, start_date=start_date, end_date=end_date)
|
|
345
348
|
typer.echo(f"Found {len(records)} arXiv papers.")
|
|
346
349
|
if not records:
|
|
@@ -374,12 +377,13 @@ def arxiv_fetch_cmd(
|
|
|
374
377
|
end_date: Optional[str] = typer.Option(None, "--end-date", help="Optional end date in YYYY-MM-DD (query mode only)."),
|
|
375
378
|
download_pdf: bool = typer.Option(True, "--download-pdf/--no-download-pdf", help="Download PDFs when available."),
|
|
376
379
|
backend: str = opt_arxiv_backend,
|
|
380
|
+
max_retries: int = opt_preprint_max_retries,
|
|
377
381
|
):
|
|
378
382
|
"""Fetch arXiv metadata and attempt to download PDFs.
|
|
379
383
|
|
|
380
384
|
Provide one of: a positional query, --file, or one or more --id values.
|
|
381
385
|
"""
|
|
382
|
-
fetcher = ArxivFetcher(root_dir=storage_dir, backend=backend)
|
|
386
|
+
fetcher = ArxivFetcher(root_dir=storage_dir, backend=backend, max_retries=max_retries)
|
|
383
387
|
output = output_dir if output_dir else storage_dir
|
|
384
388
|
|
|
385
389
|
provided = [name for name, value in (("query", query), ("--file", file), ("--id", arxiv_id)) if value]
|
|
@@ -426,6 +430,7 @@ def biorxiv_search_cmd(
|
|
|
426
430
|
end_date: Optional[str] = typer.Option(None, "--end-date", help="Optional end date in YYYY-MM-DD."),
|
|
427
431
|
window_days: int = typer.Option(365, "--window-days", help="Compatibility-only option. Retained for older scripts; not used by current Crossref-backed direct query path."),
|
|
428
432
|
use_europepmc: bool = typer.Option(True, "--europepmc/--no-europepmc", help="Also search Europe PMC full text (boolean AND) and union with Crossref results. Default: enabled."),
|
|
433
|
+
max_retries: int = opt_preprint_max_retries,
|
|
429
434
|
):
|
|
430
435
|
"""Search bioRxiv and write matching IDs to a text file.
|
|
431
436
|
|
|
@@ -438,7 +443,7 @@ def biorxiv_search_cmd(
|
|
|
438
443
|
fg=typer.colors.YELLOW,
|
|
439
444
|
)
|
|
440
445
|
|
|
441
|
-
fetcher = BioRxivFetcher(root_dir=storage_dir, window_days=window_days)
|
|
446
|
+
fetcher = BioRxivFetcher(root_dir=storage_dir, window_days=window_days, max_retries=max_retries)
|
|
442
447
|
records = fetcher.search(query=query, start_date=start_date, end_date=end_date, max_results=max_results, use_europepmc=use_europepmc)
|
|
443
448
|
typer.echo(f"Found {len(records)} bioRxiv papers.")
|
|
444
449
|
for record in records:
|
|
@@ -462,6 +467,7 @@ def biorxiv_fetch_cmd(
|
|
|
462
467
|
window_days: int = typer.Option(365, "--window-days", help="Compatibility-only option. Retained for older scripts; not used by current Crossref-backed direct query path."),
|
|
463
468
|
download_pdf: bool = typer.Option(True, "--download-pdf/--no-download-pdf", help="Download PDFs when available."),
|
|
464
469
|
use_europepmc: bool = typer.Option(True, "--europepmc/--no-europepmc", help="In query mode, also search Europe PMC full text and union with Crossref results. Default: enabled."),
|
|
470
|
+
max_retries: int = opt_preprint_max_retries,
|
|
465
471
|
):
|
|
466
472
|
"""Fetch bioRxiv metadata and attempt to download PDFs.
|
|
467
473
|
|
|
@@ -475,7 +481,7 @@ def biorxiv_fetch_cmd(
|
|
|
475
481
|
fg=typer.colors.YELLOW,
|
|
476
482
|
)
|
|
477
483
|
|
|
478
|
-
fetcher = BioRxivFetcher(root_dir=storage_dir, window_days=window_days)
|
|
484
|
+
fetcher = BioRxivFetcher(root_dir=storage_dir, window_days=window_days, max_retries=max_retries)
|
|
479
485
|
output = output_dir if output_dir else storage_dir
|
|
480
486
|
|
|
481
487
|
provided = [name for name, value in (("query", query), ("--file", file), ("--doi", doi)) if value]
|
|
@@ -523,6 +529,7 @@ def medrxiv_search_cmd(
|
|
|
523
529
|
end_date: Optional[str] = typer.Option(None, "--end-date", help="Optional end date in YYYY-MM-DD."),
|
|
524
530
|
window_days: int = typer.Option(365, "--window-days", help="Compatibility-only option. Retained for older scripts; not used by current Crossref-backed direct query path."),
|
|
525
531
|
use_europepmc: bool = typer.Option(True, "--europepmc/--no-europepmc", help="Also search Europe PMC full text (boolean AND) and union with Crossref results. Default: enabled."),
|
|
532
|
+
max_retries: int = opt_preprint_max_retries,
|
|
526
533
|
):
|
|
527
534
|
"""Search medRxiv and write matching IDs to a text file.
|
|
528
535
|
|
|
@@ -535,7 +542,7 @@ def medrxiv_search_cmd(
|
|
|
535
542
|
fg=typer.colors.YELLOW,
|
|
536
543
|
)
|
|
537
544
|
|
|
538
|
-
fetcher = BioRxivFetcher(root_dir=storage_dir, platform="medrxiv", window_days=window_days)
|
|
545
|
+
fetcher = BioRxivFetcher(root_dir=storage_dir, platform="medrxiv", window_days=window_days, max_retries=max_retries)
|
|
539
546
|
records = fetcher.search(query=query, start_date=start_date, end_date=end_date, max_results=max_results, use_europepmc=use_europepmc)
|
|
540
547
|
typer.echo(f"Found {len(records)} medRxiv papers.")
|
|
541
548
|
for record in records:
|
|
@@ -559,6 +566,7 @@ def medrxiv_fetch_cmd(
|
|
|
559
566
|
window_days: int = typer.Option(365, "--window-days", help="Compatibility-only option. Retained for older scripts; not used by current Crossref-backed direct query path."),
|
|
560
567
|
download_pdf: bool = typer.Option(True, "--download-pdf/--no-download-pdf", help="Download PDFs when available."),
|
|
561
568
|
use_europepmc: bool = typer.Option(True, "--europepmc/--no-europepmc", help="In query mode, also search Europe PMC full text and union with Crossref results. Default: enabled."),
|
|
569
|
+
max_retries: int = opt_preprint_max_retries,
|
|
562
570
|
):
|
|
563
571
|
"""Fetch medRxiv metadata and attempt to download PDFs.
|
|
564
572
|
|
|
@@ -572,7 +580,7 @@ def medrxiv_fetch_cmd(
|
|
|
572
580
|
fg=typer.colors.YELLOW,
|
|
573
581
|
)
|
|
574
582
|
|
|
575
|
-
fetcher = BioRxivFetcher(root_dir=storage_dir, platform="medrxiv", window_days=window_days)
|
|
583
|
+
fetcher = BioRxivFetcher(root_dir=storage_dir, platform="medrxiv", window_days=window_days, max_retries=max_retries)
|
|
576
584
|
output = output_dir if output_dir else storage_dir
|
|
577
585
|
|
|
578
586
|
provided = [name for name, value in (("query", query), ("--file", file), ("--doi", doi)) if value]
|
|
@@ -618,13 +626,14 @@ def chemrxiv_search_cmd(
|
|
|
618
626
|
output_dir: Optional[str] = typer.Option(None, "--output-dir", "-o", help="Directory to save searched ChemRxiv IDs."),
|
|
619
627
|
start_date: Optional[str] = typer.Option(None, "--start-date", help="Optional start date in YYYY-MM-DD."),
|
|
620
628
|
end_date: Optional[str] = typer.Option(None, "--end-date", help="Optional end date in YYYY-MM-DD."),
|
|
629
|
+
max_retries: int = opt_preprint_max_retries,
|
|
621
630
|
):
|
|
622
631
|
"""Search ChemRxiv and write matching DOIs to a text file.
|
|
623
632
|
|
|
624
633
|
Metadata is retrieved from Crossref over ChemRxiv records (prefix
|
|
625
634
|
10.26434, publisher American Chemical Society (ACS)).
|
|
626
635
|
"""
|
|
627
|
-
fetcher = ChemRxivFetcher(root_dir=storage_dir)
|
|
636
|
+
fetcher = ChemRxivFetcher(root_dir=storage_dir, max_retries=max_retries)
|
|
628
637
|
records = fetcher.search(query=query, start_date=start_date, end_date=end_date, max_results=max_results)
|
|
629
638
|
typer.echo(f"Found {len(records)} ChemRxiv papers.")
|
|
630
639
|
for record in records:
|
|
@@ -646,13 +655,14 @@ def chemrxiv_fetch_cmd(
|
|
|
646
655
|
start_date: Optional[str] = typer.Option(None, "--start-date", help="Optional start date in YYYY-MM-DD (query mode only)."),
|
|
647
656
|
end_date: Optional[str] = typer.Option(None, "--end-date", help="Optional end date in YYYY-MM-DD (query mode only)."),
|
|
648
657
|
download_pdf: bool = typer.Option(True, "--download-pdf/--no-download-pdf", help="Download PDFs when available."),
|
|
658
|
+
max_retries: int = opt_preprint_max_retries,
|
|
649
659
|
):
|
|
650
660
|
"""Fetch ChemRxiv metadata and attempt to download PDFs.
|
|
651
661
|
|
|
652
662
|
Metadata retrieval uses Crossref over ChemRxiv records (prefix 10.26434).
|
|
653
663
|
Provide one of: a positional query, --file, or one or more --doi values.
|
|
654
664
|
"""
|
|
655
|
-
fetcher = ChemRxivFetcher(root_dir=storage_dir)
|
|
665
|
+
fetcher = ChemRxivFetcher(root_dir=storage_dir, max_retries=max_retries)
|
|
656
666
|
output = output_dir if output_dir else storage_dir
|
|
657
667
|
|
|
658
668
|
provided = [name for name, value in (("query", query), ("--file", file), ("--doi", doi)) if value]
|
|
@@ -5,7 +5,6 @@ import importlib
|
|
|
5
5
|
import time
|
|
6
6
|
import xml.etree.ElementTree as ET
|
|
7
7
|
from datetime import date, datetime, timezone
|
|
8
|
-
from email.utils import parsedate_to_datetime
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import Any, Dict, Iterable, List, Optional
|
|
11
10
|
|
|
@@ -14,12 +13,14 @@ from bs4 import BeautifulSoup
|
|
|
14
13
|
|
|
15
14
|
from .source_models import SourcePaper
|
|
16
15
|
from .source_utils import (
|
|
16
|
+
DEFAULT_MAX_RETRIES,
|
|
17
17
|
build_source_record_dir,
|
|
18
18
|
download_binary,
|
|
19
19
|
extract_year,
|
|
20
20
|
normalize_text,
|
|
21
21
|
safe_filename,
|
|
22
22
|
save_json,
|
|
23
|
+
sleep_before_retry,
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
|
|
@@ -73,7 +74,7 @@ class ArxivFetcher:
|
|
|
73
74
|
root_dir: str,
|
|
74
75
|
backend: str = "native",
|
|
75
76
|
batch_size: int = 100,
|
|
76
|
-
max_retries: int =
|
|
77
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
77
78
|
request_timeout: float = 60.0,
|
|
78
79
|
):
|
|
79
80
|
self.root_dir = root_dir
|
|
@@ -382,7 +383,7 @@ class ArxivFetcher:
|
|
|
382
383
|
if response.status_code == 429:
|
|
383
384
|
last_error = RuntimeError(f"arXiv API rate limited request for {description}")
|
|
384
385
|
if attempt + 1 < self.max_retries:
|
|
385
|
-
|
|
386
|
+
sleep_before_retry(response, attempt)
|
|
386
387
|
continue
|
|
387
388
|
break
|
|
388
389
|
response.raise_for_status()
|
|
@@ -391,19 +392,19 @@ class ArxivFetcher:
|
|
|
391
392
|
except ET.ParseError as exc:
|
|
392
393
|
last_error = exc
|
|
393
394
|
if attempt + 1 < self.max_retries:
|
|
394
|
-
|
|
395
|
+
sleep_before_retry(response, attempt)
|
|
395
396
|
continue
|
|
396
397
|
break
|
|
397
398
|
except (httpx.HTTPStatusError, httpx.TimeoutException, httpx.TransportError) as exc:
|
|
398
399
|
last_error = exc
|
|
399
400
|
if attempt + 1 < self.max_retries:
|
|
400
|
-
|
|
401
|
+
sleep_before_retry(response, attempt)
|
|
401
402
|
continue
|
|
402
403
|
break
|
|
403
404
|
except Exception as exc:
|
|
404
405
|
last_error = exc
|
|
405
406
|
if attempt + 1 < self.max_retries:
|
|
406
|
-
|
|
407
|
+
sleep_before_retry(response, attempt)
|
|
407
408
|
continue
|
|
408
409
|
break
|
|
409
410
|
|
|
@@ -426,32 +427,6 @@ class ArxivFetcher:
|
|
|
426
427
|
self._http_client = httpx.Client(http2=False, **client_kwargs)
|
|
427
428
|
return self._http_client
|
|
428
429
|
|
|
429
|
-
def _sleep_before_retry(self, response: Optional[httpx.Response], attempt: int) -> None:
|
|
430
|
-
retry_after = self._retry_after_seconds(response)
|
|
431
|
-
delay = retry_after if retry_after is not None else min(30.0, 1.5 * (2**attempt))
|
|
432
|
-
time.sleep(max(0.0, delay))
|
|
433
|
-
|
|
434
|
-
def _retry_after_seconds(self, response: Optional[httpx.Response]) -> Optional[float]:
|
|
435
|
-
if response is None:
|
|
436
|
-
return None
|
|
437
|
-
|
|
438
|
-
raw_retry_after = normalize_text(response.headers.get("Retry-After", ""))
|
|
439
|
-
if not raw_retry_after:
|
|
440
|
-
return None
|
|
441
|
-
|
|
442
|
-
if raw_retry_after.isdigit():
|
|
443
|
-
return float(raw_retry_after)
|
|
444
|
-
|
|
445
|
-
try:
|
|
446
|
-
retry_after_dt = parsedate_to_datetime(raw_retry_after)
|
|
447
|
-
except (TypeError, ValueError, IndexError):
|
|
448
|
-
return None
|
|
449
|
-
|
|
450
|
-
if retry_after_dt.tzinfo is None:
|
|
451
|
-
retry_after_dt = retry_after_dt.replace(tzinfo=timezone.utc)
|
|
452
|
-
now = datetime.now(retry_after_dt.tzinfo)
|
|
453
|
-
return max(0.0, (retry_after_dt - now).total_seconds())
|
|
454
|
-
|
|
455
430
|
def _normalize_date_bounds(
|
|
456
431
|
self,
|
|
457
432
|
start_date: Optional[str],
|