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.
Files changed (171) hide show
  1. bibex_core/MergeDB.py +1189 -0
  2. bibex_core/__init__.py +13 -0
  3. bibex_core/modules/__init__.py +35 -0
  4. bibex_core/modules/api_utils.py +1172 -0
  5. bibex_core/modules/c1_utils.py +423 -0
  6. bibex_core/modules/file_utils.py +71 -0
  7. bibex_core/modules/ml_utils.py +332 -0
  8. bibex_core/modules/post_process.py +50 -0
  9. bibex_core/modules/stats_utils.py +216 -0
  10. bibex_core/scp2xlsx.py +235 -0
  11. bibex_core/wos2xlsx.py +159 -0
  12. bibex_core/xlsx2vos.py +185 -0
  13. bibexpy/__init__.py +9 -0
  14. bibexpy/__main__.py +6 -0
  15. bibexpy/_server/__init__.py +1 -0
  16. bibexpy/_server/config.py +92 -0
  17. bibexpy/_server/jobs/__init__.py +3 -0
  18. bibexpy/_server/jobs/runner.py +301 -0
  19. bibexpy/_server/main.py +211 -0
  20. bibexpy/_server/models/__init__.py +0 -0
  21. bibexpy/_server/models/project.py +25 -0
  22. bibexpy/_server/routers/__init__.py +0 -0
  23. bibexpy/_server/routers/audit.py +65 -0
  24. bibexpy/_server/routers/convert.py +101 -0
  25. bibexpy/_server/routers/disambiguate.py +177 -0
  26. bibexpy/_server/routers/downloads.py +26 -0
  27. bibexpy/_server/routers/enrich.py +42 -0
  28. bibexpy/_server/routers/export.py +116 -0
  29. bibexpy/_server/routers/export_folder.py +97 -0
  30. bibexpy/_server/routers/filter.py +131 -0
  31. bibexpy/_server/routers/jobs.py +72 -0
  32. bibexpy/_server/routers/merge.py +437 -0
  33. bibexpy/_server/routers/prepare.py +241 -0
  34. bibexpy/_server/routers/projects.py +49 -0
  35. bibexpy/_server/routers/quality.py +288 -0
  36. bibexpy/_server/routers/records.py +204 -0
  37. bibexpy/_server/routers/report.py +111 -0
  38. bibexpy/_server/routers/settings.py +383 -0
  39. bibexpy/_server/routers/system.py +209 -0
  40. bibexpy/_server/routers/tools.py +257 -0
  41. bibexpy/_server/routers/upload.py +110 -0
  42. bibexpy/_server/services/__init__.py +0 -0
  43. bibexpy/_server/services/analyses.py +364 -0
  44. bibexpy/_server/services/audit.py +340 -0
  45. bibexpy/_server/services/bibex_adapter.py +47 -0
  46. bibexpy/_server/services/bibtex_writer.py +104 -0
  47. bibexpy/_server/services/converter.py +205 -0
  48. bibexpy/_server/services/disambiguation/__init__.py +19 -0
  49. bibexpy/_server/services/disambiguation/blocking.py +581 -0
  50. bibexpy/_server/services/disambiguation/cache.py +47 -0
  51. bibexpy/_server/services/disambiguation/deepseek_client.py +203 -0
  52. bibexpy/_server/services/disambiguation/orcid.py +261 -0
  53. bibexpy/_server/services/disambiguation/pipeline.py +985 -0
  54. bibexpy/_server/services/disambiguation/similarity.py +91 -0
  55. bibexpy/_server/services/enricher.py +338 -0
  56. bibexpy/_server/services/exporter.py +112 -0
  57. bibexpy/_server/services/filter_engine.py +300 -0
  58. bibexpy/_server/services/merger.py +202 -0
  59. bibexpy/_server/services/methodology.py +102 -0
  60. bibexpy/_server/services/report_export.py +300 -0
  61. bibexpy/_server/services/ris_writer.py +62 -0
  62. bibexpy/_server/services/smart_merger.py +967 -0
  63. bibexpy/_server/services/storage.py +165 -0
  64. bibexpy/_web/404/index.html +1 -0
  65. bibexpy/_web/404.html +1 -0
  66. bibexpy/_web/_next/static/chunks/117-7ad5186126fa076d.js +2 -0
  67. bibexpy/_web/_next/static/chunks/156-75beffb8ae92a7f3.js +1 -0
  68. bibexpy/_web/_next/static/chunks/169-301c04fcec22686f.js +4 -0
  69. bibexpy/_web/_next/static/chunks/2-ecc1c36a5632ac61.js +1 -0
  70. bibexpy/_web/_next/static/chunks/244-be343e0f8aa00fb5.js +1 -0
  71. bibexpy/_web/_next/static/chunks/367-b7457c2b95f6932b.js +1 -0
  72. bibexpy/_web/_next/static/chunks/382-9cd163eed632e241.js +1 -0
  73. bibexpy/_web/_next/static/chunks/392-afa8bfa4c1a9893d.js +1 -0
  74. bibexpy/_web/_next/static/chunks/507-63607ded9f83c1aa.js +1 -0
  75. bibexpy/_web/_next/static/chunks/624-cae565632f47217d.js +1 -0
  76. bibexpy/_web/_next/static/chunks/739-a00c6b9c0f11813e.js +1 -0
  77. bibexpy/_web/_next/static/chunks/862-8e1bd5f93c082c02.js +1 -0
  78. bibexpy/_web/_next/static/chunks/app/_not-found/page-873ba59b5eb7c342.js +1 -0
  79. bibexpy/_web/_next/static/chunks/app/layout-0d5fc68ac8971a04.js +1 -0
  80. bibexpy/_web/_next/static/chunks/app/page-cf2bea5edc74459a.js +1 -0
  81. bibexpy/_web/_next/static/chunks/app/projects/[id]/convert/page-ab6dd645ae113b64.js +1 -0
  82. bibexpy/_web/_next/static/chunks/app/projects/[id]/disambiguate/page-dd98482cc47344e9.js +1 -0
  83. bibexpy/_web/_next/static/chunks/app/projects/[id]/enrich/page-4727e431468514e9.js +1 -0
  84. bibexpy/_web/_next/static/chunks/app/projects/[id]/export/page-f9005d164bb22cb0.js +1 -0
  85. bibexpy/_web/_next/static/chunks/app/projects/[id]/layout-07755fda55b3ae1f.js +1 -0
  86. bibexpy/_web/_next/static/chunks/app/projects/[id]/merge/page-45b95318ea7beccf.js +1 -0
  87. bibexpy/_web/_next/static/chunks/app/projects/[id]/records/page-bfad80457da9eb0b.js +1 -0
  88. bibexpy/_web/_next/static/chunks/app/projects/[id]/report/page-f05767e74158e995.js +1 -0
  89. bibexpy/_web/_next/static/chunks/app/projects/[id]/upload/page-8df78ca9cdecbcb8.js +1 -0
  90. bibexpy/_web/_next/static/chunks/app/projects/page-99960711ac6a13e9.js +1 -0
  91. bibexpy/_web/_next/static/chunks/app/settings/page-002df809d6c5a227.js +1 -0
  92. bibexpy/_web/_next/static/chunks/app/tools/page-27eee1c6a590859f.js +1 -0
  93. bibexpy/_web/_next/static/chunks/fd9d1056-f2718be92181f426.js +1 -0
  94. bibexpy/_web/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  95. bibexpy/_web/_next/static/chunks/main-app-d138a6e2f8c847f6.js +1 -0
  96. bibexpy/_web/_next/static/chunks/main-c4b9ddbf514503ef.js +1 -0
  97. bibexpy/_web/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  98. bibexpy/_web/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  99. bibexpy/_web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  100. bibexpy/_web/_next/static/chunks/webpack-c81f7fd28659d64f.js +1 -0
  101. bibexpy/_web/_next/static/css/1c4638f3e39cf127.css +3 -0
  102. bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_buildManifest.js +1 -0
  103. bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_ssgManifest.js +1 -0
  104. bibexpy/_web/images/PROMPTS.md +83 -0
  105. bibexpy/_web/images/about-illustration.png +0 -0
  106. bibexpy/_web/images/authors/alperen-sahin.jpg +0 -0
  107. bibexpy/_web/images/authors/burak-can-kara.jpg +0 -0
  108. bibexpy/_web/images/authors/taskin-dirsehan.jpg +0 -0
  109. bibexpy/_web/images/bibexpy-logo-full.png +0 -0
  110. bibexpy/_web/images/bibexpy-logo-header.png +0 -0
  111. bibexpy/_web/images/bibexpy-logo-in.png +0 -0
  112. bibexpy/_web/images/bibexpy-logo.webp +0 -0
  113. bibexpy/_web/images/hero-illustration.png +0 -0
  114. bibexpy/_web/images/loader.gif +0 -0
  115. bibexpy/_web/images/workflow-diagram.png +0 -0
  116. bibexpy/_web/index.html +1 -0
  117. bibexpy/_web/index.txt +11 -0
  118. bibexpy/_web/projects/_/convert/index.html +1 -0
  119. bibexpy/_web/projects/_/convert/index.txt +12 -0
  120. bibexpy/_web/projects/_/disambiguate/index.html +1 -0
  121. bibexpy/_web/projects/_/disambiguate/index.txt +12 -0
  122. bibexpy/_web/projects/_/enrich/index.html +1 -0
  123. bibexpy/_web/projects/_/enrich/index.txt +12 -0
  124. bibexpy/_web/projects/_/export/index.html +1 -0
  125. bibexpy/_web/projects/_/export/index.txt +12 -0
  126. bibexpy/_web/projects/_/merge/index.html +1 -0
  127. bibexpy/_web/projects/_/merge/index.txt +12 -0
  128. bibexpy/_web/projects/_/records/index.html +1 -0
  129. bibexpy/_web/projects/_/records/index.txt +12 -0
  130. bibexpy/_web/projects/_/report/index.html +1 -0
  131. bibexpy/_web/projects/_/report/index.txt +12 -0
  132. bibexpy/_web/projects/_/upload/index.html +1 -0
  133. bibexpy/_web/projects/_/upload/index.txt +12 -0
  134. bibexpy/_web/projects/index.html +1 -0
  135. bibexpy/_web/projects/index.txt +11 -0
  136. bibexpy/_web/settings/index.html +1 -0
  137. bibexpy/_web/settings/index.txt +11 -0
  138. bibexpy/_web/tools/README.md +65 -0
  139. bibexpy/_web/tools/bibexcel.svg +6 -0
  140. bibexpy/_web/tools/bibliometrix.png +0 -0
  141. bibexpy/_web/tools/biblioshiny.svg +12 -0
  142. bibexpy/_web/tools/citavi.svg +6 -0
  143. bibexpy/_web/tools/citespace.svg +7 -0
  144. bibexpy/_web/tools/citnetexplorer.svg +8 -0
  145. bibexpy/_web/tools/endnote.svg +6 -0
  146. bibexpy/_web/tools/excel.svg +1 -0
  147. bibexpy/_web/tools/gephi.svg +66 -0
  148. bibexpy/_web/tools/histcite.svg +7 -0
  149. bibexpy/_web/tools/index.html +1 -0
  150. bibexpy/_web/tools/index.txt +11 -0
  151. bibexpy/_web/tools/jabref.svg +122 -0
  152. bibexpy/_web/tools/latex.svg +1 -0
  153. bibexpy/_web/tools/mendeley.svg +1 -0
  154. bibexpy/_web/tools/openrefine.svg +15 -0
  155. bibexpy/_web/tools/overleaf.svg +1 -0
  156. bibexpy/_web/tools/papers.svg +7 -0
  157. bibexpy/_web/tools/powerbi.svg +1 -0
  158. bibexpy/_web/tools/python.svg +1 -0
  159. bibexpy/_web/tools/r.svg +1 -0
  160. bibexpy/_web/tools/refworks.svg +5 -0
  161. bibexpy/_web/tools/scite.svg +5 -0
  162. bibexpy/_web/tools/tableau.svg +1 -0
  163. bibexpy/_web/tools/vosviewer.png +0 -0
  164. bibexpy/_web/tools/zotero.svg +1 -0
  165. bibexpy/cli.py +242 -0
  166. bibexpy-2.0.0.dist-info/METADATA +105 -0
  167. bibexpy-2.0.0.dist-info/RECORD +171 -0
  168. bibexpy-2.0.0.dist-info/WHEEL +5 -0
  169. bibexpy-2.0.0.dist-info/entry_points.txt +2 -0
  170. bibexpy-2.0.0.dist-info/licenses/LICENSE +674 -0
  171. 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")