andito HF Staff commited on
Commit
c72e5a0
·
verified ·
1 Parent(s): 012e246

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +366 -0
app.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import csv
4
+ import time
5
+ import threading
6
+ from datetime import datetime, timezone
7
+ from typing import Dict, Optional, Tuple
8
+
9
+ import gradio as gr
10
+ from huggingface_hub import HfApi
11
+
12
+ # -------------------------
13
+ # Config
14
+ # -------------------------
15
+ DATASET_REPO_ID = os.environ.get("DATASET_REPO_ID") # Space secret
16
+ HF_TOKEN = os.environ["HF_TOKEN"] # Space secret
17
+
18
+ KEYS_PATH = "keys.csv"
19
+ B_KEYS_PATH = "b_keys.csv"
20
+
21
+
22
+ api = HfApi(token=HF_TOKEN)
23
+ _lock = threading.Lock()
24
+
25
+ # Simple cache to reduce Hub reads
26
+ _cache = {
27
+ "ts": 0.0,
28
+ "keys": None, # type: ignore
29
+ "b_keys": None, # type: ignore
30
+ }
31
+ CACHE_TTL_SEC = 10.0
32
+
33
+ # -------------------------
34
+ # Helpers
35
+ # -------------------------
36
+ def _utc_now_iso() -> str:
37
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
38
+
39
+ def _download_csv(repo_id: str, path_in_repo: str) -> str:
40
+ # Download file bytes from the dataset repo, return text
41
+ file_bytes = api.hf_hub_download(
42
+ repo_id=repo_id,
43
+ filename=path_in_repo,
44
+ repo_type="dataset",
45
+ token=HF_TOKEN,
46
+ )
47
+ with open(file_bytes, "r", encoding="utf-8") as f:
48
+ return f.read()
49
+
50
+ def _upload_csv(repo_id: str, path_in_repo: str, content: str, commit_message: str) -> None:
51
+ api.upload_file(
52
+ path_or_fileobj=io.BytesIO(content.encode("utf-8")),
53
+ path_in_repo=path_in_repo,
54
+ repo_id=repo_id,
55
+ repo_type="dataset",
56
+ commit_message=commit_message,
57
+ token=HF_TOKEN,
58
+ )
59
+
60
+ def _parse_keys(csv_text: str) -> list:
61
+ keys = []
62
+ reader = csv.DictReader(io.StringIO(csv_text))
63
+ for r in reader:
64
+ if r["key"].strip():
65
+ keys.append({
66
+ "key": r["key"].strip(),
67
+ "sent": (r.get("sent") or "").strip(),
68
+ })
69
+ return keys
70
+
71
+ def _parse_b_keys(csv_text: str) -> list:
72
+ keys = []
73
+ reader = csv.DictReader(io.StringIO(csv_text))
74
+ for r in reader:
75
+ if r["key"].strip():
76
+ keys.append({
77
+ "key": r["key"].strip(),
78
+ "count": int(r.get("count") or "0"),
79
+ })
80
+ return keys
81
+
82
+ def _serialize_keys(keys: list) -> str:
83
+ out = io.StringIO()
84
+ fieldnames = ["key", "sent"]
85
+ w = csv.DictWriter(out, fieldnames=fieldnames)
86
+ w.writeheader()
87
+ for k in keys:
88
+ w.writerow({
89
+ "key": k["key"],
90
+ "sent": k.get("sent", ""),
91
+ })
92
+ return out.getvalue()
93
+
94
+ def _serialize_b_keys(keys: list) -> str:
95
+ out = io.StringIO()
96
+ fieldnames = ["key", "count"]
97
+ w = csv.DictWriter(out, fieldnames=fieldnames)
98
+ w.writeheader()
99
+ for k in keys:
100
+ w.writerow({
101
+ "key": k["key"],
102
+ "count": k.get("count", 0),
103
+ })
104
+ return out.getvalue()
105
+
106
+ def load_state(force: bool = False):
107
+ now = time.time()
108
+ if (not force) and _cache["keys"] is not None and (now - _cache["ts"] < CACHE_TTL_SEC):
109
+ return _cache["keys"], _cache["b_keys"]
110
+
111
+ keys_csv = _download_csv(DATASET_REPO_ID, KEYS_PATH)
112
+ b_keys_csv = _download_csv(DATASET_REPO_ID, B_KEYS_PATH)
113
+
114
+ keys = _parse_keys(keys_csv)
115
+ b_keys = _parse_b_keys(b_keys_csv)
116
+
117
+ _cache["ts"] = now
118
+ _cache["keys"] = keys
119
+ _cache["b_keys"] = b_keys
120
+ return keys, b_keys
121
+
122
+ # -------------------------
123
+ # Core API
124
+ # -------------------------
125
+ def _get_user_info(request: Optional[gr.Request]) -> Optional[Dict]:
126
+ """
127
+ Extract user info from OAuth token in the request.
128
+ Returns None if user is not authenticated.
129
+ """
130
+ if request is None:
131
+ return None
132
+
133
+ # In Gradio with OAuth enabled, user info is available via request.username
134
+ # and other OAuth data via request.kwargs
135
+ if hasattr(request, 'username') and request.username:
136
+ return {
137
+ "username": request.username,
138
+ "headers": dict(request.headers) if hasattr(request, 'headers') else {}
139
+ }
140
+ return None
141
+
142
+ def claim_key(
143
+ request: Optional[gr.Request] = None,
144
+ ) -> Tuple[str, str]:
145
+ """
146
+ Returns the next available key.
147
+ Returns (key, status_message)
148
+ """
149
+ _ = request
150
+
151
+ with _lock:
152
+ keys, _ = load_state(force=True)
153
+
154
+ # Find available keys
155
+ available_keys = [k for k in keys if not k["sent"]]
156
+
157
+ if len(available_keys) < 1:
158
+ return "", "No available keys remaining."
159
+
160
+ # Send the next available key
161
+ key_to_send = available_keys[0]
162
+
163
+ # Mark key as sent
164
+ key_to_send["sent"] = _utc_now_iso()
165
+
166
+ # Save changes
167
+ updated_keys_csv = _serialize_keys(keys)
168
+
169
+ _upload_csv(
170
+ DATASET_REPO_ID,
171
+ KEYS_PATH,
172
+ updated_keys_csv,
173
+ commit_message=f"Key sent at {_utc_now_iso()}",
174
+ )
175
+
176
+ # refresh cache immediately
177
+ _cache["ts"] = 0.0
178
+
179
+ return key_to_send["key"], "Key sent successfully."
180
+
181
+ def claim_b_key(
182
+ request: Optional[gr.Request] = None,
183
+ ) -> Tuple[str, str]:
184
+ """
185
+ Returns the key and increments the usage count.
186
+ Returns (b_key, status_message)
187
+ """
188
+ _ = request
189
+
190
+ with _lock:
191
+ keys, b_keys = load_state(force=True)
192
+
193
+ if not b_keys or len(b_keys) == 0:
194
+ return "", "No keys available."
195
+
196
+ # Get the first key (you can add logic to rotate if needed)
197
+ b_key = b_keys[0]
198
+
199
+ # Increment the count
200
+ b_key["count"] += 1
201
+
202
+ # Save changes
203
+ updated_b_keys_csv = _serialize_b_keys(b_keys)
204
+
205
+ _upload_csv(
206
+ DATASET_REPO_ID,
207
+ B_KEYS_PATH,
208
+ updated_b_keys_csv,
209
+ commit_message=f"Key requested at {_utc_now_iso()}",
210
+ )
211
+
212
+ # refresh cache immediately
213
+ _cache["ts"] = 0.0
214
+
215
+ return b_key["key"], f"Key provided."
216
+
217
+ def claim_key_authenticated(
218
+ request: Optional[gr.Request] = None,
219
+ ) -> Tuple[str, str]:
220
+ """
221
+ OAuth-protected endpoint that returns the next available key.
222
+ Requires user to be authenticated via Hugging Face OAuth.
223
+ Returns (key, status_message)
224
+ """
225
+ # Check if user is authenticated
226
+ user_info = _get_user_info(request)
227
+
228
+ if user_info is None:
229
+ return "", "Authentication required. Please sign in with your Hugging Face account."
230
+
231
+ username = user_info["username"]
232
+
233
+ with _lock:
234
+ keys, _ = load_state(force=True)
235
+
236
+ # Find available keys
237
+ available_keys = [k for k in keys if not k["sent"]]
238
+
239
+ if len(available_keys) < 1:
240
+ return "", "No available keys remaining."
241
+
242
+ # Send the next available key
243
+ key_to_send = available_keys[0]
244
+
245
+ # Mark key as sent with user info
246
+ key_to_send["sent"] = f"{_utc_now_iso()} (user: {username})"
247
+
248
+ # Save changes
249
+ updated_keys_csv = _serialize_keys(keys)
250
+
251
+ _upload_csv(
252
+ DATASET_REPO_ID,
253
+ KEYS_PATH,
254
+ updated_keys_csv,
255
+ commit_message=f"Key sent to {username} at {_utc_now_iso()}",
256
+ )
257
+
258
+ # refresh cache immediately
259
+ _cache["ts"] = 0.0
260
+
261
+ return key_to_send["key"], f"Key sent successfully to {username}."
262
+
263
+ def claim_b_key_authenticated(
264
+ request: Optional[gr.Request] = None,
265
+ ) -> Tuple[str, str]:
266
+ """
267
+ OAuth-protected endpoint that returns the B key and increments the usage count.
268
+ Requires user to be authenticated via Hugging Face OAuth.
269
+ Returns (b_key, status_message)
270
+ """
271
+ # Check if user is authenticated
272
+ user_info = _get_user_info(request)
273
+
274
+ if user_info is None:
275
+ return "", "Authentication required. Please sign in with your Hugging Face account."
276
+
277
+ username = user_info["username"]
278
+
279
+ with _lock:
280
+ keys, b_keys = load_state(force=True)
281
+
282
+ if not b_keys or len(b_keys) == 0:
283
+ return "", "No keys available."
284
+
285
+ # Get the first key
286
+ b_key = b_keys[0]
287
+
288
+ # Increment the count
289
+ b_key["count"] += 1
290
+
291
+ # Save changes
292
+ updated_b_keys_csv = _serialize_b_keys(b_keys)
293
+
294
+ _upload_csv(
295
+ DATASET_REPO_ID,
296
+ B_KEYS_PATH,
297
+ updated_b_keys_csv,
298
+ commit_message=f"Key requested by {username} at {_utc_now_iso()}",
299
+ )
300
+
301
+ # refresh cache immediately
302
+ _cache["ts"] = 0.0
303
+
304
+ return b_key["key"], f"Key provided to {username}."
305
+
306
+ # -------------------------
307
+ # UI
308
+ # -------------------------
309
+ with gr.Blocks(title="API") as demo:
310
+ gr.Markdown("## API Service")
311
+ gr.Markdown("*Note: Authenticated endpoints require signing in with your Hugging Face account.*")
312
+
313
+ with gr.Tab("Service A"):
314
+ btn_a = gr.Button("Request", variant="secondary", size="sm")
315
+ out_a = gr.Textbox(label="Response", interactive=False, show_label=False)
316
+ status_a = gr.Textbox(label="", interactive=False, show_label=False)
317
+
318
+ btn_a.click(
319
+ fn=claim_key,
320
+ inputs=[],
321
+ outputs=[out_a, status_a],
322
+ api_name="claim_key",
323
+ )
324
+
325
+ with gr.Tab("Service B"):
326
+ btn_b = gr.Button("Request", variant="secondary", size="sm")
327
+ out_b = gr.Textbox(label="Response", interactive=False, show_label=False)
328
+ status_b = gr.Textbox(label="", interactive=False, show_label=False)
329
+
330
+ btn_b.click(
331
+ fn=claim_b_key,
332
+ inputs=[],
333
+ outputs=[out_b, status_b],
334
+ api_name="claim_b_key",
335
+ )
336
+
337
+ with gr.Tab("Service A (Authenticated)"):
338
+ gr.Markdown("### 🔐 Authentication Required")
339
+ gr.Markdown("This endpoint requires you to sign in with your Hugging Face account.")
340
+ btn_a_auth = gr.Button("Request Key (OAuth)", variant="primary", size="sm")
341
+ out_a_auth = gr.Textbox(label="Response", interactive=False, show_label=False)
342
+ status_a_auth = gr.Textbox(label="", interactive=False, show_label=False)
343
+
344
+ btn_a_auth.click(
345
+ fn=claim_key_authenticated,
346
+ inputs=[],
347
+ outputs=[out_a_auth, status_a_auth],
348
+ api_name="claim_key_authenticated",
349
+ )
350
+
351
+ with gr.Tab("Service B (Authenticated)"):
352
+ gr.Markdown("### 🔐 Authentication Required")
353
+ gr.Markdown("This endpoint requires you to sign in with your Hugging Face account.")
354
+ btn_b_auth = gr.Button("Request Key (OAuth)", variant="primary", size="sm")
355
+ out_b_auth = gr.Textbox(label="Response", interactive=False, show_label=False)
356
+ status_b_auth = gr.Textbox(label="", interactive=False, show_label=False)
357
+
358
+ btn_b_auth.click(
359
+ fn=claim_b_key_authenticated,
360
+ inputs=[],
361
+ outputs=[out_b_auth, status_b_auth],
362
+ api_name="claim_b_key_authenticated",
363
+ )
364
+
365
+ demo.queue()
366
+ demo.launch()