From 7af453d56c843ed6b64463b757caef8b4092eca9 Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Tue, 3 Feb 2026 03:52:20 -0500 Subject: [PATCH 1/2] feat: add Protocol definitions for adapter layer Add formal Protocol interfaces that define the contract between OpenAPI models and SDK adapters. This improves type safety, documentation, and maintainability without any breaking changes. - Created pinecone/adapters/protocols.py with 5 Protocol definitions - Updated adapter functions to use protocol type annotations - Added 13 unit tests verifying protocol compliance - All tests pass, mypy validates protocol usage Related: SDK-275 Co-authored-by: Cursor --- pinecone/adapters/__init__.py | 12 ++ pinecone/adapters/protocols.py | 137 ++++++++++++++++++++++ pinecone/adapters/response_adapters.py | 11 +- tests/unit/adapters/test_protocols.py | 156 +++++++++++++++++++++++++ 4 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 pinecone/adapters/protocols.py create mode 100644 tests/unit/adapters/test_protocols.py diff --git a/pinecone/adapters/__init__.py b/pinecone/adapters/__init__.py index e9c481ff..1e41e12f 100644 --- a/pinecone/adapters/__init__.py +++ b/pinecone/adapters/__init__.py @@ -13,6 +13,13 @@ >>> sdk_response = adapt_query_response(openapi_response) """ +from pinecone.adapters.protocols import ( + FetchResponseAdapter, + IndexModelAdapter, + IndexStatusAdapter, + QueryResponseAdapter, + UpsertResponseAdapter, +) from pinecone.adapters.response_adapters import ( adapt_fetch_response, adapt_query_response, @@ -25,4 +32,9 @@ "adapt_query_response", "adapt_upsert_response", "UpsertResponseTransformer", + "FetchResponseAdapter", + "IndexModelAdapter", + "IndexStatusAdapter", + "QueryResponseAdapter", + "UpsertResponseAdapter", ] diff --git a/pinecone/adapters/protocols.py b/pinecone/adapters/protocols.py new file mode 100644 index 00000000..1331dd45 --- /dev/null +++ b/pinecone/adapters/protocols.py @@ -0,0 +1,137 @@ +"""Protocol definitions for the adapter layer. + +This module defines formal Protocol interfaces that specify the contract between +generated OpenAPI models and SDK adapter code. These protocols make it explicit +what properties and methods the SDK code depends on from the OpenAPI models, +enabling: + +- Type safety with static type checking (mypy) +- Clear documentation of adapter dependencies +- Flexibility to change OpenAPI model implementations +- Better testability through protocol-based mocking + +Each protocol corresponds to an OpenAPI model type that adapters consume. The +protocols define only the minimal interface required by adapter functions, +isolating SDK code from the full complexity of generated models. + +Usage: + >>> from pinecone.adapters.protocols import QueryResponseAdapter + >>> def adapt_query(response: QueryResponseAdapter) -> QueryResponse: + ... return QueryResponse(matches=response.matches) +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Protocol + +if TYPE_CHECKING: + from pinecone.core.openapi.db_data.models import ScoredVector, Usage + from pinecone.core.openapi.db_control.model.index_model_status import IndexModelStatus + + +class QueryResponseAdapter(Protocol): + """Protocol for OpenAPI QueryResponse objects used in adapters. + + This protocol defines the minimal interface that SDK code depends on when + adapting an OpenAPI QueryResponse to the SDK QueryResponse dataclass. + + Attributes: + matches: List of scored vectors returned by the query. + namespace: The namespace that was queried. + usage: Optional usage statistics for the query operation. + _data_store: Internal data storage (for accessing raw response data). + _response_info: Response metadata including headers. + """ + + matches: list[ScoredVector] + namespace: str | None + usage: Usage | None + _data_store: dict[str, Any] + _response_info: Any + + +class UpsertResponseAdapter(Protocol): + """Protocol for OpenAPI UpsertResponse objects used in adapters. + + This protocol defines the minimal interface that SDK code depends on when + adapting an OpenAPI UpsertResponse to the SDK UpsertResponse dataclass. + + Attributes: + upserted_count: Number of vectors that were successfully upserted. + _response_info: Response metadata including headers. + """ + + upserted_count: int + _response_info: Any + + +class FetchResponseAdapter(Protocol): + """Protocol for OpenAPI FetchResponse objects used in adapters. + + This protocol defines the minimal interface that SDK code depends on when + adapting an OpenAPI FetchResponse to the SDK FetchResponse dataclass. + + Attributes: + namespace: The namespace from which vectors were fetched. + vectors: Dictionary mapping vector IDs to Vector objects. + usage: Optional usage statistics for the fetch operation. + _response_info: Response metadata including headers. + """ + + namespace: str + vectors: dict[str, Any] + usage: Usage | None + _response_info: Any + + +class IndexModelAdapter(Protocol): + """Protocol for OpenAPI IndexModel objects used in adapters. + + This protocol defines the minimal interface that SDK code depends on when + working with OpenAPI IndexModel objects. The IndexModel wrapper class + provides additional functionality on top of this protocol. + + Attributes: + name: The name of the index. + dimension: The dimensionality of vectors in the index. + metric: The distance metric used for similarity search. + host: The host URL for the index. + spec: The index specification (serverless, pod, or BYOC). + status: The current status of the index. + _data_store: Internal data storage (for accessing raw response data). + _configuration: OpenAPI configuration object. + _path_to_item: Path to this item in the response tree. + """ + + name: str + dimension: int + metric: str + host: str + spec: Any + status: IndexModelStatus + _data_store: dict[str, Any] + _configuration: Any + _path_to_item: tuple[str, ...] | list[str] + + def to_dict(self) -> dict[str, Any]: + """Convert the index model to a dictionary representation. + + Returns: + Dictionary representation of the index model. + """ + ... + + +class IndexStatusAdapter(Protocol): + """Protocol for IndexModelStatus objects used in adapters. + + This protocol defines the minimal interface that SDK code depends on when + working with index status information. + + Attributes: + ready: Whether the index is ready to serve requests. + state: The current state of the index (e.g., 'Ready', 'Initializing'). + """ + + ready: bool + state: str diff --git a/pinecone/adapters/response_adapters.py b/pinecone/adapters/response_adapters.py index c7decb58..2ccd67f7 100644 --- a/pinecone/adapters/response_adapters.py +++ b/pinecone/adapters/response_adapters.py @@ -13,6 +13,11 @@ from multiprocessing.pool import ApplyResult from typing import TYPE_CHECKING, Any +from pinecone.adapters.protocols import ( + FetchResponseAdapter, + QueryResponseAdapter, + UpsertResponseAdapter, +) from pinecone.adapters.utils import extract_response_metadata if TYPE_CHECKING: @@ -21,7 +26,7 @@ from pinecone.db_data.dataclasses.upsert_response import UpsertResponse -def adapt_query_response(openapi_response: Any) -> QueryResponse: +def adapt_query_response(openapi_response: QueryResponseAdapter) -> QueryResponse: """Adapt an OpenAPI QueryResponse to the SDK QueryResponse dataclass. This function extracts fields from the OpenAPI response object and @@ -61,7 +66,7 @@ def adapt_query_response(openapi_response: Any) -> QueryResponse: ) -def adapt_upsert_response(openapi_response: Any) -> UpsertResponse: +def adapt_upsert_response(openapi_response: UpsertResponseAdapter) -> UpsertResponse: """Adapt an OpenAPI UpsertResponse to the SDK UpsertResponse dataclass. Args: @@ -83,7 +88,7 @@ def adapt_upsert_response(openapi_response: Any) -> UpsertResponse: return UR(upserted_count=openapi_response.upserted_count, _response_info=response_info) -def adapt_fetch_response(openapi_response: Any) -> FetchResponse: +def adapt_fetch_response(openapi_response: FetchResponseAdapter) -> FetchResponse: """Adapt an OpenAPI FetchResponse to the SDK FetchResponse dataclass. This function extracts fields from the OpenAPI response object and diff --git a/tests/unit/adapters/test_protocols.py b/tests/unit/adapters/test_protocols.py new file mode 100644 index 00000000..d2397b51 --- /dev/null +++ b/tests/unit/adapters/test_protocols.py @@ -0,0 +1,156 @@ +"""Unit tests for adapter protocol compliance. + +These tests verify that the actual OpenAPI models satisfy the protocol +interfaces defined in pinecone.adapters.protocols. This ensures that the +adapter layer's contracts are maintained even as the OpenAPI models change. +""" + +from pinecone.adapters.protocols import ( + QueryResponseAdapter, + UpsertResponseAdapter, + FetchResponseAdapter, + IndexModelAdapter, + IndexStatusAdapter, +) +from tests.fixtures import ( + make_openapi_query_response, + make_openapi_upsert_response, + make_openapi_fetch_response, +) + + +class TestQueryResponseProtocolCompliance: + """Tests that OpenAPI QueryResponse satisfies QueryResponseAdapter protocol.""" + + def test_has_matches_attribute(self): + """Test that QueryResponse has matches attribute.""" + response = make_openapi_query_response(matches=[]) + # This satisfies the protocol check + _protocol_check: QueryResponseAdapter = response + assert hasattr(response, "matches") + + def test_has_namespace_attribute(self): + """Test that QueryResponse has namespace attribute.""" + response = make_openapi_query_response(matches=[], namespace="test") + _protocol_check: QueryResponseAdapter = response + assert hasattr(response, "namespace") + + def test_has_usage_attribute(self): + """Test that QueryResponse has usage attribute.""" + response = make_openapi_query_response(matches=[]) + _protocol_check: QueryResponseAdapter = response + assert hasattr(response, "usage") + + def test_has_data_store_attribute(self): + """Test that QueryResponse has _data_store attribute.""" + response = make_openapi_query_response(matches=[]) + _protocol_check: QueryResponseAdapter = response + assert hasattr(response, "_data_store") + + def test_has_response_info_attribute(self): + """Test that QueryResponse has _response_info attribute.""" + response = make_openapi_query_response(matches=[]) + _protocol_check: QueryResponseAdapter = response + assert hasattr(response, "_response_info") + + +class TestUpsertResponseProtocolCompliance: + """Tests that OpenAPI UpsertResponse satisfies UpsertResponseAdapter protocol.""" + + def test_has_upserted_count_attribute(self): + """Test that UpsertResponse has upserted_count attribute.""" + response = make_openapi_upsert_response(upserted_count=10) + _protocol_check: UpsertResponseAdapter = response + assert hasattr(response, "upserted_count") + assert response.upserted_count == 10 + + def test_has_response_info_attribute(self): + """Test that UpsertResponse has _response_info attribute.""" + response = make_openapi_upsert_response(upserted_count=10) + _protocol_check: UpsertResponseAdapter = response + assert hasattr(response, "_response_info") + + +class TestFetchResponseProtocolCompliance: + """Tests that OpenAPI FetchResponse satisfies FetchResponseAdapter protocol.""" + + def test_has_namespace_attribute(self): + """Test that FetchResponse has namespace attribute.""" + response = make_openapi_fetch_response(vectors={}, namespace="test") + _protocol_check: FetchResponseAdapter = response + assert hasattr(response, "namespace") + assert response.namespace == "test" + + def test_has_vectors_attribute(self): + """Test that FetchResponse has vectors attribute.""" + response = make_openapi_fetch_response(vectors={}) + _protocol_check: FetchResponseAdapter = response + assert hasattr(response, "vectors") + + def test_has_usage_attribute(self): + """Test that FetchResponse has usage attribute.""" + response = make_openapi_fetch_response(vectors={}) + _protocol_check: FetchResponseAdapter = response + assert hasattr(response, "usage") + + def test_has_response_info_attribute(self): + """Test that FetchResponse has _response_info attribute.""" + response = make_openapi_fetch_response(vectors={}) + _protocol_check: FetchResponseAdapter = response + assert hasattr(response, "_response_info") + + +class TestIndexModelProtocolCompliance: + """Tests that OpenAPI IndexModel satisfies IndexModelAdapter protocol.""" + + def test_openapi_index_model_has_required_attributes(self): + """Test that OpenAPI IndexModel has all required protocol attributes.""" + from pinecone.core.openapi.db_control.model.index_model import ( + IndexModel as OpenAPIIndexModel, + ) + from pinecone.core.openapi.db_control.model.index_model_status import IndexModelStatus + + # Create a minimal OpenAPI IndexModel + index = OpenAPIIndexModel._new_from_openapi_data( + name="test-index", + dimension=128, + metric="cosine", + host="test-host.pinecone.io", + spec={"serverless": {"cloud": "aws", "region": "us-east-1"}}, + status=IndexModelStatus._new_from_openapi_data(ready=True, state="Ready"), + ) + + # This satisfies the protocol check + _protocol_check: IndexModelAdapter = index + + # Verify all required attributes exist + assert hasattr(index, "name") + assert hasattr(index, "dimension") + assert hasattr(index, "metric") + assert hasattr(index, "host") + assert hasattr(index, "spec") + assert hasattr(index, "status") + assert hasattr(index, "_data_store") + assert hasattr(index, "_configuration") + assert hasattr(index, "_path_to_item") + assert hasattr(index, "to_dict") + assert callable(index.to_dict) + + +class TestIndexStatusProtocolCompliance: + """Tests that IndexModelStatus satisfies IndexStatusAdapter protocol.""" + + def test_openapi_index_status_has_required_attributes(self): + """Test that IndexModelStatus has all required protocol attributes.""" + from pinecone.core.openapi.db_control.model.index_model_status import IndexModelStatus + + status = IndexModelStatus._new_from_openapi_data(ready=True, state="Ready") + + # This satisfies the protocol check + _protocol_check: IndexStatusAdapter = status + + # Verify all required attributes exist + assert hasattr(status, "ready") + assert hasattr(status, "state") + assert status.ready is True + assert status.state == "Ready" From eb0b688b33bc926b0fe37fc1ab0989b0258c260a Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Tue, 3 Feb 2026 09:28:38 -0500 Subject: [PATCH 2/2] fix: make FetchResponseAdapter.namespace optional Changed FetchResponseAdapter.namespace from `str` to `str | None` to correctly reflect that the OpenAPI FetchResponse model marks namespace as optional. Updated the adapter function to handle None namespace by converting it to empty string, consistent with QueryResponse adapter. - Updated protocol type annotation - Added null-coalescing in adapter function - Added test for None namespace case Fixes Cursor Bugbot issue Co-authored-by: Cursor --- pinecone/adapters/protocols.py | 4 ++-- pinecone/adapters/response_adapters.py | 2 +- tests/unit/adapters/test_response_adapters.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pinecone/adapters/protocols.py b/pinecone/adapters/protocols.py index 1331dd45..f4c01e3e 100644 --- a/pinecone/adapters/protocols.py +++ b/pinecone/adapters/protocols.py @@ -72,13 +72,13 @@ class FetchResponseAdapter(Protocol): adapting an OpenAPI FetchResponse to the SDK FetchResponse dataclass. Attributes: - namespace: The namespace from which vectors were fetched. + namespace: The namespace from which vectors were fetched (optional). vectors: Dictionary mapping vector IDs to Vector objects. usage: Optional usage statistics for the fetch operation. _response_info: Response metadata including headers. """ - namespace: str + namespace: str | None vectors: dict[str, Any] usage: Usage | None _response_info: Any diff --git a/pinecone/adapters/response_adapters.py b/pinecone/adapters/response_adapters.py index 2ccd67f7..b77d6f32 100644 --- a/pinecone/adapters/response_adapters.py +++ b/pinecone/adapters/response_adapters.py @@ -115,7 +115,7 @@ def adapt_fetch_response(openapi_response: FetchResponseAdapter) -> FetchRespons response_info = extract_response_metadata(openapi_response) return FR( - namespace=openapi_response.namespace, + namespace=openapi_response.namespace or "", vectors={k: Vector.from_dict(v) for k, v in openapi_response.vectors.items()}, usage=openapi_response.usage, _response_info=response_info, diff --git a/tests/unit/adapters/test_response_adapters.py b/tests/unit/adapters/test_response_adapters.py index f2b0a662..05d0dd04 100644 --- a/tests/unit/adapters/test_response_adapters.py +++ b/tests/unit/adapters/test_response_adapters.py @@ -174,3 +174,13 @@ def test_fetch_response_has_response_info(self): assert hasattr(result, "_response_info") assert "raw_headers" in result._response_info + + def test_fetch_response_with_none_namespace(self): + """Test that None namespace is converted to empty string.""" + openapi_response = make_openapi_fetch_response(vectors={}) + # Manually set namespace to None to simulate API response + openapi_response._data_store["namespace"] = None + + result = adapt_fetch_response(openapi_response) + + assert result.namespace == ""