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.
Files changed (34) hide show
  1. check_your_advisor/__init__.py +17 -0
  2. check_your_advisor/__main__.py +8 -0
  3. check_your_advisor/cache.py +160 -0
  4. check_your_advisor/citations.py +586 -0
  5. check_your_advisor/cli.py +1485 -0
  6. check_your_advisor/config.py +177 -0
  7. check_your_advisor/corpus.py +54 -0
  8. check_your_advisor/download_sources.py +267 -0
  9. check_your_advisor/engine.py +288 -0
  10. check_your_advisor/export.py +125 -0
  11. check_your_advisor/http_client.py +342 -0
  12. check_your_advisor/journals.py +1175 -0
  13. check_your_advisor/pdf_utils.py +229 -0
  14. check_your_advisor/profile/__init__.py +202 -0
  15. check_your_advisor/profile/caveats.py +426 -0
  16. check_your_advisor/profile/charts.py +791 -0
  17. check_your_advisor/profile/cohesion.py +178 -0
  18. check_your_advisor/profile/html_report.py +801 -0
  19. check_your_advisor/profile/impact.py +265 -0
  20. check_your_advisor/profile/metrics.py +643 -0
  21. check_your_advisor/profile/ranking.py +837 -0
  22. check_your_advisor/profile/report.py +2277 -0
  23. check_your_advisor/profile/roles.py +576 -0
  24. check_your_advisor/profile/scoring.py +759 -0
  25. check_your_advisor/profile/svg.py +285 -0
  26. check_your_advisor/pubmed_api.py +684 -0
  27. check_your_advisor/reports.py +101 -0
  28. check_your_advisor/theses.py +1147 -0
  29. check_your_advisor-0.2.0.dist-info/METADATA +207 -0
  30. check_your_advisor-0.2.0.dist-info/RECORD +34 -0
  31. check_your_advisor-0.2.0.dist-info/WHEEL +5 -0
  32. check_your_advisor-0.2.0.dist-info/entry_points.txt +2 -0
  33. check_your_advisor-0.2.0.dist-info/licenses/LICENSE +21 -0
  34. 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,8 @@
1
+ """Allow `python -m check_your_advisor ...`."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main() or 0)
@@ -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()