BibexPy 2.0.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.
- bibex_core/MergeDB.py +1189 -0
- bibex_core/__init__.py +13 -0
- bibex_core/modules/__init__.py +35 -0
- bibex_core/modules/api_utils.py +1172 -0
- bibex_core/modules/c1_utils.py +423 -0
- bibex_core/modules/file_utils.py +71 -0
- bibex_core/modules/ml_utils.py +332 -0
- bibex_core/modules/post_process.py +50 -0
- bibex_core/modules/stats_utils.py +216 -0
- bibex_core/scp2xlsx.py +235 -0
- bibex_core/wos2xlsx.py +159 -0
- bibex_core/xlsx2vos.py +185 -0
- bibexpy/__init__.py +9 -0
- bibexpy/__main__.py +6 -0
- bibexpy/_server/__init__.py +1 -0
- bibexpy/_server/config.py +92 -0
- bibexpy/_server/jobs/__init__.py +3 -0
- bibexpy/_server/jobs/runner.py +301 -0
- bibexpy/_server/main.py +211 -0
- bibexpy/_server/models/__init__.py +0 -0
- bibexpy/_server/models/project.py +25 -0
- bibexpy/_server/routers/__init__.py +0 -0
- bibexpy/_server/routers/audit.py +65 -0
- bibexpy/_server/routers/convert.py +101 -0
- bibexpy/_server/routers/disambiguate.py +177 -0
- bibexpy/_server/routers/downloads.py +26 -0
- bibexpy/_server/routers/enrich.py +42 -0
- bibexpy/_server/routers/export.py +116 -0
- bibexpy/_server/routers/export_folder.py +97 -0
- bibexpy/_server/routers/filter.py +131 -0
- bibexpy/_server/routers/jobs.py +72 -0
- bibexpy/_server/routers/merge.py +437 -0
- bibexpy/_server/routers/prepare.py +241 -0
- bibexpy/_server/routers/projects.py +49 -0
- bibexpy/_server/routers/quality.py +288 -0
- bibexpy/_server/routers/records.py +204 -0
- bibexpy/_server/routers/report.py +111 -0
- bibexpy/_server/routers/settings.py +383 -0
- bibexpy/_server/routers/system.py +209 -0
- bibexpy/_server/routers/tools.py +257 -0
- bibexpy/_server/routers/upload.py +110 -0
- bibexpy/_server/services/__init__.py +0 -0
- bibexpy/_server/services/analyses.py +364 -0
- bibexpy/_server/services/audit.py +340 -0
- bibexpy/_server/services/bibex_adapter.py +47 -0
- bibexpy/_server/services/bibtex_writer.py +104 -0
- bibexpy/_server/services/converter.py +205 -0
- bibexpy/_server/services/disambiguation/__init__.py +19 -0
- bibexpy/_server/services/disambiguation/blocking.py +581 -0
- bibexpy/_server/services/disambiguation/cache.py +47 -0
- bibexpy/_server/services/disambiguation/deepseek_client.py +203 -0
- bibexpy/_server/services/disambiguation/orcid.py +261 -0
- bibexpy/_server/services/disambiguation/pipeline.py +985 -0
- bibexpy/_server/services/disambiguation/similarity.py +91 -0
- bibexpy/_server/services/enricher.py +338 -0
- bibexpy/_server/services/exporter.py +112 -0
- bibexpy/_server/services/filter_engine.py +300 -0
- bibexpy/_server/services/merger.py +202 -0
- bibexpy/_server/services/methodology.py +102 -0
- bibexpy/_server/services/report_export.py +300 -0
- bibexpy/_server/services/ris_writer.py +62 -0
- bibexpy/_server/services/smart_merger.py +967 -0
- bibexpy/_server/services/storage.py +165 -0
- bibexpy/_web/404/index.html +1 -0
- bibexpy/_web/404.html +1 -0
- bibexpy/_web/_next/static/chunks/117-7ad5186126fa076d.js +2 -0
- bibexpy/_web/_next/static/chunks/156-75beffb8ae92a7f3.js +1 -0
- bibexpy/_web/_next/static/chunks/169-301c04fcec22686f.js +4 -0
- bibexpy/_web/_next/static/chunks/2-ecc1c36a5632ac61.js +1 -0
- bibexpy/_web/_next/static/chunks/244-be343e0f8aa00fb5.js +1 -0
- bibexpy/_web/_next/static/chunks/367-b7457c2b95f6932b.js +1 -0
- bibexpy/_web/_next/static/chunks/382-9cd163eed632e241.js +1 -0
- bibexpy/_web/_next/static/chunks/392-afa8bfa4c1a9893d.js +1 -0
- bibexpy/_web/_next/static/chunks/507-63607ded9f83c1aa.js +1 -0
- bibexpy/_web/_next/static/chunks/624-cae565632f47217d.js +1 -0
- bibexpy/_web/_next/static/chunks/739-a00c6b9c0f11813e.js +1 -0
- bibexpy/_web/_next/static/chunks/862-8e1bd5f93c082c02.js +1 -0
- bibexpy/_web/_next/static/chunks/app/_not-found/page-873ba59b5eb7c342.js +1 -0
- bibexpy/_web/_next/static/chunks/app/layout-0d5fc68ac8971a04.js +1 -0
- bibexpy/_web/_next/static/chunks/app/page-cf2bea5edc74459a.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/convert/page-ab6dd645ae113b64.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/disambiguate/page-dd98482cc47344e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/enrich/page-4727e431468514e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/export/page-f9005d164bb22cb0.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/layout-07755fda55b3ae1f.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/merge/page-45b95318ea7beccf.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/records/page-bfad80457da9eb0b.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/report/page-f05767e74158e995.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/upload/page-8df78ca9cdecbcb8.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/page-99960711ac6a13e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/settings/page-002df809d6c5a227.js +1 -0
- bibexpy/_web/_next/static/chunks/app/tools/page-27eee1c6a590859f.js +1 -0
- bibexpy/_web/_next/static/chunks/fd9d1056-f2718be92181f426.js +1 -0
- bibexpy/_web/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- bibexpy/_web/_next/static/chunks/main-app-d138a6e2f8c847f6.js +1 -0
- bibexpy/_web/_next/static/chunks/main-c4b9ddbf514503ef.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- bibexpy/_web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- bibexpy/_web/_next/static/chunks/webpack-c81f7fd28659d64f.js +1 -0
- bibexpy/_web/_next/static/css/1c4638f3e39cf127.css +3 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_buildManifest.js +1 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_ssgManifest.js +1 -0
- bibexpy/_web/images/PROMPTS.md +83 -0
- bibexpy/_web/images/about-illustration.png +0 -0
- bibexpy/_web/images/authors/alperen-sahin.jpg +0 -0
- bibexpy/_web/images/authors/burak-can-kara.jpg +0 -0
- bibexpy/_web/images/authors/taskin-dirsehan.jpg +0 -0
- bibexpy/_web/images/bibexpy-logo-full.png +0 -0
- bibexpy/_web/images/bibexpy-logo-header.png +0 -0
- bibexpy/_web/images/bibexpy-logo-in.png +0 -0
- bibexpy/_web/images/bibexpy-logo.webp +0 -0
- bibexpy/_web/images/hero-illustration.png +0 -0
- bibexpy/_web/images/loader.gif +0 -0
- bibexpy/_web/images/workflow-diagram.png +0 -0
- bibexpy/_web/index.html +1 -0
- bibexpy/_web/index.txt +11 -0
- bibexpy/_web/projects/_/convert/index.html +1 -0
- bibexpy/_web/projects/_/convert/index.txt +12 -0
- bibexpy/_web/projects/_/disambiguate/index.html +1 -0
- bibexpy/_web/projects/_/disambiguate/index.txt +12 -0
- bibexpy/_web/projects/_/enrich/index.html +1 -0
- bibexpy/_web/projects/_/enrich/index.txt +12 -0
- bibexpy/_web/projects/_/export/index.html +1 -0
- bibexpy/_web/projects/_/export/index.txt +12 -0
- bibexpy/_web/projects/_/merge/index.html +1 -0
- bibexpy/_web/projects/_/merge/index.txt +12 -0
- bibexpy/_web/projects/_/records/index.html +1 -0
- bibexpy/_web/projects/_/records/index.txt +12 -0
- bibexpy/_web/projects/_/report/index.html +1 -0
- bibexpy/_web/projects/_/report/index.txt +12 -0
- bibexpy/_web/projects/_/upload/index.html +1 -0
- bibexpy/_web/projects/_/upload/index.txt +12 -0
- bibexpy/_web/projects/index.html +1 -0
- bibexpy/_web/projects/index.txt +11 -0
- bibexpy/_web/settings/index.html +1 -0
- bibexpy/_web/settings/index.txt +11 -0
- bibexpy/_web/tools/README.md +65 -0
- bibexpy/_web/tools/bibexcel.svg +6 -0
- bibexpy/_web/tools/bibliometrix.png +0 -0
- bibexpy/_web/tools/biblioshiny.svg +12 -0
- bibexpy/_web/tools/citavi.svg +6 -0
- bibexpy/_web/tools/citespace.svg +7 -0
- bibexpy/_web/tools/citnetexplorer.svg +8 -0
- bibexpy/_web/tools/endnote.svg +6 -0
- bibexpy/_web/tools/excel.svg +1 -0
- bibexpy/_web/tools/gephi.svg +66 -0
- bibexpy/_web/tools/histcite.svg +7 -0
- bibexpy/_web/tools/index.html +1 -0
- bibexpy/_web/tools/index.txt +11 -0
- bibexpy/_web/tools/jabref.svg +122 -0
- bibexpy/_web/tools/latex.svg +1 -0
- bibexpy/_web/tools/mendeley.svg +1 -0
- bibexpy/_web/tools/openrefine.svg +15 -0
- bibexpy/_web/tools/overleaf.svg +1 -0
- bibexpy/_web/tools/papers.svg +7 -0
- bibexpy/_web/tools/powerbi.svg +1 -0
- bibexpy/_web/tools/python.svg +1 -0
- bibexpy/_web/tools/r.svg +1 -0
- bibexpy/_web/tools/refworks.svg +5 -0
- bibexpy/_web/tools/scite.svg +5 -0
- bibexpy/_web/tools/tableau.svg +1 -0
- bibexpy/_web/tools/vosviewer.png +0 -0
- bibexpy/_web/tools/zotero.svg +1 -0
- bibexpy/cli.py +242 -0
- bibexpy-2.0.0.dist-info/METADATA +105 -0
- bibexpy-2.0.0.dist-info/RECORD +171 -0
- bibexpy-2.0.0.dist-info/WHEEL +5 -0
- bibexpy-2.0.0.dist-info/entry_points.txt +2 -0
- bibexpy-2.0.0.dist-info/licenses/LICENSE +674 -0
- bibexpy-2.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Çekirdek bibex_core modüllerini web katmanından temiz çağırmak için sarmalayıcı.
|
|
2
|
+
|
|
3
|
+
- CLI'a özgü print/input çağrılarını yutmaz; sadece stdout/stderr'i bastırır
|
|
4
|
+
- DataFrame'leri JSON-friendly hale getirir
|
|
5
|
+
- Hataları HTTPException olarak fırlatır
|
|
6
|
+
|
|
7
|
+
Faz 1'de sadece yapı kuruluyor; gerçek çağrılar Faz 2-6'da eklenecek.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import io
|
|
14
|
+
import math
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from fastapi import HTTPException
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextlib.contextmanager
|
|
21
|
+
def _suppress_stdio():
|
|
22
|
+
buf_out, buf_err = io.StringIO(), io.StringIO()
|
|
23
|
+
import sys
|
|
24
|
+
old_out, old_err = sys.stdout, sys.stderr
|
|
25
|
+
sys.stdout, sys.stderr = buf_out, buf_err
|
|
26
|
+
try:
|
|
27
|
+
yield buf_out, buf_err
|
|
28
|
+
finally:
|
|
29
|
+
sys.stdout, sys.stderr = old_out, old_err
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def df_to_records(df, max_rows: int | None = None) -> list[dict[str, Any]]:
|
|
33
|
+
"""Pandas DataFrame -> JSON-serializable kayıt listesi (NaN -> None)."""
|
|
34
|
+
if df is None:
|
|
35
|
+
return []
|
|
36
|
+
if max_rows is not None:
|
|
37
|
+
df = df.head(max_rows)
|
|
38
|
+
records = df.to_dict(orient="records")
|
|
39
|
+
for rec in records:
|
|
40
|
+
for k, v in rec.items():
|
|
41
|
+
if isinstance(v, float) and math.isnan(v):
|
|
42
|
+
rec[k] = None
|
|
43
|
+
return records
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def http_error(message: str, status: int = 500) -> HTTPException:
|
|
47
|
+
return HTTPException(status_code=status, detail=message)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""DataFrame -> BibTeX (.bib) yazıcı.
|
|
2
|
+
|
|
3
|
+
BibexPy alan adlarını BibTeX'e eşler:
|
|
4
|
+
AU -> author (semicolon -> ' and ')
|
|
5
|
+
TI -> title
|
|
6
|
+
SO -> journal
|
|
7
|
+
PY -> year
|
|
8
|
+
VL -> volume, IS -> number, AR -> articleno
|
|
9
|
+
DI -> doi, URL -> url, AB -> abstract
|
|
10
|
+
DE -> keywords (semicolon -> ', ')
|
|
11
|
+
PU -> publisher
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _escape(value: str) -> str:
|
|
23
|
+
if not value:
|
|
24
|
+
return ""
|
|
25
|
+
s = str(value)
|
|
26
|
+
# Yaygın LaTeX kaçırma — minimum (literal tutmak için)
|
|
27
|
+
s = s.replace("\\", "\\textbackslash{}")
|
|
28
|
+
for ch in "&%$#_{}":
|
|
29
|
+
s = s.replace(ch, f"\\{ch}")
|
|
30
|
+
s = s.replace("~", "\\textasciitilde{}")
|
|
31
|
+
s = s.replace("^", "\\textasciicircum{}")
|
|
32
|
+
return s
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _normalize_authors(au: str) -> str:
|
|
36
|
+
parts = [a.strip() for a in re.split(r";|\band\b", str(au)) if a.strip()]
|
|
37
|
+
return " and ".join(parts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_keywords(de: str) -> str:
|
|
41
|
+
parts = [k.strip() for k in str(de).split(";") if k.strip()]
|
|
42
|
+
return ", ".join(parts)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _slug(text: str, maxlen: int = 30) -> str:
|
|
46
|
+
s = re.sub(r"[^a-zA-Z0-9]+", "", str(text))
|
|
47
|
+
return s[:maxlen] or "anon"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _entry_type(dt: str) -> str:
|
|
51
|
+
dt = str(dt).upper()
|
|
52
|
+
if "REVIEW" in dt: return "article"
|
|
53
|
+
if "CONFERENCE" in dt or "PROCEEDING" in dt: return "inproceedings"
|
|
54
|
+
if "BOOK" in dt: return "book"
|
|
55
|
+
if "CHAPTER" in dt: return "incollection"
|
|
56
|
+
return "article"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_bibtex(df: pd.DataFrame, output: Path) -> Path:
|
|
60
|
+
used_keys: set[str] = set()
|
|
61
|
+
lines: list[str] = []
|
|
62
|
+
for i, row in df.iterrows():
|
|
63
|
+
au = row.get("AU", "") or ""
|
|
64
|
+
py = str(row.get("PY", "") or "").strip()
|
|
65
|
+
ti = row.get("TI", "") or ""
|
|
66
|
+
# cite key: SmithEtAl2020Title
|
|
67
|
+
first_author = re.split(r";|,|\band\b", str(au))[0].strip()
|
|
68
|
+
first_author = _slug(first_author.split()[0] if first_author else "Anon", 20)
|
|
69
|
+
key = f"{first_author}{py}{_slug(ti, 12)}"
|
|
70
|
+
# Uniqueness
|
|
71
|
+
base, n = key, 1
|
|
72
|
+
while key in used_keys:
|
|
73
|
+
key = f"{base}_{n}"; n += 1
|
|
74
|
+
used_keys.add(key)
|
|
75
|
+
|
|
76
|
+
etype = _entry_type(row.get("DT", "") or "")
|
|
77
|
+
lines.append(f"@{etype}{{{key},")
|
|
78
|
+
fields = {
|
|
79
|
+
"author": _normalize_authors(au),
|
|
80
|
+
"title": str(ti),
|
|
81
|
+
"journal": str(row.get("SO", "") or ""),
|
|
82
|
+
"year": py,
|
|
83
|
+
"volume": str(row.get("VL", "") or ""),
|
|
84
|
+
"number": str(row.get("IS", "") or ""),
|
|
85
|
+
"pages": str(row.get("PG", "") or ""),
|
|
86
|
+
"doi": str(row.get("DI", "") or ""),
|
|
87
|
+
"url": str(row.get("URL", "") or ""),
|
|
88
|
+
"abstract": str(row.get("AB", "") or ""),
|
|
89
|
+
"keywords": _normalize_keywords(row.get("DE", "") or ""),
|
|
90
|
+
"publisher": str(row.get("PU", "") or ""),
|
|
91
|
+
}
|
|
92
|
+
for k, v in fields.items():
|
|
93
|
+
v = v.strip()
|
|
94
|
+
if not v or v.lower() == "nan":
|
|
95
|
+
continue
|
|
96
|
+
lines.append(f" {k} = {{{_escape(v)}}},")
|
|
97
|
+
# son virgülü temizle
|
|
98
|
+
if lines[-1].endswith(","):
|
|
99
|
+
lines[-1] = lines[-1][:-1]
|
|
100
|
+
lines.append("}")
|
|
101
|
+
lines.append("")
|
|
102
|
+
|
|
103
|
+
output.write_text("\n".join(lines), encoding="utf-8")
|
|
104
|
+
return output
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""bibex_core dönüşüm fonksiyonlarını web ortamında kullanmak için sarmal.
|
|
2
|
+
|
|
3
|
+
- Tüm çıktılar `<project>/processed/` altına yazılır
|
|
4
|
+
- Stdout/stderr bastırılır (bibex_core print kullanıyor)
|
|
5
|
+
- Hatalar HTTPException olarak fırlatılır
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Iterable
|
|
12
|
+
|
|
13
|
+
from fastapi import HTTPException
|
|
14
|
+
|
|
15
|
+
from services import storage
|
|
16
|
+
from services.bibex_adapter import _suppress_stdio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _project_paths(project_id: str) -> tuple[Path, Path, Path]:
|
|
20
|
+
meta = storage.get_project(project_id)
|
|
21
|
+
if meta is None:
|
|
22
|
+
raise HTTPException(404, "Proje bulunamadı")
|
|
23
|
+
root = storage.project_dir(project_id)
|
|
24
|
+
return root, root / "raw", root / "processed"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _raw_file(raw_dir: Path, name: str) -> Path:
|
|
28
|
+
name = Path(name).name
|
|
29
|
+
p = raw_dir / name
|
|
30
|
+
if not p.exists() or not p.is_file():
|
|
31
|
+
raise HTTPException(404, f"Ham dosya bulunamadı: {name}")
|
|
32
|
+
return p
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def csv_to_xlsx(project_id: str, source_names: Iterable[str], output_name: str = "scopus_merged.xlsx") -> Path:
|
|
36
|
+
"""Scopus CSV(ler) → tek XLSX."""
|
|
37
|
+
from bibex_core.scp2xlsx import save_to_excel
|
|
38
|
+
|
|
39
|
+
_, raw, processed = _project_paths(project_id)
|
|
40
|
+
sources = [str(_raw_file(raw, n)) for n in source_names]
|
|
41
|
+
if not sources:
|
|
42
|
+
raise HTTPException(400, "En az bir CSV dosyası gerekli")
|
|
43
|
+
output = processed / Path(output_name).name
|
|
44
|
+
with _suppress_stdio():
|
|
45
|
+
ok = save_to_excel(sources, str(output))
|
|
46
|
+
if not ok or not output.exists():
|
|
47
|
+
raise HTTPException(500, "CSV → XLSX dönüşümü başarısız")
|
|
48
|
+
storage.touch_project(project_id)
|
|
49
|
+
return output
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def wos_to_xlsx(project_id: str, source_names: Iterable[str], output_name: str = "wos_merged.xlsx") -> Path:
|
|
53
|
+
"""WoS TXT(ler) → tek XLSX. Birden fazla TXT verilirse önce birleştirir."""
|
|
54
|
+
from bibex_core.wos2xlsx import save_to_excel
|
|
55
|
+
|
|
56
|
+
_, raw, processed = _project_paths(project_id)
|
|
57
|
+
sources = [_raw_file(raw, n) for n in source_names]
|
|
58
|
+
if not sources:
|
|
59
|
+
raise HTTPException(400, "En az bir TXT dosyası gerekli")
|
|
60
|
+
|
|
61
|
+
# Birleştir
|
|
62
|
+
merged_txt = processed / "_wos_merged.tmp.txt"
|
|
63
|
+
with merged_txt.open("w", encoding="utf-8") as out:
|
|
64
|
+
for src in sources:
|
|
65
|
+
out.write(src.read_text(encoding="utf-8", errors="replace"))
|
|
66
|
+
out.write("\n")
|
|
67
|
+
|
|
68
|
+
output = processed / Path(output_name).name
|
|
69
|
+
with _suppress_stdio():
|
|
70
|
+
ok = save_to_excel(str(merged_txt), str(output))
|
|
71
|
+
merged_txt.unlink(missing_ok=True)
|
|
72
|
+
if not ok or not output.exists():
|
|
73
|
+
raise HTTPException(500, "WoS → XLSX dönüşümü başarısız")
|
|
74
|
+
storage.touch_project(project_id)
|
|
75
|
+
return output
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def auto_prepare(project_id: str, ctx=None) -> dict:
|
|
79
|
+
"""Ham CSV/TXT'leri konsolide `processed/*.xlsx`'e çevirir — ÖRTÜK hazırlık.
|
|
80
|
+
|
|
81
|
+
Smart Merge job'unun ilk fazı olarak çağrılır; ayrı "Prepare" adımı yok.
|
|
82
|
+
Skip-if-fresh: bir kaynağı yalnız en yeni ham dosyası ilgili processed
|
|
83
|
+
XLSX'ten yeni ise (veya XLSX yoksa) yeniden çevirir. Hata FIRLATMAZ —
|
|
84
|
+
sorunları `skipped`'a ekler ki tek-kaynak merge yine yürüyebilsin.
|
|
85
|
+
|
|
86
|
+
ctx: opsiyonel JobContext benzeri (.log()/.progress()); tip bağımlılığı
|
|
87
|
+
eklememek için gevşek bırakıldı.
|
|
88
|
+
"""
|
|
89
|
+
_, raw, processed = _project_paths(project_id)
|
|
90
|
+
report: dict = {
|
|
91
|
+
"scopus_xlsx": None, "wos_xlsx": None,
|
|
92
|
+
"csv": 0, "txt": 0, "skipped": [], "did_convert": False,
|
|
93
|
+
}
|
|
94
|
+
if not raw.exists():
|
|
95
|
+
return report
|
|
96
|
+
|
|
97
|
+
csv_files = [f for f in raw.iterdir() if f.is_file() and f.suffix.lower() == ".csv"]
|
|
98
|
+
txt_files = [f for f in raw.iterdir() if f.is_file() and f.suffix.lower() in (".txt", ".isi")]
|
|
99
|
+
report["csv"] = len(csv_files)
|
|
100
|
+
report["txt"] = len(txt_files)
|
|
101
|
+
|
|
102
|
+
def _fresh(out_name: str, raw_paths: list[Path]) -> bool:
|
|
103
|
+
out = processed / out_name
|
|
104
|
+
if not out.exists():
|
|
105
|
+
return False
|
|
106
|
+
try:
|
|
107
|
+
newest_raw = max((p.stat().st_mtime for p in raw_paths), default=0.0)
|
|
108
|
+
return out.stat().st_mtime >= newest_raw
|
|
109
|
+
except OSError:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
def _convert(kind: str, out_name: str, legacy: str, raw_paths: list[Path], fn) -> None:
|
|
113
|
+
if _fresh(out_name, raw_paths):
|
|
114
|
+
report[f"{kind}_xlsx"] = out_name
|
|
115
|
+
if ctx:
|
|
116
|
+
ctx.log(f"{kind.capitalize()} already prepared (up to date)")
|
|
117
|
+
return
|
|
118
|
+
old = processed / legacy
|
|
119
|
+
if old.exists():
|
|
120
|
+
try:
|
|
121
|
+
old.unlink()
|
|
122
|
+
except OSError:
|
|
123
|
+
pass
|
|
124
|
+
try:
|
|
125
|
+
if ctx:
|
|
126
|
+
ctx.log(f"Preparing {kind} — {len(raw_paths)} file(s) → {out_name}")
|
|
127
|
+
out = fn(project_id, [p.name for p in raw_paths], out_name)
|
|
128
|
+
report[f"{kind}_xlsx"] = out.name
|
|
129
|
+
report["did_convert"] = True
|
|
130
|
+
except Exception as e: # noqa: BLE001 — tek-kaynak merge yürüsün diye yutulur
|
|
131
|
+
report["skipped"].append(f"{kind}: {e}")
|
|
132
|
+
|
|
133
|
+
if csv_files:
|
|
134
|
+
_convert("scopus", "scopus.xlsx", "scopus_merged.xlsx", csv_files, csv_to_xlsx)
|
|
135
|
+
if ctx:
|
|
136
|
+
ctx.progress(0.12)
|
|
137
|
+
if txt_files:
|
|
138
|
+
_convert("wos", "wos.xlsx", "wos_merged.xlsx", txt_files, wos_to_xlsx)
|
|
139
|
+
if ctx:
|
|
140
|
+
ctx.progress(0.18)
|
|
141
|
+
|
|
142
|
+
return report
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def xlsx_to_wos_txt(project_id: str, source_name: str, output_name: str | None = None) -> Path:
|
|
146
|
+
"""XLSX → WoS-format TXT (bibliometrix için)."""
|
|
147
|
+
from bibex_core.xlsx2vos import convert_excel_to_wos
|
|
148
|
+
|
|
149
|
+
_, _, processed = _project_paths(project_id)
|
|
150
|
+
src = processed / Path(source_name).name
|
|
151
|
+
if not src.exists():
|
|
152
|
+
# raw'da da bakalım, kullanıcı doğrudan yüklediyse
|
|
153
|
+
raw = storage.project_dir(project_id) / "raw"
|
|
154
|
+
alt = raw / Path(source_name).name
|
|
155
|
+
if alt.exists():
|
|
156
|
+
src = alt
|
|
157
|
+
else:
|
|
158
|
+
raise HTTPException(404, f"XLSX bulunamadı: {source_name}")
|
|
159
|
+
|
|
160
|
+
output = processed / (output_name or (src.stem + "_wos.txt"))
|
|
161
|
+
with _suppress_stdio():
|
|
162
|
+
convert_excel_to_wos(str(src), str(output))
|
|
163
|
+
if not output.exists():
|
|
164
|
+
raise HTTPException(500, "XLSX → WoS TXT dönüşümü başarısız")
|
|
165
|
+
storage.touch_project(project_id)
|
|
166
|
+
return output
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def xlsx_to_tsv(project_id: str, source_name: str, output_name: str | None = None, sep: str = "\t") -> Path:
|
|
170
|
+
"""XLSX → düz TSV/CSV (pandas)."""
|
|
171
|
+
import pandas as pd
|
|
172
|
+
|
|
173
|
+
_, _, processed = _project_paths(project_id)
|
|
174
|
+
src = processed / Path(source_name).name
|
|
175
|
+
if not src.exists():
|
|
176
|
+
raw = storage.project_dir(project_id) / "raw"
|
|
177
|
+
alt = raw / Path(source_name).name
|
|
178
|
+
if alt.exists():
|
|
179
|
+
src = alt
|
|
180
|
+
else:
|
|
181
|
+
raise HTTPException(404, f"XLSX bulunamadı: {source_name}")
|
|
182
|
+
|
|
183
|
+
ext = "tsv" if sep == "\t" else "csv"
|
|
184
|
+
output = processed / (output_name or (src.stem + f".{ext}"))
|
|
185
|
+
df = pd.read_excel(src)
|
|
186
|
+
df.to_csv(output, sep=sep, index=False, encoding="utf-8")
|
|
187
|
+
storage.touch_project(project_id)
|
|
188
|
+
return output
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def list_processed(project_id: str) -> list[dict]:
|
|
192
|
+
"""processed/ klasöründeki dosyaları listele."""
|
|
193
|
+
_, _, processed = _project_paths(project_id)
|
|
194
|
+
if not processed.exists():
|
|
195
|
+
return []
|
|
196
|
+
out = []
|
|
197
|
+
for f in sorted(processed.iterdir()):
|
|
198
|
+
if not f.is_file():
|
|
199
|
+
continue
|
|
200
|
+
out.append({
|
|
201
|
+
"name": f.name,
|
|
202
|
+
"size": f.stat().st_size,
|
|
203
|
+
"relative_path": str(f.relative_to(storage.settings.storage_path)),
|
|
204
|
+
})
|
|
205
|
+
return out
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Deneysel LLM tabanlı yazar/affiliation disambiguation."""
|
|
2
|
+
|
|
3
|
+
from .blocking import build_author_blocks, build_affiliation_blocks
|
|
4
|
+
from .similarity import jaro_winkler, normalize_name
|
|
5
|
+
from .deepseek_client import DeepSeekClient
|
|
6
|
+
from .cache import DisambiguationCache
|
|
7
|
+
from .pipeline import run_author_disambiguation, run_affiliation_disambiguation, apply_clusters
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"build_author_blocks",
|
|
11
|
+
"build_affiliation_blocks",
|
|
12
|
+
"jaro_winkler",
|
|
13
|
+
"normalize_name",
|
|
14
|
+
"DeepSeekClient",
|
|
15
|
+
"DisambiguationCache",
|
|
16
|
+
"run_author_disambiguation",
|
|
17
|
+
"run_affiliation_disambiguation",
|
|
18
|
+
"apply_clusters",
|
|
19
|
+
]
|