wikicitation-mcp 0.3.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.
- annotate_utils.py +219 -0
- citation_utils.py +286 -0
- server.py +647 -0
- wiki_api.py +343 -0
- wikicitation_mcp-0.3.0.dist-info/METADATA +437 -0
- wikicitation_mcp-0.3.0.dist-info/RECORD +9 -0
- wikicitation_mcp-0.3.0.dist-info/WHEEL +4 -0
- wikicitation_mcp-0.3.0.dist-info/entry_points.txt +2 -0
- wikicitation_mcp-0.3.0.dist-info/licenses/LICENSE +21 -0
annotate_utils.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
annotate_utils.py
|
|
3
|
+
DOI and ISBN annotation via external REST APIs.
|
|
4
|
+
Pure Python / httpx, no R required.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
_HEADERS = {
|
|
11
|
+
"User-Agent": (
|
|
12
|
+
"wikicitation-mcp/0.3 "
|
|
13
|
+
"(https://github.com/jsobel1/wikicitation-mcp)"
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get(url: str, **kwargs) -> httpx.Response:
|
|
19
|
+
return httpx.get(url, headers=_HEADERS, timeout=20, **kwargs)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# EuropePMC
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_EPMC_URL = "https://www.ebi.ac.uk/europepmc/webservices/rest/search"
|
|
27
|
+
|
|
28
|
+
def annotate_dois_europmc(doi_list: list[str]) -> list[dict]:
|
|
29
|
+
"""Annotate DOIs via EuropePMC (batches of 10)."""
|
|
30
|
+
if not doi_list:
|
|
31
|
+
return []
|
|
32
|
+
results: list[dict] = []
|
|
33
|
+
for i in range(0, len(doi_list), 10):
|
|
34
|
+
chunk = doi_list[i : i + 10]
|
|
35
|
+
query = " OR ".join(f'DOI:"{doi}"' for doi in chunk)
|
|
36
|
+
r = _get(_EPMC_URL, params={
|
|
37
|
+
"query": query,
|
|
38
|
+
"resultType": "core",
|
|
39
|
+
"pageSize": len(chunk) * 2,
|
|
40
|
+
"format": "json",
|
|
41
|
+
})
|
|
42
|
+
if r.status_code == 200:
|
|
43
|
+
results.extend(
|
|
44
|
+
r.json().get("resultList", {}).get("result", [])
|
|
45
|
+
)
|
|
46
|
+
return results
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# CrossRef
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
_CROSSREF_BASE = "https://api.crossref.org/works"
|
|
54
|
+
|
|
55
|
+
def annotate_dois_crossref(doi_list: list[str]) -> list[dict]:
|
|
56
|
+
"""Annotate DOIs via CrossRef REST API (one request per DOI)."""
|
|
57
|
+
results: list[dict] = []
|
|
58
|
+
for doi in doi_list:
|
|
59
|
+
r = _get(f"{_CROSSREF_BASE}/{doi}")
|
|
60
|
+
if r.status_code != 200:
|
|
61
|
+
results.append({"doi": doi, "error": f"HTTP {r.status_code}"})
|
|
62
|
+
continue
|
|
63
|
+
msg = r.json().get("message", {})
|
|
64
|
+
pub = msg.get("published-print", msg.get("published-online", {}))
|
|
65
|
+
year = (pub.get("date-parts", [[None]])[0] or [None])[0]
|
|
66
|
+
results.append({
|
|
67
|
+
"doi": doi,
|
|
68
|
+
"title": " ".join(msg.get("title", [])),
|
|
69
|
+
"authors": [
|
|
70
|
+
f"{a.get('given','')} {a.get('family','')}".strip()
|
|
71
|
+
for a in msg.get("author", [])
|
|
72
|
+
],
|
|
73
|
+
"journal": " ".join(msg.get("container-title", [])),
|
|
74
|
+
"year": year,
|
|
75
|
+
"publisher": msg.get("publisher", ""),
|
|
76
|
+
"type": msg.get("type", ""),
|
|
77
|
+
"is_referenced_by_count": msg.get("is-referenced-by-count", 0),
|
|
78
|
+
"url": msg.get("URL", ""),
|
|
79
|
+
})
|
|
80
|
+
return results
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Altmetric
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
_ALTMETRIC_BASE = "https://api.altmetric.com/v1"
|
|
88
|
+
|
|
89
|
+
def annotate_dois_altmetric(doi_list: list[str]) -> list[dict]:
|
|
90
|
+
"""Altmetric attention scores for a list of DOIs."""
|
|
91
|
+
results: list[dict] = []
|
|
92
|
+
for doi in doi_list:
|
|
93
|
+
r = _get(f"{_ALTMETRIC_BASE}/doi/{doi}")
|
|
94
|
+
if r.status_code == 404:
|
|
95
|
+
results.append({"doi": doi, "altmetric_score": None})
|
|
96
|
+
continue
|
|
97
|
+
if r.status_code != 200:
|
|
98
|
+
results.append({"doi": doi, "error": f"HTTP {r.status_code}"})
|
|
99
|
+
continue
|
|
100
|
+
d = r.json()
|
|
101
|
+
results.append({
|
|
102
|
+
"doi": doi,
|
|
103
|
+
"altmetric_score": d.get("score"),
|
|
104
|
+
"altmetric_id": d.get("altmetric_id"),
|
|
105
|
+
"cited_by_tweeters_count": d.get("cited_by_tweeters_count", 0),
|
|
106
|
+
"cited_by_accounts_count": d.get("cited_by_accounts_count", 0),
|
|
107
|
+
"cited_by_msm_count": d.get("cited_by_msm_count", 0),
|
|
108
|
+
"cited_by_fbwalls_count": d.get("cited_by_fbwalls_count", 0),
|
|
109
|
+
"readers_count": d.get("readers_count", 0),
|
|
110
|
+
"url": d.get("details_url", ""),
|
|
111
|
+
})
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# BibTeX via CrossRef content negotiation
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def annotate_dois_bibtex(doi_list: list[str]) -> dict:
|
|
120
|
+
"""Retrieve BibTeX entries for DOIs via CrossRef content negotiation."""
|
|
121
|
+
entries: list[str] = []
|
|
122
|
+
for doi in doi_list:
|
|
123
|
+
r = httpx.get(
|
|
124
|
+
f"https://doi.org/{doi}",
|
|
125
|
+
headers={**_HEADERS, "Accept": "application/x-bibtex"},
|
|
126
|
+
follow_redirects=True,
|
|
127
|
+
timeout=20,
|
|
128
|
+
)
|
|
129
|
+
entries.append(
|
|
130
|
+
r.text.strip() if r.status_code == 200
|
|
131
|
+
else f"% DOI {doi}: HTTP {r.status_code}"
|
|
132
|
+
)
|
|
133
|
+
return {"bibtex_entries": entries}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# ISBN — Google Books
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
_GOOGLE_BOOKS = "https://www.googleapis.com/books/v1/volumes"
|
|
141
|
+
|
|
142
|
+
def annotate_isbn_google(isbn: str) -> dict:
|
|
143
|
+
"""Book metadata from Google Books for one ISBN."""
|
|
144
|
+
clean = isbn.replace("-", "").replace(" ", "")
|
|
145
|
+
r = _get(_GOOGLE_BOOKS, params={"q": f"isbn:{clean}"})
|
|
146
|
+
if r.status_code != 200:
|
|
147
|
+
return {"isbn": isbn, "error": f"HTTP {r.status_code}"}
|
|
148
|
+
items = r.json().get("items", [])
|
|
149
|
+
if not items:
|
|
150
|
+
return {"isbn": isbn, "error": "not found"}
|
|
151
|
+
info = items[0].get("volumeInfo", {})
|
|
152
|
+
return {
|
|
153
|
+
"isbn": isbn,
|
|
154
|
+
"title": info.get("title", ""),
|
|
155
|
+
"authors": info.get("authors", []),
|
|
156
|
+
"publisher": info.get("publisher", ""),
|
|
157
|
+
"publishedDate": info.get("publishedDate", ""),
|
|
158
|
+
"description": info.get("description", "")[:500],
|
|
159
|
+
"categories": info.get("categories", []),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# ISBN — Open Library
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
_OPENLIB = "https://openlibrary.org/api/books"
|
|
168
|
+
|
|
169
|
+
def annotate_isbn_openlib(isbn: str) -> dict:
|
|
170
|
+
"""Book metadata from Open Library for one ISBN."""
|
|
171
|
+
clean = isbn.replace("-", "").replace(" ", "")
|
|
172
|
+
r = _get(_OPENLIB, params={
|
|
173
|
+
"bibkeys": f"ISBN:{clean}",
|
|
174
|
+
"format": "json",
|
|
175
|
+
"jscmd": "data",
|
|
176
|
+
})
|
|
177
|
+
if r.status_code != 200:
|
|
178
|
+
return {"isbn": isbn, "error": f"HTTP {r.status_code}"}
|
|
179
|
+
data = r.json()
|
|
180
|
+
key = f"ISBN:{clean}"
|
|
181
|
+
if key not in data:
|
|
182
|
+
return {"isbn": isbn, "error": "not found"}
|
|
183
|
+
info = data[key]
|
|
184
|
+
return {
|
|
185
|
+
"isbn": isbn,
|
|
186
|
+
"title": info.get("title", ""),
|
|
187
|
+
"authors": [a.get("name", "") for a in info.get("authors", [])],
|
|
188
|
+
"publishers": [p.get("name", "") for p in info.get("publishers", [])],
|
|
189
|
+
"publish_date": info.get("publish_date", ""),
|
|
190
|
+
"number_of_pages": info.get("number_of_pages"),
|
|
191
|
+
"subjects": [s.get("name", "") for s in info.get("subjects", [])[:10]],
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# ISBN — Altmetric
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def annotate_isbns_altmetric(isbn_list: list[str]) -> list[dict]:
|
|
200
|
+
"""Altmetric attention scores for a list of ISBNs."""
|
|
201
|
+
results: list[dict] = []
|
|
202
|
+
for isbn in isbn_list:
|
|
203
|
+
clean = isbn.replace("-", "").replace(" ", "")
|
|
204
|
+
r = _get(f"{_ALTMETRIC_BASE}/isbn/{clean}")
|
|
205
|
+
if r.status_code == 404:
|
|
206
|
+
results.append({"isbn": isbn, "altmetric_score": None})
|
|
207
|
+
continue
|
|
208
|
+
if r.status_code != 200:
|
|
209
|
+
results.append({"isbn": isbn, "error": f"HTTP {r.status_code}"})
|
|
210
|
+
continue
|
|
211
|
+
d = r.json()
|
|
212
|
+
results.append({
|
|
213
|
+
"isbn": isbn,
|
|
214
|
+
"altmetric_score": d.get("score"),
|
|
215
|
+
"cited_by_tweeters_count": d.get("cited_by_tweeters_count", 0),
|
|
216
|
+
"cited_by_accounts_count": d.get("cited_by_accounts_count", 0),
|
|
217
|
+
"url": d.get("details_url", ""),
|
|
218
|
+
})
|
|
219
|
+
return results
|
citation_utils.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
citation_utils.py
|
|
3
|
+
Citation counting, extraction, parsing, and quality metrics.
|
|
4
|
+
Pure Python (re + mwparserfromhell). Text operations need no network;
|
|
5
|
+
article-level functions fetch wikitext via wiki_api.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import mwparserfromhell
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Compiled regex patterns (mirrors wikilite pkg.env regexps)
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
DOI_RE = re.compile(r'10\.\d{4,9}/[-._;()/:a-zA-Z0-9]+')
|
|
20
|
+
URL_RE = re.compile(r'https?://[^\s\]<>"|{}\\^`]+')
|
|
21
|
+
REF_RE = re.compile(
|
|
22
|
+
r'<ref(?:\s[^>]*)?(?<!/)>.*?</ref>|<ref(?:\s[^>]*)?/>',
|
|
23
|
+
re.DOTALL | re.IGNORECASE,
|
|
24
|
+
)
|
|
25
|
+
ISBN_RE = re.compile(r'(?i)\bisbn\s*[=:]?\s*([-0-9X ]{9,18})')
|
|
26
|
+
WIKILINK_RE = re.compile(r'\[\[.*?\]\]', re.DOTALL)
|
|
27
|
+
WIKILINK_DISPLAY_RE = re.compile(r'\[\[(?:[^|\]]*\|)?([^\]]+)\]\]')
|
|
28
|
+
PMID_RE = re.compile(r'(?:pmid|PMID)\s*[=:]\s*(\d{5,9})')
|
|
29
|
+
CITE_RE = re.compile(r'\{\{[Cc]ite\b[^{}]*(?:\{\{[^{}]*\}\}[^{}]*)?\}\}',
|
|
30
|
+
re.DOTALL)
|
|
31
|
+
|
|
32
|
+
ALL_PATTERNS: dict[str, re.Pattern] = {
|
|
33
|
+
"doi": DOI_RE,
|
|
34
|
+
"url": URL_RE,
|
|
35
|
+
"ref": REF_RE,
|
|
36
|
+
"isbn": ISBN_RE,
|
|
37
|
+
"wikilink": WIKILINK_RE,
|
|
38
|
+
"pmid": PMID_RE,
|
|
39
|
+
"cite_all": CITE_RE,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Template helpers
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def _is_cs1(name: str) -> bool:
|
|
48
|
+
n = name.strip().lower()
|
|
49
|
+
return n.startswith("cite ") or n == "citation"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cite_type(template_name: str) -> str:
|
|
53
|
+
n = template_name.strip().lower()
|
|
54
|
+
return n.replace("cite ", "", 1) if n.startswith("cite ") else n
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Group 2 — counting (text-based, no network)
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def get_doi_count(text: str) -> int:
|
|
62
|
+
return len(DOI_RE.findall(text))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_ref_count(text: str) -> int:
|
|
66
|
+
return len(REF_RE.findall(text))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_url_count(text: str) -> int:
|
|
70
|
+
return len(URL_RE.findall(text))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_isbn_count(text: str) -> int:
|
|
74
|
+
return len(ISBN_RE.findall(text))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_hyperlink_count(text: str) -> int:
|
|
78
|
+
return len(WIKILINK_RE.findall(text))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_any_count(text: str, regexp: str) -> int:
|
|
82
|
+
return len(re.findall(regexp, text))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Group 2 — extraction (text-based, no network)
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def extract_citations(text: str) -> list[str]:
|
|
90
|
+
"""Return all CS1 citation templates found in wikitext."""
|
|
91
|
+
wikicode = mwparserfromhell.parse(text)
|
|
92
|
+
return [str(t) for t in wikicode.filter_templates() if _is_cs1(str(t.name))]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_wikihyperlinks(text: str) -> list[str]:
|
|
96
|
+
"""Return all [[...]] wikilinks."""
|
|
97
|
+
return WIKILINK_RE.findall(text)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def replace_wikihyperlinks(text: str) -> str:
|
|
101
|
+
"""Replace [[Link|Display]] → Display, [[Link]] → Link."""
|
|
102
|
+
return WIKILINK_DISPLAY_RE.sub(r'\1', text)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def parse_cite_type(text: str) -> dict:
|
|
106
|
+
"""
|
|
107
|
+
Parse a single CS1 template string.
|
|
108
|
+
Returns {"cite_type": str, "fields": {param: value, …}}.
|
|
109
|
+
"""
|
|
110
|
+
wikicode = mwparserfromhell.parse(text)
|
|
111
|
+
templates = [t for t in wikicode.filter_templates() if _is_cs1(str(t.name))]
|
|
112
|
+
if not templates:
|
|
113
|
+
return {}
|
|
114
|
+
t = templates[0]
|
|
115
|
+
return {
|
|
116
|
+
"cite_type": _cite_type(str(t.name)),
|
|
117
|
+
"fields": {
|
|
118
|
+
str(p.name).strip(): str(p.value).strip()
|
|
119
|
+
for p in t.params
|
|
120
|
+
if str(p.value).strip()
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_source_type_counts(text: str) -> list[dict]:
|
|
126
|
+
"""Count CS1 citation types in raw wikitext."""
|
|
127
|
+
wikicode = mwparserfromhell.parse(text)
|
|
128
|
+
counts: Counter[str] = Counter()
|
|
129
|
+
for t in wikicode.filter_templates():
|
|
130
|
+
if _is_cs1(str(t.name)):
|
|
131
|
+
counts[_cite_type(str(t.name))] += 1
|
|
132
|
+
return [{"cite_type": ct, "count": c} for ct, c in counts.most_common()]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Internal: fetch wikitext
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _get_wikitext(article_name: str, date_limit: str) -> tuple[int, str]:
|
|
140
|
+
"""Return (revid, wikitext) for the most recent revision up to date_limit."""
|
|
141
|
+
from wiki_api import get_article_recent
|
|
142
|
+
result = get_article_recent(article_name, date_limit)
|
|
143
|
+
return result["metadata"].get("revid", 0), result["wikitext"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Group 2 — article-level functions (require network)
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def extract_with_regex(
|
|
151
|
+
article_name: str,
|
|
152
|
+
regexp: str,
|
|
153
|
+
date_limit: str = "2024-01-01T00:00:00Z",
|
|
154
|
+
) -> list[dict]:
|
|
155
|
+
"""Apply a regex to the most recent article wikitext."""
|
|
156
|
+
revid, wikitext = _get_wikitext(article_name, date_limit)
|
|
157
|
+
return [
|
|
158
|
+
{"art": article_name, "revid": revid, "match": m}
|
|
159
|
+
for m in re.findall(regexp, wikitext)
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def extract_all_regex(
|
|
164
|
+
article_name: str,
|
|
165
|
+
date_limit: str = "2024-01-01T00:00:00Z",
|
|
166
|
+
) -> list[dict]:
|
|
167
|
+
"""Apply all built-in patterns to the most recent article wikitext."""
|
|
168
|
+
revid, wikitext = _get_wikitext(article_name, date_limit)
|
|
169
|
+
rows: list[dict] = []
|
|
170
|
+
for pattern_name, compiled in ALL_PATTERNS.items():
|
|
171
|
+
for m in compiled.findall(wikitext):
|
|
172
|
+
match_str = m if isinstance(m, str) else (m[0] if m else "")
|
|
173
|
+
rows.append({
|
|
174
|
+
"art": article_name,
|
|
175
|
+
"revid": revid,
|
|
176
|
+
"pattern_name": pattern_name,
|
|
177
|
+
"match": match_str,
|
|
178
|
+
})
|
|
179
|
+
return rows
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def parse_citations(
|
|
183
|
+
article_name: str,
|
|
184
|
+
date_limit: str = "2024-01-01T00:00:00Z",
|
|
185
|
+
) -> list[dict]:
|
|
186
|
+
"""Structured citation table (one row per citation template)."""
|
|
187
|
+
revid, wikitext = _get_wikitext(article_name, date_limit)
|
|
188
|
+
wikicode = mwparserfromhell.parse(wikitext)
|
|
189
|
+
rows: list[dict] = []
|
|
190
|
+
for i, t in enumerate(
|
|
191
|
+
(t for t in wikicode.filter_templates() if _is_cs1(str(t.name))), start=1
|
|
192
|
+
):
|
|
193
|
+
params = {str(p.name).strip(): str(p.value).strip() for p in t.params}
|
|
194
|
+
rows.append({
|
|
195
|
+
"art": article_name,
|
|
196
|
+
"revid": revid,
|
|
197
|
+
"cite_type": _cite_type(str(t.name)),
|
|
198
|
+
"id_cite": i,
|
|
199
|
+
"doi": params.get("doi", ""),
|
|
200
|
+
"author": params.get("author", params.get("author1",
|
|
201
|
+
params.get("last", params.get("last1", "")))),
|
|
202
|
+
"year": params.get("year", ""),
|
|
203
|
+
"title": params.get("title", ""),
|
|
204
|
+
})
|
|
205
|
+
return rows
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def parse_all_citations(
|
|
209
|
+
article_name: str,
|
|
210
|
+
date_limit: str = "2024-01-01T00:00:00Z",
|
|
211
|
+
) -> list[dict]:
|
|
212
|
+
"""Full long-form table: one row per citation *field*."""
|
|
213
|
+
revid, wikitext = _get_wikitext(article_name, date_limit)
|
|
214
|
+
wikicode = mwparserfromhell.parse(wikitext)
|
|
215
|
+
rows: list[dict] = []
|
|
216
|
+
cite_idx = 0
|
|
217
|
+
for t in wikicode.filter_templates():
|
|
218
|
+
if not _is_cs1(str(t.name)):
|
|
219
|
+
continue
|
|
220
|
+
cite_idx += 1
|
|
221
|
+
ct = _cite_type(str(t.name))
|
|
222
|
+
for p in t.params:
|
|
223
|
+
rows.append({
|
|
224
|
+
"art": article_name,
|
|
225
|
+
"revid": revid,
|
|
226
|
+
"cite_type": ct,
|
|
227
|
+
"id_cite": cite_idx,
|
|
228
|
+
"variable": str(p.name).strip(),
|
|
229
|
+
"value": str(p.value).strip(),
|
|
230
|
+
})
|
|
231
|
+
return rows
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_citation_types(
|
|
235
|
+
article_name: str,
|
|
236
|
+
date_limit: str = "2024-01-01T00:00:00Z",
|
|
237
|
+
) -> list[dict]:
|
|
238
|
+
"""Count CS1 citation types for an article."""
|
|
239
|
+
_, wikitext = _get_wikitext(article_name, date_limit)
|
|
240
|
+
return get_source_type_counts(wikitext)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_sci_score(
|
|
244
|
+
article_name: str,
|
|
245
|
+
date_limit: str = "2024-01-01T00:00:00Z",
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""
|
|
248
|
+
sci_score: fraction of CS1 templates that are cite-journal.
|
|
249
|
+
sci_score2: ratio of DOI count to <ref> tag count.
|
|
250
|
+
"""
|
|
251
|
+
_, wikitext = _get_wikitext(article_name, date_limit)
|
|
252
|
+
|
|
253
|
+
type_counts = get_source_type_counts(wikitext)
|
|
254
|
+
total_cs1 = sum(r["count"] for r in type_counts)
|
|
255
|
+
journal_count = next(
|
|
256
|
+
(r["count"] for r in type_counts if r["cite_type"] == "journal"), 0
|
|
257
|
+
)
|
|
258
|
+
sci_score = journal_count / total_cs1 if total_cs1 else 0.0
|
|
259
|
+
doi_count = get_doi_count(wikitext)
|
|
260
|
+
ref_count = get_ref_count(wikitext)
|
|
261
|
+
sci_score2 = doi_count / ref_count if ref_count else 0.0
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
"sci_score": round(sci_score, 4),
|
|
265
|
+
"sci_score2": round(sci_score2, 4),
|
|
266
|
+
"article": article_name,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_top_cited_papers(
|
|
271
|
+
article_name: str,
|
|
272
|
+
date_limit: str = "2024-01-01T00:00:00Z",
|
|
273
|
+
) -> list[dict]:
|
|
274
|
+
"""Top 40 most-cited DOIs in an article, annotated via EuropePMC."""
|
|
275
|
+
from annotate_utils import annotate_dois_europmc
|
|
276
|
+
|
|
277
|
+
_, wikitext = _get_wikitext(article_name, date_limit)
|
|
278
|
+
counter = Counter(DOI_RE.findall(wikitext))
|
|
279
|
+
top_dois = [doi for doi, _ in counter.most_common(40)]
|
|
280
|
+
|
|
281
|
+
annotated = annotate_dois_europmc(top_dois)
|
|
282
|
+
for row in annotated:
|
|
283
|
+
doi = row.get("doi", "")
|
|
284
|
+
row["wiki_count"] = counter.get(doi, counter.get(doi.lower(), 0))
|
|
285
|
+
|
|
286
|
+
return sorted(annotated, key=lambda r: r.get("wiki_count", 0), reverse=True)
|