plokmii commited on
Commit
e1f0232
·
verified ·
1 Parent(s): 852bfd1

Upload db.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. db.py +297 -0
db.py CHANGED
@@ -248,6 +248,42 @@ def init_db():
248
  CREATE INDEX IF NOT EXISTS idx_reports_patient ON reports(patient_id);
249
  CREATE INDEX IF NOT EXISTS idx_reports_date ON reports(report_date);
250
  CREATE INDEX IF NOT EXISTS idx_ab_report ON antibody_strength(report_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  ''')
252
  # migrate patients table
253
  pat_cols = [r[1] for r in conn.execute('PRAGMA table_info(patients)').fetchall()]
@@ -603,6 +639,267 @@ def get_mfi_comparison(chart_no, pra_class=None):
603
  return dates, antigens_out, pra_by_date, labels_by_rid
604
 
605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  def delete_report(report_id):
607
  """軟刪除報告(標記 is_deleted=1,不真的刪)。
608
  若 upload_file 不再被任何 active report 引用,同步刪除 uploads/ 下的檔案。"""
 
248
  CREATE INDEX IF NOT EXISTS idx_reports_patient ON reports(patient_id);
249
  CREATE INDEX IF NOT EXISTS idx_reports_date ON reports(report_date);
250
  CREATE INDEX IF NOT EXISTS idx_ab_report ON antibody_strength(report_id);
251
+
252
+ CREATE TABLE IF NOT EXISTS dsa_reports (
253
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
254
+ patient_id INTEGER NOT NULL,
255
+ report_date TEXT NOT NULL,
256
+ dsa_class TEXT NOT NULL,
257
+ pct_sa INTEGER NOT NULL,
258
+ overall TEXT NOT NULL,
259
+ specificity TEXT,
260
+ comment TEXT,
261
+ status TEXT DEFAULT 'draft',
262
+ submitted_by TEXT DEFAULT '',
263
+ upload_file TEXT DEFAULT '',
264
+ is_deleted INTEGER DEFAULT 0,
265
+ deleted_at TEXT,
266
+ created_at TEXT DEFAULT (datetime('now','localtime')),
267
+ updated_at TEXT DEFAULT (datetime('now','localtime')),
268
+ FOREIGN KEY (patient_id) REFERENCES patients(id)
269
+ );
270
+
271
+ CREATE TABLE IF NOT EXISTS dsa_antibody_strength (
272
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
273
+ report_id INTEGER NOT NULL,
274
+ antigen TEXT NOT NULL,
275
+ allele TEXT,
276
+ strength TEXT,
277
+ max_mfi REAL,
278
+ mean_mfi REAL,
279
+ no_of_beads INTEGER,
280
+ bead_ids TEXT,
281
+ FOREIGN KEY (report_id) REFERENCES dsa_reports(id) ON DELETE CASCADE
282
+ );
283
+
284
+ CREATE INDEX IF NOT EXISTS idx_dsa_reports_patient ON dsa_reports(patient_id);
285
+ CREATE INDEX IF NOT EXISTS idx_dsa_reports_date ON dsa_reports(report_date);
286
+ CREATE INDEX IF NOT EXISTS idx_dsa_ab_report ON dsa_antibody_strength(report_id);
287
  ''')
288
  # migrate patients table
289
  pat_cols = [r[1] for r in conn.execute('PRAGMA table_info(patients)').fetchall()]
 
639
  return dates, antigens_out, pra_by_date, labels_by_rid
640
 
641
 
