plokmii commited on
Commit
9b2a2fc
·
verified ·
1 Parent(s): 9f2d8c2

Upload sync.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. sync.py +174 -22
sync.py CHANGED
@@ -1,14 +1,17 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- 向同步:雲端 → Local
4
- 雲端 DB 為主,Local 只接收不上傳
5
- 用法: python sync.py
 
 
6
  """
7
 
8
  import configparser
9
  import sqlite3
10
  import shutil
11
  import os
 
12
  from pathlib import Path
13
  from datetime import datetime
14
 
@@ -48,7 +51,6 @@ def get_db_info(db_path):
48
 
49
  def download_cloud_db():
50
  from huggingface_hub import hf_hub_download
51
- import tempfile
52
  try:
53
  path = hf_hub_download(
54
  repo_id=REPO_ID, filename='pra_data.db',
@@ -73,7 +75,169 @@ def upload_db(db_path):
73
  )
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  def sync():
 
77
  print('=== 雲端 → Local 單向同步 ===\n')
78
 
79
  local_info = get_db_info(LOCAL_DB)
@@ -96,23 +260,7 @@ def sync():
96
  print('兩邊一致,不需要同步')
97
  return
98
 
99
- # 備份 Local(當天最多 5 個,總共最多 15 個)
100
- if LOCAL_DB.exists():
101
- BACKUP_DIR.mkdir(exist_ok=True)
102
- today = datetime.now().strftime('%Y%m%d')
103
- ts = datetime.now().strftime('%Y%m%d_%H%M%S')
104
- bk = BACKUP_DIR / f'pra_data_before_sync_{ts}.db'
105
- shutil.copy2(str(LOCAL_DB), str(bk))
106
- print(f'已備份 Local → {bk.name}')
107
-
108
- # 當天最多 5 個
109
- today_bks = sorted(BACKUP_DIR.glob(f'pra_data_before_sync_{today}_*.db'))
110
- for old in today_bks[:-5]:
111
- old.unlink()
112
- # 總共最多 15 個
113
- all_bks = sorted(BACKUP_DIR.glob('pra_data_before_sync_*.db'))
114
- for old in all_bks[:-15]:
115
- old.unlink()
116
 
117
  # 直接用雲端 DB 覆蓋 Local
118
  shutil.copy2(cloud_path, str(LOCAL_DB))
@@ -122,4 +270,8 @@ def sync():
122
 
123
 
124
  if __name__ == '__main__':
125
- sync()
 
 
 
 
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ 向同步:
4
+ - pull: 雲端 Local(雲端為主,覆蓋 Local)
5
+ - push: Local 雲端(Append-only merge,不刪雲端資料)
6
+ 用法: python sync.py # pull(預設)
7
+ python sync.py push # push
8
  """
9
 
10
  import configparser
11
  import sqlite3
12
  import shutil
13
  import os
14
+ import tempfile
15
  from pathlib import Path
16
  from datetime import datetime
17
 
 
51
 
52
  def download_cloud_db():
53
  from huggingface_hub import hf_hub_download
 
54
  try:
55
  path = hf_hub_download(
56
  repo_id=REPO_ID, filename='pra_data.db',
 
75
  )
76
 
77
 
