truthlock 0.3.0__py3-none-any.whl
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.
- truthlock/__init__.py +31 -0
- truthlock/client.py +436 -0
- truthlock/errors.py +44 -0
- truthlock/models.py +159 -0
- truthlock/py.typed +0 -0
- truthlock-0.3.0.dist-info/METADATA +287 -0
- truthlock-0.3.0.dist-info/RECORD +8 -0
- truthlock-0.3.0.dist-info/WHEEL +4 -0
truthlock/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Truthlock Python SDK — Cryptographic Trust Infrastructure."""
|
|
2
|
+
|
|
3
|
+
from .client import TruthlockClient
|
|
4
|
+
from .models import (
|
|
5
|
+
Algorithm,
|
|
6
|
+
Verdict,
|
|
7
|
+
AttestationStatus,
|
|
8
|
+
Attestation,
|
|
9
|
+
Issuer,
|
|
10
|
+
IssuerKey,
|
|
11
|
+
VerifyResult,
|
|
12
|
+
ProofBundle,
|
|
13
|
+
)
|
|
14
|
+
from .errors import TruthlockError, AuthenticationError, NotFoundError, ValidationError
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"TruthlockClient",
|
|
19
|
+
"Algorithm",
|
|
20
|
+
"Verdict",
|
|
21
|
+
"AttestationStatus",
|
|
22
|
+
"Attestation",
|
|
23
|
+
"Issuer",
|
|
24
|
+
"IssuerKey",
|
|
25
|
+
"VerifyResult",
|
|
26
|
+
"ProofBundle",
|
|
27
|
+
"TruthlockError",
|
|
28
|
+
"AuthenticationError",
|
|
29
|
+
"NotFoundError",
|
|
30
|
+
"ValidationError",
|
|
31
|
+
]
|
truthlock/client.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Truthlock Python SDK client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .errors import (
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServerError,
|
|
15
|
+
TruthlockError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
)
|
|
18
|
+
from .models import (
|
|
19
|
+
Attestation, Issuer, IssuerKey, ProofBundle, VerifyResult, Verdict,
|
|
20
|
+
ReceiptEvent, ReceiptType, MintReceiptRequest, ListReceiptsFilter,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TruthlockClient:
|
|
25
|
+
"""Client for the Truthlocks verification API.
|
|
26
|
+
|
|
27
|
+
Usage::
|
|
28
|
+
|
|
29
|
+
from truthlock import TruthlockClient
|
|
30
|
+
|
|
31
|
+
client = TruthlockClient(api_key="your-api-key")
|
|
32
|
+
|
|
33
|
+
# Mint an attestation
|
|
34
|
+
att = client.attestations.mint(
|
|
35
|
+
issuer_id="iss_...",
|
|
36
|
+
kid="key_...",
|
|
37
|
+
alg="Ed25519",
|
|
38
|
+
payload_b64url="...",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Verify an attestation
|
|
42
|
+
result = client.verify.verify_online(attestation_id=att.attestation_id)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
api_key: str | None = None,
|
|
48
|
+
base_url: str = "https://api.truthlocks.com",
|
|
49
|
+
environment: str = "production",
|
|
50
|
+
timeout: float = 30.0,
|
|
51
|
+
max_retries: int = 3,
|
|
52
|
+
):
|
|
53
|
+
self._base_url = base_url.rstrip("/")
|
|
54
|
+
self._api_key = api_key
|
|
55
|
+
self._environment = environment
|
|
56
|
+
self._max_retries = max_retries
|
|
57
|
+
self._http = httpx.Client(
|
|
58
|
+
base_url=self._base_url,
|
|
59
|
+
timeout=timeout,
|
|
60
|
+
headers=self._build_headers(),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.issuers = _IssuersResource(self)
|
|
64
|
+
self.keys = _KeysResource(self)
|
|
65
|
+
self.attestations = _AttestationsResource(self)
|
|
66
|
+
self.verify = _VerifyResource(self)
|
|
67
|
+
self.receipts = _ReceiptsResource(self)
|
|
68
|
+
|
|
69
|
+
def _build_headers(self) -> dict[str, str]:
|
|
70
|
+
headers: dict[str, str] = {
|
|
71
|
+
"User-Agent": "truthlock-python/0.1.0",
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"Accept": "application/json",
|
|
74
|
+
}
|
|
75
|
+
if self._api_key:
|
|
76
|
+
headers["X-API-Key"] = self._api_key
|
|
77
|
+
return headers
|
|
78
|
+
|
|
79
|
+
def _request(
|
|
80
|
+
self,
|
|
81
|
+
method: str,
|
|
82
|
+
path: str,
|
|
83
|
+
json: dict[str, Any] | None = None,
|
|
84
|
+
params: dict[str, Any] | None = None,
|
|
85
|
+
idempotent: bool = False,
|
|
86
|
+
) -> Any:
|
|
87
|
+
headers: dict[str, str] = {}
|
|
88
|
+
if idempotent:
|
|
89
|
+
headers["Idempotency-Key"] = str(uuid.uuid4())
|
|
90
|
+
|
|
91
|
+
attempts = 0
|
|
92
|
+
last_error: Exception | None = None
|
|
93
|
+
|
|
94
|
+
while attempts <= self._max_retries:
|
|
95
|
+
try:
|
|
96
|
+
resp = self._http.request(
|
|
97
|
+
method,
|
|
98
|
+
path,
|
|
99
|
+
json=json,
|
|
100
|
+
params=params,
|
|
101
|
+
headers=headers,
|
|
102
|
+
)
|
|
103
|
+
return self._handle_response(resp)
|
|
104
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
|
105
|
+
last_error = e
|
|
106
|
+
attempts += 1
|
|
107
|
+
if attempts > self._max_retries:
|
|
108
|
+
raise TruthlockError(f"Request failed after {self._max_retries} retries: {e}")
|
|
109
|
+
|
|
110
|
+
raise TruthlockError(f"Request failed: {last_error}")
|
|
111
|
+
|
|
112
|
+
def _handle_response(self, resp: httpx.Response) -> Any:
|
|
113
|
+
if resp.status_code == 204:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
if 200 <= resp.status_code < 300:
|
|
117
|
+
return resp.json()
|
|
118
|
+
|
|
119
|
+
body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
|
|
120
|
+
msg = body.get("message", resp.text[:200])
|
|
121
|
+
code = body.get("code", "")
|
|
122
|
+
|
|
123
|
+
if resp.status_code in (401, 403):
|
|
124
|
+
raise AuthenticationError(msg, status_code=resp.status_code, error_code=code)
|
|
125
|
+
if resp.status_code == 404:
|
|
126
|
+
raise NotFoundError(msg, status_code=404, error_code=code)
|
|
127
|
+
if resp.status_code in (400, 422):
|
|
128
|
+
raise ValidationError(msg, status_code=resp.status_code, error_code=code, details=body)
|
|
129
|
+
if resp.status_code == 429:
|
|
130
|
+
retry_after = float(resp.headers.get("Retry-After", 0)) or None
|
|
131
|
+
raise RateLimitError(msg, retry_after=retry_after, status_code=429, error_code=code)
|
|
132
|
+
if resp.status_code >= 500:
|
|
133
|
+
raise ServerError(msg, status_code=resp.status_code, error_code=code)
|
|
134
|
+
|
|
135
|
+
raise TruthlockError(msg, status_code=resp.status_code, error_code=code)
|
|
136
|
+
|
|
137
|
+
def close(self) -> None:
|
|
138
|
+
self._http.close()
|
|
139
|
+
|
|
140
|
+
def __enter__(self) -> TruthlockClient:
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __exit__(self, *args: Any) -> None:
|
|
144
|
+
self.close()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class _IssuersResource:
|
|
148
|
+
def __init__(self, client: TruthlockClient):
|
|
149
|
+
self._client = client
|
|
150
|
+
|
|
151
|
+
def create(
|
|
152
|
+
self,
|
|
153
|
+
name: str,
|
|
154
|
+
legal_name: str | None = None,
|
|
155
|
+
display_name: str | None = None,
|
|
156
|
+
**kwargs: Any,
|
|
157
|
+
) -> Issuer:
|
|
158
|
+
data = {"name": name, **kwargs}
|
|
159
|
+
if legal_name:
|
|
160
|
+
data["legal_name"] = legal_name
|
|
161
|
+
if display_name:
|
|
162
|
+
data["display_name"] = display_name
|
|
163
|
+
resp = self._client._request("POST", "/v1/issuers", json=data, idempotent=True)
|
|
164
|
+
return Issuer(
|
|
165
|
+
id=resp["id"],
|
|
166
|
+
name=resp.get("name", name),
|
|
167
|
+
status=resp.get("status", "ACTIVE"),
|
|
168
|
+
did=resp.get("did"),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def list(self, limit: int = 50, offset: int = 0) -> list[Issuer]:
|
|
172
|
+
resp = self._client._request("GET", "/v1/issuers", params={"limit": limit, "offset": offset})
|
|
173
|
+
items = resp.get("items", resp) if isinstance(resp, dict) else resp
|
|
174
|
+
return [Issuer(id=i["id"], name=i["name"], status=i.get("status", ""), did=i.get("did")) for i in items]
|
|
175
|
+
|
|
176
|
+
def get(self, issuer_id: str) -> Issuer:
|
|
177
|
+
resp = self._client._request("GET", f"/v1/issuers/{issuer_id}")
|
|
178
|
+
return Issuer(
|
|
179
|
+
id=resp["id"],
|
|
180
|
+
name=resp["name"],
|
|
181
|
+
status=resp.get("status", ""),
|
|
182
|
+
did=resp.get("did"),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def trust(self, issuer_id: str) -> dict[str, Any]:
|
|
186
|
+
return self._client._request("POST", f"/v1/issuers/{issuer_id}/trust")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class _KeysResource:
|
|
190
|
+
def __init__(self, client: TruthlockClient):
|
|
191
|
+
self._client = client
|
|
192
|
+
|
|
193
|
+
def register(
|
|
194
|
+
self,
|
|
195
|
+
issuer_id: str,
|
|
196
|
+
kid: str,
|
|
197
|
+
alg: str,
|
|
198
|
+
public_key_b64url: str,
|
|
199
|
+
**kwargs: Any,
|
|
200
|
+
) -> IssuerKey:
|
|
201
|
+
data = {"kid": kid, "alg": alg, "public_key_b64url": public_key_b64url, **kwargs}
|
|
202
|
+
resp = self._client._request("POST", f"/v1/issuers/{issuer_id}/keys", json=data)
|
|
203
|
+
return IssuerKey(
|
|
204
|
+
kid=resp.get("kid", kid),
|
|
205
|
+
issuer_id=issuer_id,
|
|
206
|
+
alg=resp.get("alg", alg),
|
|
207
|
+
public_key=resp.get("public_key", ""),
|
|
208
|
+
status=resp.get("status", "active"),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def list(self, issuer_id: str) -> list[IssuerKey]:
|
|
212
|
+
resp = self._client._request("GET", f"/v1/issuers/{issuer_id}/keys")
|
|
213
|
+
items = resp.get("items", resp) if isinstance(resp, dict) else resp
|
|
214
|
+
return [
|
|
215
|
+
IssuerKey(
|
|
216
|
+
kid=k["kid"],
|
|
217
|
+
issuer_id=issuer_id,
|
|
218
|
+
alg=k.get("alg", ""),
|
|
219
|
+
public_key=k.get("public_key", ""),
|
|
220
|
+
status=k.get("status", ""),
|
|
221
|
+
)
|
|
222
|
+
for k in items
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class _AttestationsResource:
|
|
227
|
+
def __init__(self, client: TruthlockClient):
|
|
228
|
+
self._client = client
|
|
229
|
+
|
|
230
|
+
def mint(
|
|
231
|
+
self,
|
|
232
|
+
issuer_id: str,
|
|
233
|
+
kid: str,
|
|
234
|
+
alg: str,
|
|
235
|
+
payload_b64url: str,
|
|
236
|
+
**kwargs: Any,
|
|
237
|
+
) -> Attestation:
|
|
238
|
+
data = {
|
|
239
|
+
"issuer_id": issuer_id,
|
|
240
|
+
"kid": kid,
|
|
241
|
+
"alg": alg,
|
|
242
|
+
"payload_b64url": payload_b64url,
|
|
243
|
+
**kwargs,
|
|
244
|
+
}
|
|
245
|
+
resp = self._client._request("POST", "/v1/attestations/mint", json=data, idempotent=True)
|
|
246
|
+
return Attestation(
|
|
247
|
+
attestation_id=resp["attestation_id"],
|
|
248
|
+
issuer_id=issuer_id,
|
|
249
|
+
kid=kid,
|
|
250
|
+
alg=alg,
|
|
251
|
+
status=resp.get("status", "active"),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def get(self, attestation_id: str) -> Attestation:
|
|
255
|
+
resp = self._client._request("GET", f"/v1/attestations/{attestation_id}")
|
|
256
|
+
return Attestation(
|
|
257
|
+
attestation_id=resp.get("attestation_id", resp.get("id", attestation_id)),
|
|
258
|
+
issuer_id=resp.get("issuer_id", ""),
|
|
259
|
+
kid=resp.get("kid", ""),
|
|
260
|
+
alg=resp.get("alg", ""),
|
|
261
|
+
status=resp.get("status", ""),
|
|
262
|
+
metadata=resp.get("metadata", {}),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def list(self, limit: int = 50, offset: int = 0) -> list[Attestation]:
|
|
266
|
+
resp = self._client._request(
|
|
267
|
+
"GET", "/v1/attestations", params={"limit": limit, "offset": offset}
|
|
268
|
+
)
|
|
269
|
+
items = resp.get("items", resp) if isinstance(resp, dict) else resp
|
|
270
|
+
return [
|
|
271
|
+
Attestation(
|
|
272
|
+
attestation_id=a.get("attestation_id", a.get("id", "")),
|
|
273
|
+
issuer_id=a.get("issuer_id", ""),
|
|
274
|
+
kid=a.get("kid", ""),
|
|
275
|
+
alg=a.get("alg", ""),
|
|
276
|
+
status=a.get("status", ""),
|
|
277
|
+
)
|
|
278
|
+
for a in items
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
def revoke(self, attestation_id: str, reason: str = "") -> dict[str, Any]:
|
|
282
|
+
return self._client._request(
|
|
283
|
+
"POST",
|
|
284
|
+
f"/v1/attestations/{attestation_id}/revoke",
|
|
285
|
+
json={"reason": reason},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def proof_bundle(self, attestation_id: str) -> ProofBundle:
|
|
289
|
+
resp = self._client._request("GET", f"/v1/attestations/{attestation_id}/proof-bundle")
|
|
290
|
+
return ProofBundle(
|
|
291
|
+
header=resp.get("header", {}),
|
|
292
|
+
attestation=resp.get("attestation", {}),
|
|
293
|
+
proofs=resp.get("proofs", []),
|
|
294
|
+
issuer_certificate=resp.get("issuer_certificate", {}),
|
|
295
|
+
bundle_signature=resp.get("bundle_signature", {}),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class _VerifyResource:
|
|
300
|
+
def __init__(self, client: TruthlockClient):
|
|
301
|
+
self._client = client
|
|
302
|
+
|
|
303
|
+
def verify_online(
|
|
304
|
+
self,
|
|
305
|
+
attestation_id: str,
|
|
306
|
+
payload_b64url: str | None = None,
|
|
307
|
+
) -> VerifyResult:
|
|
308
|
+
data: dict[str, Any] = {"attestation_id": attestation_id}
|
|
309
|
+
if payload_b64url:
|
|
310
|
+
data["payload_b64url"] = payload_b64url
|
|
311
|
+
resp = self._client._request("POST", "/v1/verify", json=data)
|
|
312
|
+
return VerifyResult(
|
|
313
|
+
verdict=Verdict(resp.get("verdict", "unknown")),
|
|
314
|
+
attestation_id=attestation_id,
|
|
315
|
+
issuer_id=resp.get("issuer_id"),
|
|
316
|
+
issued_at=resp.get("issued_at"),
|
|
317
|
+
details=resp.get("details", {}),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class _ReceiptsResource:
|
|
322
|
+
"""Receipt lifecycle operations (Ticket 81: Receipt Canonical Event Schema v2)."""
|
|
323
|
+
|
|
324
|
+
def __init__(self, client: TruthlockClient):
|
|
325
|
+
self._client = client
|
|
326
|
+
|
|
327
|
+
def mint(self, req: MintReceiptRequest, idempotency_key: str | None = None) -> ReceiptEvent:
|
|
328
|
+
"""Mint a cryptographically signed, transparency-log-anchored receipt."""
|
|
329
|
+
import dataclasses
|
|
330
|
+
data = dataclasses.asdict(req)
|
|
331
|
+
headers: dict[str, str] = {}
|
|
332
|
+
if idempotency_key:
|
|
333
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
334
|
+
resp = self._client._request("POST", "/v1/receipts", json=data, idempotent=True)
|
|
335
|
+
return self._parse_receipt(resp)
|
|
336
|
+
|
|
337
|
+
def get(self, receipt_id: str) -> ReceiptEvent:
|
|
338
|
+
"""Retrieve a receipt event by ID."""
|
|
339
|
+
resp = self._client._request("GET", f"/v1/receipts/{receipt_id}")
|
|
340
|
+
return self._parse_receipt(resp)
|
|
341
|
+
|
|
342
|
+
def list(self, f: ListReceiptsFilter | None = None) -> list[ReceiptEvent]:
|
|
343
|
+
"""List receipt events with optional filters."""
|
|
344
|
+
params: dict[str, Any] = {}
|
|
345
|
+
if f:
|
|
346
|
+
if f.receipt_type:
|
|
347
|
+
params["receipt_type"] = f.receipt_type
|
|
348
|
+
if f.issuer_id:
|
|
349
|
+
params["issuer_id"] = f.issuer_id
|
|
350
|
+
if f.status:
|
|
351
|
+
params["status"] = f.status
|
|
352
|
+
params["limit"] = f.limit
|
|
353
|
+
params["offset"] = f.offset
|
|
354
|
+
resp = self._client._request("GET", "/v1/receipts", params=params)
|
|
355
|
+
items = resp.get("items", []) if isinstance(resp, dict) else resp
|
|
356
|
+
return [self._parse_receipt(r) for r in items]
|
|
357
|
+
|
|
358
|
+
def revoke(self, receipt_id: str, reason: str = "") -> ReceiptEvent:
|
|
359
|
+
"""Revoke a receipt event."""
|
|
360
|
+
resp = self._client._request(
|
|
361
|
+
"POST",
|
|
362
|
+
f"/v1/receipts/{receipt_id}/revoke",
|
|
363
|
+
json={"reason": reason} if reason else {},
|
|
364
|
+
idempotent=True,
|
|
365
|
+
)
|
|
366
|
+
return self._parse_receipt(resp)
|
|
367
|
+
|
|
368
|
+
def list_types(self) -> list[ReceiptType]:
|
|
369
|
+
"""List all active receipt type families."""
|
|
370
|
+
resp = self._client._request("GET", "/v1/receipt-types")
|
|
371
|
+
items = resp.get("items", []) if isinstance(resp, dict) else resp
|
|
372
|
+
return [
|
|
373
|
+
ReceiptType(
|
|
374
|
+
id=rt["id"],
|
|
375
|
+
name=rt["name"],
|
|
376
|
+
display_name=rt.get("display_name", rt["name"]),
|
|
377
|
+
version=rt.get("version", "1.0.0"),
|
|
378
|
+
status=rt.get("status", "active"),
|
|
379
|
+
schema=rt.get("schema", {}),
|
|
380
|
+
description=rt.get("description"),
|
|
381
|
+
created_at=rt.get("created_at"),
|
|
382
|
+
)
|
|
383
|
+
for rt in items
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
def get_type(self, name: str) -> ReceiptType:
|
|
387
|
+
"""Get a specific receipt type. Use 'name@version' to pin a version."""
|
|
388
|
+
resp = self._client._request("GET", f"/v1/receipt-types/{name}")
|
|
389
|
+
return ReceiptType(
|
|
390
|
+
id=resp["id"],
|
|
391
|
+
name=resp["name"],
|
|
392
|
+
display_name=resp.get("display_name", resp["name"]),
|
|
393
|
+
version=resp.get("version", "1.0.0"),
|
|
394
|
+
status=resp.get("status", "active"),
|
|
395
|
+
schema=resp.get("schema", {}),
|
|
396
|
+
description=resp.get("description"),
|
|
397
|
+
created_at=resp.get("created_at"),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def get_proof_bundle(self, receipt_id: str) -> dict[str, Any]:
|
|
401
|
+
"""Get the proof bundle for offline verification."""
|
|
402
|
+
return self._client._request("GET", f"/v1/receipts/{receipt_id}/proof-bundle")
|
|
403
|
+
|
|
404
|
+
def verify(self, receipt_id: str) -> dict[str, Any]:
|
|
405
|
+
"""Verify a receipt. Returns verdict: VALID|REVOKED|INVALID_SIGNATURE|KEY_COMPROMISED|NOT_FOUND"""
|
|
406
|
+
return self._client._request("POST", "/v1/receipts/verify", json={"receipt_id": receipt_id})
|
|
407
|
+
|
|
408
|
+
def search(self, **kwargs: Any) -> dict[str, Any]:
|
|
409
|
+
"""Search receipts. Pass q=, receipt_type=, status=, from_date=, to_date=, limit=, offset=."""
|
|
410
|
+
return self._client._request("POST", "/v1/receipts/search", json=kwargs)
|
|
411
|
+
|
|
412
|
+
def export(self, format: str = "json", filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
413
|
+
"""Queue a bulk export. Returns immediately; poll get_export() for download_url."""
|
|
414
|
+
return self._client._request("POST", "/v1/receipts/export", json={"format": format, "filters": filters or {}}, idempotent=True)
|
|
415
|
+
|
|
416
|
+
def get_export(self, export_id: str) -> dict[str, Any]:
|
|
417
|
+
"""Get status and download_url of an export job."""
|
|
418
|
+
return self._client._request("GET", f"/v1/receipts/exports/{export_id}")
|
|
419
|
+
|
|
420
|
+
def redact(self, receipt_id: str) -> dict[str, Any]:
|
|
421
|
+
"""Permanently redact a receipt payload. Cryptographic proof is preserved."""
|
|
422
|
+
return self._client._request("POST", f"/v1/receipts/{receipt_id}/redact", json={}, idempotent=True)
|
|
423
|
+
|
|
424
|
+
def _parse_receipt(self, resp: dict[str, Any]) -> ReceiptEvent:
|
|
425
|
+
return ReceiptEvent(
|
|
426
|
+
receipt_id=resp["receipt_id"],
|
|
427
|
+
receipt_type=resp["receipt_type"],
|
|
428
|
+
receipt_version=resp.get("receipt_version", "1.0.0"),
|
|
429
|
+
status=resp.get("status", "active"),
|
|
430
|
+
issued_at=resp.get("issued_at", ""),
|
|
431
|
+
tenant_id=resp.get("tenant_id", ""),
|
|
432
|
+
issuer_id=resp.get("issuer_id", ""),
|
|
433
|
+
payload_hash=resp.get("payload_hash", ""),
|
|
434
|
+
signature=resp.get("signature", {}),
|
|
435
|
+
log=resp.get("log", {}),
|
|
436
|
+
)
|
truthlock/errors.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Truthlock SDK error types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TruthlockError(Exception):
|
|
8
|
+
"""Base error for all Truthlock SDK errors."""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
message: str,
|
|
13
|
+
status_code: int | None = None,
|
|
14
|
+
error_code: str | None = None,
|
|
15
|
+
details: dict[str, Any] | None = None,
|
|
16
|
+
):
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.error_code = error_code
|
|
20
|
+
self.details = details or {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthenticationError(TruthlockError):
|
|
24
|
+
"""Raised when authentication fails (401/403)."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NotFoundError(TruthlockError):
|
|
28
|
+
"""Raised when a resource is not found (404)."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ValidationError(TruthlockError):
|
|
32
|
+
"""Raised when request validation fails (400/422)."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RateLimitError(TruthlockError):
|
|
36
|
+
"""Raised when rate limit is exceeded (429)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str, retry_after: float | None = None, **kwargs: Any):
|
|
39
|
+
super().__init__(message, **kwargs)
|
|
40
|
+
self.retry_after = retry_after
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ServerError(TruthlockError):
|
|
44
|
+
"""Raised for server-side errors (5xx)."""
|
truthlock/models.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Truthlock SDK data models and enums."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Algorithm(str, Enum):
|
|
12
|
+
ED25519 = "Ed25519"
|
|
13
|
+
ES256 = "ES256"
|
|
14
|
+
RS256 = "RS256"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Verdict(str, Enum):
|
|
18
|
+
VALID = "valid"
|
|
19
|
+
INVALID = "invalid"
|
|
20
|
+
REVOKED = "revoked"
|
|
21
|
+
EXPIRED = "expired"
|
|
22
|
+
UNKNOWN = "unknown"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AttestationStatus(str, Enum):
|
|
26
|
+
ACTIVE = "active"
|
|
27
|
+
REVOKED = "revoked"
|
|
28
|
+
SUPERSEDED = "superseded"
|
|
29
|
+
EXPIRED = "expired"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Issuer:
|
|
34
|
+
id: str
|
|
35
|
+
name: str
|
|
36
|
+
status: str
|
|
37
|
+
did: str | None = None
|
|
38
|
+
tenant_id: str | None = None
|
|
39
|
+
created_at: str | None = None
|
|
40
|
+
updated_at: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class IssuerKey:
|
|
45
|
+
kid: str
|
|
46
|
+
issuer_id: str
|
|
47
|
+
alg: str
|
|
48
|
+
public_key: str
|
|
49
|
+
status: str
|
|
50
|
+
created_at: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Attestation:
|
|
55
|
+
attestation_id: str
|
|
56
|
+
issuer_id: str
|
|
57
|
+
kid: str
|
|
58
|
+
alg: str
|
|
59
|
+
status: str
|
|
60
|
+
payload_hash: str | None = None
|
|
61
|
+
created_at: str | None = None
|
|
62
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class VerifyResult:
|
|
67
|
+
verdict: Verdict
|
|
68
|
+
attestation_id: str
|
|
69
|
+
issuer_id: str | None = None
|
|
70
|
+
issued_at: str | None = None
|
|
71
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ProofBundle:
|
|
76
|
+
header: dict[str, Any] = field(default_factory=dict)
|
|
77
|
+
attestation: dict[str, Any] = field(default_factory=dict)
|
|
78
|
+
proofs: list[dict[str, Any]] = field(default_factory=list)
|
|
79
|
+
issuer_certificate: dict[str, Any] = field(default_factory=dict)
|
|
80
|
+
bundle_signature: dict[str, Any] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ============================================================================
|
|
84
|
+
# Receipt Types (Ticket 81: Receipt Canonical Event Schema v2)
|
|
85
|
+
# ============================================================================
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ReceiptStatus(str, Enum):
|
|
89
|
+
ACTIVE = "active"
|
|
90
|
+
REVOKED = "revoked"
|
|
91
|
+
SUPERSEDED = "superseded"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class RetentionPolicy(str, Enum):
|
|
95
|
+
STANDARD = "standard"
|
|
96
|
+
EXTENDED = "extended"
|
|
97
|
+
PERMANENT = "permanent"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class ReceiptSignature:
|
|
102
|
+
alg: str
|
|
103
|
+
kid: str
|
|
104
|
+
value: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class ReceiptLog:
|
|
109
|
+
log_id: str
|
|
110
|
+
leaf_index: int
|
|
111
|
+
leaf_hash: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ReceiptEvent:
|
|
116
|
+
receipt_id: str
|
|
117
|
+
receipt_type: str
|
|
118
|
+
receipt_version: str
|
|
119
|
+
status: str
|
|
120
|
+
issued_at: str
|
|
121
|
+
tenant_id: str
|
|
122
|
+
issuer_id: str
|
|
123
|
+
payload_hash: str
|
|
124
|
+
signature: dict[str, Any] = field(default_factory=dict)
|
|
125
|
+
log: dict[str, Any] = field(default_factory=dict)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class ReceiptType:
|
|
130
|
+
id: str
|
|
131
|
+
name: str
|
|
132
|
+
display_name: str
|
|
133
|
+
version: str
|
|
134
|
+
status: str
|
|
135
|
+
schema: dict[str, Any] = field(default_factory=dict)
|
|
136
|
+
description: str | None = None
|
|
137
|
+
created_at: str | None = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class MintReceiptRequest:
|
|
142
|
+
issuer_id: str
|
|
143
|
+
kid: str
|
|
144
|
+
alg: str
|
|
145
|
+
receipt_type: str
|
|
146
|
+
subject: str
|
|
147
|
+
payload: dict[str, Any]
|
|
148
|
+
receipt_version: str = "1.0.0"
|
|
149
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
150
|
+
retention_policy: str = RetentionPolicy.STANDARD
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class ListReceiptsFilter:
|
|
155
|
+
receipt_type: str | None = None
|
|
156
|
+
issuer_id: str | None = None
|
|
157
|
+
status: str | None = None
|
|
158
|
+
limit: int = 20
|
|
159
|
+
offset: int = 0
|
truthlock/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: truthlock
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Official Python SDK for the Truthlocks verification platform
|
|
5
|
+
Project-URL: Homepage, https://truthlocks.com
|
|
6
|
+
Project-URL: Documentation, https://docs.truthlocks.com/sdk/python
|
|
7
|
+
Project-URL: Repository, https://github.com/truthlocks/sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/truthlocks/sdk-python/issues
|
|
9
|
+
Author-email: Truthlocks <support@truthlocks.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: attestation,cryptographic-proof,trust,truthlock,verification
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security :: Cryptography
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx>=0.25.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<a href="https://truthlocks.com">
|
|
34
|
+
<img src="https://www.truthlocks.com/logo/logo-color-1.png" alt="Truthlocks" width="200" />
|
|
35
|
+
</a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<h1 align="center">Truthlock Python SDK</h1>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<strong>Official Python SDK for the Truthlocks Platform</strong>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a href="https://pypi.org/project/truthlock/"><img src="https://img.shields.io/pypi/v/truthlock.svg?style=flat-square" alt="PyPI version" /></a>
|
|
46
|
+
<a href="https://pypi.org/project/truthlock/"><img src="https://img.shields.io/pypi/dm/truthlock.svg?style=flat-square" alt="PyPI downloads" /></a>
|
|
47
|
+
<a href="https://github.com/truthlocks/sdk-python/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="License" /></a>
|
|
48
|
+
<a href="https://docs.truthlocks.com/sdk/python"><img src="https://img.shields.io/badge/docs-truthlocks.com-brightgreen.svg?style=flat-square" alt="Documentation" /></a>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<p align="center">
|
|
52
|
+
<a href="https://docs.truthlocks.com/sdk/python">Documentation</a> •
|
|
53
|
+
<a href="https://docs.truthlocks.com/api-reference">API Reference</a> •
|
|
54
|
+
<a href="https://github.com/truthlocks/sdk-python/issues">Issues</a>
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
Pythonic client for the **Truthlocks** cryptographic trust infrastructure. Issue attestations, verify content authenticity, manage issuers and signing keys, and query the audit trail -- with both synchronous and async support.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install truthlock
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires **Python 3.10** or later.
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from truthlock import TruthlockClient
|
|
73
|
+
|
|
74
|
+
client = TruthlockClient(
|
|
75
|
+
base_url="https://api.truthlocks.com",
|
|
76
|
+
api_key="tlk_live_...",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Create an issuer
|
|
80
|
+
issuer = client.issuers.create(
|
|
81
|
+
name="My Organization",
|
|
82
|
+
legal_name="My Organization Inc.",
|
|
83
|
+
display_name="My Org",
|
|
84
|
+
)
|
|
85
|
+
client.issuers.trust(issuer.id)
|
|
86
|
+
|
|
87
|
+
# Register a signing key
|
|
88
|
+
client.keys.register(
|
|
89
|
+
issuer_id=issuer.id,
|
|
90
|
+
kid="key-1",
|
|
91
|
+
alg="ed25519",
|
|
92
|
+
public_key_b64url="your-public-key",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Mint an attestation
|
|
96
|
+
import base64
|
|
97
|
+
payload = base64.urlsafe_b64encode(b"Hello World").rstrip(b"=").decode()
|
|
98
|
+
|
|
99
|
+
attestation = client.attestations.mint(
|
|
100
|
+
issuer_id=issuer.id,
|
|
101
|
+
kid="key-1",
|
|
102
|
+
alg="ed25519",
|
|
103
|
+
payload_b64url=payload,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
print(f"Attestation ID: {attestation.attestation_id}")
|
|
107
|
+
|
|
108
|
+
# Verify
|
|
109
|
+
result = client.verify.online(
|
|
110
|
+
attestation_id=attestation.attestation_id,
|
|
111
|
+
payload_b64url=payload,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if result.verdict == "VALID":
|
|
115
|
+
print("Document verified successfully")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Features
|
|
119
|
+
|
|
120
|
+
| Feature | Description |
|
|
121
|
+
| ------------------- | ----------------------------------------------------------- |
|
|
122
|
+
| **Attestations** | Mint, retrieve, list, and revoke cryptographic attestations |
|
|
123
|
+
| **Verification** | Online and offline verification with full verdict details |
|
|
124
|
+
| **Issuers** | Create, update, trust, and manage issuer identities |
|
|
125
|
+
| **Signing Keys** | Register, rotate, and revoke Ed25519/ECDSA signing keys |
|
|
126
|
+
| **Receipts** | Issue, retrieve, and manage structured receipt types |
|
|
127
|
+
| **Audit Trail** | Query the tamper-evident audit log for any entity |
|
|
128
|
+
| **Async Support** | Full async/await API via `AsyncTruthlockClient` |
|
|
129
|
+
| **Type Hints** | Complete type annotations for IDE autocompletion |
|
|
130
|
+
| **Auto-Retry** | Automatic retries with exponential backoff |
|
|
131
|
+
| **Pydantic Models** | Response objects are Pydantic models with validation |
|
|
132
|
+
|
|
133
|
+
## Async Support
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from truthlock import AsyncTruthlockClient
|
|
137
|
+
|
|
138
|
+
client = AsyncTruthlockClient(
|
|
139
|
+
base_url="https://api.truthlocks.com",
|
|
140
|
+
api_key="tlk_live_...",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
async def main():
|
|
144
|
+
attestation = await client.attestations.mint(...)
|
|
145
|
+
result = await client.verify.online(
|
|
146
|
+
attestation_id=attestation.attestation_id,
|
|
147
|
+
payload_b64url=payload,
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## API Resources
|
|
152
|
+
|
|
153
|
+
### Attestations
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Mint
|
|
157
|
+
att = client.attestations.mint(issuer_id=..., kid=..., alg=..., payload_b64url=...)
|
|
158
|
+
|
|
159
|
+
# Retrieve
|
|
160
|
+
att = client.attestations.get("att_abc123")
|
|
161
|
+
|
|
162
|
+
# List with pagination
|
|
163
|
+
items = client.attestations.list(limit=20, offset=0)
|
|
164
|
+
|
|
165
|
+
# Revoke
|
|
166
|
+
client.attestations.revoke("att_abc123", reason="Key compromised")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Verification
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# Online (checks revocation, expiry, and signature)
|
|
173
|
+
result = client.verify.online(attestation_id="att_...", payload_b64url="...")
|
|
174
|
+
|
|
175
|
+
# Offline (signature + payload match only)
|
|
176
|
+
result = client.verify.offline(attestation_id="att_...", payload_b64url="...")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Issuers & Keys
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# Create issuer
|
|
183
|
+
issuer = client.issuers.create(name="Acme Corp", legal_name="Acme Corp Inc.")
|
|
184
|
+
|
|
185
|
+
# Trust issuer
|
|
186
|
+
client.issuers.trust(issuer.id)
|
|
187
|
+
|
|
188
|
+
# Register key
|
|
189
|
+
client.keys.register(issuer.id, kid="primary-2026", alg="ed25519", public_key_b64url=...)
|
|
190
|
+
|
|
191
|
+
# Revoke key
|
|
192
|
+
client.keys.revoke(issuer.id, "primary-2025")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Receipts
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
# Issue
|
|
199
|
+
receipt = client.receipts.issue(
|
|
200
|
+
receipt_type="purchase",
|
|
201
|
+
issuer_id=issuer.id,
|
|
202
|
+
payload={"amount": 99.99, "currency": "USD"},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Retrieve
|
|
206
|
+
receipt = client.receipts.get(receipt.id)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Error Handling
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from truthlock.exceptions import (
|
|
213
|
+
TruthlockError,
|
|
214
|
+
AuthenticationError,
|
|
215
|
+
NotFoundError,
|
|
216
|
+
RateLimitError,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
client.attestations.get("invalid-id")
|
|
221
|
+
except AuthenticationError:
|
|
222
|
+
print("Invalid or expired API key")
|
|
223
|
+
except NotFoundError:
|
|
224
|
+
print("Attestation not found")
|
|
225
|
+
except RateLimitError as e:
|
|
226
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
227
|
+
except TruthlockError as e:
|
|
228
|
+
print(f"API error {e.status}: {e.message}")
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Configuration
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
client = TruthlockClient(
|
|
235
|
+
base_url="https://api.truthlocks.com", # required
|
|
236
|
+
api_key="tlk_live_...", # required
|
|
237
|
+
timeout=30.0, # request timeout (seconds)
|
|
238
|
+
max_retries=3, # retry on transient errors
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Django Integration
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
# settings.py
|
|
246
|
+
TRUTHLOCK_API_KEY = env("TRUTHLOCK_API_KEY")
|
|
247
|
+
TRUTHLOCK_BASE_URL = "https://api.truthlocks.com"
|
|
248
|
+
|
|
249
|
+
# views.py
|
|
250
|
+
from truthlock import TruthlockClient
|
|
251
|
+
from django.conf import settings
|
|
252
|
+
|
|
253
|
+
client = TruthlockClient(
|
|
254
|
+
base_url=settings.TRUTHLOCK_BASE_URL,
|
|
255
|
+
api_key=settings.TRUTHLOCK_API_KEY,
|
|
256
|
+
)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## FastAPI Integration
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from truthlock import AsyncTruthlockClient
|
|
263
|
+
from fastapi import FastAPI, Depends
|
|
264
|
+
|
|
265
|
+
app = FastAPI()
|
|
266
|
+
|
|
267
|
+
def get_truthlock():
|
|
268
|
+
return AsyncTruthlockClient(
|
|
269
|
+
base_url="https://api.truthlocks.com",
|
|
270
|
+
api_key=os.environ["TRUTHLOCK_API_KEY"],
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
@app.post("/attest")
|
|
274
|
+
async def create_attestation(client=Depends(get_truthlock)):
|
|
275
|
+
return await client.attestations.mint(...)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Documentation
|
|
279
|
+
|
|
280
|
+
- [SDK Guide](https://docs.truthlocks.com/sdk/python)
|
|
281
|
+
- [API Reference](https://docs.truthlocks.com/api-reference)
|
|
282
|
+
- [PyPI Package](https://pypi.org/project/truthlock/)
|
|
283
|
+
- [Examples](https://github.com/truthlocks/sdk-python/tree/main/examples)
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT -- see [LICENSE](https://github.com/truthlocks/sdk-python/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
truthlock/__init__.py,sha256=MpGnAf-3RteERA8SE4jHS3Em1cf2Y0hEVSs-mJr76Sc,645
|
|
2
|
+
truthlock/client.py,sha256=PdFjB841JA4Zpq9dmoS4o0htNVkreWrZ_LilvRklZz0,15887
|
|
3
|
+
truthlock/errors.py,sha256=hqoaH9dXIHQbSrBapTQwDKfoikw0sviBe653bgfK4HM,1150
|
|
4
|
+
truthlock/models.py,sha256=nUQ12WvckgZy7dDbY5_jzVCmio16QhiqNjefWUOfFi8,3349
|
|
5
|
+
truthlock/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
truthlock-0.3.0.dist-info/METADATA,sha256=NbbbIsQfpr5F6tJBCR9aMLPUm-ortQsYaPuf2Sgwlds,8464
|
|
7
|
+
truthlock-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
truthlock-0.3.0.dist-info/RECORD,,
|