agentscore-py 2.3.4__tar.gz → 2.4.1__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.
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/CONTRIBUTING.md +1 -1
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/PKG-INFO +1 -1
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/SECURITY.md +2 -2
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/__init__.py +44 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/client.py +4 -2
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/errors.py +2 -6
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/types.py +9 -5
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/pyproject.toml +1 -1
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_client.py +277 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_errors.py +6 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/uv.lock +20 -20
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.claude/CLAUDE.md +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/dependabot.yml +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/workflows/ci.yml +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/workflows/publish.yml +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/workflows/security.yml +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.gitignore +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/CODE_OF_CONDUCT.md +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/LICENSE +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/README.md +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/py.typed +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/test_mode.py +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/lefthook.yml +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/ruff.toml +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/__init__.py +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/conftest.py +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_integration.py +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_test_mode.py +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_types.py +0 -0
- {agentscore_py-2.3.4 → agentscore_py-2.4.1}/vulture_whitelist.py +0 -0
|
@@ -15,7 +15,7 @@ Thanks for your interest in contributing! Here's how to get started.
|
|
|
15
15
|
|
|
16
16
|
- All PRs require 1 approval before merging
|
|
17
17
|
- Squash merge to `main` is the standard
|
|
18
|
-
- Keep PRs focused
|
|
18
|
+
- Keep PRs focused: one feature or fix per PR
|
|
19
19
|
- Include tests for new functionality
|
|
20
20
|
- Make sure CI passes before requesting review
|
|
21
21
|
|
|
@@ -13,10 +13,16 @@ from agentscore.errors import (
|
|
|
13
13
|
from agentscore.test_mode import AGENTSCORE_TEST_ADDRESSES, is_agentscore_test_address
|
|
14
14
|
from agentscore.types import (
|
|
15
15
|
AccountVerification,
|
|
16
|
+
Activity,
|
|
16
17
|
AgentMemoryHint,
|
|
17
18
|
AgentMemoryIdentityPaths,
|
|
19
|
+
AgentSummary,
|
|
18
20
|
AssessResponse,
|
|
19
21
|
AssociateWalletResponse,
|
|
22
|
+
ChainEntry,
|
|
23
|
+
ChainScore,
|
|
24
|
+
Classification,
|
|
25
|
+
CredentialCreateErrorNextSteps,
|
|
20
26
|
CredentialCreateErrorResponse,
|
|
21
27
|
CredentialCreateResponse,
|
|
22
28
|
CredentialItem,
|
|
@@ -25,23 +31,39 @@ from agentscore.types import (
|
|
|
25
31
|
DecisionPolicy,
|
|
26
32
|
DenialCode,
|
|
27
33
|
EntityType,
|
|
34
|
+
EvidenceSummary,
|
|
28
35
|
Grade,
|
|
36
|
+
Identity,
|
|
29
37
|
Network,
|
|
30
38
|
NextStepsAction,
|
|
39
|
+
OperatorScore,
|
|
31
40
|
OperatorVerification,
|
|
41
|
+
PolicyCheck,
|
|
42
|
+
PolicyExplanation,
|
|
43
|
+
PolicyResult,
|
|
32
44
|
QuotaInfo,
|
|
45
|
+
RedactedClassification,
|
|
33
46
|
Reputation,
|
|
34
47
|
ReputationResponse,
|
|
35
48
|
ReputationStatus,
|
|
49
|
+
Score,
|
|
50
|
+
SessionCreateNextSteps,
|
|
36
51
|
SessionCreateRequest,
|
|
37
52
|
SessionCreateResponse,
|
|
53
|
+
SessionPollNextSteps,
|
|
38
54
|
SessionPollResponse,
|
|
39
55
|
Signer,
|
|
40
56
|
SignerMatch,
|
|
41
57
|
SignerSanctions,
|
|
58
|
+
SignerSanctionsClear,
|
|
59
|
+
SignerSanctionsHit,
|
|
60
|
+
SignerSanctionsUnavailable,
|
|
61
|
+
Subject,
|
|
42
62
|
VerificationLevel,
|
|
43
63
|
WalletAuthRequiresSigningBody,
|
|
64
|
+
WalletAuthRequiresSigningNextSteps,
|
|
44
65
|
WalletSignerMismatchBody,
|
|
66
|
+
WalletSignerMismatchNextSteps,
|
|
45
67
|
)
|
|
46
68
|
|
|
47
69
|
__version__ = _pkg_version("agentscore-py")
|
|
@@ -49,12 +71,18 @@ __version__ = _pkg_version("agentscore-py")
|
|
|
49
71
|
__all__ = [
|
|
50
72
|
"AGENTSCORE_TEST_ADDRESSES",
|
|
51
73
|
"AccountVerification",
|
|
74
|
+
"Activity",
|
|
52
75
|
"AgentMemoryHint",
|
|
53
76
|
"AgentMemoryIdentityPaths",
|
|
54
77
|
"AgentScore",
|
|
55
78
|
"AgentScoreError",
|
|
79
|
+
"AgentSummary",
|
|
56
80
|
"AssessResponse",
|
|
57
81
|
"AssociateWalletResponse",
|
|
82
|
+
"ChainEntry",
|
|
83
|
+
"ChainScore",
|
|
84
|
+
"Classification",
|
|
85
|
+
"CredentialCreateErrorNextSteps",
|
|
58
86
|
"CredentialCreateErrorResponse",
|
|
59
87
|
"CredentialCreateResponse",
|
|
60
88
|
"CredentialItem",
|
|
@@ -63,29 +91,45 @@ __all__ = [
|
|
|
63
91
|
"DecisionPolicy",
|
|
64
92
|
"DenialCode",
|
|
65
93
|
"EntityType",
|
|
94
|
+
"EvidenceSummary",
|
|
66
95
|
"Grade",
|
|
96
|
+
"Identity",
|
|
67
97
|
"InvalidCredentialError",
|
|
68
98
|
"Network",
|
|
69
99
|
"NextStepsAction",
|
|
100
|
+
"OperatorScore",
|
|
70
101
|
"OperatorVerification",
|
|
71
102
|
"PaymentRequiredError",
|
|
103
|
+
"PolicyCheck",
|
|
104
|
+
"PolicyExplanation",
|
|
105
|
+
"PolicyResult",
|
|
72
106
|
"QuotaExceededError",
|
|
73
107
|
"QuotaInfo",
|
|
74
108
|
"RateLimitedError",
|
|
109
|
+
"RedactedClassification",
|
|
75
110
|
"Reputation",
|
|
76
111
|
"ReputationResponse",
|
|
77
112
|
"ReputationStatus",
|
|
113
|
+
"Score",
|
|
114
|
+
"SessionCreateNextSteps",
|
|
78
115
|
"SessionCreateRequest",
|
|
79
116
|
"SessionCreateResponse",
|
|
117
|
+
"SessionPollNextSteps",
|
|
80
118
|
"SessionPollResponse",
|
|
81
119
|
"Signer",
|
|
82
120
|
"SignerMatch",
|
|
83
121
|
"SignerSanctions",
|
|
122
|
+
"SignerSanctionsClear",
|
|
123
|
+
"SignerSanctionsHit",
|
|
124
|
+
"SignerSanctionsUnavailable",
|
|
125
|
+
"Subject",
|
|
84
126
|
"TimeoutError",
|
|
85
127
|
"TokenExpiredError",
|
|
86
128
|
"VerificationLevel",
|
|
87
129
|
"WalletAuthRequiresSigningBody",
|
|
130
|
+
"WalletAuthRequiresSigningNextSteps",
|
|
88
131
|
"WalletSignerMismatchBody",
|
|
132
|
+
"WalletSignerMismatchNextSteps",
|
|
89
133
|
"__version__",
|
|
90
134
|
"is_agentscore_test_address",
|
|
91
135
|
]
|
|
@@ -65,7 +65,7 @@ def _do_sync(send_fn: Callable[[], httpx.Response]) -> httpx.Response:
|
|
|
65
65
|
``httpx.TimeoutException`` (and subclasses: ConnectTimeout / ReadTimeout / WriteTimeout /
|
|
66
66
|
PoolTimeout) becomes our :class:`TimeoutError`. Every other ``httpx.HTTPError`` (ConnectError,
|
|
67
67
|
NetworkError, ProtocolError, etc.) becomes :class:`AgentScoreError` with ``code='network_error'``
|
|
68
|
-
and ``status_code=0
|
|
68
|
+
and ``status_code=0``.
|
|
69
69
|
"""
|
|
70
70
|
try:
|
|
71
71
|
return send_fn()
|
|
@@ -227,7 +227,9 @@ class AgentScore:
|
|
|
227
227
|
return self._handle_response(response), response
|
|
228
228
|
|
|
229
229
|
def _handle_response(self, response: httpx.Response) -> Any:
|
|
230
|
-
|
|
230
|
+
# Treat any non-2xx as an error. httpx does not follow redirects by default,
|
|
231
|
+
# so a 3xx would otherwise fall through to the JSON-parse success path.
|
|
232
|
+
if not (200 <= response.status_code < 300):
|
|
231
233
|
raise _build_error_from_response(response)
|
|
232
234
|
try:
|
|
233
235
|
return response.json()
|
|
@@ -14,17 +14,13 @@ class AgentScoreError(Exception):
|
|
|
14
14
|
self.status_code = status_code
|
|
15
15
|
# Response-body fields beyond `error.{code,message}` — e.g. verify_url,
|
|
16
16
|
# linked_wallets, claimed_operator, actual_signer, reasons. Consumers
|
|
17
|
-
# branch on these for granular recovery
|
|
18
|
-
# in the node sibling for the canonical use). Defaults to {} so callers
|
|
17
|
+
# branch on these for granular recovery. Defaults to {} so callers
|
|
19
18
|
# constructing this error by hand without a body can omit it.
|
|
20
19
|
self.details: dict[str, Any] = details or {}
|
|
21
20
|
|
|
22
21
|
@property
|
|
23
22
|
def status(self) -> int:
|
|
24
|
-
"""Alias for ``status_code``
|
|
25
|
-
|
|
26
|
-
Polyglot codebases can use ``err.status`` regardless of which SDK raised the error.
|
|
27
|
-
"""
|
|
23
|
+
"""Alias for ``status_code`` so ``err.status`` also works."""
|
|
28
24
|
return self.status_code
|
|
29
25
|
|
|
30
26
|
|
|
@@ -322,6 +322,8 @@ class AssessResponse(_AssessResponseRequired, total=False):
|
|
|
322
322
|
class SessionCreateRequest(TypedDict, total=False):
|
|
323
323
|
context: str
|
|
324
324
|
product_name: str
|
|
325
|
+
address: str
|
|
326
|
+
operator_token: str
|
|
325
327
|
|
|
326
328
|
|
|
327
329
|
class SessionCreateNextSteps(TypedDict, total=False):
|
|
@@ -407,7 +409,11 @@ class AccountVerification(TypedDict, total=False):
|
|
|
407
409
|
jurisdiction: str | None
|
|
408
410
|
age_verified: bool
|
|
409
411
|
age_bracket: str | None
|
|
410
|
-
|
|
412
|
+
# `True` only when screened AND within the freshness window; `False` when on a
|
|
413
|
+
# sanctions list; `None` when never screened or stale.
|
|
414
|
+
sanctions_clear: bool | None
|
|
415
|
+
# ISO-8601 timestamp of the last sanctions screening, or `None` if never screened.
|
|
416
|
+
sanctions_checked_at: str | None
|
|
411
417
|
operator_type: str | None
|
|
412
418
|
|
|
413
419
|
|
|
@@ -421,11 +427,9 @@ class CredentialRevokeResponse(TypedDict):
|
|
|
421
427
|
revoked: Literal[True]
|
|
422
428
|
|
|
423
429
|
|
|
424
|
-
class
|
|
430
|
+
class CredentialCreateErrorNextSteps(TypedDict):
|
|
431
|
+
# The API always emits both fields on the 409 KYC-required body, so both are required.
|
|
425
432
|
action: NextStepsAction
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
class CredentialCreateErrorNextSteps(_CredentialCreateErrorNextSteps, total=False):
|
|
429
433
|
user_message: str
|
|
430
434
|
|
|
431
435
|
|
|
@@ -2141,3 +2141,280 @@ def test_token_expired_error_ignores_wrong_typed_body_fields():
|
|
|
2141
2141
|
# Raw values still in details for inspection.
|
|
2142
2142
|
assert err.details["verify_url"] == 12345
|
|
2143
2143
|
client.close()
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
# ---------------------------------------------------------------------------
|
|
2147
|
+
# Branch coverage: 429 retry on non-assess methods (_send_sync / _send_async)
|
|
2148
|
+
# ---------------------------------------------------------------------------
|
|
2149
|
+
|
|
2150
|
+
|
|
2151
|
+
@respx.mock
|
|
2152
|
+
def test_create_session_retries_once_on_429_then_succeeds(monkeypatch):
|
|
2153
|
+
"""`create_session` uses `_send_sync` (no response handle), exercising the 429
|
|
2154
|
+
retry branch distinct from `assess`'s `_send_sync_with_response`."""
|
|
2155
|
+
monkeypatch.setattr("agentscore.client.time.sleep", lambda _: None)
|
|
2156
|
+
route = respx.post(f"{BASE_URL}/v1/sessions").mock(
|
|
2157
|
+
side_effect=[
|
|
2158
|
+
httpx.Response(429, headers={"retry-after": "0"}, json={}),
|
|
2159
|
+
httpx.Response(200, json=SESSION_CREATE_PAYLOAD),
|
|
2160
|
+
],
|
|
2161
|
+
)
|
|
2162
|
+
client = AgentScore(api_key=API_KEY)
|
|
2163
|
+
result = client.create_session(context="checkout")
|
|
2164
|
+
assert route.call_count == 2
|
|
2165
|
+
assert result == SESSION_CREATE_PAYLOAD
|
|
2166
|
+
client.close()
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
@pytest.mark.asyncio
|
|
2170
|
+
@respx.mock
|
|
2171
|
+
async def test_acreate_session_retries_once_on_429_then_succeeds(monkeypatch):
|
|
2172
|
+
"""Async mirror: `acreate_session` uses `_send_async`, exercising its 429 retry branch."""
|
|
2173
|
+
|
|
2174
|
+
async def _no_sleep(_seconds: float) -> None:
|
|
2175
|
+
return None
|
|
2176
|
+
|
|
2177
|
+
monkeypatch.setattr("agentscore.client.asyncio.sleep", _no_sleep)
|
|
2178
|
+
route = respx.post(f"{BASE_URL}/v1/sessions").mock(
|
|
2179
|
+
side_effect=[
|
|
2180
|
+
httpx.Response(429, headers={"retry-after": "0"}, json={}),
|
|
2181
|
+
httpx.Response(200, json=SESSION_CREATE_PAYLOAD),
|
|
2182
|
+
],
|
|
2183
|
+
)
|
|
2184
|
+
client = AgentScore(api_key=API_KEY)
|
|
2185
|
+
result = await client.acreate_session(context="checkout")
|
|
2186
|
+
assert route.call_count == 2
|
|
2187
|
+
assert result == SESSION_CREATE_PAYLOAD
|
|
2188
|
+
await client.aclose()
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
# ---------------------------------------------------------------------------
|
|
2192
|
+
# Branch coverage: async non-timeout httpx.HTTPError wraps to network_error
|
|
2193
|
+
# ---------------------------------------------------------------------------
|
|
2194
|
+
|
|
2195
|
+
|
|
2196
|
+
@pytest.mark.asyncio
|
|
2197
|
+
@respx.mock
|
|
2198
|
+
async def test_aassess_connect_error_wraps_to_network_error():
|
|
2199
|
+
"""Async `_do_async`: a non-timeout httpx.HTTPError (ConnectError) becomes a generic
|
|
2200
|
+
AgentScoreError(code='network_error', status_code=0), mirroring the sync path."""
|
|
2201
|
+
respx.post(f"{BASE_URL}/v1/assess").mock(side_effect=httpx.ConnectError("dns down"))
|
|
2202
|
+
client = AgentScore(api_key=API_KEY)
|
|
2203
|
+
with pytest.raises(AgentScoreError) as exc_info:
|
|
2204
|
+
await client.aassess(address=ADDRESS)
|
|
2205
|
+
assert exc_info.value.code == "network_error"
|
|
2206
|
+
assert exc_info.value.status_code == 0
|
|
2207
|
+
# Not the typed TimeoutError subclass.
|
|
2208
|
+
from agentscore.errors import TimeoutError as AgentScoreTimeoutError
|
|
2209
|
+
|
|
2210
|
+
assert not isinstance(exc_info.value, AgentScoreTimeoutError)
|
|
2211
|
+
await client.aclose()
|
|
2212
|
+
|
|
2213
|
+
|
|
2214
|
+
# ---------------------------------------------------------------------------
|
|
2215
|
+
# Branch coverage: _build_error_from_response body-shape branches
|
|
2216
|
+
# ---------------------------------------------------------------------------
|
|
2217
|
+
|
|
2218
|
+
|
|
2219
|
+
@respx.mock
|
|
2220
|
+
def test_error_body_non_dict_json_falls_through_to_defaults():
|
|
2221
|
+
"""When the error body parses to JSON that is NOT a dict (e.g. a list), the SDK keeps
|
|
2222
|
+
code='unknown_error' and message=response.text rather than crashing."""
|
|
2223
|
+
respx.post(f"{BASE_URL}/v1/assess").mock(
|
|
2224
|
+
return_value=httpx.Response(500, json=["oops", "not", "a", "dict"]),
|
|
2225
|
+
)
|
|
2226
|
+
client = AgentScore(api_key=API_KEY)
|
|
2227
|
+
with pytest.raises(AgentScoreError) as exc_info:
|
|
2228
|
+
client.assess(address=ADDRESS)
|
|
2229
|
+
err = exc_info.value
|
|
2230
|
+
assert type(err) is AgentScoreError
|
|
2231
|
+
assert err.code == "unknown_error"
|
|
2232
|
+
assert err.status_code == 500
|
|
2233
|
+
client.close()
|
|
2234
|
+
|
|
2235
|
+
|
|
2236
|
+
@respx.mock
|
|
2237
|
+
def test_error_body_error_field_not_dict_keeps_defaults():
|
|
2238
|
+
"""When `error` is present but not a dict (e.g. a string), code/message extraction is
|
|
2239
|
+
skipped but the rest of the body is still preserved into `details`."""
|
|
2240
|
+
respx.post(f"{BASE_URL}/v1/assess").mock(
|
|
2241
|
+
return_value=httpx.Response(
|
|
2242
|
+
500,
|
|
2243
|
+
json={"error": "a bare string, not an object", "request_id": "req_123"},
|
|
2244
|
+
),
|
|
2245
|
+
)
|
|
2246
|
+
client = AgentScore(api_key=API_KEY)
|
|
2247
|
+
with pytest.raises(AgentScoreError) as exc_info:
|
|
2248
|
+
client.assess(address=ADDRESS)
|
|
2249
|
+
err = exc_info.value
|
|
2250
|
+
assert err.code == "unknown_error"
|
|
2251
|
+
assert err.status_code == 500
|
|
2252
|
+
# Everything except the `error` block is preserved.
|
|
2253
|
+
assert err.details == {"request_id": "req_123"}
|
|
2254
|
+
client.close()
|
|
2255
|
+
|
|
2256
|
+
|
|
2257
|
+
# ---------------------------------------------------------------------------
|
|
2258
|
+
# Branch coverage: sync client reuse (cached _sync_client)
|
|
2259
|
+
# ---------------------------------------------------------------------------
|
|
2260
|
+
|
|
2261
|
+
|
|
2262
|
+
@respx.mock
|
|
2263
|
+
def test_sync_client_is_reused_across_calls():
|
|
2264
|
+
"""The second call must reuse the cached httpx.Client instance (the `is not None`
|
|
2265
|
+
branch of `_get_sync_client`)."""
|
|
2266
|
+
respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD))
|
|
2267
|
+
client = AgentScore(api_key=API_KEY)
|
|
2268
|
+
client.get_reputation(ADDRESS)
|
|
2269
|
+
first = client._sync_client
|
|
2270
|
+
assert first is not None
|
|
2271
|
+
client.get_reputation(ADDRESS)
|
|
2272
|
+
assert client._sync_client is first
|
|
2273
|
+
client.close()
|
|
2274
|
+
|
|
2275
|
+
|
|
2276
|
+
# ---------------------------------------------------------------------------
|
|
2277
|
+
# Branch coverage: aget_reputation chain param + aassess optional-arg branches
|
|
2278
|
+
# ---------------------------------------------------------------------------
|
|
2279
|
+
|
|
2280
|
+
|
|
2281
|
+
@pytest.mark.asyncio
|
|
2282
|
+
@respx.mock
|
|
2283
|
+
async def test_aget_reputation_with_chain():
|
|
2284
|
+
route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(
|
|
2285
|
+
return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)
|
|
2286
|
+
)
|
|
2287
|
+
client = AgentScore(api_key=API_KEY)
|
|
2288
|
+
await client.aget_reputation(ADDRESS, chain="base")
|
|
2289
|
+
assert "chain=base" in str(route.calls.last.request.url)
|
|
2290
|
+
await client.aclose()
|
|
2291
|
+
|
|
2292
|
+
|
|
2293
|
+
@pytest.mark.asyncio
|
|
2294
|
+
@respx.mock
|
|
2295
|
+
async def test_aassess_forwards_chain_refresh_and_policy():
|
|
2296
|
+
"""Covers the `chain`, `refresh`, and `policy` body-building branches of `aassess`."""
|
|
2297
|
+
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
|
|
2298
|
+
client = AgentScore(api_key=API_KEY)
|
|
2299
|
+
await client.aassess(
|
|
2300
|
+
address=ADDRESS,
|
|
2301
|
+
chain="base",
|
|
2302
|
+
refresh=False,
|
|
2303
|
+
policy={"min_score": 50},
|
|
2304
|
+
)
|
|
2305
|
+
body = json.loads(route.calls.last.request.content)
|
|
2306
|
+
assert body["chain"] == "base"
|
|
2307
|
+
assert body["refresh"] is False
|
|
2308
|
+
assert body["policy"] == {"min_score": 50}
|
|
2309
|
+
await client.aclose()
|
|
2310
|
+
|
|
2311
|
+
|
|
2312
|
+
# ---------------------------------------------------------------------------
|
|
2313
|
+
# Branch coverage: acreate_credential optional-arg branches (label / ttl_days None)
|
|
2314
|
+
# ---------------------------------------------------------------------------
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
@pytest.mark.asyncio
|
|
2318
|
+
@respx.mock
|
|
2319
|
+
async def test_acreate_credential_omits_unset_fields():
|
|
2320
|
+
"""Both `label` and `ttl_days` default to None — the body must stay empty (covers the
|
|
2321
|
+
falsy side of both `is not None` branches)."""
|
|
2322
|
+
route = respx.post(f"{BASE_URL}/v1/credentials").mock(
|
|
2323
|
+
return_value=httpx.Response(200, json=CREDENTIAL_CREATE_PAYLOAD)
|
|
2324
|
+
)
|
|
2325
|
+
client = AgentScore(api_key=API_KEY)
|
|
2326
|
+
await client.acreate_credential()
|
|
2327
|
+
body = json.loads(route.calls.last.request.content)
|
|
2328
|
+
assert body == {}
|
|
2329
|
+
await client.aclose()
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
@pytest.mark.asyncio
|
|
2333
|
+
@respx.mock
|
|
2334
|
+
async def test_acreate_credential_forwards_label_and_ttl():
|
|
2335
|
+
"""Truthy side of both branches — label and ttl_days are forwarded."""
|
|
2336
|
+
route = respx.post(f"{BASE_URL}/v1/credentials").mock(
|
|
2337
|
+
return_value=httpx.Response(200, json=CREDENTIAL_CREATE_PAYLOAD)
|
|
2338
|
+
)
|
|
2339
|
+
client = AgentScore(api_key=API_KEY)
|
|
2340
|
+
await client.acreate_credential(label="ci", ttl_days=30)
|
|
2341
|
+
body = json.loads(route.calls.last.request.content)
|
|
2342
|
+
assert body == {"label": "ci", "ttl_days": 30}
|
|
2343
|
+
await client.aclose()
|
|
2344
|
+
|
|
2345
|
+
|
|
2346
|
+
# ---------------------------------------------------------------------------
|
|
2347
|
+
# Branch coverage: idempotency_key over-length warning (sync + async)
|
|
2348
|
+
# ---------------------------------------------------------------------------
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
@respx.mock
|
|
2352
|
+
def test_associate_wallet_warns_on_overlong_idempotency_key(caplog):
|
|
2353
|
+
from agentscore.client import _IDEMPOTENCY_KEY_MAX
|
|
2354
|
+
|
|
2355
|
+
route = respx.post(f"{BASE_URL}/v1/credentials/wallets").mock(
|
|
2356
|
+
return_value=httpx.Response(200, json={"ok": True, "first_seen": True})
|
|
2357
|
+
)
|
|
2358
|
+
client = AgentScore(api_key=API_KEY)
|
|
2359
|
+
long_key = "k" * (_IDEMPOTENCY_KEY_MAX + 1)
|
|
2360
|
+
with caplog.at_level("WARNING", logger="agentscore"):
|
|
2361
|
+
client.associate_wallet(ASSOCIATE_TOKEN, ASSOCIATE_WALLET, ASSOCIATE_NETWORK, idempotency_key=long_key)
|
|
2362
|
+
assert any("truncated server-side" in r.message for r in caplog.records)
|
|
2363
|
+
# Over-length key is still forwarded (server truncates, doesn't reject).
|
|
2364
|
+
body = json.loads(route.calls.last.request.content)
|
|
2365
|
+
assert body["idempotency_key"] == long_key
|
|
2366
|
+
client.close()
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
@pytest.mark.asyncio
|
|
2370
|
+
@respx.mock
|
|
2371
|
+
async def test_aassociate_wallet_warns_on_overlong_idempotency_key(caplog):
|
|
2372
|
+
from agentscore.client import _IDEMPOTENCY_KEY_MAX
|
|
2373
|
+
|
|
2374
|
+
route = respx.post(f"{BASE_URL}/v1/credentials/wallets").mock(
|
|
2375
|
+
return_value=httpx.Response(200, json={"ok": True, "first_seen": True})
|
|
2376
|
+
)
|
|
2377
|
+
client = AgentScore(api_key=API_KEY)
|
|
2378
|
+
long_key = "k" * (_IDEMPOTENCY_KEY_MAX + 1)
|
|
2379
|
+
with caplog.at_level("WARNING", logger="agentscore"):
|
|
2380
|
+
await client.aassociate_wallet(ASSOCIATE_TOKEN, ASSOCIATE_WALLET, ASSOCIATE_NETWORK, idempotency_key=long_key)
|
|
2381
|
+
assert any("truncated server-side" in r.message for r in caplog.records)
|
|
2382
|
+
body = json.loads(route.calls.last.request.content)
|
|
2383
|
+
assert body["idempotency_key"] == long_key
|
|
2384
|
+
await client.aclose()
|
|
2385
|
+
|
|
2386
|
+
|
|
2387
|
+
# ---------------------------------------------------------------------------
|
|
2388
|
+
# Branch coverage: sync telemetry swallows raised exceptions (not just 5xx responses)
|
|
2389
|
+
# ---------------------------------------------------------------------------
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
@respx.mock
|
|
2393
|
+
def test_telemetry_signer_match_swallows_raised_exception(caplog):
|
|
2394
|
+
"""A transport-layer exception (ConnectError) must be caught and logged, not raised,
|
|
2395
|
+
by the sync telemetry method (its `except Exception` handler)."""
|
|
2396
|
+
respx.post(f"{BASE_URL}/v1/telemetry/signer-match").mock(side_effect=httpx.ConnectError("dns down"))
|
|
2397
|
+
client = AgentScore(api_key=API_KEY)
|
|
2398
|
+
with caplog.at_level("WARNING", logger="agentscore"):
|
|
2399
|
+
client.telemetry_signer_match({"kind": "pass"}) # must not raise
|
|
2400
|
+
assert any("telemetry_signer_match failed" in r.message for r in caplog.records)
|
|
2401
|
+
client.close()
|
|
2402
|
+
|
|
2403
|
+
|
|
2404
|
+
# ---------------------------------------------------------------------------
|
|
2405
|
+
# Branch coverage: _extract_quota defensive guard for headers without .get
|
|
2406
|
+
# ---------------------------------------------------------------------------
|
|
2407
|
+
|
|
2408
|
+
|
|
2409
|
+
def test_extract_quota_returns_none_when_headers_lack_get():
|
|
2410
|
+
"""Defensive guard: if a response object exposes `.headers` without a `.get` method,
|
|
2411
|
+
`_extract_quota` returns None rather than raising."""
|
|
2412
|
+
from agentscore.client import _extract_quota
|
|
2413
|
+
|
|
2414
|
+
class _NoGetHeaders:
|
|
2415
|
+
pass
|
|
2416
|
+
|
|
2417
|
+
class _FakeResponse:
|
|
2418
|
+
headers = _NoGetHeaders()
|
|
2419
|
+
|
|
2420
|
+
assert _extract_quota(_FakeResponse()) is None # type: ignore[arg-type]
|
|
@@ -41,6 +41,12 @@ def test_details_defaults_to_empty_dict_when_omitted():
|
|
|
41
41
|
assert err.details == {}
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def test_status_property_aliases_status_code():
|
|
45
|
+
err = AgentScoreError(code="rate_limited", message="Too many requests", status_code=429)
|
|
46
|
+
assert err.status == 429
|
|
47
|
+
assert err.status == err.status_code
|
|
48
|
+
|
|
49
|
+
|
|
44
50
|
def test_details_preserves_response_body_fields_for_granular_recovery():
|
|
45
51
|
err = AgentScoreError(
|
|
46
52
|
code="wallet_signer_mismatch",
|
|
@@ -4,7 +4,7 @@ requires-python = ">=3.11"
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "agentscore-py"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.1"
|
|
8
8
|
source = { editable = "." }
|
|
9
9
|
dependencies = [
|
|
10
10
|
{ name = "httpx" },
|
|
@@ -425,27 +425,27 @@ wheels = [
|
|
|
425
425
|
|
|
426
426
|
[[package]]
|
|
427
427
|
name = "ty"
|
|
428
|
-
version = "0.0.
|
|
428
|
+
version = "0.0.39"
|
|
429
429
|
source = { registry = "https://pypi.org/simple" }
|
|
430
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
430
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d0/8d/7b5c74dc287fbcb37bae9853cec13bf44717c1735298500e4aeba31579a9/ty-0.0.39.tar.gz", hash = "sha256:f750277e76a01ecd86185960eca73823c26a53c51103568d56d4d904575159fd", size = 5702365, upload-time = "2026-05-22T21:09:56.403Z" }
|
|
431
431
|
wheels = [
|
|
432
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
433
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
434
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
435
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
436
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
437
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
438
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
439
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
440
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
441
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
442
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
443
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
444
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
445
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
446
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
447
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
448
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
432
|
+
{ url = "https://files.pythonhosted.org/packages/08/17/9b89802c26d12d0f7a27bc25d4066d941d42891e8898f9f26499f0067e32/ty-0.0.39-py3-none-linux_armv6l.whl", hash = "sha256:c1bb7ac70f1f7d70cc6655fd96558039e4562b10f489fa49c7ebfd5fcee73ad1", size = 11360431, upload-time = "2026-05-22T21:09:18.689Z" },
|
|
433
|
+
{ url = "https://files.pythonhosted.org/packages/9c/c6/663ded50e823dbf9fb9d002eca46b7cb1fb2c72b744b84f22ce732a0ee0b/ty-0.0.39-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3435b64c1e59c14c9aa39c20cc018823937cd38d55db853e74d95b8f420569b0", size = 11096281, upload-time = "2026-05-22T21:09:15.383Z" },
|
|
434
|
+
{ url = "https://files.pythonhosted.org/packages/8b/ae/5d38ba9a6456ff4c78d212cf464fd8b9a25d8118465197b0b2dc891c0b19/ty-0.0.39-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f136377ce46c73677701a9e1ad730bf72f699bcec046e422eb79d0886cac3ab", size = 10529674, upload-time = "2026-05-22T21:09:46.471Z" },
|
|
435
|
+
{ url = "https://files.pythonhosted.org/packages/be/6f/43638cb8106445d3c8817256a0731cde9dd7b6a53ae2e881294bc1930ca3/ty-0.0.39-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36b65fb0cc17f03e851d40e210d420be94ab8bc52d041328ad1e45f616036a61", size = 11055561, upload-time = "2026-05-22T21:09:36.981Z" },
|
|
436
|
+
{ url = "https://files.pythonhosted.org/packages/91/17/95e62cf4458527ce78dc386eba18f8b10c3fb64cd8c9e7e59b262ff6029d/ty-0.0.39-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4967967bfadf3860ff84c3fccdbaec8edf8aa20d0d727521084733d853de6657", size = 11127185, upload-time = "2026-05-22T21:09:31.395Z" },
|
|
437
|
+
{ url = "https://files.pythonhosted.org/packages/4e/c0/93666c213db5c71ab1b1f1a0db5f66bf8c7c0e0b0bf59859f5da8f0b3c36/ty-0.0.39-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e10ecb1297099ddf9a1f054f8bd921d1863ce85fb819a3c96ed27865a1ba6ed", size = 11608459, upload-time = "2026-05-22T21:09:12.862Z" },
|
|
438
|
+
{ url = "https://files.pythonhosted.org/packages/79/85/3b26585afc8b50230d6464bb0642feef4fab3f847e38b1f0ffa971a81446/ty-0.0.39-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b19cca70e465d71b0510656343883d62372bbe74b7845cae7c0e701d6d5264b", size = 12177101, upload-time = "2026-05-22T21:09:40.519Z" },
|
|
439
|
+
{ url = "https://files.pythonhosted.org/packages/49/4a/1039e4f6afc576dc1c3a4d22a6478904a1ad3766597cd0b93c077ab9dfce/ty-0.0.39-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:56c6704b01b9b3d80ff26b2918423b742516d1e469bef830e9254dcedc9185bf", size = 11827815, upload-time = "2026-05-22T21:09:49.89Z" },
|
|
440
|
+
{ url = "https://files.pythonhosted.org/packages/e2/c5/4688652870e350a76a8157f7ffb59ad54f37d5d10725aa7076f66ac94ec8/ty-0.0.39-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b7840ff46764b6a6757f4ade1cd0530fc3e8a0b435ca93e7602360e4cb90b6", size = 11694429, upload-time = "2026-05-22T21:09:21.568Z" },
|
|
441
|
+
{ url = "https://files.pythonhosted.org/packages/fc/72/8a1c4e823bb5bdc935a1c8140e100304e36a68a4139592f170aa9736fdb7/ty-0.0.39-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c62a3a87ce26b50819f0dbf03bd95f23f19eeb87bbc7aa732ec64277c77f1aa", size = 11869846, upload-time = "2026-05-22T21:09:28.053Z" },
|
|
442
|
+
{ url = "https://files.pythonhosted.org/packages/17/9f/cf982457b861ae22d657c5dcdbc631199f7f90264279db1d17230dfbc3ff/ty-0.0.39-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f8c34bc81a9c3516e49904e9d8330aac385377cca98390193ea02b903a40fcf0", size = 11029763, upload-time = "2026-05-22T21:09:06.791Z" },
|
|
443
|
+
{ url = "https://files.pythonhosted.org/packages/46/c9/95b64f6d43ae6e8f0b7e13dacf9c196d35819af22b1924171fba31383156/ty-0.0.39-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:66f5ab11586a64e79cb692ad685ee5469325c31b5f30bd3554f52f36dbe28cc4", size = 11146761, upload-time = "2026-05-22T21:09:10.178Z" },
|
|
444
|
+
{ url = "https://files.pythonhosted.org/packages/52/69/0a89cfb06f7632a05bf56c78e0affb4a40f81759e275376cea75c9c5abe9/ty-0.0.39-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e8d89732bcbbcb091f439e556dfc4932f198b118b47d5b85212c60662099670e", size = 11281843, upload-time = "2026-05-22T21:09:34.234Z" },
|
|
445
|
+
{ url = "https://files.pythonhosted.org/packages/0e/53/64c4a27067a46643fea2b3fcf21a8a2f838d91a65ffdd14f2e82945b9538/ty-0.0.39-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:eceb6c91dcd05a231119f82abdd9aa337513de23ca6ac990bc44f88791dc1799", size = 11792477, upload-time = "2026-05-22T21:09:24.923Z" },
|
|
446
|
+
{ url = "https://files.pythonhosted.org/packages/1a/e8/02f4dd4a12bcdbda0006f9c7ff3b99a4be06bd0d257d3bd4a5b66de074e6/ty-0.0.39-py3-none-win32.whl", hash = "sha256:891c3262314dbc80bf3e872634d23dd216306945daa9a9fcc206ce5ed21ac4c9", size = 10615377, upload-time = "2026-05-22T21:09:43.167Z" },
|
|
447
|
+
{ url = "https://files.pythonhosted.org/packages/b5/5a/aaeb22faa8d4dae90a287d4c3636c671edcff3b99be5f4fc8b79ad71eef6/ty-0.0.39-py3-none-win_amd64.whl", hash = "sha256:ba7f2d54452535419e90f6f03ff39282999e87b43c21c00559f6d7ad711a36d5", size = 11710711, upload-time = "2026-05-22T21:09:53.179Z" },
|
|
448
|
+
{ url = "https://files.pythonhosted.org/packages/a3/17/ae7339651bfcaa5f54698c8c70eaf5031baa400ecb67baec31d03a56cbd4/ty-0.0.39-py3-none-win_arm64.whl", hash = "sha256:eb4cf0fefbbfedf9a352597bb2431ebdcb7eb3a595c0f825f228e897a0ec285d", size = 11081409, upload-time = "2026-05-22T21:09:03.741Z" },
|
|
449
449
|
]
|
|
450
450
|
|
|
451
451
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|