andito HF Staff commited on
Commit
11304e8
·
verified ·
1 Parent(s): 26934a0

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +282 -0
app.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ORDERS_PATH = "orders.csv"
19
+ SENT_PATH = "sent.csv"
20
+ KEYS_PATH = "keys.csv"
21
+
22
+ # # Optional SMTP config (Space secrets)
23
+ # SMTP_HOST = os.getenv("SMTP_HOST")
24
+ # SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
25
+ # SMTP_USER = os.getenv("SMTP_USER")
26
+ # SMTP_PASS = os.getenv("SMTP_PASS")
27
+ # FROM_EMAIL = os.getenv("FROM_EMAIL", SMTP_USER or "no-reply@example.com")
28
+
29
+ api = HfApi(token=HF_TOKEN)
30
+ _lock = threading.Lock()
31
+
32
+ # Simple cache to reduce Hub reads
33
+ _cache = {
34
+ "ts": 0.0,
35
+ "orders": None, # type: ignore
36
+ "sent": None, # type: ignore
37
+ "keys": None, # type: ignore
38
+ }
39
+ CACHE_TTL_SEC = 10.0
40
+
41
+ # -------------------------
42
+ # Helpers
43
+ # -------------------------
44
+ def _utc_now_iso() -> str:
45
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
46
+
47
+ def hash_order(order_number: str) -> str:
48
+ normalized = order_number.strip().replace(" ", "")
49
+ return normalized
50
+
51
+ def _download_csv(repo_id: str, path_in_repo: str) -> str:
52
+ # Download file bytes from the dataset repo, return text
53
+ file_bytes = api.hf_hub_download(
54
+ repo_id=repo_id,
55
+ filename=path_in_repo,
56
+ repo_type="dataset",
57
+ token=HF_TOKEN,
58
+ )
59
+ with open(file_bytes, "r", encoding="utf-8") as f:
60
+ return f.read()
61
+
62
+ def _upload_csv(repo_id: str, path_in_repo: str, content: str, commit_message: str) -> None:
63
+ api.upload_file(
64
+ path_or_fileobj=io.BytesIO(content.encode("utf-8")),
65
+ path_in_repo=path_in_repo,
66
+ repo_id=repo_id,
67
+ repo_type="dataset",
68
+ commit_message=commit_message,
69
+ token=HF_TOKEN,
70
+ )
71
+
72
+ def _parse_orders(csv_text: str) -> Dict[str, Dict[str, str]]:
73
+ rows: Dict[str, Dict[str, str]] = {}
74
+ reader = csv.DictReader(io.StringIO(csv_text))
75
+ for r in reader:
76
+ rows[r["order_number"]] = {
77
+ "order_number": r["order_number"],
78
+ "contact_person": r["contact_person"],
79
+ "email_address": r["email_address"],
80
+ "quantity": r["quantity"],
81
+ }
82
+ return rows
83
+
84
+ def _parse_sent(csv_text: str) -> Dict[str, Dict[str, str]]:
85
+ rows: Dict[str, Dict[str, str]] = {}
86
+ reader = csv.DictReader(io.StringIO(csv_text))
87
+ for r in reader:
88
+ rows[r["order_number"]] = {
89
+ "order_number": r["order_number"],
90
+ "quantity": r["quantity"],
91
+ "keys_sent": (r.get("keys_sent") or "").strip(),
92
+ "quantity_keys_sent": (r.get("quantity_keys_sent") or "").strip(),
93
+ }
94
+ return rows
95
+
96
+ def _parse_keys(csv_text: str) -> list:
97
+ keys = []
98
+ reader = csv.DictReader(io.StringIO(csv_text))
99
+ for r in reader:
100
+ if r["key"].strip():
101
+ keys.append({
102
+ "key": r["key"].strip(),
103
+ "sent": (r.get("sent") or "").strip(),
104
+ })
105
+ return keys
106
+
107
+ def _serialize_sent(rows: Dict[str, Dict[str, str]]) -> str:
108
+ out = io.StringIO()
109
+ fieldnames = ["order_number", "quantity", "keys_sent", "quantity_keys_sent"]
110
+ w = csv.DictWriter(out, fieldnames=fieldnames)
111
+ w.writeheader()
112
+ for _, r in rows.items():
113
+ w.writerow({
114
+ "order_number": r["order_number"],
115
+ "quantity": r["quantity"],
116
+ "keys_sent": r.get("keys_sent", ""),
117
+ "quantity_keys_sent": r.get("quantity_keys_sent", ""),
118
+ })
119
+ return out.getvalue()
120
+
121
+ def _serialize_keys(keys: list) -> str:
122
+ out = io.StringIO()
123
+ fieldnames = ["key", "sent"]
124
+ w = csv.DictWriter(out, fieldnames=fieldnames)
125
+ w.writeheader()
126
+ for k in keys:
127
+ w.writerow({
128
+ "key": k["key"],
129
+ "sent": k.get("sent", ""),
130
+ })
131
+ return out.getvalue()
132
+
133
+ def load_state(force: bool = False):
134
+ now = time.time()
135
+ if (not force) and _cache["orders"] is not None and (now - _cache["ts"] < CACHE_TTL_SEC):
136
+ return _cache["orders"], _cache["sent"], _cache["keys"]
137
+
138
+ orders_csv = _download_csv(DATASET_REPO_ID, ORDERS_PATH)
139
+ sent_csv = _download_csv(DATASET_REPO_ID, SENT_PATH)
140
+ keys_csv = _download_csv(DATASET_REPO_ID, KEYS_PATH)
141
+
142
+ orders = _parse_orders(orders_csv)
143
+ sent = _parse_sent(sent_csv)
144
+ keys = _parse_keys(keys_csv)
145
+
146
+ _cache["ts"] = now
147
+ _cache["orders"] = orders
148
+ _cache["sent"] = sent
149
+ _cache["keys"] = keys
150
+ return orders, sent, keys
151
+
152
+ def send_key_email(to_email: str, keys: list, contact_person: str) -> None:
153
+ """
154
+ Send keys to the customer via email.
155
+ """
156
+ # if not (SMTP_HOST and SMTP_USER and SMTP_PASS):
157
+ # return
158
+
159
+ # import smtplib
160
+ # from email.message import EmailMessage
161
+
162
+ # msg = EmailMessage()
163
+ # msg["Subject"] = "Your Gradium Keys"
164
+ # msg["From"] = FROM_EMAIL
165
+ # msg["To"] = to_email
166
+ # keys_text = "\n".join([f"- {k}" for k in keys])
167
+ # msg.set_content(
168
+ # f"Hi {contact_person},\n\n"
169
+ # f"Here are your Gradium keys:\n\n"
170
+ # f"{keys_text}\n\n"
171
+ # f"Best regards,\nGradium Team\n"
172
+ # )
173
+
174
+ # with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s:
175
+ # s.starttls()
176
+ # s.login(SMTP_USER, SMTP_PASS)
177
+ # s.send_message(msg)
178
+
179
+ pass
180
+
181
+ # -------------------------
182
+ # Core API
183
+ # -------------------------
184
+ def send_keys_for_order(
185
+ order_number: str,
186
+ request: Optional[gr.Request] = None,
187
+ ) -> Tuple[str, str]:
188
+ """
189
+ Returns (keys_sent, status_message)
190
+ """
191
+ if not order_number or not order_number.strip():
192
+ return "", "Missing order number."
193
+
194
+ order_number = order_number.strip()
195
+
196
+ _ = request
197
+
198
+ with _lock:
199
+ orders, sent, keys = load_state(force=True)
200
+
201
+ order = orders.get(order_number)
202
+ if order is None:
203
+ return "", f"Order number '{order_number}' not found."
204
+
205
+ sent_info = sent.get(order_number)
206
+ if sent_info is None:
207
+ return "", f"Order '{order_number}' not found in sent.csv."
208
+
209
+ quantity_needed = int(order["quantity"])
210
+
211
+ # Check if already sent
212
+ if sent_info["keys_sent"]:
213
+ return sent_info["keys_sent"], f"Keys already sent: {sent_info['keys_sent']}"
214
+
215
+ # Find available keys
216
+ available_keys = [k for k in keys if not k["sent"]]
217
+
218
+ if len(available_keys) < quantity_needed:
219
+ return "", f"Not enough available keys. Need {quantity_needed}, have {len(available_keys)}."
220
+
221
+ # Allocate keys
222
+ keys_to_send = available_keys[:quantity_needed]
223
+ keys_str = ",".join([k["key"] for k in keys_to_send])
224
+
225
+ # Mark keys as sent
226
+ for k in keys_to_send:
227
+ k["sent"] = _utc_now_iso()
228
+
229
+ # Update sent.csv
230
+ sent_info["keys_sent"] = keys_str
231
+ sent_info["quantity_keys_sent"] = str(quantity_needed)
232
+
233
+ # Save changes
234
+ updated_sent_csv = _serialize_sent(sent)
235
+ updated_keys_csv = _serialize_keys(keys)
236
+
237
+ _upload_csv(
238
+ DATASET_REPO_ID,
239
+ SENT_PATH,
240
+ updated_sent_csv,
241
+ commit_message=f"Send keys for order {order_number} at {_utc_now_iso()}",
242
+ )
243
+ _upload_csv(
244
+ DATASET_REPO_ID,
245
+ KEYS_PATH,
246
+ updated_keys_csv,
247
+ commit_message=f"Mark keys as sent for order {order_number} at {_utc_now_iso()}",
248
+ )
249
+
250
+ # refresh cache immediately
251
+ _cache["ts"] = 0.0
252
+
253
+ # Send email
254
+ try:
255
+ send_key_email(order["email_address"], keys_str.split(","), order["contact_person"])
256
+ return keys_str, f"Keys sent successfully to {order['email_address']}."
257
+ except Exception as e:
258
+ return keys_str, f"Keys allocated: {keys_str}. Email failed: {str(e)}"
259
+
260
+ # -------------------------
261
+ # UI
262
+ # -------------------------
263
+ with gr.Blocks() as demo:
264
+ gr.Markdown(
265
+ "## Gradium Key Distribution System\n\n"
266
+ "Enter an **order number** to send keys to the customer.\n\n"
267
+ )
268
+
269
+ order_in = gr.Textbox(label="Order number", placeholder="e.g. 0000-1111")
270
+ send_btn = gr.Button("Send Keys", variant="primary")
271
+ keys_out = gr.Textbox(label="Keys sent", interactive=False)
272
+ status_out = gr.Textbox(label="Status", interactive=False)
273
+
274
+ send_btn.click(
275
+ fn=send_keys_for_order,
276
+ inputs=[order_in],
277
+ outputs=[keys_out, status_out],
278
+ api_name="send_keys",
279
+ )
280
+
281
+ demo.queue()
282
+ demo.launch()