quantufai 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantufai
3
+ Version: 0.1.0
4
+ Summary: QuantufAI developer SDK: quote quantum runs before any spend, read job status/results/receipts, export circuits, and simulate on the free sandbox — this SDK can price and read, it can never spend.
5
+ Author: QuantufAI, Inc.
6
+ License: Proprietary
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Provides-Extra: verify
10
+ Requires-Dist: cryptography>=41; extra == "verify"
11
+
12
+ # quantufai — the QuantufAI Python SDK
13
+
14
+ Quantum compute a program can **price and read — never spend**.
15
+
16
+ > ⚠️ **Publish gate:** this package is repo-code only. It is **not** on PyPI
17
+ > and must not be published or linked from public docs until the founder
18
+ > confirms counsel's provisional filing. Local installs are fine:
19
+ > `pip install -e sdks/python`.
20
+
21
+ ## What this SDK can do
22
+
23
+ | You want | Call | Key scope |
24
+ |---|---|---|
25
+ | "What would this run cost?" | `client.quote(circuit, shots=...)` | `quotes:read` |
26
+ | "Is it done yet?" | `client.job_status(job_id)` / `client.wait(job_id)` | `jobs:read` |
27
+ | Counts + ledger + error bars, verbatim | `client.job_result(job_id)` | `jobs:read` |
28
+ | The signed, tamper-evident receipt | `client.governed_receipt(job_id)` | `jobs:read` |
29
+ | Check a receipt you hold | `client.verify_receipt(receipt)` | `jobs:read` (platform tier) / none (offline tier) |
30
+ | The exact circuit that ran, as Qiskit/Cirq/Braket/pytket/QASM | `client.export_circuit(job_id, format=..., which=...)` | `results:export` |
31
+ | Download the result artifact | `client.export_result(job_id)` | `results:export` |
32
+ | Run on the **free local simulator** ($0) | `client.sandbox_simulate(circuit)` | `runs:simulate` (sandbox key) |
33
+ | What can this key do? | `client.me()` | `account:read` |
34
+
35
+ ## What this SDK cannot do — by design, not omission
36
+
37
+ **There is no dispatch method. There is no approval method. There is no
38
+ billing method.** Spending money on quantum hardware requires a human
39
+ approving a signed quote in the QuantufAI dashboard:
40
+
41
+ - The platform enforces quote-before-spend server-side; scoped API keys
42
+ cannot approve a spend.
43
+ - On top of that, this SDK's one execution method (`sandbox_simulate`) pins
44
+ every request to the free local simulator (`preferredProviders:
45
+ ["classical"]`), so **even a key carrying the paid `runs:execute` scope
46
+ cannot reach billable hardware through this SDK**. Zero eligible providers
47
+ is a typed failure on the platform — never a silent reroute.
48
+
49
+ An AI agent driving this client can tell you exactly what an experiment would
50
+ cost and read every receipt — and cannot buy anything.
51
+
52
+ ## REFUSED is a status, not an exception
53
+
54
+ Every deliberate platform refusal — missing scope, sandbox clamp, someone
55
+ else's job, quota exhausted, unprovable circuit translation — comes back as a
56
+ typed `Refusal` with the platform's `code`, `message`, and `details`
57
+ verbatim:
58
+
59
+ ```python
60
+ result = client.job_result("not-my-job")
61
+ if isinstance(result, quantufai.Refusal):
62
+ print(result.status) # "REFUSED"
63
+ print(result.code) # e.g. "job_not_found", "insufficient_scope"
64
+ print(result.details) # e.g. {"requiredScope": "jobs:read", ...}
65
+ ```
66
+
67
+ Exceptions are reserved for transport failures (`TransportError`) and
68
+ unexpected 5xx answers (`PlatformError`, body preserved).
69
+
70
+ ## Quickstart (sandbox — no card, $0)
71
+
72
+ ```python
73
+ import quantufai
74
+
75
+ client = quantufai.Client(
76
+ api_key="qfai_sk_...", # or $QUANTUFAI_API_KEY; sandbox tier: mint
77
+ # with {"tier": "sandbox"} in the dashboard
78
+ base_url="https://YOUR_HOST", # or $QUANTUFAI_BASE_URL
79
+ )
80
+
81
+ bell = """OPENQASM 2.0;
82
+ include "qelib1.inc";
83
+ qreg q[2]; creg c[2];
84
+ h q[0]; cx q[0], q[1];
85
+ measure q -> c;"""
86
+
87
+ # 1. Price it first — nothing is reserved, charged, or dispatched by quoting.
88
+ quotes = client.quote(bell, shots=1000, qubits=2)
89
+ if isinstance(quotes, quantufai.Refusal):
90
+ raise SystemExit(f"refused: {quotes.code} — {quotes.message}")
91
+ for q in quotes.quotes:
92
+ print(f"{q.provider}: ~${q.estimated_cost_usd} ({q.reasons})")
93
+
94
+ # 2. Run on the FREE local simulator (the only execution this SDK performs).
95
+ run = client.sandbox_simulate(bell, shots=1000, qubits=2)
96
+ if isinstance(run, quantufai.Refusal):
97
+ raise SystemExit(f"refused: {run.code} — {run.message}")
98
+ print(run.state, run.counts) # real statevector physics, real counts, $0
99
+
100
+ # 3. Poll (async runs), then fetch the signed receipt.
101
+ result = client.wait(run.job_id)
102
+ receipt = client.governed_receipt(run.job_id)
103
+
104
+ # 4. Verify the receipt — offline Ed25519 tier and/or the platform's check.
105
+ report = client.verify_receipt(receipt)
106
+ print(report.verified, report.offline.public_signature)
107
+
108
+ # 5. Take the exact circuit home. `which` is required — the platform refuses
109
+ # to guess between "original" and "as-dispatched".
110
+ code = client.export_circuit(run.job_id, format="qiskit", which="as-dispatched")
111
+ print(code.source)
112
+ ```
113
+
114
+ ## Receipts: what the tiers mean
115
+
116
+ - **Publicly-verifiable tier** (`publicSignature`, Ed25519): anyone can
117
+ verify offline with QuantufAI's published public key —
118
+ `verify_receipt(receipt, public_key=..., offline_only=True)` or the
119
+ standalone `tools/verify-receipt.mjs`. Needs the optional extra:
120
+ `pip install 'quantufai[verify]'`.
121
+ - **Server-attested tier** (`signature`, HMAC): the platform's own
122
+ attestation; only the platform can check it (that's what the platform tier
123
+ of `verify_receipt` asks for). Older receipts carry only this tier — the
124
+ SDK reports that honestly instead of pretending to a verdict.
125
+
126
+ ## Results are verbatim
127
+
128
+ Counts are never re-binned, error bars are never computed client-side —
129
+ `JobResult.error_bars` returns exactly the uncertainty fields the platform
130
+ attached (with their location in the payload), and returns nothing when the
131
+ platform attached none.
132
+
133
+ ## History: this package replaces deleted fabricating stubs
134
+
135
+ The repo previously carried `sdks/python/quantufai.py` and
136
+ `sdks/js/quantufai.ts` — stubs that called endpoints that never existed and
137
+ **invented job statuses by pattern-matching chat text**. They were deleted
138
+ (PR #579) and a CI test keeps them deleted. This package is their honest
139
+ replacement: every call maps to a real, mounted, scope-gated endpoint, and
140
+ anything the platform refuses surfaces as a typed `REFUSED`.
141
+
142
+ ## Publish checklist (post-filing — founder go required)
143
+
144
+ 1. Founder confirms counsel's provisional filing covers the receipt/verifier
145
+ disclosures (roadmap #4 gate).
146
+ 2. `python -m build` from `sdks/python/`; check the wheel installs clean.
147
+ 3. Reserve/publish `quantufai` on PyPI from the org account (never a personal
148
+ one); enable 2FA + trusted publishing.
149
+ 4. Only then link the SDK from `/docs` (the docs deliberately do not mention
150
+ it today).
151
+ 5. Announcement claim (exact words matter): "quantum compute an AI agent can
152
+ spend safely — quote-before-spend enforced, auditable receipts." **Never
153
+ claim "first MCP"** (IBM's Qiskit MCP servers and Conductor's CODA exist);
154
+ "first *governed* one" is the true, stronger claim.
@@ -0,0 +1,143 @@
1
+ # quantufai — the QuantufAI Python SDK
2
+
3
+ Quantum compute a program can **price and read — never spend**.
4
+
5
+ > ⚠️ **Publish gate:** this package is repo-code only. It is **not** on PyPI
6
+ > and must not be published or linked from public docs until the founder
7
+ > confirms counsel's provisional filing. Local installs are fine:
8
+ > `pip install -e sdks/python`.
9
+
10
+ ## What this SDK can do
11
+
12
+ | You want | Call | Key scope |
13
+ |---|---|---|
14
+ | "What would this run cost?" | `client.quote(circuit, shots=...)` | `quotes:read` |
15
+ | "Is it done yet?" | `client.job_status(job_id)` / `client.wait(job_id)` | `jobs:read` |
16
+ | Counts + ledger + error bars, verbatim | `client.job_result(job_id)` | `jobs:read` |
17
+ | The signed, tamper-evident receipt | `client.governed_receipt(job_id)` | `jobs:read` |
18
+ | Check a receipt you hold | `client.verify_receipt(receipt)` | `jobs:read` (platform tier) / none (offline tier) |
19
+ | The exact circuit that ran, as Qiskit/Cirq/Braket/pytket/QASM | `client.export_circuit(job_id, format=..., which=...)` | `results:export` |
20
+ | Download the result artifact | `client.export_result(job_id)` | `results:export` |
21
+ | Run on the **free local simulator** ($0) | `client.sandbox_simulate(circuit)` | `runs:simulate` (sandbox key) |
22
+ | What can this key do? | `client.me()` | `account:read` |
23
+
24
+ ## What this SDK cannot do — by design, not omission
25
+
26
+ **There is no dispatch method. There is no approval method. There is no
27
+ billing method.** Spending money on quantum hardware requires a human
28
+ approving a signed quote in the QuantufAI dashboard:
29
+
30
+ - The platform enforces quote-before-spend server-side; scoped API keys
31
+ cannot approve a spend.
32
+ - On top of that, this SDK's one execution method (`sandbox_simulate`) pins
33
+ every request to the free local simulator (`preferredProviders:
34
+ ["classical"]`), so **even a key carrying the paid `runs:execute` scope
35
+ cannot reach billable hardware through this SDK**. Zero eligible providers
36
+ is a typed failure on the platform — never a silent reroute.
37
+
38
+ An AI agent driving this client can tell you exactly what an experiment would
39
+ cost and read every receipt — and cannot buy anything.
40
+
41
+ ## REFUSED is a status, not an exception
42
+
43
+ Every deliberate platform refusal — missing scope, sandbox clamp, someone
44
+ else's job, quota exhausted, unprovable circuit translation — comes back as a
45
+ typed `Refusal` with the platform's `code`, `message`, and `details`
46
+ verbatim:
47
+
48
+ ```python
49
+ result = client.job_result("not-my-job")
50
+ if isinstance(result, quantufai.Refusal):
51
+ print(result.status) # "REFUSED"
52
+ print(result.code) # e.g. "job_not_found", "insufficient_scope"
53
+ print(result.details) # e.g. {"requiredScope": "jobs:read", ...}
54
+ ```
55
+
56
+ Exceptions are reserved for transport failures (`TransportError`) and
57
+ unexpected 5xx answers (`PlatformError`, body preserved).
58
+
59
+ ## Quickstart (sandbox — no card, $0)
60
+
61
+ ```python
62
+ import quantufai
63
+
64
+ client = quantufai.Client(
65
+ api_key="qfai_sk_...", # or $QUANTUFAI_API_KEY; sandbox tier: mint
66
+ # with {"tier": "sandbox"} in the dashboard
67
+ base_url="https://YOUR_HOST", # or $QUANTUFAI_BASE_URL
68
+ )
69
+
70
+ bell = """OPENQASM 2.0;
71
+ include "qelib1.inc";
72
+ qreg q[2]; creg c[2];
73
+ h q[0]; cx q[0], q[1];
74
+ measure q -> c;"""
75
+
76
+ # 1. Price it first — nothing is reserved, charged, or dispatched by quoting.
77
+ quotes = client.quote(bell, shots=1000, qubits=2)
78
+ if isinstance(quotes, quantufai.Refusal):
79
+ raise SystemExit(f"refused: {quotes.code} — {quotes.message}")
80
+ for q in quotes.quotes:
81
+ print(f"{q.provider}: ~${q.estimated_cost_usd} ({q.reasons})")
82
+
83
+ # 2. Run on the FREE local simulator (the only execution this SDK performs).
84
+ run = client.sandbox_simulate(bell, shots=1000, qubits=2)
85
+ if isinstance(run, quantufai.Refusal):
86
+ raise SystemExit(f"refused: {run.code} — {run.message}")
87
+ print(run.state, run.counts) # real statevector physics, real counts, $0
88
+
89
+ # 3. Poll (async runs), then fetch the signed receipt.
90
+ result = client.wait(run.job_id)
91
+ receipt = client.governed_receipt(run.job_id)
92
+
93
+ # 4. Verify the receipt — offline Ed25519 tier and/or the platform's check.
94
+ report = client.verify_receipt(receipt)
95
+ print(report.verified, report.offline.public_signature)
96
+
97
+ # 5. Take the exact circuit home. `which` is required — the platform refuses
98
+ # to guess between "original" and "as-dispatched".
99
+ code = client.export_circuit(run.job_id, format="qiskit", which="as-dispatched")
100
+ print(code.source)
101
+ ```
102
+
103
+ ## Receipts: what the tiers mean
104
+
105
+ - **Publicly-verifiable tier** (`publicSignature`, Ed25519): anyone can
106
+ verify offline with QuantufAI's published public key —
107
+ `verify_receipt(receipt, public_key=..., offline_only=True)` or the
108
+ standalone `tools/verify-receipt.mjs`. Needs the optional extra:
109
+ `pip install 'quantufai[verify]'`.
110
+ - **Server-attested tier** (`signature`, HMAC): the platform's own
111
+ attestation; only the platform can check it (that's what the platform tier
112
+ of `verify_receipt` asks for). Older receipts carry only this tier — the
113
+ SDK reports that honestly instead of pretending to a verdict.
114
+
115
+ ## Results are verbatim
116
+
117
+ Counts are never re-binned, error bars are never computed client-side —
118
+ `JobResult.error_bars` returns exactly the uncertainty fields the platform
119
+ attached (with their location in the payload), and returns nothing when the
120
+ platform attached none.
121
+
122
+ ## History: this package replaces deleted fabricating stubs
123
+
124
+ The repo previously carried `sdks/python/quantufai.py` and
125
+ `sdks/js/quantufai.ts` — stubs that called endpoints that never existed and
126
+ **invented job statuses by pattern-matching chat text**. They were deleted
127
+ (PR #579) and a CI test keeps them deleted. This package is their honest
128
+ replacement: every call maps to a real, mounted, scope-gated endpoint, and
129
+ anything the platform refuses surfaces as a typed `REFUSED`.
130
+
131
+ ## Publish checklist (post-filing — founder go required)
132
+
133
+ 1. Founder confirms counsel's provisional filing covers the receipt/verifier
134
+ disclosures (roadmap #4 gate).
135
+ 2. `python -m build` from `sdks/python/`; check the wheel installs clean.
136
+ 3. Reserve/publish `quantufai` on PyPI from the org account (never a personal
137
+ one); enable 2FA + trusted publishing.
138
+ 4. Only then link the SDK from `/docs` (the docs deliberately do not mention
139
+ it today).
140
+ 5. Announcement claim (exact words matter): "quantum compute an AI agent can
141
+ spend safely — quote-before-spend enforced, auditable receipts." **Never
142
+ claim "first MCP"** (IBM's Qiskit MCP servers and Conductor's CODA exist);
143
+ "first *governed* one" is the true, stronger claim.
@@ -0,0 +1,31 @@
1
+ ##############################################################################
2
+ # quantufai — the official QuantufAI Python SDK
3
+ #
4
+ # ⚠️ PUBLISH GATE (founder/counsel — DO NOT REMOVE): this package is
5
+ # repo-code ONLY. Do NOT publish to PyPI and do NOT link it from public docs
6
+ # until the founder confirms counsel's provisional filing (the same exposure
7
+ # gate as the publicly-verifiable-receipts surface, advancement roadmap #4).
8
+ # Building/installing locally (`pip install -e sdks/python`) is fine.
9
+ ##############################################################################
10
+
11
+ [build-system]
12
+ requires = ["setuptools>=68"]
13
+ build-backend = "setuptools.build_meta"
14
+
15
+ [project]
16
+ name = "quantufai"
17
+ version = "0.1.0"
18
+ description = "QuantufAI developer SDK: quote quantum runs before any spend, read job status/results/receipts, export circuits, and simulate on the free sandbox — this SDK can price and read, it can never spend."
19
+ readme = "README.md"
20
+ requires-python = ">=3.9"
21
+ license = { text = "Proprietary" }
22
+ authors = [{ name = "QuantufAI, Inc." }]
23
+ # ZERO runtime dependencies by design: the client is stdlib urllib. The one
24
+ # optional extra is offline receipt verification (Ed25519 needs `cryptography`).
25
+ dependencies = []
26
+
27
+ [project.optional-dependencies]
28
+ verify = ["cryptography>=41"]
29
+
30
+ [tool.setuptools]
31
+ packages = ["quantufai"]
@@ -0,0 +1,68 @@
1
+ """quantufai — the official QuantufAI Python SDK.
2
+
3
+ Quantum compute a program can PRICE and READ, but never SPEND: quotes before
4
+ any spend, job status/results with receipts and error bars verbatim, signed
5
+ receipt verification, circuit export, and a free sandbox simulator. There is
6
+ no dispatch or approval method — spending stays with a human in the dashboard
7
+ (see ``quantufai.client`` for the full contract).
8
+
9
+ This package intentionally replaces the repo's former SDK stubs, which called
10
+ endpoints that did not exist and fabricated job statuses from pattern-matched
11
+ text. Everything here calls the real, mounted, scope-gated API — and what the
12
+ API refuses, this SDK reports as a typed ``REFUSED``, never a made-up answer.
13
+ """
14
+ from ._version import __version__
15
+ from .client import FREE_SIMULATOR_PROVIDER, Client, ReceiptVerification
16
+ from .types import (
17
+ FAILED,
18
+ QUEUED,
19
+ REFUSED,
20
+ RUNNING,
21
+ SUCCEEDED,
22
+ CircuitExport,
23
+ GovernedReceipt,
24
+ JobResult,
25
+ JobStatus,
26
+ Me,
27
+ PlatformError,
28
+ Quote,
29
+ QuoteResponse,
30
+ ReceiptBundle,
31
+ Refusal,
32
+ ResultExport,
33
+ SandboxRun,
34
+ TransportError,
35
+ is_terminal,
36
+ normalize_state,
37
+ )
38
+ from .verify import ChainCheck, OfflineVerification, verify_receipt_offline
39
+
40
+ __all__ = [
41
+ "__version__",
42
+ "Client",
43
+ "FREE_SIMULATOR_PROVIDER",
44
+ "ReceiptVerification",
45
+ "Refusal",
46
+ "REFUSED",
47
+ "QUEUED",
48
+ "RUNNING",
49
+ "SUCCEEDED",
50
+ "FAILED",
51
+ "Quote",
52
+ "QuoteResponse",
53
+ "JobStatus",
54
+ "JobResult",
55
+ "ReceiptBundle",
56
+ "GovernedReceipt",
57
+ "CircuitExport",
58
+ "ResultExport",
59
+ "SandboxRun",
60
+ "Me",
61
+ "TransportError",
62
+ "PlatformError",
63
+ "normalize_state",
64
+ "is_terminal",
65
+ "OfflineVerification",
66
+ "ChainCheck",
67
+ "verify_receipt_offline",
68
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,100 @@
1
+ """Canonical JSON — a byte-exact Python port of the platform's stableStringify.
2
+
3
+ Receipt signatures cover ``stableStringify(receipt minus signature fields)``:
4
+ deterministic JSON with lexicographically sorted object keys, serialized the
5
+ way JavaScript's ``JSON.stringify`` serializes scalars. Verification in Python
6
+ therefore needs the SAME bytes — including JavaScript's number formatting
7
+ (``1e21`` -> ``"1e+21"``, ``1e-7`` -> ``"1e-7"``, ``1.0`` -> ``"1"``), which
8
+ differs from Python's ``json.dumps`` in the corners.
9
+
10
+ Parity is pinned by shared test vectors (tests/fixtures/canonical_vectors.json)
11
+ asserted against both this module and the Node implementation.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import math
17
+ from typing import Any
18
+
19
+
20
+ def _js_number(value: float) -> str:
21
+ """Format a float exactly as ECMAScript Number::toString(10) would.
22
+
23
+ JSON.stringify emits non-finite numbers as ``null``; integral values
24
+ without a fractional part; exponential notation only for |exponent|
25
+ outside [-6, 21); exponents unpadded with an explicit sign.
26
+ """
27
+ if math.isnan(value) or math.isinf(value):
28
+ return "null" # JSON.stringify(NaN) === "null"
29
+ if value == 0:
30
+ return "0"
31
+ # Shortest round-trip decimal digits (repr is shortest in Python >= 3.1,
32
+ # same guarantee V8 uses).
33
+ rep = repr(abs(value))
34
+ if "e" in rep or "E" in rep:
35
+ mantissa, _, exp_part = rep.lower().partition("e")
36
+ exp10 = int(exp_part)
37
+ else:
38
+ mantissa, exp10 = rep, 0
39
+ int_part, _, frac_part = mantissa.partition(".")
40
+ digits = (int_part + frac_part).lstrip("0")
41
+ # Decimal point position: value = 0.<digits> * 10**point
42
+ point = len(int_part.lstrip("0")) + exp10 if int_part.strip("0") else (
43
+ exp10 - (len(frac_part) - len(frac_part.lstrip("0")))
44
+ )
45
+ digits = digits.rstrip("0") or "0"
46
+ sign = "-" if value < 0 else ""
47
+
48
+ k = len(digits)
49
+ if k <= point <= 21: # integral, printed in full
50
+ return sign + digits + "0" * (point - k)
51
+ if 0 < point <= 21: # decimal point inside the digits
52
+ return sign + digits[:point] + "." + digits[point:]
53
+ if -6 < point <= 0: # small: leading zeros after "0."
54
+ return sign + "0." + "0" * (-point) + digits
55
+ # Exponential: d[.ddd]e±e (exponent = point - 1, unpadded, signed)
56
+ exponent = point - 1
57
+ exp_str = ("+" if exponent >= 0 else "-") + str(abs(exponent))
58
+ if k == 1:
59
+ return sign + digits + "e" + exp_str
60
+ return sign + digits[0] + "." + digits[1:] + "e" + exp_str
61
+
62
+
63
+ def stable_stringify(value: Any) -> str:
64
+ """Deterministic JSON with sorted object keys — parity with the platform."""
65
+ if value is None:
66
+ return "null"
67
+ if value is True:
68
+ return "true"
69
+ if value is False:
70
+ return "false"
71
+ if isinstance(value, str):
72
+ # ensure_ascii=False matches JSON.stringify (raw UTF-8, control chars
73
+ # escaped); Python and JS agree on the mandatory escape set.
74
+ return json.dumps(value, ensure_ascii=False)
75
+ if isinstance(value, int):
76
+ return str(value)
77
+ if isinstance(value, float):
78
+ # JS has one number type: 1.0 serializes as "1" (handled in _js_number).
79
+ return _js_number(value)
80
+ if isinstance(value, (list, tuple)):
81
+ return "[" + ",".join(stable_stringify(v) for v in value) + "]"
82
+ if isinstance(value, dict):
83
+ # JavaScript's Array.prototype.sort() compares strings by UTF-16 code
84
+ # units; utf-16-be byte order reproduces that exactly (receipt keys are
85
+ # ASCII in practice, but parity should not depend on practice).
86
+ items = sorted(
87
+ ((str(k), v) for k, v in value.items()),
88
+ key=lambda kv: kv[0].encode("utf-16-be"),
89
+ )
90
+ return "{" + ",".join(
91
+ json.dumps(k, ensure_ascii=False) + ":" + stable_stringify(v) for k, v in items
92
+ ) + "}"
93
+ raise TypeError(f"stable_stringify: unsupported type {type(value).__name__}")
94
+
95
+
96
+ def unsigned_canonical(receipt: dict) -> str:
97
+ """The exact bytes both platform signatures cover: the receipt minus its
98
+ ``signature`` and ``publicSignature`` fields."""
99
+ clone = {k: v for k, v in receipt.items() if k not in ("signature", "publicSignature")}
100
+ return stable_stringify(clone)