From ac8983825dac6525114604da79f3a036dc6a51bf Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 5 Apr 2026 05:03:45 -0700 Subject: [PATCH 1/5] Remove stats/agents endpoints, fix types for API parity (v1.4.0) - Remove get_stats(), get_agents() and async variants - Fix score value types from int to float (API returns JS Number) - Fix EvidenceSummary to match actual API fields (8 fields) - Add RedactedClassification for free tier chain entries - Make ChainEntry identity optional (free tier redaction) - Make OperatorScore agent_count/chains_active optional (free tier) - Fix ReputationStatus to match API values (scored/stale/known_unscored) - Make AssessResponse.chains required - Remove invalid top-level fields from ReputationResponse - Bump to v1.4.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- .coverage | Bin 0 -> 53248 bytes README.md | 8 -- agentscore/__init__.py | 14 --- agentscore/client.py | 34 +----- agentscore/types.py | 132 +++++++--------------- pyproject.toml | 4 +- tests/test_client.py | 227 ++++++-------------------------------- tests/test_integration.py | 13 --- uv.lock | 45 ++++---- 9 files changed, 104 insertions(+), 373 deletions(-) create mode 100644 .coverage diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..4651b85fb4f3854d3ff1d4ba8a106cd221bf6a84 GIT binary patch literal 53248 zcmeI)+m91f90%~Z(b8_0SzJl8Y{;HO&9b4}+MuYIh^wrykwj52#zf1sJ>8CUX3ETT z***XkNie?n4-j8`^;P3vfe$`H#3v;rniwP|F3I{kbLnN-l685&lJBIK>C8E2e&=&8 z(`k46{>c-j&soiJEyHIAr3p!vrPmpgBq>Grak|G#l2)|%1wG4s>%&%4QvS=Q$Mp}S zOyWaH|73ixKA&06{5f_l^K$C+n3>F_^0WgB1Rwwb2s|+Yos*elc4|t#e8D%$4eq-} zg}dS7_?z!7E}U9qrxxBgvB<)0Y=4^3GC$82nCqNpP42Rq+2G8yt7gUUO?#R7bsnc~ zdAuslS+tGLS|VXS$S>APW|eaJe3^=9x~65gZFZKob3uaWXg*){!wpmjH4|$E}L!%dTNp z>fFmQqg%s@LvgP9VQpQo} z6XuML)u+?Tyh+X|d6coO__t3?Uq$0@=sepM|p zx7;?Hv}Lcp1I-zeixv%YwQTcTi+3A8$9g$)K{aeA$3!Kl2Rt{H$i6ZuOVlg*s&}qI zZ=Fg;%Xfm;C2IUqksdNvcJEAPXJ+K9=YozRcuHl{_qr>oflkxiJf!Q0{loi>7$t+9 zM2w3|x(Nl7u3&kzD$ut`8vbf>3+;}2+*d!U}=*^Px7>V{!b>@?a?85Z0>O|>E#b{+` zC#fZ~d-lkka72iCN!)h~R6=a&4S@|Of~|+&H2gYyFm|z-0-xQH$iBK~Sl}XrQc*l6 zu1qLF$X}>|zD1M1xT^ypi!Hs7-{$T46{A73Nz*W08nQG8hi3pL5>^dLY?PgrAM{`G zGU%S-+|!FUETH!~wx?8cL2?@2?>e4m(rHzkw!FjfRyJEVJTVJ-v#jhm4Q|-sxq)&l zd2~$EV?hiLo|TUD%JABvjOCTtAg1`_BBuFMH0a{-MJtwN*wyHKB~HU(sWkVC!6}Xt zP9l_%&C;OsXozkoGcGjRD`u>?{Cty4M5&fh4TrmEl)e(r*ZBj*g=eFqhR=P|;!Gq7 zf(bg-dBbBBm($MZTp9#MV)cztg~c4FJ@FhG)@E*2eOTWSOWBPQWtax<)URfu6NtXRnjJL$fr!Me6Td0=&;qybO;ZX2h`B%OPEy zDtd#Io)Z`Nvc4wK4GRPy009U<00Izz00bZa0SG_<0#BZRDl2kQT>mTjFOvQTJz#+V z1Rwwb2tWV=5P$##AOHafKwu;VGK#uSkN+&>`3YH_o{Ihk;H7-=VE)B4)k@KCOZsj7 z-bhvuK|=rn5P$##AOHafKmY;|fB*y_&=tt2`{ejz041$Xr=m{*#P|Q&t{am6o!-!& zp%p9;fB*y_009U<00Izz00bZafzc9pOVgyz?!sx0etV#>Vz@24aFnm`hSRjT?HA}r zLTulopXcF)X4|hj_Pkd;+rQOsH@Qc@LvT!`awfN54tHJ04GK~ot)NQ7q>w41C7BA! zZa<<@$+S(sRHCB9_y5|io09&$Zt2g`3Kj@J00Izz00bZa0SG_<0uX?}hzT6iI=kiV zUnR%a|Jre_Gx-SRbg%!_M=Yp&{jV%(oos6RHS4?n$M64*Sf@s~5P$##AOHafKmY;| zfB*y_0D=D|pvtN=F24Vl_4^Xtus{F;5P$##AOHafKmY;|fB*y_@B|8|>Ub*n|9|}l zN&j2Fq5niHSReoa2tWV=5P$##AOHafKmY;|_}>UjX_}mRLA!J9(#4C{@2=nd?TB~% z$Gi92U$3_lD(%{(zP4AP=R?XzJHB08yZ_I5gb4u7Tu z|NpQ5D(P$bU;6K~f&~H)fB*y_009U<00Izz00bZafe{r*&?f=1rUg%`8ayeAMjrtL z-~US!BU&wl4FL#100Izz00bZa0SG_<0uX?}mITE2|G56&5)~3b00Izz00bZa0SG_< r0uX=z1V&as{Qp0$|3|j@5I6)N009U<00Izz00bZa0SG`~O9KA_D@ruM literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 5246891..7597fc6 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,6 @@ base_rep = client.get_reputation("0x1234...", chain="base") # On-the-fly assessment with policy (paid) result = client.assess("0x1234...", policy={"min_grade": "B", "min_score": 35}) print(result["decision"], result["decision_reasons"]) - -# Browse agents -agents = client.get_agents(chain="base", limit=10) -print(len(agents["items"]), agents["count"]) - -# Ecosystem stats -stats = client.get_stats() -print(stats["erc8004"]["known_agents"]) ``` ### Async diff --git a/agentscore/__init__.py b/agentscore/__init__.py index 346a821..af675ce 100644 --- a/agentscore/__init__.py +++ b/agentscore/__init__.py @@ -3,40 +3,26 @@ from agentscore.client import AgentScore from agentscore.errors import AgentScoreError from agentscore.types import ( - AgentRecord, - AgentsListResponse, AssessResponse, DecisionPolicy, EntityType, Grade, Reputation, ReputationResponse, - ReputationResponseFull, ReputationStatus, - StatsERC8004, - StatsPayments, - StatsReputation, - StatsResponse, ) __version__ = _pkg_version("agentscore-py") __all__ = [ - "AgentRecord", "AgentScore", "AgentScoreError", - "AgentsListResponse", "AssessResponse", "DecisionPolicy", "EntityType", "Grade", "Reputation", "ReputationResponse", - "ReputationResponseFull", "ReputationStatus", - "StatsERC8004", - "StatsPayments", - "StatsReputation", - "StatsResponse", "__version__", ] diff --git a/agentscore/client.py b/agentscore/client.py index 01852ec..e0a269f 100644 --- a/agentscore/client.py +++ b/agentscore/client.py @@ -9,11 +9,9 @@ if TYPE_CHECKING: from agentscore.types import ( - AgentsListResponse, AssessResponse, DecisionPolicy, - ReputationResponseFull, - StatsResponse, + ReputationResponse, ) @@ -86,7 +84,7 @@ def _handle_response(self, response: httpx.Response) -> dict: # --- Sync methods --- - def get_reputation(self, address: str, chain: str | None = None) -> ReputationResponseFull: + 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: @@ -114,22 +112,9 @@ def assess( response = client.post("/v1/assess", json=body) return self._handle_response(response) - def get_agents(self, **filters: Any) -> AgentsListResponse: - """Browse agents (free).""" - params = {k: str(v).lower() if isinstance(v, bool) else str(v) for k, v in filters.items() if v is not None} - client = self._get_sync_client() - response = client.get("/v1/agents", params=params) - return self._handle_response(response) - - def get_stats(self) -> StatsResponse: - """Get ecosystem stats (free).""" - client = self._get_sync_client() - response = client.get("/v1/stats") - return self._handle_response(response) - # --- Async methods --- - async def aget_reputation(self, address: str, chain: str | None = None) -> ReputationResponseFull: + 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: @@ -157,19 +142,6 @@ async def aassess( response = await client.post("/v1/assess", json=body) return self._handle_response(response) - async def aget_agents(self, **filters: Any) -> AgentsListResponse: - """Browse agents (free).""" - params = {k: str(v).lower() if isinstance(v, bool) else str(v) for k, v in filters.items() if v is not None} - client = self._get_async_client() - response = await client.get("/v1/agents", params=params) - return self._handle_response(response) - - async def aget_stats(self) -> StatsResponse: - """Get ecosystem stats (free).""" - client = self._get_async_client() - response = await client.get("/v1/stats") - return self._handle_response(response) - def close(self): if self._sync_client: self._sync_client.close() diff --git a/agentscore/types.py b/agentscore/types.py index 9c99a86..7204994 100644 --- a/agentscore/types.py +++ b/agentscore/types.py @@ -4,7 +4,7 @@ Grade = Literal["A", "B", "C", "D", "F"] EntityType = Literal["agent", "service", "hybrid", "wallet", "bot", "unknown"] -ReputationStatus = Literal["unknown", "known_unscored", "scored", "stale", "indexing"] +ReputationStatus = Literal["scored", "stale", "known_unscored"] class Subject(TypedDict): @@ -23,23 +23,26 @@ class Classification(TypedDict): class Score(TypedDict): - value: int | None + value: float | None grade: Grade | None scored_at: str | None status: ReputationStatus version: str -class ChainScore(TypedDict): - value: int | None +class _ChainScoreRequired(TypedDict): + value: float | None grade: Grade | None - confidence: float | None - dimensions: dict[str, float] | 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 @@ -71,36 +74,27 @@ class Reputation(TypedDict): last_feedback_at: str | None -class EvidenceSummary(TypedDict, total=False): - candidate_tx_count: int - confirmed_or_likely_tx: int - endpoint_count: int - github_stars: int - github_url: str | None - has_a2a_agent_card: bool - has_ens: bool - has_github: bool - has_website: bool - healthy_endpoints: int - is_erc8004: bool +class EvidenceSummary(TypedDict): metadata_kind: str | None - metadata_quality: str | None - multi_chain_count: int - reputation_feedback_count: int - reputation_trust_avg: float | None - reputation_uptime_avg: float | None - reputation_activity_avg: float | None - reputation_client_count: int - verified_tx_count: int + has_a2a_agent_card: bool + website_url: str | None + website_reachable: bool website_mentions_mcp: bool website_mentions_x402: bool - website_reachable: bool - website_url: str | None + github_url: str | None + github_stars: int | None + + +class RedactedClassification(TypedDict): + entity_type: EntityType -class OperatorScore(TypedDict): - score: int +class _OperatorScoreRequired(TypedDict): + score: float grade: Grade + + +class OperatorScore(_OperatorScoreRequired, total=False): agent_count: int chains_active: list[str] @@ -109,20 +103,23 @@ class AgentSummary(TypedDict): token_id: int chain: str name: str | None - score: int + score: float grade: Grade -class ChainEntry(TypedDict): +class _ChainEntryRequired(TypedDict): chain: str score: ChainScore - classification: Classification + classification: Classification | RedactedClassification + + +class ChainEntry(_ChainEntryRequired, total=False): identity: Identity activity: Activity evidence_summary: EvidenceSummary -class ReputationResponse(TypedDict): +class _ReputationResponseRequired(TypedDict): subject: Subject score: Score chains: list[ChainEntry] @@ -131,10 +128,10 @@ class ReputationResponse(TypedDict): updated_at: str | None -class ReputationResponseFull(ReputationResponse, total=False): +class ReputationResponse(_ReputationResponseRequired, total=False): + reputation: Reputation operator_score: OperatorScore agents: list[AgentSummary] - reputation: Reputation class DecisionPolicy(TypedDict, total=False): @@ -143,7 +140,7 @@ class DecisionPolicy(TypedDict, total=False): require_verified_payment_activity: bool -class AssessResponse(TypedDict): +class _AssessResponseRequired(TypedDict): subject: Subject score: Score chains: list[ChainEntry] @@ -153,64 +150,9 @@ class AssessResponse(TypedDict): data_semantics: str caveats: list[str] updated_at: str | None + + +class AssessResponse(_AssessResponseRequired, total=False): operator_score: OperatorScore | None reputation: Reputation | None agents: list[AgentSummary] - - -class AgentRecord(TypedDict): - chain: str - token_id: int - owner_address: str - agent_wallet: str | None - name: str | None - description: str | None - metadata_quality: str - score: int | None - grade: Grade | None - entity_type: EntityType | None - endpoint_count: int - website_url: str | None - github_url: str | None - has_candidate_payment_activity: bool - has_verified_payment_activity: bool - agents_sharing_owner: int | None - updated_at: str - - -class AgentsListResponse(TypedDict): - items: list[AgentRecord] - next_cursor: str | None - count: int - version: str - - -class StatsPayments(TypedDict): - addresses_with_candidate_payment_activity: int - addresses_with_verified_payment_activity: int - total_candidate_transactions: int - total_verified_transactions: int - verification_status_summary: dict[str, int] - - -class StatsERC8004(TypedDict): - known_agents: int - by_chain: dict[str, int] - metadata_quality_distribution: dict[str, int] - - -class StatsReputation(TypedDict): - total_addresses: int - scored_addresses: int - entity_distribution: dict[str, int] - score_distribution: dict[str, int] - - -class StatsResponse(TypedDict, total=False): - version: str - as_of_time: str - data_semantics: str - erc8004: StatsERC8004 - reputation: StatsReputation - payments: StatsPayments - caveats: list[str] diff --git a/pyproject.toml b/pyproject.toml index 79b6c16..00ffb73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-py" -version = "1.3.0" +version = "1.4.0" description = "Python client for the AgentScore trust and reputation API" readme = "README.md" license = "MIT" @@ -44,4 +44,6 @@ dev = [ "ruff>=0.11.0", "vulture>=2.15", "pytest-cov>=6.0", + "respx>=0.22.0", + "pytest-asyncio>=1.2.0", ] diff --git a/tests/test_client.py b/tests/test_client.py index dd92d81..7a89685 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -220,140 +220,25 @@ def test_assess_raises_on_402(): # --------------------------------------------------------------------------- -# get_agents -# --------------------------------------------------------------------------- - -AGENTS_PAYLOAD = { - "items": [ - { - "chain": "base", - "token_id": 1, - "owner_address": "0xowner", - "agent_wallet": None, - "name": "Test Agent", - "description": None, - "metadata_quality": "complete", - "score": 80, - "grade": "A", - "entity_type": "agent", - "endpoint_count": 1, - "website_url": None, - "github_url": None, - "has_candidate_payment_activity": True, - "has_verified_payment_activity": True, - "agents_sharing_owner": None, - "updated_at": "2024-01-01T00:00:00Z", - } - ], - "next_cursor": None, - "count": 1, - "version": "1", -} - - -@respx.mock -def test_get_agents_success(): - respx.get(f"{BASE_URL}/v1/agents").mock(return_value=httpx.Response(200, json=AGENTS_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - result = client.get_agents() - assert result["count"] == 1 - assert result["items"][0]["name"] == "Test Agent" - - -@respx.mock -def test_get_agents_passes_filters(): - route = respx.get(f"{BASE_URL}/v1/agents").mock(return_value=httpx.Response(200, json=AGENTS_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - client.get_agents(chain="base", grade="A") - url_str = str(route.calls.last.request.url) - assert "chain=base" in url_str - assert "grade=A" in url_str - - -@respx.mock -def test_get_agents_boolean_query_param_serialization(): - route = respx.get(f"{BASE_URL}/v1/agents").mock(return_value=httpx.Response(200, json=AGENTS_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - client.get_agents(has_verified_payment_activity=True, has_endpoint=False) - url_str = str(route.calls.last.request.url) - assert "has_verified_payment_activity=true" in url_str - assert "has_endpoint=false" in url_str - - -@respx.mock -def test_get_agents_raises_on_error(): - respx.get(f"{BASE_URL}/v1/agents").mock( - return_value=httpx.Response( - 500, - json={"error": {"code": "server_error", "message": "Unexpected error"}}, - ) - ) - client = AgentScore(api_key=API_KEY) - with pytest.raises(AgentScoreError) as exc_info: - client.get_agents() - assert exc_info.value.status_code == 500 - - -# --------------------------------------------------------------------------- -# get_stats +# Authorization header # --------------------------------------------------------------------------- -STATS_PAYLOAD = { - "version": "1", - "as_of_time": "2024-01-01T00:00:00Z", +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", - "erc8004": {"known_agents": 42, "by_chain": {"base": 42}, "metadata_quality_distribution": {}}, - "reputation": { - "total_addresses": 1000, - "scored_addresses": 500, - "entity_distribution": {}, - "score_distribution": {}, - }, - "payments": { - "addresses_with_candidate_payment_activity": 200, - "addresses_with_verified_payment_activity": 100, - "total_candidate_transactions": 5000, - "total_verified_transactions": 3000, - "verification_status_summary": {}, - }, "caveats": [], + "updated_at": "2024-01-01T00:00:00Z", } -@respx.mock -def test_get_stats_success(): - respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - result = client.get_stats() - assert result["erc8004"]["known_agents"] == 42 - assert result["reputation"]["total_addresses"] == 1000 - - -@respx.mock -def test_get_stats_raises_on_error(): - respx.get(f"{BASE_URL}/v1/stats").mock( - return_value=httpx.Response( - 503, - json={"error": {"code": "service_unavailable", "message": "Service down"}}, - ) - ) - client = AgentScore(api_key=API_KEY) - with pytest.raises(AgentScoreError) as exc_info: - client.get_stats() - assert exc_info.value.status_code == 503 - assert exc_info.value.code == "service_unavailable" - - -# --------------------------------------------------------------------------- -# Authorization header -# --------------------------------------------------------------------------- - - @respx.mock def test_auth_header_is_sent(): - route = respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD)) + route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( + return_value=httpx.Response(200, json=REPUTATION_PAYLOAD_SIMPLE) + ) client = AgentScore(api_key="my-secret-key") - client.get_stats() + client.get_reputation(ADDRESS) assert route.calls.last.request.headers["authorization"] == "Bearer my-secret-key" @@ -364,10 +249,12 @@ def test_auth_header_is_sent(): @respx.mock def test_error_missing_error_key_falls_back(): - respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(400, json={"message": "bad request"})) + respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").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_stats() + client.get_reputation(ADDRESS) assert exc_info.value.status_code == 400 assert exc_info.value.code == "unknown_error" @@ -433,38 +320,6 @@ async def test_aassess_success(): await client.aclose() -# --------------------------------------------------------------------------- -# Async: aget_agents -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -@respx.mock -async def test_aget_agents_success(): - respx.get(f"{BASE_URL}/v1/agents").mock(return_value=httpx.Response(200, json=AGENTS_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - result = await client.aget_agents() - assert result["count"] == 1 - assert result["items"][0]["name"] == "Test Agent" - await client.aclose() - - -# --------------------------------------------------------------------------- -# Async: aget_stats -# --------------------------------------------------------------------------- - - -@pytest.mark.asyncio -@respx.mock -async def test_aget_stats_success(): - respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - result = await client.aget_stats() - assert result["erc8004"]["known_agents"] == 42 - assert result["reputation"]["total_addresses"] == 1000 - await client.aclose() - - # --------------------------------------------------------------------------- # Context managers and close # --------------------------------------------------------------------------- @@ -472,34 +327,32 @@ async def test_aget_stats_success(): @respx.mock def test_sync_context_manager(): - respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD)) + respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) with AgentScore(api_key=API_KEY) as client: - result = client.get_stats() - assert result["erc8004"]["known_agents"] == 42 - # After exiting, sync client should be closed + result = client.get_reputation(ADDRESS) + assert result["score"]["grade"] == "B" assert client._sync_client is None @pytest.mark.asyncio @respx.mock async def test_async_context_manager(): - respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD)) + respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)) async with AgentScore(api_key=API_KEY) as client: - result = await client.aget_stats() - assert result["erc8004"]["known_agents"] == 42 - # After exiting, async client should be closed + result = await client.aget_reputation(ADDRESS) + assert result["score"]["grade"] == "B" 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/stats").mock( + respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock( return_value=httpx.Response(200, text="not json"), ) client = AgentScore(api_key=API_KEY) with pytest.raises(AgentScoreError) as exc_info: - client.get_stats() + client.get_reputation(ADDRESS) assert exc_info.value.code == "invalid_response" assert exc_info.value.status_code == 200 @@ -524,11 +377,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/stats").mock( - return_value=httpx.Response(200, json={"erc8004": {"known_agents": 1}}), + 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_stats() + client.get_reputation(ADDRESS) request = respx.calls[0].request assert request.headers["user-agent"] == f"agentscore-py/{version('agentscore-py')}" @@ -538,16 +391,6 @@ def test_user_agent_header_includes_version(): # --------------------------------------------------------------------------- -@respx.mock -def test_get_agents_none_values_excluded_from_params(): - route = respx.get(f"{BASE_URL}/v1/agents").mock(return_value=httpx.Response(200, json=AGENTS_PAYLOAD)) - client = AgentScore(api_key=API_KEY) - client.get_agents(chain="base", limit=None) - url_str = str(route.calls.last.request.url) - assert "chain=base" in url_str - assert "limit" not in url_str - - @respx.mock def test_assess_refresh_false_not_included_in_body(): route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD)) @@ -559,9 +402,9 @@ def test_assess_refresh_false_not_included_in_body(): @respx.mock def test_double_close(): - respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD)) + 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_stats() + client.get_reputation(ADDRESS) client.close() client.close() assert client._sync_client is None @@ -570,9 +413,9 @@ def test_double_close(): @pytest.mark.asyncio @respx.mock async def test_double_aclose(): - respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD)) + 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_stats() + await client.aget_reputation(ADDRESS) await client.aclose() await client.aclose() assert client._async_client is None @@ -618,25 +461,27 @@ def test_assess_empty_policy_dict_included_in_body(): @respx.mock def test_timeout_error_raises_agentscore_error(): - respx.get(f"{BASE_URL}/v1/stats").mock(side_effect=httpx.TimeoutException("timed out")) + respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(side_effect=httpx.TimeoutException("timed out")) client = AgentScore(api_key=API_KEY) with pytest.raises(httpx.TimeoutException): - client.get_stats() + client.get_reputation(ADDRESS) @respx.mock def test_connect_error_raises_agentscore_error(): - respx.get(f"{BASE_URL}/v1/stats").mock(side_effect=httpx.ConnectError("connection refused")) + respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(side_effect=httpx.ConnectError("connection refused")) client = AgentScore(api_key=API_KEY) with pytest.raises(httpx.ConnectError): - client.get_stats() + client.get_reputation(ADDRESS) @respx.mock def test_error_response_no_error_key_fallback(): - respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(422, json={"detail": "validation failed"})) + respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").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_stats() + client.get_reputation(ADDRESS) assert exc_info.value.status_code == 422 assert exc_info.value.code == "unknown_error" diff --git a/tests/test_integration.py b/tests/test_integration.py index 5a7ed38..979c5a4 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -123,16 +123,3 @@ def test_assess_then_get_reputation(): assert "value" in rep["score"] assert isinstance(rep["score"]["value"], (int, float)) assert rep["subject"]["address"].lower() == TEST_ADDRESS.lower() - - -def test_get_agents_items_have_expected_fields(): - client = AgentScore(api_key=API_KEY, base_url=BASE_URL) - result = client.get_agents() - - assert isinstance(result["items"], list) - assert len(result["items"]) > 0 - - for item in result["items"]: - assert "chain" in item - assert "token_id" in item - assert "owner_address" in item diff --git a/uv.lock b/uv.lock index 5e0dfcd..c7c13a2 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agentscore-py" -version = "1.3.0" +version = "1.4.0" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -25,7 +25,10 @@ dev = [ [package.dev-dependencies] dev = [ + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-cov" }, + { name = "respx" }, { name = "ruff" }, { name = "vulture" }, ] @@ -41,7 +44,9 @@ provides-extras = ["dev"] [package.metadata.requires-dev] dev = [ + { name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-cov", specifier = ">=6.0" }, + { name = "respx", specifier = ">=0.22.0" }, { name = "ruff", specifier = ">=0.11.0" }, { name = "vulture", specifier = ">=2.15" }, ] @@ -563,27 +568,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.8" +version = "0.15.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, - { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, ] [[package]] From 925c8d2b8a37306edc70584632bf403ba7a69f2b Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Mon, 6 Apr 2026 04:07:57 -0700 Subject: [PATCH 2/5] Add verification types, compliance policy fields, X-API-Key auth (TEC-177, TEC-179, TEC-182) - Add VerificationLevel literal type and OperatorVerification TypedDict - Add verification_level to ReputationResponse - Add operator_verification, verify_url, resolved_operator to AssessResponse - Add compliance policy fields to DecisionPolicy - Switch auth header from Authorization: Bearer to X-API-Key - Add python-dotenv + conftest.py for integration test env loading - Update tests for new types and auth header Co-Authored-By: Claude Opus 4.6 (1M context) --- agentscore/__init__.py | 4 + agentscore/client.py | 2 +- agentscore/types.py | 20 +++++ pyproject.toml | 1 + tests/conftest.py | 3 + tests/test_client.py | 183 +++++++++++++++++++++++++++++++++++++- tests/test_integration.py | 10 --- uv.lock | 27 ++++++ 8 files changed, 237 insertions(+), 13 deletions(-) create mode 100644 tests/conftest.py diff --git a/agentscore/__init__.py b/agentscore/__init__.py index af675ce..9a788f3 100644 --- a/agentscore/__init__.py +++ b/agentscore/__init__.py @@ -7,9 +7,11 @@ DecisionPolicy, EntityType, Grade, + OperatorVerification, Reputation, ReputationResponse, ReputationStatus, + VerificationLevel, ) __version__ = _pkg_version("agentscore-py") @@ -21,8 +23,10 @@ "DecisionPolicy", "EntityType", "Grade", + "OperatorVerification", "Reputation", "ReputationResponse", "ReputationStatus", + "VerificationLevel", "__version__", ] diff --git a/agentscore/client.py b/agentscore/client.py index e0a269f..a092e09 100644 --- a/agentscore/client.py +++ b/agentscore/client.py @@ -35,7 +35,7 @@ def __init__( def _headers(self) -> dict: return { "Accept": "application/json", - "Authorization": f"Bearer {self.api_key}", + "X-API-Key": self.api_key, "User-Agent": f"agentscore-py/{_pkg_version('agentscore-py')}", } diff --git a/agentscore/types.py b/agentscore/types.py index 7204994..c1ef3a9 100644 --- a/agentscore/types.py +++ b/agentscore/types.py @@ -5,6 +5,7 @@ Grade = Literal["A", "B", "C", "D", "F"] EntityType = Literal["agent", "service", "hybrid", "wallet", "bot", "unknown"] ReputationStatus = Literal["scored", "stale", "known_unscored"] +VerificationLevel = Literal["none", "wallet_claimed", "kyc_verified"] class Subject(TypedDict): @@ -132,12 +133,28 @@ class ReputationResponse(_ReputationResponseRequired, total=False): reputation: Reputation operator_score: OperatorScore agents: list[AgentSummary] + verification_level: VerificationLevel + + +class _OperatorVerificationRequired(TypedDict): + level: VerificationLevel + + +class OperatorVerification(_OperatorVerificationRequired, total=False): + operator_type: str | None + claimed_at: str | None + verified_at: str | None class DecisionPolicy(TypedDict, total=False): min_grade: Grade min_score: int require_verified_payment_activity: bool + require_kyc: bool + require_sanctions_clear: bool + min_age: int + blocked_jurisdictions: list[str] + require_entity_type: str class _AssessResponseRequired(TypedDict): @@ -156,3 +173,6 @@ class AssessResponse(_AssessResponseRequired, total=False): operator_score: OperatorScore | None reputation: Reputation | None agents: list[AgentSummary] + operator_verification: OperatorVerification + resolved_operator: str + verify_url: str diff --git a/pyproject.toml b/pyproject.toml index 00ffb73..4e09ae7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,4 +46,5 @@ dev = [ "pytest-cov>=6.0", "respx>=0.22.0", "pytest-asyncio>=1.2.0", + "python-dotenv>=1.2.1", ] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..bf6bd6c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,3 @@ +from dotenv import load_dotenv + +load_dotenv() diff --git a/tests/test_client.py b/tests/test_client.py index 7a89685..905e131 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -220,7 +220,7 @@ def test_assess_raises_on_402(): # --------------------------------------------------------------------------- -# Authorization header +# API key header # --------------------------------------------------------------------------- REPUTATION_PAYLOAD_SIMPLE = { @@ -239,7 +239,7 @@ def test_auth_header_is_sent(): ) client = AgentScore(api_key="my-secret-key") client.get_reputation(ADDRESS) - assert route.calls.last.request.headers["authorization"] == "Bearer my-secret-key" + assert route.calls.last.request.headers["x-api-key"] == "my-secret-key" # --------------------------------------------------------------------------- @@ -485,3 +485,182 @@ def test_error_response_no_error_key_fallback(): client.get_reputation(ADDRESS) assert exc_info.value.status_code == 422 assert exc_info.value.code == "unknown_error" + + +# --------------------------------------------------------------------------- +# Verification / Compliance fields +# --------------------------------------------------------------------------- + + +REPUTATION_WITH_VERIFICATION = { + **REPUTATION_PAYLOAD, + "verification_level": "kyc_verified", +} + +ASSESS_WITH_COMPLIANCE = { + **ASSESS_PAYLOAD, + "decision": "deny", + "decision_reasons": ["kyc_required", "sanctions_check_pending"], + "operator_verification": { + "level": "none", + "operator_type": None, + "claimed_at": None, + "verified_at": None, + }, + "verify_url": "https://agentscore.sh/verify/abc123", + "resolved_operator": "0xoperator456", +} + + +@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) + ) + client = AgentScore(api_key=API_KEY) + result = client.assess(ADDRESS) + assert result["operator_verification"]["level"] == "none" + assert result["operator_verification"]["operator_type"] is None + + +@respx.mock +def test_assess_returns_verify_url(): + respx.post(f"{BASE_URL}/v1/assess").mock( + return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE) + ) + client = AgentScore(api_key=API_KEY) + result = client.assess(ADDRESS) + assert result["verify_url"] == "https://agentscore.sh/verify/abc123" + + +@respx.mock +def test_assess_returns_resolved_operator(): + respx.post(f"{BASE_URL}/v1/assess").mock( + return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE) + ) + client = AgentScore(api_key=API_KEY) + result = client.assess(ADDRESS) + assert result["resolved_operator"] == "0xoperator456" + + +@respx.mock +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 + + +@respx.mock +def test_assess_sends_compliance_policy_fields(): + route = respx.post(f"{BASE_URL}/v1/assess").mock( + return_value=httpx.Response(200, json=ASSESS_PAYLOAD) + ) + client = AgentScore(api_key=API_KEY) + policy = { + "require_kyc": True, + "require_sanctions_clear": True, + "min_age": 90, + "blocked_jurisdictions": ["KP", "IR"], + "require_entity_type": "agent", + } + client.assess(ADDRESS, policy=policy) + body = json.loads(route.calls.last.request.content) + assert body["policy"]["require_kyc"] is True + assert body["policy"]["require_sanctions_clear"] is True + assert body["policy"]["min_age"] == 90 + assert body["policy"]["blocked_jurisdictions"] == ["KP", "IR"] + assert body["policy"]["require_entity_type"] == "agent" + + +@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(): + respx.post(f"{BASE_URL}/v1/assess").mock( + return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE) + ) + client = AgentScore(api_key=API_KEY) + result = await client.aassess(ADDRESS) + assert result["operator_verification"]["level"] == "none" + assert result["verify_url"] == "https://agentscore.sh/verify/abc123" + assert result["resolved_operator"] == "0xoperator456" + await client.aclose() + + +# --------------------------------------------------------------------------- +# Integration-style: compliance deny flow +# --------------------------------------------------------------------------- + + +@respx.mock +def test_full_compliance_deny_flow(): + """Full assess flow with compliance policy returning deny + verify_url.""" + compliance_response = { + **REPUTATION_PAYLOAD, + "decision": "deny", + "decision_reasons": ["kyc_required", "sanctions_check_pending"], + "on_the_fly": False, + "operator_verification": { + "level": "none", + "operator_type": None, + "claimed_at": None, + "verified_at": None, + }, + "verify_url": "https://agentscore.sh/verify/xyz789", + } + route = respx.post(f"{BASE_URL}/v1/assess").mock( + return_value=httpx.Response(200, json=compliance_response) + ) + client = AgentScore(api_key=API_KEY) + result = client.assess( + ADDRESS, + policy={ + "require_kyc": True, + "require_sanctions_clear": True, + }, + ) + assert result["decision"] == "deny" + assert "kyc_required" in result["decision_reasons"] + assert "sanctions_check_pending" in result["decision_reasons"] + assert result["verify_url"] == "https://agentscore.sh/verify/xyz789" + assert result["operator_verification"]["level"] == "none" + + body = json.loads(route.calls.last.request.content) + assert body["policy"]["require_kyc"] is True + assert body["policy"]["require_sanctions_clear"] is True diff --git a/tests/test_integration.py b/tests/test_integration.py index 979c5a4..d1afd9a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -104,16 +104,6 @@ def test_get_reputation_operator_score(): assert isinstance(op["chains_active"], list) -def test_get_reputation_reputation_field(): - client = AgentScore(api_key=API_KEY, base_url=BASE_URL) - rep = client.get_reputation(TEST_ADDRESS) - r = rep.get("reputation") - if not r: - pytest.skip("no reputation on test address") - assert isinstance(r["feedback_count"], int) - assert isinstance(r["client_count"], int) - - def test_assess_then_get_reputation(): client = AgentScore(api_key=API_KEY, base_url=BASE_URL) assessed = client.assess(TEST_ADDRESS) diff --git a/uv.lock b/uv.lock index c7c13a2..125e147 100644 --- a/uv.lock +++ b/uv.lock @@ -28,6 +28,8 @@ dev = [ { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-cov" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "respx" }, { name = "ruff" }, { name = "vulture" }, @@ -46,6 +48,7 @@ provides-extras = ["dev"] dev = [ { name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-cov", specifier = ">=6.0" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "respx", specifier = ">=0.22.0" }, { name = "ruff", specifier = ">=0.11.0" }, { name = "vulture", specifier = ">=2.15" }, @@ -554,6 +557,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + [[package]] name = "respx" version = "0.22.0" From f851ee2fd081299c445d4eded5839e31e1467d5a Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Mon, 6 Apr 2026 04:31:00 -0700 Subject: [PATCH 3/5] Fix ruff formatting in test_client.py Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_client.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 905e131..95f7b45 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -524,9 +524,7 @@ def test_get_reputation_returns_verification_level(): @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) - ) + 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 @@ -534,9 +532,7 @@ def test_get_reputation_omits_verification_level_when_absent(): @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) - ) + respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)) client = AgentScore(api_key=API_KEY) result = client.assess(ADDRESS) assert result["operator_verification"]["level"] == "none" @@ -545,9 +541,7 @@ def test_assess_returns_operator_verification(): @respx.mock def test_assess_returns_verify_url(): - respx.post(f"{BASE_URL}/v1/assess").mock( - return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE) - ) + respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)) client = AgentScore(api_key=API_KEY) result = client.assess(ADDRESS) assert result["verify_url"] == "https://agentscore.sh/verify/abc123" @@ -555,9 +549,7 @@ def test_assess_returns_verify_url(): @respx.mock def test_assess_returns_resolved_operator(): - respx.post(f"{BASE_URL}/v1/assess").mock( - return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE) - ) + respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)) client = AgentScore(api_key=API_KEY) result = client.assess(ADDRESS) assert result["resolved_operator"] == "0xoperator456" @@ -565,9 +557,7 @@ def test_assess_returns_resolved_operator(): @respx.mock def test_assess_omits_verification_fields_when_absent(): - respx.post(f"{BASE_URL}/v1/assess").mock( - return_value=httpx.Response(200, json=ASSESS_PAYLOAD) - ) + 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 @@ -577,9 +567,7 @@ def test_assess_omits_verification_fields_when_absent(): @respx.mock def test_assess_sends_compliance_policy_fields(): - route = respx.post(f"{BASE_URL}/v1/assess").mock( - return_value=httpx.Response(200, json=ASSESS_PAYLOAD) - ) + route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD)) client = AgentScore(api_key=API_KEY) policy = { "require_kyc": True, @@ -612,9 +600,7 @@ async def test_aget_reputation_returns_verification_level(): @pytest.mark.asyncio @respx.mock async def test_aassess_returns_compliance_fields(): - respx.post(f"{BASE_URL}/v1/assess").mock( - return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE) - ) + respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)) client = AgentScore(api_key=API_KEY) result = await client.aassess(ADDRESS) assert result["operator_verification"]["level"] == "none" @@ -644,9 +630,7 @@ def test_full_compliance_deny_flow(): }, "verify_url": "https://agentscore.sh/verify/xyz789", } - route = respx.post(f"{BASE_URL}/v1/assess").mock( - return_value=httpx.Response(200, json=compliance_response) - ) + route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=compliance_response)) client = AgentScore(api_key=API_KEY) result = client.assess( ADDRESS, From de58c3d08049325ed8591299a050534c277eda61 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Mon, 6 Apr 2026 04:41:47 -0700 Subject: [PATCH 4/5] Gitignore .coverage Co-Authored-By: Claude Opus 4.6 (1M context) --- .coverage | Bin 53248 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index 4651b85fb4f3854d3ff1d4ba8a106cd221bf6a84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI)+m91f90%~Z(b8_0SzJl8Y{;HO&9b4}+MuYIh^wrykwj52#zf1sJ>8CUX3ETT z***XkNie?n4-j8`^;P3vfe$`H#3v;rniwP|F3I{kbLnN-l685&lJBIK>C8E2e&=&8 z(`k46{>c-j&soiJEyHIAr3p!vrPmpgBq>Grak|G#l2)|%1wG4s>%&%4QvS=Q$Mp}S zOyWaH|73ixKA&06{5f_l^K$C+n3>F_^0WgB1Rwwb2s|+Yos*elc4|t#e8D%$4eq-} zg}dS7_?z!7E}U9qrxxBgvB<)0Y=4^3GC$82nCqNpP42Rq+2G8yt7gUUO?#R7bsnc~ zdAuslS+tGLS|VXS$S>APW|eaJe3^=9x~65gZFZKob3uaWXg*){!wpmjH4|$E}L!%dTNp z>fFmQqg%s@LvgP9VQpQo} z6XuML)u+?Tyh+X|d6coO__t3?Uq$0@=sepM|p zx7;?Hv}Lcp1I-zeixv%YwQTcTi+3A8$9g$)K{aeA$3!Kl2Rt{H$i6ZuOVlg*s&}qI zZ=Fg;%Xfm;C2IUqksdNvcJEAPXJ+K9=YozRcuHl{_qr>oflkxiJf!Q0{loi>7$t+9 zM2w3|x(Nl7u3&kzD$ut`8vbf>3+;}2+*d!U}=*^Px7>V{!b>@?a?85Z0>O|>E#b{+` zC#fZ~d-lkka72iCN!)h~R6=a&4S@|Of~|+&H2gYyFm|z-0-xQH$iBK~Sl}XrQc*l6 zu1qLF$X}>|zD1M1xT^ypi!Hs7-{$T46{A73Nz*W08nQG8hi3pL5>^dLY?PgrAM{`G zGU%S-+|!FUETH!~wx?8cL2?@2?>e4m(rHzkw!FjfRyJEVJTVJ-v#jhm4Q|-sxq)&l zd2~$EV?hiLo|TUD%JABvjOCTtAg1`_BBuFMH0a{-MJtwN*wyHKB~HU(sWkVC!6}Xt zP9l_%&C;OsXozkoGcGjRD`u>?{Cty4M5&fh4TrmEl)e(r*ZBj*g=eFqhR=P|;!Gq7 zf(bg-dBbBBm($MZTp9#MV)cztg~c4FJ@FhG)@E*2eOTWSOWBPQWtax<)URfu6NtXRnjJL$fr!Me6Td0=&;qybO;ZX2h`B%OPEy zDtd#Io)Z`Nvc4wK4GRPy009U<00Izz00bZa0SG_<0#BZRDl2kQT>mTjFOvQTJz#+V z1Rwwb2tWV=5P$##AOHafKwu;VGK#uSkN+&>`3YH_o{Ihk;H7-=VE)B4)k@KCOZsj7 z-bhvuK|=rn5P$##AOHafKmY;|fB*y_&=tt2`{ejz041$Xr=m{*#P|Q&t{am6o!-!& zp%p9;fB*y_009U<00Izz00bZafzc9pOVgyz?!sx0etV#>Vz@24aFnm`hSRjT?HA}r zLTulopXcF)X4|hj_Pkd;+rQOsH@Qc@LvT!`awfN54tHJ04GK~ot)NQ7q>w41C7BA! zZa<<@$+S(sRHCB9_y5|io09&$Zt2g`3Kj@J00Izz00bZa0SG_<0uX?}hzT6iI=kiV zUnR%a|Jre_Gx-SRbg%!_M=Yp&{jV%(oos6RHS4?n$M64*Sf@s~5P$##AOHafKmY;| zfB*y_0D=D|pvtN=F24Vl_4^Xtus{F;5P$##AOHafKmY;|fB*y_@B|8|>Ub*n|9|}l zN&j2Fq5niHSReoa2tWV=5P$##AOHafKmY;|_}>UjX_}mRLA!J9(#4C{@2=nd?TB~% z$Gi92U$3_lD(%{(zP4AP=R?XzJHB08yZ_I5gb4u7Tu z|NpQ5D(P$bU;6K~f&~H)fB*y_009U<00Izz00bZafe{r*&?f=1rUg%`8ayeAMjrtL z-~US!BU&wl4FL#100Izz00bZa0SG_<0uX?}mITE2|G56&5)~3b00Izz00bZa0SG_< r0uX=z1V&as{Qp0$|3|j@5I6)N009U<00Izz00bZa0SG`~O9KA_D@ruM diff --git a/.gitignore b/.gitignore index 9817cce..2a5e7a5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ .ruff_cache/ .env coverage/ +.coverage From 81b6338903f3d94da002ff67bf0881a6f5b0a512 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Mon, 6 Apr 2026 05:13:16 -0700 Subject: [PATCH 5/5] Add compliance/verification examples to README Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 7597fc6..52b2ab3 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,21 @@ base_rep = client.get_reputation("0x1234...", chain="base") # On-the-fly assessment with policy (paid) result = client.assess("0x1234...", policy={"min_grade": "B", "min_score": 35}) print(result["decision"], result["decision_reasons"]) + +# Compliance assessment with verification policy +gated = client.assess("0x1234...", policy={ + "require_kyc": True, + "require_sanctions_clear": True, + "min_age": 21, +}) + +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" ``` ### Async