plokmii commited on
Commit
b1f6e79
·
verified ·
1 Parent(s): f054eeb

Upload sync.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. sync.py +264 -0
sync.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 雙向同步 Local ↔ HF Space 資料庫
4
+ 智慧合併:兩邊都留最多最新的,重複的以最新為主
5
+ 用法: python sync.py
6
+ """
7
+
8
+ import configparser
9
+ import sqlite3
10
+ import shutil
11
+ import tempfile
12
+ import os
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+
16
+ cfg = configparser.ConfigParser()
17
+ cfg.read('Myconfig.ini')
18
+ TOKEN = cfg['Hugging Face']['Token'].strip()
19
+ USERNAME = cfg['Hugging Face']['Username'].strip()
20
+ REPO_ID = f'{USERNAME}/pradsa'
21
+
22
+ LOCAL_DB = Path('pra_data.db')
23
+ BACKUP_DIR = LOCAL_DB.parent / 'backups'
24
+
25
+
26
+ def get_db_info(db_path):
27
+ if not Path(db_path).exists():
28
+ return {'patients': 0, 'reports': 0, 'users': 0, 'latest': ''}
29
+ conn = sqlite3.connect(str(db_path))
30
+ conn.row_factory = sqlite3.Row
31
+ try:
32
+ patients = conn.execute('SELECT COUNT(*) as c FROM patients').fetchone()['c']
33
+ reports = conn.execute('SELECT COUNT(*) as c FROM reports').fetchone()['c']
34
+ users = conn.execute('SELECT COUNT(*) as c FROM users').fetchone()['c']
35
+ latest = conn.execute(
36
+ "SELECT MAX(COALESCE(updated_at, created_at)) as t FROM reports"
37
+ ).fetchone()['t'] or ''
38
+ except Exception:
39
+ patients, reports, users, latest = 0, 0, 0, ''
40
+ conn.close()
41
+ return {'patients': patients, 'reports': reports, 'users': users, 'latest': latest}
42
+
43
+
44
+ def download_cloud_db():
45
+ from huggingface_hub import hf_hub_download
46
+ try:
47
+ path = hf_hub_download(
48
+ repo_id=REPO_ID, filename='pra_data.db',
49
+ repo_type='space', token=TOKEN,
50
+ local_dir=tempfile.gettempdir(),
51
+ force_download=True,
52
+ )
53
+ return path
54
+ except Exception as e:
55
+ print(f' 雲端 DB 下載失敗: {e}')
56
+ return None
57
+
58
+
59
+ def upload_db(db_path):
60
+ from huggingface_hub import HfApi
61
+ api = HfApi(token=TOKEN)
62
+ api.upload_file(
63
+ path_or_fileobj=str(db_path),
64
+ path_in_repo='pra_data.db',
65
+ repo_id=REPO_ID,
66
+ repo_type='space',
67
+ )
68
+
69
+
70
+ def backup_local():
71
+ if LOCAL_DB.exists():
72
+ BACKUP_DIR.mkdir(exist_ok=True)
73
+ ts = datetime.now().strftime('%Y%m%d_%H%M%S')
74
+ dst = BACKUP_DIR / f'pra_data_before_sync_{ts}.db'
75
+ shutil.copy2(str(LOCAL_DB), str(dst))
76
+ return dst
77
+ return None
78
+
79
+
80
+ def merge_dbs(local_path, cloud_path):
81
+ """
82
+ 智慧合併兩個 DB。
83
+ - patients: 以 chart_no 為 key,兩邊都留
84
+ - reports: 以 (chart_no, report_date, pra_class) 為 key
85
+ 重複的保留 updated_at/created_at 較新的
86
+ - antibody_strength: 跟著 report 走
87
+ - users: 以 username 為 key,兩邊都留
88
+ 回傳合併後的 DB 路徑
89
+ """
90
+ merged_path = str(Path(tempfile.gettempdir()) / 'pra_merged.db')
91
+ shutil.copy2(str(local_path), merged_path)
92
+
93
+ local = sqlite3.connect(merged_path)
94
+ local.row_factory = sqlite3.Row
95
+ cloud = sqlite3.connect(str(cloud_path))
96
+ cloud.row_factory = sqlite3.Row
97
+
98
+ stats = {'patients_added': 0, 'reports_added': 0, 'reports_updated': 0, 'users_added': 0}
99
+
100
+ # --- Merge patients ---
101
+ cloud_patients = cloud.execute('SELECT * FROM patients').fetchall()
102
+ for cp in cloud_patients:
103
+ existing = local.execute('SELECT id FROM patients WHERE chart_no=?', (cp['chart_no'],)).fetchone()
104
+ if not existing:
105
+ local.execute(
106
+ 'INSERT INTO patients (patient_name, chart_no, donor_hla, created_at) VALUES (?,?,?,?)',
107
+ (cp['patient_name'], cp['chart_no'], cp['donor_hla'] or '', cp['created_at']))
108
+ stats['patients_added'] += 1
109
+ else:
110
+ # Update donor_hla if cloud has it and local doesn't
111
+ local_donor = local.execute('SELECT donor_hla FROM patients WHERE id=?', (existing['id'],)).fetchone()
112
+ if cp['donor_hla'] and not (local_donor and local_donor['donor_hla']):
113
+ local.execute('UPDATE patients SET donor_hla=? WHERE id=?', (cp['donor_hla'], existing['id']))
114
+
115
+ # --- Merge reports ---
116
+ # Build chart_no -> local patient_id mapping
117
+ local_patients = {r['chart_no']: r['id'] for r in local.execute('SELECT id, chart_no FROM patients').fetchall()}
118
+
119
+ cloud_reports = cloud.execute('''
120
+ SELECT r.*, p.chart_no FROM reports r JOIN patients p ON p.id = r.patient_id
121
+ ''').fetchall()
122
+
123
+ for cr in cloud_reports:
124
+ chart_no = cr['chart_no']
125
+ local_pid = local_patients.get(chart_no)
126
+ if not local_pid:
127
+ continue
128
+
129
+ # Check if same report exists locally
130
+ existing = local.execute(
131
+ 'SELECT id, COALESCE(updated_at, created_at) as ts FROM reports WHERE patient_id=? AND report_date=? AND pra_class=?',
132
+ (local_pid, cr['report_date'], cr['pra_class'])
133
+ ).fetchone()
134
+
135
+ cloud_ts = cr['updated_at'] or cr['created_at'] or ''
136
+
137
+ if not existing:
138
+ # Cloud has a report local doesn't -> add
139
+ cur = local.execute(
140
+ '''INSERT INTO reports (patient_id, report_date, pra_class, pra_percent, overall,
141
+ specificity, comment, status, submitted_by, created_at, updated_at)
142
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)''',
143
+ (local_pid, cr['report_date'], cr['pra_class'], cr['pra_percent'],
144
+ cr['overall'], cr['specificity'], cr['comment'], cr['status'] or 'draft',
145
+ cr['submitted_by'] or '', cr['created_at'], cr['updated_at']))
146
+ new_rid = cur.lastrowid
147
+ # Copy antibody_strength
148
+ cloud_abs = cloud.execute('SELECT * FROM antibody_strength WHERE report_id=?', (cr['id'],)).fetchall()
149
+ for ab in cloud_abs:
150
+ local.execute(
151
+ '''INSERT INTO antibody_strength (report_id, antigen, allele, max_mfi, mean_mfi, no_of_beads, bead_ids)
152
+ VALUES (?,?,?,?,?,?,?)''',
153
+ (new_rid, ab['antigen'], ab['allele'], ab['max_mfi'], ab['mean_mfi'], ab['no_of_beads'], ab['bead_ids']))
154
+ stats['reports_added'] += 1
155
+
156
+ elif cloud_ts > (existing['ts'] or ''):
157
+ # Cloud is newer -> update
158
+ rid = existing['id']
159
+ local.execute(
160
+ '''UPDATE reports SET pra_percent=?, overall=?, specificity=?, comment=?,
161
+ status=?, submitted_by=?, updated_at=? WHERE id=?''',
162
+ (cr['pra_percent'], cr['overall'], cr['specificity'], cr['comment'],
163
+ cr['status'] or 'draft', cr['submitted_by'] or '', cr['updated_at'], rid))
164
+ # Replace antibody_strength
165
+ local.execute('DELETE FROM antibody_strength WHERE report_id=?', (rid,))
166
+ cloud_abs = cloud.execute('SELECT * FROM antibody_strength WHERE report_id=?', (cr['id'],)).fetchall()
167
+ for ab in cloud_abs:
168
+ local.execute(
169
+ '''INSERT INTO antibody_strength (report_id, antigen, allele, max_mfi, mean_mfi, no_of_beads, bead_ids)
170
+ VALUES (?,?,?,?,?,?,?)''',
171
+ (rid, ab['antigen'], ab['allele'], ab['max_mfi'], ab['mean_mfi'], ab['no_of_beads'], ab['bead_ids']))
172
+ stats['reports_updated'] += 1
173
+
174
+ # --- Merge users ---
175
+ stats['users_updated'] = 0
176
+ cloud_users = cloud.execute('SELECT * FROM users').fetchall()
177
+ for cu in cloud_users:
178
+ existing = local.execute('SELECT id, COALESCE(edit_time, created_at) as ts FROM users WHERE username=?',
179
+ (cu['username'],)).fetchone()
180
+ cloud_ts = ''
181
+ try:
182
+ cloud_ts = cu['edit_time'] or cu['created_at'] or ''
183
+ except (IndexError, KeyError):
184
+ cloud_ts = cu['created_at'] if 'created_at' in cu.keys() else ''
185
+
186
+ if not existing:
187
+ # Cloud only -> add
188
+ cols = ['username', 'display_name', 'password_hash', 'password_plain', 'role', 'created_at', 'edit_time']
189
+ vals = []
190
+ for c in cols:
191
+ try:
192
+ vals.append(cu[c] or '')
193
+ except (IndexError, KeyError):
194
+ vals.append('')
195
+ local.execute(
196
+ f'INSERT INTO users ({",".join(cols)}) VALUES ({",".join(["?"]*len(cols))})', vals)
197
+ stats['users_added'] += 1
198
+ elif cloud_ts > (existing['ts'] or ''):
199
+ # Cloud is newer -> update
200
+ local.execute(
201
+ '''UPDATE users SET display_name=?, password_hash=?, password_plain=?, role=?, edit_time=?
202
+ WHERE id=?''',
203
+ (cu['display_name'] or '', cu['password_hash'], cu['password_plain'] or '',
204
+ cu['role'] or 'user', cloud_ts, existing['id']))
205
+ stats['users_updated'] += 1
206
+
207
+ local.commit()
208
+ local.close()
209
+ cloud.close()
210
+
211
+ return merged_path, stats
212
+
213
+
214
+ def sync():
215
+ print('=== PRA DB Sync ===\n')
216
+
217
+ local_info = get_db_info(LOCAL_DB)
218
+ print(f'Local: {local_info["patients"]} patients, {local_info["reports"]} reports, {local_info["users"]} users, latest: {local_info["latest"] or "(none)"}')
219
+
220
+ print('Downloading cloud DB...')
221
+ cloud_path = download_cloud_db()
222
+ if not cloud_path:
223
+ print('無法取得雲端 DB')
224
+ return
225
+
226
+ cloud_info = get_db_info(cloud_path)
227
+ print(f'Cloud: {cloud_info["patients"]} patients, {cloud_info["reports"]} reports, {cloud_info["users"]} users, latest: {cloud_info["latest"] or "(none)"}')
228
+ print()
229
+
230
+ if (local_info['latest'] == cloud_info['latest']
231
+ and local_info['reports'] == cloud_info['reports']
232
+ and local_info['patients'] == cloud_info['patients']
233
+ and local_info['users'] == cloud_info['users']):
234
+ print('兩邊一致,不需要同步')
235
+ return
236
+
237
+ # Merge
238
+ print('合併中...')
239
+ bk = backup_local()
240
+ if bk:
241
+ print(f' 已備份 Local → {bk.name}')
242
+
243
+ merged_path, stats = merge_dbs(LOCAL_DB, cloud_path)
244
+
245
+ print(f' 新增 patients: {stats["patients_added"]}')
246
+ print(f' 新增 reports: {stats["reports_added"]}')
247
+ print(f' 更新 reports: {stats["reports_updated"]}')
248
+ print(f' 新增 users: {stats["users_added"]}')
249
+ print(f' 更新 users: {stats["users_updated"]}')
250
+
251
+ # Replace local with merged
252
+ shutil.copy2(merged_path, str(LOCAL_DB))
253
+ merged_info = get_db_info(LOCAL_DB)
254
+ print(f'\nMerged: {merged_info["patients"]} patients, {merged_info["reports"]} reports, {merged_info["users"]} users')
255
+
256
+ # Upload merged to cloud
257
+ print('上傳合併結果到雲端...')
258
+ upload_db(LOCAL_DB)
259
+
260
+ print('\n同步完成')
261
+
262
+
263
+ if __name__ == '__main__':
264
+ sync()