642
+ # ============================================================
643
+ # DSA helpers (Single-Antigen / Donor-Specific Antibody)
644
+ # ============================================================
645
+
646
+ def find_active_duplicate_dsa(patient_id, report_date, dsa_class):
647
+ conn = get_conn()
648
+ row = conn.execute(
649
+ '''SELECT id FROM dsa_reports
650
+ WHERE patient_id=? AND report_date=? AND dsa_class=?
651
+ AND COALESCE(is_deleted,0)=0''',
652
+ (patient_id, report_date, dsa_class)
653
+ ).fetchone()
654
+ conn.close()
655
+ return row['id'] if row else None
656
+
657
+
658
+ def save_dsa_report(patient_id, report_date, dsa_class, pct_sa, overall,
659
+ specificity, comment, sero_mfi_list, status='draft',
660
+ submitted_by='', upload_file='', mode='overwrite'):
661
+ backup_db()
662
+ conn = get_conn()
663
+ existing = None
664
+ if mode == 'overwrite':
665
+ existing = conn.execute(
666
+ '''SELECT id FROM dsa_reports
667
+ WHERE patient_id=? AND report_date=? AND dsa_class=?
668
+ AND COALESCE(is_deleted,0)=0''',
669
+ (patient_id, report_date, dsa_class)
670
+ ).fetchone()
671
+
672
+ if existing:
673
+ report_id = existing['id']
674
+ conn.execute('''UPDATE dsa_reports SET pct_sa=?, overall=?, specificity=?, comment=?,
675
+ status=?, submitted_by=?, upload_file=?,
676
+ updated_at=datetime('now','localtime') WHERE id=?''',
677
+ (pct_sa, overall, specificity, comment, status, submitted_by,
678
+ upload_file or '', report_id))
679
+ conn.execute('DELETE FROM dsa_antibody_strength WHERE report_id=?', (report_id,))
680
+ else:
681
+ cur = conn.execute(
682
+ '''INSERT INTO dsa_reports (patient_id, report_date, dsa_class, pct_sa, overall,
683
+ specificity, comment, status, submitted_by, upload_file)
684
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
685
+ (patient_id, report_date, dsa_class, pct_sa, overall, specificity, comment,
686
+ status, submitted_by, upload_file or ''))
687
+ report_id = cur.lastrowid
688
+
689
+ for m in sero_mfi_list:
690
+ conn.execute(
691
+ '''INSERT INTO dsa_antibody_strength
692
+ (report_id, antigen, allele, strength, max_mfi, mean_mfi, no_of_beads, bead_ids)
693
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
694
+ (report_id, m.get('sero', ''), m.get('alleles', ''),
695
+ m.get('strength', ''), m.get('max_mfi', 0), m.get('mean_mfi', 0),
696
+ m.get('count', 0), m.get('beads', '')))
697
+
698
+ conn.commit()
699
+ conn.close()
700
+ schedule_auto_push()
701
+ return report_id
702
+
703
+
704
+ def get_all_dsa_reports(limit=200):
705
+ conn = get_conn()
706
+ rows = conn.execute('''
707
+ SELECT r.*, p.patient_name, p.chart_no, p.donor_hla
708
+ FROM dsa_reports r JOIN patients p ON p.id = r.patient_id
709
+ WHERE COALESCE(r.is_deleted, 0) = 0
710
+ ORDER BY p.patient_name, p.chart_no, r.report_date DESC, r.dsa_class
711
+ LIMIT ?
712
+ ''', (limit,)).fetchall()
713
+ conn.close()
714
+ return [dict(r) for r in rows]
715
+
716
+
717
+ def get_all_dsa_patients():
718
+ conn = get_conn()
719
+ rows = conn.execute('''
720
+ SELECT p.id, p.patient_name, p.chart_no,
721
+ COUNT(r.id) as report_count,
722
+ MAX(r.report_date) as latest_date,
723
+ GROUP_CONCAT(DISTINCT r.dsa_class) as classes
724
+ FROM patients p
725
+ LEFT JOIN dsa_reports r ON r.patient_id = p.id AND COALESCE(r.is_deleted, 0) = 0
726
+ GROUP BY p.id
727
+ HAVING report_count > 0
728
+ ORDER BY latest_date DESC
729
+ ''').fetchall()
730
+ conn.close()
731
+ return [dict(r) for r in rows]
732
+
733
+
734
+ def get_dsa_patient_reports(chart_no):
735
+ conn = get_conn()
736
+ patient = conn.execute('SELECT * FROM patients WHERE chart_no=?', (chart_no,)).fetchone()
737
+ if not patient:
738
+ conn.close()
739
+ return None, []
740
+ reports = conn.execute('''
741
+ SELECT * FROM dsa_reports WHERE patient_id=? AND COALESCE(is_deleted,0)=0
742
+ ORDER BY report_date DESC, dsa_class
743
+ ''', (patient['id'],)).fetchall()
744
+ out = []
745
+ for r in reports:
746
+ ab = conn.execute('SELECT * FROM dsa_antibody_strength WHERE report_id=? ORDER BY id',
747
+ (r['id'],)).fetchall()
748
+ out.append({**dict(r), 'antibodies': [dict(a) for a in ab]})
749
+ conn.close()
750
+ return dict(patient), out
751
+
752
+
753
+ def get_dsa_mfi_comparison(chart_no, dsa_class=None):
754
+ """Same shape as PRA's get_mfi_comparison but reads dsa_reports."""
755
+ conn = get_conn()
756
+ patient = conn.execute('SELECT id FROM patients WHERE chart_no=?', (chart_no,)).fetchone()
757
+ if not patient:
758
+ conn.close()
759
+ return [], [], {}, {}
760
+
761
+ q = '''SELECT id, report_date, pct_sa, specificity FROM dsa_reports
762
+ WHERE patient_id = ? AND COALESCE(is_deleted, 0) = 0'''
763
+ params = [patient['id']]
764
+ if dsa_class:
765
+ q += ' AND dsa_class = ?'
766
+ params.append(dsa_class)
767
+ q += ' ORDER BY report_date, id'
768
+ reports = conn.execute(q, params).fetchall()
769
+
770
+ from collections import defaultdict
771
+ date_count = defaultdict(int)
772
+ for r in reports:
773
+ date_count[r['report_date']] += 1
774
+ date_seen = defaultdict(int)
775
+ labels_by_rid = {}
776
+ dates = []
777
+ pct_by_date = {}
778
+ for r in reports:
779
+ d = r['report_date']
780
+ if date_count[d] == 1:
781
+ label = d
782
+ else:
783
+ suffix = chr(ord('A') + date_seen[d])
784
+ label = f'{d} ({suffix})'
785
+ date_seen[d] += 1
786
+ labels_by_rid[r['id']] = label
787
+ dates.append(label)
788
+ pct_by_date[label] = r['pct_sa']
789
+
790
+ if not reports:
791
+ conn.close()
792
+ return [], [], {}, {}
793
+
794
+ rep_ids = [r['id'] for r in reports]
795
+ placeholders = ','.join('?' * len(rep_ids))
796
+ ab_rows = conn.execute(f'''
797
+ SELECT report_id, antigen, allele, max_mfi, mean_mfi, no_of_beads
798
+ FROM dsa_antibody_strength
799
+ WHERE report_id IN ({placeholders})
800
+ ORDER BY antigen
801
+ ''', rep_ids).fetchall()
802
+ conn.close()
803
+
804
+ import re as _re
805
+ parsed = {}
806
+ for r in reports:
807
+ spec = r['specificity'] or ''
808
+ sa_map = {}
809
+ bare = set()
810
+ for mt in _re.finditer(r'([A-Za-z][A-Za-z0-9]*)\(([^)]*)\)', spec):
811
+ s = mt.group(1).strip()
812
+ for a in mt.group(2).split():
813
+ a = a.strip()
814
+ if a:
815
+ sa_map.setdefault(s, set()).add(a)
816
+ no_parens = _re.sub(r'\([^)]*\)', ' ', spec)
817
+ for tok in no_parens.split():
818
+ tok = tok.strip()
819
+ if '*' in tok and ':' in tok:
820
+ bare.add(tok)
821
+ parsed[r['id']] = {'sero_alleles': sa_map, 'bare': bare}
822
+
823
+ allele_to_sero = {}
824
+ for p in parsed.values():
825
+ for s, alleles in p['sero_alleles'].items():
826
+ for a in alleles:
827
+ allele_to_sero[a] = s
828
+
829
+ ab_map = {}
830
+ for ab in ab_rows:
831
+ ab_map[(ab['report_id'], ab['antigen'])] = dict(ab)
832
+
833
+ all_alleles = set()
834
+ for p in parsed.values():
835
+ for alleles in p['sero_alleles'].values():
836
+ all_alleles.update(alleles)
837
+ all_alleles.update(p['bare'])
838
+
839
+ def sk(a):
840
+ order = {'A': 0, 'B': 1, 'C': 2, 'Cw': 2,
841
+ 'DR': 3, 'DRB1': 3, 'DRB3': 3, 'DRB4': 3, 'DRB5': 3,
842
+ 'DQ': 4, 'DQB1': 4, 'DQA1': 5,
843
+ 'DP': 6, 'DPB1': 6, 'DPA1': 7}
844
+ loc = a.split('*')[0] if '*' in a else (_re.match(r'[A-Za-z]+', a) or _re.match(r'.', a)).group()
845
+ nums = _re.findall(r'\d+', a)
846
+ n1 = int(nums[0]) if nums else 0
847
+ n2 = int(nums[1]) if len(nums) > 1 else 0
848
+ return (order.get(loc, 99), n1, n2, a)
849
+
850
+ out = []
851
+ for allele in sorted(all_alleles, key=sk):
852
+ sero = allele_to_sero.get(allele)
853
+ mfi_by_date = {}
854
+ for r in reports:
855
+ rid = r['id']
856
+ lbl = labels_by_rid[rid]
857
+ p = parsed[rid]
858
+ present = allele in p['sero_alleles'].get(sero, set()) if sero else allele in p['bare']
859
+ if not present:
860
+ continue
861
+ ab = ab_map.get((rid, sero)) if sero else None
862
+ if not ab:
863
+ ab = ab_map.get((rid, allele))
864
+ if ab:
865
+ mfi_by_date[lbl] = {
866
+ 'max_mfi': ab['max_mfi'],
867
+ 'mean_mfi': ab['mean_mfi'],
868
+ 'no_of_beads': ab['no_of_beads'],
869
+ }
870
+ if not mfi_by_date:
871
+ continue
872
+ out.append({'antigen': sero if sero else allele, 'allele': allele,
873
+ 'mfi_by_date': mfi_by_date})
874
+ return dates, out, pct_by_date, labels_by_rid
875
+
876
+
877
+ def delete_dsa_report(report_id):
878
+ backup_db()
879
+ conn = get_conn()
880
+ row = conn.execute('SELECT upload_file FROM dsa_reports WHERE id=?', (report_id,)).fetchone()
881
+ upload_file = row['upload_file'] if row and row['upload_file'] else ''
882
+ conn.execute("""UPDATE dsa_reports SET is_deleted=1, deleted_at=datetime('now','localtime'),
883
+ updated_at=datetime('now','localtime') WHERE id=?""", (report_id,))
884
+ conn.commit()
885
+ if upload_file:
886
+ still_used = conn.execute(
887
+ 'SELECT COUNT(*) FROM dsa_reports WHERE upload_file=? AND id!=? AND COALESCE(is_deleted,0)=0',
888
+ (upload_file, report_id)
889
+ ).fetchone()[0]
890
+ # Also check PRA reports
891
+ still_used += conn.execute(
892
+ 'SELECT COUNT(*) FROM reports WHERE upload_file=? AND COALESCE(is_deleted,0)=0',
893
+ (upload_file,)
894
+ ).fetchone()[0]
895
+ conn.close()
896
+ if still_used == 0:
897
+ delete_upload(upload_file)
898
+ else:
899
+ conn.close()
900
+ schedule_auto_push()
901
+
902
+
903
  def delete_report(report_id):
904
  """軟刪除報告(標記 is_deleted=1,不真的刪)。
905
  若 upload_file 不再被任何 active report 引用,同步刪除 uploads/ 下的檔案。"""