78
+ def _backup_local(tag='sync'):
79
+ """備份 Local DB(當天最多 5 個,總共最多 15 個)"""
80
+ if not LOCAL_DB.exists():
81
+ return None
82
+ BACKUP_DIR.mkdir(exist_ok=True)
83
+ today = datetime.now().strftime('%Y%m%d')
84
+ ts = datetime.now().strftime('%Y%m%d_%H%M%S')
85
+ bk = BACKUP_DIR / f'pra_data_before_{tag}_{ts}.db'
86
+ shutil.copy2(str(LOCAL_DB), str(bk))
87
+ print(f'已備份 Local → {bk.name}')
88
+
89
+ today_bks = sorted(BACKUP_DIR.glob(f'pra_data_before_{tag}_{today}_*.db'))
90
+ for old in today_bks[:-5]:
91
+ old.unlink()
92
+ all_bks = sorted(BACKUP_DIR.glob(f'pra_data_before_{tag}_*.db'))
93
+ for old in all_bks[:-15]:
94
+ old.unlink()
95
+ return bk
96
+
97
+
98
+ def merge_local_into_cloud(local_path, cloud_path, merged_path):
99
+ """
100
+ Append-only merge:以雲端為底,把 Local 的新增/更新資料合併進去。
101
+ 絕不刪除雲端已有的 patients / reports / users。
102
+ """
103
+ shutil.copy2(cloud_path, merged_path)
104
+
105
+ merged = sqlite3.connect(str(merged_path))
106
+ merged.row_factory = sqlite3.Row
107
+ local = sqlite3.connect(str(local_path))
108
+ local.row_factory = sqlite3.Row
109
+
110
+ # --- patients: 以 chart_no 為 key,UPSERT ---
111
+ local_patients = local.execute('SELECT * FROM patients').fetchall()
112
+ for p in local_patients:
113
+ existing = merged.execute('SELECT id FROM patients WHERE chart_no=?', (p['chart_no'],)).fetchone()
114
+ if existing:
115
+ # 更新 patient_name, donor_hla(取較新的)
116
+ merged.execute('UPDATE patients SET patient_name=?, donor_hla=? WHERE chart_no=?',
117
+ (p['patient_name'], p['donor_hla'] or '', p['chart_no']))
118
+ else:
119
+ merged.execute(
120
+ 'INSERT INTO patients (patient_name, chart_no, created_at, donor_hla) VALUES (?,?,?,?)',
121
+ (p['patient_name'], p['chart_no'], p['created_at'], p['donor_hla'] or ''))
122
+
123
+ # 建立 chart_no → merged patient_id 對照
124
+ merged_patients = {r['chart_no']: r['id'] for r in merged.execute('SELECT id, chart_no FROM patients').fetchall()}
125
+ # 建立 local patient_id → chart_no 對照
126
+ local_pt_map = {r['id']: r['chart_no'] for r in local.execute('SELECT id, chart_no FROM patients').fetchall()}
127
+
128
+ # --- reports: 以 (patient chart_no + report_date + pra_class) 為 key ---
129
+ local_reports = local.execute('SELECT * FROM reports').fetchall()
130
+ for r in local_reports:
131
+ chart_no = local_pt_map.get(r['patient_id'])
132
+ if not chart_no:
133
+ continue
134
+ merged_pid = merged_patients.get(chart_no)
135
+ if not merged_pid:
136
+ continue
137
+
138
+ existing = merged.execute(
139
+ 'SELECT id, COALESCE(is_deleted,0) as is_deleted, updated_at FROM reports WHERE patient_id=? AND report_date=? AND pra_class=?',
140
+ (merged_pid, r['report_date'], r['pra_class'])
141
+ ).fetchone()
142
+
143
+ if existing:
144
+ # 雲端有此 report → 更新內容(但不把雲端 active 的改為 deleted)
145
+ cloud_deleted = existing['is_deleted']
146
+ local_deleted = r['is_deleted'] if r['is_deleted'] is not None else 0
147
+ # 如果 local 標記刪除但雲端沒有 → 保留雲端的 active 狀態
148
+ new_deleted = 0 if (cloud_deleted == 0 and local_deleted == 1) else local_deleted
149
+ merged.execute('''UPDATE reports SET pra_percent=?, overall=?, specificity=?, comment=?,
150
+ status=?, submitted_by=?, upload_file=?, updated_at=?,
151
+ is_deleted=?, deleted_at=? WHERE id=?''',
152
+ (r['pra_percent'], r['overall'], r['specificity'], r['comment'],
153
+ r['status'] or 'draft', r['submitted_by'] or '',
154
+ r['upload_file'] or '', r['updated_at'],
155
+ new_deleted, r['deleted_at'] if new_deleted else None,
156
+ existing['id']))
157
+ merged_rid = existing['id']
158
+ else:
159
+ # 雲端沒有 → 新增
160
+ cur = merged.execute(
161
+ '''INSERT INTO reports (patient_id, report_date, pra_class, pra_percent, overall,
162
+ specificity, comment, status, submitted_by, upload_file, created_at, updated_at,
163
+ is_deleted, deleted_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
164
+ (merged_pid, r['report_date'], r['pra_class'], r['pra_percent'], r['overall'],
165
+ r['specificity'], r['comment'], r['status'] or 'draft',
166
+ r['submitted_by'] or '', r['upload_file'] or '',
167
+ r['created_at'], r['updated_at'],
168
+ r['is_deleted'] if r['is_deleted'] is not None else 0, r['deleted_at']))
169
+ merged_rid = cur.lastrowid
170
+
171
+ # antibody_strength: 用 local 的資料覆蓋(屬於同一 report)
172
+ local_rid = r['id']
173
+ local_abs = local.execute('SELECT * FROM antibody_strength WHERE report_id=?', (local_rid,)).fetchall()
174
+ if local_abs:
175
+ merged.execute('DELETE FROM antibody_strength WHERE report_id=?', (merged_rid,))
176
+ for a in local_abs:
177
+ merged.execute(
178
+ '''INSERT INTO antibody_strength (report_id, antigen, allele, max_mfi, mean_mfi, no_of_beads, bead_ids)
179
+ VALUES (?,?,?,?,?,?,?)''',
180
+ (merged_rid, a['antigen'], a['allele'], a['max_mfi'], a['mean_mfi'],
181
+ a['no_of_beads'], a['bead_ids']))
182
+
183
+ # --- users: 以 username 為 key,只新增不刪 ---
184
+ local_users = local.execute('SELECT * FROM users').fetchall()
185
+ for u in local_users:
186
+ existing = merged.execute('SELECT id FROM users WHERE username=?', (u['username'],)).fetchone()
187
+ if existing:
188
+ # 更新 display_name, role, password(取 local 版本)
189
+ merged.execute('UPDATE users SET display_name=?, password_hash=?, password_plain=?, role=?, edit_time=? WHERE id=?',
190
+ (u['display_name'] or '', u['password_hash'], u['password_plain'] or '',
191
+ u['role'] or 'user', u['edit_time'], existing['id']))
192
+ else:
193
+ merged.execute(
194
+ 'INSERT INTO users (username, display_name, password_hash, password_plain, role, created_at, edit_time) VALUES (?,?,?,?,?,?,?)',
195
+ (u['username'], u['display_name'] or '', u['password_hash'],
196
+ u['password_plain'] or '', u['role'] or 'user', u['created_at'], u['edit_time']))
197
+
198
+ merged.commit()
199
+ local.close()
200
+ merged.close()
201
+
202
+
203
+ def push():
204
+ """Local → 雲端 (Append-only merge)"""
205
+ print('=== Local → 雲端 Append-only Push ===\n')
206
+
207
+ local_info = get_db_info(LOCAL_DB)
208
+ print(f'Local: {local_info["patients"]} 病患, {local_info["reports"]} 報告, {local_info["users"]} 使用者')
209
+
210
+ print('下載雲端 DB...')
211
+ cloud_path = download_cloud_db()
212
+ if not cloud_path:
213
+ print('無法取得雲端 DB,改為直接上傳 Local DB')
214
+ upload_db(LOCAL_DB)
215
+ print('已直接上傳')
216
+ return
217
+
218
+ cloud_info = get_db_info(cloud_path)
219
+ print(f'Cloud: {cloud_info["patients"]} 病患, {cloud_info["reports"]} 報告, {cloud_info["users"]} 使用者')
220
+ print()
221
+
222
+ # Merge
223
+ merged_path = Path(tempfile.gettempdir()) / 'pra_data_merged.db'
224
+ merge_local_into_cloud(str(LOCAL_DB), cloud_path, str(merged_path))
225
+
226
+ merged_info = get_db_info(merged_path)
227
+ print(f'Merged: {merged_info["patients"]} 病患, {merged_info["reports"]} 報告, {merged_info["users"]} 使用者')
228
+
229
+ # Upload merged
230
+ upload_db(merged_path)
231
+ print('\n已推送 merged DB 到雲端')
232
+
233
+ # 也更新 Local 為 merged 版本
234
+ _backup_local('push')
235
+ shutil.copy2(str(merged_path), str(LOCAL_DB))
236
+ print('Local DB 已同步為 merged 版本')
237
+
238
+
239
  def sync():
240
+ """雲端 → Local(pull,雲端為主)"""
241
  print('=== 雲端 → Local 單向同步 ===\n')
242
 
243
  local_info = get_db_info(LOCAL_DB)
 
260
  print('兩邊一致,不需要同步')
261
  return
262
 
263
+ _backup_local('sync')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  # 直接用雲端 DB 覆蓋 Local
266
  shutil.copy2(cloud_path, str(LOCAL_DB))
 
270
 
271
 
272
  if __name__ == '__main__':
273
+ import sys
274
+ if len(sys.argv) > 1 and sys.argv[1] == 'push':
275
+ push()
276
+ else:
277
+ sync()