LightRiderInc commited on
Commit
c074e05
·
verified ·
1 Parent(s): 6414ff6

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +127 -28
app.py CHANGED
@@ -17,6 +17,7 @@ place, and this app never generates randomness itself.
17
  from __future__ import annotations
18
 
19
  import datetime as dt
 
20
  import json
21
  import os
22
  import threading
@@ -30,7 +31,7 @@ MIN_BYTES, MAX_BYTES = 16, 512
30
  PER_VISITOR_DAILY = int(os.environ.get("DEMO_PER_VISITOR_DAILY", "5"))
31
  GLOBAL_DAILY = int(os.environ.get("DEMO_GLOBAL_DAILY", "200"))
32
  PUBLIC_SITE = "https://entropy.lightriderinc.com"
33
- BRAND = "#ef3b39"
34
 
35
  # Reads LR_API_KEY from the environment.
36
  client = lre.EntropyAPI(max_retries=1, timeout=60)
@@ -160,39 +161,119 @@ def _friendly(err: lre.EntropyAPIError, what: str) -> str:
160
 
161
  def _badge(ok: bool) -> str:
162
  if ok:
163
- return (
164
- '<div class="lr-badge lr-ok">✔ Verified: this receipt is signed by Light Rider and hasn\'t been '
165
- "changed.</div>"
166
- )
167
- return '<div class="lr-badge lr-bad">✘ Not verified: the receipt\'s signature didn\'t match Light Rider\'s public key.</div>'
 
 
168
 
169
 
170
- def generate(mode: str, source_id: str | None, custom_ids: list[str] | None, n_bytes: float, request: gr.Request | None = None):
171
- """One draw. Returns (hex, badge, receipt, details, usage note)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  visitor = visitor_id(request)
173
  n = int(n_bytes)
174
- empty = ("", "", "")
 
 
 
175
 
176
  if mode == "Custom pool":
177
  ids = list(custom_ids or [])
178
  if not (2 <= len(ids) <= 8):
179
- return *empty, "Pick **2 to 8** sources for a custom pool.", _usage(visitor)
180
  what, kwargs = "One of the picked sources", {"sources": ids}
181
  else:
182
  if not source_id:
183
- return *empty, "Pick a source first (use **Refresh sources** if the list is empty).", _usage(visitor)
184
  what, kwargs = source_id, {"source": source_id}
185
  if not (MIN_BYTES <= n <= MAX_BYTES):
186
- return *empty, f"Choose {MIN_BYTES} to {MAX_BYTES} bytes.", _usage(visitor)
187
 
188
  refusal = limits.reserve(visitor)
189
  if refusal:
190
- return *empty, refusal, _usage(visitor)
191
  try:
192
  result = client.get(n, **kwargs)
193
  except lre.EntropyAPIError as err:
194
  limits.release(visitor) # a failed draw is never charged
195
- return *empty, _friendly(err, what), _usage(visitor)
196
 
197
  try:
198
  verified = client.verify(result.receipt)
@@ -202,11 +283,10 @@ def generate(mode: str, source_id: str | None, custom_ids: list[str] | None, n_b
202
  # Exact text, built in Python: a browser JSON viewer would round the
203
  # 64-bit timestamp, and a rounded receipt no longer verifies.
204
  receipt_text = json.dumps(receipt, indent=2, ensure_ascii=False)
205
- details = (
206
- f"**{len(result.data)} bytes** from **{', '.join(result.sources)}** · request `{result.request_id}` · "
207
- f"policy `{receipt.get('policy')}` · signed {receipt.get('signature_alg')}"
208
- )
209
- return result.hex, _badge(verified), receipt_text, details, _usage(visitor)
210
 
211
 
212
  def _usage(visitor: str) -> str:
@@ -244,17 +324,29 @@ CODE_TABS = {
244
 
245
  CODE_FOOTER = 'Create your API key at [entropy.lightriderinc.com/developers](https://entropy.lightriderinc.com/developers). It gives full access to your account, so keep it out of public code. Sources: `anu`, `cisco`, `qispace`, `rigetti`, `iqm`, `iqm-qec`. Price: 1 credit ($0.01) per 256 bytes.'
246
 
 
 
247
  CSS = f"""
