proof-holdings-sdk 1.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.
- proof_holdings_sdk-1.1.0/.gitignore +7 -0
- proof_holdings_sdk-1.1.0/CHANGELOG.md +21 -0
- proof_holdings_sdk-1.1.0/CLAUDE.md +40 -0
- proof_holdings_sdk-1.1.0/LICENSE +21 -0
- proof_holdings_sdk-1.1.0/PKG-INFO +253 -0
- proof_holdings_sdk-1.1.0/README.md +225 -0
- proof_holdings_sdk-1.1.0/pyproject.toml +44 -0
- proof_holdings_sdk-1.1.0/pytest.ini +2 -0
- proof_holdings_sdk-1.1.0/release-fingerprint.json +5 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/__init__.py +38 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/client.py +111 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/credentials.py +178 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/crypto/__init__.py +29 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/crypto/hitl_crypto.py +428 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/errors.py +119 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/http.py +191 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/jwks/__init__.py +1 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/jwks/key_set_cache.py +340 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/jwks/keyset.py +526 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/polling.py +87 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/py.typed +0 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/__init__.py +37 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/authorizations.py +99 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/circles.py +137 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/confirmations.py +66 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/delegations.py +90 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/hitl.py +113 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/hitl_keys.py +28 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/me.py +67 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/profiles.py +101 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/proof_me.py +40 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/proofs.py +524 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/public_profiles.py +52 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/sessions.py +64 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/streams.py +184 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/templates.py +83 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/verification_requests.py +118 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/verifications.py +214 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/resources/webhook_deliveries.py +48 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/streaming.py +256 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/types.py +2191 -0
- proof_holdings_sdk-1.1.0/src/proof_sdk/version.py +1 -0
- proof_holdings_sdk-1.1.0/tests/conftest.py +27 -0
- proof_holdings_sdk-1.1.0/tests/integration/__init__.py +0 -0
- proof_holdings_sdk-1.1.0/tests/integration/conftest.py +174 -0
- proof_holdings_sdk-1.1.0/tests/integration/test_client.py +83 -0
- proof_holdings_sdk-1.1.0/tests/integration/test_errors.py +75 -0
- proof_holdings_sdk-1.1.0/tests/integration/test_live_lifecycle.py +235 -0
- proof_holdings_sdk-1.1.0/tests/integration/test_polling.py +86 -0
- proof_holdings_sdk-1.1.0/tests/integration/test_proof_me.py +49 -0
- proof_holdings_sdk-1.1.0/tests/integration/test_resources.py +57 -0
- proof_holdings_sdk-1.1.0/tests/integration/test_retries.py +29 -0
- proof_holdings_sdk-1.1.0/tests/integration/test_streaming.py +29 -0
- proof_holdings_sdk-1.1.0/tests/test_client.py +36 -0
- proof_holdings_sdk-1.1.0/tests/test_credentials_fixtures.py +220 -0
- proof_holdings_sdk-1.1.0/tests/test_delegations.py +104 -0
- proof_holdings_sdk-1.1.0/tests/test_errors.py +106 -0
- proof_holdings_sdk-1.1.0/tests/test_hitl_crypto.py +293 -0
- proof_holdings_sdk-1.1.0/tests/test_http.py +322 -0
- proof_holdings_sdk-1.1.0/tests/test_jwks_rotation_fixtures.py +405 -0
- proof_holdings_sdk-1.1.0/tests/test_offline_revocation.py +574 -0
- proof_holdings_sdk-1.1.0/tests/test_packaging.py +56 -0
- proof_holdings_sdk-1.1.0/tests/test_polling.py +214 -0
- proof_holdings_sdk-1.1.0/tests/test_resources.py +814 -0
- proof_holdings_sdk-1.1.0/tests/test_streaming.py +586 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to [`proof-holdings-sdk`](https://pypi.org/project/proof-holdings-sdk/) (import name
|
|
4
|
+
`proof_sdk`). Source: [ProofHoldings/sdk-python](https://github.com/ProofHoldings/sdk-python).
|
|
5
|
+
|
|
6
|
+
## 1.1.0 — 2026-10-14
|
|
7
|
+
|
|
8
|
+
The first release on PyPI.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Proof()` without a key resolves one: a non-empty `PROOF_API_KEY`, else the entry that
|
|
13
|
+
`npx @proof-holdings/mcp-server login` saved in `~/.proof-holdings/credentials` for the client's base
|
|
14
|
+
URL. The base URL is the `base_url` argument, else `PROOF_BASE_URL`, else
|
|
15
|
+
`https://api.proof.holdings`. An explicit key behaves as before; an explicit empty key is an error.
|
|
16
|
+
- `verifications.delete_domain_verification(id)` deletes a domain verification attempt that is not
|
|
17
|
+
verified (pending, verifying or failed). A verified domain is refused with 409 `domain_verified`.
|
|
18
|
+
- Types for account deletion and export, identifier, domain and project removal, the artifact claim
|
|
19
|
+
lookup, and the Delegations section of a public profile.
|
|
20
|
+
- API-key listings carry `replaced_key_ids`: a key's usage covers every key it replaced.
|
|
21
|
+
- The `free` billing plan in `BillingPlan`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Python SDK CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Official Python client SDK for the proof.holdings REST API. PyPI distribution `proof-holdings-sdk`; the import package stays `proof_sdk` under `src/`, and because the two names differ, `pyproject.toml`'s wheel target names `src/proof_sdk` explicitly.
|
|
5
|
+
|
|
6
|
+
## Narrative Summary
|
|
7
|
+
The client (`client.py`) wires 17 resources: verifications, verification_requests, proofs, sessions, streams, webhook_deliveries, authorizations, confirmations, hitl, hitl_keys, templates, profiles, public_profiles, me, proof_me, circles, delegations. **api-map rule**: a module counts toward `available` only once it is BOTH re-exported from `resources/__init__.py` AND instantiated as `Class(self._http)` in `client.py` — keyword arguments after `self._http` are allowed (`Proofs(self._http, max_jwks_age=...)`), and both parsers of `client.py` accept them: `getWiredPyResources` in `scripts/api-map-shared.ts` and `readPyClient` in `src/__tests__/drift/sdk-code-samples.test.ts`. **Construction**: `Proof(api_key: str | None = None, *, base_url: str | None = None, …)` — an omitted key and the base URL resolve through `proof_sdk/credentials.py` (stdlib only, so `test_packaging.py` sees nothing new; the cross-SDK contract is in `../CLAUDE.md`): `resolve_api_key`, `resolve_base_url`, `credential_key`, `default_credentials_path`, `read_credentials_file`, `missing_key_message`, the frozen dataclass `ResolvedKey`, `CredentialsFileError`, `LOGIN_COMMAND` and `DEFAULT_BASE_URL` (which `client.py` re-exports, having been its home before). `__init__.py` exports `resolve_api_key`, `resolve_base_url`, `ResolvedKey`, `LOGIN_COMMAND`. The HTTP layer (`http.py`) exposes `request`/`get`/`post`/`put`/`delete` (`delete` accepts a `json=` body). `AsyncClient` is built with `follow_redirects=True` so the avatar 301 is followed; non-JSON 2xx returns a `{"data": bytes, "content_type": str}` envelope (see `../CLAUDE.md`).
|
|
8
|
+
|
|
9
|
+
- `hitl` — config CRUD (`create`/`list`/`retrieve`/`update`/`archive`/`finalize`/`request_authorization`/`create_chat_id_discovery`/`get_approver_keys`).
|
|
10
|
+
- `templates` (7 → `/templates*`), `profiles` (10 incl. profile-template CRUD → `/me/profiles*`), `public_profiles` (`get_by_id`/`get_avatar`/`get_by_username`/`get_badge`/`check_username`, unauthenticated; `get_by_id` takes the PUBLIC `profile_id` only; `get_avatar` returns `{"data": bytes, "content_type": str}`; `get_badge(username)` returns the embed descriptor — branch on its `state`, since a username with no public record answers 200, never 404).
|
|
11
|
+
- `me` (class `Me`) — the dual-auth data surface `summary`/`search`/`api_key_self`/`api_key_usage`.
|
|
12
|
+
- `verifications` — also multi-channel (`create_multi_channel`/`get_multi_channel_status`; returns the generated `MultiChannel*` TypedDicts). Test-mode groups have no completion path (`unsupported_for_group`).
|
|
13
|
+
- `delegations` (class `Delegations`) — Proof of Delegation (`create`/`list`/`retrieve`/`revoke` → `/api/v1/delegations*`). Return annotations use the generated aliases from `types.py`, and since `m-fix-delegation-list-cascade` flattened the four delegation OpenAPI schemas off a shared shape, ALL of them are real TypedDicts — `DelegationWithToken`/`DelegationDetail` had been literal `Any` for as long as they existed (allOf compositions degrade to `Any` in the Python type generator), so the detail shipped untyped until then. `DelegationListResponse.data` is `list[DelegationListItem]` — `Delegation` plus the derived `effective_status`/`is_valid`; the `status` kwarg filters the delegation's own stored record, not the derived one. Since `m-fix-proof-api-consistency` the writes carry the derived pair too, so `create` is annotated `DelegationDetail` and `revoke` `DelegationListItem` (previously `DelegationWithToken` / `Delegation`).
|
|
14
|
+
- `proof_me` (class `ProofMe`) — the Proof-Me surface (2 async methods: `create_identity_challenge`/`get_identity_challenge`; returns the generated `IdentityChallengeResponse` from `types.py`). Challenger enrollment lives on the `circles` resource.
|
|
15
|
+
- `circles` (class `Circles`) — Circle people-primitive: CRUD + member management + member-channel management (13 methods). Response types (`Circle`, `CircleListResponse`, `CircleMember`, `CircleMemberChannel`, `CircleMemberChannelListResponse`, `CircleMemberDrillResponse`, `CircleMemberInviteResponse`, `CircleMemberListResponse`) are imported from the generated `types.py`; the resource module no longer declares them as inline `TypedDict`s (unified 2026-09, `m-sdk-proofme-circle-reshape` — the two had drifted into a field-for-field duplicate).
|
|
16
|
+
|
|
17
|
+
**⚠️ Parser constraints (load-bearing) — `parsePySdkMethods` in `scripts/api-map-shared.ts`:**
|
|
18
|
+
- The PATCH verb MUST stay on the SAME LINE as `self._http.request(` — the same-line regex only recognizes `self._http.request("VERB", ...)` when `"VERB"` is on the opening line; breaking it across lines silently DROPS the endpoint from the `available` claim (affects `hitl.update`, `profiles.update`).
|
|
19
|
+
- Multi-segment path f-strings (e.g. `profiles.update_template`/`delete_template`) MUST stay on ONE line — the parser doesn't join adjacent string literals across lines.
|
|
20
|
+
|
|
21
|
+
The multi-channel status f-string uses `{quote(group_id, safe='')}` with SINGLE quotes — nesting `safe=""` in a double-quoted f-string is PEP 701 (3.12+) while `requires-python` is `>=3.10` (several pre-existing methods already violate this — a known follow-up).
|
|
22
|
+
|
|
23
|
+
- **Streaming** (`streaming.py`, `resources/streams.py`) — every stream is an async generator yielding `{"event", "id", "data"}` dicts like the other SDKs (`_parse_sse_block` keeps the `event:`/`id:` fields; `data` is JSON-decoded, or the raw text when not JSON); the polling fallback yields `poll` events and a terminal snapshot as `terminal`. `cancel_event` is RACED against each socket read (`_until_cancelled`), so a set event stops the generator promptly even while the server is silent between its 30s keepalives, raising `asyncio.CancelledError`. The asset stream's fallback polls `…/assets/{idx}/status`, ends on `verified`/`failed`/`expired`/`revoked`, and — through `request_terminal_states` — on a request `cancelled`/`expired` (cross-SDK contract in `../CLAUDE.md`). Tests: `tests/test_streaming.py`.
|
|
24
|
+
- `proofs` (class `Proofs`) — `verify_offline(token, *, check_revocation=True)` reads the token's status-list slot by default and enforces the key-rotation rules (contract in `../CLAUDE.md`); `refresh_jwks()` forces a refetch, `clear_tombstones()` resets tombstones, `Proof(max_jwks_age=...)` (seconds, validated by `validate_max_jwks_age` when `Proofs` is constructed) bounds a stale key set. Python-specific:
|
|
25
|
+
- `proof_sdk/jwks/keyset.py` ports the verifier's `packages/delegation-verifier/src/keyset.ts` function for function (state persistence omitted — the key set is in memory only); a rule change lands in the verifier first. `proof_sdk/jwks/key_set_cache.py` is the I/O shell (`KeySetCache`: TTL, cooldowns, confirmed-only fallback, staleness; single-flight is one asyncio Task awaited through `asyncio.shield`, so a cancelled waiter does not cancel the shared fetch). Test seams `set_jwks_clock_for_tests` / `set_jwks_event_listener_for_tests`; a tombstone naming a live key is logged as a warning on the `proof_sdk` logger.
|
|
26
|
+
- PyJWT only imports keys (`jwt.PyJWK`) and checks signatures (`jwt.PyJWS().decode` per candidate). The claims are checked by `_claims_failure` in `resources/proofs.py` on the SDK's clock, which PyJWT cannot take: `exp`/`iat`/`nbf` must be finite numbers when present (a NumericDate must be a JSON number — PyJWT coerced numeric strings), `sub`/`jti` strings when present, and on the token path (not the status list) `iss` must match and a non-empty `aud` is refused (no audience is asked for).
|
|
27
|
+
- The key set and the status list both go through `http.fetch_public`, a second credential-free `AsyncClient` (`follow_redirects=False`, branded UA) that STREAMS via `client.stream` under one `asyncio.wait_for` over the whole transfer — httpx's own timeout bounds each socket operation and the read loop would restart it on every chunk — and abandons the body one byte past `max_bytes` (512 KiB). `verify_offline` answers a result dict on an unexpected error during checking rather than raising (it still raises `ImportError` without PyJWT), and `_origin` is wrapped where it is called: `urlsplit(...).port` RAISES for a port outside 0-65535 and `urlsplit` for a malformed IPv6 literal, which made this the only SDK that raised instead of failing closed.
|
|
28
|
+
|
|
29
|
+
## Key Files
|
|
30
|
+
- `src/proof_sdk/` (package root), `pyproject.toml`, `pytest.ini`, `tests/conftest.py`.
|
|
31
|
+
|
|
32
|
+
## Test Layout
|
|
33
|
+
- **Unit** (`tests/test_*.py`) via `pytest --ignore=tests/integration` (what `python-sdk-tests` runs on PRs after `pip install -e '.[jwt]'`; `pytest.ini` sets no `testpaths`, so a bare `pytest` also collects the integration suite). `test_resources.py` covers templates/profiles/public_profiles; `test_http.py` covers the `{data, content_type}` envelope + the `follow_redirects` assertion; `test_packaging.py` is a packaging drift guard — every top-level third-party import in `src/proof_sdk` must be covered by a declared `[project]` dependency in `pyproject.toml` (runtime deps: `httpx`, `typing-extensions`, `cryptography` — the last because `proof_sdk.crypto.hitl_crypto` imports it at module top and a confirmation cannot be created without `encrypt_message`, so a bare `pip install proof-holdings-sdk` must carry it; only `jwt`, the offline-verification extra imported lazily in `proofs.py` and `jwks/key_set_cache.py`, is exempt). `test_offline_revocation.py` runs against a real loopback HTTP server; `test_jwks_rotation_fixtures.py` runs the shared rotation cases through an `httpx.MockTransport` patched into the SDK's `AsyncClient`. `test_credentials_fixtures.py` runs the shared credentials cases through `Proof()` with `HOME` monkeypatched to `tmp_path`; the `AsyncClient` it swaps in must be built with the SDK's `base_url` and headers (the key read back from `proof._http._api_key`), as `tests/integration/conftest.py` does, because `http.py` puts both on the client and `request()` passes none per request.
|
|
34
|
+
- **Integration** (`tests/integration/`) — dual-mode via `httpx.MockTransport` in `conftest.py` (live when `PROOF_BASE_URL`+`PROOF_API_KEY_TEST` are set). Live staging is behind Cloudflare Access; header injection happens ONLY in `conftest.py`'s `_patch_client_with_cf_access` (production client stays unaware). Any client rebuild must go through `_sdk_default_headers()` to preserve the branded `proof-sdk-python/<VERSION>` UA. See `../CLAUDE.md` for the CF-Access / User-Agent / phone-identifier invariants.
|
|
35
|
+
|
|
36
|
+
Invoke (mock): `pytest tests/integration/`.
|
|
37
|
+
|
|
38
|
+
## Related Documentation
|
|
39
|
+
- `../CLAUDE.md` - SDK suite overview (cross-SDK invariants).
|
|
40
|
+
- `../../docs/api-map.yaml` - API parity map.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Proof Holdings
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: proof-holdings-sdk
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Official Python SDK for the proof.holdings verification API
|
|
5
|
+
Project-URL: Homepage, https://proof.holdings
|
|
6
|
+
Project-URL: Documentation, https://api.proof.holdings/api/docs/
|
|
7
|
+
Project-URL: Repository, https://github.com/ProofHoldings/sdk-python
|
|
8
|
+
Author: proof.holdings
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,authentication,jwt,proof-holdings,sdk,verification
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: cryptography>=41.0.0
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Requires-Dist: typing-extensions>=4.0
|
|
25
|
+
Provides-Extra: jwt
|
|
26
|
+
Requires-Dist: pyjwt>=2.8.0; extra == 'jwt'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# proof-holdings-sdk
|
|
30
|
+
|
|
31
|
+
Official Python SDK for the [proof.holdings](https://proof.holdings) verification API.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install proof-holdings-sdk
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For offline JWT verification:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install 'proof-holdings-sdk[jwt]'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
from proof_sdk import Proof
|
|
50
|
+
|
|
51
|
+
async def main():
|
|
52
|
+
async with Proof("pk_live_...") as proof:
|
|
53
|
+
# Create a phone verification
|
|
54
|
+
v = await proof.verifications.create(
|
|
55
|
+
type="phone",
|
|
56
|
+
channel="whatsapp",
|
|
57
|
+
identifier="+1234567890",
|
|
58
|
+
)
|
|
59
|
+
print(f"Verification {v['id']} created: {v['status']}")
|
|
60
|
+
|
|
61
|
+
# Wait for user to complete verification
|
|
62
|
+
result = await proof.verifications.wait_for_completion(v["id"])
|
|
63
|
+
print(f"Result: {result['status']}")
|
|
64
|
+
|
|
65
|
+
asyncio.run(main())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Resources
|
|
69
|
+
|
|
70
|
+
### Verifications
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# Create
|
|
74
|
+
v = await proof.verifications.create(type="domain", channel="dns", identifier="example.com")
|
|
75
|
+
|
|
76
|
+
# Retrieve
|
|
77
|
+
v = await proof.verifications.retrieve("ver_abc123")
|
|
78
|
+
|
|
79
|
+
# List with filters
|
|
80
|
+
page = await proof.verifications.list(status="verified", type="phone", limit=10)
|
|
81
|
+
|
|
82
|
+
# Trigger DNS/HTTP check
|
|
83
|
+
v = await proof.verifications.verify("ver_abc123")
|
|
84
|
+
|
|
85
|
+
# Submit OTP code
|
|
86
|
+
v = await proof.verifications.submit("ver_abc123", code="ABC123")
|
|
87
|
+
|
|
88
|
+
# Poll until complete
|
|
89
|
+
v = await proof.verifications.wait_for_completion("ver_abc123", interval=2.0, timeout=300.0)
|
|
90
|
+
|
|
91
|
+
# WABA (WhatsApp Business) verification
|
|
92
|
+
v = await proof.verifications.create(type="waba", channel="waba_otp", identifier="+37069199199")
|
|
93
|
+
# OTP is sent via WhatsApp — submit with proof.verifications.submit(v["id"], code="123456")
|
|
94
|
+
|
|
95
|
+
# Telegram Bot verification
|
|
96
|
+
v = await proof.verifications.create(
|
|
97
|
+
type="telegram_bot", channel="telegram_bot_token", identifier="@mybusinessbot"
|
|
98
|
+
)
|
|
99
|
+
# Submit bot token with proof.verifications.submit(v["id"], code="bot_token_here")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Verification Requests (Multi-Asset)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Create multi-asset request
|
|
106
|
+
req = await proof.verification_requests.create(
|
|
107
|
+
assets=[
|
|
108
|
+
{"type": "phone", "required": True},
|
|
109
|
+
{"type": "email", "identifier": "user@example.com"},
|
|
110
|
+
],
|
|
111
|
+
reference_id="user_123",
|
|
112
|
+
callback_url="https://yourapp.com/webhook",
|
|
113
|
+
expires_in=86400, # 24 hours in seconds
|
|
114
|
+
)
|
|
115
|
+
print(f"Send user to: {req['verification_url']}")
|
|
116
|
+
|
|
117
|
+
# Poll until complete
|
|
118
|
+
result = await proof.verification_requests.wait_for_completion(req["id"])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Proofs
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# Validate online (checks revocation)
|
|
125
|
+
result = await proof.proofs.validate("eyJhbGciOi...")
|
|
126
|
+
|
|
127
|
+
# Revoke
|
|
128
|
+
response = await proof.proofs.revoke("ver_abc123", reason="User requested")
|
|
129
|
+
|
|
130
|
+
# Get revocation list
|
|
131
|
+
revoked = await proof.proofs.list_revoked()
|
|
132
|
+
|
|
133
|
+
# Offline verification (requires proof-holdings-sdk[jwt]) — no API call with your key. Reads two public
|
|
134
|
+
# documents: the JWKS for the signature, and the issuer's status list for revocation, which is
|
|
135
|
+
# checked by default.
|
|
136
|
+
result = await proof.proofs.verify_offline("eyJhbGciOi...")
|
|
137
|
+
if not result["valid"]:
|
|
138
|
+
# "revoked" | "suspended" | "expired" | "invalid" | "status_unavailable"
|
|
139
|
+
# | "key_revoked" | "jwks_stale" | "wrong_key_purpose" | "outside_key_window"
|
|
140
|
+
# | "token_lifetime_exceeded" | "unknown_key" — see "Signing-key rotation" below
|
|
141
|
+
print(result["reason"])
|
|
142
|
+
|
|
143
|
+
# "status_unavailable" means the revocation status could NOT be read — an unreachable list, one
|
|
144
|
+
# that failed its own checks, or a slot the list does not cover. It is a refusal to guess, not a
|
|
145
|
+
# claim that the proof is withdrawn. A token minted without a status-list slot stays valid and
|
|
146
|
+
# reports revocation_checked=False, exactly as the issuer's own validate answers for it.
|
|
147
|
+
|
|
148
|
+
# Signature only, for an air-gapped check against a warm key cache:
|
|
149
|
+
signature_only = await proof.proofs.verify_offline("eyJhbGciOi...", check_revocation=False)
|
|
150
|
+
assert signature_only["revocation_checked"] is False # never assume it was checked
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Signing-key rotation
|
|
154
|
+
|
|
155
|
+
`verify_offline` fetches and caches the issuer's key set (JWKS) itself and follows its rotation
|
|
156
|
+
rules. The cached set is reused for 10 minutes; a `kid` it does not know triggers one refetch, at
|
|
157
|
+
most once every 30 seconds.
|
|
158
|
+
|
|
159
|
+
- **`key_revoked`** — the issuer revoked the key that signed the token. The result carries
|
|
160
|
+
`pull_hint: {"issuer_origin", "handle"}`: where to fetch the re-signed token (`handle` is the
|
|
161
|
+
proof's `proof_id`, or a delegation's `sub`). The handle is read from the token itself, whose
|
|
162
|
+
signature a revoked key no longer vouches for: URL-encode it before building a request from it. A
|
|
163
|
+
revocation takes effect only after two key-set fetches at least 5 minutes apart both carry it, and
|
|
164
|
+
is then kept for a year.
|
|
165
|
+
- **`jwks_stale`** — the key set could not be refreshed and the cached copy is older than
|
|
166
|
+
`max_jwks_age` (seconds; default 24 hours, at most 25 days; `Proof(key, max_jwks_age=...)`). The
|
|
167
|
+
result carries `cache_age_seconds`. Call `refresh_jwks()` once the issuer is reachable.
|
|
168
|
+
- **`wrong_key_purpose`**, **`outside_key_window`**, **`token_lifetime_exceeded`** — the key belongs
|
|
169
|
+
to another token family, the token was signed outside the key's validity window, or it lives
|
|
170
|
+
longer than the key allows. **`unknown_key`** — no published key matches the token's `kid`.
|
|
171
|
+
|
|
172
|
+
Revoked-key tombstones are kept in memory only — they are not persisted, so a restarted process
|
|
173
|
+
learns them again from the issuer. `clear_tombstones()` forgets them on purpose. `refresh_jwks()`
|
|
174
|
+
makes the next verification refetch the key set (and drops the cached status list); it does not
|
|
175
|
+
forget tombstones. A tombstone naming a key last seen live is logged as a warning on the
|
|
176
|
+
`proof_sdk` logger.
|
|
177
|
+
|
|
178
|
+
### Sessions (Phone-First Flow)
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
session = await proof.sessions.create(channel="telegram")
|
|
182
|
+
print(f"Deep link: {session['deep_link']}")
|
|
183
|
+
print(f"QR code: {session['qr_code']}")
|
|
184
|
+
|
|
185
|
+
# Poll until verified
|
|
186
|
+
result = await proof.sessions.wait_for_completion(session["id"])
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Webhook Deliveries
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# List deliveries
|
|
193
|
+
deliveries = await proof.webhook_deliveries.list(status="failed")
|
|
194
|
+
|
|
195
|
+
# Retry a failed delivery
|
|
196
|
+
result = await proof.webhook_deliveries.retry("del_abc123")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Error Handling
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from proof_sdk import Proof, NotFoundError, RateLimitError, RequestTimeoutError, ProofError
|
|
203
|
+
|
|
204
|
+
async with Proof("pk_live_...") as proof:
|
|
205
|
+
try:
|
|
206
|
+
v = await proof.verifications.retrieve("nonexistent")
|
|
207
|
+
except NotFoundError as e:
|
|
208
|
+
print(f"Not found: {e.code}")
|
|
209
|
+
except RateLimitError:
|
|
210
|
+
print("Rate limited, try again later")
|
|
211
|
+
except ProofError as e:
|
|
212
|
+
print(f"API error {e.status_code}: {e.code} - {e}")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Configuration
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
proof = Proof(
|
|
219
|
+
"pk_live_...",
|
|
220
|
+
base_url="https://api.proof.holdings", # Default
|
|
221
|
+
timeout=30.0, # Seconds, default 30
|
|
222
|
+
max_retries=2, # Default 2
|
|
223
|
+
max_jwks_age=86_400, # Offline verification stale bound, seconds (default 24h)
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Credentials
|
|
228
|
+
|
|
229
|
+
Sign in once with `npx @proof-holdings/mcp-server login` and construct the client with no key:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
proof = Proof() # or Proof(base_url="...")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
When the key argument is omitted it is resolved in this order:
|
|
236
|
+
|
|
237
|
+
1. `PROOF_API_KEY`, when set and non-empty.
|
|
238
|
+
2. The entry for this client's base URL in `~/.proof-holdings/credentials`, the file the login
|
|
239
|
+
command writes. The file is keyed by base URL, so a key saved against staging is never sent
|
|
240
|
+
to production.
|
|
241
|
+
3. Otherwise construction raises `ValueError`, naming the three roads: pass the key, set
|
|
242
|
+
`PROOF_API_KEY`, or run `npx @proof-holdings/mcp-server login`.
|
|
243
|
+
|
|
244
|
+
The base URL is the `base_url` argument, else a non-empty `PROOF_BASE_URL`, else
|
|
245
|
+
`https://api.proof.holdings`. An explicit key is used as given, and an explicit empty key
|
|
246
|
+
(`Proof("")`) is an error — neither reads the environment or the file.
|
|
247
|
+
|
|
248
|
+
## Requirements
|
|
249
|
+
|
|
250
|
+
- Python >= 3.10
|
|
251
|
+
- httpx >= 0.25.0
|
|
252
|
+
- cryptography >= 41.0.0 (HITL message encryption)
|
|
253
|
+
- PyJWT (optional, the `jwt` extra, for offline verification)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# proof-holdings-sdk
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [proof.holdings](https://proof.holdings) verification API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install proof-holdings-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For offline JWT verification:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install 'proof-holdings-sdk[jwt]'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import asyncio
|
|
21
|
+
from proof_sdk import Proof
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
async with Proof("pk_live_...") as proof:
|
|
25
|
+
# Create a phone verification
|
|
26
|
+
v = await proof.verifications.create(
|
|
27
|
+
type="phone",
|
|
28
|
+
channel="whatsapp",
|
|
29
|
+
identifier="+1234567890",
|
|
30
|
+
)
|
|
31
|
+
print(f"Verification {v['id']} created: {v['status']}")
|
|
32
|
+
|
|
33
|
+
# Wait for user to complete verification
|
|
34
|
+
result = await proof.verifications.wait_for_completion(v["id"])
|
|
35
|
+
print(f"Result: {result['status']}")
|
|
36
|
+
|
|
37
|
+
asyncio.run(main())
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Resources
|
|
41
|
+
|
|
42
|
+
### Verifications
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# Create
|
|
46
|
+
v = await proof.verifications.create(type="domain", channel="dns", identifier="example.com")
|
|
47
|
+
|
|
48
|
+
# Retrieve
|
|
49
|
+
v = await proof.verifications.retrieve("ver_abc123")
|
|
50
|
+
|
|
51
|
+
# List with filters
|
|
52
|
+
page = await proof.verifications.list(status="verified", type="phone", limit=10)
|
|
53
|
+
|
|
54
|
+
# Trigger DNS/HTTP check
|
|
55
|
+
v = await proof.verifications.verify("ver_abc123")
|
|
56
|
+
|
|
57
|
+
# Submit OTP code
|
|
58
|
+
v = await proof.verifications.submit("ver_abc123", code="ABC123")
|
|
59
|
+
|
|
60
|
+
# Poll until complete
|
|
61
|
+
v = await proof.verifications.wait_for_completion("ver_abc123", interval=2.0, timeout=300.0)
|
|
62
|
+
|
|
63
|
+
# WABA (WhatsApp Business) verification
|
|
64
|
+
v = await proof.verifications.create(type="waba", channel="waba_otp", identifier="+37069199199")
|
|
65
|
+
# OTP is sent via WhatsApp — submit with proof.verifications.submit(v["id"], code="123456")
|
|
66
|
+
|
|
67
|
+
# Telegram Bot verification
|
|
68
|
+
v = await proof.verifications.create(
|
|
69
|
+
type="telegram_bot", channel="telegram_bot_token", identifier="@mybusinessbot"
|
|
70
|
+
)
|
|
71
|
+
# Submit bot token with proof.verifications.submit(v["id"], code="bot_token_here")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Verification Requests (Multi-Asset)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Create multi-asset request
|
|
78
|
+
req = await proof.verification_requests.create(
|
|
79
|
+
assets=[
|
|
80
|
+
{"type": "phone", "required": True},
|
|
81
|
+
{"type": "email", "identifier": "user@example.com"},
|
|
82
|
+
],
|
|
83
|
+
reference_id="user_123",
|
|
84
|
+
callback_url="https://yourapp.com/webhook",
|
|
85
|
+
expires_in=86400, # 24 hours in seconds
|
|
86
|
+
)
|
|
87
|
+
print(f"Send user to: {req['verification_url']}")
|
|
88
|
+
|
|
89
|
+
# Poll until complete
|
|
90
|
+
result = await proof.verification_requests.wait_for_completion(req["id"])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Proofs
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Validate online (checks revocation)
|
|
97
|
+
result = await proof.proofs.validate("eyJhbGciOi...")
|
|
98
|
+
|
|
99
|
+
# Revoke
|
|
100
|
+
response = await proof.proofs.revoke("ver_abc123", reason="User requested")
|
|
101
|
+
|
|
102
|
+
# Get revocation list
|
|
103
|
+
revoked = await proof.proofs.list_revoked()
|
|
104
|
+
|
|
105
|
+
# Offline verification (requires proof-holdings-sdk[jwt]) — no API call with your key. Reads two public
|
|
106
|
+
# documents: the JWKS for the signature, and the issuer's status list for revocation, which is
|
|
107
|
+
# checked by default.
|
|
108
|
+
result = await proof.proofs.verify_offline("eyJhbGciOi...")
|
|
109
|
+
if not result["valid"]:
|
|
110
|
+
# "revoked" | "suspended" | "expired" | "invalid" | "status_unavailable"
|
|
111
|
+
# | "key_revoked" | "jwks_stale" | "wrong_key_purpose" | "outside_key_window"
|
|
112
|
+
# | "token_lifetime_exceeded" | "unknown_key" — see "Signing-key rotation" below
|
|
113
|
+
print(result["reason"])
|
|
114
|
+
|
|
115
|
+
# "status_unavailable" means the revocation status could NOT be read — an unreachable list, one
|
|
116
|
+
# that failed its own checks, or a slot the list does not cover. It is a refusal to guess, not a
|
|
117
|
+
# claim that the proof is withdrawn. A token minted without a status-list slot stays valid and
|
|
118
|
+
# reports revocation_checked=False, exactly as the issuer's own validate answers for it.
|
|
119
|
+
|
|
120
|
+
# Signature only, for an air-gapped check against a warm key cache:
|
|
121
|
+
signature_only = await proof.proofs.verify_offline("eyJhbGciOi...", check_revocation=False)
|
|
122
|
+
assert signature_only["revocation_checked"] is False # never assume it was checked
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Signing-key rotation
|
|
126
|
+
|
|
127
|
+
`verify_offline` fetches and caches the issuer's key set (JWKS) itself and follows its rotation
|
|
128
|
+
rules. The cached set is reused for 10 minutes; a `kid` it does not know triggers one refetch, at
|
|
129
|
+
most once every 30 seconds.
|
|
130
|
+
|
|
131
|
+
- **`key_revoked`** — the issuer revoked the key that signed the token. The result carries
|
|
132
|
+
`pull_hint: {"issuer_origin", "handle"}`: where to fetch the re-signed token (`handle` is the
|
|
133
|
+
proof's `proof_id`, or a delegation's `sub`). The handle is read from the token itself, whose
|
|
134
|
+
signature a revoked key no longer vouches for: URL-encode it before building a request from it. A
|
|
135
|
+
revocation takes effect only after two key-set fetches at least 5 minutes apart both carry it, and
|
|
136
|
+
is then kept for a year.
|
|
137
|
+
- **`jwks_stale`** — the key set could not be refreshed and the cached copy is older than
|
|
138
|
+
`max_jwks_age` (seconds; default 24 hours, at most 25 days; `Proof(key, max_jwks_age=...)`). The
|
|
139
|
+
result carries `cache_age_seconds`. Call `refresh_jwks()` once the issuer is reachable.
|
|
140
|
+
- **`wrong_key_purpose`**, **`outside_key_window`**, **`token_lifetime_exceeded`** — the key belongs
|
|
141
|
+
to another token family, the token was signed outside the key's validity window, or it lives
|
|
142
|
+
longer than the key allows. **`unknown_key`** — no published key matches the token's `kid`.
|
|
143
|
+
|
|
144
|
+
Revoked-key tombstones are kept in memory only — they are not persisted, so a restarted process
|
|
145
|
+
learns them again from the issuer. `clear_tombstones()` forgets them on purpose. `refresh_jwks()`
|
|
146
|
+
makes the next verification refetch the key set (and drops the cached status list); it does not
|
|
147
|
+
forget tombstones. A tombstone naming a key last seen live is logged as a warning on the
|
|
148
|
+
`proof_sdk` logger.
|
|
149
|
+
|
|
150
|
+
### Sessions (Phone-First Flow)
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
session = await proof.sessions.create(channel="telegram")
|
|
154
|
+
print(f"Deep link: {session['deep_link']}")
|
|
155
|
+
print(f"QR code: {session['qr_code']}")
|
|
156
|
+
|
|
157
|
+
# Poll until verified
|
|
158
|
+
result = await proof.sessions.wait_for_completion(session["id"])
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Webhook Deliveries
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# List deliveries
|
|
165
|
+
deliveries = await proof.webhook_deliveries.list(status="failed")
|
|
166
|
+
|
|
167
|
+
# Retry a failed delivery
|
|
168
|
+
result = await proof.webhook_deliveries.retry("del_abc123")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Error Handling
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from proof_sdk import Proof, NotFoundError, RateLimitError, RequestTimeoutError, ProofError
|
|
175
|
+
|
|
176
|
+
async with Proof("pk_live_...") as proof:
|
|
177
|
+
try:
|
|
178
|
+
v = await proof.verifications.retrieve("nonexistent")
|
|
179
|
+
except NotFoundError as e:
|
|
180
|
+
print(f"Not found: {e.code}")
|
|
181
|
+
except RateLimitError:
|
|
182
|
+
print("Rate limited, try again later")
|
|
183
|
+
except ProofError as e:
|
|
184
|
+
print(f"API error {e.status_code}: {e.code} - {e}")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Configuration
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
proof = Proof(
|
|
191
|
+
"pk_live_...",
|
|
192
|
+
base_url="https://api.proof.holdings", # Default
|
|
193
|
+
timeout=30.0, # Seconds, default 30
|
|
194
|
+
max_retries=2, # Default 2
|
|
195
|
+
max_jwks_age=86_400, # Offline verification stale bound, seconds (default 24h)
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Credentials
|
|
200
|
+
|
|
201
|
+
Sign in once with `npx @proof-holdings/mcp-server login` and construct the client with no key:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
proof = Proof() # or Proof(base_url="...")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
When the key argument is omitted it is resolved in this order:
|
|
208
|
+
|
|
209
|
+
1. `PROOF_API_KEY`, when set and non-empty.
|
|
210
|
+
2. The entry for this client's base URL in `~/.proof-holdings/credentials`, the file the login
|
|
211
|
+
command writes. The file is keyed by base URL, so a key saved against staging is never sent
|
|
212
|
+
to production.
|
|
213
|
+
3. Otherwise construction raises `ValueError`, naming the three roads: pass the key, set
|
|
214
|
+
`PROOF_API_KEY`, or run `npx @proof-holdings/mcp-server login`.
|
|
215
|
+
|
|
216
|
+
The base URL is the `base_url` argument, else a non-empty `PROOF_BASE_URL`, else
|
|
217
|
+
`https://api.proof.holdings`. An explicit key is used as given, and an explicit empty key
|
|
218
|
+
(`Proof("")`) is an error — neither reads the environment or the file.
|
|
219
|
+
|
|
220
|
+
## Requirements
|
|
221
|
+
|
|
222
|
+
- Python >= 3.10
|
|
223
|
+
- httpx >= 0.25.0
|
|
224
|
+
- cryptography >= 41.0.0 (HITL message encryption)
|
|
225
|
+
- PyJWT (optional, the `jwt` extra, for offline verification)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "proof-holdings-sdk"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "Official Python SDK for the proof.holdings verification API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "proof.holdings" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["proof-holdings", "verification", "sdk", "authentication", "api", "jwt"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.25.0",
|
|
29
|
+
"typing-extensions>=4.0",
|
|
30
|
+
"cryptography>=41.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
jwt = [
|
|
35
|
+
"PyJWT>=2.8.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/proof_sdk"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://proof.holdings"
|
|
43
|
+
Documentation = "https://api.proof.holdings/api/docs/"
|
|
44
|
+
Repository = "https://github.com/ProofHoldings/sdk-python"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Official Python SDK for the proof.holdings verification API."""
|
|
2
|
+
|
|
3
|
+
from .client import Proof
|
|
4
|
+
from .credentials import LOGIN_COMMAND, ResolvedKey, resolve_api_key, resolve_base_url
|
|
5
|
+
from .errors import (
|
|
6
|
+
ProofError,
|
|
7
|
+
ValidationError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
ForbiddenError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
ConflictError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
ServerError,
|
|
14
|
+
NetworkError,
|
|
15
|
+
RequestTimeoutError,
|
|
16
|
+
PollingTimeoutError,
|
|
17
|
+
)
|
|
18
|
+
from .version import VERSION
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Proof",
|
|
22
|
+
"ProofError",
|
|
23
|
+
"ValidationError",
|
|
24
|
+
"AuthenticationError",
|
|
25
|
+
"ForbiddenError",
|
|
26
|
+
"NotFoundError",
|
|
27
|
+
"ConflictError",
|
|
28
|
+
"RateLimitError",
|
|
29
|
+
"ServerError",
|
|
30
|
+
"NetworkError",
|
|
31
|
+
"RequestTimeoutError",
|
|
32
|
+
"PollingTimeoutError",
|
|
33
|
+
"LOGIN_COMMAND",
|
|
34
|
+
"ResolvedKey",
|
|
35
|
+
"resolve_api_key",
|
|
36
|
+
"resolve_base_url",
|
|
37
|
+
"VERSION",
|
|
38
|
+
]
|