Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files
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 |
-
|
| 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 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
| 168 |
|
| 169 |
|
| 170 |
-
def
|
| 171 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
visitor = visitor_id(request)
|
| 173 |
n = int(n_bytes)
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
if mode == "Custom pool":
|
| 177 |
ids = list(custom_ids or [])
|
| 178 |
if not (2 <= len(ids) <= 8):
|
| 179 |
-
return
|
| 180 |
what, kwargs = "One of the picked sources", {"sources": ids}
|
| 181 |
else:
|
| 182 |
if not source_id:
|
| 183 |
-
return
|
| 184 |
what, kwargs = source_id, {"source": source_id}
|
| 185 |
if not (MIN_BYTES <= n <= MAX_BYTES):
|
| 186 |
-
return
|
| 187 |
|
| 188 |
refusal = limits.reserve(visitor)
|
| 189 |
if refusal:
|
| 190 |
-
return
|
| 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
|
| 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 |
-
|
| 206 |
-
|
| 207 |
-
|
| 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 |
-
|
| 250 |
-
.lr-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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 |
|