Files
airflow-coolify/scripts/edoxid_calendar_state_store.py
T
Power BI Dev a357e0622e throttle polling outside business hours
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.
2026-07-27 22:33:04 +07:00

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}