e0a6efd204
apache/airflow:2.7.1 base image ships Python 3.8, which doesn't have zoneinfo (3.9+) and can't evaluate `X | Y` / list[X] type hints at runtime (3.10+). Add `from __future__ import annotations` everywhere and swap zoneinfo -> pytz (already a dependency in this repo).
101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime
|
|
|
|
|
|
class StateStore:
|
|
"""State lokal (SQLite) tracking submission_id -> event_id + content_hash.
|
|
Sengaja TIDAK di doxid2022 — kalau file ini hilang/reset, paling buruk
|
|
event lama ke-recreate di Calendar, tidak merusak apa pun di DB eDOXID."""
|
|
|
|
def __init__(self, path: str):
|
|
self._conn = sqlite3.connect(path)
|
|
self._conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS sync_state (
|
|
submission_id INTEGER PRIMARY KEY,
|
|
event_id TEXT NOT NULL,
|
|
content_hash TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
notified_h1 INTEGER NOT NULL DEFAULT 0,
|
|
notified_1h INTEGER NOT NULL DEFAULT 0
|
|
)
|
|
"""
|
|
)
|
|
self._conn.commit()
|
|
|
|
def close(self) -> None:
|
|
self._conn.close()
|
|
|
|
@staticmethod
|
|
def compute_hash(submission: dict) -> str:
|
|
payload = json.dumps(
|
|
[
|
|
str(submission["schedule"]),
|
|
submission["room"],
|
|
submission["title"],
|
|
submission["type_id"],
|
|
submission["student_email"],
|
|
submission["student_nim"],
|
|
submission["verificators"],
|
|
],
|
|
sort_keys=True,
|
|
default=str,
|
|
)
|
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
|
|
def find(self, submission_id: int) -> dict | None:
|
|
row = self._conn.execute(
|
|
"SELECT event_id, content_hash FROM sync_state WHERE submission_id = ?",
|
|
(submission_id,),
|
|
).fetchone()
|
|
if row is None:
|
|
return None
|
|
return {"event_id": row[0], "content_hash": row[1]}
|
|
|
|
def save(self, submission_id: int, event_id: str, content_hash: str) -> None:
|
|
self._conn.execute(
|
|
"""
|
|
INSERT INTO sync_state (submission_id, event_id, content_hash, updated_at)
|
|
VALUES (?, ?, ?, ?)
|
|
""",
|
|
(submission_id, event_id, content_hash, datetime.utcnow().isoformat()),
|
|
)
|
|
self._conn.commit()
|
|
|
|
def update_hash(self, submission_id: int, content_hash: str) -> None:
|
|
self._conn.execute(
|
|
"UPDATE sync_state SET content_hash = ?, updated_at = ? WHERE submission_id = ?",
|
|
(content_hash, datetime.utcnow().isoformat(), submission_id),
|
|
)
|
|
self._conn.commit()
|
|
|
|
def remove(self, submission_id: int) -> None:
|
|
self._conn.execute("DELETE FROM sync_state WHERE submission_id = ?", (submission_id,))
|
|
self._conn.commit()
|
|
|
|
def is_notified(self, submission_id: int, which: str) -> bool:
|
|
column = "notified_h1" if which == "h1" else "notified_1h"
|
|
row = self._conn.execute(
|
|
f"SELECT {column} FROM sync_state WHERE submission_id = ?", (submission_id,)
|
|
).fetchone()
|
|
return bool(row and row[0])
|
|
|
|
def mark_notified(self, submission_id: int, which: str) -> None:
|
|
column = "notified_h1" if which == "h1" else "notified_1h"
|
|
self._conn.execute(
|
|
f"UPDATE sync_state SET {column} = 1 WHERE submission_id = ?", (submission_id,)
|
|
)
|
|
self._conn.commit()
|
|
|
|
def find_orphaned(self, active_submission_ids: set[int]) -> dict[int, str]:
|
|
"""Submission yang tercatat di state tapi schedule-nya sudah di-null-kan
|
|
(dibatalkan) atau baris submission-nya sudah dihapus — bukan sekadar
|
|
tanggalnya lewat. `active_submission_ids` harus dari query TANPA filter
|
|
tanggal (lihat Database.get_ids_with_active_schedule)."""
|
|
rows = self._conn.execute("SELECT submission_id, event_id FROM sync_state").fetchall()
|
|
return {sid: eid for sid, eid in rows if sid not in active_submission_ids}
|