check-your-advisor 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.
- check_your_advisor/__init__.py +17 -0
- check_your_advisor/__main__.py +8 -0
- check_your_advisor/cache.py +160 -0
- check_your_advisor/citations.py +586 -0
- check_your_advisor/cli.py +1485 -0
- check_your_advisor/config.py +177 -0
- check_your_advisor/corpus.py +54 -0
- check_your_advisor/download_sources.py +267 -0
- check_your_advisor/engine.py +288 -0
- check_your_advisor/export.py +125 -0
- check_your_advisor/http_client.py +342 -0
- check_your_advisor/journals.py +1175 -0
- check_your_advisor/pdf_utils.py +229 -0
- check_your_advisor/profile/__init__.py +202 -0
- check_your_advisor/profile/caveats.py +426 -0
- check_your_advisor/profile/charts.py +791 -0
- check_your_advisor/profile/cohesion.py +178 -0
- check_your_advisor/profile/html_report.py +801 -0
- check_your_advisor/profile/impact.py +265 -0
- check_your_advisor/profile/metrics.py +643 -0
- check_your_advisor/profile/ranking.py +837 -0
- check_your_advisor/profile/report.py +2277 -0
- check_your_advisor/profile/roles.py +576 -0
- check_your_advisor/profile/scoring.py +759 -0
- check_your_advisor/profile/svg.py +285 -0
- check_your_advisor/pubmed_api.py +684 -0
- check_your_advisor/reports.py +101 -0
- check_your_advisor/theses.py +1147 -0
- check_your_advisor-0.2.0.dist-info/METADATA +207 -0
- check_your_advisor-0.2.0.dist-info/RECORD +34 -0
- check_your_advisor-0.2.0.dist-info/WHEEL +5 -0
- check_your_advisor-0.2.0.dist-info/entry_points.txt +2 -0
- check_your_advisor-0.2.0.dist-info/licenses/LICENSE +21 -0
- check_your_advisor-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
check-your-advisor — author-disambiguated PubMed harvesting and reference verification.
|
|
3
|
+
|
|
4
|
+
Two entry points over one shared HTTP and normalisation layer:
|
|
5
|
+
|
|
6
|
+
fetch/download Search PubMed, keep only the target researcher's papers via
|
|
7
|
+
ORCID + affiliation + email verification, race 8 open-access
|
|
8
|
+
sources for the PDF, then verify the downloaded file really
|
|
9
|
+
is the requested paper.
|
|
10
|
+
|
|
11
|
+
verify Check a bibliography against CrossRef and PubMed, including
|
|
12
|
+
bidirectional DOI <-> PMID resolution.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.3.1"
|
|
16
|
+
|
|
17
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
缓存模块(SQLite)
|
|
3
|
+
==================
|
|
4
|
+
替代原代码中仅通过 os.path.exists(pdf_path) 检查的简陋缓存。
|
|
5
|
+
|
|
6
|
+
改进:
|
|
7
|
+
- 记录每篇论文的元数据(DOI、PMID、下载状态、来源、时间)
|
|
8
|
+
- 支持过期清理
|
|
9
|
+
- 支持查询命中/未命中统计
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import sqlite3
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("check_your_advisor.cache")
|
|
19
|
+
|
|
20
|
+
CREATE_TABLE_SQL = """
|
|
21
|
+
CREATE TABLE IF NOT EXISTS paper_cache (
|
|
22
|
+
doi TEXT,
|
|
23
|
+
pmid TEXT,
|
|
24
|
+
title TEXT,
|
|
25
|
+
pdf_path TEXT,
|
|
26
|
+
source TEXT,
|
|
27
|
+
status TEXT DEFAULT 'pending',
|
|
28
|
+
file_size_bytes INTEGER DEFAULT 0,
|
|
29
|
+
created_at REAL,
|
|
30
|
+
updated_at REAL,
|
|
31
|
+
PRIMARY KEY (pmid)
|
|
32
|
+
);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_doi ON paper_cache(doi);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_status ON paper_cache(status);
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PaperCache:
|
|
39
|
+
def __init__(self, db_path: str = "paper_cache.db"):
|
|
40
|
+
self.db_path = db_path
|
|
41
|
+
db_parent = Path(db_path).parent
|
|
42
|
+
if str(db_parent) not in {"", "."}:
|
|
43
|
+
db_parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
# check_same_thread=False 允许跨线程使用;加锁保证串行访问
|
|
45
|
+
self._conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
46
|
+
self._conn.row_factory = sqlite3.Row
|
|
47
|
+
self._lock = threading.Lock()
|
|
48
|
+
self._init_db()
|
|
49
|
+
self.hits = 0
|
|
50
|
+
self.misses = 0
|
|
51
|
+
|
|
52
|
+
def _init_db(self):
|
|
53
|
+
with self._lock:
|
|
54
|
+
cursor = self._conn.cursor()
|
|
55
|
+
cursor.executescript(CREATE_TABLE_SQL)
|
|
56
|
+
self._conn.commit()
|
|
57
|
+
|
|
58
|
+
def lookup(self, pmid: str) -> dict | None:
|
|
59
|
+
"""查询缓存。返回 dict 或 None。"""
|
|
60
|
+
with self._lock:
|
|
61
|
+
cursor = self._conn.cursor()
|
|
62
|
+
cursor.execute("SELECT * FROM paper_cache WHERE pmid = ?", (pmid,))
|
|
63
|
+
row = cursor.fetchone()
|
|
64
|
+
|
|
65
|
+
if row and row["status"] == "downloaded" and row["pdf_path"]:
|
|
66
|
+
if Path(row["pdf_path"]).exists():
|
|
67
|
+
self.hits += 1
|
|
68
|
+
logger.debug(" 缓存命中: PMID %s", pmid)
|
|
69
|
+
return dict(row)
|
|
70
|
+
else:
|
|
71
|
+
logger.warning(" 缓存记录存在但文件丢失: %s", row["pdf_path"])
|
|
72
|
+
self.update(pmid, status="pending", pdf_path="")
|
|
73
|
+
self.misses += 1
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def update(self, pmid: str, **kwargs):
|
|
77
|
+
"""更新或插入缓存记录"""
|
|
78
|
+
now = time.time()
|
|
79
|
+
with self._lock:
|
|
80
|
+
existing = self._conn.execute(
|
|
81
|
+
"SELECT pmid FROM paper_cache WHERE pmid = ?", (pmid,)
|
|
82
|
+
).fetchone()
|
|
83
|
+
|
|
84
|
+
if existing:
|
|
85
|
+
sets = ", ".join(f"{k} = ?" for k in kwargs)
|
|
86
|
+
vals = list(kwargs.values()) + [now, pmid]
|
|
87
|
+
self._conn.execute(
|
|
88
|
+
f"UPDATE paper_cache SET {sets}, updated_at = ? WHERE pmid = ?",
|
|
89
|
+
vals,
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
kwargs["pmid"] = pmid
|
|
93
|
+
kwargs["created_at"] = now
|
|
94
|
+
kwargs["updated_at"] = now
|
|
95
|
+
cols = ", ".join(kwargs.keys())
|
|
96
|
+
placeholders = ", ".join("?" for _ in kwargs)
|
|
97
|
+
self._conn.execute(
|
|
98
|
+
f"INSERT INTO paper_cache ({cols}) VALUES ({placeholders})",
|
|
99
|
+
list(kwargs.values()),
|
|
100
|
+
)
|
|
101
|
+
self._conn.commit()
|
|
102
|
+
|
|
103
|
+
def mark_downloaded(
|
|
104
|
+
self,
|
|
105
|
+
pmid: str,
|
|
106
|
+
pdf_path: str,
|
|
107
|
+
source: str,
|
|
108
|
+
file_size: int = 0,
|
|
109
|
+
doi: str = "",
|
|
110
|
+
title: str = "",
|
|
111
|
+
):
|
|
112
|
+
self.update(
|
|
113
|
+
pmid,
|
|
114
|
+
doi=doi,
|
|
115
|
+
title=title,
|
|
116
|
+
status="downloaded",
|
|
117
|
+
pdf_path=pdf_path,
|
|
118
|
+
source=source,
|
|
119
|
+
file_size_bytes=file_size,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def mark_failed(self, pmid: str, doi: str = "", title: str = ""):
|
|
123
|
+
self.update(pmid, doi=doi, title=title, status="all_sources_failed")
|
|
124
|
+
|
|
125
|
+
def cleanup_expired(self, max_age_days: int = 90) -> int:
|
|
126
|
+
"""清理超过指定天数的失败记录(下载成功的不清理)。返回删除条数。"""
|
|
127
|
+
cutoff = time.time() - max_age_days * 86400
|
|
128
|
+
with self._lock:
|
|
129
|
+
cursor = self._conn.cursor()
|
|
130
|
+
cursor.execute(
|
|
131
|
+
"DELETE FROM paper_cache WHERE status = 'all_sources_failed' AND updated_at < ?",
|
|
132
|
+
(cutoff,),
|
|
133
|
+
)
|
|
134
|
+
deleted = cursor.rowcount
|
|
135
|
+
self._conn.commit()
|
|
136
|
+
if deleted:
|
|
137
|
+
logger.info(" 清理了 %d 条过期失败记录", deleted)
|
|
138
|
+
return deleted
|
|
139
|
+
|
|
140
|
+
def stats(self) -> dict:
|
|
141
|
+
with self._lock:
|
|
142
|
+
cursor = self._conn.cursor()
|
|
143
|
+
total = cursor.execute("SELECT COUNT(*) FROM paper_cache").fetchone()[0]
|
|
144
|
+
downloaded = cursor.execute(
|
|
145
|
+
"SELECT COUNT(*) FROM paper_cache WHERE status = 'downloaded'"
|
|
146
|
+
).fetchone()[0]
|
|
147
|
+
failed = cursor.execute(
|
|
148
|
+
"SELECT COUNT(*) FROM paper_cache WHERE status = 'all_sources_failed'"
|
|
149
|
+
).fetchone()[0]
|
|
150
|
+
return {
|
|
151
|
+
"total": total,
|
|
152
|
+
"downloaded": downloaded,
|
|
153
|
+
"failed": failed,
|
|
154
|
+
"session_hits": self.hits,
|
|
155
|
+
"session_misses": self.misses,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def close(self):
|
|
159
|
+
with self._lock:
|
|
160
|
+
self._conn.close()
|