truststate 0.2.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.
- truststate/__init__.py +40 -0
- truststate/client.py +500 -0
- truststate/decorators.py +118 -0
- truststate/exceptions.py +18 -0
- truststate/middleware.py +127 -0
- truststate/types.py +112 -0
- truststate-0.2.0.dist-info/METADATA +235 -0
- truststate-0.2.0.dist-info/RECORD +11 -0
- truststate-0.2.0.dist-info/WHEEL +5 -0
- truststate-0.2.0.dist-info/licenses/LICENSE +21 -0
- truststate-0.2.0.dist-info/top_level.txt +1 -0
truststate/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TrustState Python SDK
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
A Python SDK for TrustState compliance validation.
|
|
6
|
+
|
|
7
|
+
Basic usage::
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from truststate import TrustStateClient
|
|
11
|
+
|
|
12
|
+
client = TrustStateClient(api_key="your-api-key")
|
|
13
|
+
|
|
14
|
+
async def main():
|
|
15
|
+
result = await client.check(
|
|
16
|
+
entity_type="AgentResponse",
|
|
17
|
+
data={"responseText": "Hello!", "confidenceScore": 0.95}
|
|
18
|
+
)
|
|
19
|
+
print(result.passed, result.record_id)
|
|
20
|
+
|
|
21
|
+
asyncio.run(main())
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .client import TrustStateClient
|
|
25
|
+
from .types import ComplianceResult, BatchResult, EvidenceItem
|
|
26
|
+
from .decorators import compliant
|
|
27
|
+
from .middleware import TrustStateMiddleware
|
|
28
|
+
from .exceptions import TrustStateError
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"TrustStateClient",
|
|
32
|
+
"ComplianceResult",
|
|
33
|
+
"BatchResult",
|
|
34
|
+
"EvidenceItem",
|
|
35
|
+
"compliant",
|
|
36
|
+
"TrustStateMiddleware",
|
|
37
|
+
"TrustStateError",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
__version__ = "0.2.0"
|
truststate/client.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""TrustStateClient — async HTTP client for the TrustState compliance API.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from truststate import TrustStateClient
|
|
6
|
+
|
|
7
|
+
client = TrustStateClient(api_key="your-key")
|
|
8
|
+
result = await client.check("AgentResponse", {"text": "...", "score": 0.9})
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import random
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from .exceptions import TrustStateError
|
|
20
|
+
from .types import BatchResult, ComplianceResult, EvidenceItem
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TrustStateClient:
|
|
24
|
+
"""Async client for the TrustState compliance validation API.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
api_key: Your TrustState API key (used as X-API-Key header for writes).
|
|
28
|
+
base_url: Override the default API base URL.
|
|
29
|
+
default_schema_version: Schema version applied when not specified per-call.
|
|
30
|
+
default_actor_id: Actor ID applied when not specified per-call.
|
|
31
|
+
mock: If True, all HTTP calls are skipped and synthetic results are returned.
|
|
32
|
+
mock_pass_rate: Probability (0.0–1.0) that a mock check returns passed=True.
|
|
33
|
+
1.0 = always pass, 0.0 = always fail, 0.8 = 80% pass rate.
|
|
34
|
+
timeout: HTTP request timeout in seconds.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
DEFAULT_BASE_URL = "https://truststate-api.apps.trustchainlabs.com"
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: str,
|
|
42
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
43
|
+
default_schema_version: Optional[str] = None,
|
|
44
|
+
default_actor_id: str = "",
|
|
45
|
+
mock: bool = False,
|
|
46
|
+
mock_pass_rate: float = 1.0,
|
|
47
|
+
timeout: int = 30,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._api_key = api_key
|
|
50
|
+
self._base_url = base_url.rstrip("/")
|
|
51
|
+
self._default_schema_version = default_schema_version
|
|
52
|
+
self._default_actor_id = default_actor_id
|
|
53
|
+
self._mock = mock
|
|
54
|
+
self._mock_pass_rate = float(mock_pass_rate)
|
|
55
|
+
self._timeout = timeout
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Public API
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
async def check(
|
|
62
|
+
self,
|
|
63
|
+
entity_type: str,
|
|
64
|
+
data: Dict[str, Any],
|
|
65
|
+
action: str = "CREATE",
|
|
66
|
+
entity_id: Optional[str] = None,
|
|
67
|
+
schema_version: Optional[str] = None,
|
|
68
|
+
actor_id: Optional[str] = None,
|
|
69
|
+
) -> ComplianceResult:
|
|
70
|
+
"""Submit a single record for compliance checking.
|
|
71
|
+
|
|
72
|
+
Internally wraps the record in a one-item batch and calls POST /v1/write/batch.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
entity_type: The type/category of the entity (e.g. "AgentResponse").
|
|
76
|
+
data: The record payload to validate.
|
|
77
|
+
action: The action being performed — "CREATE", "UPDATE", or "DELETE".
|
|
78
|
+
entity_id: Optional stable identifier for this entity. Auto-generated (uuid4)
|
|
79
|
+
if not provided.
|
|
80
|
+
schema_version: Override the client's default schema version.
|
|
81
|
+
actor_id: Override the client's default actor ID.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
ComplianceResult with pass/fail status and, if passed, a record_id.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
TrustStateError: On HTTP 4xx/5xx responses.
|
|
88
|
+
"""
|
|
89
|
+
eid = entity_id or str(uuid.uuid4())
|
|
90
|
+
|
|
91
|
+
if self._mock:
|
|
92
|
+
return self._mock_single_result(eid)
|
|
93
|
+
|
|
94
|
+
batch_result = await self.check_batch(
|
|
95
|
+
items=[
|
|
96
|
+
{
|
|
97
|
+
"entity_type": entity_type,
|
|
98
|
+
"data": data,
|
|
99
|
+
"action": action,
|
|
100
|
+
"entity_id": eid,
|
|
101
|
+
"schema_version": schema_version,
|
|
102
|
+
"actor_id": actor_id,
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
default_schema_version=schema_version,
|
|
106
|
+
default_actor_id=actor_id,
|
|
107
|
+
)
|
|
108
|
+
return batch_result.results[0]
|
|
109
|
+
|
|
110
|
+
async def check_batch(
|
|
111
|
+
self,
|
|
112
|
+
items: List[Dict[str, Any]],
|
|
113
|
+
default_schema_version: Optional[str] = None,
|
|
114
|
+
default_actor_id: Optional[str] = None,
|
|
115
|
+
feed_label: Optional[str] = None,
|
|
116
|
+
) -> BatchResult:
|
|
117
|
+
"""Submit multiple records for compliance checking in a single API call.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
items: List of item dicts. Each may contain:
|
|
121
|
+
- entity_type (required)
|
|
122
|
+
- data (required)
|
|
123
|
+
- action (optional, default "upsert")
|
|
124
|
+
- entity_id (optional, auto-generated if absent)
|
|
125
|
+
- schema_version (optional — server auto-resolves to active schema if omitted)
|
|
126
|
+
- actor_id (optional)
|
|
127
|
+
default_schema_version: Fallback schema version for items that don't specify one.
|
|
128
|
+
If None, the server auto-resolves to the active schema for each entity type.
|
|
129
|
+
default_actor_id: Fallback actor ID for items that don't specify one.
|
|
130
|
+
feed_label: Optional label identifying this feed/source (e.g. "core-banking-feed").
|
|
131
|
+
Echoed back on every item result — useful for multi-feed pipelines.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
BatchResult summarising acceptance/rejection counts and per-item results.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
TrustStateError: On HTTP 4xx/5xx responses.
|
|
138
|
+
"""
|
|
139
|
+
schema_ver = default_schema_version or self._default_schema_version
|
|
140
|
+
actor = default_actor_id or self._default_actor_id
|
|
141
|
+
|
|
142
|
+
# Normalise items and assign missing entity IDs
|
|
143
|
+
normalised = []
|
|
144
|
+
for item in items:
|
|
145
|
+
eid = item.get("entity_id") or str(uuid.uuid4())
|
|
146
|
+
entry: Dict[str, Any] = {
|
|
147
|
+
"entityType": item["entity_type"],
|
|
148
|
+
"data": item["data"],
|
|
149
|
+
"action": item.get("action", "upsert"),
|
|
150
|
+
"entityId": eid,
|
|
151
|
+
"actorId": item.get("actor_id") or actor or "sdk-writer",
|
|
152
|
+
}
|
|
153
|
+
sv = item.get("schema_version") or schema_ver
|
|
154
|
+
if sv:
|
|
155
|
+
entry["schemaVersion"] = sv
|
|
156
|
+
normalised.append(entry)
|
|
157
|
+
|
|
158
|
+
if self._mock:
|
|
159
|
+
return self._mock_batch_result(normalised, feed_label=feed_label)
|
|
160
|
+
|
|
161
|
+
payload: Dict[str, Any] = {"items": normalised}
|
|
162
|
+
if default_actor_id or self._default_actor_id:
|
|
163
|
+
payload["defaultActorId"] = default_actor_id or self._default_actor_id
|
|
164
|
+
if schema_ver:
|
|
165
|
+
payload["defaultSchemaVersion"] = schema_ver
|
|
166
|
+
if feed_label:
|
|
167
|
+
payload["feedLabel"] = feed_label
|
|
168
|
+
|
|
169
|
+
response_json = await self._post("/v1/write/batch", payload)
|
|
170
|
+
return self._parse_batch_response(response_json)
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
# BYOP Evidence fetch helpers
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async def fetch_fx_rate(
|
|
177
|
+
self,
|
|
178
|
+
from_currency: str,
|
|
179
|
+
to_currency: str,
|
|
180
|
+
provider_id: str = "reuters-fx",
|
|
181
|
+
max_age_seconds: int = 300,
|
|
182
|
+
) -> EvidenceItem:
|
|
183
|
+
"""Fetch an FX rate oracle evidence item.
|
|
184
|
+
|
|
185
|
+
In mock mode returns a deterministic stub value (MYR/USD = 4.72).
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
from_currency: Source currency code (e.g. "MYR").
|
|
189
|
+
to_currency: Target currency code (e.g. "USD").
|
|
190
|
+
provider_id: Oracle provider ID (default "reuters-fx").
|
|
191
|
+
max_age_seconds: Max acceptable age in seconds (default 300).
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
EvidenceItem ready to pass to check() or check_batch().
|
|
195
|
+
"""
|
|
196
|
+
subject = {"from": from_currency, "to": to_currency}
|
|
197
|
+
if self._mock:
|
|
198
|
+
stub_rates = {"MYR_USD": 0.2119, "USD_MYR": 4.72, "EUR_USD": 1.085, "GBP_USD": 1.267}
|
|
199
|
+
key = f"{from_currency}_{to_currency}"
|
|
200
|
+
value = stub_rates.get(key, 1.0)
|
|
201
|
+
return EvidenceItem(
|
|
202
|
+
provider_id=provider_id,
|
|
203
|
+
provider_type="fx_rate",
|
|
204
|
+
subject=subject,
|
|
205
|
+
observed_value=value,
|
|
206
|
+
observed_at=__import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat(),
|
|
207
|
+
max_age_seconds=max_age_seconds,
|
|
208
|
+
mock=True,
|
|
209
|
+
)
|
|
210
|
+
data = await self._get(f"/v1/oracle/fx-rate?from={from_currency}&to={to_currency}&providerId={provider_id}")
|
|
211
|
+
return EvidenceItem(
|
|
212
|
+
provider_id=data.get("providerId", provider_id),
|
|
213
|
+
provider_type="fx_rate",
|
|
214
|
+
subject=subject,
|
|
215
|
+
observed_value=data["observedValue"],
|
|
216
|
+
observed_at=data["observedAt"],
|
|
217
|
+
max_age_seconds=max_age_seconds,
|
|
218
|
+
proof_hash=data.get("proofHash"),
|
|
219
|
+
raw_proof_uri=data.get("rawProofUri"),
|
|
220
|
+
attestation=data.get("attestation"),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
async def fetch_kyc_status(
|
|
224
|
+
self,
|
|
225
|
+
subject_id: str,
|
|
226
|
+
provider_id: str = "sumsub-kyc",
|
|
227
|
+
max_age_seconds: int = 86400,
|
|
228
|
+
) -> EvidenceItem:
|
|
229
|
+
"""Fetch a KYC status oracle evidence item.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
subject_id: The entity/wallet/account being KYC-checked.
|
|
233
|
+
provider_id: Oracle provider ID (default "sumsub-kyc").
|
|
234
|
+
max_age_seconds: Max acceptable age (default 86400 = 24h).
|
|
235
|
+
"""
|
|
236
|
+
subject = {"id": subject_id}
|
|
237
|
+
if self._mock:
|
|
238
|
+
return EvidenceItem(
|
|
239
|
+
provider_id=provider_id,
|
|
240
|
+
provider_type="kyc_status",
|
|
241
|
+
subject=subject,
|
|
242
|
+
observed_value="PASS",
|
|
243
|
+
observed_at=__import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat(),
|
|
244
|
+
max_age_seconds=max_age_seconds,
|
|
245
|
+
mock=True,
|
|
246
|
+
)
|
|
247
|
+
data = await self._get(f"/v1/oracle/kyc-status?subjectId={subject_id}&providerId={provider_id}")
|
|
248
|
+
return EvidenceItem(
|
|
249
|
+
provider_id=data.get("providerId", provider_id),
|
|
250
|
+
provider_type="kyc_status",
|
|
251
|
+
subject=subject,
|
|
252
|
+
observed_value=data["observedValue"],
|
|
253
|
+
observed_at=data["observedAt"],
|
|
254
|
+
max_age_seconds=max_age_seconds,
|
|
255
|
+
proof_hash=data.get("proofHash"),
|
|
256
|
+
attestation=data.get("attestation"),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
async def fetch_credit_score(
|
|
260
|
+
self,
|
|
261
|
+
subject_id: str,
|
|
262
|
+
provider_id: str = "coface-credit",
|
|
263
|
+
max_age_seconds: int = 86400,
|
|
264
|
+
) -> EvidenceItem:
|
|
265
|
+
"""Fetch a credit score oracle evidence item.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
subject_id: Entity being scored.
|
|
269
|
+
provider_id: Oracle provider ID (default "coface-credit").
|
|
270
|
+
max_age_seconds: Max acceptable age (default 86400 = 24h).
|
|
271
|
+
"""
|
|
272
|
+
subject = {"id": subject_id}
|
|
273
|
+
if self._mock:
|
|
274
|
+
return EvidenceItem(
|
|
275
|
+
provider_id=provider_id,
|
|
276
|
+
provider_type="credit_score",
|
|
277
|
+
subject=subject,
|
|
278
|
+
observed_value=720,
|
|
279
|
+
observed_at=__import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat(),
|
|
280
|
+
max_age_seconds=max_age_seconds,
|
|
281
|
+
mock=True,
|
|
282
|
+
)
|
|
283
|
+
data = await self._get(f"/v1/oracle/credit-score?subjectId={subject_id}&providerId={provider_id}")
|
|
284
|
+
return EvidenceItem(
|
|
285
|
+
provider_id=data.get("providerId", provider_id),
|
|
286
|
+
provider_type="credit_score",
|
|
287
|
+
subject=subject,
|
|
288
|
+
observed_value=data["observedValue"],
|
|
289
|
+
observed_at=data["observedAt"],
|
|
290
|
+
max_age_seconds=max_age_seconds,
|
|
291
|
+
proof_hash=data.get("proofHash"),
|
|
292
|
+
attestation=data.get("attestation"),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
async def fetch_sanctions(
|
|
296
|
+
self,
|
|
297
|
+
subject_id: str,
|
|
298
|
+
provider_id: str = "refinitiv-sanct",
|
|
299
|
+
max_age_seconds: int = 3600,
|
|
300
|
+
) -> EvidenceItem:
|
|
301
|
+
"""Fetch a sanctions screening oracle evidence item."""
|
|
302
|
+
subject = {"id": subject_id}
|
|
303
|
+
if self._mock:
|
|
304
|
+
return EvidenceItem(
|
|
305
|
+
provider_id=provider_id,
|
|
306
|
+
provider_type="sanctions",
|
|
307
|
+
subject=subject,
|
|
308
|
+
observed_value="CLEAR",
|
|
309
|
+
observed_at=__import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat(),
|
|
310
|
+
max_age_seconds=max_age_seconds,
|
|
311
|
+
mock=True,
|
|
312
|
+
)
|
|
313
|
+
data = await self._get(f"/v1/oracle/sanctions?subjectId={subject_id}&providerId={provider_id}")
|
|
314
|
+
return EvidenceItem(
|
|
315
|
+
provider_id=data.get("providerId", provider_id),
|
|
316
|
+
provider_type="sanctions",
|
|
317
|
+
subject=subject,
|
|
318
|
+
observed_value=data["observedValue"],
|
|
319
|
+
observed_at=data["observedAt"],
|
|
320
|
+
max_age_seconds=max_age_seconds,
|
|
321
|
+
proof_hash=data.get("proofHash"),
|
|
322
|
+
attestation=data.get("attestation"),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
async def check_with_evidence(
|
|
326
|
+
self,
|
|
327
|
+
entity_type: str,
|
|
328
|
+
data: Dict[str, Any],
|
|
329
|
+
evidence: List[EvidenceItem],
|
|
330
|
+
action: str = "CREATE",
|
|
331
|
+
entity_id: Optional[str] = None,
|
|
332
|
+
schema_version: Optional[str] = None,
|
|
333
|
+
actor_id: Optional[str] = None,
|
|
334
|
+
) -> ComplianceResult:
|
|
335
|
+
"""Submit a compliance check with oracle evidence attached.
|
|
336
|
+
|
|
337
|
+
Convenience wrapper around check() that serialises EvidenceItem objects
|
|
338
|
+
and bundles them in the write payload.
|
|
339
|
+
|
|
340
|
+
Example::
|
|
341
|
+
|
|
342
|
+
fx = await client.fetch_fx_rate("MYR", "USD")
|
|
343
|
+
kyc = await client.fetch_kyc_status("0x1234abcd")
|
|
344
|
+
result = await client.check_with_evidence(
|
|
345
|
+
"SukukBond",
|
|
346
|
+
{"issuerId": "...", "faceValue": 500000, "currency": "MYR"},
|
|
347
|
+
evidence=[fx, kyc],
|
|
348
|
+
)
|
|
349
|
+
"""
|
|
350
|
+
eid = entity_id or str(uuid.uuid4())
|
|
351
|
+
schema_ver = schema_version or self._default_schema_version
|
|
352
|
+
actor = actor_id or self._default_actor_id
|
|
353
|
+
|
|
354
|
+
if self._mock:
|
|
355
|
+
return self._mock_single_result(eid)
|
|
356
|
+
|
|
357
|
+
item: Dict[str, Any] = {
|
|
358
|
+
"entityType": entity_type,
|
|
359
|
+
"data": data,
|
|
360
|
+
"action": action,
|
|
361
|
+
"entityId": eid,
|
|
362
|
+
"actorId": actor or "sdk-writer",
|
|
363
|
+
"evidence": [e.to_dict() for e in evidence],
|
|
364
|
+
}
|
|
365
|
+
if schema_ver:
|
|
366
|
+
item["schemaVersion"] = schema_ver
|
|
367
|
+
payload = {"items": [item]}
|
|
368
|
+
response_json = await self._post("/v1/write/batch", payload)
|
|
369
|
+
return self._parse_batch_response(response_json).results[0]
|
|
370
|
+
|
|
371
|
+
async def verify(self, record_id: str, bearer_token: str) -> Dict[str, Any]:
|
|
372
|
+
"""Retrieve an immutable compliance record from the ledger.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
record_id: The record ID returned by a previous check() that passed.
|
|
376
|
+
bearer_token: A valid Bearer token for the TrustState API.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
The full record dict from the API.
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
TrustStateError: On HTTP 4xx/5xx responses.
|
|
383
|
+
"""
|
|
384
|
+
url = f"{self._base_url}/v1/records/{record_id}"
|
|
385
|
+
headers = {"Authorization": f"Bearer {bearer_token}"}
|
|
386
|
+
|
|
387
|
+
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
|
388
|
+
try:
|
|
389
|
+
resp = await client.get(url, headers=headers)
|
|
390
|
+
except httpx.RequestError as exc:
|
|
391
|
+
raise TrustStateError(f"Network error: {exc}") from exc
|
|
392
|
+
|
|
393
|
+
if resp.status_code >= 400:
|
|
394
|
+
raise TrustStateError(
|
|
395
|
+
f"API error {resp.status_code}: {resp.text}", resp.status_code
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return resp.json()
|
|
399
|
+
|
|
400
|
+
# ------------------------------------------------------------------
|
|
401
|
+
# Internal helpers
|
|
402
|
+
# ------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
async def _get(self, path: str) -> Dict[str, Any]:
|
|
405
|
+
"""Make an authenticated GET request and return the JSON response body."""
|
|
406
|
+
url = f"{self._base_url}{path}"
|
|
407
|
+
headers = {"X-API-Key": self._api_key}
|
|
408
|
+
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
|
409
|
+
try:
|
|
410
|
+
resp = await client.get(url, headers=headers)
|
|
411
|
+
except httpx.RequestError as exc:
|
|
412
|
+
raise TrustStateError(f"Network error: {exc}") from exc
|
|
413
|
+
if resp.status_code >= 400:
|
|
414
|
+
raise TrustStateError(f"API error {resp.status_code}: {resp.text}", resp.status_code)
|
|
415
|
+
return resp.json()
|
|
416
|
+
|
|
417
|
+
async def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
418
|
+
"""Make an authenticated POST request and return the JSON response body."""
|
|
419
|
+
url = f"{self._base_url}{path}"
|
|
420
|
+
headers = {
|
|
421
|
+
"Content-Type": "application/json",
|
|
422
|
+
"X-API-Key": self._api_key,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
|
426
|
+
try:
|
|
427
|
+
resp = await client.post(url, json=payload, headers=headers)
|
|
428
|
+
except httpx.RequestError as exc:
|
|
429
|
+
raise TrustStateError(f"Network error: {exc}") from exc
|
|
430
|
+
|
|
431
|
+
if resp.status_code >= 400:
|
|
432
|
+
raise TrustStateError(
|
|
433
|
+
f"API error {resp.status_code}: {resp.text}", resp.status_code
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return resp.json()
|
|
437
|
+
|
|
438
|
+
def _parse_batch_response(self, data: Dict[str, Any]) -> BatchResult:
|
|
439
|
+
"""Convert raw API JSON into a BatchResult."""
|
|
440
|
+
raw_results = data.get("results", [])
|
|
441
|
+
results = []
|
|
442
|
+
for r in raw_results:
|
|
443
|
+
accepted = r.get("status") == "accepted"
|
|
444
|
+
results.append(
|
|
445
|
+
ComplianceResult(
|
|
446
|
+
passed=accepted,
|
|
447
|
+
record_id=r.get("recordId"),
|
|
448
|
+
request_id=r.get("requestId", ""),
|
|
449
|
+
entity_id=r.get("entityId", ""),
|
|
450
|
+
fail_reason=r.get("failReason"),
|
|
451
|
+
failed_step=r.get("failedStep"),
|
|
452
|
+
feed_label=r.get("feedLabel"),
|
|
453
|
+
mock=False,
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return BatchResult(
|
|
458
|
+
batch_id=data.get("batchId", str(uuid.uuid4())),
|
|
459
|
+
total=data.get("total", len(results)),
|
|
460
|
+
accepted=data.get("accepted", sum(1 for r in results if r.passed)),
|
|
461
|
+
rejected=data.get("rejected", sum(1 for r in results if not r.passed)),
|
|
462
|
+
results=results,
|
|
463
|
+
feed_label=data.get("feedLabel"),
|
|
464
|
+
mock=False,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# ------------------------------------------------------------------
|
|
468
|
+
# Mock helpers (no network calls)
|
|
469
|
+
# ------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
def _mock_single_result(self, entity_id: str) -> ComplianceResult:
|
|
472
|
+
"""Generate a synthetic ComplianceResult for mock mode."""
|
|
473
|
+
passed = random.random() < self._mock_pass_rate
|
|
474
|
+
return ComplianceResult(
|
|
475
|
+
passed=passed,
|
|
476
|
+
record_id=f"mock-rec-{uuid.uuid4()}" if passed else None,
|
|
477
|
+
request_id=f"mock-req-{uuid.uuid4()}",
|
|
478
|
+
entity_id=entity_id,
|
|
479
|
+
fail_reason=None if passed else "Mock: simulated policy failure",
|
|
480
|
+
failed_step=None if passed else 9,
|
|
481
|
+
mock=True,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def _mock_batch_result(self, normalised_items: List[Dict[str, Any]], feed_label: Optional[str] = None) -> BatchResult:
|
|
485
|
+
"""Generate a synthetic BatchResult for mock mode."""
|
|
486
|
+
results = [
|
|
487
|
+
self._mock_single_result(item["entityId"]) for item in normalised_items
|
|
488
|
+
]
|
|
489
|
+
for r in results:
|
|
490
|
+
r.feed_label = feed_label
|
|
491
|
+
accepted = sum(1 for r in results if r.passed)
|
|
492
|
+
return BatchResult(
|
|
493
|
+
batch_id=f"mock-batch-{uuid.uuid4()}",
|
|
494
|
+
total=len(results),
|
|
495
|
+
accepted=accepted,
|
|
496
|
+
rejected=len(results) - accepted,
|
|
497
|
+
results=results,
|
|
498
|
+
feed_label=feed_label,
|
|
499
|
+
mock=True,
|
|
500
|
+
)
|
truststate/decorators.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Decorators for automatic TrustState compliance checking.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from truststate import TrustStateClient, compliant
|
|
6
|
+
|
|
7
|
+
ts = TrustStateClient(api_key="your-key")
|
|
8
|
+
|
|
9
|
+
@compliant(ts, entity_type="AgentResponse", action="CREATE")
|
|
10
|
+
async def generate_response(customer_id: str) -> dict:
|
|
11
|
+
return {"responseText": "Hello!", "confidenceScore": 0.95}
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import functools
|
|
17
|
+
import inspect
|
|
18
|
+
import logging
|
|
19
|
+
import warnings
|
|
20
|
+
from typing import Any, Callable, Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def compliant(
|
|
26
|
+
client: Any, # TrustStateClient — avoid circular import
|
|
27
|
+
entity_type: str,
|
|
28
|
+
action: str = "CREATE",
|
|
29
|
+
data_fn: Optional[Callable[[Any], dict]] = None,
|
|
30
|
+
on_fail: str = "raise",
|
|
31
|
+
) -> Callable:
|
|
32
|
+
"""Decorator that submits the wrapped function's return value to TrustState.
|
|
33
|
+
|
|
34
|
+
The decorated function runs first; its return value is then submitted for
|
|
35
|
+
compliance validation. Depending on ``on_fail``, a failed check either raises
|
|
36
|
+
an exception, logs a warning, or returns None.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
client: A TrustStateClient instance.
|
|
40
|
+
entity_type: The entity type to register with TrustState.
|
|
41
|
+
action: The action string — "CREATE", "UPDATE", or "DELETE".
|
|
42
|
+
data_fn: Optional mapping function: ``data_fn(return_value) -> dict``.
|
|
43
|
+
If omitted, the return value must already be a dict (or have ``__dict__``).
|
|
44
|
+
on_fail: Behaviour when compliance check fails:
|
|
45
|
+
- ``"raise"`` — raise TrustStateError (default).
|
|
46
|
+
- ``"warn"`` — log a warning and return the original value anyway.
|
|
47
|
+
- ``"return_none"`` — silently return None.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Decorated async function.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If on_fail is not a recognised value.
|
|
54
|
+
TrustStateError: (when on_fail="raise") if the compliance check fails.
|
|
55
|
+
"""
|
|
56
|
+
if on_fail not in ("raise", "warn", "return_none"):
|
|
57
|
+
raise ValueError(f"on_fail must be 'raise', 'warn', or 'return_none'; got {on_fail!r}")
|
|
58
|
+
|
|
59
|
+
def decorator(func: Callable) -> Callable:
|
|
60
|
+
if not inspect.iscoroutinefunction(func):
|
|
61
|
+
raise TypeError(
|
|
62
|
+
f"@compliant requires an async function; {func.__name__} is not async."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@functools.wraps(func)
|
|
66
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
67
|
+
# Run the original function
|
|
68
|
+
result = await func(*args, **kwargs)
|
|
69
|
+
|
|
70
|
+
# Convert result to dict for submission
|
|
71
|
+
if data_fn is not None:
|
|
72
|
+
data = data_fn(result)
|
|
73
|
+
elif isinstance(result, dict):
|
|
74
|
+
data = result
|
|
75
|
+
elif hasattr(result, "__dict__"):
|
|
76
|
+
data = result.__dict__
|
|
77
|
+
else:
|
|
78
|
+
raise TypeError(
|
|
79
|
+
f"Cannot extract dict from return value of {func.__name__}. "
|
|
80
|
+
"Provide a data_fn or return a dict/dataclass."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Submit to TrustState
|
|
84
|
+
compliance = await client.check(
|
|
85
|
+
entity_type=entity_type,
|
|
86
|
+
data=data,
|
|
87
|
+
action=action,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if compliance.passed:
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
# Handle failure
|
|
94
|
+
if on_fail == "raise":
|
|
95
|
+
from .exceptions import TrustStateError
|
|
96
|
+
raise TrustStateError(
|
|
97
|
+
f"Compliance check failed for {entity_type}: "
|
|
98
|
+
f"{compliance.fail_reason} (step {compliance.failed_step})"
|
|
99
|
+
)
|
|
100
|
+
elif on_fail == "warn":
|
|
101
|
+
warnings.warn(
|
|
102
|
+
f"TrustState compliance failed for {entity_type} "
|
|
103
|
+
f"(entity_id={compliance.entity_id}): {compliance.fail_reason}",
|
|
104
|
+
stacklevel=2,
|
|
105
|
+
)
|
|
106
|
+
return result
|
|
107
|
+
else: # return_none
|
|
108
|
+
logger.debug(
|
|
109
|
+
"Compliance failed for %s entity_id=%s reason=%s — returning None",
|
|
110
|
+
entity_type,
|
|
111
|
+
compliance.entity_id,
|
|
112
|
+
compliance.fail_reason,
|
|
113
|
+
)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
return wrapper
|
|
117
|
+
|
|
118
|
+
return decorator
|
truststate/exceptions.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Custom exceptions for the TrustState SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TrustStateError(Exception):
|
|
5
|
+
"""Raised when the TrustState API returns an error response.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
message: Human-readable description of the error.
|
|
9
|
+
status_code: HTTP status code returned by the API (if applicable).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str, status_code: int = 0) -> None:
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.message = message
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return f"TrustStateError(message={self.message!r}, status_code={self.status_code})"
|
truststate/middleware.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""FastAPI middleware for automatic TrustState compliance checking.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from truststate import TrustStateClient, TrustStateMiddleware
|
|
7
|
+
|
|
8
|
+
app = FastAPI()
|
|
9
|
+
client = TrustStateClient(api_key="your-key")
|
|
10
|
+
app.add_middleware(TrustStateMiddleware, client=client)
|
|
11
|
+
|
|
12
|
+
Any request that includes the ``X-Compliance-Entity-Type`` header will have its
|
|
13
|
+
body submitted to TrustState before being passed to the route handler.
|
|
14
|
+
A failed compliance check returns HTTP 422.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
27
|
+
from starlette.requests import Request
|
|
28
|
+
from starlette.responses import JSONResponse, Response
|
|
29
|
+
from starlette.types import ASGIApp
|
|
30
|
+
|
|
31
|
+
_STARLETTE_AVAILABLE = True
|
|
32
|
+
except ImportError: # pragma: no cover
|
|
33
|
+
_STARLETTE_AVAILABLE = False
|
|
34
|
+
BaseHTTPMiddleware = object # type: ignore[misc,assignment]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TrustStateMiddleware(BaseHTTPMiddleware): # type: ignore[misc]
|
|
38
|
+
"""FastAPI/Starlette middleware that gates requests on TrustState compliance.
|
|
39
|
+
|
|
40
|
+
Inspects two custom headers on every incoming request:
|
|
41
|
+
|
|
42
|
+
- ``X-Compliance-Entity-Type``: The TrustState entity type to validate against.
|
|
43
|
+
- ``X-Compliance-Action``: The action string (default "CREATE").
|
|
44
|
+
|
|
45
|
+
If ``X-Compliance-Entity-Type`` is present, the raw request body is parsed as
|
|
46
|
+
JSON and submitted to TrustState. The request is allowed through only if the
|
|
47
|
+
compliance check passes; otherwise a 422 response is returned immediately.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
app: The ASGI application to wrap.
|
|
51
|
+
client: A TrustStateClient instance.
|
|
52
|
+
entity_id_header: Optional request header that carries a stable entity ID.
|
|
53
|
+
If absent, one is auto-generated per request.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
app: Any,
|
|
59
|
+
client: Any, # TrustStateClient — avoid circular import
|
|
60
|
+
entity_id_header: str = "X-Compliance-Entity-Id",
|
|
61
|
+
) -> None:
|
|
62
|
+
if not _STARLETTE_AVAILABLE:
|
|
63
|
+
raise ImportError(
|
|
64
|
+
"starlette is required for TrustStateMiddleware. "
|
|
65
|
+
"Install it with: pip install starlette"
|
|
66
|
+
)
|
|
67
|
+
super().__init__(app)
|
|
68
|
+
self._client = client
|
|
69
|
+
self._entity_id_header = entity_id_header
|
|
70
|
+
|
|
71
|
+
async def dispatch(self, request: Request, call_next: Any) -> Response:
|
|
72
|
+
entity_type: Optional[str] = request.headers.get("X-Compliance-Entity-Type")
|
|
73
|
+
|
|
74
|
+
# Pass through if the compliance header is absent
|
|
75
|
+
if not entity_type:
|
|
76
|
+
return await call_next(request)
|
|
77
|
+
|
|
78
|
+
action = request.headers.get("X-Compliance-Action", "CREATE")
|
|
79
|
+
entity_id = request.headers.get(self._entity_id_header)
|
|
80
|
+
|
|
81
|
+
# Read and parse the request body
|
|
82
|
+
try:
|
|
83
|
+
raw_body = await request.body()
|
|
84
|
+
data = json.loads(raw_body) if raw_body else {}
|
|
85
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
86
|
+
logger.warning("TrustStateMiddleware: failed to parse request body: %s", exc)
|
|
87
|
+
return JSONResponse(
|
|
88
|
+
{"error": "Invalid JSON body for compliance check"},
|
|
89
|
+
status_code=400,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Run compliance check
|
|
93
|
+
try:
|
|
94
|
+
result = await self._client.check(
|
|
95
|
+
entity_type=entity_type,
|
|
96
|
+
data=data,
|
|
97
|
+
action=action,
|
|
98
|
+
entity_id=entity_id or None,
|
|
99
|
+
)
|
|
100
|
+
except Exception as exc: # noqa: BLE001
|
|
101
|
+
logger.error("TrustStateMiddleware: compliance check error: %s", exc)
|
|
102
|
+
return JSONResponse(
|
|
103
|
+
{"error": "Compliance service unavailable", "detail": str(exc)},
|
|
104
|
+
status_code=503,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if not result.passed:
|
|
108
|
+
logger.info(
|
|
109
|
+
"TrustStateMiddleware: blocked request — entity_type=%s reason=%s",
|
|
110
|
+
entity_type,
|
|
111
|
+
result.fail_reason,
|
|
112
|
+
)
|
|
113
|
+
return JSONResponse(
|
|
114
|
+
{
|
|
115
|
+
"error": "Compliance check failed",
|
|
116
|
+
"fail_reason": result.fail_reason,
|
|
117
|
+
"failed_step": result.failed_step,
|
|
118
|
+
"entity_id": result.entity_id,
|
|
119
|
+
},
|
|
120
|
+
status_code=422,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Attach the record ID so downstream handlers can use it
|
|
124
|
+
response = await call_next(request)
|
|
125
|
+
if result.record_id:
|
|
126
|
+
response.headers["X-Compliance-Record-Id"] = result.record_id
|
|
127
|
+
return response
|
truststate/types.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Data types for the TrustState SDK.
|
|
2
|
+
|
|
3
|
+
This module defines the result dataclasses returned by TrustStateClient.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import uuid as _uuid
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ComplianceResult:
|
|
16
|
+
"""Result of a single compliance check.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
passed: True if the record passed all compliance checks.
|
|
20
|
+
record_id: Immutable ledger record ID — only set when passed=True.
|
|
21
|
+
request_id: Unique ID for this API request (useful for support/debugging).
|
|
22
|
+
entity_id: The entity ID that was submitted (caller-supplied or auto-generated).
|
|
23
|
+
fail_reason: Human-readable reason for failure — only set when passed=False.
|
|
24
|
+
failed_step: Numeric step that failed (8=schema validation, 9=policy check).
|
|
25
|
+
feed_label: The feed label from the batch request — useful for multi-feed pipelines.
|
|
26
|
+
mock: True when the result was synthesised locally in mock mode (no HTTP call made).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
passed: bool
|
|
30
|
+
record_id: Optional[str]
|
|
31
|
+
request_id: str
|
|
32
|
+
entity_id: str
|
|
33
|
+
fail_reason: Optional[str]
|
|
34
|
+
failed_step: Optional[int]
|
|
35
|
+
feed_label: Optional[str] = None
|
|
36
|
+
mock: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class EvidenceItem:
|
|
41
|
+
"""A single oracle evidence item to be submitted alongside a write.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
evidence_id: Unique ID for this evidence item (auto-generated if not provided).
|
|
45
|
+
provider_id: Registered oracle provider ID (e.g. "reuters-fx").
|
|
46
|
+
provider_type: Oracle provider type (e.g. "fx_rate", "kyc_status").
|
|
47
|
+
subject: Key-value dict identifying the data subject (e.g. {"from": "MYR", "to": "USD"}).
|
|
48
|
+
observed_value: The oracle-reported value (numeric or string).
|
|
49
|
+
observed_at: ISO-8601 timestamp when the value was observed by the oracle.
|
|
50
|
+
retrieved_at: ISO-8601 timestamp when you fetched this value (defaults to now).
|
|
51
|
+
max_age_seconds: Maximum acceptable age of this evidence in seconds (default 300).
|
|
52
|
+
proof_hash: Optional sha256:<hex> hash of the raw proof document.
|
|
53
|
+
raw_proof_uri: Optional URL to the raw proof document for background verification.
|
|
54
|
+
attestation: Optional dict with ``type``, ``algorithm``, ``signature`` fields.
|
|
55
|
+
mock: True when synthesised locally (no real oracle call).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
provider_id: str
|
|
59
|
+
provider_type: str
|
|
60
|
+
subject: Dict[str, Any]
|
|
61
|
+
observed_value: Any
|
|
62
|
+
observed_at: str
|
|
63
|
+
evidence_id: str = field(default_factory=lambda: str(_uuid.uuid4()))
|
|
64
|
+
retrieved_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
65
|
+
max_age_seconds: int = 300
|
|
66
|
+
proof_hash: Optional[str] = None
|
|
67
|
+
raw_proof_uri: Optional[str] = None
|
|
68
|
+
attestation: Optional[Dict[str, Any]] = None
|
|
69
|
+
mock: bool = False
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
72
|
+
"""Serialise to the wire format expected by the TrustState API."""
|
|
73
|
+
d: Dict[str, Any] = {
|
|
74
|
+
"evidenceId": self.evidence_id,
|
|
75
|
+
"providerId": self.provider_id,
|
|
76
|
+
"providerType": self.provider_type,
|
|
77
|
+
"subject": self.subject,
|
|
78
|
+
"observedValue": self.observed_value,
|
|
79
|
+
"observedAt": self.observed_at,
|
|
80
|
+
"retrievedAt": self.retrieved_at,
|
|
81
|
+
"maxAgeSeconds": self.max_age_seconds,
|
|
82
|
+
}
|
|
83
|
+
if self.proof_hash:
|
|
84
|
+
d["proofHash"] = self.proof_hash
|
|
85
|
+
if self.raw_proof_uri:
|
|
86
|
+
d["rawProofUri"] = self.raw_proof_uri
|
|
87
|
+
if self.attestation:
|
|
88
|
+
d["attestation"] = self.attestation
|
|
89
|
+
return d
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class BatchResult:
|
|
94
|
+
"""Aggregated result for a batch compliance submission.
|
|
95
|
+
|
|
96
|
+
Attributes:
|
|
97
|
+
batch_id: Unique ID for this batch request.
|
|
98
|
+
total: Total number of items submitted.
|
|
99
|
+
accepted: Number of items that passed compliance checks.
|
|
100
|
+
rejected: Number of items that failed compliance checks.
|
|
101
|
+
results: Per-item ComplianceResult list (same order as submitted items).
|
|
102
|
+
feed_label: The feed label for this batch (echoed from request).
|
|
103
|
+
mock: True when running in mock mode (no HTTP calls made).
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
batch_id: str
|
|
107
|
+
total: int
|
|
108
|
+
accepted: int
|
|
109
|
+
rejected: int
|
|
110
|
+
results: List[ComplianceResult]
|
|
111
|
+
feed_label: Optional[str] = None
|
|
112
|
+
mock: bool = False
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: truststate
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for TrustState compliance validation
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://trustchainlabs.com
|
|
7
|
+
Project-URL: Repository, https://github.com/MyreneBot/truststate-py
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/MyreneBot/truststate-py/issues
|
|
9
|
+
Keywords: compliance,AI governance,truststate,audit
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: dataclasses-json>=0.6
|
|
24
|
+
Provides-Extra: fastapi
|
|
25
|
+
Requires-Dist: starlette>=0.27; extra == "fastapi"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# TrustState Python SDK
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/truststate/)
|
|
34
|
+
[](https://pypi.org/project/truststate/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
Python SDK for the [TrustState](https://truststate.apps.trustchainlabs.com) compliance API — validate, audit, and enforce compliance rules on any entity or data record. Built for financial services, AI governance, and regulated industries.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install truststate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.9+.
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import asyncio
|
|
51
|
+
from truststate import TrustStateClient
|
|
52
|
+
|
|
53
|
+
client = TrustStateClient(api_key="ts_your_api_key")
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
result = await client.check(
|
|
57
|
+
entity_type="SukukBond",
|
|
58
|
+
data={
|
|
59
|
+
"id": "BOND-001",
|
|
60
|
+
"issuerId": "ISS-001",
|
|
61
|
+
"currency": "MYR",
|
|
62
|
+
"faceValue": 5_000_000,
|
|
63
|
+
"maturityDate": "2030-06-01",
|
|
64
|
+
"status": "DRAFT",
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if result.passed:
|
|
69
|
+
print(f"✅ Passed — record ID: {result.record_id}")
|
|
70
|
+
else:
|
|
71
|
+
print(f"❌ Failed — {result.fail_reason} (step {result.failed_step})")
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Batch Writes
|
|
77
|
+
|
|
78
|
+
Submit multiple records in a single API call. Useful for feed-based pipelines.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
result = await client.check_batch(
|
|
82
|
+
items=[
|
|
83
|
+
{"entity_type": "SukukBond", "data": {"id": "BOND-001", ...}},
|
|
84
|
+
{"entity_type": "SukukBond", "data": {"id": "BOND-002", ...}},
|
|
85
|
+
{"entity_type": "SukukBond", "data": {"id": "BOND-003", ...}},
|
|
86
|
+
],
|
|
87
|
+
feed_label="core-banking-feed", # echoed on every item result
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
print(f"Accepted: {result.accepted}/{result.total}")
|
|
91
|
+
for item in result.results:
|
|
92
|
+
print(f" {item.entity_id}: {'✅' if item.passed else '❌'} {item.feed_label}")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## BYOP Evidence (Oracle Data)
|
|
96
|
+
|
|
97
|
+
Attach oracle evidence to compliance checks — FX rates, KYC status, credit scores, sanctions screening.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# Fetch evidence from registered oracle providers
|
|
101
|
+
fx = await client.fetch_fx_rate("MYR", "USD")
|
|
102
|
+
kyc = await client.fetch_kyc_status("actor-jasim")
|
|
103
|
+
score = await client.fetch_credit_score("actor-jasim")
|
|
104
|
+
|
|
105
|
+
# Submit with evidence attached
|
|
106
|
+
result = await client.check_with_evidence(
|
|
107
|
+
entity_type="SukukBond",
|
|
108
|
+
data={"id": "BOND-001", "issuerId": "ISS-001", "currency": "MYR", "faceValue": 5_000_000},
|
|
109
|
+
evidence=[fx, kyc, score],
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Mock Mode
|
|
114
|
+
|
|
115
|
+
Test without making any API calls. Useful for unit tests and local development.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
client = TrustStateClient(
|
|
119
|
+
api_key="any",
|
|
120
|
+
mock=True,
|
|
121
|
+
mock_pass_rate=0.8, # 80% of checks will pass
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
result = await client.check("SukukBond", {"id": "TEST-001", ...})
|
|
125
|
+
print(result.mock) # True
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Django / FastAPI Middleware
|
|
129
|
+
|
|
130
|
+
Automatically validate every incoming request body against TrustState policies.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# FastAPI
|
|
134
|
+
from truststate import TrustStateMiddleware
|
|
135
|
+
|
|
136
|
+
app.add_middleware(
|
|
137
|
+
TrustStateMiddleware,
|
|
138
|
+
api_key="ts_your_api_key",
|
|
139
|
+
entity_type="AgentResponse",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Django
|
|
143
|
+
MIDDLEWARE = [
|
|
144
|
+
"truststate.middleware.TrustStateMiddleware",
|
|
145
|
+
...
|
|
146
|
+
]
|
|
147
|
+
TRUSTSTATE_API_KEY = "ts_your_api_key"
|
|
148
|
+
TRUSTSTATE_ENTITY_TYPE = "AgentResponse"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## `@compliant` Decorator
|
|
152
|
+
|
|
153
|
+
Wrap any async function to automatically submit its return value for compliance checking.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from truststate import compliant, TrustStateClient
|
|
157
|
+
|
|
158
|
+
client = TrustStateClient(api_key="ts_your_api_key")
|
|
159
|
+
|
|
160
|
+
@compliant(client=client, entity_type="AgentResponse")
|
|
161
|
+
async def generate_response(prompt: str) -> dict:
|
|
162
|
+
return {"text": "Hello!", "score": 0.95}
|
|
163
|
+
|
|
164
|
+
result = await generate_response("What is TrustState?")
|
|
165
|
+
# result is a ComplianceResult — .passed, .record_id, etc.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
| Parameter | Type | Default | Description |
|
|
171
|
+
|---|---|---|---|
|
|
172
|
+
| `api_key` | `str` | required | Your TrustState API key |
|
|
173
|
+
| `base_url` | `str` | production URL | Override the API base URL |
|
|
174
|
+
| `default_schema_version` | `str \| None` | `None` | Schema version (auto-resolved if omitted) |
|
|
175
|
+
| `default_actor_id` | `str` | `""` | Actor ID for the audit trail |
|
|
176
|
+
| `mock` | `bool` | `False` | Enable mock mode (no HTTP calls) |
|
|
177
|
+
| `mock_pass_rate` | `float` | `1.0` | Pass probability in mock mode (0.0–1.0) |
|
|
178
|
+
| `timeout` | `int` | `30` | HTTP timeout in seconds |
|
|
179
|
+
|
|
180
|
+
## API Reference
|
|
181
|
+
|
|
182
|
+
### `check(entity_type, data, *, action, entity_id, schema_version, actor_id)`
|
|
183
|
+
|
|
184
|
+
Submit a single record for compliance checking.
|
|
185
|
+
|
|
186
|
+
Returns: `ComplianceResult`
|
|
187
|
+
|
|
188
|
+
### `check_batch(items, *, default_schema_version, default_actor_id, feed_label)`
|
|
189
|
+
|
|
190
|
+
Submit up to 500 records in a single call.
|
|
191
|
+
|
|
192
|
+
Returns: `BatchResult`
|
|
193
|
+
|
|
194
|
+
### `check_with_evidence(entity_type, data, evidence, *, action, entity_id, schema_version, actor_id)`
|
|
195
|
+
|
|
196
|
+
Submit a record with oracle evidence attached.
|
|
197
|
+
|
|
198
|
+
Returns: `ComplianceResult`
|
|
199
|
+
|
|
200
|
+
### `fetch_fx_rate(from_currency, to_currency, *, provider_id, max_age_seconds)`
|
|
201
|
+
### `fetch_kyc_status(subject_id, *, provider_id, max_age_seconds)`
|
|
202
|
+
### `fetch_credit_score(subject_id, *, provider_id, max_age_seconds)`
|
|
203
|
+
### `fetch_sanctions(subject_id, *, provider_id, max_age_seconds)`
|
|
204
|
+
|
|
205
|
+
Fetch oracle evidence items from registered providers.
|
|
206
|
+
|
|
207
|
+
Returns: `EvidenceItem`
|
|
208
|
+
|
|
209
|
+
### `verify(record_id, bearer_token)`
|
|
210
|
+
|
|
211
|
+
Retrieve an immutable compliance record from the ledger.
|
|
212
|
+
|
|
213
|
+
Returns: `dict`
|
|
214
|
+
|
|
215
|
+
## ComplianceResult
|
|
216
|
+
|
|
217
|
+
| Field | Type | Description |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `passed` | `bool` | True if all checks passed |
|
|
220
|
+
| `record_id` | `str \| None` | Immutable ledger record ID (only when passed) |
|
|
221
|
+
| `request_id` | `str` | Unique API request ID |
|
|
222
|
+
| `entity_id` | `str` | Entity ID that was submitted |
|
|
223
|
+
| `fail_reason` | `str \| None` | Human-readable failure reason |
|
|
224
|
+
| `failed_step` | `int \| None` | Step that failed (8=schema, 9=policy) |
|
|
225
|
+
| `feed_label` | `str \| None` | Feed label from batch request |
|
|
226
|
+
| `mock` | `bool` | True if synthesised in mock mode |
|
|
227
|
+
|
|
228
|
+
## Requirements
|
|
229
|
+
|
|
230
|
+
- Python 3.9+
|
|
231
|
+
- `httpx>=0.27`
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT © Trustchain Labs
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
truststate/__init__.py,sha256=RkDdM4_OqTgHIZVmMj1mOF7eCuJ8xtf7jJf8WGSa25c,903
|
|
2
|
+
truststate/client.py,sha256=aq_KXiXivXFpeYcWZFo5ZxlAtMVrdDvK5ydoHXnJVQU,19577
|
|
3
|
+
truststate/decorators.py,sha256=oWImPCKN64X8bGs-ABXrn2hFdQHOgWzNpKudNesqOTs,4214
|
|
4
|
+
truststate/exceptions.py,sha256=gm27qY_kq3vbULR-7ZxP2anqC-DO8nEItcOM2g1kauU,604
|
|
5
|
+
truststate/middleware.py,sha256=Zv2aHgJY8oIXLWhfN-AK9GTboHD_IU2gQeGgSf8_mvI,4607
|
|
6
|
+
truststate/types.py,sha256=buX2PI9C-DfWLwc5DM8ja64ebiE21A6hb4NjN5R_MJg,4398
|
|
7
|
+
truststate-0.2.0.dist-info/licenses/LICENSE,sha256=3kMyI1A8h3bMejvRDgRx58bQT1KauBLKEnqxXMZX-tk,1066
|
|
8
|
+
truststate-0.2.0.dist-info/METADATA,sha256=f25K45vkgtvcJibDD5BXnRbQVGqU0Bp-6p1eWC8Pv6k,7190
|
|
9
|
+
truststate-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
truststate-0.2.0.dist-info/top_level.txt,sha256=YNcW7zAANI3PMnRsT4IBqd-Z0ZDlkcSCki0hnUCLSeM,11
|
|
11
|
+
truststate-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MyreneBot
|
|
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 @@
|
|
|
1
|
+
truststate
|