diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 2469838..eec5c74 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -8,7 +8,6 @@ Two identity paths: `X-Wallet-Address` (wallet-based) and `X-Operator-Token` (cr ## Methods (sync + async) -- `get_reputation` / `aget_reputation` — cached reputation lookup (free) - `assess` / `aassess` — identity gate with policy (paid). Accepts `operator_token` for non-wallet agents. Response includes `linked_wallets[]` and `resolved_operator`. Optional `signer: { address, network }` opts into server-side wallet-signer-match AND OFAC SDN wallet-address screening — the response then carries both a `signer_match` block (wallet-binding verdict: `pass` / `wallet_signer_mismatch` / `wallet_auth_requires_wallet_signing`) and a `signer_sanctions` block (discriminated union: `{status: "clear"}` | `{sanctioned: True, ofac_label, sdn_uid, listed_at}` | `{status: "unavailable"}`). Wallet-OFAC SDN enforcement on the `signer` block is unconditional whenever a signer is supplied — no `policy.require_sanctions_clear` opt-in required. A `sanctioned: True` OR `status: "unavailable"` verdict flips the response `decision` to `deny` with `decision_reasons` including `sanctions_flagged` or `sanctions_check_unavailable` respectively (fail-closed; OFAC strict-liability). `policy.require_sanctions_clear` is the separate NAME-based screen on the resolved operator's KYC identity. - `create_session` / `acreate_session` — create verification session. Returns `agent_memory` + `next_steps`. - `poll_session` / `apoll_session` — poll session status, returns credential when verified, plus `next_steps.action`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 228f7b2..cd7fbef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - uses: useblacksmith/checkout@v1 - uses: astral-sh/setup-uv@v10.2.0 with: - version: "0.12.20" + version: "0.12.22" - run: uv python install 3.12 - uses: actions/cache@v6 with: @@ -49,7 +49,7 @@ jobs: - uses: useblacksmith/checkout@v1 - uses: astral-sh/setup-uv@v10.2.0 with: - version: "0.12.20" + version: "0.12.22" - run: uv python install ${{ matrix.python }} - run: uv sync --frozen --all-extras --all-groups --python ${{ matrix.python }} - run: uv run --python ${{ matrix.python }} pytest tests/ -q diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7adda43..56dac1c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,7 +21,7 @@ jobs: - uses: astral-sh/setup-uv@v10.2.0 with: - version: "0.12.20" + version: "0.12.22" - name: Set version from tag run: sed -i "s/^version = .*/version = \"${GITHUB_REF_NAME#v}\"/" pyproject.toml diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 30718df..4445aca 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -38,7 +38,7 @@ jobs: - uses: useblacksmith/checkout@v1 - uses: astral-sh/setup-uv@v10.2.0 with: - version: "0.12.20" + version: "0.12.22" - uses: actions/setup-python@v7 with: python-version: "3.13" diff --git a/README.md b/README.md index 426996e..ff9a43b 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,6 @@ from agentscore import AgentScore client = AgentScore(api_key="as_live_...") -# Look up cached reputation (free) -rep = client.get_reputation("0x1234...") -print(rep["score"]["value"], rep["score"]["grade"]) - -# Filter to a specific chain -base_rep = client.get_reputation("0x1234...", chain="base") - # Identity gate with policy (paid) gated = client.assess( "0x1234...", @@ -39,9 +32,8 @@ if gated["decision"] == "deny": print(gated["decision_reasons"]) # ["kyc_required"] print(gated.get("verify_url")) # URL for operator verification -# Check verification level -rep = client.get_reputation("0x1234...") -print(rep.get("verification_level")) # "none" | "wallet_claimed" | "kyc_verified" +# Verification level of the operator behind the wallet +print(gated["operator_verification"]["level"]) # "none" | "kyc_verified" ``` ### Credential-Based Identity @@ -75,7 +67,7 @@ client.create_session(kind="sign_in") # registration-only: account sign-in, no ### Wallet resolution -`assess()` responses include `resolved_operator` and `linked_wallets`, all same-operator sibling wallets (claimed via SIWE or captured via prior `associate_wallet`). The list may mix EVM addresses (`0x...` lowercased) and Solana addresses (base58, case-preserved) for cross-chain operators; merchants doing wallet-signer-match checks should accept a payment signed by any address in the list, regardless of chain. The `address` parameter on `assess()` and `get_reputation()` accepts either format; the network is auto-detected from the address shape. +`assess()` responses include `resolved_operator` and `linked_wallets`, all same-operator sibling wallets (claimed via SIWE or captured via prior `associate_wallet`). The list may mix EVM addresses (`0x...` lowercased) and Solana addresses (base58, case-preserved) for cross-chain operators; merchants doing wallet-signer-match checks should accept a payment signed by any address in the list, regardless of chain. The `address` parameter on `assess()` accepts either format; the network is auto-detected from the address shape. ### Server-side signer-match + sanctions screening @@ -138,7 +130,6 @@ All methods have async variants prefixed with `a`: ```python async with AgentScore(api_key="as_live_...") as client: - rep = await client.aget_reputation("0x1234...") result = await client.aassess("0x1234...", policy={"require_kyc": True}) # Identity model methods @@ -158,7 +149,7 @@ async with AgentScore(api_key="as_live_...") as client: ```python with AgentScore(api_key="as_live_...") as client: - rep = client.get_reputation("0x1234...") + result = client.assess("0x1234...") ``` ## Configuration @@ -178,7 +169,7 @@ with AgentScore(api_key="as_live_...") as client: from agentscore import AgentScore, AgentScoreError try: - rep = client.get_reputation("0xinvalid") + client.assess("0xinvalid") except AgentScoreError as e: print(e.code, e.status_code, str(e)) ``` diff --git a/agentscore/__init__.py b/agentscore/__init__.py index 7d9dae2..d939fdb 100644 --- a/agentscore/__init__.py +++ b/agentscore/__init__.py @@ -13,17 +13,12 @@ from agentscore.test_mode import AGENTSCORE_TEST_ADDRESSES, is_agentscore_test_address from agentscore.types import ( AccountVerification, - Activity, AgentMemoryHint, AgentMemoryIdentityPaths, - AgentSummary, AipProvenance, AipSignatureMaterial, AssessResponse, AssociateWalletResponse, - ChainEntry, - ChainScore, - Classification, CredentialCreateErrorNextSteps, CredentialCreateErrorResponse, CredentialCreateResponse, @@ -32,23 +27,13 @@ CredentialRevokeResponse, DecisionPolicy, DenialCode, - EntityType, - EvidenceSummary, - Grade, - Identity, Network, NextStepsAction, - OperatorScore, OperatorVerification, PolicyCheck, PolicyExplanation, PolicyResult, QuotaInfo, - RedactedClassification, - Reputation, - ReputationResponse, - ReputationStatus, - Score, SessionCreateNextSteps, SessionCreateRequest, SessionCreateResponse, @@ -60,7 +45,6 @@ SignerSanctionsClear, SignerSanctionsHit, SignerSanctionsUnavailable, - Subject, VerificationLevel, WalletAuthRequiresSigningBody, WalletAuthRequiresSigningNextSteps, @@ -73,19 +57,14 @@ __all__ = [ "AGENTSCORE_TEST_ADDRESSES", "AccountVerification", - "Activity", "AgentMemoryHint", "AgentMemoryIdentityPaths", "AgentScore", "AgentScoreError", - "AgentSummary", "AipProvenance", "AipSignatureMaterial", "AssessResponse", "AssociateWalletResponse", - "ChainEntry", - "ChainScore", - "Classification", "CredentialCreateErrorNextSteps", "CredentialCreateErrorResponse", "CredentialCreateResponse", @@ -94,14 +73,9 @@ "CredentialRevokeResponse", "DecisionPolicy", "DenialCode", - "EntityType", - "EvidenceSummary", - "Grade", - "Identity", "InvalidCredentialError", "Network", "NextStepsAction", - "OperatorScore", "OperatorVerification", "PaymentRequiredError", "PolicyCheck", @@ -110,11 +84,6 @@ "QuotaExceededError", "QuotaInfo", "RateLimitedError", - "RedactedClassification", - "Reputation", - "ReputationResponse", - "ReputationStatus", - "Score", "SessionCreateNextSteps", "SessionCreateRequest", "SessionCreateResponse", @@ -126,7 +95,6 @@ "SignerSanctionsClear", "SignerSanctionsHit", "SignerSanctionsUnavailable", - "Subject", "TimeoutError", "TokenExpiredError", "VerificationLevel", diff --git a/agentscore/client.py b/agentscore/client.py index d8a6c76..9e2abe8 100644 --- a/agentscore/client.py +++ b/agentscore/client.py @@ -141,7 +141,6 @@ def _build_error_from_response(response: httpx.Response) -> AgentScoreError: CredentialRevokeResponse, DecisionPolicy, Network, - ReputationResponse, SessionCreateResponse, SessionPollResponse, Signer, @@ -247,14 +246,6 @@ def _handle_response(self, response: httpx.Response) -> Any: # --- Sync methods --- - def get_reputation(self, address: str, chain: str | None = None) -> ReputationResponse: - """Get cached reputation for an address (free, read-only). Optionally filter by chain.""" - params: dict[str, str] = {} - if chain: - params["chain"] = chain - client = self._get_sync_client() - return self._send_sync(lambda: client.get(f"/v1/reputation/{address}", params=params)) - def assess( self, address: str | None = None, @@ -310,7 +301,7 @@ def create_session( operator_token: str | None = None, kind: Literal["kyc", "sign_in"] | None = None, ) -> SessionCreateResponse: - """Create an assessment session for deferred scoring. + """Create a verification or sign-in session. ``address`` pre-associates the session with a known wallet (EVM ``0x...`` or Solana base58). ``operator_token`` pre-associates with an existing ``opc_...`` — @@ -404,14 +395,6 @@ def associate_wallet( # --- Async methods --- - async def aget_reputation(self, address: str, chain: str | None = None) -> ReputationResponse: - """Get cached reputation for an address (free, read-only). Optionally filter by chain.""" - params: dict[str, str] = {} - if chain: - params["chain"] = chain - client = self._get_async_client() - return await self._send_async(lambda: client.get(f"/v1/reputation/{address}", params=params)) - async def aassess( self, address: str | None = None, @@ -460,7 +443,7 @@ async def acreate_session( operator_token: str | None = None, kind: Literal["kyc", "sign_in"] | None = None, ) -> SessionCreateResponse: - """Create an assessment session for deferred scoring. + """Create a verification or sign-in session. ``address`` pre-associates the session with a known wallet (EVM ``0x...`` or Solana base58). ``operator_token`` pre-associates with an existing ``opc_...`` — diff --git a/agentscore/types.py b/agentscore/types.py index 97cc402..a4af7cc 100644 --- a/agentscore/types.py +++ b/agentscore/types.py @@ -2,144 +2,9 @@ from typing import Literal, NotRequired, TypedDict -Grade = Literal["A", "B", "C", "D", "F"] -EntityType = Literal["agent", "service", "hybrid", "wallet", "bot", "unknown", "individual", "entity"] -ReputationStatus = Literal["scored", "stale", "known_unscored"] VerificationLevel = Literal["none", "wallet_claimed", "kyc_verified"] -class _SubjectRequired(TypedDict): - chains: list[str] - - -class Subject(_SubjectRequired, total=False): - address: str - credential_prefix: str - - -class Classification(TypedDict): - entity_type: EntityType - confidence: float | None - is_known: bool - is_known_erc8004_agent: bool - has_candidate_payment_activity: bool - has_verified_payment_activity: bool - reasons: list[str] - - -class Score(TypedDict): - value: float | None - grade: Grade | None - scored_at: str | None - status: ReputationStatus - version: str - - -class _ChainScoreRequired(TypedDict): - value: float | None - grade: Grade | None - scored_at: str | None - status: ReputationStatus - version: str - - -class ChainScore(_ChainScoreRequired, total=False): - confidence: float | None - dimensions: dict[str, float] | None - - -class Identity(TypedDict): - ens_name: str | None - website_url: str | None - github_url: str | None - - -class Activity(TypedDict): - total_candidate_transactions: int - total_verified_transactions: int - as_candidate_payer: int - as_candidate_payee: int - as_verified_payer: int - as_verified_payee: int - counterparties_count: int - active_days: int - active_months: int - first_candidate_tx_at: str | None - last_candidate_tx_at: str | None - first_verified_tx_at: str | None - last_verified_tx_at: str | None - - -class Reputation(TypedDict): - feedback_count: int - client_count: int - trust_avg: float | None - uptime_avg: float | None - activity_avg: float | None - last_feedback_at: str | None - - -class EvidenceSummary(TypedDict): - metadata_kind: str | None - has_a2a_agent_card: bool - website_url: str | None - website_reachable: bool - website_mentions_mcp: bool - website_mentions_x402: bool - github_url: str | None - github_stars: int | None - - -class RedactedClassification(TypedDict): - entity_type: EntityType - - -class _OperatorScoreRequired(TypedDict): - score: float - grade: Grade - - -class OperatorScore(_OperatorScoreRequired, total=False): - agent_count: int - chains_active: list[str] - - -class AgentSummary(TypedDict): - token_id: int - chain: str - name: str | None - score: float - grade: Grade - - -class _ChainEntryRequired(TypedDict): - chain: str - score: ChainScore - classification: Classification | RedactedClassification - - -class ChainEntry(_ChainEntryRequired, total=False): - identity: Identity - activity: Activity - evidence_summary: EvidenceSummary - - -class _ReputationResponseRequired(TypedDict): - subject: Subject - score: Score - chains: list[ChainEntry] - data_semantics: str - caveats: list[str] - updated_at: str | None - - -class ReputationResponse(_ReputationResponseRequired, total=False): - reputation: Reputation - operator_score: OperatorScore - agents: list[AgentSummary] - verification_level: VerificationLevel - - class _OperatorVerificationRequired(TypedDict): level: VerificationLevel diff --git a/pyproject.toml b/pyproject.toml index 11be88e..c421ff1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "hatchling.build" [project] name = "agentscore-py" -version = "2.6.11" +version = "2.7.0" description = "Python client for the AgentScore APIs" readme = "README.md" license = "MIT" requires-python = ">=3.11" -keywords = ["agentscore", "x402", "erc-8004", "agent", "trust", "reputation", "wallet", "blockchain", "ai-agent", "agentic-payments"] +keywords = ["agentscore", "x402", "agent", "trust", "wallet", "blockchain", "ai-agent", "agentic-payments"] dependencies = [ "httpx>=0.25.0,<1.0.0", ] diff --git a/tests/test_client.py b/tests/test_client.py index 2c096a3..e2dacd6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -54,95 +54,55 @@ def test_constructor_default_timeout(): # --------------------------------------------------------------------------- -# get_reputation +# Request handling (shared by every method; exercised through list_credentials) # --------------------------------------------------------------------------- -REPUTATION_PAYLOAD = { - "subject": {"chains": ["base"], "address": ADDRESS}, - "score": { - "value": 75, - "grade": "B", - "scored_at": "2024-01-01T00:00:00Z", - "status": "scored", - "version": "1", - }, - "chains": [ - { - "chain": "base", - "score": {"value": 75, "grade": "B"}, - "classification": {"entity_type": "wallet", "confidence": 0.9}, - "identity": {}, - "activity": {}, - "evidence_summary": {}, - }, - ], - "data_semantics": "live", - "caveats": [], - "updated_at": "2024-01-01T00:00:00Z", -} - - -@respx.mock -def test_get_reputation_success(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - result = client.get_reputation(ADDRESS) - assert result["score"]["grade"] == "B" - assert result["subject"]["address"] == ADDRESS +EMPTY_CREDENTIAL_LIST = {"credentials": []} -@respx.mock -def test_get_reputation_no_chain_param(): - route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_PAYLOAD) - ) - client = AgentScore(api_key=API_KEY) - client.get_reputation(ADDRESS) - assert "chain" not in str(route.calls.last.request.url) +def test_reputation_lookup_is_removed(): + assert not hasattr(AgentScore, "get_reputation") + assert not hasattr(AgentScore, "aget_reputation") @respx.mock -def test_get_reputation_with_chain(): - route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_PAYLOAD) - ) +def test_request_returns_parsed_body(): + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST)) client = AgentScore(api_key=API_KEY) - client.get_reputation(ADDRESS, chain="base") - assert "chain=base" in str(route.calls.last.request.url) + result = client.list_credentials() + assert result == EMPTY_CREDENTIAL_LIST @respx.mock -def test_get_reputation_raises_on_404(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( +def test_request_raises_on_404(): + respx.get(f"{BASE_URL}/v1/credentials").mock( return_value=httpx.Response(404, json={"error": {"code": "not_found", "message": "Address not found"}}) ) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - client.get_reputation(ADDRESS) + client.list_credentials() assert exc_info.value.status_code == 404 assert exc_info.value.code == "not_found" @respx.mock -def test_get_reputation_raises_on_401(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( +def test_request_raises_on_401(): + respx.get(f"{BASE_URL}/v1/credentials").mock( return_value=httpx.Response(401, json={"error": {"code": "unauthorized", "message": "Invalid API key"}}) ) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - client.get_reputation(ADDRESS) + client.list_credentials() assert exc_info.value.status_code == 401 assert exc_info.value.code == "unauthorized" @respx.mock -def test_get_reputation_raises_on_500_non_json(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(500, text="Internal Server Error") - ) +def test_request_raises_on_500_non_json(): + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(500, text="Internal Server Error")) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - client.get_reputation(ADDRESS) + client.list_credentials() assert exc_info.value.status_code == 500 assert exc_info.value.code == "unknown_error" @@ -152,10 +112,13 @@ def test_get_reputation_raises_on_500_non_json(): # --------------------------------------------------------------------------- ASSESS_PAYLOAD = { - **REPUTATION_PAYLOAD, "decision": "allow", "decision_reasons": [], - "on_the_fly": True, + "policy_result": None, + "explanation": [], + "identity_method": "wallet", + "operator_verification": {"level": "none"}, + "resolved_operator": None, } @@ -165,7 +128,7 @@ def test_assess_success(): client = AgentScore(api_key=API_KEY) result = client.assess(ADDRESS) assert result["decision"] == "allow" - assert result["on_the_fly"] is True + assert result["identity_method"] == "wallet" @respx.mock @@ -223,22 +186,12 @@ def test_assess_raises_on_402(): # API key header # --------------------------------------------------------------------------- -REPUTATION_PAYLOAD_SIMPLE = { - "subject": {"chains": ["base"], "address": ADDRESS}, - "score": {"value": 75, "grade": "B", "scored_at": "2024-01-01T00:00:00Z", "status": "scored", "version": "1"}, - "data_semantics": "live", - "caveats": [], - "updated_at": "2024-01-01T00:00:00Z", -} - @respx.mock def test_auth_header_is_sent(): - route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_PAYLOAD_SIMPLE) - ) + route = respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST)) client = AgentScore(api_key="my-secret-key") - client.get_reputation(ADDRESS) + client.list_credentials() assert route.calls.last.request.headers["x-api-key"] == "my-secret-key" @@ -249,12 +202,10 @@ def test_auth_header_is_sent(): @respx.mock def test_error_missing_error_key_falls_back(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(400, json={"message": "bad request"}) - ) + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(400, json={"message": "bad request"})) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - client.get_reputation(ADDRESS) + client.list_credentials() assert exc_info.value.status_code == 400 assert exc_info.value.code == "unknown_error" @@ -504,30 +455,29 @@ async def test_aassess_returns_aip_provenance(): # --------------------------------------------------------------------------- -# Async: aget_reputation +# Async request handling # --------------------------------------------------------------------------- @pytest.mark.asyncio @respx.mock -async def test_aget_reputation_success(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) +async def test_async_request_returns_parsed_body(): + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST)) client = AgentScore(api_key=API_KEY) - result = await client.aget_reputation(ADDRESS) - assert result["score"]["grade"] == "B" - assert result["subject"]["address"] == ADDRESS + result = await client.alist_credentials() + assert result == EMPTY_CREDENTIAL_LIST await client.aclose() @pytest.mark.asyncio @respx.mock -async def test_aget_reputation_raises_on_error(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( +async def test_async_request_raises_on_error(): + respx.get(f"{BASE_URL}/v1/credentials").mock( return_value=httpx.Response(404, json={"error": {"code": "not_found", "message": "Not found"}}) ) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - await client.aget_reputation(ADDRESS) + await client.alist_credentials() assert exc_info.value.status_code == 404 assert exc_info.value.code == "not_found" await client.aclose() @@ -545,7 +495,7 @@ async def test_aassess_success(): client = AgentScore(api_key=API_KEY) result = await client.aassess(ADDRESS) assert result["decision"] == "allow" - assert result["on_the_fly"] is True + assert result["identity_method"] == "wallet" await client.aclose() @@ -556,32 +506,32 @@ async def test_aassess_success(): @respx.mock def test_sync_context_manager(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST)) with AgentScore(api_key=API_KEY) as client: - result = client.get_reputation(ADDRESS) - assert result["score"]["grade"] == "B" + result = client.list_credentials() + assert result["credentials"] == [] assert client._sync_client is None @pytest.mark.asyncio @respx.mock async def test_async_context_manager(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST)) async with AgentScore(api_key=API_KEY) as client: - result = await client.aget_reputation(ADDRESS) - assert result["score"]["grade"] == "B" + result = await client.alist_credentials() + assert result["credentials"] == [] assert client._async_client is None @respx.mock def test_success_response_with_invalid_json(): """A 200 response with non-JSON body should raise AgentScoreError.""" - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( + respx.get(f"{BASE_URL}/v1/credentials").mock( return_value=httpx.Response(200, text="not json"), ) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - client.get_reputation(ADDRESS) + client.list_credentials() assert exc_info.value.code == "invalid_response" assert exc_info.value.status_code == 200 @@ -606,11 +556,11 @@ def test_user_agent_header_includes_version(): """User-Agent header should include package version.""" from importlib.metadata import version - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_PAYLOAD), + respx.get(f"{BASE_URL}/v1/credentials").mock( + return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST), ) client = AgentScore(api_key=API_KEY) - client.get_reputation(ADDRESS) + client.list_credentials() request = respx.calls[0].request assert request.headers["user-agent"] == f"agentscore-py/{version('agentscore-py')}" @@ -620,11 +570,11 @@ def test_user_agent_header_prepends_custom_user_agent(): """Custom user_agent should be rendered as '{custom} ({default})'.""" from importlib.metadata import version - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_PAYLOAD), + respx.get(f"{BASE_URL}/v1/credentials").mock( + return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST), ) client = AgentScore(api_key=API_KEY, user_agent="my-app/1.2.3") - client.get_reputation(ADDRESS) + client.list_credentials() request = respx.calls[0].request expected = f"my-app/1.2.3 (agentscore-py/{version('agentscore-py')})" assert request.headers["user-agent"] == expected @@ -658,9 +608,9 @@ def test_assess_refresh_omitted_when_default(): @respx.mock def test_double_close(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST)) client = AgentScore(api_key=API_KEY) - client.get_reputation(ADDRESS) + client.list_credentials() client.close() client.close() assert client._sync_client is None @@ -669,9 +619,9 @@ def test_double_close(): @pytest.mark.asyncio @respx.mock async def test_double_aclose(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST)) client = AgentScore(api_key=API_KEY) - await client.aget_reputation(ADDRESS) + await client.alist_credentials() await client.aclose() await client.aclose() assert client._async_client is None @@ -682,29 +632,19 @@ async def test_double_aclose(): async def test_concurrent_async_calls(): import asyncio - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=EMPTY_CREDENTIAL_LIST)) client = AgentScore(api_key=API_KEY) results = await asyncio.gather( - client.aget_reputation(ADDRESS), - client.aget_reputation(ADDRESS), - client.aget_reputation(ADDRESS), + client.alist_credentials(), + client.alist_credentials(), + client.alist_credentials(), ) assert len(results) == 3 for r in results: - assert r["score"]["grade"] == "B" + assert r["credentials"] == [] await client.aclose() -@respx.mock -def test_empty_chain_string_not_included_in_params(): - route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_PAYLOAD) - ) - client = AgentScore(api_key=API_KEY) - client.get_reputation(ADDRESS, chain="") - assert "chain" not in str(route.calls.last.request.url) - - @respx.mock def test_assess_empty_policy_dict_included_in_body(): route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD)) @@ -719,10 +659,10 @@ def test_assess_empty_policy_dict_included_in_body(): def test_timeout_error_raises_agentscore_error(): from agentscore.errors import TimeoutError as AgentScoreTimeoutError - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(side_effect=httpx.TimeoutException("timed out")) + respx.get(f"{BASE_URL}/v1/credentials").mock(side_effect=httpx.TimeoutException("timed out")) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreTimeoutError) as exc_info: - client.get_reputation(ADDRESS) + client.list_credentials() # TimeoutError subclasses AgentScoreError so existing `except AgentScoreError` blocks still catch it. assert isinstance(exc_info.value, AgentScoreError) assert exc_info.value.code == "timeout" @@ -730,10 +670,10 @@ def test_timeout_error_raises_agentscore_error(): @respx.mock def test_connect_error_raises_agentscore_error(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(side_effect=httpx.ConnectError("connection refused")) + respx.get(f"{BASE_URL}/v1/credentials").mock(side_effect=httpx.ConnectError("connection refused")) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - client.get_reputation(ADDRESS) + client.list_credentials() # All httpx-layer errors (Timeout, Connect, Protocol, Network) are wrapped — parity with node-sdk. # ConnectError specifically maps to network_error; TimeoutException is the only one that becomes TimeoutError. assert exc_info.value.code == "network_error" @@ -742,12 +682,10 @@ def test_connect_error_raises_agentscore_error(): @respx.mock def test_error_response_no_error_key_fallback(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(422, json={"detail": "validation failed"}) - ) + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(422, json={"detail": "validation failed"})) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - client.get_reputation(ADDRESS) + client.list_credentials() assert exc_info.value.status_code == 422 assert exc_info.value.code == "unknown_error" @@ -757,11 +695,6 @@ def test_error_response_no_error_key_fallback(): # --------------------------------------------------------------------------- -REPUTATION_WITH_VERIFICATION = { - **REPUTATION_PAYLOAD, - "verification_level": "kyc_verified", -} - ASSESS_WITH_COMPLIANCE = { **ASSESS_PAYLOAD, "decision": "deny", @@ -777,24 +710,6 @@ def test_error_response_no_error_key_fallback(): } -@respx.mock -def test_get_reputation_returns_verification_level(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_WITH_VERIFICATION) - ) - client = AgentScore(api_key=API_KEY) - result = client.get_reputation(ADDRESS) - assert result["verification_level"] == "kyc_verified" - - -@respx.mock -def test_get_reputation_omits_verification_level_when_absent(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - result = client.get_reputation(ADDRESS) - assert "verification_level" not in result - - @respx.mock def test_assess_returns_operator_verification(): respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)) @@ -825,9 +740,8 @@ def test_assess_omits_verification_fields_when_absent(): respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD)) client = AgentScore(api_key=API_KEY) result = client.assess(ADDRESS) - assert "operator_verification" not in result assert "verify_url" not in result - assert "resolved_operator" not in result + assert "linked_wallets" not in result @respx.mock @@ -848,18 +762,6 @@ def test_assess_sends_compliance_policy_fields(): assert body["policy"]["blocked_jurisdictions"] == ["KP", "IR"] -@pytest.mark.asyncio -@respx.mock -async def test_aget_reputation_returns_verification_level(): - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_WITH_VERIFICATION) - ) - client = AgentScore(api_key=API_KEY) - result = await client.aget_reputation(ADDRESS) - assert result["verification_level"] == "kyc_verified" - await client.aclose() - - @pytest.mark.asyncio @respx.mock async def test_aassess_returns_compliance_fields(): @@ -881,10 +783,9 @@ async def test_aassess_returns_compliance_fields(): def test_full_compliance_deny_flow(): """Full assess flow with compliance policy returning deny + verify_url.""" compliance_response = { - **REPUTATION_PAYLOAD, + **ASSESS_PAYLOAD, "decision": "deny", "decision_reasons": ["kyc_required", "sanctions_flagged"], - "on_the_fly": False, "operator_verification": { "level": "none", "operator_type": None, @@ -1117,16 +1018,8 @@ def test_error_response_with_no_extra_fields_yields_empty_details(): SESSION_POLL_COMPLETE_PAYLOAD = { "session_id": "ses_abc123", "status": "complete", - "score": { - "value": 80, - "grade": "B", - "scored_at": "2024-01-01T00:00:00Z", - "status": "scored", - "version": "1", - }, - "decision": "allow", - "decision_reasons": [], - "subject": {"chains": ["base"], "address": ADDRESS}, + "operator_token": "opc_test123", + "completed_at": "2024-01-01T00:00:00Z", } @@ -1149,8 +1042,7 @@ def test_poll_session_complete(): client = AgentScore(api_key=API_KEY) result = client.poll_session("ses_abc123", "ps_secret456") assert result["status"] == "complete" - assert result["score"]["grade"] == "B" - assert result["decision"] == "allow" + assert result["operator_token"] == "opc_test123" @respx.mock @@ -1382,7 +1274,7 @@ async def test_apoll_session_success(): client = AgentScore(api_key=API_KEY) result = await client.apoll_session("ses_abc123", "ps_secret456") assert result["status"] == "complete" - assert result["score"]["grade"] == "B" + assert result["operator_token"] == "opc_test123" await client.aclose() @@ -2405,33 +2297,21 @@ def test_error_body_error_field_not_dict_keeps_defaults(): def test_sync_client_is_reused_across_calls(): """The second call must reuse the cached httpx.Client instance (the `is not None` branch of `_get_sync_client`).""" - respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) + respx.get(f"{BASE_URL}/v1/credentials").mock(return_value=httpx.Response(200, json=CREDENTIAL_LIST_PAYLOAD)) client = AgentScore(api_key=API_KEY) - client.get_reputation(ADDRESS) + client.list_credentials() first = client._sync_client assert first is not None - client.get_reputation(ADDRESS) + client.list_credentials() assert client._sync_client is first client.close() # --------------------------------------------------------------------------- -# Branch coverage: aget_reputation chain param + aassess optional-arg branches +# Branch coverage: aassess optional-arg branches # --------------------------------------------------------------------------- -@pytest.mark.asyncio -@respx.mock -async def test_aget_reputation_with_chain(): - route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( - return_value=httpx.Response(200, json=REPUTATION_PAYLOAD) - ) - client = AgentScore(api_key=API_KEY) - await client.aget_reputation(ADDRESS, chain="base") - assert "chain=base" in str(route.calls.last.request.url) - await client.aclose() - - @pytest.mark.asyncio @respx.mock async def test_aassess_forwards_chain_refresh_and_policy(): @@ -2442,12 +2322,12 @@ async def test_aassess_forwards_chain_refresh_and_policy(): address=ADDRESS, chain="base", refresh=False, - policy={"min_score": 50}, + policy={"require_kyc": True}, ) body = json.loads(route.calls.last.request.content) assert body["chain"] == "base" assert body["refresh"] is False - assert body["policy"] == {"min_score": 50} + assert body["policy"] == {"require_kyc": True} await client.aclose() diff --git a/tests/test_integration.py b/tests/test_integration.py index 5f838b8..eef0f69 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -14,68 +14,6 @@ ) -def test_get_reputation_shape(): - client = AgentScore(api_key=API_KEY, base_url=BASE_URL) - rep = client.get_reputation(TEST_ADDRESS) - - assert "chains" in rep["subject"] - assert isinstance(rep["subject"]["chains"], list) - assert len(rep["subject"]["chains"]) > 0 - - assert "value" in rep["score"] - assert "grade" in rep["score"] - assert "scored_at" in rep["score"] - assert "status" in rep["score"] - assert "version" in rep["score"] - assert "confidence" not in rep["score"] - assert "dimensions" not in rep["score"] - - assert "chains" in rep - assert isinstance(rep["chains"], list) - assert len(rep["chains"]) > 0 - - chain = rep["chains"][0] - assert "chain" in chain - assert "score" in chain - assert "classification" in chain - assert "identity" in chain - assert "activity" in chain - assert "evidence_summary" in chain - - assert "agents" in rep - assert isinstance(rep["agents"], list) - - -def test_get_reputation_chain_filter(): - client = AgentScore(api_key=API_KEY, base_url=BASE_URL) - rep = client.get_reputation(TEST_ADDRESS, chain="base") - - assert rep["subject"]["chains"] == ["base"] - assert len(rep["chains"]) == 1 - assert rep["chains"][0]["chain"] == "base" - - -def test_get_reputation_chain_entry_full_fields(): - client = AgentScore(api_key=API_KEY, base_url=BASE_URL) - rep = client.get_reputation(TEST_ADDRESS) - chain = rep["chains"][0] - - assert "confidence" in chain["score"] - assert "dimensions" in chain["score"] - assert "as_verified_payer" in chain["activity"] - assert "active_days" in chain["activity"] - assert "first_candidate_tx_at" in chain["activity"] - - -def test_get_reputation_metadata_fields(): - client = AgentScore(api_key=API_KEY, base_url=BASE_URL) - rep = client.get_reputation(TEST_ADDRESS) - - assert "caveats" in rep - assert "data_semantics" in rep - assert "updated_at" in rep - - def test_assess_flat_decision_shape(): client = AgentScore(api_key=API_KEY, base_url=BASE_URL) result = client.assess(TEST_ADDRESS) @@ -94,26 +32,3 @@ def test_assess_policy_deny(): assert "kyc_required" in result["decision_reasons"] assert "verify_url" in result assert "/verify" in result["verify_url"] - - -def test_get_reputation_operator_score(): - client = AgentScore(api_key=API_KEY, base_url=BASE_URL) - rep = client.get_reputation(TEST_ADDRESS) - op = rep.get("operator_score") - if not op: - pytest.skip("no operator_score on test address") - assert isinstance(op["score"], int) - assert isinstance(op["grade"], str) - assert isinstance(op["agent_count"], int) - assert isinstance(op["chains_active"], list) - - -def test_assess_then_get_reputation(): - client = AgentScore(api_key=API_KEY, base_url=BASE_URL) - assessed = client.assess(TEST_ADDRESS) - assert "decision" in assessed - - rep = client.get_reputation(TEST_ADDRESS) - assert "value" in rep["score"] - assert isinstance(rep["score"]["value"], (int, float)) - assert rep["subject"]["address"].lower() == TEST_ADDRESS.lower() diff --git a/uv.lock b/uv.lock index 4ead3dc..30b6c1b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.11" [options] @@ -7,7 +7,7 @@ prerelease-mode = "disallow" [[package]] name = "agentscore-py" -version = "2.6.11" +version = "2.7.0" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -260,15 +260,15 @@ wheels = [ [[package]] name = "lefthook" -version = "2.1.14" +version = "2.1.16" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/54/9b67d7b19bc4e61345065d53ce103d3ab81dc1dc5a5e35695af076c1506c/lefthook-2.1.14-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:e60ec36d5ec6094bea11c063aa09f11972509d6f6647d82ad1ee12d61f0859a2", size = 5427421, upload-time = "2026-09-14T10:26:00.346Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a0/72c430c1e261f11c67f57befb52432f8635ce8268dc0f7f1013125c803f0/lefthook-2.1.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dd628f4b7436d825758ed98fc5e52c5ed3c782e1bbf4c623fc7d3b45ec3216dc", size = 4950978, upload-time = "2026-09-14T10:26:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/92/df/b44d9c547d45337e6bff854143c70579b34efea9076e20cd5b7c2dbe910d/lefthook-2.1.14-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:fd2aecae214cdcbb6336485e01fe114a002d78a80dbb49907b9f9f039d57d840", size = 4782004, upload-time = "2026-09-14T10:26:03.733Z" }, - { url = "https://files.pythonhosted.org/packages/9a/04/216ca938c50e5b5d199b74157217b3963d1de166bf82ee32fab510d0a89d/lefthook-2.1.14-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:ee4ea57cbf07fdaa6faca8e18ba1b09e3246b1d7d8fc1a9c411299bbe7c24bae", size = 5361787, upload-time = "2026-09-14T10:26:05.164Z" }, - { url = "https://files.pythonhosted.org/packages/91/63/1e1c38fa8e590f23d22df778425a3783d9f1df1137cc9bf7125240cbf6a4/lefthook-2.1.14-py3-none-win_amd64.whl", hash = "sha256:d28d43e98c42e723e92e2045c9b40e7908f128f63eec1ad3ebd37a0e9dc5cc74", size = 5509521, upload-time = "2026-09-14T10:26:06.699Z" }, - { url = "https://files.pythonhosted.org/packages/de/0c/8fc504b2c8222faf8877ffb597ab007201e296e73d99602b21c2a57934ac/lefthook-2.1.14-py3-none-win_arm64.whl", hash = "sha256:2be15ca9164f5921e5170d522624a3dc6033ae38b4a8a90ba05598d22803876c", size = 4861855, upload-time = "2026-09-14T10:26:07.947Z" }, + { url = "https://files.pythonhosted.org/packages/76/bc/1a5b4e112cc34d236e4e40333394fd49cf7dcf2ab95045aff7f987a83d71/lefthook-2.1.16-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:15b9eefd27e0e75e1e32a4fc082dfe38b89e8b7baaec3f4dcdc464966d22b903", size = 5430199, upload-time = "2026-10-01T17:30:16.027Z" }, + { url = "https://files.pythonhosted.org/packages/a8/65/e5f016ac3589cd7d878dd49c28a7b42705330eaf5b5877e120056ec17153/lefthook-2.1.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54ba55d82a60ef2389da78ab59ad231e6ae93380a9982a8e94d85cee5707d68c", size = 4953571, upload-time = "2026-10-01T17:30:18.016Z" }, + { url = "https://files.pythonhosted.org/packages/98/f3/d9681ea064ab20b0d13b0dc7f1f4dbbac080fd44664a13665345ec852554/lefthook-2.1.16-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:0d744b4c282283d8d33f04a39f9d0fcb364343c2f1d9eb716a106d21e9013fe2", size = 4784711, upload-time = "2026-10-01T17:30:19.683Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/8117f851d8524bd900264b6a098cfc6676562c94594b4f7b5c64a3884c60/lefthook-2.1.16-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:6dcb06338830eadb9d4ae9d34b5e18cbb1655307401323d708002f0e2ebb2c57", size = 5364835, upload-time = "2026-10-01T17:30:21.233Z" }, + { url = "https://files.pythonhosted.org/packages/45/0a/348d164e62af3c01a60c11933f8fb6420f48fbdf4e6cc41286aae72586f9/lefthook-2.1.16-py3-none-win_amd64.whl", hash = "sha256:5c85a1be88a3dda928ccb80c70536ecc997a1707421ad7d4e8d7520c75a423bf", size = 5511450, upload-time = "2026-10-01T17:30:22.934Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0e/1067788d8013f471bde4565d2db8c4414c787f3cc0ea90e8b6e67a7a3a2e/lefthook-2.1.16-py3-none-win_arm64.whl", hash = "sha256:ee2b72bf44649b514aec8773ad8bbe981a804682a561356fd65054e0a5d56b5f", size = 4864565, upload-time = "2026-10-01T17:30:24.764Z" }, ] [[package]] @@ -343,11 +343,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.3" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, ] [[package]] @@ -364,27 +364,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.9" +version = "0.16.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" }, - { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" }, - { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" }, - { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" }, - { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" }, - { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" }, - { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" }, - { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" }, - { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" }, - { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" }, - { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" }, - { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" }, - { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, ] [[package]]