File size: 3,200 Bytes
8debb9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""์‚ฌ์šฉ์ž ์ž…๋ ฅยท๊ต์—ด ๊ฒฐ๊ณผ ๊ธฐ๋ก โ€” Supabase(PostgREST) ์— ์ง์ ‘ REST ํ˜ธ์ถœ.

์˜› gradio ๋ฐ๋ชจ(`services/prj-chosun-gradio/blindtest/db.py`)์˜ `articles` ยท `pipeline_runs` ๋‘ ํ…Œ์ด๋ธ”์„
๊ทธ๋Œ€๋กœ ์“ด๋‹ค(์Šคํ‚ค๋งˆ๋Š” `schema.sql`). SDK ๋Œ€์‹  httpx ๋กœ REST ๋ฅผ ๋ถ€๋ฅด๋Š” ๊ฒƒ๋„ ๊ทธ๋•Œ์™€ ๊ฐ™๋‹ค โ€” `supabase`
ํŒจํ‚ค์ง€๊ฐ€ ๋Œ๊ณ  ์˜ค๋Š” `realtime`/`websockets` ํ•€์ด ๋‹ค๋ฅธ ์˜์กด์„ฑ๊ณผ ์ถฉ๋Œํ–ˆ๋˜ ์ „๋ ฅ ๋•Œ๋ฌธ.

ํ™˜๊ฒฝ ๋ณ€์ˆ˜:
  SUPABASE_URL โ€” ํ”„๋กœ์ ํŠธ URL (https://xxx.supabase.co)
  SUPABASE_KEY (๋˜๋Š” SUPABASE_ANON_KEY) โ€” anon ํ‚ค (RLS ์ •์ฑ…์€ schema.sql)

๋‘˜ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์—†์œผ๋ฉด ๋ชจ๋“  ์“ฐ๊ธฐ๋Š” no-op ์ด๊ณ  `is_configured()` ๊ฐ€ False ๋‹ค. ๊ธฐ๋ก ์‹คํŒจ๋Š” ์ ˆ๋Œ€ ๋ฐ๋ชจ๋ฅผ
๋ฉˆ์ถ”์ง€ ์•Š๋Š”๋‹ค โ€” `last_error()` ์— ํ•œ ์ค„ ๋‚จ๊ธฐ๊ณ  None ์„ ๋Œ๋ ค์ค€๋‹ค.
"""

from __future__ import annotations

import os

import httpx

TIMEOUT_S = 10.0
_last_error: str | None = None


def _url() -> str:
    return os.environ.get("SUPABASE_URL", "").strip().rstrip("/")


def _key() -> str:
    # ์˜› gradio ๋ฐ๋ชจ๋Š” SUPABASE_KEY, ์ตœ๊ทผ .env ๋Š” SUPABASE_ANON_KEY ๋ฅผ ์“ด๋‹ค -- ๋‘˜ ๋‹ค ๋ฐ›๋Š”๋‹ค
    return (os.environ.get("SUPABASE_KEY") or os.environ.get("SUPABASE_ANON_KEY") or "").strip()


def is_configured() -> bool:
    return bool(_url() and _key())


def last_error() -> str | None:
    return _last_error


def _post(table: str, row: dict) -> list[dict]:
    headers = {
        "apikey": _key(),
        "Authorization": f"Bearer {_key()}",
        "Content-Type": "application/json",
        "Prefer": "return=representation",
    }
    with httpx.Client(timeout=TIMEOUT_S) as client:
        resp = client.post(f"{_url()}/rest/v1/{table}", headers=headers, json=row)
    if resp.status_code >= 300:
        raise RuntimeError(f"{resp.status_code} {resp.text[:200]}")
    return resp.json() if resp.content else []


def _insert(table: str, row: dict) -> int | None:
    global _last_error
    if not is_configured():
        return None
    try:
        data = _post(table, row)
        _last_error = None
        return data[0]["id"] if data else None
    except Exception as exc:  # noqa: BLE001 โ€” ๊ธฐ๋ก์€ ๋ฐ๋ชจ์˜ ๋ถ€์ˆ˜ ๊ธฐ๋Šฅ, ์–ด๋–ค ์‹คํŒจ๋„ ์‚ผ์ผœ์„œ ํ•œ ์ค„๋งŒ ๋‚จ๊ธด๋‹ค
        _last_error = f"{table}: {type(exc).__name__}: {exc}"
        return None


def save_article(source_text: str) -> int | None:
    """์‚ฌ์šฉ์ž ์ž…๋ ฅ ์›๋ฌธ 1๊ฑด. ๊ต์—ด ์‹คํ–‰ **์ „**์— ๋ถ€๋ฅธ๋‹ค โ€” ์‹คํ–‰์ด ์‹คํŒจํ•ด๋„ ์ž…๋ ฅ์€ ๋‚จ๊ฒŒ."""
    return _insert("articles", {"source_text": source_text})


def save_run(
    article_id: int | None,
    *,
    pipeline_key: str,
    prompt_key: str,
    model: str,
    output: str,
    processing_time_s: float,
) -> int | None:
    """๊ต์—ด ๊ฒฐ๊ณผ 1๊ฑด. `article_id` ๊ฐ€ ์—†์œผ๋ฉด(์ž…๋ ฅ ๊ธฐ๋ก ์‹คํŒจ) ๊ฒฐ๊ณผ๋„ ๊ธฐ๋กํ•˜์ง€ ์•Š๋Š”๋‹ค."""
    if article_id is None:
        return None
    return _insert(
        "pipeline_runs",
        {
            "article_id": article_id,
            "pipeline_key": pipeline_key,
            "prompt_key": prompt_key,
            "model": model,
            "output": output,
            "processing_time_s": round(processing_time_s, 2),
        },
    )