From cbe5b1a23f2365b70735ecac3e0530c81898856d Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 2 Oct 2026 11:12:23 -0700 Subject: [PATCH] Remove get_reputation, take dependency patches, release 2.7.0 GET /v1/reputation was retired with the transaction pipeline and now answers 404, so the sync and async methods and the score, grade, chain and reputation types that only they returned come out. Test fixtures now carry the shapes the API returns. --- .claude/CLAUDE.md | 1 - .github/workflows/ci.yml | 4 +- .github/workflows/publish.yml | 2 +- .github/workflows/security.yml | 2 +- README.md | 19 +-- agentscore/__init__.py | 32 ---- agentscore/client.py | 21 +-- agentscore/types.py | 135 ---------------- pyproject.toml | 4 +- tests/test_client.py | 276 ++++++++++----------------------- tests/test_integration.py | 85 ---------- uv.lock | 62 ++++---- 12 files changed, 122 insertions(+), 521 deletions(-) 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]]