v0.5.0
This commit is contained in:
parent
8b8dc1788c
commit
316e3dc0bd
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,3 +21,6 @@ cli/zkac_cli/__pycache__
|
||||
# Experiments
|
||||
*.ipynb
|
||||
*.png
|
||||
|
||||
# wasm-pack output for browser demo
|
||||
demo/static/pkg/
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -748,7 +748,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zkac"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"chacha20poly1305",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zkac"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
description = "Zero-Knowledge Access Control: BBS+ anonymous credentials (BLS12-381) with encrypted transport (X25519/ChaCha20-Poly1305)"
|
||||
|
||||
|
||||
14
README.md
14
README.md
@ -4,6 +4,7 @@
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Changelog](CHANGELOG.md)** — releases and breaking API notes
|
||||
- **[Python API](docs/PYTHON_API.md)** — types and usage for `import zkac`
|
||||
- **[Security model](docs/SECURITY.md)** — threat model, assumptions, operational guidance
|
||||
- **[Fuzzing](docs/FUZZING.md)** — `cargo-fuzz` harnesses
|
||||
@ -19,16 +20,25 @@ Public API highlights: `zkac::Node`, `zkac::Credential`, `zkac::RoleRegistry`, `
|
||||
|
||||
## Python
|
||||
|
||||
Requires Rust toolchain and [maturin](https://www.maturin.rs/).
|
||||
Requires a Rust toolchain. [maturin](https://www.maturin.rs/) builds the `zkac` extension; it is not on your `PATH` until you install it.
|
||||
|
||||
```bash
|
||||
uv venv && source .venv/bin/activate
|
||||
maturin develop --features python
|
||||
# Pick one way to get the `maturin` command:
|
||||
uv sync --extra dev # installs maturin into this venv
|
||||
# or: uv pip install maturin
|
||||
# or: uvx maturin develop # no install; runs maturin once from PyPI
|
||||
|
||||
maturin develop # features come from [tool.maturin] in pyproject.toml
|
||||
# Console script ``zkac-node`` lives in ``cli/``; install it into the venv:
|
||||
uv sync --extra cli # or ``--extra demo`` (Flask demos + zkac-node)
|
||||
python -c "import zkac; print(zkac.role_id('admin').hex())"
|
||||
```
|
||||
|
||||
Run tests: `cargo test` and `pytest tests/test_zkac.py`.
|
||||
|
||||
Local web UI over the CLI: `demo/cli_web_server.py` (see `demo/README.md`). Optional in-browser WASM for the legacy Flask demo: `wasm/README.md` and `./demo/build_wasm.sh` (needs **rustup** + `wasm32-unknown-unknown`).
|
||||
|
||||
## License
|
||||
|
||||
See repository license file (if present).
|
||||
|
||||
@ -55,14 +55,16 @@ zkac-node auth bob --registry <REGISTRY_ID> --role analyst --server localhost:98
|
||||
| `registry get <id> <server> --registry R` | Fetch registry state |
|
||||
| `registry list <id>` | List registries this user owns locally |
|
||||
| `grant <id> --server S --registry R --role X --to <pk>` | Admin grant (encrypted to recipient pk) |
|
||||
| `credentials list <id> [--server S …]` | Local credentials + pending grants via detection tags + PIR |
|
||||
| `collect <id> <spec> [--pool-index N]` | Fetch and finalize a pending credential via single-server PIR |
|
||||
| `credentials list <id> [--server S …]` | Pending grants: tags + SimplePIR (handle row) + `get_grant_blob` + decrypt |
|
||||
| `collect <id> <spec> [--pool-index N]` | Same retrieval path, then claim |
|
||||
| `auth <id> --registry R --role X [--server S]` | Authenticated session |
|
||||
|
||||
## Protocol & threat model
|
||||
|
||||
See [docs/SECURITY.md](../docs/SECURITY.md) in the repo root for the full model, including PIR and detection tags.
|
||||
|
||||
**Custom clients:** Encrypted management JSON supports `pool_tags`, `pir_hints` / `pir_query`, **`get_grant_blob`** (after decoding the PIR handle), and `claim_grant`. Match the CLI’s two-phase mailbox fetch unless you embed ciphertext in the PIR row yourself.
|
||||
|
||||
**Operational scaling:** The server grant pool is append-only (claimed rows are tombstones), so pool length grows with every grant. Large pools increase discovery traffic, PIR query size, and server work per retrieval (all linear in pool length). Treat unbounded growth as a potential DoS and capacity risk; mitigations are listed under *Known limitations* and *Future work* in `docs/SECURITY.md`. Transport, BBS+ auth, registry state updates, and issuance queues have separate scaling profiles (CPU dominated by BBS+, state size linear in role count, queue memory); see **Scaling and complexity (transport, credentials, registries)** in the same doc.
|
||||
|
||||
## Storage layout
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "zkac-node"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["zkac"]
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import socket
|
||||
from pathlib import Path
|
||||
@ -22,8 +23,22 @@ def _unb64(s: str) -> bytes:
|
||||
|
||||
|
||||
def _parse_server(server: str) -> tuple[str, int]:
|
||||
host, _, port = server.rpartition(":")
|
||||
return host or "127.0.0.1", int(port)
|
||||
host, sep, port_s = server.rpartition(":")
|
||||
if not sep:
|
||||
raise ValueError(
|
||||
f"invalid server {server!r}: expected host:port (e.g. 127.0.0.1:9800). "
|
||||
"That is the TCP address of the running node, not the userid from "
|
||||
"`zkac-node serve <userid>`."
|
||||
)
|
||||
try:
|
||||
port = int(port_s, 10)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"invalid server {server!r}: port after ':' must be a number (host:port)"
|
||||
) from e
|
||||
if not 1 <= port <= 65535:
|
||||
raise ValueError(f"invalid server port {port}")
|
||||
return (host or "127.0.0.1"), port
|
||||
|
||||
|
||||
def parse_spec(spec: str) -> tuple[str, str, str]:
|
||||
@ -38,7 +53,10 @@ def _resolve_server_pk(userid: str, server: str) -> zkac.PublicKey:
|
||||
pin = store.load_server_pin(userid, server)
|
||||
if pin is None:
|
||||
raise RuntimeError(
|
||||
f"no pinned key for {server}; run: zkac-node server pin {userid} {server} --key <hex>"
|
||||
f"no pinned transport key for {server!r} under client {userid!r} "
|
||||
f"(pins are per client identity in ~/.zkac/{userid}/, not the userid "
|
||||
"passed to `zkac-node serve <…>` on the server). "
|
||||
f"Run: zkac-node server pin {userid} {server} --key <hex>"
|
||||
)
|
||||
return zkac.PublicKey.from_bytes(_unb64(pin["server_public_key_b64"]))
|
||||
|
||||
@ -104,6 +122,37 @@ def _save_cached_hints(userid: str, server: str, pool_version: str,
|
||||
(_cache_dir(userid) / f"{key}.bin").write_bytes(hints_bytes)
|
||||
|
||||
|
||||
def _fetch_hints_bytes(framed: FramedSession) -> tuple[bytes, str]:
|
||||
"""Download PIR hint blob; uses chunked wire protocol when hints exceed one frame."""
|
||||
resp = _ok(_mgmt_cmd(framed, {"cmd": "pir_hints"}))
|
||||
if "hints_b64" in resp:
|
||||
return _unb64(resp["hints_b64"]), resp["pool_version"]
|
||||
|
||||
total = int(resp["hints_total"])
|
||||
pv = resp["pool_version"]
|
||||
buf = bytearray()
|
||||
off = 0
|
||||
while off < total:
|
||||
resp = _ok(
|
||||
_mgmt_cmd(
|
||||
framed,
|
||||
{"cmd": "pir_hints", "offset": off, "pool_version": pv},
|
||||
)
|
||||
)
|
||||
if resp.get("pool_version") != pv:
|
||||
raise RuntimeError("PIR hints pool_version changed during download")
|
||||
piece = _unb64(resp["slice_b64"])
|
||||
if not piece and off < total:
|
||||
raise RuntimeError("PIR hints short read")
|
||||
buf.extend(piece)
|
||||
off += len(piece)
|
||||
if len(buf) != total:
|
||||
raise RuntimeError(
|
||||
f"PIR hints size mismatch (got {len(buf)}, expected {total})"
|
||||
)
|
||||
return bytes(buf), pv
|
||||
|
||||
|
||||
def _pir_client(userid: str, framed: FramedSession, server: str) -> tuple[zkac.PirClient, str]:
|
||||
"""Fetch pool_info, load or refresh hints, return (PirClient, pool_version)."""
|
||||
info = _ok(_mgmt_cmd(framed, {"cmd": "pool_info"}))
|
||||
@ -115,9 +164,7 @@ def _pir_client(userid: str, framed: FramedSession, server: str) -> tuple[zkac.P
|
||||
if cached is not None:
|
||||
return zkac.PirClient(cached, n, rb), pv
|
||||
|
||||
resp = _ok(_mgmt_cmd(framed, {"cmd": "pir_hints"}))
|
||||
hints_bytes = _unb64(resp["hints_b64"])
|
||||
pv = resp["pool_version"]
|
||||
hints_bytes, pv = _fetch_hints_bytes(framed)
|
||||
_save_cached_hints(userid, server, pv, n, rb, hints_bytes)
|
||||
return zkac.PirClient(hints_bytes, n, rb), pv
|
||||
|
||||
@ -127,13 +174,62 @@ def _fetch_row(
|
||||
pir_client: zkac.PirClient, pool_version: str, pool_index: int,
|
||||
) -> dict:
|
||||
q, state = pir_client.query(pool_index)
|
||||
resp = _ok(_mgmt_cmd(framed, {
|
||||
"cmd": "pir_query",
|
||||
"query_b64": _b64(q),
|
||||
"pool_version": pool_version,
|
||||
}))
|
||||
raw = bytes(pir_client.decode(_unb64(resp["answer_b64"]), state))
|
||||
return json.loads(raw.rstrip(b"\x00").decode("utf-8"))
|
||||
q_b64 = _b64(q)
|
||||
resp = _ok(
|
||||
_mgmt_cmd(
|
||||
framed,
|
||||
{"cmd": "pir_query", "query_b64": q_b64, "pool_version": pool_version},
|
||||
)
|
||||
)
|
||||
if "answer_b64" in resp:
|
||||
ans_bytes = _unb64(resp["answer_b64"])
|
||||
else:
|
||||
total = int(resp["answer_total"])
|
||||
buf = bytearray()
|
||||
off = 0
|
||||
while off < total:
|
||||
chunk = _ok(
|
||||
_mgmt_cmd(
|
||||
framed,
|
||||
{
|
||||
"cmd": "pir_query",
|
||||
"query_b64": q_b64,
|
||||
"pool_version": pool_version,
|
||||
"offset": off,
|
||||
},
|
||||
)
|
||||
)
|
||||
piece = _unb64(chunk["slice_b64"])
|
||||
if not piece and off < total:
|
||||
raise RuntimeError("PIR answer short read")
|
||||
buf.extend(piece)
|
||||
off += len(piece)
|
||||
if len(buf) != total:
|
||||
raise RuntimeError(
|
||||
f"PIR answer size mismatch (got {len(buf)}, expected {total})"
|
||||
)
|
||||
ans_bytes = bytes(buf)
|
||||
raw = bytes(pir_client.decode(ans_bytes, state))
|
||||
handle = json.loads(raw.rstrip(b"\x00").decode("utf-8"))
|
||||
if handle.get("v") != 1:
|
||||
raise RuntimeError("unsupported PIR handle version")
|
||||
grant_id = handle["g"]
|
||||
expect_digest = handle["h"]
|
||||
blob = _ok(
|
||||
_mgmt_cmd(framed, {"cmd": "get_grant_blob", "grant_id": grant_id}),
|
||||
)
|
||||
ct_b64 = blob["ciphertext_b64"]
|
||||
actual = hashlib.sha256(_unb64(ct_b64)).hexdigest()
|
||||
if actual != expect_digest:
|
||||
raise RuntimeError("grant ciphertext does not match PIR handle (tampered blob?)")
|
||||
row = {
|
||||
"grant_id": grant_id,
|
||||
"eph_pk_b64": blob["eph_pk_b64"],
|
||||
"ciphertext_b64": ct_b64,
|
||||
"to_tag_b64": blob.get("to_tag_b64", ""),
|
||||
"claimed": blob.get("claimed", False),
|
||||
}
|
||||
return row
|
||||
|
||||
|
||||
# ── Public operations ────────────────────────────────────────────────
|
||||
|
||||
@ -12,14 +12,16 @@ The server stores only cryptographically verified opaque blobs:
|
||||
<data_dir>/registries/<rid>.cert raw state cert bytes
|
||||
<data_dir>/mailbox/grants_pool.json anonymous append-only grant pool
|
||||
|
||||
Recipients discover matching grants via a cheap detection-tag index
|
||||
(``pool_tags``) and retrieve individual rows via single-server
|
||||
LWE-based PIR (``pir_query`` with precomputed ``pir_hints``).
|
||||
Recipients discover matching grants via a cheap detection-tag index
|
||||
(``pool_tags``). PIR (``pir_query``) returns a **small handle** per row;
|
||||
bulk ciphertext is fetched with ``get_grant_blob`` (split payload).
|
||||
Large PIR answers are streamed in slices (same pattern as ``pir_hints``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
@ -28,7 +30,11 @@ import traceback
|
||||
from pathlib import Path
|
||||
|
||||
import zkac
|
||||
from zkac.tcp import FramedSession, server_handshake_anon
|
||||
from zkac.tcp import MAX_TCP_FRAME_BYTES, FramedSession, server_handshake_anon
|
||||
|
||||
# Serialized PIR hints can be huge (64 KiB records × LWE width). Each mgmt reply must
|
||||
# stay under :data:`MAX_TCP_FRAME_BYTES` after encryption; base64 expands ~4/3.
|
||||
_PIR_HINT_CHUNK = min(131_072, max(16_384, (MAX_TCP_FRAME_BYTES * 3) // 5))
|
||||
|
||||
|
||||
def _b64(data: bytes) -> str:
|
||||
@ -39,11 +45,14 @@ def _unb64(s: str) -> bytes:
|
||||
return base64.b64decode(s)
|
||||
|
||||
|
||||
def _pad_grant_record(entry: dict) -> bytes:
|
||||
raw = json.dumps(entry, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
||||
def _pir_handle_bytes(grant_id: str, ciphertext_b64: str) -> bytes:
|
||||
"""Fixed-size PIR row: JSON handle binding ``grant_id`` to ciphertext (SHA-256)."""
|
||||
ct_digest = hashlib.sha256(_unb64(ciphertext_b64)).hexdigest()
|
||||
handle = {"v": 1, "g": grant_id, "h": ct_digest}
|
||||
raw = json.dumps(handle, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
||||
if len(raw) > zkac.PIR_RECORD_BYTES:
|
||||
raise ValueError(
|
||||
f"grant record exceeds PIR_RECORD_BYTES ({zkac.PIR_RECORD_BYTES})"
|
||||
f"PIR handle exceeds PIR_RECORD_BYTES ({zkac.PIR_RECORD_BYTES})"
|
||||
)
|
||||
return raw + b"\x00" * (zkac.PIR_RECORD_BYTES - len(raw))
|
||||
|
||||
@ -94,7 +103,7 @@ class _ServerStore:
|
||||
if not cert_path.exists():
|
||||
continue
|
||||
try:
|
||||
mgr.create(p.read_bytes(), cert_path.read_bytes())
|
||||
mgr.restore(p.read_bytes(), cert_path.read_bytes())
|
||||
count += 1
|
||||
except Exception as exc:
|
||||
print(f"[server] skip registry {rid_hex}: {exc}")
|
||||
@ -143,7 +152,7 @@ class _ServerStore:
|
||||
if self._pir_server is not None and not self._pir_dirty:
|
||||
return self._pir_server
|
||||
records = self._load_pool()
|
||||
packed = [_pad_grant_record(r) for r in records]
|
||||
packed = [_pir_handle_bytes(r["grant_id"], r["ciphertext_b64"]) for r in records]
|
||||
db = zkac.PirDatabase(packed, zkac.PIR_RECORD_BYTES)
|
||||
self._pir_server = zkac.PirServer(db)
|
||||
self._pir_dirty = False
|
||||
@ -153,8 +162,8 @@ class _ServerStore:
|
||||
|
||||
def post_grant(self, entry: dict) -> tuple[str, int]:
|
||||
grant_id = os.urandom(16).hex()
|
||||
_pir_handle_bytes(grant_id, entry["ciphertext_b64"])
|
||||
row = {"grant_id": grant_id, "claimed": False, **entry}
|
||||
_pad_grant_record(row)
|
||||
with self._lock:
|
||||
records = self._load_pool()
|
||||
pool_index = len(records)
|
||||
@ -190,14 +199,15 @@ class _ServerStore:
|
||||
pir = self._ensure_pir()
|
||||
return bytes(pir.hints()), bytes(pir.version()).hex()
|
||||
|
||||
def pir_answer(self, query_b64: str, pool_version: str) -> tuple[str, str] | None:
|
||||
def pir_answer_bytes(self, query_b64: str, pool_version: str) -> bytes | None:
|
||||
"""Return raw PIR answer bytes, or ``None`` if ``pool_version`` is stale."""
|
||||
with self._lock:
|
||||
pir = self._ensure_pir()
|
||||
current = bytes(pir.version()).hex()
|
||||
if current != pool_version:
|
||||
return None
|
||||
ans = pir.answer(_unb64(query_b64))
|
||||
return _b64(ans), current
|
||||
return bytes(ans)
|
||||
|
||||
def claim_grant(self, grant_id: str) -> dict | None:
|
||||
with self._lock:
|
||||
@ -211,11 +221,30 @@ class _ServerStore:
|
||||
return dict(e)
|
||||
return None
|
||||
|
||||
def get_grant_blob(self, grant_id: str) -> dict | None:
|
||||
"""Return public grant fields for second-phase fetch (after PIR handle)."""
|
||||
with self._lock:
|
||||
for e in self._load_pool():
|
||||
if e.get("grant_id") == grant_id:
|
||||
return {
|
||||
"eph_pk_b64": e.get("eph_pk_b64", ""),
|
||||
"ciphertext_b64": e.get("ciphertext_b64", ""),
|
||||
"to_tag_b64": e.get("to_tag_b64", ""),
|
||||
"claimed": bool(e.get("claimed", False)),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
# ── Command dispatch (inside encrypted session) ──────────────────────
|
||||
|
||||
def _dispatch(cmd: dict, mgr: zkac.RegistryManager, store: _ServerStore,
|
||||
server_pk_b64: str, transcript_hash: bytes) -> dict:
|
||||
def _dispatch(
|
||||
cmd: dict,
|
||||
mgr: zkac.RegistryManager,
|
||||
store: _ServerStore,
|
||||
server_pk_b64: str,
|
||||
transcript_hash: bytes,
|
||||
conn_ctx: dict,
|
||||
) -> dict:
|
||||
try:
|
||||
action = cmd.get("cmd")
|
||||
|
||||
@ -272,15 +301,83 @@ def _dispatch(cmd: dict, mgr: zkac.RegistryManager, store: _ServerStore,
|
||||
return {"ok": True, "tags": entries, "pool_version": version}
|
||||
|
||||
if action == "pir_hints":
|
||||
hints_bytes, version = store.pir_hints()
|
||||
return {"ok": True, "hints_b64": _b64(hints_bytes), "pool_version": version}
|
||||
raw, version = store.pir_hints()
|
||||
offset = cmd.get("offset")
|
||||
if offset is not None:
|
||||
if cmd.get("pool_version") != version:
|
||||
return {"error": "stale_version"}
|
||||
try:
|
||||
off = int(offset)
|
||||
except (TypeError, ValueError):
|
||||
return {"error": "bad offset"}
|
||||
if off < 0 or off > len(raw):
|
||||
return {"error": "bad offset"}
|
||||
piece = raw[off : off + _PIR_HINT_CHUNK]
|
||||
nxt = off + len(piece)
|
||||
return {
|
||||
"ok": True,
|
||||
"pool_version": version,
|
||||
"slice_b64": _b64(piece),
|
||||
"offset": off,
|
||||
"returned": len(piece),
|
||||
"done": nxt >= len(raw),
|
||||
}
|
||||
if len(raw) <= _PIR_HINT_CHUNK:
|
||||
return {"ok": True, "hints_b64": _b64(raw), "pool_version": version}
|
||||
return {
|
||||
"ok": True,
|
||||
"pool_version": version,
|
||||
"hints_total": len(raw),
|
||||
"chunk": _PIR_HINT_CHUNK,
|
||||
}
|
||||
|
||||
if action == "pir_query":
|
||||
result = store.pir_answer(cmd["query_b64"], cmd["pool_version"])
|
||||
if result is None:
|
||||
q_b64 = cmd.get("query_b64", "")
|
||||
pv = cmd.get("pool_version", "")
|
||||
offset = cmd.get("offset")
|
||||
|
||||
if offset is None:
|
||||
ans_bytes = store.pir_answer_bytes(q_b64, pv)
|
||||
if ans_bytes is None:
|
||||
return {"error": "stale_version"}
|
||||
conn_ctx.pop("pir_answer_buffer", None)
|
||||
conn_ctx.pop("pir_answer_pv", None)
|
||||
if len(ans_bytes) <= _PIR_HINT_CHUNK:
|
||||
return {"ok": True, "answer_b64": _b64(ans_bytes), "pool_version": pv}
|
||||
conn_ctx["pir_answer_buffer"] = ans_bytes
|
||||
conn_ctx["pir_answer_pv"] = pv
|
||||
return {
|
||||
"ok": True,
|
||||
"pool_version": pv,
|
||||
"answer_total": len(ans_bytes),
|
||||
"chunk": _PIR_HINT_CHUNK,
|
||||
}
|
||||
|
||||
if conn_ctx.get("pir_answer_pv") != pv:
|
||||
return {"error": "stale_version"}
|
||||
ans_b64, version = result
|
||||
return {"ok": True, "answer_b64": ans_b64, "pool_version": version}
|
||||
buf = conn_ctx.get("pir_answer_buffer")
|
||||
if buf is None:
|
||||
return {"error": "no PIR answer in progress"}
|
||||
try:
|
||||
off = int(offset)
|
||||
except (TypeError, ValueError):
|
||||
return {"error": "bad offset"}
|
||||
if off < 0 or off > len(buf):
|
||||
return {"error": "bad offset"}
|
||||
piece = buf[off : off + _PIR_HINT_CHUNK]
|
||||
nxt = off + len(piece)
|
||||
done = nxt >= len(buf)
|
||||
if done:
|
||||
conn_ctx.pop("pir_answer_buffer", None)
|
||||
conn_ctx.pop("pir_answer_pv", None)
|
||||
return {
|
||||
"ok": True,
|
||||
"pool_version": pv,
|
||||
"slice_b64": _b64(piece),
|
||||
"offset": off,
|
||||
"returned": len(piece),
|
||||
"done": done,
|
||||
}
|
||||
|
||||
if action == "claim_grant":
|
||||
entry = store.claim_grant(cmd["grant_id"])
|
||||
@ -288,6 +385,15 @@ def _dispatch(cmd: dict, mgr: zkac.RegistryManager, store: _ServerStore,
|
||||
return {"error": "grant not found"}
|
||||
return {"ok": True, "grant": entry}
|
||||
|
||||
if action == "get_grant_blob":
|
||||
gid = cmd.get("grant_id", "")
|
||||
if not gid:
|
||||
return {"error": "missing grant_id"}
|
||||
blob = store.get_grant_blob(gid)
|
||||
if blob is None:
|
||||
return {"error": "grant not found"}
|
||||
return {"ok": True, **blob}
|
||||
|
||||
return {"error": f"unknown command: {action}"}
|
||||
|
||||
except Exception as exc:
|
||||
@ -309,13 +415,14 @@ def _handle_conn(conn: socket.socket, addr: tuple, node: zkac.Node,
|
||||
op = hello.get("op")
|
||||
|
||||
if op == "mgmt":
|
||||
conn_ctx: dict = {}
|
||||
while True:
|
||||
try:
|
||||
data = framed.recv()
|
||||
except (ConnectionError, OSError):
|
||||
break
|
||||
cmd = json.loads(data)
|
||||
resp = _dispatch(cmd, mgr, store, server_pk_b64, transcript_hash)
|
||||
resp = _dispatch(cmd, mgr, store, server_pk_b64, transcript_hash, conn_ctx)
|
||||
framed.send(json.dumps(resp).encode())
|
||||
|
||||
elif op == "auth":
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ZKAC client for client-managed registries.
|
||||
|
||||
1. Issues a credential locally (admin issues to self — in production this
|
||||
would go through the server's E2E-encrypted issuance relay).
|
||||
2. Verifies the registry state certificate.
|
||||
3. Authenticates via managed-registry handshake.
|
||||
4. Sends a JSON request over the encrypted session.
|
||||
|
||||
Usage:
|
||||
python client_managed.py --role analyst
|
||||
python client_managed.py --role operator
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
import zkac
|
||||
from zkac.tcp import FramedSession, client_handshake_managed
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="ZKAC managed-registry client")
|
||||
ap.add_argument("--creds-dir", type=Path,
|
||||
default=Path(__file__).resolve().parent / "creds")
|
||||
ap.add_argument("--role", default="analyst", choices=["analyst", "operator"])
|
||||
ap.add_argument("--host", default="127.0.0.1")
|
||||
ap.add_argument("--port", type=int, default=9877)
|
||||
args = ap.parse_args()
|
||||
|
||||
creds_dir: Path = args.creds_dir
|
||||
admin_data = json.loads((creds_dir / "managed_admin.json").read_text(encoding="utf-8"))
|
||||
reg_data = json.loads((creds_dir / "managed_registry.json").read_text(encoding="utf-8"))
|
||||
transport_data = json.loads((creds_dir / "transport.json").read_text(encoding="utf-8"))
|
||||
|
||||
# Reconstruct admin issuer to issue a credential for the chosen role.
|
||||
# In production, this would be done via the E2E issuance relay.
|
||||
admin_issuer = zkac.BbsIssuer.from_secret_key(
|
||||
base64.b64decode(admin_data["admin_issuer_secret_b64"])
|
||||
)
|
||||
admin_pk = admin_issuer.public_key()
|
||||
|
||||
role_rid = zkac.role_id(args.role)
|
||||
req = zkac.prepare_blind_request()
|
||||
blind_sig = admin_issuer.issue_blind(req.commitment_with_proof(), role_rid, 1)
|
||||
cred = zkac.Credential.finalize(
|
||||
blind_sig, req.member_secret(), req.prover_blind(), role_rid, 1, admin_pk
|
||||
)
|
||||
|
||||
# Verify registry state certificate before trusting it
|
||||
state_bytes = base64.b64decode(reg_data["state_bytes_b64"])
|
||||
state_cert = base64.b64decode(reg_data["state_cert_b64"])
|
||||
registry_id = bytes.fromhex(reg_data["registry_id_hex"])
|
||||
|
||||
issuer_pk_for_verify = zkac.BbsPublicKey.from_bytes(
|
||||
base64.b64decode(reg_data["admin_issuer_public_key_b64"])
|
||||
)
|
||||
expected_rid = zkac.registry_id(issuer_pk_for_verify)
|
||||
assert expected_rid == registry_id, "registry_id mismatch"
|
||||
assert zkac.RegistryState.verify_cert(issuer_pk_for_verify, state_cert, state_bytes), \
|
||||
"state certificate verification failed"
|
||||
print(f"Registry state verified (registry_id={registry_id.hex()[:16]}…)")
|
||||
|
||||
# Connect and authenticate
|
||||
server_pk = zkac.PublicKey.from_bytes(
|
||||
base64.b64decode(transport_data["server_public_key_b64"])
|
||||
)
|
||||
node = zkac.Node(zkac.Keypair())
|
||||
|
||||
sock = socket.create_connection((args.host, args.port))
|
||||
try:
|
||||
session = client_handshake_managed(
|
||||
sock, node, server_pk, cred, registry_id
|
||||
)
|
||||
framed = FramedSession(sock, session)
|
||||
|
||||
framed.send(json.dumps({"path": "/api"}).encode())
|
||||
reply = json.loads(framed.recv().decode())
|
||||
print(json.dumps(reply, indent=2))
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,149 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ZKAC TCP server using client-managed registries.
|
||||
|
||||
Loads a registry from managed_registry.json (created by setup_managed_demo.py),
|
||||
verifies BBS+ state certificates, and authenticates clients against the registry.
|
||||
Also handles credential issuance requests via the E2E-encrypted relay.
|
||||
|
||||
Run setup_managed_demo.py first, then this server, then client_managed.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
import zkac
|
||||
from zkac.tcp import (
|
||||
FramedSession,
|
||||
read_frame,
|
||||
write_frame,
|
||||
server_handshake_managed,
|
||||
)
|
||||
|
||||
|
||||
def load_managed_registry(creds_dir: Path, mgr: zkac.RegistryManager) -> bytes:
|
||||
"""Load the managed registry state + cert into the manager. Returns registry_id."""
|
||||
r = json.loads((creds_dir / "managed_registry.json").read_text(encoding="utf-8"))
|
||||
state_bytes = base64.b64decode(r["state_bytes_b64"])
|
||||
state_cert = base64.b64decode(r["state_cert_b64"])
|
||||
rid = mgr.create(state_bytes, state_cert)
|
||||
return rid
|
||||
|
||||
|
||||
def _role_label(role_id: bytes) -> str:
|
||||
for name in ("analyst", "operator"):
|
||||
if role_id == zkac.role_id(name):
|
||||
return name
|
||||
return role_id.hex()[:16]
|
||||
|
||||
|
||||
def api_body_for_role(role_id: bytes) -> dict:
|
||||
if role_id == zkac.role_id("analyst"):
|
||||
return {
|
||||
"path": "/api",
|
||||
"role": "analyst",
|
||||
"datasets": ["summary", "aggregated_metrics"],
|
||||
"note": "Analyst tier: aggregated data only.",
|
||||
"registry": "client-managed",
|
||||
}
|
||||
if role_id == zkac.role_id("operator"):
|
||||
return {
|
||||
"path": "/api",
|
||||
"role": "operator",
|
||||
"datasets": ["summary", "aggregated_metrics", "raw_logs", "pii"],
|
||||
"note": "Operator tier: full access.",
|
||||
"registry": "client-managed",
|
||||
}
|
||||
return {"error": "unknown role", "path": "/api"}
|
||||
|
||||
|
||||
def handle_client(
|
||||
conn: socket.socket,
|
||||
addr: tuple,
|
||||
creds_dir: Path,
|
||||
mgr: zkac.RegistryManager,
|
||||
) -> None:
|
||||
peer = f"{addr[0]}:{addr[1]}"
|
||||
print(f"[zkac-managed] connect peer={peer}")
|
||||
|
||||
try:
|
||||
t = json.loads((creds_dir / "transport.json").read_text(encoding="utf-8"))
|
||||
sk = base64.b64decode(t["server_secret_key_b64"])
|
||||
server_kp = zkac.Keypair.from_secret_key(sk)
|
||||
node = zkac.Node(server_kp)
|
||||
|
||||
session, registry_id, role_id = server_handshake_managed(conn, node, mgr)
|
||||
label = _role_label(role_id)
|
||||
print(
|
||||
f"[zkac-managed] auth_ok peer={peer} registry={registry_id.hex()[:16]}… "
|
||||
f"role={label!r}"
|
||||
)
|
||||
|
||||
framed = FramedSession(conn, session)
|
||||
raw = framed.recv()
|
||||
req = json.loads(raw.decode("utf-8"))
|
||||
print(f"[zkac-managed] request peer={peer} {req!r}")
|
||||
|
||||
path = req.get("path")
|
||||
if path != "/api":
|
||||
body = {"error": "unsupported path", "got": path}
|
||||
else:
|
||||
body = api_body_for_role(role_id)
|
||||
|
||||
out = json.dumps(body).encode()
|
||||
framed.send(out)
|
||||
print(f"[zkac-managed] response peer={peer} {list(body.keys())}")
|
||||
|
||||
except (ConnectionError, BrokenPipeError, OSError) as e:
|
||||
print(f"[zkac-managed] peer={peer} connection_error: {e!r}")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
print(f"[zkac-managed] peer={peer} protocol_error: {e!r}")
|
||||
except Exception as e:
|
||||
print(f"[zkac-managed] peer={peer} unexpected_error: {e!r}")
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="ZKAC managed-registry TCP server")
|
||||
ap.add_argument("--creds-dir", type=Path,
|
||||
default=Path(__file__).resolve().parent / "creds")
|
||||
ap.add_argument("--host", default="127.0.0.1")
|
||||
ap.add_argument("--port", type=int, default=9877)
|
||||
args = ap.parse_args()
|
||||
|
||||
creds_dir: Path = args.creds_dir
|
||||
if not (creds_dir / "managed_registry.json").is_file():
|
||||
raise SystemExit(
|
||||
f"Missing {creds_dir}/managed_registry.json — run setup_managed_demo.py first."
|
||||
)
|
||||
|
||||
mgr = zkac.RegistryManager()
|
||||
rid = load_managed_registry(creds_dir, mgr)
|
||||
print(f"Loaded managed registry {rid.hex()[:16]}…")
|
||||
print(f"ZKAC managed {args.host}:{args.port}")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((args.host, args.port))
|
||||
sock.listen(8)
|
||||
while True:
|
||||
conn, addr = sock.accept()
|
||||
threading.Thread(
|
||||
target=handle_client,
|
||||
args=(conn, addr, creds_dir, mgr),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,105 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate credentials for the managed-registry demo.
|
||||
|
||||
Creates:
|
||||
creds/managed_admin.json – admin BBS+ issuer key + admin credential + issuance keypair
|
||||
creds/managed_registry.json – serialized registry state + cert (analyst + operator roles)
|
||||
creds/transport.json – server transport key (created if not already present)
|
||||
|
||||
Users request credentials through the server at runtime (E2E-encrypted issuance).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import zkac
|
||||
|
||||
ROLES = ("analyst", "operator")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Generate managed-registry demo files.")
|
||||
ap.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parent / "creds",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
out: Path = args.output_dir
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Admin BBS+ issuer (signs credentials AND certifies registry state)
|
||||
admin_issuer = zkac.BbsIssuer()
|
||||
admin_pk = admin_issuer.public_key()
|
||||
admin_rid = zkac.admin_role_id()
|
||||
|
||||
# Self-issue admin credential
|
||||
req = zkac.prepare_blind_request()
|
||||
sig = admin_issuer.issue_blind(req.commitment_with_proof(), admin_rid, 0)
|
||||
admin_cred = zkac.Credential.finalize(
|
||||
sig, req.member_secret(), req.prover_blind(), admin_rid, 0, admin_pk
|
||||
)
|
||||
|
||||
# X25519 issuance keypair for E2E-encrypted credential requests
|
||||
issuance_kp = zkac.IssuanceKeypair()
|
||||
|
||||
# Build registry state with roles (admin is also the issuer for all roles)
|
||||
role_entries = []
|
||||
for name in ROLES:
|
||||
role_entries.append((zkac.role_id(name), admin_pk, 1))
|
||||
|
||||
state = zkac.RegistryState.build(
|
||||
admin_pk, issuance_kp.public_key_bytes(), 1, b"\x00" * 32, role_entries
|
||||
)
|
||||
state_bytes = state.serialize()
|
||||
state_cert = state.certify(admin_cred)
|
||||
registry_id = state.registry_id()
|
||||
|
||||
# Save admin material
|
||||
admin_payload = {
|
||||
"admin_issuer_secret_b64": base64.b64encode(admin_issuer.secret_key_bytes()).decode(),
|
||||
"admin_issuer_public_key_b64": base64.b64encode(admin_pk.to_bytes()).decode(),
|
||||
"admin_member_secret_b64": base64.b64encode(req.member_secret()).decode(),
|
||||
"admin_prover_blind_b64": base64.b64encode(req.prover_blind()).decode(),
|
||||
"admin_blind_sig_b64": base64.b64encode(sig).decode(),
|
||||
"issuance_secret_b64": base64.b64encode(issuance_kp.secret_bytes()).decode(),
|
||||
"issuance_public_key_b64": base64.b64encode(issuance_kp.public_key_bytes()).decode(),
|
||||
"registry_id_hex": registry_id.hex(),
|
||||
}
|
||||
(out / "managed_admin.json").write_text(json.dumps(admin_payload, indent=2), encoding="utf-8")
|
||||
|
||||
# Save registry state + cert
|
||||
reg_payload = {
|
||||
"registry_id_hex": registry_id.hex(),
|
||||
"state_bytes_b64": base64.b64encode(state_bytes).decode(),
|
||||
"state_cert_b64": base64.b64encode(bytes(state_cert)).decode(),
|
||||
"admin_issuer_public_key_b64": base64.b64encode(admin_pk.to_bytes()).decode(),
|
||||
"issuance_public_key_b64": base64.b64encode(issuance_kp.public_key_bytes()).decode(),
|
||||
"roles": list(ROLES),
|
||||
}
|
||||
(out / "managed_registry.json").write_text(json.dumps(reg_payload, indent=2), encoding="utf-8")
|
||||
|
||||
# Transport key (create if not present)
|
||||
transport_path = out / "transport.json"
|
||||
if not transport_path.is_file():
|
||||
server_kp = zkac.Keypair()
|
||||
transport_payload = {
|
||||
"server_secret_key_b64": base64.b64encode(server_kp.secret_key_bytes()).decode(),
|
||||
"server_public_key_b64": base64.b64encode(server_kp.public_key().to_bytes()).decode(),
|
||||
}
|
||||
transport_path.write_text(json.dumps(transport_payload, indent=2), encoding="utf-8")
|
||||
|
||||
print(f"Wrote managed-registry demo files to {out}")
|
||||
print(f"Registry ID: {registry_id.hex()}")
|
||||
print(f"Roles: {', '.join(ROLES)}")
|
||||
print(f"\nAdmin can issue credentials for these roles through the server.")
|
||||
print(f"Users request credentials via E2E-encrypted issuance relay.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,6 +1,6 @@
|
||||
# ZKAC Python API Reference
|
||||
|
||||
Version 0.4.1. Cryptographic stack: **BBS+** on BLS12-381 (credentials), **X25519** + **ChaCha20-Poly1305** (transport), **Schnorr/Ristretto255** (identity), **BLAKE2b** (role IDs, signatures).
|
||||
Version 0.5.0. Cryptographic stack: **BBS+** on BLS12-381 (credentials), **X25519** + **ChaCha20-Poly1305** (transport), **Schnorr/Ristretto255** (identity), **BLAKE2b** (role IDs, signatures), **LWE** (single-server SimplePIR for mailbox handles).
|
||||
|
||||
```python
|
||||
import zkac
|
||||
@ -12,6 +12,33 @@ import zkac
|
||||
|
||||
Upper bound on BBS+ proof size in an encrypted auth packet (256 KiB). Larger proofs are rejected.
|
||||
|
||||
### `PIR_RECORD_BYTES`
|
||||
|
||||
Fixed byte length of each PIR plaintext row (mailbox **handle** JSON, padded with zeros). **256** in 0.5.0. Must match the server’s PIR database packing and the `record_bytes` argument to `PirClient(...)`.
|
||||
|
||||
## Single-server PIR (SimplePIR)
|
||||
|
||||
Used by the CLI/WASM mailbox path: the server’s hint blob starts with magic ``ZKACSP1`` (not compatible with 0.4.x DoublePIR hints).
|
||||
|
||||
### `PirDatabase(records, record_bytes)`
|
||||
|
||||
`records` is a list of `bytes`, each **exactly** `record_bytes` long (padded plaintext cells, one byte per LWE plaintext slot).
|
||||
|
||||
### `PirServer(db)`
|
||||
|
||||
`hints() -> bytes`, `version() -> bytes` (32-byte pool/hint digest), `answer(query: bytes) -> bytes` (little-endian `u32` limbs: **one `u32` per record byte**, i.e. `record_bytes` limbs for SimplePIR).
|
||||
|
||||
### `PirClient(hints_bytes, n_records, record_bytes)`
|
||||
|
||||
`hints_bytes` must deserialize with matching `n_records` and `record_bytes`.
|
||||
|
||||
- `query(index) -> (query_bytes, PirClientState)` — `query_bytes` length **4 × n_records** (one `u32` per database column).
|
||||
- `decode(answer_bytes, state) -> bytes` — `answer_bytes` must hold **`record_bytes` × 4** bytes (`record_bytes` little-endian `u32`s). Consumes `PirClientState`.
|
||||
|
||||
### `grant_detection_tag(secret_key: bytes, public_key: bytes) -> bytes`
|
||||
|
||||
16-byte tag for grant discovery (`secret_key` / `public_key` are 32-byte X25519 keys).
|
||||
|
||||
## Transport identity (Ristretto255)
|
||||
|
||||
### `Keypair()`
|
||||
@ -112,4 +139,4 @@ Raises `ValueError` with descriptive messages for crypto failures, replay, ident
|
||||
|
||||
## Further reading
|
||||
|
||||
[Security model and assumptions](./SECURITY.md)
|
||||
[Security model and assumptions](./SECURITY.md) · [Changelog / breaking releases](../CHANGELOG.md)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Security model and audit notes (ZKAC 0.4.1)
|
||||
# Security model and audit notes (ZKAC 0.5.0)
|
||||
|
||||
This document summarizes the design, residual risks, and recommendations for operators integrating **ZKAC**. It is not a substitute for independent review before high-assurance deployment.
|
||||
|
||||
@ -48,7 +48,7 @@ Grants live in a single **anonymous append-only pool** (no recipient identifier
|
||||
|
||||
**Discovery (cheap, no PIR):** The server exposes a `pool_tags` command returning all `(eph_pk, tag)` pairs. The client computes `X25519(my_issuance_sk, eph_pk_j)` for each entry and derives the expected tag via `BLAKE2b-512("zkac-grant-tag" || shared_secret)[..16]`. Matching entries are the client's grants. This scan is a single round-trip transferring ~48 bytes per pool entry and is computed locally.
|
||||
|
||||
**Retrieval (PIR):** Matching rows are fetched individually via LWE-based single-server PIR (`pir_query`). The server precomputes hints (`H = D · A^T` where A is a seeded public matrix); the client caches hints keyed by `pool_version` and only refetches when the pool changes.
|
||||
**Retrieval (PIR + split payload):** For each matching pool index, the client runs LWE-based single-server **SimplePIR** (`pir_query`). The PIR database row is only a **small handle** (JSON: version, `grant_id`, SHA-256 of the ciphertext) padded to `PIR_RECORD_BYTES`; the bulk ciphertext is fetched in a second management round-trip (`get_grant_blob`). The client checks that the blob’s hash matches the handle before decrypting. The server learns **which `grant_id`** was requested on the second hop (unlike the PIR index, which stays private). Hints use `H = D · A^T` (seeded public matrix `A`); the client caches hints keyed by `pool_version`.
|
||||
|
||||
```
|
||||
Admin Server (opaque relay) Recipient
|
||||
@ -63,15 +63,17 @@ Admin Server (opaque relay) Recipient
|
||||
| | | local tag match
|
||||
| |<-- pir_query(j) -----------|
|
||||
| |--- answer ----------------->|
|
||||
| | | PIR decode → row
|
||||
| | | trial-decrypt → cred
|
||||
| | | PIR decode → handle
|
||||
| |<-- get_grant_blob ---------|
|
||||
| |--- blob fields ----------->|
|
||||
| | | verify hash → decrypt
|
||||
| |<-- claim_grant ------------|
|
||||
| | (tombstone / claimed) |
|
||||
```
|
||||
|
||||
## PIR security (LWE)
|
||||
|
||||
Private information retrieval uses the **SimplePIR** construction (first layer of DoublePIR, Henzinger–Hong–Corrigan-Gibbs–Meiklejohn–Vaikuntanathan, USENIX Security '23). Security rests on the **decisional Learning With Errors (LWE)** assumption:
|
||||
Private information retrieval uses the **SimplePIR** construction (Henzinger–Hong–Corrigan-Gibbs–Meiklejohn–Vaikuntanathan, USENIX Security '23). Security rests on the **decisional Learning With Errors (LWE)** assumption:
|
||||
|
||||
- **Parameters:** LWE dimension n=1024, ciphertext modulus q=2^32, plaintext modulus p=256, discrete Gaussian noise σ=6.4.
|
||||
- **Classical security:** ~128 bits (based on lattice estimator analysis at these parameters).
|
||||
@ -259,14 +261,14 @@ Recommended strategies:
|
||||
- **zkryptium dependency:** Security follows the underlying crate and BLS12-381/BBS+ standards; keep dependencies updated.
|
||||
- **Key distribution:** The library provides the cryptographic mechanism; initial key distribution is an application-layer responsibility.
|
||||
- **Honest-but-curious PIR:** The server can return incorrect PIR answers. Corrupted answers are caught by E2E decryption / BBS+ verification, but censorship (omitting grants) is not detected at the PIR layer. Cross-replica hash comparison or a transparency log can mitigate this.
|
||||
- **Hint size:** PIR hints are approximately `record_bytes × N_LWE × 4` bytes (~256 MB for 64 KiB records with N_LWE=1024). Hints are cached client-side and only refetched when the pool version changes.
|
||||
- **Hint size:** PIR hints are approximately `56 + record_bytes × N_LWE × 4` bytes (on the order of **1 MiB** with `record_bytes = 256` and `N_LWE = 1024`). Hints are cached client-side and only refetched when the pool version changes.
|
||||
- **Unbounded grant pool:** Rows are never removed from the pool file; only marked claimed. Pool length `n` therefore grows monotonically with every posted grant. That increases discovery traffic (`pool_tags` is O(n)), PIR query size (O(n) bytes per query), server work per PIR answer (O(n × record_bytes)), and hint **rebuild** cost when the pool changes (O(n × record_bytes × N_LWE)). Operators should plan for bounded pools or archival; the codebase does not yet enforce limits.
|
||||
|
||||
## Future work
|
||||
|
||||
- **Bounded grant pool and anti-DoS:** Introduce explicit **pool caps**, **rate limits** on `post_grant`, **per-registry quotas**, or **pool generations** (rotate to a fresh empty pool while archiving the old one). Optionally **compact** the on-disk pool by rewriting only unclaimed rows and bumping a generation id so PIR indices stay meaningful without retaining every tombstone forever. Any design must preserve stable addressing for in-flight collects or migrate clients with explicit pool ids.
|
||||
- **Scale beyond large `n`:** Today’s bottleneck is **linear cost in pool length `n`** for each PIR retrieval: client upload ~4n bytes per query, server matrix–vector multiply O(n × record_bytes), and discovery O(n). For very large pools, future work includes **sublinear-communication PIR** (e.g. full DoublePIR second layer), **sharded pools** with client-side routing, **streaming or chunked hints**, or **moving heavy work off the hot path** (precomputed answers, CDN for hints) — trading complexity, trust, or privacy for throughput.
|
||||
- **DoublePIR second layer:** The current implementation uses SimplePIR (the first layer of DoublePIR). For very large pools where per-query answer size matters, a second SimplePIR layer can compress answers without API changes.
|
||||
- **Scale beyond large `n`:** Today’s bottleneck is **linear cost in pool length `n`** for each PIR retrieval: client upload ~4n bytes per query, server matrix–vector multiply O(n × record_bytes), and discovery O(n). For very large pools, future work includes **sublinear-communication PIR** (e.g. DoublePIR-style layering), **sharded pools** with client-side routing, **streaming or chunked hints**, or **moving heavy work off the hot path** (precomputed answers, CDN for hints) — trading complexity, trust, or privacy for throughput.
|
||||
- **DoublePIR / layered PIR:** The Rust tree still carries a Figure‑14 DoublePIR reference implementation (`fig14`) for tests and research. Production mailbox PIR is SimplePIR on handle-only rows plus `get_grant_blob` for ciphertext.
|
||||
- **Verifiable PIR:** Adding a commitment to the pool state (e.g. Merkle tree or KZG) and proof of correct answer computation would defend against malicious server responses beyond what E2E encryption catches.
|
||||
- **Pool commitment / transparency:** Publishing a hash of `(pool_version, hints, tags)` to a public log or allowing cross-replica comparison would detect censorship by a malicious server.
|
||||
|
||||
|
||||
2
fuzz/Cargo.lock
generated
2
fuzz/Cargo.lock
generated
@ -709,7 +709,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zkac"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"chacha20poly1305",
|
||||
|
||||
@ -4,14 +4,31 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "zkac"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
description = "Zero-Knowledge Access Control: BBS+ anonymous credentials with encrypted transport"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"ipykernel>=6.31.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
# Editable install of ``cli/`` (``zkac-node`` console script). Not part of the base
|
||||
# ``uv sync`` so library-only installs stay minimal; use ``--extra cli`` or ``demo``.
|
||||
cli = ["zkac-node"]
|
||||
demo = [
|
||||
"flask>=3.0",
|
||||
"flask-sock>=0.7",
|
||||
"zkac-node",
|
||||
]
|
||||
dev = [
|
||||
"maturin>=1.0,<2.0",
|
||||
"zkac-node",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
zkac-node = { path = "cli", editable = true }
|
||||
|
||||
[tool.maturin]
|
||||
features = ["python"]
|
||||
module-name = "zkac._zkac"
|
||||
|
||||
@ -4,6 +4,8 @@ ZKAC — Zero-Knowledge Access Control
|
||||
BBS+ anonymous credentials (BLS12-381) with encrypted transport (Ristretto255 / X25519).
|
||||
"""
|
||||
|
||||
__version__ = "0.5.0"
|
||||
|
||||
from zkac._zkac import (
|
||||
MAX_BBS_AUTH_PROOF_BYTES,
|
||||
PIR_RECORD_BYTES,
|
||||
@ -34,6 +36,7 @@ from zkac._zkac import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"MAX_BBS_AUTH_PROOF_BYTES",
|
||||
"PIR_RECORD_BYTES",
|
||||
"Keypair",
|
||||
|
||||
@ -154,6 +154,35 @@ pub fn prepare_blind_request() -> Result<BlindRequest> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Finalize a member credential using 32-byte `prover_blind` wire encoding (matches Python `Credential.finalize`).
|
||||
pub fn finalize_credential_from_parts(
|
||||
blind_sig_bytes: &[u8],
|
||||
member_secret: &[u8],
|
||||
prover_blind_bytes: &[u8],
|
||||
role_id: [u8; 32],
|
||||
epoch: u64,
|
||||
issuer_pk: &IssuerPublicKey,
|
||||
) -> Result<Credential> {
|
||||
if prover_blind_bytes.len() != 32 {
|
||||
return Err(Error::CredentialError(
|
||||
"prover_blind must be 32 bytes".into(),
|
||||
));
|
||||
}
|
||||
let pb_arr: [u8; 32] = prover_blind_bytes
|
||||
.try_into()
|
||||
.map_err(|_| Error::CredentialError("prover_blind length".into()))?;
|
||||
let blind_factor = BlindFactor::from_bytes(&pb_arr)
|
||||
.map_err(|e| Error::CredentialError(e.to_string()))?;
|
||||
Credential::finalize(
|
||||
blind_sig_bytes,
|
||||
member_secret.to_vec(),
|
||||
blind_factor,
|
||||
role_id,
|
||||
epoch,
|
||||
issuer_pk,
|
||||
)
|
||||
}
|
||||
|
||||
impl Credential {
|
||||
/// Finalize a credential after receiving the blind signature from the issuer.
|
||||
pub fn finalize(
|
||||
|
||||
@ -4,7 +4,7 @@ pub mod roles;
|
||||
pub mod schnorr;
|
||||
|
||||
pub use bbs::{
|
||||
Credential, IssuerKeyPair, IssuerPublicKey, Presentation,
|
||||
finalize_credential_from_parts, Credential, IssuerKeyPair, IssuerPublicKey, Presentation,
|
||||
prepare_blind_request, role_id, verify_presentation,
|
||||
};
|
||||
pub use registry::RegistryState;
|
||||
|
||||
@ -1,41 +1,53 @@
|
||||
//! SimplePIR-based single-server Private Information Retrieval.
|
||||
//! SimplePIR-style single-server PIR (Henzinger–Hong–Corrigan-Gibbs–Meiklejohn–Vaikuntanathan, USENIX Security 2023),
|
||||
//! first-layer construction over ``Z_{2^{32}}`` with plaintext modulus ``p = 256``.
|
||||
//!
|
||||
//! Implements the first layer of DoublePIR (Henzinger–Hong–Corrigan-Gibbs–
|
||||
//! Meiklejohn–Vaikuntanathan, USENIX Security '23). For full-record retrieval
|
||||
//! the second compression layer is unnecessary — the client needs the entire
|
||||
//! column — so this is equivalent to SimplePIR. The second layer can be added
|
||||
//! as an optimisation for very large pools without API changes.
|
||||
//! ``H = D · A^T`` (offline hint), query ``q = A^T s + e + Δ·e_index``, answer ``D·q``; client removes ``H·s`` and rounds.
|
||||
//! The grant **mailbox** uses a **split payload**: each PIR row is only ``RECORD_BYTES`` of **handle** bytes (built
|
||||
//! in the CLI server); the bulk ciphertext is fetched separately via ``get_grant_blob``.
|
||||
//!
|
||||
//! Security: decisional LWE with n=1024, q=2^32, σ=6.4 (128-bit classical).
|
||||
//! Security: decisional LWE with ``n = N_LWE``, ``q = 2^32``, ``σ = 6.4``.
|
||||
|
||||
use blake2::Blake2b512;
|
||||
use digest::Digest;
|
||||
use rand::Rng;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::Rng;
|
||||
|
||||
use super::params::*;
|
||||
use super::lwe;
|
||||
use super::db::Database;
|
||||
use super::lwe;
|
||||
use super::params::*;
|
||||
|
||||
// ── Hints (public matrix seed + precomputed H = D · A^T) ────────────
|
||||
const HINT_MAGIC: &[u8; 8] = b"ZKACSP1\0";
|
||||
const CLIENT_STATE_MAGIC: &[u8; 8] = b"ZKACSPst";
|
||||
|
||||
// ── Hints (public matrix seed + precomputed ``H = D · A^T``) ─────────
|
||||
|
||||
pub struct Hints {
|
||||
seed: [u8; 32],
|
||||
n_records: usize,
|
||||
cells_per_record: usize,
|
||||
/// Row-major `cells_per_record × N_LWE` matrix (mod 2^32).
|
||||
/// Row-major ``cells_per_record × N_LWE`` matrix (mod ``2^32``).
|
||||
hint: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Hints {
|
||||
pub fn n_records(&self) -> usize { self.n_records }
|
||||
pub fn cells_per_record(&self) -> usize { self.cells_per_record }
|
||||
pub fn seed(&self) -> &[u8; 32] { &self.seed }
|
||||
pub fn hint_matrix(&self) -> &[u32] { &self.hint }
|
||||
pub fn n_records(&self) -> usize {
|
||||
self.n_records
|
||||
}
|
||||
pub fn cells_per_record(&self) -> usize {
|
||||
self.cells_per_record
|
||||
}
|
||||
pub fn seed(&self) -> &[u8; 32] {
|
||||
&self.seed
|
||||
}
|
||||
pub fn hint_matrix(&self) -> &[u32] {
|
||||
&self.hint
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let n = self.cells_per_record * N_LWE;
|
||||
let mut buf = Vec::with_capacity(32 + 8 + 8 + n * 4);
|
||||
debug_assert_eq!(self.hint.len(), n);
|
||||
let mut buf = Vec::with_capacity(8 + 32 + 8 + 8 + n * 4);
|
||||
buf.extend_from_slice(HINT_MAGIC);
|
||||
buf.extend_from_slice(&self.seed);
|
||||
buf.extend_from_slice(&(self.n_records as u64).to_le_bytes());
|
||||
buf.extend_from_slice(&(self.cells_per_record as u64).to_le_bytes());
|
||||
@ -46,27 +58,38 @@ impl Hints {
|
||||
}
|
||||
|
||||
pub fn deserialize(data: &[u8]) -> Result<Self, &'static str> {
|
||||
if data.len() < 48 {
|
||||
if data.len() < 8 + 32 + 8 + 8 {
|
||||
return Err("hint data too short");
|
||||
}
|
||||
if &data[0..8] != HINT_MAGIC {
|
||||
return Err("unknown PIR hint format (expected SimplePIR v1)");
|
||||
}
|
||||
let mut seed = [0u8; 32];
|
||||
seed.copy_from_slice(&data[..32]);
|
||||
let n_records = u64::from_le_bytes(data[32..40].try_into().unwrap()) as usize;
|
||||
let cells_per_record = u64::from_le_bytes(data[40..48].try_into().unwrap()) as usize;
|
||||
let n = cells_per_record.checked_mul(N_LWE).ok_or("overflow")?;
|
||||
if data.len() != 48 + n * 4 {
|
||||
seed.copy_from_slice(&data[8..40]);
|
||||
let n_records = u64::from_le_bytes(data[40..48].try_into().unwrap()) as usize;
|
||||
let cells_per_record = u64::from_le_bytes(data[48..56].try_into().unwrap()) as usize;
|
||||
let n = cells_per_record
|
||||
.checked_mul(N_LWE)
|
||||
.ok_or("overflow")?;
|
||||
let need = 56 + n * 4;
|
||||
if data.len() != need {
|
||||
return Err("hint data length mismatch");
|
||||
}
|
||||
let hint: Vec<u32> = data[48..]
|
||||
let hint: Vec<u32> = data[56..]
|
||||
.chunks_exact(4)
|
||||
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
|
||||
.collect();
|
||||
Ok(Hints { seed, n_records, cells_per_record, hint })
|
||||
Ok(Hints {
|
||||
seed,
|
||||
n_records,
|
||||
cells_per_record,
|
||||
hint,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn version(&self) -> [u8; 32] {
|
||||
let mut h = Blake2b512::new();
|
||||
h.update(b"zkac-pir-hints-v1");
|
||||
h.update(b"zkac-pir-hints-sp1-v1");
|
||||
h.update(self.seed);
|
||||
h.update((self.n_records as u64).to_le_bytes());
|
||||
h.update((self.cells_per_record as u64).to_le_bytes());
|
||||
@ -88,49 +111,63 @@ pub struct Server {
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Build server state: generates a random public matrix A and precomputes
|
||||
/// the hint H = D · A^T. This is the expensive offline phase.
|
||||
pub fn new(db: Database) -> Self {
|
||||
let m = db.n_records();
|
||||
let ell = db.cells_per_record();
|
||||
|
||||
let m = db.n_records();
|
||||
let mut seed = [0u8; 32];
|
||||
OsRng.fill(&mut seed);
|
||||
|
||||
let hint = if m == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
let a = lwe::gen_matrix(&seed, N_LWE, m);
|
||||
lwe::mat_mul_bt(db.data(), &a, ell, m, N_LWE)
|
||||
};
|
||||
let hints = Hints {
|
||||
seed,
|
||||
n_records: m,
|
||||
cells_per_record: ell,
|
||||
hint,
|
||||
};
|
||||
Server { db, hints }
|
||||
}
|
||||
|
||||
Server {
|
||||
hints: Hints { seed, n_records: m, cells_per_record: ell, hint },
|
||||
db,
|
||||
pub fn hints(&self) -> &Hints {
|
||||
&self.hints
|
||||
}
|
||||
|
||||
pub fn version(&self) -> [u8; 32] {
|
||||
self.hints.version()
|
||||
}
|
||||
|
||||
/// ``answer = D · q`` (mod ``2^32``); ``q`` is ``m`` ``u32`` limbs serialized as little-endian bytes.
|
||||
pub fn answer(&self, query: &[u8]) -> Vec<u32> {
|
||||
let m = self.hints.n_records;
|
||||
let ell = self.hints.cells_per_record;
|
||||
if m == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let q: Vec<u32> = query
|
||||
.chunks_exact(4)
|
||||
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
|
||||
.collect();
|
||||
if q.len() != m {
|
||||
return Vec::new();
|
||||
}
|
||||
lwe::mat_vec_mul(self.db.data(), &q, ell, m)
|
||||
}
|
||||
|
||||
pub fn hints(&self) -> &Hints { &self.hints }
|
||||
|
||||
pub fn version(&self) -> [u8; 32] { self.hints.version() }
|
||||
|
||||
/// Compute answer = D · query (mod 2^32). The query vector has `n_records`
|
||||
/// entries; the answer has `cells_per_record` entries.
|
||||
pub fn answer(&self, query: &[u32]) -> Vec<u32> {
|
||||
lwe::mat_vec_mul(
|
||||
self.db.data(), query,
|
||||
self.db.cells_per_record(), self.db.n_records(),
|
||||
)
|
||||
pub fn n_records(&self) -> usize {
|
||||
self.hints.n_records
|
||||
}
|
||||
pub fn record_bytes(&self) -> usize {
|
||||
self.hints.cells_per_record
|
||||
}
|
||||
|
||||
pub fn n_records(&self) -> usize { self.db.n_records() }
|
||||
pub fn record_bytes(&self) -> usize { self.db.record_bytes() }
|
||||
}
|
||||
|
||||
// ── Client ──────────────────────────────────────────────────────────
|
||||
|
||||
pub struct ClientState {
|
||||
secret: Vec<u32>,
|
||||
pub secret: Vec<u32>,
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
@ -142,48 +179,43 @@ impl Client {
|
||||
Client { hints }
|
||||
}
|
||||
|
||||
pub fn version(&self) -> [u8; 32] { self.hints.version() }
|
||||
pub fn n_records(&self) -> usize { self.hints.n_records }
|
||||
pub fn record_bytes(&self) -> usize { self.hints.cells_per_record }
|
||||
pub fn version(&self) -> [u8; 32] {
|
||||
self.hints.version()
|
||||
}
|
||||
pub fn n_records(&self) -> usize {
|
||||
self.hints.n_records
|
||||
}
|
||||
pub fn record_bytes(&self) -> usize {
|
||||
self.hints.cells_per_record
|
||||
}
|
||||
|
||||
/// Generate a PIR query for `index`. Returns the query vector (to send to
|
||||
/// the server) and opaque client state (needed for decoding the answer).
|
||||
pub fn query(&self, index: usize) -> (Vec<u32>, ClientState) {
|
||||
pub fn query(&self, index: usize) -> (Vec<u8>, ClientState) {
|
||||
assert!(index < self.hints.n_records, "PIR query index out of range");
|
||||
|
||||
let mut rng = OsRng;
|
||||
let s = lwe::sample_uniform_vec(&mut rng, N_LWE);
|
||||
let e = lwe::sample_error_vec(&mut rng, self.hints.n_records);
|
||||
|
||||
// q = A^T · s + e (A is N_LWE × n_records)
|
||||
let a = lwe::gen_matrix(self.hints.seed(), N_LWE, self.hints.n_records);
|
||||
let mut q = lwe::mat_t_vec_mul(&a, &s, N_LWE, self.hints.n_records);
|
||||
|
||||
for j in 0..self.hints.n_records {
|
||||
q[j] = q[j].wrapping_add(e[j]);
|
||||
}
|
||||
q[index] = q[index].wrapping_add(DELTA);
|
||||
|
||||
(q, ClientState { secret: s })
|
||||
(serialize_vec(&q), ClientState { secret: s })
|
||||
}
|
||||
|
||||
/// Decode the server's answer using the saved client state.
|
||||
/// Returns the `record_bytes`-length plaintext record.
|
||||
pub fn decode(&self, answer: &[u32], state: &ClientState) -> Vec<u8> {
|
||||
// h_s = H · s (cells_per_record × N_LWE) · (N_LWE) → cells_per_record
|
||||
let h_s = lwe::mat_vec_mul(
|
||||
self.hints.hint_matrix(), &state.secret,
|
||||
self.hints.cells_per_record, N_LWE,
|
||||
self.hints.hint_matrix(),
|
||||
&state.secret,
|
||||
self.hints.cells_per_record,
|
||||
N_LWE,
|
||||
);
|
||||
|
||||
(0..self.hints.cells_per_record)
|
||||
.map(|i| lwe::round_to_plaintext(answer[i].wrapping_sub(h_s[i])))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Serialization helpers for query / answer vectors ────────────────
|
||||
|
||||
pub fn serialize_vec(v: &[u32]) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(v.len() * 4);
|
||||
for &val in v {
|
||||
@ -198,27 +230,58 @@ pub fn deserialize_vec(data: &[u8]) -> Vec<u32> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Persist [`ClientState`] for WASM / out-of-process flows.
|
||||
pub fn serialize_client_state(st: &ClientState) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(8 + st.secret.len() * 4);
|
||||
buf.extend_from_slice(CLIENT_STATE_MAGIC);
|
||||
for &x in &st.secret {
|
||||
buf.extend_from_slice(&x.to_le_bytes());
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn deserialize_client_state(data: &[u8]) -> Result<ClientState, &'static str> {
|
||||
if data.len() < 8 + N_LWE * 4 {
|
||||
return Err("client state too short");
|
||||
}
|
||||
if &data[0..8] != CLIENT_STATE_MAGIC {
|
||||
return Err("bad client state magic");
|
||||
}
|
||||
let mut secret = vec![0u32; N_LWE];
|
||||
let mut off = 8;
|
||||
for i in 0..N_LWE {
|
||||
secret[i] = u32::from_le_bytes(data[off..off + 4].try_into().map_err(|_| "parse")?);
|
||||
off += 4;
|
||||
}
|
||||
if off != data.len() {
|
||||
return Err("client state trailing garbage");
|
||||
}
|
||||
Ok(ClientState { secret })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_test_records(n: usize, rec_bytes: usize) -> Vec<Vec<u8>> {
|
||||
(0..n).map(|i| {
|
||||
let mut rec = vec![0u8; rec_bytes];
|
||||
rec[0] = i as u8;
|
||||
rec[1] = (i.wrapping_mul(37) & 0xFF) as u8;
|
||||
if rec_bytes > 2 {
|
||||
rec[rec_bytes - 1] = 0xAA;
|
||||
}
|
||||
rec
|
||||
}).collect()
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let mut rec = vec![0u8; rec_bytes];
|
||||
rec[0] = i as u8;
|
||||
rec[1] = (i.wrapping_mul(37) & 0xFF) as u8;
|
||||
if rec_bytes > 2 {
|
||||
rec[rec_bytes - 1] = 0xAA;
|
||||
}
|
||||
rec
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_small() {
|
||||
let records = make_test_records(8, 128);
|
||||
let records = make_test_records(8, 24);
|
||||
let refs: Vec<&[u8]> = records.iter().map(|r| r.as_slice()).collect();
|
||||
let db = Database::new(&refs, 128);
|
||||
let db = Database::new(&refs, 24);
|
||||
let server = Server::new(db);
|
||||
let hints_bytes = server.hints().serialize();
|
||||
let client = Client::new(Hints::deserialize(&hints_bytes).unwrap());
|
||||
@ -240,14 +303,8 @@ mod tests {
|
||||
let client = Client::new(Hints::deserialize(&server.hints().serialize()).unwrap());
|
||||
|
||||
let (q, state) = client.query(2);
|
||||
let q_bytes = serialize_vec(&q);
|
||||
let q2 = deserialize_vec(&q_bytes);
|
||||
assert_eq!(q, q2);
|
||||
|
||||
let ans = server.answer(&q2);
|
||||
let ans_bytes = serialize_vec(&ans);
|
||||
let ans2 = deserialize_vec(&ans_bytes);
|
||||
let decoded = client.decode(&ans2, &state);
|
||||
let ans = server.answer(&q);
|
||||
let decoded = client.decode(&ans, &state);
|
||||
assert_eq!(decoded, records[2]);
|
||||
}
|
||||
|
||||
@ -263,7 +320,6 @@ mod tests {
|
||||
let s2 = Server::new(db2);
|
||||
let v2 = s2.version();
|
||||
|
||||
// Different seeds → different versions (with overwhelming probability).
|
||||
assert_ne!(v1, v2);
|
||||
}
|
||||
|
||||
@ -284,4 +340,33 @@ mod tests {
|
||||
assert_eq!(original.hint_matrix(), restored.hint_matrix());
|
||||
assert_eq!(original.version(), restored.version());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_state_serialize_roundtrip() {
|
||||
let records = make_test_records(3, 32);
|
||||
let refs: Vec<&[u8]> = records.iter().map(|r| r.as_slice()).collect();
|
||||
let db = Database::new(&refs, 32);
|
||||
let server = Server::new(db);
|
||||
let client = Client::new(Hints::deserialize(&server.hints().serialize()).unwrap());
|
||||
let (q, st) = client.query(1);
|
||||
let bytes = serialize_client_state(&st);
|
||||
let st2 = deserialize_client_state(&bytes).unwrap();
|
||||
let ans = server.answer(&q);
|
||||
let d1 = client.decode(&ans, &st);
|
||||
let d2 = client.decode(&ans, &st2);
|
||||
assert_eq!(d1, d2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_sizes_match_simple_pir_formulas() {
|
||||
let m: u64 = 100;
|
||||
let ell = RECORD_BYTES as u64;
|
||||
let n = N_LWE as u64;
|
||||
let hint = 56u64 + ell * n * 4u64;
|
||||
let query = m * 4u64;
|
||||
let answer = ell * 4u64;
|
||||
assert_eq!(hint, 56 + (RECORD_BYTES as u64) * (N_LWE as u64) * 4);
|
||||
assert!(hint > query && hint > answer);
|
||||
let _ = (hint, query, answer);
|
||||
}
|
||||
}
|
||||
|
||||
544
src/pir/fig14.rs
Normal file
544
src/pir/fig14.rs
Normal file
@ -0,0 +1,544 @@
|
||||
//! DoublePIR (Figure 14, Appendix E of ePrint 2022/949 / USENIX Security 2023).
|
||||
//!
|
||||
//! Implements Setup / Query / Answer / Recover over a database in ``Z_p^{ℓ×m}``
|
||||
//! (here ``p = 256``, digits stored as ``u32``), with LWE dimension ``n``,
|
||||
//! ``κ = ⌈32 / log₂(p)⌉ = 4`` base-``p`` digits per ``Z_{2^32}`` limb, and the
|
||||
//! same ``Δ`` / rounding as the rest of this crate.
|
||||
//!
|
||||
//! **Integration note:** one ``Recover`` returns a **single** plaintext limb at
|
||||
//! ``(irow, icol)``. Production wiring (``doublepir``) batches ``ℓ`` inner
|
||||
//! queries per record fetch; keep ``ℓ`` modest so ``4·m + 4·ℓ²`` query size stays
|
||||
//! practical (see ``params::RECORD_BYTES``).
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use rand::Rng;
|
||||
|
||||
use super::lwe;
|
||||
use super::params::{DELTA, N_LWE, P};
|
||||
|
||||
/// κ digits per ``u32`` wire element (32 bits / 8 bits per digit).
|
||||
pub const KAPPA: usize = 4;
|
||||
|
||||
fn gen_matrix(seed: &[u8; 32], rows: usize, cols: usize) -> Vec<u32> {
|
||||
lwe::gen_matrix(seed, rows, cols)
|
||||
}
|
||||
|
||||
/// ``db`` column-major: ``db[i * m + j]`` = row ``i``, column ``j``, values in ``0..P``.
|
||||
pub fn db_get(db: &[u32], m: usize, irow: usize, icol: usize) -> u32 {
|
||||
db[irow * m + icol]
|
||||
}
|
||||
|
||||
fn mat_mul_at_b(a_t: &[u32], n: usize, m: usize, b: &[u32], m2: usize, ell: usize) -> Vec<u32> {
|
||||
debug_assert_eq!(m, m2);
|
||||
let mut out = vec![0u32; n * ell];
|
||||
for i in 0..n {
|
||||
for j in 0..ell {
|
||||
let mut acc = 0u64;
|
||||
for k in 0..m {
|
||||
acc = acc.wrapping_add(
|
||||
(a_t[i * m + k] as u64).wrapping_mul(b[k * ell + j] as u64),
|
||||
);
|
||||
}
|
||||
out[i * ell + j] = acc as u32;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn mat_mul_ab(a: &[u32], rows: usize, inner: usize, b: &[u32], inner2: usize, cols: usize) -> Vec<u32> {
|
||||
debug_assert_eq!(inner, inner2);
|
||||
let mut c = vec![0u32; rows * cols];
|
||||
for i in 0..rows {
|
||||
for j in 0..cols {
|
||||
let mut acc = 0u64;
|
||||
for k in 0..inner {
|
||||
acc = acc.wrapping_add(
|
||||
(a[i * inner + k] as u64).wrapping_mul(b[k * cols + j] as u64),
|
||||
);
|
||||
}
|
||||
c[i * cols + j] = acc as u32;
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// Decompose each ``u32`` into ``KAPPA`` base-``P`` digits (row-major ``κa × b``).
|
||||
fn decomp_matrix(inp: &[u32], rows: usize, cols: usize) -> Vec<u32> {
|
||||
let mut out = vec![0u32; (rows * KAPPA) * cols];
|
||||
for i in 0..rows {
|
||||
for j in 0..cols {
|
||||
let mut v = inp[i * cols + j];
|
||||
for t in 0..KAPPA {
|
||||
out[(i * KAPPA + t) * cols + j] = (v % P) as u32;
|
||||
v /= P;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Inverse of [`decomp_matrix`].
|
||||
fn recomp_matrix(inp: &[u32], rows: usize, cols: usize) -> Vec<u32> {
|
||||
let mut out = vec![0u32; rows * cols];
|
||||
for i in 0..rows {
|
||||
for j in 0..cols {
|
||||
let mut acc: u32 = 0;
|
||||
let mut pow: u32 = 1;
|
||||
for t in 0..KAPPA {
|
||||
let d = inp[(i * KAPPA + t) * cols + j] % P;
|
||||
acc = acc.wrapping_add(d.wrapping_mul(pow));
|
||||
pow = pow.wrapping_mul(P);
|
||||
}
|
||||
out[i * cols + j] = acc;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Round each limb to the nearest multiple of ``Δ`` (Figure 14 ``RoundΔ``).
|
||||
fn round_delta_vec(v: &[u32]) -> Vec<u32> {
|
||||
let d = DELTA as u64;
|
||||
let half = d / 2;
|
||||
v.iter()
|
||||
.map(|&x| {
|
||||
let vx = x as u64;
|
||||
((vx.wrapping_add(half)) / d).wrapping_mul(d) as u32
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Server secret from Setup: ``hint_s`` (``κ n × ℓ``), ``A1`` (``m × n``), ``A2`` (``ℓ × n``).
|
||||
pub struct DoublePirServerState {
|
||||
pub hint_s: Vec<u32>,
|
||||
pub a1: Vec<u32>,
|
||||
pub a2: Vec<u32>,
|
||||
pub ell: usize,
|
||||
pub m: usize,
|
||||
pub n: usize,
|
||||
}
|
||||
|
||||
/// Client offline hint ``hint_c`` (``κ n × n``).
|
||||
pub struct DoublePirClientHint {
|
||||
pub hint_c: Vec<u32>,
|
||||
pub ell: usize,
|
||||
pub m: usize,
|
||||
pub n: usize,
|
||||
}
|
||||
|
||||
pub fn setup(
|
||||
db: &[u32],
|
||||
ell: usize,
|
||||
m: usize,
|
||||
seed1: &[u8; 32],
|
||||
seed2: &[u8; 32],
|
||||
) -> (DoublePirServerState, DoublePirClientHint) {
|
||||
let n = N_LWE;
|
||||
assert!(db.len() >= ell * m);
|
||||
let a1 = gen_matrix(seed1, m, n);
|
||||
let a2 = gen_matrix(seed2, ell, n);
|
||||
let a1_t = transpose(&a1, m, n);
|
||||
let d_t = transpose(db, ell, m);
|
||||
let raw = mat_mul_at_b(&a1_t, n, m, &d_t, m, ell);
|
||||
let hint_s = decomp_matrix(&raw, n, ell);
|
||||
let hint_c = mat_mul_ab(&hint_s, n * KAPPA, ell, &a2, ell, n);
|
||||
(
|
||||
DoublePirServerState {
|
||||
hint_s,
|
||||
a1,
|
||||
a2,
|
||||
ell,
|
||||
m,
|
||||
n,
|
||||
},
|
||||
DoublePirClientHint { hint_c, ell, m, n },
|
||||
)
|
||||
}
|
||||
|
||||
fn transpose(inp: &[u32], rows: usize, cols: usize) -> Vec<u32> {
|
||||
let mut out = vec![0u32; cols * rows];
|
||||
for i in 0..rows {
|
||||
for j in 0..cols {
|
||||
out[j * rows + i] = inp[i * cols + j];
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub struct DoublePirQueryState {
|
||||
pub s1: Vec<u32>,
|
||||
pub s2: Vec<u32>,
|
||||
pub irow: usize,
|
||||
pub icol: usize,
|
||||
}
|
||||
|
||||
pub fn query(
|
||||
irow: usize,
|
||||
icol: usize,
|
||||
ell: usize,
|
||||
m: usize,
|
||||
n: usize,
|
||||
a1: &[u32],
|
||||
a2: &[u32],
|
||||
) -> (DoublePirQueryState, Vec<u32>, Vec<u32>) {
|
||||
let mut rng = OsRng;
|
||||
query_with_rng(&mut rng, irow, icol, ell, m, n, a1, a2)
|
||||
}
|
||||
|
||||
/// Same as [`query`], but uses the given RNG (for deterministic tests).
|
||||
pub fn query_with_rng(
|
||||
rng: &mut impl Rng,
|
||||
irow: usize,
|
||||
icol: usize,
|
||||
ell: usize,
|
||||
m: usize,
|
||||
n: usize,
|
||||
a1: &[u32],
|
||||
a2: &[u32],
|
||||
) -> (DoublePirQueryState, Vec<u32>, Vec<u32>) {
|
||||
assert!(irow < ell && icol < m);
|
||||
let s1 = lwe::sample_uniform_vec(rng, n);
|
||||
let s2 = lwe::sample_uniform_vec(rng, n);
|
||||
let e1 = lwe::sample_error_vec(rng, m);
|
||||
let e2 = lwe::sample_error_vec(rng, ell);
|
||||
|
||||
let mut c1 = mat_mul_ab(a1, m, n, &s1, n, 1);
|
||||
for i in 0..m {
|
||||
c1[i] = c1[i].wrapping_add(e1[i]);
|
||||
}
|
||||
c1[icol] = c1[icol].wrapping_add(DELTA);
|
||||
|
||||
let mut c2 = mat_mul_ab(a2, ell, n, &s2, n, 1);
|
||||
for i in 0..ell {
|
||||
c2[i] = c2[i].wrapping_add(e2[i]);
|
||||
}
|
||||
c2[irow] = c2[irow].wrapping_add(DELTA);
|
||||
|
||||
(
|
||||
DoublePirQueryState {
|
||||
s1,
|
||||
s2,
|
||||
irow,
|
||||
icol,
|
||||
},
|
||||
c1,
|
||||
c2,
|
||||
)
|
||||
}
|
||||
|
||||
pub struct DoublePirAnswer {
|
||||
pub h: Vec<u32>,
|
||||
pub ans_h: Vec<u32>,
|
||||
pub ans2: Vec<u32>,
|
||||
}
|
||||
|
||||
pub fn answer(
|
||||
db: &[u32],
|
||||
st: &DoublePirServerState,
|
||||
c1: &[u32],
|
||||
c2: &[u32],
|
||||
) -> DoublePirAnswer {
|
||||
let ell = st.ell;
|
||||
let m = st.m;
|
||||
let n = st.n;
|
||||
let row_c1_t: Vec<u32> = c1.to_vec();
|
||||
let d_t = transpose(db, ell, m);
|
||||
let one_ell = mat_mul_at_b(&row_c1_t, 1, m, &d_t, m, ell);
|
||||
let ans1 = decomp_matrix(&one_ell, 1, ell);
|
||||
let h = mat_mul_ab(&ans1, KAPPA, ell, &st.a2, ell, n);
|
||||
|
||||
let mut stacked = vec![0u32; (n * KAPPA + KAPPA) * ell];
|
||||
stacked[..n * KAPPA * ell].copy_from_slice(&st.hint_s);
|
||||
stacked[n * KAPPA * ell..].copy_from_slice(&ans1);
|
||||
let ans_h2 = mat_mul_ab(&stacked, n * KAPPA + KAPPA, ell, c2, ell, 1);
|
||||
let ans_h = ans_h2[..n * KAPPA].to_vec();
|
||||
let ans2 = ans_h2[n * KAPPA..].to_vec();
|
||||
DoublePirAnswer { h, ans_h, ans2 }
|
||||
}
|
||||
|
||||
fn mat_vec_mul(mat: &[u32], rows: usize, cols: usize, v: &[u32]) -> Vec<u32> {
|
||||
let mut out = vec![0u32; rows];
|
||||
for r in 0..rows {
|
||||
let mut acc = 0u64;
|
||||
for c in 0..cols {
|
||||
acc = acc.wrapping_add((mat[r * cols + c] as u64).wrapping_mul(v[c] as u64));
|
||||
}
|
||||
out[r] = acc as u32;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Number of ``u32`` limbs in a serialized [`DoublePirAnswer`] (fixed for ``n = N_LWE``).
|
||||
pub const ANSWER_U32_LEN: usize = 2 * KAPPA * N_LWE + KAPPA;
|
||||
|
||||
/// Figure 14 ``Recover`` (scalar at ``(irow, icol)``).
|
||||
pub fn recover(
|
||||
st: &DoublePirQueryState,
|
||||
hint_c: &[u32],
|
||||
ans: &DoublePirAnswer,
|
||||
ell: usize,
|
||||
m: usize,
|
||||
n: usize,
|
||||
) -> u32 {
|
||||
recover_with_secrets(&st.s1, &st.s2, hint_c, ans, ell, m, n)
|
||||
}
|
||||
|
||||
/// Same as [`recover`] but takes secret vectors explicitly (avoids cloning ``s1`` per row when
|
||||
/// decoding a full column with shared ``s1``).
|
||||
pub fn recover_with_secrets(
|
||||
s1: &[u32],
|
||||
s2: &[u32],
|
||||
hint_c: &[u32],
|
||||
ans: &DoublePirAnswer,
|
||||
ell: usize,
|
||||
m: usize,
|
||||
n: usize,
|
||||
) -> u32 {
|
||||
let _ = (ell, m);
|
||||
let kn = KAPPA * n;
|
||||
debug_assert_eq!(hint_c.len(), kn * n);
|
||||
debug_assert_eq!(s1.len(), n);
|
||||
debug_assert_eq!(s2.len(), n);
|
||||
debug_assert_eq!(ans.h.len(), KAPPA * n);
|
||||
debug_assert_eq!(ans.ans_h.len(), kn);
|
||||
debug_assert_eq!(ans.ans2.len(), KAPPA);
|
||||
|
||||
let mut mmat = vec![0u32; (kn + KAPPA) * n];
|
||||
mmat[..kn * n].copy_from_slice(hint_c);
|
||||
for r in 0..KAPPA {
|
||||
for c in 0..n {
|
||||
mmat[(kn + r) * n + c] = ans.h[r * n + c];
|
||||
}
|
||||
}
|
||||
|
||||
let col = mat_vec_mul(&mmat, kn + KAPPA, n, s2);
|
||||
let mut diff = vec![0u32; kn + KAPPA];
|
||||
for r in 0..kn {
|
||||
diff[r] = ans.ans_h[r].wrapping_sub(col[r]);
|
||||
}
|
||||
for r in 0..KAPPA {
|
||||
diff[kn + r] = ans.ans2[r].wrapping_sub(col[kn + r]);
|
||||
}
|
||||
|
||||
// Figure 14: ``[h1|a1] <- Recomp(RoundDelta([ĥ1|â1] / Delta))``.
|
||||
// In ``Z_{2^{32}}`` with plaintext modulus ``p=256``: first round each limb to
|
||||
// the nearest multiple of ``Δ``, then divide by ``Δ`` to get base-``p`` digits,
|
||||
// then ``Recomp`` groups ``κ`` digits into one ring element (inverse of Setup ``Decomp``).
|
||||
let rd = round_delta_vec(&diff);
|
||||
let digits: Vec<u32> = rd
|
||||
.iter()
|
||||
.map(|&x| ((x as u64).wrapping_div(DELTA as u64) % (P as u64)) as u32)
|
||||
.collect();
|
||||
let h1_a1 = recomp_matrix(&digits, n + 1, 1);
|
||||
|
||||
let mut d_hat = h1_a1[n];
|
||||
for j in 0..n {
|
||||
d_hat = d_hat.wrapping_sub(s1[j].wrapping_mul(h1_a1[j]));
|
||||
}
|
||||
lwe::round_to_plaintext(d_hat) as u32
|
||||
}
|
||||
|
||||
pub fn flatten_answer(ans: &DoublePirAnswer) -> Vec<u32> {
|
||||
debug_assert_eq!(ans.h.len() + ans.ans_h.len() + ans.ans2.len(), ANSWER_U32_LEN);
|
||||
let mut v = Vec::with_capacity(ANSWER_U32_LEN);
|
||||
v.extend_from_slice(&ans.h);
|
||||
v.extend_from_slice(&ans.ans_h);
|
||||
v.extend_from_slice(&ans.ans2);
|
||||
v
|
||||
}
|
||||
|
||||
pub fn unflatten_answer(v: &[u32]) -> Result<DoublePirAnswer, &'static str> {
|
||||
if v.len() != ANSWER_U32_LEN {
|
||||
return Err("DoublePIR answer u32 length mismatch");
|
||||
}
|
||||
let kn = KAPPA * N_LWE;
|
||||
let h = v[..KAPPA * N_LWE].to_vec();
|
||||
let ans_h = v[KAPPA * N_LWE..2 * kn].to_vec();
|
||||
let ans2 = v[2 * kn..].to_vec();
|
||||
Ok(DoublePirAnswer { h, ans_h, ans2 })
|
||||
}
|
||||
|
||||
/// One row of a batched column query: fresh ``s2`` / ``c2`` with bump at ``irow``.
|
||||
pub struct RowQueryPart {
|
||||
pub s2: Vec<u32>,
|
||||
pub irow: usize,
|
||||
}
|
||||
|
||||
/// Client secrets for fetching an entire DB column (one record): shared ``s1`` / ``c1``, one
|
||||
/// second-layer state per row.
|
||||
pub struct ColumnQueryState {
|
||||
pub s1: Vec<u32>,
|
||||
pub icol: usize,
|
||||
pub rows: Vec<RowQueryPart>,
|
||||
}
|
||||
|
||||
/// Build ``c1`` once, then one ``(s2, c2)`` per row ``0..ell``, all targeting the same column
|
||||
/// ``icol`` (record index).
|
||||
pub fn query_column_all_rows_with_rng(
|
||||
rng: &mut impl Rng,
|
||||
icol: usize,
|
||||
ell: usize,
|
||||
m: usize,
|
||||
n: usize,
|
||||
a1: &[u32],
|
||||
a2: &[u32],
|
||||
) -> (ColumnQueryState, Vec<u32>, Vec<Vec<u32>>) {
|
||||
assert!(icol < m && ell > 0);
|
||||
let s1 = lwe::sample_uniform_vec(rng, n);
|
||||
let e1 = lwe::sample_error_vec(rng, m);
|
||||
let mut c1 = mat_mul_ab(a1, m, n, &s1, n, 1);
|
||||
for i in 0..m {
|
||||
c1[i] = c1[i].wrapping_add(e1[i]);
|
||||
}
|
||||
c1[icol] = c1[icol].wrapping_add(DELTA);
|
||||
|
||||
let mut rows = Vec::with_capacity(ell);
|
||||
let mut c2s = Vec::with_capacity(ell);
|
||||
for irow in 0..ell {
|
||||
let s2 = lwe::sample_uniform_vec(rng, n);
|
||||
let e2 = lwe::sample_error_vec(rng, ell);
|
||||
let mut c2 = mat_mul_ab(a2, ell, n, &s2, n, 1);
|
||||
for i in 0..ell {
|
||||
c2[i] = c2[i].wrapping_add(e2[i]);
|
||||
}
|
||||
c2[irow] = c2[irow].wrapping_add(DELTA);
|
||||
rows.push(RowQueryPart { s2, irow });
|
||||
c2s.push(c2);
|
||||
}
|
||||
(
|
||||
ColumnQueryState {
|
||||
s1,
|
||||
icol,
|
||||
rows,
|
||||
},
|
||||
c1,
|
||||
c2s,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::Rng;
|
||||
use rand::SeedableRng;
|
||||
|
||||
fn rand_db(rng: &mut OsRng, ell: usize, m: usize) -> Vec<u32> {
|
||||
(0..ell * m).map(|_| rng.gen_range(0u32..P)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_pir_hint_c_matches_hint_s_a2() {
|
||||
let mut rng = OsRng;
|
||||
let ell = 3usize;
|
||||
let m = 5usize;
|
||||
let n = N_LWE;
|
||||
let db = rand_db(&mut rng, ell, m);
|
||||
let seed1 = rng.gen();
|
||||
let seed2 = rng.gen();
|
||||
let (srv, cli) = setup(&db, ell, m, &seed1, &seed2);
|
||||
let kn = KAPPA * n;
|
||||
let hc = mat_mul_ab(&srv.hint_s, kn, ell, &srv.a2, ell, n);
|
||||
assert_eq!(hc, cli.hint_c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_pir_answer_ans_h_matches_hint_s_times_c2() {
|
||||
let mut rng = OsRng;
|
||||
let ell = 4usize;
|
||||
let m = 6usize;
|
||||
let n = N_LWE;
|
||||
let db = rand_db(&mut rng, ell, m);
|
||||
let seed1 = rng.gen();
|
||||
let seed2 = rng.gen();
|
||||
let (srv, _cli) = setup(&db, ell, m, &seed1, &seed2);
|
||||
let (_qst, c1, c2) = query_noiseless(1, 2, ell, m, n, &srv.a1, &srv.a2);
|
||||
let ans = answer(&db, &srv, &c1, &c2);
|
||||
let kn = KAPPA * n;
|
||||
let expect_top = mat_mul_ab(&srv.hint_s, kn, ell, &c2, ell, 1);
|
||||
assert_eq!(ans.ans_h, expect_top, "ans_h != hint_s @ c2");
|
||||
let d_t = transpose(&db, ell, m);
|
||||
let one_ell = mat_mul_at_b(&c1, 1, m, &d_t, m, ell);
|
||||
let ans1 = decomp_matrix(&one_ell, 1, ell);
|
||||
let expect_h = mat_mul_ab(&ans1, KAPPA, ell, &srv.a2, ell, n);
|
||||
assert_eq!(ans.h, expect_h, "h != ans1 @ A2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_pir_fig14_roundtrip_tiny() {
|
||||
let mut rng = OsRng;
|
||||
let ell = 3usize;
|
||||
let m = 5usize;
|
||||
let n = N_LWE;
|
||||
let db = rand_db(&mut rng, ell, m);
|
||||
let seed1 = rng.gen();
|
||||
let seed2 = rng.gen();
|
||||
let (srv, cli) = setup(&db, ell, m, &seed1, &seed2);
|
||||
for irow in 0..ell {
|
||||
for icol in 0..m {
|
||||
let (qst, c1, c2) = query_noiseless(irow, icol, ell, m, n, &srv.a1, &srv.a2);
|
||||
let ans = answer(&db, &srv, &c1, &c2);
|
||||
let got = recover(&qst, &cli.hint_c, &ans, ell, m, n);
|
||||
let want = db_get(&db, m, irow, icol);
|
||||
assert_eq!(got, want, "mismatch at ({irow},{icol})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Noisy LWE errors (same σ as production); several RNG seeds (each cell × trial).
|
||||
#[test]
|
||||
fn double_pir_fig14_roundtrip_noisy_smoke() {
|
||||
let ell = 3usize;
|
||||
let m = 5usize;
|
||||
let n = N_LWE;
|
||||
let db: Vec<u32> = (0..ell * m).map(|i| (i as u32 * 17 + 41) % P).collect();
|
||||
let seed1 = [7u8; 32];
|
||||
let seed2 = [11u8; 32];
|
||||
let (srv, cli) = setup(&db, ell, m, &seed1, &seed2);
|
||||
for trial in 0u64..6 {
|
||||
let mut rng = StdRng::seed_from_u64(trial);
|
||||
for irow in 0..ell {
|
||||
for icol in 0..m {
|
||||
let (qst, c1, c2) =
|
||||
query_with_rng(&mut rng, irow, icol, ell, m, n, &srv.a1, &srv.a2);
|
||||
let ans = answer(&db, &srv, &c1, &c2);
|
||||
let got = recover(&qst, &cli.hint_c, &ans, ell, m, n);
|
||||
let want = db_get(&db, m, irow, icol);
|
||||
assert_eq!(
|
||||
got, want,
|
||||
"noisy mismatch trial {trial} at ({irow},{icol})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministic query with **zero** LWE noise (test / debugging only).
|
||||
fn query_noiseless(
|
||||
irow: usize,
|
||||
icol: usize,
|
||||
ell: usize,
|
||||
m: usize,
|
||||
n: usize,
|
||||
a1: &[u32],
|
||||
a2: &[u32],
|
||||
) -> (DoublePirQueryState, Vec<u32>, Vec<u32>) {
|
||||
assert!(irow < ell && icol < m);
|
||||
let mut rng = OsRng;
|
||||
let s1 = lwe::sample_uniform_vec(&mut rng, n);
|
||||
let s2 = lwe::sample_uniform_vec(&mut rng, n);
|
||||
let mut c1 = mat_mul_ab(a1, m, n, &s1, n, 1);
|
||||
c1[icol] = c1[icol].wrapping_add(DELTA);
|
||||
let mut c2 = mat_mul_ab(a2, ell, n, &s2, n, 1);
|
||||
c2[irow] = c2[irow].wrapping_add(DELTA);
|
||||
(
|
||||
DoublePirQueryState {
|
||||
s1,
|
||||
s2,
|
||||
irow,
|
||||
icol,
|
||||
},
|
||||
c1,
|
||||
c2,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,15 @@ pub mod params;
|
||||
pub mod lwe;
|
||||
pub mod db;
|
||||
pub mod doublepir;
|
||||
/// Figure 14 DoublePIR (ePrint 2022/949). Kept for research / correctness tests only; production PIR is SimplePIR in ``doublepir``.
|
||||
pub mod fig14;
|
||||
|
||||
pub use params::RECORD_BYTES;
|
||||
pub use db::Database;
|
||||
pub use doublepir::{Server, Client, ClientState, Hints, serialize_vec, deserialize_vec};
|
||||
pub use doublepir::{
|
||||
deserialize_client_state, deserialize_vec, serialize_client_state, serialize_vec, Client,
|
||||
ClientState, Hints, Server,
|
||||
};
|
||||
|
||||
use blake2::Blake2b512;
|
||||
use digest::Digest;
|
||||
|
||||
@ -10,5 +10,8 @@ pub const DELTA: u32 = 1 << 24;
|
||||
/// Discrete Gaussian standard deviation for LWE error sampling.
|
||||
pub const SIGMA: f64 = 6.4;
|
||||
|
||||
/// Fixed record size for PIR (bytes). Must match the CLI grant padding.
|
||||
pub const RECORD_BYTES: usize = 64 * 1024;
|
||||
/// Fixed record size for PIR (bytes). Must match the CLI **handle** padding (split payload).
|
||||
///
|
||||
/// Each mailbox row’s PIR cell is only a small handle (``grant_id`` + ciphertext digest);
|
||||
/// bulk ciphertext is fetched via ``get_grant_blob``. Hint size scales as ``O(RECORD_BYTES · N_LWE · n)``.
|
||||
pub const RECORD_BYTES: usize = 256;
|
||||
|
||||
@ -467,6 +467,12 @@ impl PyRegistryManager {
|
||||
Ok(PyBytes::new(py, &rid))
|
||||
}
|
||||
|
||||
/// Load certified registry state from disk (any version); see [`RegistryManager::restore`].
|
||||
fn restore<'py>(&mut self, py: Python<'py>, state_bytes: &[u8], state_cert: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
||||
let rid = self.inner.restore(state_bytes, state_cert).map_err(to_py_err)?;
|
||||
Ok(PyBytes::new(py, &rid))
|
||||
}
|
||||
|
||||
fn update(&mut self, registry_id: &[u8], state_bytes: &[u8], state_cert: &[u8]) -> PyResult<()> {
|
||||
let rid = to_32(registry_id, "registry_id")?;
|
||||
self.inner.update(&rid, state_bytes, state_cert).map_err(to_py_err)
|
||||
@ -827,7 +833,7 @@ impl PyNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ── PIR (DoublePIR / SimplePIR, LWE-based) ──────────────────────────
|
||||
// ── PIR (SimplePIR, LWE-based) ─────────────────────────────────────
|
||||
|
||||
#[pyclass(name = "PirDatabase")]
|
||||
pub struct PyPirDatabase {
|
||||
@ -906,11 +912,7 @@ impl PyPirServer {
|
||||
}
|
||||
|
||||
fn answer<'py>(&self, py: Python<'py>, query: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
|
||||
let q = crate::pir::deserialize_vec(query);
|
||||
if q.len() != self.inner.n_records() {
|
||||
return Err(PyValueError::new_err("query length mismatch"));
|
||||
}
|
||||
let ans = self.inner.answer(&q);
|
||||
let ans = self.inner.answer(query);
|
||||
Ok(PyBytes::new(py, &crate::pir::serialize_vec(&ans)))
|
||||
}
|
||||
|
||||
@ -966,7 +968,7 @@ impl PyPirClient {
|
||||
}
|
||||
let (q, state) = self.inner.query(index);
|
||||
Ok((
|
||||
PyBytes::new(py, &crate::pir::serialize_vec(&q)),
|
||||
PyBytes::new(py, &q),
|
||||
PyPirClientState { inner: Some(state) },
|
||||
))
|
||||
}
|
||||
@ -981,8 +983,13 @@ impl PyPirClient {
|
||||
PyValueError::new_err("PirClientState already consumed")
|
||||
})?;
|
||||
let ans = crate::pir::deserialize_vec(answer);
|
||||
if ans.len() != self.inner.record_bytes() {
|
||||
return Err(PyValueError::new_err("answer length mismatch"));
|
||||
let expect = self.inner.record_bytes();
|
||||
if ans.len() != expect {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"answer length mismatch: got {} u32, want {} (one limb per record byte)",
|
||||
ans.len(),
|
||||
expect
|
||||
)));
|
||||
}
|
||||
let decoded = self.inner.decode(&ans, &st);
|
||||
Ok(PyBytes::new(py, &decoded))
|
||||
|
||||
@ -72,6 +72,37 @@ impl RegistryManager {
|
||||
Ok(rid)
|
||||
}
|
||||
|
||||
/// Load a registry from persisted certified state (e.g. after process restart).
|
||||
///
|
||||
/// Verifies the state certificate and rebuilds the role cache. Unlike [`Self::create`],
|
||||
/// this does not require `version == 1` or zero `prev_state_hash` — the snapshot is
|
||||
/// trusted as long as the admin BBS+ certificate over `state_bytes` verifies.
|
||||
pub fn restore(
|
||||
&mut self,
|
||||
state_bytes: &[u8],
|
||||
state_cert_bytes: &[u8],
|
||||
) -> Result<[u8; 32]> {
|
||||
let state = RegistryState::deserialize(state_bytes)?;
|
||||
let cert = Presentation::from_bytes(state_cert_bytes.to_vec());
|
||||
RegistryState::verify_cert(&state.admin_issuer_pk, &cert, state_bytes)?;
|
||||
|
||||
let rid = state.registry_id;
|
||||
if self.registries.contains_key(&rid) {
|
||||
return Err(Error::RegistryAlreadyExists);
|
||||
}
|
||||
|
||||
let role_cache = build_role_cache(&state);
|
||||
self.registries.insert(rid, StoredRegistry {
|
||||
state_bytes: state_bytes.to_vec(),
|
||||
state_cert_bytes: state_cert_bytes.to_vec(),
|
||||
role_cache,
|
||||
pending_requests: HashMap::new(),
|
||||
granted: HashMap::new(),
|
||||
});
|
||||
|
||||
Ok(rid)
|
||||
}
|
||||
|
||||
/// Verify an admin BBS+ proof for a registry (the `__admin__` role
|
||||
/// with epoch 0, using the session transcript hash as nonce).
|
||||
pub fn verify_admin(
|
||||
@ -272,6 +303,42 @@ mod tests {
|
||||
assert!(!cert_bytes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_roundtrip_after_update() {
|
||||
let mut mgr = RegistryManager::new();
|
||||
let (issuer, admin_cred, rid) = create_test_registry(&mut mgr);
|
||||
let pk = issuer.public_key();
|
||||
|
||||
let (old_bytes, _) = mgr.get(&rid).unwrap();
|
||||
let prev_hash = RegistryState::state_hash(old_bytes);
|
||||
|
||||
let role_issuer = IssuerKeyPair::generate().unwrap();
|
||||
let state2 = RegistryState::new(
|
||||
pk,
|
||||
[0u8; 32],
|
||||
2,
|
||||
prev_hash,
|
||||
vec![RoleEntry {
|
||||
role_id: bbs::role_id("analyst"),
|
||||
issuer_pk: role_issuer.public_key(),
|
||||
epoch: 1,
|
||||
}],
|
||||
);
|
||||
let bytes2 = state2.serialize();
|
||||
let cert2 = state2.certify(&admin_cred, &bytes2).unwrap();
|
||||
mgr.update(&rid, &bytes2, cert2.to_bytes()).unwrap();
|
||||
|
||||
let (snap_state, snap_cert) = mgr.get(&rid).unwrap();
|
||||
let mut mgr2 = RegistryManager::new();
|
||||
let rid2 = mgr2.restore(snap_state, snap_cert).unwrap();
|
||||
assert_eq!(rid, rid2);
|
||||
let (s2, c2) = mgr2.get(&rid).unwrap();
|
||||
assert_eq!(s2, snap_state);
|
||||
assert_eq!(c2, snap_cert);
|
||||
let st = RegistryState::deserialize(s2).unwrap();
|
||||
assert_eq!(st.version, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_duplicate_rejected() {
|
||||
let mut mgr = RegistryManager::new();
|
||||
|
||||
@ -133,6 +133,35 @@ class TestRegistryManager:
|
||||
new_state = zkac.RegistryState.deserialize(new_bytes)
|
||||
assert new_state.version() == 2
|
||||
|
||||
def test_restore_snapshot_after_update(self):
|
||||
mgr = zkac.RegistryManager()
|
||||
issuer, pk, admin_cred = make_admin()
|
||||
rid, _, _ = create_registry(mgr, issuer, admin_cred)
|
||||
|
||||
old_bytes, _ = mgr.get(rid)
|
||||
old_state = zkac.RegistryState.deserialize(old_bytes)
|
||||
prev_hash = old_state.state_hash()
|
||||
|
||||
role_issuer = zkac.BbsIssuer()
|
||||
state2 = zkac.RegistryState.build(
|
||||
pk,
|
||||
b"\x00" * 32,
|
||||
2,
|
||||
prev_hash,
|
||||
[(zkac.role_id("analyst"), role_issuer.public_key(), 1)],
|
||||
)
|
||||
cert2 = state2.certify(admin_cred)
|
||||
mgr.update(rid, state2.serialize(), cert2)
|
||||
|
||||
snap_state, snap_cert = mgr.get(rid)
|
||||
mgr2 = zkac.RegistryManager()
|
||||
rid2 = mgr2.restore(snap_state, snap_cert)
|
||||
assert rid == rid2
|
||||
s2, c2 = mgr2.get(rid)
|
||||
assert s2 == snap_state
|
||||
assert c2 == snap_cert
|
||||
assert zkac.RegistryState.deserialize(s2).version() == 2
|
||||
|
||||
def test_wrong_version_rejected(self):
|
||||
mgr = zkac.RegistryManager()
|
||||
issuer, pk, admin_cred = make_admin()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Tests for LWE-based PIR (DoublePIR / SimplePIR) via the Python bindings."""
|
||||
"""Tests for LWE-based SimplePIR via the Python bindings."""
|
||||
|
||||
import json
|
||||
import os
|
||||
@ -30,10 +30,10 @@ def test_roundtrip_small():
|
||||
|
||||
|
||||
def test_roundtrip_medium():
|
||||
"""64 records at full PIR_RECORD_BYTES size."""
|
||||
"""64 records padded to ``PIR_RECORD_BYTES`` (must fit LWE-encoded plaintext)."""
|
||||
records = []
|
||||
for i in range(64):
|
||||
entry = {"id": i, "payload": os.urandom(128).hex()}
|
||||
entry = {"id": i, "payload": os.urandom(32).hex()}
|
||||
records.append(_pad(entry))
|
||||
db = zkac.PirDatabase(records, zkac.PIR_RECORD_BYTES)
|
||||
server = zkac.PirServer(db)
|
||||
|
||||
556
uv.lock
generated
556
uv.lock
generated
@ -1,11 +1,10 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
"python_full_version < '3.10'",
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -26,13 +25,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" },
|
||||
{ name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" },
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
@ -107,18 +114,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -165,10 +172,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6b/668f21567e3250463beb6a401e7d598baa2a0907224000d7f68b9442c243/debugpy-1.8.20-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:bff8990f040dacb4c314864da95f7168c5a58a30a66e0eea0fb85e2586a92cd6", size = 2100484, upload-time = "2026-01-29T23:04:09.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/49/223143d1da586b891f35a45515f152742ad85bfc10d2e02e697f65c83b32/debugpy-1.8.20-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:70ad9ae09b98ac307b82c16c151d27ee9d68ae007a2e7843ba621b5ce65333b5", size = 3081272, upload-time = "2026-01-29T23:04:11.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/24/9f219c9290fe8bee4f63f9af8ebac440c802e6181d7f39a79abcb5fdff2f/debugpy-1.8.20-cp39-cp39-win32.whl", hash = "sha256:9eeed9f953f9a23850c85d440bf51e3c56ed5d25f8560eeb29add815bd32f7ee", size = 5285196, upload-time = "2026-01-29T23:04:13.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/f3/4a12d7b1b09e3b79ba6e3edfa0c677b8b8bdf110bc4b3607e0f29fb4e8b3/debugpy-1.8.20-cp39-cp39-win_amd64.whl", hash = "sha256:760813b4fff517c75bfe7923033c107104e76acfef7bda011ffea8736e9a66f8", size = 5317163, upload-time = "2026-01-29T23:04:15.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" },
|
||||
]
|
||||
|
||||
@ -203,119 +206,89 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.7.1"
|
||||
name = "flask"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp", marker = "python_full_version < '3.10'" },
|
||||
{ name = "blinker" },
|
||||
{ name = "click" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markupsafe" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipykernel"
|
||||
version = "6.31.0"
|
||||
name = "flask-sock"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "appnope", marker = "python_full_version < '3.10' and sys_platform == 'darwin'" },
|
||||
{ name = "comm", marker = "python_full_version < '3.10'" },
|
||||
{ name = "debugpy", marker = "python_full_version < '3.10'" },
|
||||
{ name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version < '3.10'" },
|
||||
{ name = "nest-asyncio", marker = "python_full_version < '3.10'" },
|
||||
{ name = "packaging", marker = "python_full_version < '3.10'" },
|
||||
{ name = "psutil", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pyzmq", marker = "python_full_version < '3.10'" },
|
||||
{ name = "tornado", marker = "python_full_version < '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.10'" },
|
||||
{ name = "flask" },
|
||||
{ name = "simple-websocket" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8d/8f/c6ab717dc90f4e46d1430335cd4ab13e3629410bb760c0ead6de476760fb/flask-sock-0.7.0.tar.gz", hash = "sha256:e023b578284195a443b8d8bdb4469e6a6acf694b89aeb51315b1a34fcf427b7d", size = 4334, upload-time = "2023-10-02T22:32:42.973Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/98/107728ce3f430b5481eb426ccc5e1f7c8ab0bd01eaf231c62a8d528ff721/flask_sock-0.7.0-py3-none-any.whl", hash = "sha256:caac4d679392aaf010d02fabcf73d52019f5bdaf1c9c131ec5a428cb3491204a", size = 3982, upload-time = "2023-10-02T22:32:41.778Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipykernel"
|
||||
version = "7.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "appnope", marker = "python_full_version >= '3.10' and sys_platform == 'darwin'" },
|
||||
{ name = "comm", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "debugpy", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "appnope", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "comm" },
|
||||
{ name = "debugpy" },
|
||||
{ name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "ipython", version = "9.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
||||
{ name = "ipython", version = "9.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "nest-asyncio", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "psutil", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pyzmq", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "tornado", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "jupyter-client" },
|
||||
{ name = "jupyter-core" },
|
||||
{ name = "matplotlib-inline" },
|
||||
{ name = "nest-asyncio" },
|
||||
{ name = "packaging" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pyzmq" },
|
||||
{ name = "tornado" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "8.18.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
|
||||
{ name = "decorator", marker = "python_full_version < '3.10'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.10'" },
|
||||
{ name = "jedi", marker = "python_full_version < '3.10'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pygments", marker = "python_full_version < '3.10'" },
|
||||
{ name = "stack-data", marker = "python_full_version < '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.10'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "8.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version == '3.10.*'",
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" },
|
||||
{ name = "decorator", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "jedi", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "pygments", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "stack-data", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "traitlets", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version == '3.10.*'" },
|
||||
{ name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
|
||||
{ name = "decorator", marker = "python_full_version < '3.11'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "jedi", marker = "python_full_version < '3.11'" },
|
||||
{ name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
|
||||
{ name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
|
||||
{ name = "pygments", marker = "python_full_version < '3.11'" },
|
||||
{ name = "stack-data", marker = "python_full_version < '3.11'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" }
|
||||
wheels = [
|
||||
@ -383,6 +356,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jedi"
|
||||
version = "0.19.2"
|
||||
@ -396,81 +378,131 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-client"
|
||||
version = "8.6.3"
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
|
||||
{ name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "python-dateutil", marker = "python_full_version < '3.10'" },
|
||||
{ name = "pyzmq", marker = "python_full_version < '3.10'" },
|
||||
{ name = "tornado", marker = "python_full_version < '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.10'" },
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-client"
|
||||
version = "8.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "python-dateutil", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pyzmq", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "tornado", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "jupyter-core" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pyzmq" },
|
||||
{ name = "tornado" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-core"
|
||||
version = "5.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pywin32", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" },
|
||||
{ name = "traitlets", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-core"
|
||||
version = "5.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "platformdirs", version = "4.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "traitlets", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib-inline"
|
||||
version = "0.2.1"
|
||||
@ -483,6 +515,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maturin"
|
||||
version = "1.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/39/16/b284a7bc4af3dd87717c784278c1b8cb18606ad1f6f7a671c47bfd9c3df0/maturin-1.13.1.tar.gz", hash = "sha256:9a87ff3b8e4d1c6eac33ebfe8e261e8236516d98d45c0323550621819b5a1a2f", size = 340369, upload-time = "2026-04-09T15:14:07.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/4d/a23fc95be881aa8c7a6ea353410417872e4d7065df03d7f3db8f0dbed4a7/maturin-1.13.1-py3-none-linux_armv6l.whl", hash = "sha256:416e4e01cb88b798e606ee43929df897e42c1647b722ef68283816cca99a8742", size = 10102444, upload-time = "2026-04-09T15:13:48.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/1e/65c385d65bae95cf04895d52f39dbed8b1453ae55da2903d252ade40a774/maturin-1.13.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:72888e87819ce546d0d2df900e4b385e4ef299077d92ee37b48923a5602dae94", size = 19576043, upload-time = "2026-04-09T15:14:08.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/13/f6bc868d0bfecd9314870b97f530a167e31f7878ac4945c78245c6eef69c/maturin-1.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:98b5fcf1a186c217830a8295ecc2989c6b1cf50945417adfc15252107b9475b7", size = 10117339, upload-time = "2026-04-09T15:13:40.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/58/279e081305c11c1c1c4fccacf77df8959646c5d4de7a57ec7e787653e270/maturin-1.13.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:3da18cccf2f683c0977bff9146a0908d6ffce836d600665736ac01679f588cb9", size = 10139689, upload-time = "2026-04-09T15:13:38.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/94/69391af5396c6aab723932240803f49e5f3de3dd7c57d32f02d237a0ce32/maturin-1.13.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:6b1e5916a253243e8f5f9e847b62bbc98420eec48c9ce2e2e8724c6da89d359b", size = 10551141, upload-time = "2026-04-09T15:13:42.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/bf/4edac2667b49e3733438062ae416413b8fc8d42e1bd499ba15e1fb02fc55/maturin-1.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:dc91031e0619c1e28730279ef9ee5f106c9b9ec806b013f888676b242f892eb7", size = 9983094, upload-time = "2026-04-09T15:13:56.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/94/a6d651cfe8fc6bf2e892c90e3cdbb25c06d81c9115140d03ea1a68a97575/maturin-1.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:001741c6cff56aa8ea59a0d78ae990c0550d0e3e82b00b683eedb4158a8ef7e6", size = 9949980, upload-time = "2026-04-09T15:13:59.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/d1/82c067464f848e38af9910bce55eb54302b1c1284a279d515dbfcf5994f5/maturin-1.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:01c845825c917c07c1d0b2c9032c59c16a7d383d1e649a46481d3e5693c2750f", size = 13186276, upload-time = "2026-04-09T15:13:45.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f4/25367baf1025580f047f9b37598bb3fadc416e24536afd4f28e190335c73/maturin-1.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f69093ed4a0e6464e52a7fc26d714f859ce15630ec8070743398c6bf41f38a9e", size = 10891837, upload-time = "2026-04-09T15:13:35.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/be/caafad8ce74974b7deafdf144d12f758993dfea4c66c9905b138f51a7792/maturin-1.13.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:c1490584f3c70af45466ee99065b49e6657ebdccac6b10571bb44681309c9396", size = 10351032, upload-time = "2026-04-09T15:14:01.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/0e/970a721d27cfa410e8bfa0a1e32e6ef52cb8169692110a5fdabe1af3f570/maturin-1.13.1-py3-none-win32.whl", hash = "sha256:c6a720b252c99de072922dbe4432ab19662b6f80045b0355fec23bdfccb450da", size = 8855465, upload-time = "2026-04-09T15:13:51.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/70/7c1e0d65fa147d5479055a171541c82b8cdfc1c825d85a82240470f14176/maturin-1.13.1-py3-none-win_amd64.whl", hash = "sha256:a2017d2281203d0c6570240e7d746564d766d756105823b7de68bda6ae722711", size = 10230471, upload-time = "2026-04-09T15:13:53.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/2a/afe0193b673a79ffd2e01ad999511b7e9e6b49af02bb3759d82a78c3043d/maturin-1.13.1-py3-none-win_arm64.whl", hash = "sha256:2839024dcd65776abb4759e5bca29941971e095574162a4d335191da4be9ff24", size = 8905575, upload-time = "2026-04-09T15:14:03.891Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nest-asyncio"
|
||||
version = "1.6.0"
|
||||
@ -522,27 +578,10 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
||||
@ -606,27 +645,10 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version == '3.10.*'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
@ -653,31 +675,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyzmq"
|
||||
version = "27.1.0"
|
||||
@ -739,16 +736,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/4e/782eb6df91b6a9d9afa96c2dcfc5cac62562a68eb62a02210101f886014d/pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb", size = 1330426, upload-time = "2025-09-08T23:09:21.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/ca/2b8693d06b1db4e0c084871e4c9d7842b561d0a6ff9d780640f5e3e9eb55/pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429", size = 906559, upload-time = "2025-09-08T23:09:22.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b3/b99b39e2cfdcebd512959780e4d299447fd7f46010b1d88d63324e2481ec/pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d", size = 863816, upload-time = "2025-09-08T23:09:24.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/b2/018fa8e8eefb34a625b1a45e2effcbc9885645b22cdd0a68283f758351e7/pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345", size = 666735, upload-time = "2025-09-08T23:09:26.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/05/8ae778f7cd7c94030731ae2305e6a38f3a333b6825f56c0c03f2134ccf1b/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968", size = 1655425, upload-time = "2025-09-08T23:09:28.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/ad/d69478a97a3f3142f9dbbbd9daa4fcf42541913a85567c36d4cfc19b2218/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098", size = 2033729, upload-time = "2025-09-08T23:09:30.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/6d/e3c6ad05bc1cddd25094e66cc15ae8924e15c67e231e93ed2955c401007e/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f", size = 1891803, upload-time = "2025-09-08T23:09:31.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/a7/97e8be0daaca157511563160b67a13d4fe76b195e3fa6873cb554ad46be3/pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78", size = 567627, upload-time = "2025-09-08T23:09:33.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/91/70bbf3a7c5b04c904261ef5ba224d8a76315f6c23454251bf5f55573a8a1/pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db", size = 632315, upload-time = "2025-09-08T23:09:36.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b5/a4173a83c7fd37f6bdb5a800ea338bc25603284e9ef8681377cec006ede4/pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc", size = 559833, upload-time = "2025-09-08T23:09:38.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" },
|
||||
@ -759,11 +746,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/f4/c2e978cf6b833708bad7d6396c3a20c19750585a1775af3ff13c435e1912/pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f", size = 836257, upload-time = "2025-09-08T23:10:07.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/5f/4e10c7f57a4c92ab0fbb2396297aa8d618e6f5b9b8f8e9756d56f3e6fc52/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8", size = 800203, upload-time = "2025-09-08T23:10:09.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/72/a74a007cd636f903448c6ab66628104b1fc5f2ba018733d5eabb94a0a6fb/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381", size = 758756, upload-time = "2025-09-08T23:10:11.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/d4/30c25b91f2b4786026372f5ef454134d7f576fcf4ac58539ad7dd5de4762/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172", size = 567742, upload-time = "2025-09-08T23:10:14.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/aa/ee86edad943438cd0316964020c4b6d09854414f9f945f8e289ea6fcc019/pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9", size = 544857, upload-time = "2025-09-08T23:10:16.431Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple-websocket"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wsproto" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -789,6 +783,60 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.5.5"
|
||||
@ -834,22 +882,70 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.23.0"
|
||||
name = "werkzeug"
|
||||
version = "3.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wsproto"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zkac"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "ipykernel", version = "7.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "ipykernel" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cli = [
|
||||
{ name = "zkac-node" },
|
||||
]
|
||||
demo = [
|
||||
{ name = "flask" },
|
||||
{ name = "flask-sock" },
|
||||
{ name = "zkac-node" },
|
||||
]
|
||||
dev = [
|
||||
{ name = "maturin" },
|
||||
{ name = "zkac-node" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "ipykernel", specifier = ">=6.31.0" }]
|
||||
requires-dist = [
|
||||
{ name = "flask", marker = "extra == 'demo'", specifier = ">=3.0" },
|
||||
{ name = "flask-sock", marker = "extra == 'demo'", specifier = ">=0.7" },
|
||||
{ name = "ipykernel", specifier = ">=6.31.0" },
|
||||
{ name = "maturin", marker = "extra == 'dev'", specifier = ">=1.0,<2.0" },
|
||||
{ name = "zkac-node", marker = "extra == 'cli'", editable = "cli" },
|
||||
{ name = "zkac-node", marker = "extra == 'demo'", editable = "cli" },
|
||||
{ name = "zkac-node", marker = "extra == 'dev'", editable = "cli" },
|
||||
]
|
||||
provides-extras = ["cli", "demo", "dev"]
|
||||
|
||||
[[package]]
|
||||
name = "zkac-node"
|
||||
version = "0.2.0"
|
||||
source = { editable = "cli" }
|
||||
dependencies = [
|
||||
{ name = "zkac" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "zkac" }]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user