248
- .lr-badge {{ padding: 10px 14px; border-radius: 8px; font-weight: 600; }}
249
- .lr-ok {{ background: #e8f5e9; color: #1b5e20; border: 1px solid #a5d6a7; }}
250
- .lr-bad {{ background: #fdecea; color: #b71c1c; border: 1px solid #f5c2c0; }}
251
- #lr-generate {{ background: {BRAND}; color: white; }}
 
 
 
 
 
 
 
252
  """
253
 
254
 
255
  def build() -> gr.Blocks:
256
  with gr.Blocks(title="Light Rider Quantum Entropy") as demo:
257
  gr.Markdown(INTRO)
 
 
 
258
  with gr.Tab("Generate"):
259
  with gr.Row():
260
  with gr.Column(scale=1):
@@ -270,9 +362,12 @@ def build() -> gr.Blocks:
270
  with gr.Column(scale=2):
271
  message = gr.Markdown()
272
  out_hex = gr.Textbox(label="Entropy (hex)", lines=4, buttons=["copy"], interactive=False)
273
- badge = gr.HTML()
274
- with gr.Accordion("Signed receipt (JSON)", open=False):
275
- receipt = gr.Code(language="json", interactive=False, show_label=False)
 
 
 
276
  with gr.Tab("Use in your code"):
277
  with gr.Tabs():
278
  for label, text in CODE_TABS.items():
@@ -283,13 +378,17 @@ def build() -> gr.Blocks:
283
  mode.change(toggle_mode, mode, [source, custom], api_visibility="private")
284
  refresh.click(load_sources, None, [source, custom, status], api_visibility="private")
285
  demo.load(load_sources, None, [source, custom, status], api_visibility="private")
 
 
286
  go.click(
287
  generate,
288
- [mode, source, custom, n_bytes],
289
- [out_hex, badge, receipt, message, usage],
290
  api_visibility="private",
291
  concurrency_limit=4,
292
  )
 
 
293
  return demo
294
 
295
 
 
17
  from __future__ import annotations
18
 
19
  import datetime as dt
20
+ import html
21
  import json
22
  import os
23
  import threading
 
31
  PER_VISITOR_DAILY = int(os.environ.get("DEMO_PER_VISITOR_DAILY", "5"))
32
  GLOBAL_DAILY = int(os.environ.get("DEMO_GLOBAL_DAILY", "200"))
33
  PUBLIC_SITE = "https://entropy.lightriderinc.com"
34
+ BUTTON_RED = "#dc2626" # brand red, deep enough for white text (4.8:1)
35
 
36
  # Reads LR_API_KEY from the environment.
37
  client = lre.EntropyAPI(max_retries=1, timeout=60)
 
161
 
162
  def _badge(ok: bool) -> str:
163
  if ok:
164
+ return '<div class="lr-badge lr-ok">✔ Verified: signed by Light Rider and unchanged.</div>'
165
+ return '<div class="lr-badge lr-bad">✘ Not verified: the signature didn\'t match Light Rider\'s public key.</div>'
166
+
167
+
168
+ # --------------------------------------------------------------------- receipt view
169
+
170
+ MODE_LABELS = {"single_source": "Single source", "custom_pool": "Custom pool", "multi_source": "Custom pool"}
171
 
172
 
173
+ def _local_time(ns, offset_min, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
174
+ """A receipt time in the visitor's local time. `offset_min` is the
175
+ browser's getTimezoneOffset() (minutes behind UTC); UTC if unknown."""
176
+ try:
177
+ t = dt.datetime.fromtimestamp(int(ns) / 1e9, dt.timezone.utc)
178
+ except (TypeError, ValueError, OverflowError, OSError):
179
+ return "unknown"
180
+ try:
181
+ off = int(offset_min)
182
+ except (TypeError, ValueError):
183
+ off = 0
184
+ local = t.astimezone(dt.timezone(dt.timedelta(minutes=-off)))
185
+ if off == 0:
186
+ zone = "UTC"
187
+ else:
188
+ h, m = divmod(abs(off), 60)
189
+ zone = f"UTC{'+' if off < 0 else '-'}{h}" + (f":{m:02d}" if m else "")
190
+ return f"{local.strftime(fmt)} ({zone})"
191
+
192
+
193
+ def receipt_view(receipt: dict, verified: bool, offset_min) -> str:
194
+ """Status line and a readable summary of a receipt (the signed JSON is shown below it)."""
195
+ e = html.escape
196
+
197
+ def mono(v) -> str:
198
+ return f"<code>{e(str(v))}</code>"
199
+
200
+ policy = str(receipt.get("policy", ""))
201
+ sig = f"{'✔ Verified' if verified else '✘ Not verified'} ({e(str(receipt.get('signature_alg', '')))})"
202
+ fields = [
203
+ ("Mode", e(MODE_LABELS.get(policy, policy.replace("_", " ").capitalize()))),
204
+ ("Pool", mono(receipt.get("pool_id", ""))),
205
+ ("Contributing sources", ", ".join(mono(s) for s in receipt.get("contributing_sources", []))),
206
+ ("Bytes", e(str(receipt.get("output_bytes", "")))),
207
+ ("Input min-entropy", e(f"{receipt.get('input_min_entropy_bits', '')} bits")),
208
+ ("Quality score", e(str(receipt.get("quality_score", "")))),
209
+ ("Extractor", e(str(receipt.get("extractor_alg", "")))),
210
+ ("DRBG", e(str(receipt.get("drbg_alg", "")))),
211
+ ("Issued at", e(_local_time(receipt.get("timestamp_unix_ns"), offset_min))),
212
+ ("Request ID", mono(receipt.get("request_id", ""))),
213
+ ("Signature", sig),
214
+ ]
215
+ grid = "".join(f'<div><span class="lr-label">{k}</span><span class="lr-value">{v}</span></div>' for k, v in fields)
216
+ return f'{_badge(verified)}<div class="lr-card"><div class="lr-grid">{grid}</div></div>'
217
+
218
+
219
+ # --------------------------------------------------------------------- your draws (this browser session)
220
+ #
221
+ # Kept in Gradio session state only, and only what the receipt already says:
222
+ # never the bytes. It ends with the session.
223
+
224
+ HISTORY_HEADERS = ["Time", "Source", "Bytes", "Request ID"]
225
+
226
+
227
+ def history_rows(history: list[dict], offset_min) -> list[list]:
228
+ return [
229
+ [_local_time(d["receipt"].get("timestamp_unix_ns"), offset_min, "%H:%M:%S"),
230
+ ", ".join(d["receipt"].get("contributing_sources", [])),
231
+ d["receipt"].get("output_bytes"),
232
+ d["receipt"].get("request_id")]
233
+ for d in history
234
+ ]
235
+
236
+
237
+ def show_draw(history: list[dict], offset_min, evt: gr.SelectData):
238
+ """Clicking a row in "Your draws" shows that draw's receipt."""
239
+ row = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
240
+ if not history or row is None or not (0 <= row < len(history)):
241
+ return gr.update(), gr.update(), gr.update(), gr.update()
242
+ d = history[row]
243
+ note = "Showing an earlier draw. Its bytes were shown once and aren't kept; this is its receipt."
244
+ return "", receipt_view(d["receipt"], d["verified"], offset_min), d["receipt_text"], note
245
+
246
+
247
+ def generate(mode: str, source_id: str | None, custom_ids: list[str] | None, n_bytes: float,
248
+ history: list[dict], offset_min, request: gr.Request | None = None):
249
+ """One draw. Returns (hex, receipt view, receipt JSON, message, usage note, history, history rows)."""
250
  visitor = visitor_id(request)
251
  n = int(n_bytes)
252
+ history = list(history or [])
253
+
254
+ def fail(message: str):
255
+ return "", gr.update(), gr.update(), message, _usage(visitor), history, gr.update()
256
 
257
  if mode == "Custom pool":
258
  ids = list(custom_ids or [])
259
  if not (2 <= len(ids) <= 8):
260
+ return fail("Pick **2 to 8** sources for a custom pool.")
261
  what, kwargs = "One of the picked sources", {"sources": ids}
262
  else:
263
  if not source_id:
264
+ return fail("Pick a source first (use **Refresh sources** if the list is empty).")
265
  what, kwargs = source_id, {"source": source_id}
266
  if not (MIN_BYTES <= n <= MAX_BYTES):
267
+ return fail(f"Choose {MIN_BYTES} to {MAX_BYTES} bytes.")
268
 
269
  refusal = limits.reserve(visitor)
270
  if refusal:
271
+ return fail(refusal)
272
  try:
273
  result = client.get(n, **kwargs)
274
  except lre.EntropyAPIError as err:
275
  limits.release(visitor) # a failed draw is never charged
276
+ return fail(_friendly(err, what))
277
 
278
  try:
279
  verified = client.verify(result.receipt)
 
283
  # Exact text, built in Python: a browser JSON viewer would round the
284
  # 64-bit timestamp, and a rounded receipt no longer verifies.
285
  receipt_text = json.dumps(receipt, indent=2, ensure_ascii=False)
286
+ history.insert(0, {"receipt": receipt, "receipt_text": receipt_text, "verified": verified})
287
+ message = f"**{len(result.data)} bytes** from **{', '.join(result.sources)}**."
288
+ return (result.hex, receipt_view(receipt, verified, offset_min), receipt_text, message, _usage(visitor),
289
+ history, history_rows(history, offset_min))
 
290
 
291
 
292
  def _usage(visitor: str) -> str:
 
324
 
325
  CODE_FOOTER = 'Create your API key at [entropy.lightriderinc.com/developers](https://entropy.lightriderinc.com/developers). It gives full access to your account, so keep it out of public code. Sources: `anu`, `cisco`, `qispace`, `rigetti`, `iqm`, `iqm-qec`. Price: 1 credit ($0.01) per 256 bytes.'
326
 
327
+ # Theme variables and translucent tints only, so every custom block reads in
328
+ # both the light and the dark theme.
329
  CSS = f"""
330
+ .lr-badge {{ padding: 10px 14px; border-radius: 8px; font-weight: 600; border: 1px solid;
331
+ color: var(--body-text-color); margin-bottom: 10px; }}
332
+ .lr-ok {{ background: rgba(34, 197, 94, 0.14); border-color: rgba(34, 197, 94, 0.6); }}
333
+ .lr-bad {{ background: rgba(239, 68, 68, 0.14); border-color: rgba(239, 68, 68, 0.6); }}
334
+ .lr-card {{ border: 1px solid var(--border-color-primary); border-radius: 8px; padding: 12px 14px;
335
+ background: var(--background-fill-secondary); }}
336
+ .lr-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 10px 18px; }}
337
+ .lr-label {{ display: block; font-size: 12px; color: var(--body-text-color); opacity: 0.75; }}
338
+ .lr-value {{ color: var(--body-text-color); overflow-wrap: anywhere; }}
339
+ .lr-value code {{ font-family: var(--font-mono); background: none; padding: 0; color: inherit; }}
340
+ #lr-generate {{ background: {BUTTON_RED}; color: white; }}
341
  """
342
 
343
 
344
  def build() -> gr.Blocks:
345
  with gr.Blocks(title="Light Rider Quantum Entropy") as demo:
346
  gr.Markdown(INTRO)
347
+ history = gr.State([])
348
+ # Browser-side value (a State lives on the server, out of reach of the page's JS).
349
+ tz_offset = gr.Number(value=0, visible=False)
350
  with gr.Tab("Generate"):
351
  with gr.Row():
352
  with gr.Column(scale=1):
 
362
  with gr.Column(scale=2):
363
  message = gr.Markdown()
364
  out_hex = gr.Textbox(label="Entropy (hex)", lines=4, buttons=["copy"], interactive=False)
365
+ view = gr.HTML()
366
+ receipt = gr.Code(language="json", interactive=False, label="Signed receipt (JSON)",
367
+ buttons=["copy", "download"])
368
+ gr.Markdown("### Your draws\nThis browser session only. Click a draw to see its receipt.")
369
+ history_table = gr.Dataframe(headers=HISTORY_HEADERS, value=[], interactive=False, show_label=False,
370
+ wrap=True)
371
  with gr.Tab("Use in your code"):
372
  with gr.Tabs():
373
  for label, text in CODE_TABS.items():
 
378
  mode.change(toggle_mode, mode, [source, custom], api_visibility="private")
379
  refresh.click(load_sources, None, [source, custom, status], api_visibility="private")
380
  demo.load(load_sources, None, [source, custom, status], api_visibility="private")
381
+ # The visitor's UTC offset, so receipt times show in their local time.
382
+ demo.load(None, None, tz_offset, js="() => new Date().getTimezoneOffset()", api_visibility="private")
383
  go.click(
384
  generate,
385
+ [mode, source, custom, n_bytes, history, tz_offset],
386
+ [out_hex, view, receipt, message, usage, history, history_table],
387
  api_visibility="private",
388
  concurrency_limit=4,
389
  )
390
+ history_table.select(show_draw, [history, tz_offset], [out_hex, view, receipt, message],
391
+ api_visibility="private")
392
  return demo
393
 
394