preprint-fulltext 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.
- preprint_fulltext-0.1.0/.claude-plugin/marketplace.json +15 -0
- preprint_fulltext-0.1.0/.claude-plugin/plugin.json +12 -0
- preprint_fulltext-0.1.0/.cursor/rules/preprint-fulltext.mdc +106 -0
- preprint_fulltext-0.1.0/.github/copilot-instructions.md +102 -0
- preprint_fulltext-0.1.0/.github/workflows/live-smoke.yml +28 -0
- preprint_fulltext-0.1.0/.github/workflows/publish-pypi.yml +26 -0
- preprint_fulltext-0.1.0/.github/workflows/publish-registry.yml +41 -0
- preprint_fulltext-0.1.0/.github/workflows/test.yml +43 -0
- preprint_fulltext-0.1.0/.gitignore +33 -0
- preprint_fulltext-0.1.0/AGENTS.md +102 -0
- preprint_fulltext-0.1.0/LICENSE +40 -0
- preprint_fulltext-0.1.0/PKG-INFO +304 -0
- preprint_fulltext-0.1.0/README.md +258 -0
- preprint_fulltext-0.1.0/llms.txt +108 -0
- preprint_fulltext-0.1.0/preprint_fulltext/__init__.py +8 -0
- preprint_fulltext-0.1.0/preprint_fulltext/cli.py +324 -0
- preprint_fulltext-0.1.0/preprint_fulltext/config.py +127 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/__init__.py +1 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/assemble.py +72 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/cache.py +49 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/chunk.py +170 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/http.py +71 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/ids.py +133 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/jats.py +240 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/licenses.py +131 -0
- preprint_fulltext-0.1.0/preprint_fulltext/core/models.py +153 -0
- preprint_fulltext-0.1.0/preprint_fulltext/mcp_server.py +270 -0
- preprint_fulltext-0.1.0/preprint_fulltext/pipeline/__init__.py +1 -0
- preprint_fulltext-0.1.0/preprint_fulltext/pipeline/export.py +84 -0
- preprint_fulltext-0.1.0/preprint_fulltext/pipeline/ingest.py +153 -0
- preprint_fulltext-0.1.0/preprint_fulltext/pipeline/resolve.py +45 -0
- preprint_fulltext-0.1.0/preprint_fulltext/pipeline/router.py +176 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/__init__.py +1 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/arxiv.py +166 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/arxiv_html.py +202 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/base.py +47 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/biorxiv_api.py +124 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/biorxiv_html.py +200 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/biorxiv_s3.py +249 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/europepmc.py +169 -0
- preprint_fulltext-0.1.0/preprint_fulltext/sources/openalex.py +170 -0
- preprint_fulltext-0.1.0/pyproject.toml +72 -0
- preprint_fulltext-0.1.0/scripts/build_agent_docs.py +81 -0
- preprint_fulltext-0.1.0/server.json +35 -0
- preprint_fulltext-0.1.0/skills/preprint-fulltext/SKILL.md +113 -0
- preprint_fulltext-0.1.0/tests/__init__.py +0 -0
- preprint_fulltext-0.1.0/tests/conftest.py +45 -0
- preprint_fulltext-0.1.0/tests/factories.py +57 -0
- preprint_fulltext-0.1.0/tests/fixtures/arxiv_api.xml +18 -0
- preprint_fulltext-0.1.0/tests/fixtures/arxiv_latexml.html +41 -0
- preprint_fulltext-0.1.0/tests/fixtures/biorxiv_details.json +18 -0
- preprint_fulltext-0.1.0/tests/fixtures/biorxiv_page.html +44 -0
- preprint_fulltext-0.1.0/tests/fixtures/epmc_resolve.json +15 -0
- preprint_fulltext-0.1.0/tests/fixtures/epmc_search_page1.json +12 -0
- preprint_fulltext-0.1.0/tests/fixtures/epmc_search_page2.json +10 -0
- preprint_fulltext-0.1.0/tests/fixtures/jats_biorxiv_arr.xml +43 -0
- preprint_fulltext-0.1.0/tests/fixtures/jats_biorxiv_sample.xml +78 -0
- preprint_fulltext-0.1.0/tests/fixtures/openalex_search.json +24 -0
- preprint_fulltext-0.1.0/tests/fixtures/openalex_work.json +7 -0
- preprint_fulltext-0.1.0/tests/test_arxiv.py +147 -0
- preprint_fulltext-0.1.0/tests/test_assemble.py +59 -0
- preprint_fulltext-0.1.0/tests/test_biorxiv_api.py +97 -0
- preprint_fulltext-0.1.0/tests/test_biorxiv_html.py +108 -0
- preprint_fulltext-0.1.0/tests/test_biorxiv_s3.py +147 -0
- preprint_fulltext-0.1.0/tests/test_chunk.py +113 -0
- preprint_fulltext-0.1.0/tests/test_cli_discover_search.py +57 -0
- preprint_fulltext-0.1.0/tests/test_cli_get.py +66 -0
- preprint_fulltext-0.1.0/tests/test_cli_ingest.py +78 -0
- preprint_fulltext-0.1.0/tests/test_config.py +81 -0
- preprint_fulltext-0.1.0/tests/test_europepmc.py +139 -0
- preprint_fulltext-0.1.0/tests/test_export.py +64 -0
- preprint_fulltext-0.1.0/tests/test_ids.py +79 -0
- preprint_fulltext-0.1.0/tests/test_ingest.py +116 -0
- preprint_fulltext-0.1.0/tests/test_jats.py +120 -0
- preprint_fulltext-0.1.0/tests/test_licenses.py +60 -0
- preprint_fulltext-0.1.0/tests/test_live.py +137 -0
- preprint_fulltext-0.1.0/tests/test_mcp.py +165 -0
- preprint_fulltext-0.1.0/tests/test_models.py +147 -0
- preprint_fulltext-0.1.0/tests/test_openalex.py +104 -0
- preprint_fulltext-0.1.0/tests/test_router.py +154 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "preprint-fulltext",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Min Dai",
|
|
5
|
+
"email": "dai@broadinstitute.org",
|
|
6
|
+
"url": "https://fishelllab.hms.harvard.edu"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "preprint-fulltext",
|
|
11
|
+
"source": ".",
|
|
12
|
+
"description": "Retrieve bioRxiv/medRxiv/arXiv preprint full text as structured sections; search/discover preprints; build embedding-ready corpora. CLI + MCP."
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "preprint-fulltext",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Retrieve full text of bioRxiv/medRxiv/arXiv preprints as structured sections to ground scientific reasoning and deep research, search/discover preprints, and build embedding-ready corpora (Europe PMC + openRxiv S3 + arXiv LaTeXML), via CLI and MCP.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Min Dai",
|
|
7
|
+
"email": "dai@broadinstitute.org",
|
|
8
|
+
"url": "https://fishelllab.hms.harvard.edu"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/genecell/preprint-fulltext",
|
|
11
|
+
"license": "BSD-3-Clause"
|
|
12
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Retrieve full text of bioRxiv/medRxiv/arXiv preprints as structured sections to ground scientific reasoning and deep research (methods/results, not just abstracts), or build embedding-ready corpora. Use when a task needs a preprint's full text or sections from a DOI/arXiv id/URL; to verify a claim against a paper's Results; to extract methods or parameters; keyword/title/author search over preprints; discovery by topic/date; or a chunked JSONL/Parquet corpus for embeddings/RAG. Triggers on: bioRxiv, medRxiv, arXiv, openRxiv, preprint full text, JATS, a 10.1101/ or 10.64898/ DOI, an arXiv id (2401.10515), "get the methods/results section", "what does this paper actually do", "search preprints".
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# preprint-fulltext
|
|
8
|
+
|
|
9
|
+
A CLI + Python library + MCP server for retrieving bioRxiv/medRxiv preprint full text
|
|
10
|
+
and turning it into structured, embedding-ready data. One canonical model and one
|
|
11
|
+
shared JATS parser back every path.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
- The user gives a **preprint DOI or bioRxiv/medRxiv URL** and wants the full text or
|
|
16
|
+
a specific section (methods, results, …).
|
|
17
|
+
- The user wants to **search** preprints by keyword, **title**, **abstract**, or
|
|
18
|
+
**author**, or **discover** them by topic/category/date.
|
|
19
|
+
- The user wants a **corpus** of preprint full text chunked for embeddings/RAG.
|
|
20
|
+
|
|
21
|
+
Do NOT use for published-journal full text in general (this is preprint-scoped), and
|
|
22
|
+
do not scrape the bioRxiv website in bulk — use `ingest --source s3` for corpora.
|
|
23
|
+
|
|
24
|
+
## Why full text (for grounded reasoning)
|
|
25
|
+
|
|
26
|
+
Prefer full text over abstracts when the task needs real evidence: methods, quantities,
|
|
27
|
+
protocols, datasets, and results live in the body. `get_fulltext` returns section-labeled
|
|
28
|
+
text (`kind` = methods / results / discussion / …) with `source`, `version`, and `license`,
|
|
29
|
+
so you can:
|
|
30
|
+
- **cite precisely** — quote/attribute the exact section of a specific paper + version;
|
|
31
|
+
- **verify claims** — check an assertion against the paper's Results, not its abstract;
|
|
32
|
+
- **extract methods/parameters** — read the Methods section directly;
|
|
33
|
+
- **stay within-license** — each Section/Chunk carries its license and a link-back source.
|
|
34
|
+
This grounds reasoning in the primary source rather than a recalled (possibly stale) summary.
|
|
35
|
+
|
|
36
|
+
## CLI
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# One preprint -> structured sections (Europe PMC -> S3 router). Accepts DOI, doi.org
|
|
40
|
+
# URL, or a bioRxiv/medRxiv content URL. --markdown for readable output; default JSON.
|
|
41
|
+
preprint-fulltext get <DOI|URL> [--markdown] [--version N] [--source europepmc|s3|html] [--html]
|
|
42
|
+
|
|
43
|
+
# Search -> one SearchHit per line (JSONL). --field scopes the query.
|
|
44
|
+
preprint-fulltext search "<query>" [--field fulltext|title|abstract|author] [--source europepmc|openalex] [-n N]
|
|
45
|
+
|
|
46
|
+
# Discover by topic/category/date -> SearchHit JSONL (OpenAlex by default).
|
|
47
|
+
preprint-fulltext discover [--query Q] [--category C] [--since YYYY-MM] [--to YYYY-MM-DD] [-n N]
|
|
48
|
+
|
|
49
|
+
# Bulk corpus -> Chunk JSONL/Parquet + <out>_manifest.jsonl (resumable, incremental).
|
|
50
|
+
preprint-fulltext ingest <out.jsonl> --source s3|europepmc [--server biorxiv|medrxiv|both]
|
|
51
|
+
[--since YYYY-MM] [--redistribution] [--format jsonl|parquet] [-n N]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Notes for agents:
|
|
55
|
+
- **arXiv:** `get` accepts an arXiv id (`2401.10515`), `arXiv:` prefix, an arxiv.org URL, or
|
|
56
|
+
the `10.48550/arXiv.*` DOI, and returns full text from arXiv's LaTeXML HTML (native →
|
|
57
|
+
ar5iv). `search --source arxiv` / `discover --source arxiv` query the arXiv API. arXiv is
|
|
58
|
+
**not** available in `ingest` (single-document only).
|
|
59
|
+
- **Versions:** a DOI resolves to the **latest version by default** (openRxiv semantics).
|
|
60
|
+
Pass `--version N` for a specific one. Europe PMC only serves the latest indexed
|
|
61
|
+
version, so an explicit older version is fetched via the HTML source (`--source html
|
|
62
|
+
--version 1`) or S3. The retrieved version is reported in `preprint.version`.
|
|
63
|
+
- `get` with no `--source` tries Europe PMC, then S3 (if AWS creds), and only tries the
|
|
64
|
+
public HTML page if you pass `--html`. Europe PMC full text exists for the **CC /
|
|
65
|
+
open-access subset**; all-rights-reserved preprints need S3 or `--html`.
|
|
66
|
+
- `--field title`/`abstract` match **all terms** (not an exact phrase); `--field author`
|
|
67
|
+
matches an author name. For topical multi-word queries prefer `--field fulltext`.
|
|
68
|
+
- `ingest --source s3` is **requester-pays** (needs AWS credentials and costs money;
|
|
69
|
+
~$0.09/GB). `ingest --source europepmc` is free but only covers the CC subset.
|
|
70
|
+
- `--redistribution` degrades non-redistributable works to metadata-only stubs.
|
|
71
|
+
|
|
72
|
+
## Python
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from preprint_fulltext.pipeline.router import Router
|
|
76
|
+
from preprint_fulltext.core.chunk import chunk_fulltext
|
|
77
|
+
|
|
78
|
+
result = Router().get_fulltext("10.1101/2024.01.15.575000") # -> GetResult
|
|
79
|
+
ft = result.fulltext # FullText | None (result.reason if None)
|
|
80
|
+
chunks = chunk_fulltext(ft) # list[Chunk], embedding-ready
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## MCP tools
|
|
84
|
+
|
|
85
|
+
Run `preprint-fulltext-mcp` (stdio). Tools:
|
|
86
|
+
- `search_preprints(query, source?, limit?)` → list of hits
|
|
87
|
+
- `get_fulltext(doi, as_markdown?)` → structured sections (or Markdown)
|
|
88
|
+
- `get_metadata(doi)` → bibliographic metadata (abstract only)
|
|
89
|
+
- `resolve(doi_or_openalex_id)` → cross-identifier resolution
|
|
90
|
+
|
|
91
|
+
Bulk `ingest` is intentionally not an MCP tool.
|
|
92
|
+
|
|
93
|
+
## Data model (what you get back)
|
|
94
|
+
|
|
95
|
+
- `FullText`: `preprint` + ordered `sections` (`kind` ∈ abstract|intro|methods|results|
|
|
96
|
+
discussion|other) + `retrieved_from`.
|
|
97
|
+
- `Chunk` (the corpus record): `doi, version, chunk_id, section_kind, text,
|
|
98
|
+
token_count, char_start, char_end, license, source`. One JSONL line per chunk.
|
|
99
|
+
- `SearchHit`: `title, doi?, openalex_id?, score?, source, snippet?, oa_url?, has_fulltext`.
|
|
100
|
+
|
|
101
|
+
## Gotchas
|
|
102
|
+
|
|
103
|
+
- openRxiv DOIs use prefix **`10.64898`** since 2025-12 (legacy **`10.1101`** before).
|
|
104
|
+
- OpenAlex requires an API key (`OPENALEX_API_KEY`) since 2026-02-13; missing → HTTP 409.
|
|
105
|
+
- Set `CONTACT_EMAIL` for the Europe PMC / OpenAlex polite pools.
|
|
106
|
+
- Retrieved content keeps its own license; corpora are for your own TDM, not re-hosting.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<!-- GENERATED from skills/preprint-fulltext/SKILL.md by scripts/build_agent_docs.py — do not edit. -->
|
|
2
|
+
|
|
3
|
+
# preprint-fulltext
|
|
4
|
+
|
|
5
|
+
A CLI + Python library + MCP server for retrieving bioRxiv/medRxiv preprint full text
|
|
6
|
+
and turning it into structured, embedding-ready data. One canonical model and one
|
|
7
|
+
shared JATS parser back every path.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- The user gives a **preprint DOI or bioRxiv/medRxiv URL** and wants the full text or
|
|
12
|
+
a specific section (methods, results, …).
|
|
13
|
+
- The user wants to **search** preprints by keyword, **title**, **abstract**, or
|
|
14
|
+
**author**, or **discover** them by topic/category/date.
|
|
15
|
+
- The user wants a **corpus** of preprint full text chunked for embeddings/RAG.
|
|
16
|
+
|
|
17
|
+
Do NOT use for published-journal full text in general (this is preprint-scoped), and
|
|
18
|
+
do not scrape the bioRxiv website in bulk — use `ingest --source s3` for corpora.
|
|
19
|
+
|
|
20
|
+
## Why full text (for grounded reasoning)
|
|
21
|
+
|
|
22
|
+
Prefer full text over abstracts when the task needs real evidence: methods, quantities,
|
|
23
|
+
protocols, datasets, and results live in the body. `get_fulltext` returns section-labeled
|
|
24
|
+
text (`kind` = methods / results / discussion / …) with `source`, `version`, and `license`,
|
|
25
|
+
so you can:
|
|
26
|
+
- **cite precisely** — quote/attribute the exact section of a specific paper + version;
|
|
27
|
+
- **verify claims** — check an assertion against the paper's Results, not its abstract;
|
|
28
|
+
- **extract methods/parameters** — read the Methods section directly;
|
|
29
|
+
- **stay within-license** — each Section/Chunk carries its license and a link-back source.
|
|
30
|
+
This grounds reasoning in the primary source rather than a recalled (possibly stale) summary.
|
|
31
|
+
|
|
32
|
+
## CLI
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# One preprint -> structured sections (Europe PMC -> S3 router). Accepts DOI, doi.org
|
|
36
|
+
# URL, or a bioRxiv/medRxiv content URL. --markdown for readable output; default JSON.
|
|
37
|
+
preprint-fulltext get <DOI|URL> [--markdown] [--version N] [--source europepmc|s3|html] [--html]
|
|
38
|
+
|
|
39
|
+
# Search -> one SearchHit per line (JSONL). --field scopes the query.
|
|
40
|
+
preprint-fulltext search "<query>" [--field fulltext|title|abstract|author] [--source europepmc|openalex] [-n N]
|
|
41
|
+
|
|
42
|
+
# Discover by topic/category/date -> SearchHit JSONL (OpenAlex by default).
|
|
43
|
+
preprint-fulltext discover [--query Q] [--category C] [--since YYYY-MM] [--to YYYY-MM-DD] [-n N]
|
|
44
|
+
|
|
45
|
+
# Bulk corpus -> Chunk JSONL/Parquet + <out>_manifest.jsonl (resumable, incremental).
|
|
46
|
+
preprint-fulltext ingest <out.jsonl> --source s3|europepmc [--server biorxiv|medrxiv|both]
|
|
47
|
+
[--since YYYY-MM] [--redistribution] [--format jsonl|parquet] [-n N]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Notes for agents:
|
|
51
|
+
- **arXiv:** `get` accepts an arXiv id (`2401.10515`), `arXiv:` prefix, an arxiv.org URL, or
|
|
52
|
+
the `10.48550/arXiv.*` DOI, and returns full text from arXiv's LaTeXML HTML (native →
|
|
53
|
+
ar5iv). `search --source arxiv` / `discover --source arxiv` query the arXiv API. arXiv is
|
|
54
|
+
**not** available in `ingest` (single-document only).
|
|
55
|
+
- **Versions:** a DOI resolves to the **latest version by default** (openRxiv semantics).
|
|
56
|
+
Pass `--version N` for a specific one. Europe PMC only serves the latest indexed
|
|
57
|
+
version, so an explicit older version is fetched via the HTML source (`--source html
|
|
58
|
+
--version 1`) or S3. The retrieved version is reported in `preprint.version`.
|
|
59
|
+
- `get` with no `--source` tries Europe PMC, then S3 (if AWS creds), and only tries the
|
|
60
|
+
public HTML page if you pass `--html`. Europe PMC full text exists for the **CC /
|
|
61
|
+
open-access subset**; all-rights-reserved preprints need S3 or `--html`.
|
|
62
|
+
- `--field title`/`abstract` match **all terms** (not an exact phrase); `--field author`
|
|
63
|
+
matches an author name. For topical multi-word queries prefer `--field fulltext`.
|
|
64
|
+
- `ingest --source s3` is **requester-pays** (needs AWS credentials and costs money;
|
|
65
|
+
~$0.09/GB). `ingest --source europepmc` is free but only covers the CC subset.
|
|
66
|
+
- `--redistribution` degrades non-redistributable works to metadata-only stubs.
|
|
67
|
+
|
|
68
|
+
## Python
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from preprint_fulltext.pipeline.router import Router
|
|
72
|
+
from preprint_fulltext.core.chunk import chunk_fulltext
|
|
73
|
+
|
|
74
|
+
result = Router().get_fulltext("10.1101/2024.01.15.575000") # -> GetResult
|
|
75
|
+
ft = result.fulltext # FullText | None (result.reason if None)
|
|
76
|
+
chunks = chunk_fulltext(ft) # list[Chunk], embedding-ready
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## MCP tools
|
|
80
|
+
|
|
81
|
+
Run `preprint-fulltext-mcp` (stdio). Tools:
|
|
82
|
+
- `search_preprints(query, source?, limit?)` → list of hits
|
|
83
|
+
- `get_fulltext(doi, as_markdown?)` → structured sections (or Markdown)
|
|
84
|
+
- `get_metadata(doi)` → bibliographic metadata (abstract only)
|
|
85
|
+
- `resolve(doi_or_openalex_id)` → cross-identifier resolution
|
|
86
|
+
|
|
87
|
+
Bulk `ingest` is intentionally not an MCP tool.
|
|
88
|
+
|
|
89
|
+
## Data model (what you get back)
|
|
90
|
+
|
|
91
|
+
- `FullText`: `preprint` + ordered `sections` (`kind` ∈ abstract|intro|methods|results|
|
|
92
|
+
discussion|other) + `retrieved_from`.
|
|
93
|
+
- `Chunk` (the corpus record): `doi, version, chunk_id, section_kind, text,
|
|
94
|
+
token_count, char_start, char_end, license, source`. One JSONL line per chunk.
|
|
95
|
+
- `SearchHit`: `title, doi?, openalex_id?, score?, source, snippet?, oa_url?, has_fulltext`.
|
|
96
|
+
|
|
97
|
+
## Gotchas
|
|
98
|
+
|
|
99
|
+
- openRxiv DOIs use prefix **`10.64898`** since 2025-12 (legacy **`10.1101`** before).
|
|
100
|
+
- OpenAlex requires an API key (`OPENALEX_API_KEY`) since 2026-02-13; missing → HTTP 409.
|
|
101
|
+
- Set `CONTACT_EMAIL` for the Europe PMC / OpenAlex polite pools.
|
|
102
|
+
- Retrieved content keeps its own license; corpora are for your own TDM, not re-hosting.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: live-smoke
|
|
2
|
+
# Opt-in LIVE smoke against the real public APIs (Europe PMC, arXiv, bioRxiv/medRxiv JSON API).
|
|
3
|
+
# Kept OUT of the normal `test` job so CI stays offline/free by default. Run it manually
|
|
4
|
+
# before a release, or on the weekly schedule, to catch upstream API drift (e.g. an endpoint
|
|
5
|
+
# change or a new DOI prefix). Uses the CONTACT_EMAIL repo variable for the polite pools.
|
|
6
|
+
#
|
|
7
|
+
# NOTE: requester-pays S3 live tests are NOT run here (cost); they stay behind
|
|
8
|
+
# PREPRINT_FULLTEXT_LIVE_S3 and are exercised locally with credentials only.
|
|
9
|
+
on:
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
schedule:
|
|
12
|
+
- cron: "0 6 * * 1" # Mondays 06:00 UTC
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
live:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with: { python-version: "3.11" }
|
|
21
|
+
- name: Install
|
|
22
|
+
run: python -m pip install -e ".[dev]"
|
|
23
|
+
- name: Live smoke (Europe PMC + arXiv + bioRxiv/medRxiv metadata; OpenAlex if key set)
|
|
24
|
+
env:
|
|
25
|
+
PREPRINT_FULLTEXT_LIVE: "1"
|
|
26
|
+
CONTACT_EMAIL: ${{ vars.CONTACT_EMAIL }}
|
|
27
|
+
OPENALEX_API_KEY: ${{ secrets.OPENALEX_API_KEY }} # optional; OpenAlex test skips if unset
|
|
28
|
+
run: pytest -m live -v
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: publish-pypi
|
|
2
|
+
# Publish preprint-fulltext to PyPI on a GitHub Release, via PyPI Trusted Publishing
|
|
3
|
+
# (OIDC — no API token needed). ONE-TIME SETUP: on pypi.org add a Trusted Publisher for
|
|
4
|
+
# project "preprint-fulltext" pointing at genecell/preprint-fulltext + this workflow, with
|
|
5
|
+
# the environment field left blank. (A GitHub Environment is optional — add one on both
|
|
6
|
+
# sides only if you want a manual-approval gate before publishing.)
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write # OIDC identity for Trusted Publishing
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with: { python-version: "3.11" }
|
|
21
|
+
- name: Build sdist + wheel
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install build
|
|
24
|
+
python -m build
|
|
25
|
+
- name: Publish to PyPI
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: publish-registry
|
|
2
|
+
# Register preprint-fulltext in the official MCP Registry (registry.modelcontextprotocol.io)
|
|
3
|
+
# via GitHub OIDC — no tokens. The io.github.genecell/* namespace is authorized by OIDC from
|
|
4
|
+
# this repo (owner genecell). The registry verifies ownership by fetching the PyPI package's
|
|
5
|
+
# `mcp-name:` marker, so run this ONLY AFTER the new version is live on PyPI.
|
|
6
|
+
#
|
|
7
|
+
# HOW TO RELEASE: cut a GitHub Release (publish-pypi.yml uploads to PyPI) → confirm the version
|
|
8
|
+
# is live on pypi.org → then Actions tab → publish-registry → Run workflow.
|
|
9
|
+
on:
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish-registry:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write # OIDC identity for the io.github.genecell namespace
|
|
17
|
+
contents: read
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- name: Sync server.json version to pyproject.toml
|
|
21
|
+
run: |
|
|
22
|
+
python - <<'PY'
|
|
23
|
+
import re, json, pathlib
|
|
24
|
+
ver = re.search(r'^version\s*=\s*"([^"]+)"', pathlib.Path("pyproject.toml").read_text(), re.M).group(1)
|
|
25
|
+
p = pathlib.Path("server.json"); d = json.loads(p.read_text())
|
|
26
|
+
d["version"] = ver
|
|
27
|
+
for pkg in d.get("packages", []):
|
|
28
|
+
pkg["version"] = ver
|
|
29
|
+
p.write_text(json.dumps(d, indent=2) + "\n")
|
|
30
|
+
print("server.json version ->", ver)
|
|
31
|
+
PY
|
|
32
|
+
- name: Install mcp-publisher (latest linux/amd64)
|
|
33
|
+
run: |
|
|
34
|
+
url=$(curl -s https://api.github.com/repos/modelcontextprotocol/registry/releases/latest \
|
|
35
|
+
| grep -o 'https://[^"]*mcp-publisher[^"]*linux_amd64.tar.gz' | head -1)
|
|
36
|
+
echo "Downloading $url"
|
|
37
|
+
curl -L "$url" | tar xz mcp-publisher
|
|
38
|
+
- name: Publish to the MCP Registry
|
|
39
|
+
run: |
|
|
40
|
+
./mcp-publisher login github-oidc
|
|
41
|
+
./mcp-publisher publish
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
# Offline test suite (respx mocks HTTP, moto mocks S3) + lint. No live network,
|
|
3
|
+
# no AWS, no cost — matches the project's "green offline by default" guarantee.
|
|
4
|
+
on:
|
|
5
|
+
push: { branches: [master, main] }
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
cache: pip
|
|
21
|
+
- name: Install (dev extras)
|
|
22
|
+
run: python -m pip install -e ".[dev]"
|
|
23
|
+
- name: Lint
|
|
24
|
+
run: ruff check preprint_fulltext/
|
|
25
|
+
- name: Test (offline)
|
|
26
|
+
run: pytest -q
|
|
27
|
+
- name: Build sdist + wheel
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install build
|
|
30
|
+
python -m build
|
|
31
|
+
|
|
32
|
+
docs-sync:
|
|
33
|
+
# Fail if the generated agent docs are stale vs skills/preprint-fulltext/SKILL.md.
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: actions/setup-python@v5
|
|
38
|
+
with: { python-version: "3.11" }
|
|
39
|
+
- name: Regenerate agent docs and check for drift
|
|
40
|
+
run: |
|
|
41
|
+
python scripts/build_agent_docs.py
|
|
42
|
+
git diff --exit-code -- AGENTS.md llms.txt .cursor .github/copilot-instructions.md \
|
|
43
|
+
|| (echo "::error::agent docs are stale — run: python scripts/build_agent_docs.py" && exit 1)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Tooling
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# Project
|
|
19
|
+
.cache/
|
|
20
|
+
*.jsonl
|
|
21
|
+
*.parquet
|
|
22
|
+
_manifest.jsonl
|
|
23
|
+
preprint-fulltext.toml
|
|
24
|
+
|
|
25
|
+
# stato — version the captured modules, but not transient history
|
|
26
|
+
.stato/.history/
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
|
|
31
|
+
# internal dev context (lives at the project root, not in the public package)
|
|
32
|
+
.stato/
|
|
33
|
+
CLAUDE.md
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<!-- GENERATED from skills/preprint-fulltext/SKILL.md by scripts/build_agent_docs.py — do not edit. -->
|
|
2
|
+
|
|
3
|
+
# preprint-fulltext
|
|
4
|
+
|
|
5
|
+
A CLI + Python library + MCP server for retrieving bioRxiv/medRxiv preprint full text
|
|
6
|
+
and turning it into structured, embedding-ready data. One canonical model and one
|
|
7
|
+
shared JATS parser back every path.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- The user gives a **preprint DOI or bioRxiv/medRxiv URL** and wants the full text or
|
|
12
|
+
a specific section (methods, results, …).
|
|
13
|
+
- The user wants to **search** preprints by keyword, **title**, **abstract**, or
|
|
14
|
+
**author**, or **discover** them by topic/category/date.
|
|
15
|
+
- The user wants a **corpus** of preprint full text chunked for embeddings/RAG.
|
|
16
|
+
|
|
17
|
+
Do NOT use for published-journal full text in general (this is preprint-scoped), and
|
|
18
|
+
do not scrape the bioRxiv website in bulk — use `ingest --source s3` for corpora.
|
|
19
|
+
|
|
20
|
+
## Why full text (for grounded reasoning)
|
|
21
|
+
|
|
22
|
+
Prefer full text over abstracts when the task needs real evidence: methods, quantities,
|
|
23
|
+
protocols, datasets, and results live in the body. `get_fulltext` returns section-labeled
|
|
24
|
+
text (`kind` = methods / results / discussion / …) with `source`, `version`, and `license`,
|
|
25
|
+
so you can:
|
|
26
|
+
- **cite precisely** — quote/attribute the exact section of a specific paper + version;
|
|
27
|
+
- **verify claims** — check an assertion against the paper's Results, not its abstract;
|
|
28
|
+
- **extract methods/parameters** — read the Methods section directly;
|
|
29
|
+
- **stay within-license** — each Section/Chunk carries its license and a link-back source.
|
|
30
|
+
This grounds reasoning in the primary source rather than a recalled (possibly stale) summary.
|
|
31
|
+
|
|
32
|
+
## CLI
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# One preprint -> structured sections (Europe PMC -> S3 router). Accepts DOI, doi.org
|
|
36
|
+
# URL, or a bioRxiv/medRxiv content URL. --markdown for readable output; default JSON.
|
|
37
|
+
preprint-fulltext get <DOI|URL> [--markdown] [--version N] [--source europepmc|s3|html] [--html]
|
|
38
|
+
|
|
39
|
+
# Search -> one SearchHit per line (JSONL). --field scopes the query.
|
|
40
|
+
preprint-fulltext search "<query>" [--field fulltext|title|abstract|author] [--source europepmc|openalex] [-n N]
|
|
41
|
+
|
|
42
|
+
# Discover by topic/category/date -> SearchHit JSONL (OpenAlex by default).
|
|
43
|
+
preprint-fulltext discover [--query Q] [--category C] [--since YYYY-MM] [--to YYYY-MM-DD] [-n N]
|
|
44
|
+
|
|
45
|
+
# Bulk corpus -> Chunk JSONL/Parquet + <out>_manifest.jsonl (resumable, incremental).
|
|
46
|
+
preprint-fulltext ingest <out.jsonl> --source s3|europepmc [--server biorxiv|medrxiv|both]
|
|
47
|
+
[--since YYYY-MM] [--redistribution] [--format jsonl|parquet] [-n N]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Notes for agents:
|
|
51
|
+
- **arXiv:** `get` accepts an arXiv id (`2401.10515`), `arXiv:` prefix, an arxiv.org URL, or
|
|
52
|
+
the `10.48550/arXiv.*` DOI, and returns full text from arXiv's LaTeXML HTML (native →
|
|
53
|
+
ar5iv). `search --source arxiv` / `discover --source arxiv` query the arXiv API. arXiv is
|
|
54
|
+
**not** available in `ingest` (single-document only).
|
|
55
|
+
- **Versions:** a DOI resolves to the **latest version by default** (openRxiv semantics).
|
|
56
|
+
Pass `--version N` for a specific one. Europe PMC only serves the latest indexed
|
|
57
|
+
version, so an explicit older version is fetched via the HTML source (`--source html
|
|
58
|
+
--version 1`) or S3. The retrieved version is reported in `preprint.version`.
|
|
59
|
+
- `get` with no `--source` tries Europe PMC, then S3 (if AWS creds), and only tries the
|
|
60
|
+
public HTML page if you pass `--html`. Europe PMC full text exists for the **CC /
|
|
61
|
+
open-access subset**; all-rights-reserved preprints need S3 or `--html`.
|
|
62
|
+
- `--field title`/`abstract` match **all terms** (not an exact phrase); `--field author`
|
|
63
|
+
matches an author name. For topical multi-word queries prefer `--field fulltext`.
|
|
64
|
+
- `ingest --source s3` is **requester-pays** (needs AWS credentials and costs money;
|
|
65
|
+
~$0.09/GB). `ingest --source europepmc` is free but only covers the CC subset.
|
|
66
|
+
- `--redistribution` degrades non-redistributable works to metadata-only stubs.
|
|
67
|
+
|
|
68
|
+
## Python
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from preprint_fulltext.pipeline.router import Router
|
|
72
|
+
from preprint_fulltext.core.chunk import chunk_fulltext
|
|
73
|
+
|
|
74
|
+
result = Router().get_fulltext("10.1101/2024.01.15.575000") # -> GetResult
|
|
75
|
+
ft = result.fulltext # FullText | None (result.reason if None)
|
|
76
|
+
chunks = chunk_fulltext(ft) # list[Chunk], embedding-ready
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## MCP tools
|
|
80
|
+
|
|
81
|
+
Run `preprint-fulltext-mcp` (stdio). Tools:
|
|
82
|
+
- `search_preprints(query, source?, limit?)` → list of hits
|
|
83
|
+
- `get_fulltext(doi, as_markdown?)` → structured sections (or Markdown)
|
|
84
|
+
- `get_metadata(doi)` → bibliographic metadata (abstract only)
|
|
85
|
+
- `resolve(doi_or_openalex_id)` → cross-identifier resolution
|
|
86
|
+
|
|
87
|
+
Bulk `ingest` is intentionally not an MCP tool.
|
|
88
|
+
|
|
89
|
+
## Data model (what you get back)
|
|
90
|
+
|
|
91
|
+
- `FullText`: `preprint` + ordered `sections` (`kind` ∈ abstract|intro|methods|results|
|
|
92
|
+
discussion|other) + `retrieved_from`.
|
|
93
|
+
- `Chunk` (the corpus record): `doi, version, chunk_id, section_kind, text,
|
|
94
|
+
token_count, char_start, char_end, license, source`. One JSONL line per chunk.
|
|
95
|
+
- `SearchHit`: `title, doi?, openalex_id?, score?, source, snippet?, oa_url?, has_fulltext`.
|
|
96
|
+
|
|
97
|
+
## Gotchas
|
|
98
|
+
|
|
99
|
+
- openRxiv DOIs use prefix **`10.64898`** since 2025-12 (legacy **`10.1101`** before).
|
|
100
|
+
- OpenAlex requires an API key (`OPENALEX_API_KEY`) since 2026-02-13; missing → HTTP 409.
|
|
101
|
+
- Set `CONTACT_EMAIL` for the Europe PMC / OpenAlex polite pools.
|
|
102
|
+
- Retrieved content keeps its own license; corpora are for your own TDM, not re-hosting.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Min Dai and the preprint-fulltext maintainers
|
|
4
|
+
(Gord Fishell Lab, Harvard Medical School / Broad Institute)
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
NOTE ON RETRIEVED CONTENT: This BSD-3-Clause license covers the preprint-fulltext
|
|
35
|
+
software only. It does NOT relicense any preprint full text the software
|
|
36
|
+
retrieves. Preprint content remains under its author-selected license (e.g.
|
|
37
|
+
CC-BY, CC-BY-NC, or all-rights-reserved). Corpora built with this tool are for
|
|
38
|
+
the operator's own text/data mining under the openRxiv TDM terms; do not
|
|
39
|
+
re-host or redistribute openRxiv full text, and link back to the openRxiv-hosted
|
|
40
|
+
text in any tool built on it. See the openRxiv TDM terms for details.
|