SeaWolf-AI commited on
Commit
efffe05
ยท
verified ยท
1 Parent(s): e129580

ledger falls back when no persistent storage

Browse files
Files changed (2) hide show
  1. app/ledger.py +149 -129
  2. 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
- ROOT = os.environ.get("FINCHAL_DATA", os.path.join(os.path.dirname(__file__), "_data"))
14
- LOCK = threading.Lock()
15
-
16
-
17
- def _p(*a):
18
- p = os.path.join(ROOT, *a)
19
- os.makedirs(os.path.dirname(p), exist_ok=True)
20
- return p
21
-
22
-
23
- def now():
24
- return datetime.now(timezone.utc).replace(microsecond=0)
25
-
26
-
27
- def _read(path, default):
28
- try:
29
- with open(path, encoding="utf-8") as f:
30
- return json.load(f)
31
- except Exception:
32
- return default
33
-
34
-
35
- def _write(path, obj):
36
- tmp = path + ".tmp"
37
- with open(tmp, "w", encoding="utf-8") as f:
38
- json.dump(obj, f, ensure_ascii=False)
39
- os.replace(tmp, path) # ์›์ž์  ๊ต์ฒด โ€” ์ค‘๊ฐ„์— ์ฃฝ์–ด๋„ ๋ฐ˜์ชฝ ํŒŒ์ผ์ด ์•ˆ ๋‚จ๋Š”๋‹ค
40
-
41
-
42
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ์ฐธ๊ฐ€์ž โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
43
- def make_key(user_id):
44
- raw = "%s|%s|finchal" % (user_id, os.environ.get("FINCHAL_SALT", "s1"))
45
- return "fc_" + hashlib.sha256(raw.encode()).hexdigest()[:24]
46
-
47
-
48
- def register(user_id, strategy, ptype, assets):
49
- """์ฐธ๊ฐ€ ๋“ฑ๋ก. ์ด๋ฏธ ์žˆ์œผ๋ฉด ์ „๋žต๋ช…ยท์œ ํ˜•๋งŒ ๊ฐฑ์‹ ํ•˜๊ณ  ํ‚ค๋Š” ์œ ์ง€ํ•œ๋‹ค."""
50
- with LOCK:
51
- u = _read(_p("users.json"), {})
52
- key = make_key(user_id)
53
- prev = u.get(user_id, {})
54
- u[user_id] = {
55
- "user": user_id, "key": key, "strategy": strategy, "type": ptype,
56
- "assets": sorted(set(assets)),
57
- "joined": prev.get("joined", now().isoformat()),
58
- "visits": prev.get("visits", []),
59
- }
60
- _write(_p("users.json"), u)
61
- return u[user_id]
62
-
63
-
64
- def user_by_key(key):
65
- for v in _read(_p("users.json"), {}).values():
66
- if v.get("key") == key:
67
- return v
68
- return None
69
-
70
-
71
- def touch(user_id):
72
- """์ ‘์† ๊ธฐ๋ก โ€” ์‚ฌ๋žŒ ๋ถ€๋ฌธ์˜ '์ฃผ 1ํšŒ ์ ‘์†' ์š”๊ฑด ํŒ์ •์— ์“ด๋‹ค."""
73
- with LOCK:
74
- u = _read(_p("users.json"), {})
75
- if user_id in u:
76
- v = u[user_id].setdefault("visits", [])
77
- d = now().date().isoformat()
78
- if not v or v[-1] != d:
79
- v.append(d)
80
- v[:] = v[-400:]
81
- _write(_p("users.json"), u)
82
-
83
-
84
- def all_users():
85
- return list(_read(_p("users.json"), {}).values())
86
-
87
-
88
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ํฌ์ง€์…˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
89
- def submit(user_id, asset, position, source="api"):
90
- """ํฌ์ง€์…˜ ์ œ์ถœ. ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  ์ค„ ๋‹จ์œ„๋กœ ์Œ“๋Š”๋‹ค."""
91
- pos = max(-1.0, min(1.0, float(position)))
92
- rec = {"t": now().isoformat(), "u": user_id, "a": asset,
93
- "p": pos, "src": source}
94
- with LOCK:
95
- with open(_p("positions", "%s.jsonl" % asset), "a", encoding="utf-8") as f:
96
- f.write(json.dumps(rec, ensure_ascii=False) + "\n")
97
- touch(user_id)
98
- return rec
99
-
100
-
101
- def positions(asset, user_id=None):
102
- out = []
103
- p = _p("positions", "%s.jsonl" % asset)
104
- if not os.path.exists(p):
105
- return out
106
- with open(p, encoding="utf-8") as f:
107
- for line in f:
108
- try:
109
- r = json.loads(line)
110
- except Exception:
111
- continue
112
- if user_id is None or r.get("u") == user_id:
113
- out.append(r)
114
- return out
115
-
116
-
117
- def latest(asset, user_id):
118
- ps = positions(asset, user_id)
119
- return ps[-1] if ps else None
120
-
121
-
122
- # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ์ ์ˆ˜ ์Šค๋ƒ…์ƒท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
123
- def save_scores(asset, rows):
124
- _write(_p("scores", "%s.json" % asset),
125
- {"at": now().isoformat(), "rows": rows})
126
-
127
-
128
- def load_scores(asset):
129
- return _read(_p("scores", "%s.json" % asset), {"at": None, "rows": []})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "์˜์† ์Šคํ† ๋ฆฌ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žฌ์‹œ์ž‘ํ•˜๋ฉด ์›์žฅ์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค."}