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,204 @@
|
|
|
1
|
+
"""Kayıt seviyesinde işlemler — toplu silme, seçilenleri zenginleştirme.
|
|
2
|
+
|
|
3
|
+
Her destrüktif işlem önce snapshot alır ve audit log'a yazılır.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from fastapi import APIRouter, HTTPException
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from services import analyses, audit, filter_engine, merger, storage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
router = APIRouter(prefix="/projects/{project_id}/records", tags=["records"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _snapshot_dataset(project_id: str, df: pd.DataFrame, reason: str) -> str:
|
|
23
|
+
"""Mevcut datasetin bir kopyasını AKTİF analizin snapshots/ klasörüne yaz. Path döndür."""
|
|
24
|
+
snaps = analyses.work_dir(project_id) / "snapshots"
|
|
25
|
+
snaps.mkdir(exist_ok=True)
|
|
26
|
+
stamp = time.strftime("%Y%m%d_%H%M%S")
|
|
27
|
+
p = snaps / f"pre_{reason}_{stamp}.xlsx"
|
|
28
|
+
df.to_excel(p, index=False)
|
|
29
|
+
rel = str(p.relative_to(storage.settings.storage_path))
|
|
30
|
+
return rel
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _save_dataset(project_id: str, df: pd.DataFrame) -> str:
|
|
34
|
+
"""Aktif merged_*.xlsx dosyasının üzerine yaz, cache'i temizle."""
|
|
35
|
+
p = merger.merged_dataset_path(project_id)
|
|
36
|
+
if p is None:
|
|
37
|
+
raise HTTPException(409, "no_active_merged_dataset")
|
|
38
|
+
df.to_excel(p, index=False)
|
|
39
|
+
# cache invalidate
|
|
40
|
+
filter_engine._DF_CACHE.clear()
|
|
41
|
+
return str(p.relative_to(storage.settings.storage_path))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DeletePayload(BaseModel):
|
|
45
|
+
uids: list[str] = Field(default_factory=list, description="Benzersiz satır UID'leri (önerilen)")
|
|
46
|
+
dois: list[str] = Field(default_factory=list, description="DOI listesi (yalnız DOI'li kayıtlar için)")
|
|
47
|
+
indices: list[int] = Field(default_factory=list, description="Pozisyon tabanlı (fallback, kırılgan)")
|
|
48
|
+
reason: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.post("/delete")
|
|
52
|
+
def delete_records(project_id: str, payload: DeletePayload):
|
|
53
|
+
"""UID, DOI veya satır indekslerine göre toplu silme. Snapshot alınır."""
|
|
54
|
+
if storage.get_project(project_id) is None:
|
|
55
|
+
raise HTTPException(404, "project_not_found")
|
|
56
|
+
df = filter_engine.load_merged(project_id)
|
|
57
|
+
total_before = int(len(df))
|
|
58
|
+
|
|
59
|
+
if not payload.uids and not payload.dois and not payload.indices:
|
|
60
|
+
raise HTTPException(400, "no_records_to_delete")
|
|
61
|
+
|
|
62
|
+
keep = pd.Series(True, index=df.index)
|
|
63
|
+
|
|
64
|
+
if payload.uids and "UID" in df.columns:
|
|
65
|
+
uid_set = {str(u).strip() for u in payload.uids if u}
|
|
66
|
+
df_uid = df["UID"].astype(str).str.strip()
|
|
67
|
+
keep &= ~df_uid.isin(uid_set)
|
|
68
|
+
|
|
69
|
+
if payload.dois and "DI" in df.columns:
|
|
70
|
+
doi_set = {str(d).strip().lower() for d in payload.dois if d}
|
|
71
|
+
df_doi = df["DI"].astype(str).str.strip().str.lower()
|
|
72
|
+
keep &= ~df_doi.isin(doi_set)
|
|
73
|
+
|
|
74
|
+
if payload.indices:
|
|
75
|
+
idx_to_drop = set(payload.indices)
|
|
76
|
+
keep &= ~pd.Series([i in idx_to_drop for i in range(len(df))], index=df.index)
|
|
77
|
+
|
|
78
|
+
deleted_count = int((~keep).sum())
|
|
79
|
+
if deleted_count == 0:
|
|
80
|
+
return {"deleted": 0, "kept": total_before, "snapshot": None}
|
|
81
|
+
|
|
82
|
+
snapshot = _snapshot_dataset(project_id, df, "delete")
|
|
83
|
+
df_new = df.loc[keep].reset_index(drop=True)
|
|
84
|
+
saved_path = _save_dataset(project_id, df_new)
|
|
85
|
+
|
|
86
|
+
audit.write(
|
|
87
|
+
project_id,
|
|
88
|
+
kind="records_delete",
|
|
89
|
+
title=f"{deleted_count} kayıt silindi",
|
|
90
|
+
title_key="audit.titles.recordsDeleted",
|
|
91
|
+
title_params={"n": deleted_count},
|
|
92
|
+
details={
|
|
93
|
+
"before": total_before,
|
|
94
|
+
"after": int(len(df_new)),
|
|
95
|
+
"deleted": deleted_count,
|
|
96
|
+
"by_uid": len(payload.uids),
|
|
97
|
+
"by_doi": len(payload.dois),
|
|
98
|
+
"by_index": len(payload.indices),
|
|
99
|
+
"reason": payload.reason,
|
|
100
|
+
"saved_path": saved_path,
|
|
101
|
+
},
|
|
102
|
+
before={"total": total_before},
|
|
103
|
+
after={"total": int(len(df_new))},
|
|
104
|
+
snapshot=snapshot,
|
|
105
|
+
user_action="manual_bulk_delete",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"deleted": deleted_count,
|
|
110
|
+
"kept": int(len(df_new)),
|
|
111
|
+
"total_before": total_before,
|
|
112
|
+
"snapshot": snapshot,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RestoreSnapshotPayload(BaseModel):
|
|
117
|
+
snapshot: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@router.post("/restore-snapshot")
|
|
121
|
+
def restore_snapshot(project_id: str, payload: RestoreSnapshotPayload):
|
|
122
|
+
"""Snapshot dosyasını aktif dataset olarak geri yükle."""
|
|
123
|
+
if storage.get_project(project_id) is None:
|
|
124
|
+
raise HTTPException(404, "project_not_found")
|
|
125
|
+
snap_path = storage.settings.storage_path / payload.snapshot
|
|
126
|
+
if not snap_path.exists():
|
|
127
|
+
raise HTTPException(404, f"snapshot_not_found: {payload.snapshot}")
|
|
128
|
+
df = pd.read_excel(snap_path)
|
|
129
|
+
saved_path = _save_dataset(project_id, df)
|
|
130
|
+
audit.write(
|
|
131
|
+
project_id,
|
|
132
|
+
kind="snapshot_restore",
|
|
133
|
+
title=f"Snapshot geri yüklendi: {Path(payload.snapshot).name}",
|
|
134
|
+
title_key="audit.titles.snapshotRestored",
|
|
135
|
+
title_params={"name": Path(payload.snapshot).name},
|
|
136
|
+
details={"snapshot": payload.snapshot, "restored_count": int(len(df)), "saved_path": saved_path},
|
|
137
|
+
user_action="restore",
|
|
138
|
+
)
|
|
139
|
+
return {"restored": int(len(df)), "snapshot": payload.snapshot}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@router.get("/snapshots")
|
|
143
|
+
def list_snapshots(project_id: str):
|
|
144
|
+
"""Mevcut snapshot dosyalarını listele."""
|
|
145
|
+
if storage.get_project(project_id) is None:
|
|
146
|
+
raise HTTPException(404, "project_not_found")
|
|
147
|
+
snaps_dir = analyses.work_dir(project_id) / "snapshots"
|
|
148
|
+
if not snaps_dir.exists():
|
|
149
|
+
return []
|
|
150
|
+
items = []
|
|
151
|
+
for f in sorted(snaps_dir.iterdir(), reverse=True):
|
|
152
|
+
if f.is_file() and f.suffix.lower() == ".xlsx":
|
|
153
|
+
items.append({
|
|
154
|
+
"name": f.name,
|
|
155
|
+
"relative_path": str(f.relative_to(storage.settings.storage_path)),
|
|
156
|
+
"size": f.stat().st_size,
|
|
157
|
+
"mtime": f.stat().st_mtime,
|
|
158
|
+
})
|
|
159
|
+
return items
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class UpdatePayload(BaseModel):
|
|
163
|
+
uid: Optional[str] = None
|
|
164
|
+
doi: Optional[str] = None
|
|
165
|
+
fields: dict[str, Any] = Field(default_factory=dict, description="Güncellenecek alan -> yeni değer")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@router.post("/update")
|
|
169
|
+
def update_record(project_id: str, payload: UpdatePayload):
|
|
170
|
+
"""Tek bir kaydın alanlarını elle düzenle (UID veya DOI ile bul). Snapshot alınır."""
|
|
171
|
+
if storage.get_project(project_id) is None:
|
|
172
|
+
raise HTTPException(404, "project_not_found")
|
|
173
|
+
if not payload.fields:
|
|
174
|
+
raise HTTPException(400, "no_fields_to_update")
|
|
175
|
+
df = filter_engine.load_merged(project_id)
|
|
176
|
+
|
|
177
|
+
mask = pd.Series(False, index=df.index)
|
|
178
|
+
if payload.uid and "UID" in df.columns:
|
|
179
|
+
mask |= df["UID"].astype(str).str.strip() == str(payload.uid).strip()
|
|
180
|
+
if not mask.any() and payload.doi and "DI" in df.columns:
|
|
181
|
+
mask |= df["DI"].astype(str).str.strip().str.lower() == str(payload.doi).strip().lower()
|
|
182
|
+
idxs = df.index[mask].tolist()
|
|
183
|
+
if not idxs:
|
|
184
|
+
raise HTTPException(404, "record_not_found")
|
|
185
|
+
idx = idxs[0]
|
|
186
|
+
|
|
187
|
+
snapshot = _snapshot_dataset(project_id, df, "edit")
|
|
188
|
+
for k, v in payload.fields.items():
|
|
189
|
+
if k not in df.columns:
|
|
190
|
+
df[k] = ""
|
|
191
|
+
df.at[idx, k] = "" if v is None else v
|
|
192
|
+
saved_path = _save_dataset(project_id, df)
|
|
193
|
+
|
|
194
|
+
audit.write(
|
|
195
|
+
project_id,
|
|
196
|
+
kind="records_edit",
|
|
197
|
+
title=f"Kayıt düzenlendi ({len(payload.fields)} alan)",
|
|
198
|
+
title_key="audit.titles.recordEdited",
|
|
199
|
+
title_params={"n": len(payload.fields)},
|
|
200
|
+
details={"uid": payload.uid, "doi": payload.doi, "fields": list(payload.fields.keys()), "saved_path": saved_path},
|
|
201
|
+
snapshot=snapshot,
|
|
202
|
+
user_action="manual_edit",
|
|
203
|
+
)
|
|
204
|
+
return {"updated": 1, "fields": len(payload.fields), "snapshot": snapshot}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Rapor endpoint'leri — ham günlük (MD/TXT/PDF) + LLM metodoloji raporu (MD/TXT/PDF).
|
|
2
|
+
|
|
3
|
+
Export adımından sonraki son adım. İki çıktı türü:
|
|
4
|
+
• Ham operasyon günlüğü — sürecin birebir kaydı.
|
|
5
|
+
• Metodoloji raporu — günlüğün LLM ile İngilizce makale metnine dönüştürülmüş hali.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException
|
|
11
|
+
from fastapi.responses import PlainTextResponse, Response
|
|
12
|
+
|
|
13
|
+
from services import audit, methodology, report_export, storage
|
|
14
|
+
|
|
15
|
+
router = APIRouter(prefix="/projects/{project_id}/report", tags=["report"])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _require(project_id: str) -> None:
|
|
19
|
+
if storage.get_project(project_id) is None:
|
|
20
|
+
raise HTTPException(404, "project_not_found")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _pdf(content: bytes, filename: str) -> Response:
|
|
24
|
+
return Response(
|
|
25
|
+
content=content,
|
|
26
|
+
media_type="application/pdf",
|
|
27
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _attach_text(text: str, filename: str, media: str) -> Response:
|
|
32
|
+
return Response(
|
|
33
|
+
content=text,
|
|
34
|
+
media_type=f"{media}; charset=utf-8",
|
|
35
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ───────────────────────── Ham günlük ─────────────────────────
|
|
40
|
+
|
|
41
|
+
@router.get("/log.md")
|
|
42
|
+
def log_markdown(project_id: str):
|
|
43
|
+
_require(project_id)
|
|
44
|
+
return _attach_text(audit.format_markdown_report(project_id), "operation_log.md", "text/markdown")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.get("/log.txt")
|
|
48
|
+
def log_text(project_id: str):
|
|
49
|
+
_require(project_id)
|
|
50
|
+
return _attach_text(report_export.format_text_report(project_id), "operation_log.txt", "text/plain")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get("/log.pdf")
|
|
54
|
+
def log_pdf(project_id: str):
|
|
55
|
+
_require(project_id)
|
|
56
|
+
meta = storage.get_project(project_id)
|
|
57
|
+
title = f"Operation Report — {meta.name if meta else project_id}"
|
|
58
|
+
pdf = report_export.render_pdf(title, audit.format_markdown_report(project_id))
|
|
59
|
+
return _pdf(pdf, "operation_log.pdf")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ───────────────────────── Metodoloji raporu (LLM) ─────────────────────────
|
|
63
|
+
|
|
64
|
+
@router.get("/methodology")
|
|
65
|
+
def methodology_get(project_id: str):
|
|
66
|
+
_require(project_id)
|
|
67
|
+
report = methodology.get_methodology(project_id)
|
|
68
|
+
return report or {"text": None}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.post("/methodology")
|
|
72
|
+
def methodology_generate(project_id: str):
|
|
73
|
+
_require(project_id)
|
|
74
|
+
report = methodology.generate_methodology(project_id)
|
|
75
|
+
audit.write(
|
|
76
|
+
project_id,
|
|
77
|
+
kind="report",
|
|
78
|
+
title="Metodoloji raporu üretildi (LLM)",
|
|
79
|
+
title_key="audit.titles.methodologyGenerated",
|
|
80
|
+
details={"model": report.get("model"), "step_count": report.get("step_count"), "lang": "en"},
|
|
81
|
+
user_action="generate_methodology",
|
|
82
|
+
)
|
|
83
|
+
return report
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _methodology_text_or_404(project_id: str) -> str:
|
|
87
|
+
report = methodology.get_methodology(project_id)
|
|
88
|
+
if not report or not report.get("text"):
|
|
89
|
+
raise HTTPException(404, "methodology_not_generated")
|
|
90
|
+
return report["text"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.get("/methodology.md")
|
|
94
|
+
def methodology_md(project_id: str):
|
|
95
|
+
_require(project_id)
|
|
96
|
+
return _attach_text(_methodology_text_or_404(project_id), "methodology_report.md", "text/markdown")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.get("/methodology.txt")
|
|
100
|
+
def methodology_txt(project_id: str):
|
|
101
|
+
_require(project_id)
|
|
102
|
+
text = report_export._strip_emoji(_methodology_text_or_404(project_id))
|
|
103
|
+
return _attach_text(text, "methodology_report.txt", "text/plain")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@router.get("/methodology.pdf")
|
|
107
|
+
def methodology_pdf(project_id: str):
|
|
108
|
+
_require(project_id)
|
|
109
|
+
text = _methodology_text_or_404(project_id)
|
|
110
|
+
pdf = report_export.render_pdf(methodology.methodology_title(project_id), text)
|
|
111
|
+
return _pdf(pdf, "methodology_report.pdf")
|