from __future__ import annotations from datetime import datetime, timedelta, timezone from zoneinfo import ZoneInfo from config import settings _KST = ZoneInfo('Asia/Seoul') _BUCKET_MINUTES = 5 def normalize_bucket_kst(bucket: datetime) -> datetime: if bucket.tzinfo is None: return bucket return bucket.astimezone(_KST).replace(tzinfo=None) def floor_bucket_kst(value: datetime, bucket_minutes: int = _BUCKET_MINUTES) -> datetime: if value.tzinfo is None: localized = value.replace(tzinfo=_KST) else: localized = value.astimezone(_KST) floored_minute = (localized.minute // bucket_minutes) * bucket_minutes return localized.replace(minute=floored_minute, second=0, microsecond=0) def compute_safe_bucket(now: datetime | None = None) -> datetime: current = now or datetime.now(timezone.utc) if current.tzinfo is None: current = current.replace(tzinfo=timezone.utc) safe_point = current.astimezone(_KST) - timedelta(minutes=settings.SNPDB_SAFE_DELAY_MIN) return floor_bucket_kst(safe_point).replace(tzinfo=None) def compute_initial_window_start(hours: int, safe_bucket: datetime | None = None) -> datetime: anchor = normalize_bucket_kst(safe_bucket or compute_safe_bucket()) return anchor - timedelta(hours=hours) def compute_incremental_window_start(last_bucket: datetime) -> datetime: normalized = normalize_bucket_kst(last_bucket) return normalized - timedelta(minutes=settings.SNPDB_BACKFILL_BUCKETS * _BUCKET_MINUTES)