agentscore-py 2.4.0__tar.gz → 2.4.2__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.4.0 → agentscore_py-2.4.2}/PKG-INFO +1 -1
  2. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/agentscore/client.py +3 -1
  3. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/pyproject.toml +1 -1
  4. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/tests/test_client.py +277 -0
  5. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/tests/test_errors.py +6 -0
  6. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/uv.lock +1 -1
  7. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.claude/CLAUDE.md +0 -0
  8. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  9. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  10. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  11. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.github/dependabot.yml +0 -0
  12. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.github/workflows/ci.yml +0 -0
  13. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.github/workflows/publish.yml +0 -0
  14. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.github/workflows/security.yml +0 -0
  15. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/.gitignore +0 -0
  16. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/CODE_OF_CONDUCT.md +0 -0
  17. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/CONTRIBUTING.md +0 -0
  18. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/LICENSE +0 -0
  19. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/README.md +0 -0
  20. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/SECURITY.md +0 -0
  21. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/agentscore/__init__.py +0 -0
  22. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/agentscore/errors.py +0 -0
  23. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/agentscore/py.typed +0 -0
  24. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/agentscore/test_mode.py +0 -0
  25. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/agentscore/types.py +0 -0
  26. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/lefthook.yml +0 -0
  27. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/ruff.toml +0 -0
  28. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/tests/__init__.py +0 -0
  29. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/tests/conftest.py +0 -0
  30. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/tests/test_integration.py +0 -0
  31. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/tests/test_test_mode.py +0 -0
  32. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/tests/test_types.py +0 -0
  33. {agentscore_py-2.4.0 → agentscore_py-2.4.2}/vulture_whitelist.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentscore-py
3
- Version: 2.4.0
3
+ Version: 2.4.2
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
@@ -93,7 +93,9 @@ def _build_error_from_response(response: httpx.Response) -> AgentScoreError:
93
93
  dedicated subclass for.
94
94
  """
95
95
  code = "unknown_error"
96
- message = response.text
96
+ # Concise default, overridden by ``error.message`` when the body carries one. The raw
97
+ # body, when it is JSON, is preserved in ``details`` below.
98
+ message = f"Request failed with status {response.status_code}"
97
99
  details: dict[str, Any] = {}
98
100
 
99
101
  try:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentscore-py"
7
- version = "2.4.0"
7
+ version = "2.4.2"
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 a status-based default message 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.4.0"
7
+ version = "2.4.2"
8
8
  source = { editable = "." }
9
9
  dependencies = [
10
10
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes