TIF_E41253385/api_slearn/pulihkan_data_prodi.py

74 lines
4.2 KiB
Python

"""Targeted recovery. Stop backend first. Default is read-only inspection."""
import argparse
import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
EXPECTED = {'Manajemen Informatika', 'Manajemen Informatika Internasional',
'Teknik Komputer Internasional', 'Teknik Informatika Internasional'}
REASON = 'Input keliru: empat nama prodi digunakan sebagai ulasan. Tidak boleh diaktifkan.'
def repair(store, apply=False):
path = store / 'registry.sqlite3'
if not path.is_file():
raise ValueError('Registry tidak ditemukan.')
db = sqlite3.connect(path.as_uri() + ('?mode=rw' if apply else '?mode=ro'), uri=True)
db.row_factory = sqlite3.Row
try:
if apply:
db.execute('BEGIN IMMEDIATE')
versions = {r['id']: json.loads(r['metadata']) for r in db.execute('SELECT * FROM versions')}
if set(versions) != {'v1', 'v2', 'v3'}:
raise ValueError('Versi berbeda dari kondisi yang diperiksa. Pemulihan dibatalkan.')
if db.execute("SELECT COUNT(*) FROM jobs WHERE status IN ('queued','running')").fetchone()[0]:
raise ValueError('Masih ada training berjalan. Hentikan dan periksa dahulu.')
rows = [dict(r) for r in db.execute("SELECT * FROM additions WHERE version='v3'")]
count = db.execute('SELECT COUNT(*) FROM additions').fetchone()[0]
if not rows and versions['v3'].get('invalidated') and count == 49:
print('Sudah dipulihkan. Kumpulan data latih 505; v3 tidak valid.')
return
if len(rows) != 4 or {r['text'] for r in rows} != EXPECTED or count != 53:
raise ValueError('Data tidak cocok dengan empat prodi yang diperiksa. Tidak ada perubahan.')
if versions['v2']['jumlah_data_training'] != 505:
raise ValueError('Metadata v2 tidak sesuai.')
for name in ('model_logistic_regression.joblib', 'tfidf_vectorizer.joblib'):
if not (store / 'v2' / name).is_file():
raise ValueError('Berkas model v2 tidak lengkap.')
print('Terverifikasi: 4 nama prodi pada v3; 509 -> 505 data latih.')
if not apply:
print('Pemeriksaan saja. Belum ada data yang diubah. Jalankan dengan --apply untuk memulihkan.')
return
# Make an SQLite-consistent recovery copy before changing any rows.
backup = store.parent / ('registry_sebelum_pemulihan_' + datetime.now().strftime('%Y%m%d_%H%M%S_%f') + '.sqlite3')
source = sqlite3.connect(path.as_uri() + '?mode=ro', uri=True)
dest = sqlite3.connect(backup)
try:
source.backup(dest)
finally:
source.close(); dest.close()
db.execute('CREATE TABLE IF NOT EXISTS quarantined_additions (text_key TEXT PRIMARY KEY, record_json TEXT NOT NULL, reason TEXT NOT NULL, quarantined_at TEXT NOT NULL)')
stamp = datetime.now(timezone.utc).isoformat()
for row in rows:
db.execute('INSERT INTO quarantined_additions VALUES (?,?,?,?)', (row['text_key'], json.dumps(row), REASON, stamp))
db.execute('DELETE FROM additions WHERE text_key=? AND version=?', (row['text_key'], 'v3'))
versions['v3'].update(invalidated=True, invalid_reason=REASON, invalidated_at=stamp)
db.execute("UPDATE versions SET metadata=? WHERE id='v3'", (json.dumps(versions['v3']),))
db.execute("UPDATE settings SET value='v2' WHERE key='active'")
db.execute("UPDATE jobs SET message=? WHERE version='v3'", ('Model v3 tidak valid: input prodi. Empat data telah dikarantina; kumpulan training dipulihkan.',))
db.commit()
print('Selesai: 4 data dikarantina, kumpulan data latih 505, v2 aktif, v3 ditandai tidak valid.')
print('Berkas model dan riwayat analisis tidak dihapus. Cadangan tambahan:', backup)
finally:
db.close()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--store', type=Path, default=Path(__file__).resolve().parent / 'model_store')
parser.add_argument('--apply', action='store_true')
args = parser.parse_args()
try:
repair(args.store.resolve(), args.apply)
except (ValueError, sqlite3.Error) as error:
raise SystemExit(str(error))