truthlock 0.3.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,48 @@
1
+ node_modules
2
+ .DS_Store
3
+ dist
4
+ .env
5
+ .env.*
6
+ .env.local
7
+ .env.development
8
+ .env.production
9
+ .next
10
+ .turbo
11
+ coverage
12
+ # Go binaries
13
+ main
14
+ server
15
+ *.exe
16
+ !**/cmd/**/server/
17
+ !**/internal/**/server/
18
+ !**/lib/**/server/
19
+ bin/
20
+
21
+ # Build artifacts
22
+ *.zip
23
+
24
+ # Local tooling configs that may contain secrets
25
+ .claude/settings.local.json
26
+
27
+ # Temporary files
28
+ tmp/
29
+ *.log
30
+
31
+ # Local CloudWatch Insights dumps (do not commit)
32
+ .cursor-cw-*.json
33
+
34
+ # Audit artifacts
35
+ audit-screenshots/
36
+ audit-browser-results.json
37
+ scripts/audit-*.cjs
38
+ scripts/audit-*.mjs
39
+ scripts/audit-*.ps1
40
+ tmp_login.json
41
+
42
+ # Playwright prod report
43
+ playwright-report-prod/
44
+
45
+ # mTLS certificates (generated, contain private keys)
46
+ infra/mtls/certs/
47
+ .gitnexus
48
+ tools/gitnexus/
@@ -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> &bull;
53
+ <a href="https://docs.truthlocks.com/api-reference">API Reference</a> &bull;
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,256 @@
1
+ <p align="center">
2
+ <a href="https://truthlocks.com">
3
+ <img src="https://www.truthlocks.com/logo/logo-color-1.png" alt="Truthlocks" width="200" />
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">Truthlock Python SDK</h1>
8
+
9
+ <p align="center">
10
+ <strong>Official Python SDK for the Truthlocks Platform</strong>
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://pypi.org/project/truthlock/"><img src="https://img.shields.io/pypi/v/truthlock.svg?style=flat-square" alt="PyPI version" /></a>
15
+ <a href="https://pypi.org/project/truthlock/"><img src="https://img.shields.io/pypi/dm/truthlock.svg?style=flat-square" alt="PyPI downloads" /></a>
16
+ <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>
17
+ <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>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="https://docs.truthlocks.com/sdk/python">Documentation</a> &bull;
22
+ <a href="https://docs.truthlocks.com/api-reference">API Reference</a> &bull;
23
+ <a href="https://github.com/truthlocks/sdk-python/issues">Issues</a>
24
+ </p>
25
+
26
+ ---
27
+
28
+ 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.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install truthlock
34
+ ```
35
+
36
+ Requires **Python 3.10** or later.
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ from truthlock import TruthlockClient
42
+
43
+ client = TruthlockClient(
44
+ base_url="https://api.truthlocks.com",
45
+ api_key="tlk_live_...",
46
+ )
47
+
48
+ # Create an issuer
49
+ issuer = client.issuers.create(
50
+ name="My Organization",
51
+ legal_name="My Organization Inc.",
52
+ display_name="My Org",
53
+ )
54
+ client.issuers.trust(issuer.id)
55
+
56
+ # Register a signing key
57
+ client.keys.register(
58
+ issuer_id=issuer.id,
59
+ kid="key-1",
60
+ alg="ed25519",
61
+ public_key_b64url="your-public-key",
62
+ )
63
+
64
+ # Mint an attestation
65
+ import base64
66
+ payload = base64.urlsafe_b64encode(b"Hello World").rstrip(b"=").decode()
67
+
68
+ attestation = client.attestations.mint(
69
+ issuer_id=issuer.id,
70
+ kid="key-1",
71
+ alg="ed25519",
72
+ payload_b64url=payload,
73
+ )
74
+
75
+ print(f"Attestation ID: {attestation.attestation_id}")
76
+
77
+ # Verify
78
+ result = client.verify.online(
79
+ attestation_id=attestation.attestation_id,
80
+ payload_b64url=payload,
81
+ )
82
+
83
+ if result.verdict == "VALID":
84
+ print("Document verified successfully")
85
+ ```
86
+
87
+ ## Features
88
+
89
+ | Feature | Description |
90
+ | ------------------- | ----------------------------------------------------------- |
91
+ | **Attestations** | Mint, retrieve, list, and revoke cryptographic attestations |
92
+ | **Verification** | Online and offline verification with full verdict details |
93
+ | **Issuers** | Create, update, trust, and manage issuer identities |
94
+ | **Signing Keys** | Register, rotate, and revoke Ed25519/ECDSA signing keys |
95
+ | **Receipts** | Issue, retrieve, and manage structured receipt types |
96
+ | **Audit Trail** | Query the tamper-evident audit log for any entity |
97
+ | **Async Support** | Full async/await API via `AsyncTruthlockClient` |
98
+ | **Type Hints** | Complete type annotations for IDE autocompletion |
99
+ | **Auto-Retry** | Automatic retries with exponential backoff |
100
+ | **Pydantic Models** | Response objects are Pydantic models with validation |
101
+
102
+ ## Async Support
103
+
104
+ ```python
105
+ from truthlock import AsyncTruthlockClient
106
+
107
+ client = AsyncTruthlockClient(
108
+ base_url="https://api.truthlocks.com",
109
+ api_key="tlk_live_...",
110
+ )
111
+
112
+ async def main():
113
+ attestation = await client.attestations.mint(...)
114
+ result = await client.verify.online(
115
+ attestation_id=attestation.attestation_id,
116
+ payload_b64url=payload,
117
+ )
118
+ ```
119
+
120
+ ## API Resources
121
+
122
+ ### Attestations
123
+
124
+ ```python
125
+ # Mint
126
+ att = client.attestations.mint(issuer_id=..., kid=..., alg=..., payload_b64url=...)
127
+
128
+ # Retrieve
129
+ att = client.attestations.get("att_abc123")
130
+
131
+ # List with pagination
132
+ items = client.attestations.list(limit=20, offset=0)
133
+
134
+ # Revoke
135
+ client.attestations.revoke("att_abc123", reason="Key compromised")
136
+ ```
137
+
138
+ ### Verification
139
+
140
+ ```python
141
+ # Online (checks revocation, expiry, and signature)
142
+ result = client.verify.online(attestation_id="att_...", payload_b64url="...")
143
+
144
+ # Offline (signature + payload match only)
145
+ result = client.verify.offline(attestation_id="att_...", payload_b64url="...")
146
+ ```
147
+
148
+ ### Issuers & Keys
149
+
150
+ ```python
151
+ # Create issuer
152
+ issuer = client.issuers.create(name="Acme Corp", legal_name="Acme Corp Inc.")
153
+
154
+ # Trust issuer
155
+ client.issuers.trust(issuer.id)
156
+
157
+ # Register key
158
+ client.keys.register(issuer.id, kid="primary-2026", alg="ed25519", public_key_b64url=...)
159
+
160
+ # Revoke key
161
+ client.keys.revoke(issuer.id, "primary-2025")
162
+ ```
163
+
164
+ ### Receipts
165
+
166
+ ```python
167
+ # Issue
168
+ receipt = client.receipts.issue(
169
+ receipt_type="purchase",
170
+ issuer_id=issuer.id,
171
+ payload={"amount": 99.99, "currency": "USD"},
172
+ )
173
+
174
+ # Retrieve
175
+ receipt = client.receipts.get(receipt.id)
176
+ ```
177
+
178
+ ## Error Handling
179
+
180
+ ```python
181
+ from truthlock.exceptions import (
182
+ TruthlockError,
183
+ AuthenticationError,
184
+ NotFoundError,
185
+ RateLimitError,
186
+ )
187
+
188
+ try:
189
+ client.attestations.get("invalid-id")
190
+ except AuthenticationError:
191
+ print("Invalid or expired API key")
192
+ except NotFoundError:
193
+ print("Attestation not found")
194
+ except RateLimitError as e:
195
+ print(f"Rate limited. Retry after {e.retry_after}s")
196
+ except TruthlockError as e:
197
+ print(f"API error {e.status}: {e.message}")
198
+ ```
199
+
200
+ ## Configuration
201
+
202
+ ```python
203
+ client = TruthlockClient(
204
+ base_url="https://api.truthlocks.com", # required
205
+ api_key="tlk_live_...", # required
206
+ timeout=30.0, # request timeout (seconds)
207
+ max_retries=3, # retry on transient errors
208
+ )
209
+ ```
210
+
211
+ ## Django Integration
212
+
213
+ ```python
214
+ # settings.py
215
+ TRUTHLOCK_API_KEY = env("TRUTHLOCK_API_KEY")
216
+ TRUTHLOCK_BASE_URL = "https://api.truthlocks.com"
217
+
218
+ # views.py
219
+ from truthlock import TruthlockClient
220
+ from django.conf import settings
221
+
222
+ client = TruthlockClient(
223
+ base_url=settings.TRUTHLOCK_BASE_URL,
224
+ api_key=settings.TRUTHLOCK_API_KEY,
225
+ )
226
+ ```
227
+
228
+ ## FastAPI Integration
229
+
230
+ ```python
231
+ from truthlock import AsyncTruthlockClient
232
+ from fastapi import FastAPI, Depends
233
+
234
+ app = FastAPI()
235
+
236
+ def get_truthlock():
237
+ return AsyncTruthlockClient(
238
+ base_url="https://api.truthlocks.com",
239
+ api_key=os.environ["TRUTHLOCK_API_KEY"],
240
+ )
241
+
242
+ @app.post("/attest")
243
+ async def create_attestation(client=Depends(get_truthlock)):
244
+ return await client.attestations.mint(...)
245
+ ```
246
+
247
+ ## Documentation
248
+
249
+ - [SDK Guide](https://docs.truthlocks.com/sdk/python)
250
+ - [API Reference](https://docs.truthlocks.com/api-reference)
251
+ - [PyPI Package](https://pypi.org/project/truthlock/)
252
+ - [Examples](https://github.com/truthlocks/sdk-python/tree/main/examples)
253
+
254
+ ## License
255
+
256
+ MIT -- see [LICENSE](https://github.com/truthlocks/sdk-python/blob/main/LICENSE) for details.
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "truthlock"
7
+ version = "0.3.0"
8
+ description = "Official Python SDK for the Truthlocks verification platform"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Truthlocks", email = "support@truthlocks.com" },
14
+ ]
15
+ keywords = [
16
+ "truthlock",
17
+ "verification",
18
+ "attestation",
19
+ "cryptographic-proof",
20
+ "trust",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Topic :: Security :: Cryptography",
33
+ "Topic :: Software Development :: Libraries :: Python Modules",
34
+ "Typing :: Typed",
35
+ ]
36
+ dependencies = [
37
+ "httpx>=0.25.0",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "pytest>=7.0",
43
+ "pytest-asyncio>=0.21",
44
+ "ruff>=0.1",
45
+ ]
46
+
47
+ [project.urls]
48
+ Homepage = "https://truthlocks.com"
49
+ Documentation = "https://docs.truthlocks.com/sdk/python"
50
+ Repository = "https://github.com/truthlocks/sdk-python"
51
+ Issues = "https://github.com/truthlocks/sdk-python/issues"
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/truthlock"]
55
+
56
+ [tool.ruff]
57
+ target-version = "py39"
58
+ line-length = 100
@@ -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
+ ]
@@ -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
+ )
@@ -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)."""
@@ -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
File without changes
File without changes
@@ -0,0 +1,20 @@
1
+ """Basic tests for TruthlockClient initialization."""
2
+
3
+ from truthlock import TruthlockClient, Algorithm, Verdict
4
+
5
+
6
+ def test_client_init():
7
+ client = TruthlockClient(api_key="test-key", base_url="https://api.example.com")
8
+ assert client._api_key == "test-key"
9
+ assert client._base_url == "https://api.example.com"
10
+ client.close()
11
+
12
+
13
+ def test_client_context_manager():
14
+ with TruthlockClient(api_key="test") as client:
15
+ assert client is not None
16
+
17
+
18
+ def test_enums():
19
+ assert Algorithm.ED25519.value == "Ed25519"
20
+ assert Verdict.VALID.value == "valid"