truststate 0.2.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,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,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
+ [![PyPI version](https://img.shields.io/pypi/v/truststate.svg)](https://pypi.org/project/truststate/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/truststate.svg)](https://pypi.org/project/truststate/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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,205 @@
1
+ # TrustState Python SDK
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/truststate.svg)](https://pypi.org/project/truststate/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/truststate.svg)](https://pypi.org/project/truststate/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
+
7
+ 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.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install truststate
13
+ ```
14
+
15
+ Requires Python 3.9+.
16
+
17
+ ## Quickstart
18
+
19
+ ```python
20
+ import asyncio
21
+ from truststate import TrustStateClient
22
+
23
+ client = TrustStateClient(api_key="ts_your_api_key")
24
+
25
+ async def main():
26
+ result = await client.check(
27
+ entity_type="SukukBond",
28
+ data={
29
+ "id": "BOND-001",
30
+ "issuerId": "ISS-001",
31
+ "currency": "MYR",
32
+ "faceValue": 5_000_000,
33
+ "maturityDate": "2030-06-01",
34
+ "status": "DRAFT",
35
+ },
36
+ )
37
+
38
+ if result.passed:
39
+ print(f"✅ Passed — record ID: {result.record_id}")
40
+ else:
41
+ print(f"❌ Failed — {result.fail_reason} (step {result.failed_step})")
42
+
43
+ asyncio.run(main())
44
+ ```
45
+
46
+ ## Batch Writes
47
+
48
+ Submit multiple records in a single API call. Useful for feed-based pipelines.
49
+
50
+ ```python
51
+ result = await client.check_batch(
52
+ items=[
53
+ {"entity_type": "SukukBond", "data": {"id": "BOND-001", ...}},
54
+ {"entity_type": "SukukBond", "data": {"id": "BOND-002", ...}},
55
+ {"entity_type": "SukukBond", "data": {"id": "BOND-003", ...}},
56
+ ],
57
+ feed_label="core-banking-feed", # echoed on every item result
58
+ )
59
+
60
+ print(f"Accepted: {result.accepted}/{result.total}")
61
+ for item in result.results:
62
+ print(f" {item.entity_id}: {'✅' if item.passed else '❌'} {item.feed_label}")
63
+ ```
64
+
65
+ ## BYOP Evidence (Oracle Data)
66
+
67
+ Attach oracle evidence to compliance checks — FX rates, KYC status, credit scores, sanctions screening.
68
+
69
+ ```python
70
+ # Fetch evidence from registered oracle providers
71
+ fx = await client.fetch_fx_rate("MYR", "USD")
72
+ kyc = await client.fetch_kyc_status("actor-jasim")
73
+ score = await client.fetch_credit_score("actor-jasim")
74
+
75
+ # Submit with evidence attached
76
+ result = await client.check_with_evidence(
77
+ entity_type="SukukBond",
78
+ data={"id": "BOND-001", "issuerId": "ISS-001", "currency": "MYR", "faceValue": 5_000_000},
79
+ evidence=[fx, kyc, score],
80
+ )
81
+ ```
82
+
83
+ ## Mock Mode
84
+
85
+ Test without making any API calls. Useful for unit tests and local development.
86
+
87
+ ```python
88
+ client = TrustStateClient(
89
+ api_key="any",
90
+ mock=True,
91
+ mock_pass_rate=0.8, # 80% of checks will pass
92
+ )
93
+
94
+ result = await client.check("SukukBond", {"id": "TEST-001", ...})
95
+ print(result.mock) # True
96
+ ```
97
+
98
+ ## Django / FastAPI Middleware
99
+
100
+ Automatically validate every incoming request body against TrustState policies.
101
+
102
+ ```python
103
+ # FastAPI
104
+ from truststate import TrustStateMiddleware
105
+
106
+ app.add_middleware(
107
+ TrustStateMiddleware,
108
+ api_key="ts_your_api_key",
109
+ entity_type="AgentResponse",
110
+ )
111
+
112
+ # Django
113
+ MIDDLEWARE = [
114
+ "truststate.middleware.TrustStateMiddleware",
115
+ ...
116
+ ]
117
+ TRUSTSTATE_API_KEY = "ts_your_api_key"
118
+ TRUSTSTATE_ENTITY_TYPE = "AgentResponse"
119
+ ```
120
+
121
+ ## `@compliant` Decorator
122
+
123
+ Wrap any async function to automatically submit its return value for compliance checking.
124
+
125
+ ```python
126
+ from truststate import compliant, TrustStateClient
127
+
128
+ client = TrustStateClient(api_key="ts_your_api_key")
129
+
130
+ @compliant(client=client, entity_type="AgentResponse")
131
+ async def generate_response(prompt: str) -> dict:
132
+ return {"text": "Hello!", "score": 0.95}
133
+
134
+ result = await generate_response("What is TrustState?")
135
+ # result is a ComplianceResult — .passed, .record_id, etc.
136
+ ```
137
+
138
+ ## Configuration
139
+
140
+ | Parameter | Type | Default | Description |
141
+ |---|---|---|---|
142
+ | `api_key` | `str` | required | Your TrustState API key |
143
+ | `base_url` | `str` | production URL | Override the API base URL |
144
+ | `default_schema_version` | `str \| None` | `None` | Schema version (auto-resolved if omitted) |
145
+ | `default_actor_id` | `str` | `""` | Actor ID for the audit trail |
146
+ | `mock` | `bool` | `False` | Enable mock mode (no HTTP calls) |
147
+ | `mock_pass_rate` | `float` | `1.0` | Pass probability in mock mode (0.0–1.0) |
148
+ | `timeout` | `int` | `30` | HTTP timeout in seconds |
149
+
150
+ ## API Reference
151
+
152
+ ### `check(entity_type, data, *, action, entity_id, schema_version, actor_id)`
153
+
154
+ Submit a single record for compliance checking.
155
+
156
+ Returns: `ComplianceResult`
157
+
158
+ ### `check_batch(items, *, default_schema_version, default_actor_id, feed_label)`
159
+
160
+ Submit up to 500 records in a single call.
161
+
162
+ Returns: `BatchResult`
163
+
164
+ ### `check_with_evidence(entity_type, data, evidence, *, action, entity_id, schema_version, actor_id)`
165
+
166
+ Submit a record with oracle evidence attached.
167
+
168
+ Returns: `ComplianceResult`
169
+
170
+ ### `fetch_fx_rate(from_currency, to_currency, *, provider_id, max_age_seconds)`
171
+ ### `fetch_kyc_status(subject_id, *, provider_id, max_age_seconds)`
172
+ ### `fetch_credit_score(subject_id, *, provider_id, max_age_seconds)`
173
+ ### `fetch_sanctions(subject_id, *, provider_id, max_age_seconds)`
174
+
175
+ Fetch oracle evidence items from registered providers.
176
+
177
+ Returns: `EvidenceItem`
178
+
179
+ ### `verify(record_id, bearer_token)`
180
+
181
+ Retrieve an immutable compliance record from the ledger.
182
+
183
+ Returns: `dict`
184
+
185
+ ## ComplianceResult
186
+
187
+ | Field | Type | Description |
188
+ |---|---|---|
189
+ | `passed` | `bool` | True if all checks passed |
190
+ | `record_id` | `str \| None` | Immutable ledger record ID (only when passed) |
191
+ | `request_id` | `str` | Unique API request ID |
192
+ | `entity_id` | `str` | Entity ID that was submitted |
193
+ | `fail_reason` | `str \| None` | Human-readable failure reason |
194
+ | `failed_step` | `int \| None` | Step that failed (8=schema, 9=policy) |
195
+ | `feed_label` | `str \| None` | Feed label from batch request |
196
+ | `mock` | `bool` | True if synthesised in mock mode |
197
+
198
+ ## Requirements
199
+
200
+ - Python 3.9+
201
+ - `httpx>=0.27`
202
+
203
+ ## License
204
+
205
+ MIT © Trustchain Labs
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "truststate"
7
+ version = "0.2.0"
8
+ description = "Python SDK for TrustState compliance validation"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ keywords = ["compliance", "AI governance", "truststate", "audit"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.27",
26
+ "dataclasses-json>=0.6",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ fastapi = ["starlette>=0.27"]
31
+ dev = ["pytest>=7", "pytest-asyncio>=0.23"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://trustchainlabs.com"
35
+ Repository = "https://github.com/MyreneBot/truststate-py"
36
+ "Bug Tracker" = "https://github.com/MyreneBot/truststate-py/issues"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["."]
40
+ include = ["truststate*"]
41
+
42
+ [tool.pytest.ini_options]
43
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,181 @@
1
+ """Unit tests for TrustStateClient.
2
+
3
+ Run with: pytest tests/test_client.py -v
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import pytest
10
+ from truststate import TrustStateClient
11
+ from truststate.types import ComplianceResult, BatchResult
12
+
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Helpers
16
+ # ---------------------------------------------------------------------------
17
+
18
+ def run(coro):
19
+ """Run a coroutine in a new event loop (compatibility helper)."""
20
+ return asyncio.get_event_loop().run_until_complete(coro)
21
+
22
+
23
+ def make_mock_client(mock_pass_rate: float = 1.0) -> TrustStateClient:
24
+ return TrustStateClient(
25
+ api_key="test-key",
26
+ mock=True,
27
+ mock_pass_rate=mock_pass_rate,
28
+ )
29
+
30
+
31
+ SAMPLE_DATA = {"responseText": "Hello", "confidenceScore": 0.9}
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Tests
36
+ # ---------------------------------------------------------------------------
37
+
38
+ class TestMockMode:
39
+ """Tests that run entirely in mock mode — no network calls required."""
40
+
41
+ def test_check_passes_in_mock_mode(self):
42
+ """check() returns a passing ComplianceResult when mock_pass_rate=1.0."""
43
+ client = make_mock_client(mock_pass_rate=1.0)
44
+ result = run(client.check("AgentResponse", SAMPLE_DATA))
45
+
46
+ assert isinstance(result, ComplianceResult)
47
+ assert result.passed is True
48
+ assert result.record_id is not None
49
+ assert result.record_id.startswith("mock-rec-")
50
+ assert result.fail_reason is None
51
+ assert result.failed_step is None
52
+ assert result.mock is True
53
+
54
+ def test_check_batch_in_mock_mode(self):
55
+ """check_batch() returns a BatchResult with mock=True for all results."""
56
+ client = make_mock_client(mock_pass_rate=1.0)
57
+ items = [
58
+ {"entity_type": "Tx", "data": {"amount": 100}},
59
+ {"entity_type": "Tx", "data": {"amount": 200}},
60
+ {"entity_type": "Tx", "data": {"amount": 300}},
61
+ ]
62
+ batch = run(client.check_batch(items))
63
+
64
+ assert isinstance(batch, BatchResult)
65
+ assert batch.total == 3
66
+ assert batch.accepted == 3
67
+ assert batch.rejected == 0
68
+ assert len(batch.results) == 3
69
+ assert batch.mock is True
70
+ assert all(r.mock for r in batch.results)
71
+ assert all(r.passed for r in batch.results)
72
+
73
+ def test_mock_pass_rate_zero_always_fails(self):
74
+ """mock_pass_rate=0.0 means every check() call returns passed=False."""
75
+ client = make_mock_client(mock_pass_rate=0.0)
76
+
77
+ # Run multiple times to confirm it is deterministic at 0.0
78
+ for _ in range(10):
79
+ result = run(client.check("AgentResponse", SAMPLE_DATA))
80
+ assert result.passed is False
81
+ assert result.record_id is None
82
+ assert result.fail_reason is not None
83
+ assert result.failed_step == 9
84
+ assert result.mock is True
85
+
86
+ def test_entity_id_auto_generated(self):
87
+ """check() generates a unique entity_id when none is provided."""
88
+ client = make_mock_client()
89
+ result1 = run(client.check("AgentResponse", SAMPLE_DATA))
90
+ result2 = run(client.check("AgentResponse", SAMPLE_DATA))
91
+
92
+ # Both should have non-empty entity IDs
93
+ assert result1.entity_id
94
+ assert result2.entity_id
95
+ # They should be different (uuid4 generated)
96
+ assert result1.entity_id != result2.entity_id
97
+
98
+ def test_entity_id_preserved_when_provided(self):
99
+ """check() preserves a caller-supplied entity_id."""
100
+ client = make_mock_client()
101
+ my_id = "my-stable-entity-id-123"
102
+ result = run(client.check("AgentResponse", SAMPLE_DATA, entity_id=my_id))
103
+
104
+ assert result.entity_id == my_id
105
+
106
+ def test_batch_partial_pass(self):
107
+ """With mixed pass rate, accepted + rejected == total."""
108
+ import random
109
+ random.seed(42) # deterministic for the test
110
+ client = make_mock_client(mock_pass_rate=0.5)
111
+ items = [{"entity_type": "X", "data": {"v": i}} for i in range(20)]
112
+ batch = run(client.check_batch(items))
113
+
114
+ assert batch.total == 20
115
+ assert batch.accepted + batch.rejected == batch.total
116
+ assert len(batch.results) == 20
117
+
118
+ def test_check_batch_auto_generates_entity_ids(self):
119
+ """check_batch() auto-generates entity_id for items that omit it."""
120
+ client = make_mock_client()
121
+ items = [
122
+ {"entity_type": "T", "data": {"x": 1}}, # no entity_id
123
+ {"entity_type": "T", "data": {"x": 2}, "entity_id": "explicit-id"},
124
+ ]
125
+ batch = run(client.check_batch(items))
126
+
127
+ assert batch.results[0].entity_id # auto-generated, non-empty
128
+ assert batch.results[1].entity_id == "explicit-id"
129
+
130
+
131
+ class TestErrorHandling:
132
+ """Tests for TrustStateError and validation."""
133
+
134
+ def test_trust_state_error_attributes(self):
135
+ """TrustStateError carries message and status_code."""
136
+ from truststate.exceptions import TrustStateError
137
+ err = TrustStateError("something broke", 422)
138
+ assert err.message == "something broke"
139
+ assert err.status_code == 422
140
+ assert "422" in repr(err)
141
+
142
+ def test_compliant_decorator_raises_on_fail(self):
143
+ """@compliant with on_fail='raise' raises TrustStateError when check fails."""
144
+ from truststate import compliant
145
+ from truststate.exceptions import TrustStateError
146
+
147
+ client = make_mock_client(mock_pass_rate=0.0)
148
+
149
+ @compliant(client, entity_type="Test", on_fail="raise")
150
+ async def my_fn():
151
+ return {"value": 1}
152
+
153
+ with pytest.raises(TrustStateError):
154
+ run(my_fn())
155
+
156
+ def test_compliant_decorator_returns_none_on_fail(self):
157
+ """@compliant with on_fail='return_none' returns None when check fails."""
158
+ from truststate import compliant
159
+
160
+ client = make_mock_client(mock_pass_rate=0.0)
161
+
162
+ @compliant(client, entity_type="Test", on_fail="return_none")
163
+ async def my_fn():
164
+ return {"value": 1}
165
+
166
+ result = run(my_fn())
167
+ assert result is None
168
+
169
+ def test_compliant_passes_through_value_on_success(self):
170
+ """@compliant returns the original value when check passes."""
171
+ from truststate import compliant
172
+
173
+ client = make_mock_client(mock_pass_rate=1.0)
174
+ expected = {"responseText": "hi", "confidenceScore": 0.9}
175
+
176
+ @compliant(client, entity_type="Test", on_fail="raise")
177
+ async def my_fn():
178
+ return expected
179
+
180
+ result = run(my_fn())
181
+ assert result == expected