sciwrite-lint 0.2.0__py3-none-any.whl
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.
- sciwrite_lint/__init__.py +3 -0
- sciwrite_lint/__main__.py +527 -0
- sciwrite_lint/_network.py +195 -0
- sciwrite_lint/api.py +1484 -0
- sciwrite_lint/checks/__init__.py +1 -0
- sciwrite_lint/checks/_section_utils.py +111 -0
- sciwrite_lint/checks/cite_purpose.py +122 -0
- sciwrite_lint/checks/claim_support.py +96 -0
- sciwrite_lint/checks/cross_section_consistency.py +185 -0
- sciwrite_lint/checks/dangling_cite.py +93 -0
- sciwrite_lint/checks/dangling_ref.py +116 -0
- sciwrite_lint/checks/full_paper_consistency.py +604 -0
- sciwrite_lint/checks/ref_internal_checks.py +919 -0
- sciwrite_lint/checks/reference_accuracy.py +277 -0
- sciwrite_lint/checks/reference_exists.py +119 -0
- sciwrite_lint/checks/reference_unreliable.py +244 -0
- sciwrite_lint/checks/registry.py +136 -0
- sciwrite_lint/checks/retracted_cite.py +96 -0
- sciwrite_lint/checks/structure_promises.py +115 -0
- sciwrite_lint/checks/unreferenced_figure.py +70 -0
- sciwrite_lint/claims.py +330 -0
- sciwrite_lint/claude_backend.py +94 -0
- sciwrite_lint/claude_cli.py +405 -0
- sciwrite_lint/cli/__init__.py +1 -0
- sciwrite_lint/cli/check.py +480 -0
- sciwrite_lint/cli/config.py +229 -0
- sciwrite_lint/cli/fetch.py +250 -0
- sciwrite_lint/cli/misc.py +1202 -0
- sciwrite_lint/cli/rank.py +470 -0
- sciwrite_lint/cli/verify.py +437 -0
- sciwrite_lint/config.py +646 -0
- sciwrite_lint/cross_paper.py +174 -0
- sciwrite_lint/eval_claims.py +1196 -0
- sciwrite_lint/fulltext.py +851 -0
- sciwrite_lint/llm_utils.py +386 -0
- sciwrite_lint/local_pdfs.py +122 -0
- sciwrite_lint/manuscript_store.py +674 -0
- sciwrite_lint/models.py +139 -0
- sciwrite_lint/pdf/__init__.py +1 -0
- sciwrite_lint/pdf/grobid.py +785 -0
- sciwrite_lint/pdf/pdf_download.py +258 -0
- sciwrite_lint/pipeline.py +2694 -0
- sciwrite_lint/prompt_safety.py +30 -0
- sciwrite_lint/rate_limiter.py +227 -0
- sciwrite_lint/references/__init__.py +1 -0
- sciwrite_lint/references/citations.py +715 -0
- sciwrite_lint/references/crossref.py +282 -0
- sciwrite_lint/references/embedding_store.py +380 -0
- sciwrite_lint/references/matching.py +273 -0
- sciwrite_lint/references/metadata.py +273 -0
- sciwrite_lint/references/reference_store.py +823 -0
- sciwrite_lint/references/retraction_watch.py +178 -0
- sciwrite_lint/references/workspace_db.py +1390 -0
- sciwrite_lint/report.py +163 -0
- sciwrite_lint/schemas.py +260 -0
- sciwrite_lint/scoring/__init__.py +1 -0
- sciwrite_lint/scoring/chain.py +716 -0
- sciwrite_lint/scoring/contribution.py +322 -0
- sciwrite_lint/scoring/scilint_score.py +611 -0
- sciwrite_lint/tex_parser.py +248 -0
- sciwrite_lint/usage.py +594 -0
- sciwrite_lint/vision/__init__.py +1 -0
- sciwrite_lint/vision/cache.py +140 -0
- sciwrite_lint/vision/describe.py +311 -0
- sciwrite_lint/vision/image_extraction.py +491 -0
- sciwrite_lint/vision/pipeline.py +207 -0
- sciwrite_lint/vllm/__init__.py +1 -0
- sciwrite_lint/vllm/metrics.py +157 -0
- sciwrite_lint/vllm/vllm_server.py +445 -0
- sciwrite_lint/web.py +369 -0
- sciwrite_lint-0.2.0.dist-info/METADATA +268 -0
- sciwrite_lint-0.2.0.dist-info/RECORD +76 -0
- sciwrite_lint-0.2.0.dist-info/WHEEL +5 -0
- sciwrite_lint-0.2.0.dist-info/entry_points.txt +2 -0
- sciwrite_lint-0.2.0.dist-info/licenses/LICENSE +21 -0
- sciwrite_lint-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""CLI handlers for 'fetch' command and fetch helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from sciwrite_lint.config import LintConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def eager_parse(key: str, pdf_path: Path, refs_dir: Path) -> None:
|
|
15
|
+
"""Parse a newly-downloaded PDF and compute embeddings (best-effort)."""
|
|
16
|
+
try:
|
|
17
|
+
from sciwrite_lint.references.reference_store import parse_and_embed
|
|
18
|
+
|
|
19
|
+
text, chunks = asyncio.run(parse_and_embed(key, pdf_path, refs_dir))
|
|
20
|
+
if text:
|
|
21
|
+
suffix = f", {chunks} chunks embedded" if chunks else ""
|
|
22
|
+
logger.info(f"Parsed {key}: {len(text)} chars{suffix}")
|
|
23
|
+
else:
|
|
24
|
+
logger.warning(f"Parse failed for {key} (GROBID running?)")
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.warning(f"Parse skipped for {key}: {e}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fetch_for_citations(
|
|
30
|
+
citations: list,
|
|
31
|
+
config: LintConfig,
|
|
32
|
+
references_dir: Path,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Attempt full-text acquisition for citations missing local files."""
|
|
35
|
+
from sciwrite_lint.fulltext import acquire_fulltext
|
|
36
|
+
from sciwrite_lint.references.metadata import (
|
|
37
|
+
compute_tier,
|
|
38
|
+
load_metadata,
|
|
39
|
+
save_metadata,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
refs_dir = references_dir
|
|
43
|
+
|
|
44
|
+
# Check local_pdfs_dir for user-provided PDFs before downloading
|
|
45
|
+
from sciwrite_lint.local_pdfs import copy_local_pdf, match_local_pdfs
|
|
46
|
+
|
|
47
|
+
local_pdfs_dir = config.local_pdfs_dir
|
|
48
|
+
if local_pdfs_dir.is_dir() and any(local_pdfs_dir.glob("*.pdf")):
|
|
49
|
+
titles = {}
|
|
50
|
+
for c in citations:
|
|
51
|
+
if c.local_status != "none":
|
|
52
|
+
continue
|
|
53
|
+
meta = load_metadata(c.key, refs_dir)
|
|
54
|
+
if not meta or meta.access.get("tier") == "T1":
|
|
55
|
+
continue
|
|
56
|
+
titles[c.key] = c.title or meta.canonical.get("title", "")
|
|
57
|
+
|
|
58
|
+
if titles:
|
|
59
|
+
matched, _ = match_local_pdfs(local_pdfs_dir, titles)
|
|
60
|
+
for key, pdf_path in matched.items():
|
|
61
|
+
meta = load_metadata(key, refs_dir)
|
|
62
|
+
if not meta:
|
|
63
|
+
continue
|
|
64
|
+
local_path = copy_local_pdf(pdf_path, key, refs_dir)
|
|
65
|
+
meta.access["local_file"] = local_path
|
|
66
|
+
meta.access["tier"] = compute_tier(meta)
|
|
67
|
+
save_metadata(meta, refs_dir)
|
|
68
|
+
logger.info(f"{key}: using local PDF [{meta.access['tier']}]")
|
|
69
|
+
|
|
70
|
+
async def _do_fetch():
|
|
71
|
+
for c in citations:
|
|
72
|
+
if c.local_status != "none":
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
meta = load_metadata(c.key, refs_dir)
|
|
76
|
+
if not meta:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
tier = meta.access.get("tier", "")
|
|
80
|
+
if tier == "T1":
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
doi = meta.canonical.get("doi") or c.doi
|
|
84
|
+
arxiv_id = meta.canonical.get("arxiv_id")
|
|
85
|
+
oa_url = meta.access.get("oa_url")
|
|
86
|
+
s2_pdf_url = meta.canonical.get("s2_pdf_url")
|
|
87
|
+
pmcid = meta.canonical.get("pmcid")
|
|
88
|
+
expected_title = meta.canonical.get("title", "") or c.title
|
|
89
|
+
expected_authors = meta.canonical.get("authors") or c.authors
|
|
90
|
+
|
|
91
|
+
logger.info(f"Fetching {c.key}...")
|
|
92
|
+
result = await acquire_fulltext(
|
|
93
|
+
c.key,
|
|
94
|
+
refs_dir,
|
|
95
|
+
doi=doi,
|
|
96
|
+
arxiv_id=arxiv_id,
|
|
97
|
+
oa_url=oa_url,
|
|
98
|
+
s2_pdf_url=s2_pdf_url,
|
|
99
|
+
pmcid=pmcid,
|
|
100
|
+
expected_title=expected_title,
|
|
101
|
+
expected_authors=expected_authors,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if result.found and result.local_path:
|
|
105
|
+
meta.access["local_file"] = result.local_path
|
|
106
|
+
meta.access["tier"] = compute_tier(meta)
|
|
107
|
+
save_metadata(meta, refs_dir)
|
|
108
|
+
logger.info(
|
|
109
|
+
f"{c.key}: downloaded {result.local_path} [now {meta.access['tier']}]"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Eager parse + embed: cache GROBID output and compute embeddings
|
|
113
|
+
if result.local_path.endswith(".pdf"):
|
|
114
|
+
from sciwrite_lint.references.reference_store import parse_and_embed
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
text, chunks = await parse_and_embed(
|
|
118
|
+
c.key, refs_dir / result.local_path, refs_dir
|
|
119
|
+
)
|
|
120
|
+
if text:
|
|
121
|
+
suffix = f", {chunks} chunks embedded" if chunks else ""
|
|
122
|
+
logger.info(f"{c.key}: parsed {len(text)} chars{suffix}")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.warning(f"{c.key}: parse skipped: {e}")
|
|
125
|
+
elif result.url:
|
|
126
|
+
logger.warning(f"{c.key}: manual download needed: {result.url}")
|
|
127
|
+
else:
|
|
128
|
+
logger.warning(f"{c.key}: no source found")
|
|
129
|
+
|
|
130
|
+
if result.abstract and not meta.canonical.get("abstract"):
|
|
131
|
+
meta.canonical["abstract"] = result.abstract
|
|
132
|
+
meta.access["tier"] = compute_tier(meta)
|
|
133
|
+
save_metadata(meta, refs_dir)
|
|
134
|
+
|
|
135
|
+
asyncio.run(_do_fetch())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_fetch(args: argparse.Namespace) -> int:
|
|
139
|
+
"""Download full text for citations missing local files."""
|
|
140
|
+
from sciwrite_lint.references.metadata import load_all_metadata
|
|
141
|
+
from sciwrite_lint.pipeline import extract_citations_for_paper
|
|
142
|
+
|
|
143
|
+
from sciwrite_lint.__main__ import _load_config, _resolve_paper
|
|
144
|
+
from sciwrite_lint.cli.config import check_api_config
|
|
145
|
+
|
|
146
|
+
config = _load_config(args)
|
|
147
|
+
|
|
148
|
+
api_errors = check_api_config(config, needs_email=True)
|
|
149
|
+
if api_errors:
|
|
150
|
+
for e in api_errors:
|
|
151
|
+
logger.error(f" ✗ {e}")
|
|
152
|
+
return 2
|
|
153
|
+
pc = _resolve_paper(config, args.paper)
|
|
154
|
+
if not pc:
|
|
155
|
+
return 2
|
|
156
|
+
|
|
157
|
+
ws = config.paper_workspace(pc.name)
|
|
158
|
+
ws.ensure_dirs()
|
|
159
|
+
refs_dir = ws.root
|
|
160
|
+
|
|
161
|
+
if args.key:
|
|
162
|
+
all_meta = load_all_metadata(refs_dir)
|
|
163
|
+
meta = all_meta.get(args.key)
|
|
164
|
+
if not meta:
|
|
165
|
+
print(f"No metadata for '{args.key}'. Run 'sciwrite-lint verify' first.")
|
|
166
|
+
return 1
|
|
167
|
+
fetch_single(
|
|
168
|
+
args.key,
|
|
169
|
+
meta,
|
|
170
|
+
config,
|
|
171
|
+
references_dir=refs_dir,
|
|
172
|
+
)
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
if not pc.file_path.exists():
|
|
176
|
+
logger.error(f"Error: {pc.file_path} not found")
|
|
177
|
+
return 1
|
|
178
|
+
|
|
179
|
+
citations = asyncio.run(
|
|
180
|
+
extract_citations_for_paper(pc.file_path, config, bib_path=pc.bib)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
logger.info(f"Fetching full text for {pc.name}...")
|
|
184
|
+
fetch_for_citations(
|
|
185
|
+
citations,
|
|
186
|
+
config,
|
|
187
|
+
references_dir=refs_dir,
|
|
188
|
+
)
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def fetch_single(
|
|
193
|
+
key: str,
|
|
194
|
+
meta: object,
|
|
195
|
+
config: LintConfig,
|
|
196
|
+
references_dir: Path,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Fetch full text for a single citation."""
|
|
199
|
+
from sciwrite_lint.fulltext import acquire_fulltext
|
|
200
|
+
from sciwrite_lint.references.metadata import compute_tier, save_metadata
|
|
201
|
+
|
|
202
|
+
refs_dir = references_dir
|
|
203
|
+
|
|
204
|
+
doi = meta.canonical.get("doi") # type: ignore[attr-defined]
|
|
205
|
+
arxiv_id = meta.canonical.get("arxiv_id") # type: ignore[attr-defined]
|
|
206
|
+
oa_url = meta.access.get("oa_url") # type: ignore[attr-defined]
|
|
207
|
+
s2_pdf_url = meta.canonical.get("s2_pdf_url") # type: ignore[attr-defined]
|
|
208
|
+
pmcid = meta.canonical.get("pmcid") # type: ignore[attr-defined]
|
|
209
|
+
expected_title = meta.canonical.get("title", "") # type: ignore[attr-defined]
|
|
210
|
+
|
|
211
|
+
async def _do():
|
|
212
|
+
logger.info(f"Fetching full text for {key}...")
|
|
213
|
+
result = await acquire_fulltext(
|
|
214
|
+
key,
|
|
215
|
+
refs_dir,
|
|
216
|
+
doi=doi,
|
|
217
|
+
arxiv_id=arxiv_id,
|
|
218
|
+
oa_url=oa_url,
|
|
219
|
+
s2_pdf_url=s2_pdf_url,
|
|
220
|
+
pmcid=pmcid,
|
|
221
|
+
expected_title=expected_title,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if result.found and result.local_path:
|
|
225
|
+
meta.access["local_file"] = result.local_path # type: ignore[attr-defined]
|
|
226
|
+
meta.access["tier"] = compute_tier(meta) # type: ignore[attr-defined]
|
|
227
|
+
save_metadata(meta, refs_dir) # type: ignore[arg-type]
|
|
228
|
+
logger.info(
|
|
229
|
+
f"{key}: downloaded {result.local_path} [now {meta.access['tier']}]" # type: ignore[attr-defined]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Eager parse + embed
|
|
233
|
+
if result.local_path.endswith(".pdf"):
|
|
234
|
+
from sciwrite_lint.references.reference_store import parse_and_embed
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
text, chunks = await parse_and_embed(
|
|
238
|
+
key, refs_dir / result.local_path, refs_dir
|
|
239
|
+
)
|
|
240
|
+
if text:
|
|
241
|
+
suffix = f", {chunks} chunks embedded" if chunks else ""
|
|
242
|
+
logger.info(f"{key}: parsed {len(text)} chars{suffix}")
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.warning(f"{key}: parse skipped: {e}")
|
|
245
|
+
elif result.url:
|
|
246
|
+
logger.warning(f"{key}: manual download needed: {result.url}")
|
|
247
|
+
else:
|
|
248
|
+
logger.warning(f"{key}: no source found")
|
|
249
|
+
|
|
250
|
+
asyncio.run(_do())
|