Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pinecone/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,4 +32,9 @@
"adapt_query_response",
"adapt_upsert_response",
"UpsertResponseTransformer",
"FetchResponseAdapter",
"IndexModelAdapter",
"IndexStatusAdapter",
"QueryResponseAdapter",
"UpsertResponseAdapter",
]
137 changes: 137 additions & 0 deletions pinecone/adapters/protocols.py
Original file line number Diff line number Diff line change
@@ -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 (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 | None
vectors: dict[str, Any]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protocol incorrectly marks vectors as required when API allows None

Medium Severity

The FetchResponseAdapter protocol defines vectors: dict[str, Any] as a required attribute, but the OpenAPI spec marks vectors as [optional]. If the API returns a response without vectors (or with null), the adapter code at openapi_response.vectors.items() will crash with an AttributeError. The protocol type should be dict[str, Any] | None to match the actual API contract and prompt defensive handling.

Fix in Cursor Fix in Web

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
13 changes: 9 additions & 4 deletions pinecone/adapters/response_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -110,7 +115,7 @@ def adapt_fetch_response(openapi_response: Any) -> FetchResponse:
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,
Expand Down
156 changes: 156 additions & 0 deletions tests/unit/adapters/test_protocols.py
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 10 additions & 0 deletions tests/unit/adapters/test_response_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == ""
Loading