a357e0622e
Single DAG, single 30-min schedule instead of 5-min or two separate DAGs. run_sync() itself decides whether to do real work: always during business hours (default 07:00-17:00 WIB), otherwise only once per EDOXID_OFF_HOURS_INTERVAL_MINUTES (default 120) tracked via a last_full_sync timestamp in the sqlite state. Off-hours skips return immediately without touching DB/Calendar/Gmail at all.
118 lines
4.4 KiB
Python
118 lines
4.4 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.execute(
|
|
"CREATE TABLE IF NOT EXISTS sync_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
|
|
)
|
|
self._conn.commit()
|
|
|
|
def get_last_full_sync(self) -> datetime | None:
|
|
row = self._conn.execute(
|
|
"SELECT value FROM sync_meta WHERE key = 'last_full_sync'"
|
|
).fetchone()
|
|
return datetime.fromisoformat(row[0]) if row else None
|
|
|
|
def set_last_full_sync(self, when: datetime) -> None:
|
|
self._conn.execute(
|
|
"INSERT INTO sync_meta (key, value) VALUES ('last_full_sync', ?) "
|
|
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
(when.isoformat(),),
|
|
)
|
|
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}
|