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.
Files changed (33) hide show
  1. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/CONTRIBUTING.md +1 -1
  3. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/PKG-INFO +1 -1
  4. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/SECURITY.md +2 -2
  5. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/__init__.py +44 -0
  6. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/client.py +4 -2
  7. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/errors.py +2 -6
  8. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/types.py +9 -5
  9. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/pyproject.toml +1 -1
  10. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_client.py +277 -0
  11. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_errors.py +6 -0
  12. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/uv.lock +20 -20
  13. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.claude/CLAUDE.md +0 -0
  14. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/dependabot.yml +0 -0
  17. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/workflows/ci.yml +0 -0
  18. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/workflows/publish.yml +0 -0
  19. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.github/workflows/security.yml +0 -0
  20. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/.gitignore +0 -0
  21. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/CODE_OF_CONDUCT.md +0 -0
  22. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/LICENSE +0 -0
  23. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/README.md +0 -0
  24. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/py.typed +0 -0
  25. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/agentscore/test_mode.py +0 -0
  26. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/lefthook.yml +0 -0
  27. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/ruff.toml +0 -0
  28. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/__init__.py +0 -0
  29. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/conftest.py +0 -0
  30. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_integration.py +0 -0
  31. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_test_mode.py +0 -0
  32. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/tests/test_types.py +0 -0
  33. {agentscore_py-2.3.4 → agentscore_py-2.4.1}/vulture_whitelist.py +0 -0
@@ -17,5 +17,5 @@ What you expected to happen.
17
17
 
18
18
  **Environment**
19
19
  - Package version:
20
- - Runtime (Node.js/Python version):
20
+ - Python version:
21
21
  - OS:
@@ -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 — one feature or fix per PR
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentscore-py
3
- Version: 2.3.4
3
+ Version: 2.4.1
4
4
  Summary: Python client for the AgentScore APIs
5
5
  Project-URL: Homepage, https://agentscore.sh
6
6
  Project-URL: Repository, https://github.com/agentscore/python-sdk
@@ -19,5 +19,5 @@ We will acknowledge receipt within 48 hours and aim to release a fix within 7 da
19
19
 
20
20
  | Version | Supported |
21
21
  |---------|-----------|
