Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """์์ฅ โ ์ฐธ๊ฐ์ยทํฌ์ง์ ยท์ ์๋ฅผ ๋ด๋๋ค. | |
| ํ์ผ ๊ธฐ๋ฐ์ด๋ค. HF Space ์ฌ์์ ์ ์ฌ๋ผ์ง๋ฏ๋ก ์ด์์์๋ ์ธ๋ถ ์ ์ฅ์๋ก ์ฎ๊ธด๋ค. | |
| ์ง๊ธ์ ๊ตฌ์กฐ๋ฅผ ํ์ ํ๋ ๊ฒ ๋ชฉ์ ์ด๊ณ , ์ ์ฅ ์์น๋ง ๋ฐ๊พธ๋ฉด ๋๋๋ก ์ธํฐํ์ด์ค๋ฅผ ์ข๊ฒ ๋๋ค. | |
| ๐ด ํฌ์ง์ ์ ์ ๋ ๋ฎ์ด์ฐ์ง ์๋๋ค. ๋งค ์ ์ถ์ append ํ๋ค. | |
| ๋ฎ์ด์ฐ๋ฉด "์ธ์ ๋ฌด์์ ๋๋๊ฐ"๊ฐ ์ฌ๋ผ์ง๊ณ , ๊ทธ๊ฒ ์ฌ๋ผ์ง๋ฉด ์ฑ์ ์ ์ฌํํ ์ ์๋ค. | |
| """ | |
| import os, json, time, hashlib, threading | |
| from datetime import datetime, timezone | |
| def _root(): | |
| """์์ฅ ์์น. ์์ ์คํ ๋ฆฌ์ง๊ฐ ์์ผ๋ฉด ๊ฑฐ๊ธฐ, ์์ผ๋ฉด ์ปจํ ์ด๋ ์. | |
| ๐ด HF Space์ ์์ ์คํ ๋ฆฌ์ง๋ฅผ ์ ๋ถ์ด๋ฉด /data ๊ฐ ์๋ค. ๊ทธ๋ ์ฃฝ์ด ๋ฒ๋ฆฌ๋ฉด | |
| ํ๋ฉด ์์ฒด๊ฐ ์ ๋ฌ๋ค. ๋ ์๋ '์ฌ์์ํ๋ฉด ์ฌ๋ผ์ง๋ค'๋ ์ฌ์ค์ด ๋ณด์ด๊ฒ ํ๋ค. | |
| ์์ฅ์ด ํ๋ฐ๋๋ ๊ฒ๊ณผ ์๋น์ค๊ฐ ์ ๋จ๋ ๊ฒ์ ๋ ๋ค ๋์์ง๋ง, ํ์๋ | |
| ๋ฌด์์ด ๋ฌธ์ ์ธ์ง์กฐ์ฐจ ์ ์ ์๋ค. | |
| """ | |
| want = os.environ.get("FINCHAL_DATA") | |
| if want: | |
| try: | |
| os.makedirs(want, exist_ok=True) | |
| t = os.path.join(want, ".w") | |
| open(t, "w").close(); os.remove(t) | |
| return want | |
| except Exception: | |
| pass | |
| return os.path.join(os.path.dirname(os.path.abspath(__file__)), "_data") | |
| ROOT = _root() | |
| LOCK = threading.Lock() | |
| # โโโโโโโโโโโโโโโโโโโโโโโ ์์ฅ ๋ฏธ๋ฌ โโโโโโโโโโโโโโโโโโโโโโโ | |
| # ๐ด HF Space์ /data ๋ ์์ ์คํ ๋ฆฌ์ง๋ฅผ ๋ถ์ด์ง ์์ผ๋ฉด ์ฌ์์์ ์ฌ๋ผ์ง๋ค. | |
| # ์ค์ธกํ๋ค โ ์ฌ์์ ํ ๋ถํ ํ์์ด ์์ด์ก๋ค(boots 1 -> 1). | |
| # ์ ๋ฃ ์คํ ๋ฆฌ์ง ๋์ ๋น๊ณต๊ฐ ๋ฐ์ดํฐ์ ์ ์ฅ์์ ์์ฅ์ ๋ฏธ๋ฌ๋งํ๋ค. | |
| # ์์ฆ 4๊ฐ์์น ๊ธฐ๋ก์ด ์ปจํ ์ด๋ ์ฌ์์ ํ ๋ฒ์ ๋ ์๊ฐ๋ฉด ๋ํ๊ฐ ๋๋๋ค. | |
| MIRROR = os.environ.get("FINCHAL_MIRROR") # ์: FINAL-Bench/finchal-ledger | |
| _MIR = {"ok": False, "dirty": False, "at": 0.0, "n": 0, "err": None} | |
| def _api(): | |
| from huggingface_hub import HfApi | |
| tok = os.environ.get("HF_TOKEN") or os.environ.get("FINCHAL_HF_TOKEN") | |
| return HfApi(token=tok) if tok else None | |
| def restore(): | |
| """๋ถํ ์ ๋ฏธ๋ฌ์์ ์์ฅ์ ๋์ด๋ฆฐ๋ค. ์ด๊ฒ ์คํจํ๋ฉด ๋ฏธ๋ฌ์ ์ฐ์ง ์๋๋ค. | |
| ๐ ๋ณต์ ์คํจ ํ flush ํ๋ฉด ๋น ์์ฅ์ด ๋ฏธ๋ฌ๋ฅผ ๋ฎ์ด ์์ฆ์ด ํต์งธ๋ก ์ฌ๋ผ์ง๋ค. | |
| ํ ๋ฒ์ ์๋ชป๋ ๋ฎ์ด์ฐ๊ธฐ๊ฐ 4๊ฐ์์ ์ง์ด๋ค. | |
| """ | |
| if not MIRROR: | |
| return "๋ฏธ๋ฌ ์์" | |
| try: | |
| from huggingface_hub import snapshot_download | |
| a = _api() | |
| if a is None: | |
| _MIR["err"] = "HF_TOKEN ์์" | |
| return _MIR["err"] | |
| try: | |
| a.repo_info(MIRROR, repo_type="dataset") | |
| except Exception: | |
| a.create_repo(MIRROR, repo_type="dataset", private=True, exist_ok=True) | |
| # ๐ด ๊ธฐ๋ณธ ์บ์๋ก ๋ฐ์ผ๋ฉด ๊ฒฝ๋ก์ .cache ๊ฐ ๋ค์ด๊ฐ๋ค. ๊ทธ๊ฑธ ๊ฑธ๋ฌ๋ด๋ | |
| # ํํฐ๋ฅผ ๋๋ฉด **๋ฐ์ ํ์ผ์ด ์ ๋ถ ๊ฑธ๋ฌ์ง๋ค** โ ์ค์ ๋ก ๊ทธ๋ ๊ฒ ๋์ด | |
| # "๋ณต์ 0๊ฐ"๊ฐ ๋์๋ค. ๋ชฉ์ ์ง๋ฅผ ์ฐ๋ฆฌ๊ฐ ์ ํด ๊ทธ ํจ์ ์ ์์ค๋ค. | |
| tmp = os.path.join(ROOT, ".restore") | |
| d = snapshot_download(MIRROR, repo_type="dataset", token=a.token, | |
| local_dir=tmp) | |
| import shutil, glob | |
| os.makedirs(os.path.join(ROOT, "positions"), exist_ok=True) | |
| n = 0 | |
| for src in glob.glob(os.path.join(d, "**", "*"), recursive=True): | |
| if not os.path.isfile(src): | |
| continue | |
| rel = os.path.relpath(src, d) | |
| if rel.startswith(".git") or rel.startswith(".huggingface"): | |
| continue | |
| dst = os.path.join(ROOT, rel) | |
| os.makedirs(os.path.dirname(dst), exist_ok=True) | |
| shutil.copyfile(src, dst) | |
| n += 1 | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| _MIR["ok"] = True | |
| _MIR["n"] = n | |
| return "๋ณต์ %d๊ฐ" % n | |
| except Exception as e: | |
| _MIR["err"] = "%s: %s" % (type(e).__name__, e) | |
| return _MIR["err"] | |
| def flush(force=False): | |
| """์์ฅ์ ๋ฏธ๋ฌ์ ์ฌ๋ฆฐ๋ค. ์ ์ถ๋ง๋ค ์ฌ๋ฆฌ๋ฉด ์ปค๋ฐ์ด ํญ์ฃผํ๋ฏ๋ก ๋ชจ์์ ๋ณด๋ธ๋ค.""" | |
| if not (MIRROR and _MIR["ok"]) or (not _MIR["dirty"] and not force): | |
| return False | |
| try: | |
| a = _api() | |
| a.upload_folder(repo_id=MIRROR, repo_type="dataset", folder_path=ROOT, | |
| commit_message="ledger sync", | |
| ignore_patterns=["*.tmp", ".cache/*", ".restore/*", | |
| ".huggingface/*"]) | |
| _MIR["dirty"] = False | |
| _MIR["at"] = time.time() | |
| return True | |
| except Exception as e: | |
| _MIR["err"] = "%s: %s" % (type(e).__name__, e) | |
| return False | |
| def mirror_status(): | |
| return {"repo": MIRROR, "restored": _MIR["ok"], "files": _MIR["n"], | |
| "pending": _MIR["dirty"], "error": _MIR["err"]} | |
| def boot_check(): | |
| """์์ฅ์ด ์ฌ์์์ ๊ฒฌ๋๋์ง **์ค์ธก**ํ๋ค. | |
| ๐ด '์ฐ๊ธฐ๊ฐ ๋๋๊ฐ'๋ก ํ์ ํ๋ฉด ์ ๋๋ค. ์์ ์คํ ๋ฆฌ์ง๊ฐ ์์ด๋ /data ๋ | |
| ๋ง๋ค์ด์ง๊ณ ์จ์ง๋ค โ ์ฌ์์ํ ๋ ์ฌ๋ผ์ง ๋ฟ์ด๋ค. ๊ทธ๋์ ์ฐ๊ธฐ ๊ฒ์ฌ๋ | |
| ํญ์ ํต๊ณผํ๋ ๊ฐ์ง ์ ํธ๋ค. ์ค์ ๋ก ํ์ธํ๋ ค๋ฉด ์ด์ ๋ถํ ์ ํ์ ์ด | |
| ๋จ์ ์๋์ง๋ฅผ ๋ด์ผ ํ๋ค. | |
| ๋ฐํ: (ํ์ , ๋ถํ ํ์) | |
| "ํ์ธ๋จ" ์ด์ ๋ถํ ๊ธฐ๋ก์ด ์ด์๋จ์๋ค = ์์ | |
| "๋ฏธํ์ธ" ์ฒซ ๋ถํ ์ด๋ผ ์์ง ์ ์ ์๋ค | |
| "ํ๋ฐ๋จ" ๊ธฐ๋ก์ ๋จ๊ฒผ๋๋ฐ ์ฌ๋ผ์ก๋ค = ์ฌ์์๋ง๋ค ์์ฅ์ด ๋ ์๊ฐ๋ค | |
| """ | |
| f = os.path.join(ROOT, "_boot.json") | |
| prev = _read(f, None) | |
| n = (prev or {}).get("n", 0) + 1 | |
| if prev is None: | |
| # ์ปจํ ์ด๋๋ ์์๋๋๋ฐ ํ์ ์ด ์๋ค. ์ฒซ ๋ถํ ์ธ์ง ๋ ์๊ฐ ๊ฑด์ง๋ | |
| # ์ด ์์ ์ ๊ตฌ๋ถ๋์ง ์๋๋ค. ํ์ ์ ๋ค์ ๋ถํ ์ผ๋ก ๋ฏธ๋ฃฌ๋ค. | |
| verdict = "๋ฏธํ์ธ" | |
| else: | |
| verdict = "ํ์ธ๋จ" | |
| try: | |
| _write(f, {"n": n, "at": now().isoformat(), "root": ROOT}) | |
| except Exception: | |
| pass | |
| return verdict, n | |
| def _p(*a): | |
| p = os.path.join(ROOT, *a) | |
| os.makedirs(os.path.dirname(p), exist_ok=True) | |
| return p | |
| def now(): | |
| return datetime.now(timezone.utc).replace(microsecond=0) | |
| def _read(path, default): | |
| try: | |
| with open(path, encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception: | |
| return default | |
| def _write(path, obj): | |
| tmp = path + ".tmp" | |
| with open(tmp, "w", encoding="utf-8") as f: | |
| json.dump(obj, f, ensure_ascii=False) | |
| os.replace(tmp, path) # ์์์ ๊ต์ฒด โ ์ค๊ฐ์ ์ฃฝ์ด๋ ๋ฐ์ชฝ ํ์ผ์ด ์ ๋จ๋๋ค | |
| # โโโโโโโโโโโโโโโโโโโโโโโ ์ฐธ๊ฐ์ โโโโโโโโโโโโโโโโโโโโโโโ | |
| def make_key(user_id): | |
| raw = "%s|%s|finchal" % (user_id, os.environ.get("FINCHAL_SALT", "s1")) | |
| return "fc_" + hashlib.sha256(raw.encode()).hexdigest()[:24] | |
| def register(user_id, strategy, ptype, assets): | |
| """์ฐธ๊ฐ ๋ฑ๋ก. ์ด๋ฏธ ์์ผ๋ฉด ์ ๋ต๋ช ยท์ ํ๋ง ๊ฐฑ์ ํ๊ณ ํค๋ ์ ์งํ๋ค.""" | |
| with LOCK: | |
| u = _read(_p("users.json"), {}) | |
| key = make_key(user_id) | |
| prev = u.get(user_id, {}) | |
| u[user_id] = { | |
| "user": user_id, "key": key, "strategy": strategy, "type": ptype, | |
| "assets": sorted(set(assets)), | |
| "joined": prev.get("joined", now().isoformat()), | |
| "visits": prev.get("visits", []), | |
| } | |
| _write(_p("users.json"), u) | |
| _MIR["dirty"] = True | |
| return u[user_id] | |
| def user_by_key(key): | |
| for v in _read(_p("users.json"), {}).values(): | |
| if v.get("key") == key: | |
| return v | |
| return None | |
| def touch(user_id): | |
| """์ ์ ๊ธฐ๋ก โ ์ฌ๋ ๋ถ๋ฌธ์ '์ฃผ 1ํ ์ ์' ์๊ฑด ํ์ ์ ์ด๋ค.""" | |
| with LOCK: | |
| u = _read(_p("users.json"), {}) | |
| if user_id in u: | |
| v = u[user_id].setdefault("visits", []) | |
| d = now().date().isoformat() | |
| if not v or v[-1] != d: | |
| v.append(d) | |
| v[:] = v[-400:] | |
| _write(_p("users.json"), u) | |
| def all_users(): | |
| return list(_read(_p("users.json"), {}).values()) | |
| # โโโโโโโโโโโโโโโโโโโโโโโ ํฌ์ง์ โโโโโโโโโโโโโโโโโโโโโโโ | |
| def submit(user_id, asset, position, source="api"): | |
| """ํฌ์ง์ ์ ์ถ. ๋ฎ์ด์ฐ์ง ์๊ณ ์ค ๋จ์๋ก ์๋๋ค.""" | |
| pos = max(-1.0, min(1.0, float(position))) | |
| rec = {"t": now().isoformat(), "u": user_id, "a": asset, | |
| "p": pos, "src": source} | |
| with LOCK: | |
| with open(_p("positions", "%s.jsonl" % asset), "a", encoding="utf-8") as f: | |
| f.write(json.dumps(rec, ensure_ascii=False) + "\n") | |
| _MIR["dirty"] = True | |
| touch(user_id) | |
| return rec | |
| def seed(user_id, asset, position, at, source="seed"): | |
| """์ง์ ํ ์๊ฐ์ผ๋ก ํฌ์ง์ ์ ๊ธฐ๋กํ๋ค. ๊ธฐ์ค์ ์ ์์ฆ ๊ฐ๋ง ์์ ์ ์ฌ์ ๋๋ง ์ด๋ค. | |
| ๐ด ์ฐธ๊ฐ์์๊ฒ๋ ์ ๋ ์ด์ด ์ฃผ์ง ์๋๋ค. ๊ณผ๊ฑฐ ์๊ฐ์ผ๋ก ํฌ์ง์ ์ ๋ฃ์ ์ ์์ผ๋ฉด | |
| ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ ๋์ ์ ๋ฆฌํ ์์ ์ ์ง์ ํ ๊ฒ์ผ๋ก ๊พธ๋ฐ ์ ์๋ค. | |
| """ | |
| pos = max(-1.0, min(1.0, float(position))) | |
| rec = {"t": str(at), "u": user_id, "a": asset, "p": pos, "src": source} | |
| with LOCK: | |
| with open(_p("positions", "%s.jsonl" % asset), "a", encoding="utf-8") as f: | |
| f.write(json.dumps(rec, ensure_ascii=False) + "\n") | |
| _MIR["dirty"] = True | |
| return rec | |
| def positions(asset, user_id=None): | |
| out = [] | |
| p = _p("positions", "%s.jsonl" % asset) | |
| if not os.path.exists(p): | |
| return out | |
| with open(p, encoding="utf-8") as f: | |
| for line in f: | |
| try: | |
| r = json.loads(line) | |
| except Exception: | |
| continue | |
| if user_id is None or r.get("u") == user_id: | |
| out.append(r) | |
| return out | |
| def latest(asset, user_id): | |
| ps = positions(asset, user_id) | |
| return ps[-1] if ps else None | |
| # โโโโโโโโโโโโโโโโโโโโโโโ ์ ์ ์ค๋ ์ท โโโโโโโโโโโโโโโโโโโโโโโ | |
| def save_scores(asset, rows): | |
| _write(_p("scores", "%s.json" % asset), | |
| {"at": now().isoformat(), "rows": rows}) | |
| def load_scores(asset): | |
| return _read(_p("scores", "%s.json" % asset), {"at": None, "rows": []}) | |