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,300 @@
1
+ """Rapor dışa aktarma yardımcıları.
2
+
3
+ İki tür çıktı:
4
+ • Ham operasyon günlüğü (audit log) → Markdown / düz metin / PDF
5
+ • LLM metodoloji raporu (İngilizce prose) → Markdown / düz metin / PDF
6
+
7
+ PDF üretimi reportlab ile yapılır (saf Python, sistem bağımlılığı yok).
8
+ Emoji ve markdown sembolleri PDF/TXT'de temizlenir; .md zaten zengin kalır.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import datetime as dt
14
+ import io
15
+ import re
16
+ from typing import Any
17
+
18
+ from services import audit, storage
19
+
20
+ # ─────────────────────────────────────────────────────────────────────────
21
+ # Metin temizleme
22
+ # ─────────────────────────────────────────────────────────────────────────
23
+
24
+ # Emoji + dingbat + ok işareti aralıkları (reportlab varsayılan fontu render edemez)
25
+ _EMOJI = re.compile(
26
+ "["
27
+ "\U0001F000-\U0001FAFF" # emoji blokları
28
+ "\U00002600-\U000027BF" # misc semboller + dingbatlar
29
+ "\U0001F1E6-\U0001F1FF" # bayraklar
30
+ "\U00002190-\U000021FF" # oklar (→ ↩ vb.)
31
+ "\U00002B00-\U00002BFF" # misc oklar/semboller
32
+ "️" # variation selector
33
+ "]+",
34
+ flags=re.UNICODE,
35
+ )
36
+
37
+
38
+ def _strip_emoji(s: str) -> str:
39
+ return _EMOJI.sub("", str(s)).strip()
40
+
41
+
42
+ def _esc_inline(s: str) -> str:
43
+ """reportlab Paragraph mini-HTML için güvenli kaçış + sınırlı markdown.
44
+
45
+ Önce XML özel karakterleri kaçırılır, ardından **kalın** ve `kod` işaretleri
46
+ kontrollü etiketlere çevrilir.
47
+ """
48
+ s = _strip_emoji(s)
49
+ s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
50
+ s = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", s)
51
+ s = re.sub(r"`(.+?)`", r'<font face="Courier">\1</font>', s)
52
+ return s
53
+
54
+
55
+ # ─────────────────────────────────────────────────────────────────────────
56
+ # PDF üretimi (markdown-benzeri metin → PDF)
57
+ # ─────────────────────────────────────────────────────────────────────────
58
+
59
+ def render_pdf(title: str, md_text: str) -> bytes:
60
+ """Başlık + markdown-benzeri gövdeyi temiz bir PDF'e dönüştürür.
61
+
62
+ Desteklenen: # / ## / ### başlık, - / * madde, | a | b | tablo,
63
+ **kalın**, `kod`, boş satır = boşluk. Emoji'ler atılır.
64
+ """
65
+ from reportlab.lib import colors
66
+ from reportlab.lib.enums import TA_LEFT
67
+ from reportlab.lib.pagesizes import A4
68
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
69
+ from reportlab.lib.units import cm
70
+ from reportlab.platypus import (
71
+ ListFlowable, ListItem, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle,
72
+ )
73
+
74
+ buf = io.BytesIO()
75
+ doc = SimpleDocTemplate(
76
+ buf, pagesize=A4,
77
+ leftMargin=2 * cm, rightMargin=2 * cm, topMargin=2 * cm, bottomMargin=2 * cm,
78
+ title=_strip_emoji(title) or "Report", author="BibexPy v2",
79
+ )
80
+ base = getSampleStyleSheet()
81
+ brand = colors.HexColor("#3F32B5")
82
+ st_body = ParagraphStyle("body", parent=base["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
83
+ st_h1 = ParagraphStyle("h1", parent=base["Heading1"], fontSize=17, spaceAfter=8, textColor=brand)
84
+ st_h2 = ParagraphStyle("h2", parent=base["Heading2"], fontSize=13, spaceBefore=10, spaceAfter=4, textColor=colors.HexColor("#1F2A44"))
85
+ st_h3 = ParagraphStyle("h3", parent=base["Heading3"], fontSize=11, spaceBefore=8, spaceAfter=3)
86
+ st_meta = ParagraphStyle("meta", parent=base["Normal"], fontSize=8.5, textColor=colors.HexColor("#64748B"))
87
+
88
+ flow: list[Any] = []
89
+ flow.append(Paragraph(_esc_inline(title), st_h1))
90
+ flow.append(Paragraph(dt.datetime.now().strftime("%Y-%m-%d %H:%M"), st_meta))
91
+ flow.append(Spacer(1, 10))
92
+
93
+ lines = (md_text or "").split("\n")
94
+ bullets: list[str] = []
95
+
96
+ def flush_bullets() -> None:
97
+ nonlocal bullets
98
+ if bullets:
99
+ flow.append(ListFlowable(
100
+ [ListItem(Paragraph(b, st_body), leftIndent=12) for b in bullets],
101
+ bulletType="bullet", start="•", leftIndent=10,
102
+ ))
103
+ flow.append(Spacer(1, 4))
104
+ bullets = []
105
+
106
+ i = 0
107
+ n = len(lines)
108
+ while i < n:
109
+ raw = lines[i].rstrip()
110
+ stripped = raw.strip()
111
+
112
+ # Tablo bloğu
113
+ if stripped.startswith("|"):
114
+ flush_bullets()
115
+ rows: list[list[str]] = []
116
+ while i < n and lines[i].strip().startswith("|"):
117
+ cells = [c.strip() for c in lines[i].strip().strip("|").split("|")]
118
+ i += 1
119
+ # ayraç satırı ( |---|---| ) atla
120
+ if cells and all(set(c) <= set("-: ") for c in cells):
121
+ continue
122
+ rows.append(cells)
123
+ if rows:
124
+ ncol = max(len(r) for r in rows)
125
+ data = [[Paragraph(_esc_inline(c), st_body) for c in (r + [""] * (ncol - len(r)))] for r in rows]
126
+ tbl = Table(data, hAlign="LEFT")
127
+ tbl.setStyle(TableStyle([
128
+ ("BACKGROUND", (0, 0), (-1, 0), brand),
129
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
130
+ ("FONTSIZE", (0, 0), (-1, -1), 9),
131
+ ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#CBD5E1")),
132
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
133
+ ("LEFTPADDING", (0, 0), (-1, -1), 5),
134
+ ("RIGHTPADDING", (0, 0), (-1, -1), 5),
135
+ ("TOPPADDING", (0, 0), (-1, -1), 3),
136
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
137
+ ]))
138
+ flow.append(tbl)
139
+ flow.append(Spacer(1, 6))
140
+ continue
141
+
142
+ if not stripped:
143
+ flush_bullets()
144
+ flow.append(Spacer(1, 5))
145
+ i += 1
146
+ continue
147
+ if stripped.startswith("### "):
148
+ flush_bullets(); flow.append(Paragraph(_esc_inline(stripped[4:]), st_h3)); i += 1; continue
149
+ if stripped.startswith("## "):
150
+ flush_bullets(); flow.append(Paragraph(_esc_inline(stripped[3:]), st_h2)); i += 1; continue
151
+ if stripped.startswith("# "):
152
+ flush_bullets(); flow.append(Paragraph(_esc_inline(stripped[2:]), st_h1)); i += 1; continue
153
+ if stripped.startswith(("- ", "* ")):
154
+ bullets.append(_esc_inline(stripped[2:])); i += 1; continue
155
+
156
+ flush_bullets()
157
+ flow.append(Paragraph(_esc_inline(stripped), st_body))
158
+ i += 1
159
+
160
+ flush_bullets()
161
+ doc.build(flow)
162
+ return buf.getvalue()
163
+
164
+
165
+ # ─────────────────────────────────────────────────────────────────────────
166
+ # Ham günlük → düz metin
167
+ # ─────────────────────────────────────────────────────────────────────────
168
+
169
+ def format_text_report(project_id: str) -> str:
170
+ """Operasyon günlüğünün temiz, markdown'sız, emojisiz düz metin sürümü."""
171
+ meta = storage.get_project(project_id)
172
+ entries = audit.read(project_id)
173
+ name = meta.name if meta else project_id
174
+
175
+ out: list[str] = []
176
+ out.append("BibexPy v2 - Operation Report")
177
+ out.append("=" * 40)
178
+ out.append(f"Project : {name} ({project_id})")
179
+ out.append(f"Generated : {dt.datetime.now().strftime('%Y-%m-%d %H:%M')}")
180
+ out.append(f"Total operations: {len(entries)}")
181
+ out.append("")
182
+
183
+ if not entries:
184
+ out.append("No operations recorded yet.")
185
+ return "\n".join(out)
186
+
187
+ s = audit.summary(project_id)
188
+ out.append("Summary")
189
+ out.append("-" * 40)
190
+ for k, c in sorted(s.get("by_kind", {}).items(), key=lambda x: -x[1]):
191
+ label = _strip_emoji(audit.KIND_LABELS.get(k, k))
192
+ out.append(f" {label:<28} {c}")
193
+ out.append("")
194
+
195
+ out.append("Chronology")
196
+ out.append("-" * 40)
197
+ for idx, e in enumerate(entries, 1):
198
+ ts = e.get("ts")
199
+ ts_str = dt.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else "-"
200
+ label = _strip_emoji(audit.KIND_LABELS.get(e.get("kind", ""), e.get("kind", "?")))
201
+ title = _strip_emoji(e.get("title", ""))
202
+ out.append(f"{idx}. [{label}] {title}")
203
+ out.append(f" {ts_str}")
204
+ details = e.get("details") or {}
205
+ for k, v in details.items():
206
+ if v is None or v == "":
207
+ continue
208
+ if isinstance(v, (list, dict)):
209
+ v = str(v)
210
+ v = str(v)
211
+ if len(v) > 160:
212
+ v = v[:160] + "..."
213
+ out.append(f" - {k}: {v}")
214
+ if e.get("snapshot"):
215
+ out.append(f" - snapshot: {str(e['snapshot']).split('/')[-1].split(chr(92))[-1]}")
216
+ out.append("")
217
+ return "\n".join(out)
218
+
219
+
220
+ # ─────────────────────────────────────────────────────────────────────────
221
+ # LLM metodoloji raporu için günlük digest'i
222
+ # ─────────────────────────────────────────────────────────────────────────
223
+
224
+ # Günlük kind → İngilizce aksiyon etiketi (LLM'in İngilizce yazması için)
225
+ _ACTION_EN = {
226
+ "project_create": "Project created",
227
+ "upload": "Source files uploaded",
228
+ "convert": "Raw files converted to tabular format",
229
+ "merge": "WoS/Scopus records merged and deduplicated",
230
+ "merge_borderline": "Borderline duplicate pairs reviewed manually",
231
+ "analysis_activate": "Active analysis selected",
232
+ "analysis_delete": "Analysis removed",
233
+ "filter_save": "Filter/screening preset defined",
234
+ "records_delete": "Records removed (screening/exclusion)",
235
+ "enrich_api": "Missing fields enriched via external APIs (e.g. Crossref/OpenAlex)",
236
+ "enrich_ml": "Missing fields enriched via machine-learning prediction",
237
+ "enrich_field": "Specific field enriched",
238
+ "enrich_selected": "Selected records enriched",
239
+ "enrich_selected_requested": "Selected records enriched",
240
+ "disambiguate": "Author/affiliation names disambiguated (normalization/splitting)",
241
+ "disambiguate_authors": "Author names disambiguated",
242
+ "disambiguate_affiliations": "Affiliation names standardized",
243
+ "snapshot_restore": "Dataset restored from a previous snapshot",
244
+ "export": "Prepared dataset exported",
245
+ }
246
+
247
+
248
+ def _clean_metrics(details: dict) -> dict:
249
+ """Digest için gürültüsüz metrik alt kümesi — sayılar + kısa değerler + küçük listeler."""
250
+ out: dict[str, Any] = {}
251
+ for k, v in (details or {}).items():
252
+ if v is None or v == "":
253
+ continue
254
+ if isinstance(v, bool) or isinstance(v, (int, float)):
255
+ out[k] = v
256
+ elif isinstance(v, str):
257
+ if len(v) <= 120:
258
+ out[k] = v
259
+ elif isinstance(v, list):
260
+ if len(v) <= 12 and all(isinstance(x, (str, int, float)) for x in v):
261
+ short = [x for x in v if not (isinstance(x, str) and len(x) > 80)]
262
+ if short:
263
+ out[k] = short
264
+ elif isinstance(v, dict):
265
+ if len(v) <= 12:
266
+ out[k] = v
267
+ return out
268
+
269
+
270
+ def build_log_digest(project_id: str) -> dict:
271
+ """LLM'e gönderilecek temiz, İngilizce-aksiyonlu kronolojik digest."""
272
+ meta = storage.get_project(project_id)
273
+ entries = audit.read(project_id)
274
+ steps: list[dict] = []
275
+ for e in entries:
276
+ kind = e.get("kind", "")
277
+ # job runner her job için completed/failed kaydı yazar; sadece anlamlı olanları al
278
+ ua = e.get("user_action") or ""
279
+ if ua.startswith("job_") and ua != "job_completed":
280
+ continue # failed/cancelled/queued/running ara kayıtları atla
281
+ ts = e.get("ts")
282
+ steps.append({
283
+ "ts": dt.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") if ts else None,
284
+ "kind": kind,
285
+ "action": _ACTION_EN.get(kind, kind.replace("_", " ")),
286
+ "metrics": _clean_metrics(e.get("details") or {}),
287
+ })
288
+ # Toplam kayıt sayısı (varsa) — bağlam için
289
+ total_records = None
290
+ try:
291
+ from services import filter_engine
292
+ df = filter_engine.load_merged(project_id)
293
+ total_records = int(len(df))
294
+ except Exception:
295
+ total_records = None
296
+ return {
297
+ "project_name": meta.name if meta else project_id,
298
+ "current_record_count": total_records,
299
+ "steps": steps,
300
+ }
@@ -0,0 +1,62 @@
1
+ """DataFrame -> RIS yazıcı."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ import pandas as pd
9
+
10
+
11
+ TYPE_MAP = {
12
+ "ARTICLE": "JOUR",
13
+ "REVIEW": "JOUR",
14
+ "CONFERENCE PAPER": "CPAPER",
15
+ "BOOK": "BOOK",
16
+ "BOOK CHAPTER": "CHAP",
17
+ "EDITORIAL": "JOUR",
18
+ }
19
+
20
+
21
+ def _ty(dt: str) -> str:
22
+ return TYPE_MAP.get(str(dt).upper().strip(), "JOUR")
23
+
24
+
25
+ def _split(value: str) -> list[str]:
26
+ return [v.strip() for v in re.split(r";", str(value)) if v.strip()]
27
+
28
+
29
+ def write_ris(df: pd.DataFrame, output: Path) -> Path:
30
+ with output.open("w", encoding="utf-8") as f:
31
+ for _, row in df.iterrows():
32
+ f.write(f"TY - {_ty(row.get('DT', '') or '')}\n")
33
+ for au in _split(row.get("AU", "")):
34
+ f.write(f"AU - {au}\n")
35
+ ti = str(row.get("TI", "") or "").strip()
36
+ if ti: f.write(f"TI - {ti}\n")
37
+ so = str(row.get("SO", "") or "").strip()
38
+ if so: f.write(f"JO - {so}\n")
39
+ ji = str(row.get("JI", "") or "").strip()
40
+ if ji: f.write(f"J2 - {ji}\n")
41
+ py = str(row.get("PY", "") or "").strip()
42
+ if py: f.write(f"PY - {py}\n")
43
+ vl = str(row.get("VL", "") or "").strip()
44
+ if vl: f.write(f"VL - {vl}\n")
45
+ issue = str(row.get("IS", "") or "").strip()
46
+ if issue: f.write(f"IS - {issue}\n")
47
+ pg = str(row.get("PG", "") or "").strip()
48
+ if pg: f.write(f"SP - {pg}\n")
49
+ di = str(row.get("DI", "") or "").strip()
50
+ if di: f.write(f"DO - {di}\n")
51
+ url = str(row.get("URL", "") or "").strip()
52
+ if url: f.write(f"UR - {url}\n")
53
+ ab = str(row.get("AB", "") or "").strip()
54
+ if ab: f.write(f"AB - {ab}\n")
55
+ for kw in _split(row.get("DE", "")):
56
+ f.write(f"KW - {kw}\n")
57
+ pu = str(row.get("PU", "") or "").strip()
58
+ if pu: f.write(f"PB - {pu}\n")
59
+ sn = str(row.get("SN", "") or "").strip()
60
+ if sn: f.write(f"SN - {sn}\n")
61
+ f.write("ER - \n\n")
62
+ return output