SalexAI commited on
Commit
de997fd
Β·
verified Β·
1 Parent(s): 1d278b0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +202 -110
app.py CHANGED
@@ -1,123 +1,215 @@
1
- # app.py β€” ultra-minimal Gradio Space with 3 API endpoints (in-memory)
2
  import time
3
- import gradio as gr
4
- from collections import deque
5
-
6
- UI_TITLE = "Central Cards β€” Sanity Feed (In-Memory)"
7
- STORE = deque(maxlen=500) # [(id, kind, text, payload_json, ts)]
8
- ID = 0
9
-
10
- def now(): return int(time.time())
11
-
12
- def push(kind, text, payload=None):
13
- global ID
14
- ID += 1
15
- STORE.appendleft((ID, kind, text, payload, now()))
16
- return ID
17
-
18
- # === API functions (visible via /api/predict/<name>) ===
19
- def api_ingest(text: str):
20
- text = (text or "").strip()
21
- if not text:
22
- raise gr.Error("'text' is required")
23
- mid = push("text", text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  return {"ok": True, "id": mid}
25
 
26
- def api_orders(order: dict):
27
- # expect: {season, info:{name,room,email}, picked:[{name,rarity,power}], summary:{counts,v100,valueSum}}
28
- season = order.get("season")
29
- info = order.get("info") or {}
30
- picked = order.get("picked") or []
31
- summary = order.get("summary") or {}
32
-
33
  lines = []
34
- lines.append(f"# πŸ“¦ Season {season} Order")
35
- lines.append(f"**Name:** {info.get('name','')}")
36
- lines.append(f"**Class:** {info.get('room','')}")
37
- lines.append(f"**Email:** {info.get('email','')}")
38
  lines.append("")
39
  lines.append("## Results (x10)")
40
- for i, c in enumerate(picked, 1):
41
- lines.append(f"{i}. **{c.get('name','')}** β€” _{c.get('rarity','')}_, Power **{c.get('power','')}**")
42
- counts = summary.get("counts", {})
43
  lines.append("")
44
- lines.append(f"**Rarity Breakdown:** C:{counts.get('Common',0)} β€’ U:{counts.get('Uncommon',0)} β€’ R:{counts.get('Rare',0)} β€’ UR:{counts.get('Ultra Rare',0)} β€’ L:{counts.get('Legendary',0)}")
45
- lines.append(f"**Value Score:** {summary.get('v100',0)}/100 (raw {summary.get('valueSum',0)})")
46
- md = "\n".join(lines)
47
-
48
- import json
49
- mid = push("order", md, json.dumps(order, ensure_ascii=False))
 
 
50
  return {"ok": True, "id": mid}
51
 
52
- def api_suggestions(s: dict):
53
- name = (s.get("name") or "").strip()
54
- role = (s.get("role") or "").strip()
55
- why = (s.get("why") or "").strip()
56
- if not name or not role or not why:
57
- raise gr.Error("name, role, and why are required")
58
-
59
- pw = s.get("powers") or []
60
- wk = s.get("weaknesses") or []
61
-
62
  lines = []
63
  lines.append("# πŸ’‘ New Card Suggestion")
64
- lines.append(f"**Name:** {name}")
65
- lines.append(f"**Role:** {role}")
66
- lines.append(f"**Why:** {why}")
67
- lines.append(f"**Powers:** {', '.join(pw) if pw else 'β€”'}")
68
- lines.append(f"**Weaknesses:** {', '.join(wk) if wk else 'β€”'}")
69
- md = "\n".join(lines)
70
-
71
- import json
72
- mid = push("suggestion", md, json.dumps(s, ensure_ascii=False))
73
  return {"ok": True, "id": mid}
74
 
75
- # === UI (simple feed) ===
76
- CSS = """
77
- .card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
78
- .card { border: 1px solid #e5e7eb; border-radius: 14px; padding: 14px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.05); }
79
- .card h4 { margin: 0 0 6px; font-size: 14px; color: #6b7280; }
80
- .card pre { background:#0b1020; color:#d7e6ff; padding:10px; border-radius:10px; overflow:auto; }
81
- .badge { display:inline-block; font-size:11px; padding:2px 8px; border:1px solid #ddd; border-radius:999px; margin-left:6px; }
82
- """
83
-
84
- def render(limit: int = 200, kind: str = ""):
85
- items = []
86
- for (mid, k, text, payload, ts) in list(STORE)[:limit]:
87
- if kind and k != kind: continue
88
- tstr = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
89
- safe = gr.utils.sanitize_html(text)
90
- payload_block = ""
91
- if payload:
92
- payload_block = f"<details><summary>Payload</summary><pre>{gr.utils.sanitize_html(payload)}</pre></details>"
93
- items.append(f"<div class='card'><h4>#{mid} β€’ {tstr} <span class='badge'>{k}</span></h4><div>{safe}</div>{payload_block}</div>")
94
- html = "<div class='card-grid'>" + "".join(items) + "</div>" if items else "<p>No items yet.</p>"
95
- return f"<style>{CSS}</style>{html}"
96
-
97
- with gr.Blocks(title=UI_TITLE, css=CSS) as demo:
98
- gr.Markdown(f"# {UI_TITLE}\nPOST to `/api/predict/orders`, `/api/predict/suggestions`, `/api/predict/ingest`.")
99
- with gr.Row():
100
- kind = gr.Dropdown(choices=["", "order", "suggestion", "text"], value="", label="Filter", scale=2)
101
- limit = gr.Slider(20, 500, value=200, step=20, label="Max items", scale=3)
102
- btn = gr.Button("Refresh", variant="primary")
103
- wipe = gr.Button("Wipe (in-memory)")
104
- out = gr.HTML(render())
105
- btn.click(render, inputs=[kind, limit], outputs=[out])
106
- wipe.click(lambda: (STORE.clear(), render())[1], outputs=[out])
107
- gr.Timer(2.0).tick(render, inputs=[kind, limit], outputs=[out])
108
-
109
- # Hidden API endpoints
110
- _ing_in = gr.Textbox(visible=False, label="text")
111
- _ing_out = gr.JSON(visible=False)
112
- gr.Button(visible=False).click(api_ingest, inputs=[_ing_in], outputs=[_ing_out], api_name="ingest")
113
-
114
- _ord_in = gr.JSON(visible=False)
115
- _ord_out = gr.JSON(visible=False)
116
- gr.Button(visible=False).click(api_orders, inputs=[_ord_in], outputs=[_ord_out], api_name="orders")
117
-
118
- _sug_in = gr.JSON(visible=False)
119
- _sug_out = gr.JSON(visible=False)
120
- gr.Button(visible=False).click(api_suggestions, inputs=[_sug_in], outputs=[_sug_out], api_name="suggestions")
121
-
122
- # IMPORTANT: expose Gradio app at module scope
123
- app = demo
 
1
+ import os
2
  import time
3
+ import json
4
+ import sqlite3
5
+ from typing import Optional, Dict, Any, List
6
+
7
+ from fastapi import FastAPI, Request, HTTPException, status, Depends
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel, Field
10
+ from starlette.responses import HTMLResponse
11
+
12
+ # =========================
13
+ # Config
14
+ # =========================
15
+ DB_PATH = os.getenv("DB_PATH", "messages.db")
16
+ API_KEY = os.getenv("API_KEY", "").strip() # set in Space Secrets for auth
17
+ TITLE = "Central Cards β€” Orders Feed (FastAPI)"
18
+
19
+ # =========================
20
+ # DB helpers (SQLite)
21
+ # =========================
22
+ def init_db():
23
+ con = sqlite3.connect(DB_PATH)
24
+ cur = con.cursor()
25
+ cur.execute(
26
+ """
27
+ CREATE TABLE IF NOT EXISTS messages (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ kind TEXT NOT NULL, -- "order" | "suggestion" | "text"
30
+ text TEXT NOT NULL, -- human-readable / markdown summary
31
+ payload TEXT, -- raw JSON string (optional)
32
+ ts INTEGER NOT NULL -- epoch seconds
33
+ )
34
+ """
35
+ )
36
+ con.commit()
37
+ con.close()
38
+
39
+ def db() -> sqlite3.Connection:
40
+ return sqlite3.connect(DB_PATH)
41
+
42
+ def insert_message(kind: str, text: str, payload_json: Optional[str] = None) -> int:
43
+ con = db()
44
+ cur = con.cursor()
45
+ cur.execute(
46
+ "INSERT INTO messages(kind, text, payload, ts) VALUES(?, ?, ?, ?)",
47
+ (kind, text, payload_json or None, int(time.time())),
48
+ )
49
+ con.commit()
50
+ mid = cur.lastrowid
51
+ con.close()
52
+ return mid
53
+
54
+ def list_messages(limit: int = 300) -> List[tuple]:
55
+ con = db()
56
+ cur = con.cursor()
57
+ cur.execute("SELECT id, kind, text, payload, ts FROM messages ORDER BY id DESC LIMIT ?", (limit,))
58
+ rows = cur.fetchall()
59
+ con.close()
60
+ return rows
61
+
62
+ def clear_messages():
63
+ con = db()
64
+ cur = con.cursor()
65
+ cur.execute("DELETE FROM messages")
66
+ con.commit()
67
+ con.close()
68
+
69
+ init_db()
70
+
71
+ # =========================
72
+ # API models
73
+ # =========================
74
+ class IngestText(BaseModel):
75
+ text: str = Field(..., min_length=1)
76
+
77
+ class StudentInfo(BaseModel):
78
+ name: str
79
+ room: str
80
+ email: str
81
+
82
+ class DrawnCard(BaseModel):
83
+ name: str
84
+ role: Optional[str] = ""
85
+ rarity: str
86
+ power: int
87
+ bio: Optional[str] = ""
88
+ powers: Optional[list[str]] = []
89
+ weaknesses: Optional[list[str]] = []
90
+
91
+ class OrderSummary(BaseModel):
92
+ counts: Dict[str, int]
93
+ v100: int
94
+ valueSum: int
95
+
96
+ class OrderPayload(BaseModel):
97
+ season: int
98
+ info: StudentInfo
99
+ picked: list[DrawnCard]
100
+ summary: OrderSummary
101
+ ts: Optional[int] = None
102
+
103
+ class SuggestionPayload(BaseModel):
104
+ name: str
105
+ role: str
106
+ why: str
107
+ powers: list[str] = []
108
+ weaknesses: list[str] = []
109
+ ts: Optional[int] = None
110
+
111
+ # =========================
112
+ # FastAPI app
113
+ # =========================
114
+ app = FastAPI(title=TITLE, version="1.0.0")
115
+
116
+ # CORS so your static site can call this
117
+ app.add_middleware(
118
+ CORSMiddleware,
119
+ allow_origins=["*"], # lock this down to your site if you want
120
+ allow_methods=["GET", "POST", "OPTIONS"],
121
+ allow_headers=["*"],
122
+ )
123
+
124
+ def require_api_key(request: Request):
125
+ """If API_KEY is set, require X-API-Key to match."""
126
+ if not API_KEY:
127
+ return True
128
+ if request.headers.get("X-API-Key") != API_KEY:
129
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
130
+ return True
131
+
132
+ # ---------- Utility ----------
133
+ def fmt_ts(ts: int) -> str:
134
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
135
+
136
+ # ---------- Routes ----------
137
+ @app.get("/", response_class=HTMLResponse)
138
+ def root():
139
+ return f"""
140
+ <html><head><meta charset="utf-8"/><title>{TITLE}</title></head>
141
+ <body style="font-family: ui-sans-serif, system-ui; max-width: 920px; margin: 40px auto; line-height:1.5">
142
+ <h1>{TITLE}</h1>
143
+ <p>Use the JSON endpoints from your frontend.</p>
144
+ <ul>
145
+ <li><code>GET /health</code></li>
146
+ <li><code>GET /messages</code></li>
147
+ <li><code>POST /ingest</code> &mdash; <code>{"{ text: string }"}</code></li>
148
+ <li><code>POST /orders</code> &mdash; order object</li>
149
+ <li><code>POST /suggestions</code> &mdash; suggestion object</li>
150
+ </ul>
151
+ <p>If <code>API_KEY</code> is set, include header <code>X-API-Key: &lt;key&gt;</code> on POSTs.</p>
152
+ </body></html>
153
+ """
154
+
155
+ @app.get("/health")
156
+ def health():
157
+ return {"ok": True, "time": int(time.time())}
158
+
159
+ @app.get("/messages")
160
+ def get_messages(limit: int = 500):
161
+ rows = list_messages(limit=limit)
162
+ return {
163
+ "messages": [
164
+ {"id": r[0], "kind": r[1], "text": r[2], "payload": r[3], "ts": r[4], "ts_readable": fmt_ts(r[4])}
165
+ for r in rows
166
+ ]
167
+ }
168
+
169
+ @app.post("/ingest")
170
+ def ingest_text(payload: IngestText, _: bool = Depends(require_api_key)):
171
+ mid = insert_message("text", payload.text.strip())
172
  return {"ok": True, "id": mid}
173
 
174
+ @app.post("/orders")
175
+ def ingest_order(order: OrderPayload, _: bool = Depends(require_api_key)):
176
+ # server-side markdown-style summary for the feed
 
 
 
 
177
  lines = []
178
+ lines.append(f"# πŸ“¦ Season {order.season} Order")
179
+ lines.append(f"**Name:** {order.info.name}")
180
+ lines.append(f"**Class:** {order.info.room}")
181
+ lines.append(f"**Email:** {order.info.email}")
182
  lines.append("")
183
  lines.append("## Results (x10)")
184
+ for i, c in enumerate(order.picked, 1):
185
+ lines.append(f"{i}. **{c.name}** β€” _{c.rarity}_, Power **{c.power}**")
186
+ counts = order.summary.counts
187
  lines.append("")
188
+ lines.append(
189
+ f"**Rarity Breakdown:** C:{counts.get('Common',0)} β€’ U:{counts.get('Uncommon',0)} β€’ "
190
+ f"R:{counts.get('Rare',0)} β€’ UR:{counts.get('Ultra Rare',0)} β€’ L:{counts.get('Legendary',0)}"
191
+ )
192
+ lines.append(f"**Value Score:** {order.summary.v100}/100 (raw {order.summary.valueSum})")
193
+ summary_md = "\n".join(lines)
194
+
195
+ mid = insert_message("order", summary_md, json.dumps(order.dict(), ensure_ascii=False))
196
  return {"ok": True, "id": mid}
197
 
198
+ @app.post("/suggestions")
199
+ def ingest_suggestion(s: SuggestionPayload, _: bool = Depends(require_api_key)):
 
 
 
 
 
 
 
 
200
  lines = []
201
  lines.append("# πŸ’‘ New Card Suggestion")
202
+ lines.append(f"**Name:** {s.name}")
203
+ lines.append(f"**Role:** {s.role}")
204
+ lines.append(f"**Why:** {s.why}")
205
+ lines.append(f"**Powers:** {', '.join(s.powers) if s.powers else 'β€”'}")
206
+ lines.append(f"**Weaknesses:** {', '.join(s.weaknesses) if s.weaknesses else 'β€”'}")
207
+ summary_md = "\n".join(lines)
208
+
209
+ mid = insert_message("suggestion", summary_md, json.dumps(s.dict(), ensure_ascii=False))
 
210
  return {"ok": True, "id": mid}
211
 
212
+ @app.post("/admin/wipe")
213
+ def admin_wipe(_: bool = Depends(require_api_key)):
214
+ clear_messages()
215
+ return {"ok": True}