crossref-local 0.3.0__py3-none-any.whl → 0.4.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.
- crossref_local/__init__.py +86 -22
- crossref_local/__main__.py +6 -0
- crossref_local/aio.py +0 -0
- crossref_local/api.py +148 -5
- crossref_local/cache.py +466 -0
- crossref_local/cache_export.py +83 -0
- crossref_local/cache_viz.py +296 -0
- crossref_local/citations.py +0 -0
- crossref_local/cli.py +358 -97
- crossref_local/cli_cache.py +179 -0
- crossref_local/cli_completion.py +245 -0
- crossref_local/cli_main.py +20 -0
- crossref_local/cli_mcp.py +275 -0
- crossref_local/config.py +99 -3
- crossref_local/db.py +3 -1
- crossref_local/fts.py +38 -4
- crossref_local/impact_factor/__init__.py +0 -0
- crossref_local/impact_factor/calculator.py +0 -0
- crossref_local/impact_factor/journal_lookup.py +0 -0
- crossref_local/mcp_server.py +413 -0
- crossref_local/models.py +0 -0
- crossref_local/remote.py +269 -0
- crossref_local/server.py +352 -0
- {crossref_local-0.3.0.dist-info → crossref_local-0.4.0.dist-info}/METADATA +152 -7
- crossref_local-0.4.0.dist-info/RECORD +27 -0
- crossref_local-0.4.0.dist-info/entry_points.txt +3 -0
- crossref_local-0.3.0.dist-info/RECORD +0 -16
- crossref_local-0.3.0.dist-info/entry_points.txt +0 -2
- {crossref_local-0.3.0.dist-info → crossref_local-0.4.0.dist-info}/WHEEL +0 -0
crossref_local/server.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""FastAPI server for CrossRef Local with FTS5 search.
|
|
2
|
+
|
|
3
|
+
This server provides proper full-text search using FTS5 index,
|
|
4
|
+
unlike the Django API which only scans a limited subset.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
crossref-local api # Run on default port 8333
|
|
8
|
+
crossref-local api --port 8080 # Custom port
|
|
9
|
+
|
|
10
|
+
# Or directly:
|
|
11
|
+
uvicorn crossref_local.server:app --host 0.0.0.0 --port 8333
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from typing import Optional, List
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI, Query, HTTPException
|
|
18
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
from . import fts, __version__
|
|
22
|
+
from .db import get_db
|
|
23
|
+
from .models import Work
|
|
24
|
+
|
|
25
|
+
app = FastAPI(
|
|
26
|
+
title="CrossRef Local API",
|
|
27
|
+
description="Fast full-text search across 167M+ scholarly works",
|
|
28
|
+
version=__version__,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# CORS middleware
|
|
32
|
+
app.add_middleware(
|
|
33
|
+
CORSMiddleware,
|
|
34
|
+
allow_origins=["*"],
|
|
35
|
+
allow_methods=["*"],
|
|
36
|
+
allow_headers=["*"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WorkResponse(BaseModel):
|
|
41
|
+
doi: str
|
|
42
|
+
title: Optional[str] = None
|
|
43
|
+
authors: List[str] = []
|
|
44
|
+
year: Optional[int] = None
|
|
45
|
+
journal: Optional[str] = None
|
|
46
|
+
issn: Optional[str] = None
|
|
47
|
+
volume: Optional[str] = None
|
|
48
|
+
issue: Optional[str] = None
|
|
49
|
+
page: Optional[str] = None
|
|
50
|
+
abstract: Optional[str] = None
|
|
51
|
+
citation_count: Optional[int] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SearchResponse(BaseModel):
|
|
55
|
+
query: str
|
|
56
|
+
total: int
|
|
57
|
+
returned: int
|
|
58
|
+
elapsed_ms: float
|
|
59
|
+
results: List[WorkResponse]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InfoResponse(BaseModel):
|
|
63
|
+
name: str = "CrossRef Local API"
|
|
64
|
+
version: str = __version__
|
|
65
|
+
status: str = "running"
|
|
66
|
+
mode: str = "local"
|
|
67
|
+
total_papers: int
|
|
68
|
+
fts_indexed: int
|
|
69
|
+
citations: int
|
|
70
|
+
database_path: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.get("/")
|
|
74
|
+
def root():
|
|
75
|
+
"""API root with endpoint information."""
|
|
76
|
+
return {
|
|
77
|
+
"name": "CrossRef Local API",
|
|
78
|
+
"version": __version__,
|
|
79
|
+
"status": "running",
|
|
80
|
+
"endpoints": {
|
|
81
|
+
"health": "/health",
|
|
82
|
+
"info": "/info",
|
|
83
|
+
"search": "/works?q=<query>",
|
|
84
|
+
"get_by_doi": "/works/{doi}",
|
|
85
|
+
"batch": "/works/batch",
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.get("/health")
|
|
91
|
+
def health():
|
|
92
|
+
"""Health check endpoint."""
|
|
93
|
+
db = get_db()
|
|
94
|
+
return {
|
|
95
|
+
"status": "healthy",
|
|
96
|
+
"database_connected": db is not None,
|
|
97
|
+
"database_path": str(db.db_path) if db else None,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.get("/info", response_model=InfoResponse)
|
|
102
|
+
def info():
|
|
103
|
+
"""Get database statistics."""
|
|
104
|
+
db = get_db()
|
|
105
|
+
|
|
106
|
+
row = db.fetchone("SELECT COUNT(*) as count FROM works")
|
|
107
|
+
work_count = row["count"] if row else 0
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
row = db.fetchone("SELECT COUNT(*) as count FROM works_fts")
|
|
111
|
+
fts_count = row["count"] if row else 0
|
|
112
|
+
except Exception:
|
|
113
|
+
fts_count = 0
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
row = db.fetchone("SELECT COUNT(*) as count FROM citations")
|
|
117
|
+
citation_count = row["count"] if row else 0
|
|
118
|
+
except Exception:
|
|
119
|
+
citation_count = 0
|
|
120
|
+
|
|
121
|
+
return InfoResponse(
|
|
122
|
+
total_papers=work_count,
|
|
123
|
+
fts_indexed=fts_count,
|
|
124
|
+
citations=citation_count,
|
|
125
|
+
database_path=str(db.db_path),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.get("/works", response_model=SearchResponse)
|
|
130
|
+
def search_works(
|
|
131
|
+
q: str = Query(..., description="Search query (FTS5 syntax supported)"),
|
|
132
|
+
limit: int = Query(10, ge=1, le=100, description="Max results"),
|
|
133
|
+
offset: int = Query(0, ge=0, description="Skip first N results"),
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
Full-text search across works.
|
|
137
|
+
|
|
138
|
+
Uses FTS5 index for fast searching across titles, abstracts, and authors.
|
|
139
|
+
Supports FTS5 query syntax like AND, OR, NOT, "exact phrases".
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
/works?q=machine learning
|
|
143
|
+
/works?q="neural network" AND hippocampus
|
|
144
|
+
/works?q=CRISPR&limit=20
|
|
145
|
+
"""
|
|
146
|
+
start = time.perf_counter()
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
results = fts.search(q, limit=limit, offset=offset)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
raise HTTPException(status_code=400, detail=f"Search error: {e}")
|
|
152
|
+
|
|
153
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
154
|
+
|
|
155
|
+
return SearchResponse(
|
|
156
|
+
query=q,
|
|
157
|
+
total=results.total,
|
|
158
|
+
returned=len(results.works),
|
|
159
|
+
elapsed_ms=round(elapsed_ms, 2),
|
|
160
|
+
results=[
|
|
161
|
+
WorkResponse(
|
|
162
|
+
doi=w.doi,
|
|
163
|
+
title=w.title,
|
|
164
|
+
authors=w.authors,
|
|
165
|
+
year=w.year,
|
|
166
|
+
journal=w.journal,
|
|
167
|
+
issn=w.issn,
|
|
168
|
+
volume=w.volume,
|
|
169
|
+
issue=w.issue,
|
|
170
|
+
page=w.page,
|
|
171
|
+
abstract=w.abstract,
|
|
172
|
+
citation_count=w.citation_count,
|
|
173
|
+
)
|
|
174
|
+
for w in results.works
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.get("/works/{doi:path}", response_model=Optional[WorkResponse])
|
|
180
|
+
def get_work(doi: str):
|
|
181
|
+
"""
|
|
182
|
+
Get work metadata by DOI.
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
/works/10.1038/nature12373
|
|
186
|
+
/works/10.1016/j.cell.2020.01.001
|
|
187
|
+
"""
|
|
188
|
+
db = get_db()
|
|
189
|
+
metadata = db.get_metadata(doi)
|
|
190
|
+
|
|
191
|
+
if metadata is None:
|
|
192
|
+
raise HTTPException(status_code=404, detail=f"DOI not found: {doi}")
|
|
193
|
+
|
|
194
|
+
work = Work.from_metadata(doi, metadata)
|
|
195
|
+
|
|
196
|
+
return WorkResponse(
|
|
197
|
+
doi=work.doi,
|
|
198
|
+
title=work.title,
|
|
199
|
+
authors=work.authors,
|
|
200
|
+
year=work.year,
|
|
201
|
+
journal=work.journal,
|
|
202
|
+
issn=work.issn,
|
|
203
|
+
volume=work.volume,
|
|
204
|
+
issue=work.issue,
|
|
205
|
+
page=work.page,
|
|
206
|
+
abstract=work.abstract,
|
|
207
|
+
citation_count=work.citation_count,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class BatchRequest(BaseModel):
|
|
212
|
+
dois: List[str]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class BatchResponse(BaseModel):
|
|
216
|
+
requested: int
|
|
217
|
+
found: int
|
|
218
|
+
results: List[WorkResponse]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.post("/works/batch", response_model=BatchResponse)
|
|
222
|
+
def get_works_batch(request: BatchRequest):
|
|
223
|
+
"""
|
|
224
|
+
Get multiple works by DOI.
|
|
225
|
+
|
|
226
|
+
Request body: {"dois": ["10.1038/...", "10.1016/..."]}
|
|
227
|
+
"""
|
|
228
|
+
db = get_db()
|
|
229
|
+
results = []
|
|
230
|
+
|
|
231
|
+
for doi in request.dois:
|
|
232
|
+
metadata = db.get_metadata(doi)
|
|
233
|
+
if metadata:
|
|
234
|
+
work = Work.from_metadata(doi, metadata)
|
|
235
|
+
results.append(
|
|
236
|
+
WorkResponse(
|
|
237
|
+
doi=work.doi,
|
|
238
|
+
title=work.title,
|
|
239
|
+
authors=work.authors,
|
|
240
|
+
year=work.year,
|
|
241
|
+
journal=work.journal,
|
|
242
|
+
abstract=work.abstract,
|
|
243
|
+
citation_count=work.citation_count,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return BatchResponse(
|
|
248
|
+
requested=len(request.dois),
|
|
249
|
+
found=len(results),
|
|
250
|
+
results=results,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# For backwards compatibility with existing API endpoints
|
|
255
|
+
@app.get("/api/search/")
|
|
256
|
+
def api_search_compat(
|
|
257
|
+
title: Optional[str] = None,
|
|
258
|
+
q: Optional[str] = None,
|
|
259
|
+
doi: Optional[str] = None,
|
|
260
|
+
limit: int = 10,
|
|
261
|
+
):
|
|
262
|
+
"""Backwards-compatible search endpoint."""
|
|
263
|
+
query = title or q
|
|
264
|
+
|
|
265
|
+
if doi:
|
|
266
|
+
# DOI lookup
|
|
267
|
+
try:
|
|
268
|
+
work = get_work(doi)
|
|
269
|
+
return {
|
|
270
|
+
"query": {"doi": doi},
|
|
271
|
+
"results": [work.model_dump()],
|
|
272
|
+
"total": 1,
|
|
273
|
+
"returned": 1,
|
|
274
|
+
}
|
|
275
|
+
except HTTPException:
|
|
276
|
+
return {"query": {"doi": doi}, "results": [], "total": 0, "returned": 0}
|
|
277
|
+
|
|
278
|
+
if not query:
|
|
279
|
+
raise HTTPException(
|
|
280
|
+
status_code=400, detail="Specify q, title, or doi parameter"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Call fts.search directly (not the endpoint function)
|
|
284
|
+
results = fts.search(query, limit=limit, offset=0)
|
|
285
|
+
return {
|
|
286
|
+
"query": {
|
|
287
|
+
"title": query,
|
|
288
|
+
"doi": None,
|
|
289
|
+
"year": None,
|
|
290
|
+
"authors": None,
|
|
291
|
+
"limit": limit,
|
|
292
|
+
},
|
|
293
|
+
"results": [
|
|
294
|
+
WorkResponse(
|
|
295
|
+
doi=w.doi,
|
|
296
|
+
title=w.title,
|
|
297
|
+
authors=w.authors,
|
|
298
|
+
year=w.year,
|
|
299
|
+
journal=w.journal,
|
|
300
|
+
issn=w.issn,
|
|
301
|
+
volume=w.volume,
|
|
302
|
+
issue=w.issue,
|
|
303
|
+
page=w.page,
|
|
304
|
+
abstract=w.abstract,
|
|
305
|
+
citation_count=w.citation_count,
|
|
306
|
+
).model_dump()
|
|
307
|
+
for w in results.works
|
|
308
|
+
],
|
|
309
|
+
"total": results.total,
|
|
310
|
+
"returned": len(results.works),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@app.get("/api/stats/")
|
|
315
|
+
def api_stats_compat():
|
|
316
|
+
"""Backwards-compatible stats endpoint."""
|
|
317
|
+
db = get_db()
|
|
318
|
+
|
|
319
|
+
row = db.fetchone("SELECT COUNT(*) as count FROM works")
|
|
320
|
+
work_count = row["count"] if row else 0
|
|
321
|
+
|
|
322
|
+
# Get table names
|
|
323
|
+
tables = []
|
|
324
|
+
for row in db.fetchall("SELECT name FROM sqlite_master WHERE type='table'"):
|
|
325
|
+
tables.append(row["name"])
|
|
326
|
+
|
|
327
|
+
# Get index names
|
|
328
|
+
indices = []
|
|
329
|
+
for row in db.fetchall("SELECT name FROM sqlite_master WHERE type='index'"):
|
|
330
|
+
if row["name"]:
|
|
331
|
+
indices.append(row["name"])
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
"total_papers": work_count,
|
|
335
|
+
"database_size_mb": None,
|
|
336
|
+
"year_range": None,
|
|
337
|
+
"total_journals": 0,
|
|
338
|
+
"total_citations": None,
|
|
339
|
+
"tables": tables,
|
|
340
|
+
"indices": indices,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def run_server(host: str = "0.0.0.0", port: int = 8333):
|
|
345
|
+
"""Run the FastAPI server."""
|
|
346
|
+
import uvicorn
|
|
347
|
+
|
|
348
|
+
uvicorn.run(app, host=host, port=port)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
if __name__ == "__main__":
|
|
352
|
+
run_server()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crossref-local
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Local CrossRef database with 167M+ works and full-text search
|
|
5
5
|
Project-URL: Homepage, https://github.com/ywatanabe1989/crossref_local
|
|
6
6
|
Project-URL: Repository, https://github.com/ywatanabe1989/crossref_local
|
|
@@ -18,15 +18,37 @@ Classifier: Topic :: Database
|
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: click>=8.0
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: fastapi>=0.100; extra == 'all'
|
|
23
|
+
Requires-Dist: fastmcp>=0.4; extra == 'all'
|
|
24
|
+
Requires-Dist: matplotlib>=3.7; extra == 'all'
|
|
25
|
+
Requires-Dist: networkx>=3.0; extra == 'all'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'all'
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'all'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'all'
|
|
29
|
+
Requires-Dist: pyvis>=0.3; extra == 'all'
|
|
30
|
+
Requires-Dist: uvicorn>=0.20; extra == 'all'
|
|
31
|
+
Provides-Extra: api
|
|
32
|
+
Requires-Dist: fastapi>=0.100; extra == 'api'
|
|
33
|
+
Requires-Dist: uvicorn>=0.20; extra == 'api'
|
|
21
34
|
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
22
36
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
23
37
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
38
|
+
Provides-Extra: mcp
|
|
39
|
+
Requires-Dist: fastmcp>=0.4; extra == 'mcp'
|
|
24
40
|
Provides-Extra: viz
|
|
25
41
|
Requires-Dist: matplotlib>=3.7; extra == 'viz'
|
|
26
42
|
Requires-Dist: networkx>=3.0; extra == 'viz'
|
|
27
43
|
Requires-Dist: pyvis>=0.3; extra == 'viz'
|
|
28
44
|
Description-Content-Type: text/markdown
|
|
29
45
|
|
|
46
|
+
<!-- ---
|
|
47
|
+
!-- Timestamp: 2026-01-16 19:15:51
|
|
48
|
+
!-- Author: ywatanabe
|
|
49
|
+
!-- File: /home/ywatanabe/proj/crossref-local/README.md
|
|
50
|
+
!-- --- -->
|
|
51
|
+
|
|
30
52
|
# CrossRef Local
|
|
31
53
|
|
|
32
54
|
Local CrossRef database with 167M+ scholarly works, full-text search, and impact factor calculation.
|
|
@@ -39,6 +61,22 @@ Local CrossRef database with 167M+ scholarly works, full-text search, and impact
|
|
|
39
61
|
<img src="examples/readme_figure.png" alt="CrossRef Local Demo" width="800"/>
|
|
40
62
|
</p>
|
|
41
63
|
|
|
64
|
+
<details>
|
|
65
|
+
<summary><strong>MCP Demo Video</strong></summary>
|
|
66
|
+
|
|
67
|
+
<p align="center">
|
|
68
|
+
<a href="https://scitex.ai/media/videos/crossref-local-v0.3.1-demo.mp4">
|
|
69
|
+
<img src="examples/demo_mcp_out/crossref-local-v0.3.1-demo-thumbnail_6m55s.png" alt="Demo Video Thumbnail" width="600"/>
|
|
70
|
+
</a>
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
Live demonstration of MCP server integration with Claude Code for `epilepsy seizure prediction` literature review:
|
|
74
|
+
- Full-text search on title, abstracts, and keywords across 167M papers (22ms response)
|
|
75
|
+
|
|
76
|
+
📄 [Full demo documentation](examples/demo_mcp.org) | 📊 [Generated diagrams](examples/demo_mcp_out/)
|
|
77
|
+
|
|
78
|
+
</details>
|
|
79
|
+
|
|
42
80
|
<details>
|
|
43
81
|
<summary><strong>Why CrossRef Local?</strong></summary>
|
|
44
82
|
|
|
@@ -119,24 +157,110 @@ async def main():
|
|
|
119
157
|
|
|
120
158
|
```bash
|
|
121
159
|
crossref-local search "CRISPR genome editing" -n 5
|
|
122
|
-
crossref-local
|
|
123
|
-
crossref-local
|
|
160
|
+
crossref-local search-by-doi 10.1038/nature12373
|
|
161
|
+
crossref-local status # Configuration and database stats
|
|
124
162
|
```
|
|
125
163
|
|
|
126
164
|
With abstracts (`-a` flag):
|
|
127
165
|
```
|
|
128
|
-
$ crossref-local search "CRISPR" -n 1 -a
|
|
166
|
+
$ crossref-local search "RS-1 enhances CRISPR" -n 1 -a
|
|
129
167
|
|
|
130
|
-
Found
|
|
168
|
+
Found 4 matches in 128.4ms
|
|
131
169
|
|
|
132
170
|
1. RS-1 enhances CRISPR/Cas9- and TALEN-mediated knock-in efficiency (2016)
|
|
133
171
|
DOI: 10.1038/ncomms10548
|
|
134
172
|
Journal: Nature Communications
|
|
135
173
|
Abstract: Zinc-finger nuclease, transcription activator-like effector nuclease
|
|
136
|
-
and CRISPR/Cas9 are becoming major tools for genome editing
|
|
137
|
-
|
|
174
|
+
and CRISPR/Cas9 are becoming major tools for genome editing...
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
<details>
|
|
180
|
+
<summary><strong>HTTP API</strong></summary>
|
|
181
|
+
|
|
182
|
+
Start the FastAPI server:
|
|
183
|
+
```bash
|
|
184
|
+
crossref-local run-server-http --host 0.0.0.0 --port 8333
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Endpoints:
|
|
188
|
+
```bash
|
|
189
|
+
# Search works (FTS5)
|
|
190
|
+
curl "http://localhost:8333/works?q=CRISPR&limit=10"
|
|
191
|
+
|
|
192
|
+
# Get by DOI
|
|
193
|
+
curl "http://localhost:8333/works/10.1038/nature12373"
|
|
194
|
+
|
|
195
|
+
# Batch DOI lookup
|
|
196
|
+
curl -X POST "http://localhost:8333/works/batch" \
|
|
197
|
+
-H "Content-Type: application/json" \
|
|
198
|
+
-d '{"dois": ["10.1038/nature12373", "10.1126/science.aax0758"]}'
|
|
199
|
+
|
|
200
|
+
# Database info
|
|
201
|
+
curl "http://localhost:8333/info"
|
|
138
202
|
```
|
|
139
203
|
|
|
204
|
+
HTTP mode (connect to running server):
|
|
205
|
+
```bash
|
|
206
|
+
# On local machine (if server is remote)
|
|
207
|
+
ssh -L 8333:127.0.0.1:8333 your-server
|
|
208
|
+
|
|
209
|
+
# Python client
|
|
210
|
+
from crossref_local import configure_http
|
|
211
|
+
configure_http("http://localhost:8333")
|
|
212
|
+
|
|
213
|
+
# Or via CLI
|
|
214
|
+
crossref-local --http search "CRISPR"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
</details>
|
|
218
|
+
|
|
219
|
+
<details>
|
|
220
|
+
<summary><strong>MCP Server</strong></summary>
|
|
221
|
+
|
|
222
|
+
Run as MCP (Model Context Protocol) server:
|
|
223
|
+
```bash
|
|
224
|
+
crossref-local run-server-mcp
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Local MCP client configuration:
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"mcpServers": {
|
|
231
|
+
"crossref-local": {
|
|
232
|
+
"command": "crossref-local",
|
|
233
|
+
"args": ["run-server-mcp"],
|
|
234
|
+
"env": {
|
|
235
|
+
"CROSSREF_LOCAL_DB": "/path/to/crossref.db"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Remote MCP via HTTP (recommended):
|
|
243
|
+
```bash
|
|
244
|
+
# On server: start persistent MCP server
|
|
245
|
+
crossref-local run-server-mcp -t http --host 0.0.0.0 --port 8082
|
|
246
|
+
```
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"mcpServers": {
|
|
250
|
+
"crossref-remote": {
|
|
251
|
+
"url": "http://your-server:8082/mcp"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
See [docs/remote-deployment.md](docs/remote-deployment.md) for systemd and Docker setup.
|
|
258
|
+
|
|
259
|
+
Available tools:
|
|
260
|
+
- `search` - Full-text search across 167M+ papers
|
|
261
|
+
- `search_by_doi` - Get paper by DOI
|
|
262
|
+
- `status` - Database statistics
|
|
263
|
+
|
|
140
264
|
</details>
|
|
141
265
|
|
|
142
266
|
<details>
|
|
@@ -188,6 +312,27 @@ Searching 167M records in milliseconds via FTS5.
|
|
|
188
312
|
|
|
189
313
|
</details>
|
|
190
314
|
|
|
315
|
+
<details>
|
|
316
|
+
<summary><strong>Related Projects</strong></summary>
|
|
317
|
+
|
|
318
|
+
**[openalex-local](https://github.com/ywatanabe1989/openalex-local)** - Sister project with OpenAlex data:
|
|
319
|
+
|
|
320
|
+
| Feature | crossref-local | openalex-local |
|
|
321
|
+
|---------|----------------|----------------|
|
|
322
|
+
| Works | 167M | 284M |
|
|
323
|
+
| Abstracts | ~21% | ~45-60% |
|
|
324
|
+
| Update frequency | Real-time | Monthly |
|
|
325
|
+
| DOI authority | ✓ (source) | Uses CrossRef |
|
|
326
|
+
| Citations | Raw references | Linked works |
|
|
327
|
+
| Concepts/Topics | ❌ | ✓ |
|
|
328
|
+
| Author IDs | ❌ | ✓ |
|
|
329
|
+
| Best for | DOI lookup, raw refs | Semantic search |
|
|
330
|
+
|
|
331
|
+
**When to use CrossRef**: Real-time DOI updates, raw reference parsing, authoritative metadata.
|
|
332
|
+
**When to use OpenAlex**: Semantic search, citation analysis, topic discovery.
|
|
333
|
+
|
|
334
|
+
</details>
|
|
335
|
+
|
|
191
336
|
|
|
192
337
|
---
|
|
193
338
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
crossref_local/__init__.py,sha256=eCvA9q-nZSWCIoMZxINV5xsboXovv4YeqZfjfLzi820,3696
|
|
2
|
+
crossref_local/__main__.py,sha256=N1c1ESGgJkAwsSWXANUgmzxC1OJEIqw-cl9m4pmNP7s,110
|
|
3
|
+
crossref_local/aio.py,sha256=En2btSn3euRbEYav1919gsmdC8iQaMbgGUso-IThCwo,5490
|
|
4
|
+
crossref_local/api.py,sha256=4VGThHU3RZaH5dgjax69iUHfRlh7j91LaM00guDKtKs,7436
|
|
5
|
+
crossref_local/cache.py,sha256=sMrmiUJr8-zRBDXnHg0M_3q3MpgzT7V1bqMKb5kpKVg,12291
|
|
6
|
+
crossref_local/cache_export.py,sha256=DU44Rv894fZz6OnBETbgwYzwVOPg3zJ6sP85yE5EYbc,2519
|
|
7
|
+
crossref_local/cache_viz.py,sha256=0VAHK-i1xR5StEYrJ_IAs0HSk0MQNVFi4EjPtlZEwTY,8498
|
|
8
|
+
crossref_local/citations.py,sha256=QFahv84upNnXP_89A8bHxEbAdz7wHbh5LEniGcAiHas,12402
|
|
9
|
+
crossref_local/cli.py,sha256=OMgrWviTT6Q9wLPWyhb9qDiRlVAJCuRDwDVpddyRPUk,16352
|
|
10
|
+
crossref_local/cli_cache.py,sha256=8QykJ7-H313x7oKRw5fJ9Coay27Y8uBWGx5JR1VQ1_Y,6220
|
|
11
|
+
crossref_local/cli_completion.py,sha256=yzqwMbGbbqXJQHdztuaqji-AdFTd8zDDhHeNGdw4NpU,7341
|
|
12
|
+
crossref_local/cli_main.py,sha256=NgCGB5-ThofTRPoxxwZpsfCQTo5WIw8oP7zBSKhEiDQ,444
|
|
13
|
+
crossref_local/cli_mcp.py,sha256=JBGveR_K7M_EzmHb3G9xr1IZPHAf-vvht-yAn5IzkQM,8524
|
|
14
|
+
crossref_local/config.py,sha256=tBAqnnscLOIF5dobEjOA4b57OVfGeGhpnmRonYKt7ng,4839
|
|
15
|
+
crossref_local/db.py,sha256=x7dXQXjsFN4LavtkNAKTNw1cUBMG-2h53-Z-Xlq6aoQ,3696
|
|
16
|
+
crossref_local/fts.py,sha256=yZMh_vmtFentXKAFGTS4z7ZNNj7p_ItgfFP5i0yQltw,4448
|
|
17
|
+
crossref_local/mcp_server.py,sha256=BN1BByTI54Rh-QoMZA2wUTV7lxdX-fLvoiEaH3YP-W0,10974
|
|
18
|
+
crossref_local/models.py,sha256=b_yYb91O6RwEPpEqe2Wmdz12WIfE5itjEus4-fCLxLI,5476
|
|
19
|
+
crossref_local/remote.py,sha256=Ju4NewXsw_cNX4vq6OeSu8fKtyf9CLwno-4uWkpwAOM,8638
|
|
20
|
+
crossref_local/server.py,sha256=CpCZm5ZVLSPfcke1lGkvEOp1XsH6bUGm5M0i9pj343U,9059
|
|
21
|
+
crossref_local/impact_factor/__init__.py,sha256=pcgVCPogBisANYE5Vp2PHVGPgxoMsSXr-6utqVE97-4,559
|
|
22
|
+
crossref_local/impact_factor/calculator.py,sha256=eZ13URAZzPdRyAQpS8zXe_T33e2lm_gQhtoJCXbfIGM,15977
|
|
23
|
+
crossref_local/impact_factor/journal_lookup.py,sha256=Ztx6ZeWxfmPvA3KfcW5h_yz01XPstIdk91j3nu2Q-qw,8846
|
|
24
|
+
crossref_local-0.4.0.dist-info/METADATA,sha256=l-quaEeSKQwlveyZgKwZfW1SwyNsM9G8yZhGOHtGtMs,9508
|
|
25
|
+
crossref_local-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
+
crossref_local-0.4.0.dist-info/entry_points.txt,sha256=1LhHFzJ7VWhNmGY6yIGDJ5EhKTYCyU7CbLEooXQWhLQ,116
|
|
27
|
+
crossref_local-0.4.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
crossref_local/__init__.py,sha256=vJFOk5somcdkuTT7UhVjAR4kj8p5JYVVHgmgCtTmENs,1544
|
|
2
|
-
crossref_local/aio.py,sha256=En2btSn3euRbEYav1919gsmdC8iQaMbgGUso-IThCwo,5490
|
|
3
|
-
crossref_local/api.py,sha256=FPZBStNLD7hnjjnpUzKfBFKo7gv3JHOLEIFSydH53bw,3370
|
|
4
|
-
crossref_local/citations.py,sha256=QFahv84upNnXP_89A8bHxEbAdz7wHbh5LEniGcAiHas,12402
|
|
5
|
-
crossref_local/cli.py,sha256=9ISMQSvWZHYrEO_gYXJB-Ju0lZCr9ifH8jFnltkgYTU,8639
|
|
6
|
-
crossref_local/config.py,sha256=_tHZOHBbbw5BcmkWdTlqbSffijlQZkPkexl2YxW4GmE,1980
|
|
7
|
-
crossref_local/db.py,sha256=xiTYFQxsIXmV6_QmXMWf6eX7GJv5D4icy67lBvQjUQI,3529
|
|
8
|
-
crossref_local/fts.py,sha256=e5mxAbHWrO9E1GvG1WFDFxoCs_4RQIdAPYLSuMf8JCM,3395
|
|
9
|
-
crossref_local/models.py,sha256=b_yYb91O6RwEPpEqe2Wmdz12WIfE5itjEus4-fCLxLI,5476
|
|
10
|
-
crossref_local/impact_factor/__init__.py,sha256=pcgVCPogBisANYE5Vp2PHVGPgxoMsSXr-6utqVE97-4,559
|
|
11
|
-
crossref_local/impact_factor/calculator.py,sha256=eZ13URAZzPdRyAQpS8zXe_T33e2lm_gQhtoJCXbfIGM,15977
|
|
12
|
-
crossref_local/impact_factor/journal_lookup.py,sha256=Ztx6ZeWxfmPvA3KfcW5h_yz01XPstIdk91j3nu2Q-qw,8846
|
|
13
|
-
crossref_local-0.3.0.dist-info/METADATA,sha256=3z5SlpYph6S6V6fwu5omXXyyNeGubZGvyqic8BU8-n8,5659
|
|
14
|
-
crossref_local-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
-
crossref_local-0.3.0.dist-info/entry_points.txt,sha256=TaFQ1y-tIym2dqgE6xUUeXTvy2uCHNKoYeRO4w6ndWQ,59
|
|
16
|
-
crossref_local-0.3.0.dist-info/RECORD,,
|
|
File without changes
|