Files
airflow-coolify/scripts/edoxid_calendar_google.py
T
Power BI Dev 7a6dc7d9ae fix invalid_scope error on token refresh
Requesting a narrower scope explicitly (scopes=[...]) during refresh
was rejected by Google as invalid_scope in the Airflow container's
google-auth version, even though it worked locally with a newer one.
Drop scopes= entirely — the refresh_token already carries the full
granted scope (calendar + gmail.send), no need to request a subset.
2026-07-27 22:16:31 +07:00

192 lines
7.1 KiB
Python

from __future__ import annotations
from datetime import datetime, timedelta
import pytz
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from scripts.edoxid_calendar_config import GoogleConfig
JAKARTA_TZ = pytz.timezone("Asia/Jakarta")
TYPE_LABELS = {
1: "Seminar Proposal",
2: "Seminar Hasil",
3: "Sidang Akhir",
}
DAY_NAMES = {
"Monday": "Senin", "Tuesday": "Selasa", "Wednesday": "Rabu", "Thursday": "Kamis",
"Friday": "Jumat", "Saturday": "Sabtu", "Sunday": "Minggu",
}
MONTH_NAMES = {
"January": "Januari", "February": "Februari", "March": "Maret", "April": "April",
"May": "Mei", "June": "Juni", "July": "Juli", "August": "Agustus",
"September": "September", "October": "Oktober", "November": "November", "December": "Desember",
}
# --- Fungsi format, dipakai bareng oleh event Calendar & email reminder (edoxid_calendar_gmail_reminder.py) ---
def get_schedule_datetime(submission: dict) -> datetime:
schedule = submission["schedule"]
if isinstance(schedule, str):
schedule = datetime.fromisoformat(schedule)
if schedule.tzinfo is not None:
return schedule
return JAKARTA_TZ.localize(schedule)
def get_type_label(submission: dict) -> str:
return TYPE_LABELS.get(submission["type_id"], "Sidang")
def format_indonesian_date(date: datetime) -> str:
day = DAY_NAMES.get(date.strftime("%A"), date.strftime("%A"))
month = MONTH_NAMES.get(date.strftime("%B"), date.strftime("%B"))
return f"{day}, {date.strftime('%d')} {month} {date.strftime('%Y')}"
def build_description(submission: dict) -> str:
schedule = get_schedule_datetime(submission)
type_label = get_type_label(submission)
pembimbing = [v["name"] for v in submission["verificators"] if v["type"] == "Pembimbing"]
penguji = [v["name"] for v in submission["verificators"] if v["type"] == "Penguji"]
lines = [
f"UNDANGAN {type_label.upper()} SECARA LURING",
"JURUSAN INFORMATIKA",
"",
"Mengundang Bapak dan Ibu dosen tim pembimbing dan penguji untuk hadir pada "
f"{type_label.lower()} mahasiswa yang dilaksanakan secara Luring.",
"",
f"👨‍🎓 {submission['student_name']} - {submission['student_nim']}",
"",
f"📖 {submission['title']}",
"",
f"📅 {format_indonesian_date(schedule)}",
f"🕑 {schedule.strftime('%H:%M')} WIB",
"",
]
for i, name in enumerate(pembimbing, start=1):
lines.append(f"📕 Pembimbing {i} : {name}")
for i, name in enumerate(penguji, start=1):
lines.append(f"📒 Penguji {i} : {name}")
lines += [
"",
"Tempat:",
submission.get("room") or "-",
"",
"Terimakasih.",
"ttd",
"Kajur Informatika",
]
return "\n".join(lines)
def format_submission_summary(submission: dict) -> str:
"""Ringkasan 1-baris untuk log Airflow — supaya kelihatan jelas di task log
siapa yang dapat undangan/reminder tanpa perlu buka Calendar/email manual."""
schedule = get_schedule_datetime(submission)
pembimbing = [v["name"] for v in submission["verificators"] if v["type"] == "Pembimbing"]
penguji = [v["name"] for v in submission["verificators"] if v["type"] == "Penguji"]
return (
f"{submission['student_name']} ({submission['student_nim']}) — "
f"{get_type_label(submission)}\"{submission['title']}\" — "
f"{format_indonesian_date(schedule)} {schedule.strftime('%H:%M')} WIB "
f"@ {submission.get('room') or '-'} | "
f"Pembimbing: {', '.join(pembimbing) or '-'} | "
f"Penguji: {', '.join(penguji) or '-'}"
)
def build_attendee_emails(submission: dict) -> list[str]:
emails = [submission["student_email"]] + [v["email"] for v in submission["verificators"]]
seen = set()
deduped = []
for email in emails:
if email and email not in seen:
seen.add(email)
deduped.append(email)
return deduped
class GoogleCalendarService:
"""Auth via OAuth refresh token sebagai tu@informatika.untan.ac.id LANGSUNG
(bukan service account) — service account tidak bisa invite attendee tanpa
Domain-Wide Delegation of Authority (dicoba & ditolak Google 2026-07-27)."""
def __init__(self, config: GoogleConfig):
self._calendar_id = config.calendar_id
# Sengaja TIDAK set `scopes=` — lihat catatan di GmailService.__init__
# (edoxid_calendar_gmail.py) soal `invalid_scope` yang muncul di
# container Airflow (google-auth versi lain) saat scope refresh
# diminta lebih sempit dari yang tercakup refresh_token aslinya.
creds = Credentials(
token=None,
refresh_token=config.refresh_token,
token_uri="https://oauth2.googleapis.com/token",
client_id=config.client_id,
client_secret=config.client_secret,
)
creds.refresh(Request())
self._service = build("calendar", "v3", credentials=creds, cache_discovery=False)
def create_event(self, submission: dict) -> str:
event = self._build_event(submission)
result = (
self._service.events()
.insert(calendarId=self._calendar_id, body=event, sendUpdates="all")
.execute()
)
return result["id"]
def update_event(self, event_id: str, submission: dict) -> None:
event = self._build_event(submission)
self._service.events().update(
calendarId=self._calendar_id, eventId=event_id, body=event, sendUpdates="all"
).execute()
def delete_event(self, event_id: str) -> None:
try:
self._service.events().delete(
calendarId=self._calendar_id, eventId=event_id, sendUpdates="all"
).execute()
except HttpError as e:
# 404/410 berarti event sudah tidak ada (mis. dihapus manual) — aman diabaikan
if e.resp.status not in (404, 410):
raise
def _build_event(self, submission: dict) -> dict:
schedule = get_schedule_datetime(submission)
end = schedule + timedelta(hours=1)
return {
"summary": f"{get_type_label(submission)}{submission['student_name']}",
"description": build_description(submission),
"location": submission.get("room") or "",
"start": {"dateTime": schedule.isoformat(), "timeZone": "Asia/Jakarta"},
"end": {"dateTime": end.isoformat(), "timeZone": "Asia/Jakarta"},
"attendees": [{"email": email} for email in build_attendee_emails(submission)],
"reminders": {"useDefault": False, "overrides": self._build_reminders(schedule)},
}
@staticmethod
def _build_reminders(schedule: datetime) -> list[dict]:
h1 = schedule.replace(hour=7, minute=0, second=0, microsecond=0) - timedelta(days=1)
minutes_h1 = max(0, int((schedule - h1).total_seconds() // 60))
return [
{"method": "popup", "minutes": minutes_h1},
{"method": "popup", "minutes": 60},
]