Spaces:
Running
Running
ledger falls back when no persistent storage
Browse files- app/ledger.py +149 -129
- app/main.py +4 -1
app/ledger.py
CHANGED
|
@@ -1,129 +1,149 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""์์ฅ โ ์ฐธ๊ฐ์ยทํฌ์ง์
ยท์ ์๋ฅผ ๋ด๋๋ค.
|
| 3 |
-
|
| 4 |
-
ํ์ผ ๊ธฐ๋ฐ์ด๋ค. HF Space ์ฌ์์ ์ ์ฌ๋ผ์ง๋ฏ๋ก ์ด์์์๋ ์ธ๋ถ ์ ์ฅ์๋ก ์ฎ๊ธด๋ค.
|
| 5 |
-
์ง๊ธ์ ๊ตฌ์กฐ๋ฅผ ํ์ ํ๋ ๊ฒ ๋ชฉ์ ์ด๊ณ , ์ ์ฅ ์์น๋ง ๋ฐ๊พธ๋ฉด ๋๋๋ก ์ธํฐํ์ด์ค๋ฅผ ์ข๊ฒ ๋๋ค.
|
| 6 |
-
|
| 7 |
-
๐ด ํฌ์ง์
์ ์ ๋ ๋ฎ์ด์ฐ์ง ์๋๋ค. ๋งค ์ ์ถ์ append ํ๋ค.
|
| 8 |
-
๋ฎ์ด์ฐ๋ฉด "์ธ์ ๋ฌด์์ ๋๋๊ฐ"๊ฐ ์ฌ๋ผ์ง๊ณ , ๊ทธ๊ฒ ์ฌ๋ผ์ง๋ฉด ์ฑ์ ์ ์ฌํํ ์ ์๋ค.
|
| 9 |
-
"""
|
| 10 |
-
import os, json, time, hashlib, threading
|
| 11 |
-
from datetime import datetime, timezone
|
| 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 |
-
os.
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def
|
| 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 |
-
u =
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""์์ฅ โ ์ฐธ๊ฐ์ยทํฌ์ง์
ยท์ ์๋ฅผ ๋ด๋๋ค.
|
| 3 |
+
|
| 4 |
+
ํ์ผ ๊ธฐ๋ฐ์ด๋ค. HF Space ์ฌ์์ ์ ์ฌ๋ผ์ง๋ฏ๋ก ์ด์์์๋ ์ธ๋ถ ์ ์ฅ์๋ก ์ฎ๊ธด๋ค.
|
| 5 |
+
์ง๊ธ์ ๊ตฌ์กฐ๋ฅผ ํ์ ํ๋ ๊ฒ ๋ชฉ์ ์ด๊ณ , ์ ์ฅ ์์น๋ง ๋ฐ๊พธ๋ฉด ๋๋๋ก ์ธํฐํ์ด์ค๋ฅผ ์ข๊ฒ ๋๋ค.
|
| 6 |
+
|
| 7 |
+
๐ด ํฌ์ง์
์ ์ ๋ ๋ฎ์ด์ฐ์ง ์๋๋ค. ๋งค ์ ์ถ์ append ํ๋ค.
|
| 8 |
+
๋ฎ์ด์ฐ๋ฉด "์ธ์ ๋ฌด์์ ๋๋๊ฐ"๊ฐ ์ฌ๋ผ์ง๊ณ , ๊ทธ๊ฒ ์ฌ๋ผ์ง๋ฉด ์ฑ์ ์ ์ฌํํ ์ ์๋ค.
|
| 9 |
+
"""
|
| 10 |
+
import os, json, time, hashlib, threading
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
|
| 13 |
+
def _root():
|
| 14 |
+
"""์์ฅ ์์น. ์์ ์คํ ๋ฆฌ์ง๊ฐ ์์ผ๋ฉด ๊ฑฐ๊ธฐ, ์์ผ๋ฉด ์ปจํ
์ด๋ ์.
|
| 15 |
+
|
| 16 |
+
๐ด HF Space์ ์์ ์คํ ๋ฆฌ์ง๋ฅผ ์ ๋ถ์ด๋ฉด /data ๊ฐ ์๋ค. ๊ทธ๋ ์ฃฝ์ด ๋ฒ๋ฆฌ๋ฉด
|
| 17 |
+
ํ๋ฉด ์์ฒด๊ฐ ์ ๋ฌ๋ค. ๋ ์๋ '์ฌ์์ํ๋ฉด ์ฌ๋ผ์ง๋ค'๋ ์ฌ์ค์ด ๋ณด์ด๊ฒ ํ๋ค.
|
| 18 |
+
์์ฅ์ด ํ๋ฐ๋๋ ๊ฒ๊ณผ ์๋น์ค๊ฐ ์ ๋จ๋ ๊ฒ์ ๋ ๋ค ๋์์ง๋ง, ํ์๋
|
| 19 |
+
๋ฌด์์ด ๋ฌธ์ ์ธ์ง์กฐ์ฐจ ์ ์ ์๋ค.
|
| 20 |
+
"""
|
| 21 |
+
want = os.environ.get("FINCHAL_DATA")
|
| 22 |
+
if want:
|
| 23 |
+
try:
|
| 24 |
+
os.makedirs(want, exist_ok=True)
|
| 25 |
+
t = os.path.join(want, ".w")
|
| 26 |
+
open(t, "w").close(); os.remove(t)
|
| 27 |
+
return want, True
|
| 28 |
+
except Exception:
|
| 29 |
+
pass
|
| 30 |
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "_data"), False
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
ROOT, PERSISTENT = _root()
|
| 34 |
+
LOCK = threading.Lock()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _p(*a):
|
| 38 |
+
p = os.path.join(ROOT, *a)
|
| 39 |
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
| 40 |
+
return p
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def now():
|
| 44 |
+
return datetime.now(timezone.utc).replace(microsecond=0)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _read(path, default):
|
| 48 |
+
try:
|
| 49 |
+
with open(path, encoding="utf-8") as f:
|
| 50 |
+
return json.load(f)
|
| 51 |
+
except Exception:
|
| 52 |
+
return default
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _write(path, obj):
|
| 56 |
+
tmp = path + ".tmp"
|
| 57 |
+
with open(tmp, "w", encoding="utf-8") as f:
|
| 58 |
+
json.dump(obj, f, ensure_ascii=False)
|
| 59 |
+
os.replace(tmp, path) # ์์์ ๊ต์ฒด โ ์ค๊ฐ์ ์ฃฝ์ด๋ ๋ฐ์ชฝ ํ์ผ์ด ์ ๋จ๋๋ค
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ์ฐธ๊ฐ์ โโโโโโโโโโโโโโโโโโโโโโโ
|
| 63 |
+
def make_key(user_id):
|
| 64 |
+
raw = "%s|%s|finchal" % (user_id, os.environ.get("FINCHAL_SALT", "s1"))
|
| 65 |
+
return "fc_" + hashlib.sha256(raw.encode()).hexdigest()[:24]
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def register(user_id, strategy, ptype, assets):
|
| 69 |
+
"""์ฐธ๊ฐ ๋ฑ๋ก. ์ด๋ฏธ ์์ผ๋ฉด ์ ๋ต๋ช
ยท์ ํ๋ง ๊ฐฑ์ ํ๊ณ ํค๋ ์ ์งํ๋ค."""
|
| 70 |
+
with LOCK:
|
| 71 |
+
u = _read(_p("users.json"), {})
|
| 72 |
+
key = make_key(user_id)
|
| 73 |
+
prev = u.get(user_id, {})
|
| 74 |
+
u[user_id] = {
|
| 75 |
+
"user": user_id, "key": key, "strategy": strategy, "type": ptype,
|
| 76 |
+
"assets": sorted(set(assets)),
|
| 77 |
+
"joined": prev.get("joined", now().isoformat()),
|
| 78 |
+
"visits": prev.get("visits", []),
|
| 79 |
+
}
|
| 80 |
+
_write(_p("users.json"), u)
|
| 81 |
+
return u[user_id]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def user_by_key(key):
|
| 85 |
+
for v in _read(_p("users.json"), {}).values():
|
| 86 |
+
if v.get("key") == key:
|
| 87 |
+
return v
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def touch(user_id):
|
| 92 |
+
"""์ ์ ๊ธฐ๋ก โ ์ฌ๋ ๋ถ๋ฌธ์ '์ฃผ 1ํ ์ ์' ์๊ฑด ํ์ ์ ์ด๋ค."""
|
| 93 |
+
with LOCK:
|
| 94 |
+
u = _read(_p("users.json"), {})
|
| 95 |
+
if user_id in u:
|
| 96 |
+
v = u[user_id].setdefault("visits", [])
|
| 97 |
+
d = now().date().isoformat()
|
| 98 |
+
if not v or v[-1] != d:
|
| 99 |
+
v.append(d)
|
| 100 |
+
v[:] = v[-400:]
|
| 101 |
+
_write(_p("users.json"), u)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def all_users():
|
| 105 |
+
return list(_read(_p("users.json"), {}).values())
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ํฌ์ง์
โโโโโโโโโโโโโโโโโโโโโโโ
|
| 109 |
+
def submit(user_id, asset, position, source="api"):
|
| 110 |
+
"""ํฌ์ง์
์ ์ถ. ๋ฎ์ด์ฐ์ง ์๊ณ ์ค ๋จ์๋ก ์๋๋ค."""
|
| 111 |
+
pos = max(-1.0, min(1.0, float(position)))
|
| 112 |
+
rec = {"t": now().isoformat(), "u": user_id, "a": asset,
|
| 113 |
+
"p": pos, "src": source}
|
| 114 |
+
with LOCK:
|
| 115 |
+
with open(_p("positions", "%s.jsonl" % asset), "a", encoding="utf-8") as f:
|
| 116 |
+
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
| 117 |
+
touch(user_id)
|
| 118 |
+
return rec
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def positions(asset, user_id=None):
|
| 122 |
+
out = []
|
| 123 |
+
p = _p("positions", "%s.jsonl" % asset)
|
| 124 |
+
if not os.path.exists(p):
|
| 125 |
+
return out
|
| 126 |
+
with open(p, encoding="utf-8") as f:
|
| 127 |
+
for line in f:
|
| 128 |
+
try:
|
| 129 |
+
r = json.loads(line)
|
| 130 |
+
except Exception:
|
| 131 |
+
continue
|
| 132 |
+
if user_id is None or r.get("u") == user_id:
|
| 133 |
+
out.append(r)
|
| 134 |
+
return out
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def latest(asset, user_id):
|
| 138 |
+
ps = positions(asset, user_id)
|
| 139 |
+
return ps[-1] if ps else None
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ์ ์ ์ค๋
์ท โโโโโโโโโโโโโโโโโโโโโโโ
|
| 143 |
+
def save_scores(asset, rows):
|
| 144 |
+
_write(_p("scores", "%s.json" % asset),
|
| 145 |
+
{"at": now().isoformat(), "rows": rows})
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def load_scores(asset):
|
| 149 |
+
return _read(_p("scores", "%s.json" % asset), {"at": None, "rows": []})
|
app/main.py
CHANGED
|
@@ -346,4 +346,7 @@ def _startup():
|
|
| 346 |
|
| 347 |
@app.get("/health")
|
| 348 |
def health():
|
| 349 |
-
return {"ok": True, "season": S1["name"], "assets": list(ASSETS)
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
@app.get("/health")
|
| 348 |
def health():
|
| 349 |
+
return {"ok": True, "season": S1["name"], "assets": list(ASSETS),
|
| 350 |
+
"ledger": L.ROOT, "persistent": L.PERSISTENT,
|
| 351 |
+
"warn": None if L.PERSISTENT else
|
| 352 |
+
"์์ ์คํ ๋ฆฌ์ง๊ฐ ์์ต๋๋ค. ์ปจํ
์ด๋๊ฐ ์ฌ์์ํ๋ฉด ์์ฅ์ด ์ฌ๋ผ์ง๋๋ค."}
|