22
- | 1.x | ✅ |
23
- | < 1.0 | ❌ |
22
+ | 2.x | ✅ |
23
+ | < 2.0 | ❌ |
@@ -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`` — parity with the node-sdk catch-all.
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
- if response.status_code >= 400:
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 (see the mcp denial-code rendering
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`` — parity with node-sdk's attribute name.
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
- sanctions_status: str | None
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 _CredentialCreateErrorNextSteps(TypedDict):
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentscore-py"
7
- version = "2.3.4"
7
+ version = "2.4.1"
8
8
  description = "Python client for the AgentScore APIs"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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.3.4"
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.38"
428
+ version = "0.0.39"
429
429
  source = { registry = "https://pypi.org/simple" }
430
- sdist = { url = "https://files.pythonhosted.org/packages/33/3b/45be6b37d5060d6917bf7f1f234c00d360fc5f8b7486f8a96af640e25661/ty-0.0.38.tar.gz", hash = "sha256:fbc8d47f7630457669ab41e333dc093897fdb7ead1ffc94dcf8f30b5d39aa56d", size = 5681218, upload-time = "2026-05-20T00:15:32.781Z" }
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/54/43/ea9b4e57d6a266670dbe34858e92f6093ca054ad1b48f1c82580a72340fb/ty-0.0.38-py3-none-linux_armv6l.whl", hash = "sha256:3501dcf44ca03f813f9cb4fabfdf601adc0ac1337c411405b470530679e37a45", size = 11289326, upload-time = "2026-05-20T00:14:52.371Z" },
433
- { url = "https://files.pythonhosted.org/packages/5d/ff/24e2f623a1c6b5f5ccf8bf82fccd937033c6a7dba57a4028c7f41270fa4a/ty-0.0.38-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b34b4094b76252c3e8c90762cdd5e8a9f1101534484745ff4b480f71eb38ac2e", size = 11063047, upload-time = "2026-05-20T00:14:42.832Z" },
434
- { url = "https://files.pythonhosted.org/packages/e9/41/4f0d910f0acbd20b358eda80a5cd6a8361d27ff5b8e87ab559d3f69f125e/ty-0.0.38-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c518ad33a877677365baab2e21d82cf59ffee789203a15a143f5179ee5a1d3f8", size = 10494436, upload-time = "2026-05-20T00:15:24.425Z" },
435
- { url = "https://files.pythonhosted.org/packages/69/d8/da06833422082aa98b169a391f9197e2d73865e96c90b6979ac886b890a2/ty-0.0.38-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9238494722303eccddc6a27eb647948b694eecd6b974910d13b9e6cd46bbeb6a", size = 11000992, upload-time = "2026-05-20T00:14:58.368Z" },
436
- { url = "https://files.pythonhosted.org/packages/16/f7/e1172197fb827e6410ca3eb0dc68ef2789f3c70683696f2a0ce5c90764fd/ty-0.0.38-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d91d7336c5d51bf822ac0df512f300584ca4dcca041fc6a6d7df03a8ddbb31", size = 11058583, upload-time = "2026-05-20T00:15:11.314Z" },
437
- { url = "https://files.pythonhosted.org/packages/5b/61/7fbaf0c05981e006a8804287819c574dff90a6bf8e96efad7226be0700aa/ty-0.0.38-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65165879814993450710b9349791e4898c65e36b1e14eec554884c06a2f20ff1", size = 11531036, upload-time = "2026-05-20T00:15:14.62Z" },
438
- { url = "https://files.pythonhosted.org/packages/49/e3/47c0c64e401d50f925df3e52479d4e7626754b2a9e38201d142fdacd6252/ty-0.0.38-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d61868b8d1c4033bf8088191de953fed245c2f9e1bb9d2d53e5699170b0924c", size = 12129991, upload-time = "2026-05-20T00:14:39.475Z" },
439
- { url = "https://files.pythonhosted.org/packages/90/99/2f452d02901bcd7f1b109cf5b848727ce37f372c3406143aa52d1305d40e/ty-0.0.38-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f9a9175548c98dbff7707865738c07c2b1f8e07a09b8c68101baebb5dac59a4", size = 11756167, upload-time = "2026-05-20T00:15:27.526Z" },
440
- { url = "https://files.pythonhosted.org/packages/dd/0c/c7e14d111c813e1a20b82e944f1c997c4631a2bb710eaa64fb6b26835e13/ty-0.0.38-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375d3a964c6b4aea2e9237fdb5eb9ed03dc43088986a94209a28a4ea3b62001c", size = 11637099, upload-time = "2026-05-20T00:15:21.261Z" },
441
- { url = "https://files.pythonhosted.org/packages/37/de/ab02659dd1ed62898db7db4d37f9937c80854dd45e95093fa0fe10328d82/ty-0.0.38-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:cdfd547782c45267aa0b52abad31bd406bf4768c264532ef9e2360cd3c6ce048", size = 11813583, upload-time = "2026-05-20T00:14:45.875Z" },
442
- { url = "https://files.pythonhosted.org/packages/7e/57/bd1b5ebf4e71a4295484afac0202df1740b0807762b86744b1bef4534984/ty-0.0.38-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:858bc675b75626470abe4e6c3b3934b853642b04f2ac4d7139fcefea3b48b213", size = 10975405, upload-time = "2026-05-20T00:15:30.354Z" },
443
- { url = "https://files.pythonhosted.org/packages/e7/55/0305c78711bbd23922cf291996a08ef9544f4179da98e9a75c14e608f379/ty-0.0.38-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54be4f00432870da42cd74fe145a3362fd248e22d032c74bd807cb45bf068f94", size = 11097551, upload-time = "2026-05-20T00:14:55.179Z" },
444
- { url = "https://files.pythonhosted.org/packages/7c/4f/7effe7f9a6ac9719eb7234172c01739c5f888bb47f9acc2ea8da1f4afed3/ty-0.0.38-py3-none-musllinux_1_2_i686.whl", hash = "sha256:494af66a76a86dbf16a3003d3b63b03484aa4c7489dfe11f3ee5413b98b22d60", size = 11214391, upload-time = "2026-05-20T00:15:18.094Z" },
445
- { url = "https://files.pythonhosted.org/packages/75/cd/d9fdfec3a74a6ad0209fa5e7113ae29d4f457d0651cfbb813b4c6563e0d4/ty-0.0.38-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3d92527c4be78a5ce6d32e8bb0aa2a6988d4076eddf1294e56fdaf06d1a98e7e", size = 11730871, upload-time = "2026-05-20T00:14:49.219Z" },
446
- { url = "https://files.pythonhosted.org/packages/0e/4a/beefade12d109b4f7793d61b04b4478b1ad4d1465a719e7ff55b2d42461a/ty-0.0.38-py3-none-win32.whl", hash = "sha256:36fc5dd5dc09207ff3004b1560a79a3fb8d12456daeec914a7b802a918da654c", size = 10548583, upload-time = "2026-05-20T00:15:07.892Z" },
447
- { url = "https://files.pythonhosted.org/packages/15/64/941b205e2e46cc2297c245c64aa7691410b7454fa4d07a6cb3cf59487833/ty-0.0.38-py3-none-win_amd64.whl", hash = "sha256:eef0a8956ba14514076b1a963d13eb32986d9ebad7f0527b3cc01cb68bf35147", size = 11650542, upload-time = "2026-05-20T00:15:01.441Z" },
448
- { url = "https://files.pythonhosted.org/packages/59/02/c1c4f9ec4b94d95190636fa13f79c32f65165fbe3a0503882d4df164d2ac/ty-0.0.38-py3-none-win_arm64.whl", hash = "sha256:79abfc8658a026c30b1c955613437dab3ef4b12feca56a3e6df50903cc39e07f", size = 11010307, upload-time = "2026-05-20T00:15:04.567Z" },
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