JHyeok5's picture
Upload folder using huggingface_hub
f029017 verified
"""
Trend Engine ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ โ€” ์žฌ์‹œ๋„, ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ, retention ์ •๋ฆฌ
S7: ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ณตํ†ต ๋ž˜ํผ
S6: ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ ํ—ฌํผ
"""
import functools
import logging
import random
import time
from datetime import date, timedelta
logger = logging.getLogger(__name__)
# ------------------------------------------------------------------
# S7: ์žฌ์‹œ๋„ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ
# ------------------------------------------------------------------
def retry_on_failure(max_retries=3, base_delay=1.0, exceptions=(Exception,)):
"""API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์žฌ์‹œ๋„ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_retries - 1:
logger.error(
"%s failed after %d retries: %s",
func.__name__, max_retries, e,
)
raise
wait = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
logger.warning(
"%s attempt %d failed: %s. Retrying in %.1fs",
func.__name__, attempt + 1, e, wait,
)
time.sleep(wait)
return wrapper
return decorator
# ------------------------------------------------------------------
# S6: ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ ํ—ฌํผ
# ------------------------------------------------------------------
def get_week_period() -> tuple[date, date]:
"""์ด๋ฒˆ ์ฃผ ์›”์š”์ผ(period_start)๊ณผ ์˜ค๋Š˜(period_end)์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค."""
today = date.today()
period_start = today - timedelta(days=today.weekday()) # ์ด๋ฒˆ ์ฃผ ์›”์š”์ผ
return period_start, today
# ------------------------------------------------------------------
# S6: Retention ์ •๋ฆฌ
# ------------------------------------------------------------------
def safe_upsert_spot_trend(supabase, row: dict) -> bool:
"""PostgREST partial unique index ํ˜ธํ™˜ upsert (DELETE + INSERT).
spot_trends ํ…Œ์ด๋ธ”์˜ partial unique index(WHERE spot_id != '__pending__')๊ฐ€
PostgREST์˜ on_conflict ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ํ˜ธํ™˜๋˜์ง€ ์•Š์•„ 42P10 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
์ด๋ฅผ ์šฐํšŒํ•˜๊ธฐ ์œ„ํ•ด ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์‚ญ์ œ ํ›„ ์ƒˆ ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ฝ์ž…ํ•œ๋‹ค.
__pending__ ํ–‰์€ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค (blog_post INSERT๋Š” ๋ณ„๋„ ๊ฒฝ๋กœ).
"""
sid = row.get("spot_id", "")
if sid == "__pending__":
# __pending__์€ ์ค‘๋ณต ํ—ˆ์šฉ โ€” ์ผ๋ฐ˜ INSERT ์‚ฌ์šฉ
supabase.table("spot_trends").insert(row).execute()
return True
try:
supabase.table("spot_trends").delete() \
.eq("spot_id", sid) \
.eq("source", row["source"]) \
.eq("metric_type", row["metric_type"]) \
.eq("period_start", row["period_start"]) \
.execute()
except Exception:
pass # ์‚ญ์ œํ•  ๋ ˆ์ฝ”๋“œ ์—†์œผ๋ฉด ๋ฌด์‹œ
supabase.table("spot_trends").insert(row).execute()
return True
def cleanup_old_trends(supabase, retention_weeks: int = 12) -> int:
"""retention_weeks ์ด์ƒ ๋œ spot_trends ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•œ๋‹ค.
__pending__ ๋ธ”๋กœ๊ทธ ์›๋ณธ ๋ ˆ์ฝ”๋“œ๋„ ํ•จ๊ป˜ ์ •๋ฆฌํ•œ๋‹ค.
"""
cutoff = (date.today() - timedelta(weeks=retention_weeks)).isoformat()
try:
result = (
supabase.table("spot_trends")
.delete()
.lt("collected_at", cutoff)
.execute()
)
deleted = len(result.data) if result.data else 0
logger.info(
"retention ์ •๋ฆฌ ์™„๋ฃŒ: %d๊ฑด ์‚ญ์ œ (cutoff: %s, %d์ฃผ ์ด์ „)",
deleted, cutoff, retention_weeks,
)
return deleted
except Exception as e:
logger.warning("retention ์ •๋ฆฌ ์‹คํŒจ: %s", e)
return 0