From 7af453d56c843ed6b64463b757caef8b4092eca9 Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Tue, 3 Feb 2026 03:52:20 -0500 Subject: [PATCH 1/4] 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/4] 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 == "" From ad0aeb65cbbc4f24edf721a23bb51f3c3f0dbf04 Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Tue, 3 Feb 2026 11:18:40 -0500 Subject: [PATCH 3/4] feat: extract IndexModel spec resolution to adapter layer Create adapter infrastructure for IndexModel spec handling, isolating SDK code from OpenAPI model internals and preparing for future API format changes. Changes: - Add pinecone/adapters/index_adapter.py with adapt_index_spec() function - Extract 150+ lines of complex oneOf resolution logic from IndexModel - Handle serverless/pod/byoc specs with proper optional field support - Simplify IndexModel from 176 to 40 lines by delegating to adapter - Add comprehensive unit tests for all spec types and edge cases - Maintain 100% backward compatibility with existing behavior The adapter properly handles: - Optional fields (read_capacity, source_collection, schema) - Nested oneOf types (ReadCapacity with OnDemand/Dedicated modes) - Type compatibility between request and response models - Caching behavior in IndexModel wrapper Closes SDK-276 Co-authored-by: Cursor --- pinecone/adapters/__init__.py | 2 + pinecone/adapters/index_adapter.py | 237 ++++++++++++++++++++++ pinecone/adapters/protocols.py | 14 +- pinecone/adapters/response_adapters.py | 13 +- pinecone/db_control/models/index_model.py | 153 +------------- tests/fixtures/db_data_models.py | 64 +++--- tests/unit/adapters/test_index_adapter.py | 150 ++++++++++++++ tests/unit/adapters/test_protocols.py | 56 +++++ 8 files changed, 510 insertions(+), 179 deletions(-) create mode 100644 pinecone/adapters/index_adapter.py create mode 100644 tests/unit/adapters/test_index_adapter.py diff --git a/pinecone/adapters/__init__.py b/pinecone/adapters/__init__.py index 1e41e12f..15030aa9 100644 --- a/pinecone/adapters/__init__.py +++ b/pinecone/adapters/__init__.py @@ -26,9 +26,11 @@ adapt_upsert_response, UpsertResponseTransformer, ) +from pinecone.adapters.index_adapter import adapt_index_spec __all__ = [ "adapt_fetch_response", + "adapt_index_spec", "adapt_query_response", "adapt_upsert_response", "UpsertResponseTransformer", diff --git a/pinecone/adapters/index_adapter.py b/pinecone/adapters/index_adapter.py new file mode 100644 index 00000000..ce2a6bff --- /dev/null +++ b/pinecone/adapters/index_adapter.py @@ -0,0 +1,237 @@ +"""Adapter functions for IndexModel spec resolution. + +This module provides adapter functions that handle the complex oneOf schema resolution +for IndexModel spec fields. This isolates the SDK wrapper code from the internal +structure of OpenAPI models and their deserialization logic. + +The adapter extracts spec resolution logic from the IndexModel wrapper, making it +easier to support future API format changes (e.g., schema-based dimension/metric). +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from pinecone.openapi_support.model_utils import deserialize_model + +if TYPE_CHECKING: + from pinecone.adapters.protocols import IndexModelAdapter + + +def adapt_index_spec(index: "IndexModelAdapter") -> Any: + """Adapt an IndexModel's spec field, handling oneOf schema resolution. + + The OpenAPI spec for IndexModel.spec is a oneOf union of Serverless, PodBased, + and BYOC types. The OpenAPI generator's deserialization sometimes fails to properly + resolve which variant to use, leaving spec as a raw dict. This adapter manually + detects the correct type and constructs the appropriate wrapper. + + This function handles three spec types: + - serverless: Contains nested ServerlessSpecResponse with optional ReadCapacity + - pod: Contains nested PodSpec + - byoc: Contains nested ByocSpec + + Args: + index: An IndexModel-like object conforming to IndexModelAdapter protocol. + + Returns: + The deserialized IndexSpec (Serverless, PodBased, or BYOC), or None if spec + is not present. Returns Any to satisfy mypy since the actual return types + are oneOf variants that don't have a common base type accessible here. + + Example: + >>> spec = adapt_index_spec(index) + >>> if hasattr(spec, 'serverless'): + ... print(f"Cloud: {spec.serverless.cloud}") + """ + from pinecone.core.openapi.db_control.model.index_spec import IndexSpec + + # Access _data_store directly to avoid OpenAPI model attribute resolution + spec_value = index._data_store.get("spec") + if spec_value is None: + # Fallback to getattr in case spec is stored differently + spec_value = getattr(index, "spec", None) + + if not isinstance(spec_value, dict): + # Already an IndexSpec instance or None + return spec_value + + # Get configuration from the underlying model for proper deserialization + config = index._configuration + path_to_item = index._path_to_item + + # Convert to list if needed and append 'spec' to path_to_item for proper error reporting + if isinstance(path_to_item, (list, tuple)): + spec_path = list(path_to_item) + ["spec"] + else: + spec_path = ["spec"] + + # Manually detect which oneOf schema to use based on discriminator keys + if "serverless" in spec_value: + return _adapt_serverless_spec(spec_value, spec_path, config) + elif "pod" in spec_value: + return _adapt_pod_spec(spec_value, spec_path, config) + elif "byoc" in spec_value: + return _adapt_byoc_spec(spec_value, spec_path, config) + else: + # Fallback: try deserialize_model (shouldn't happen with valid API responses) + return deserialize_model( + spec_value, + IndexSpec, + spec_path, + check_type=True, + configuration=config, + spec_property_naming=False, + ) + + +def _adapt_serverless_spec(spec_value: dict[str, Any], spec_path: list[str], config: Any) -> Any: + """Adapt a serverless spec, handling nested ReadCapacity oneOf. + + Args: + spec_value: Raw spec dict from _data_store + spec_path: Path to spec in response tree for error reporting + config: OpenAPI configuration object + + Returns: + Serverless wrapper instance containing ServerlessSpecResponse + """ + from pinecone.core.openapi.db_control.model.serverless import Serverless + from pinecone.core.openapi.db_control.model.serverless_spec_response import ( + ServerlessSpecResponse, + ) + from pinecone.core.openapi.db_control.model.read_capacity_response import ReadCapacityResponse + from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec_response import ( + ReadCapacityOnDemandSpecResponse, + ) + from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec_response import ( + ReadCapacityDedicatedSpecResponse, + ) + + # Deserialize the nested serverless dict to ServerlessSpecResponse + serverless_dict = dict(spec_value["serverless"]) + + # Handle nested read_capacity if present (it's also a oneOf with discriminator) + # Set to None if not present, as it's optional in the OpenAPI spec + read_capacity_spec = None + if "read_capacity" in serverless_dict and isinstance(serverless_dict["read_capacity"], dict): + read_capacity_dict = serverless_dict["read_capacity"] + mode = read_capacity_dict.get("mode") + + # Use discriminator to determine which ReadCapacity spec to use + if mode == "OnDemand": + read_capacity_spec = deserialize_model( + read_capacity_dict, + ReadCapacityOnDemandSpecResponse, + spec_path + ["serverless", "read_capacity"], + check_type=True, + configuration=config, + spec_property_naming=False, + ) + elif mode == "Dedicated": + read_capacity_spec = deserialize_model( + read_capacity_dict, + ReadCapacityDedicatedSpecResponse, + spec_path + ["serverless", "read_capacity"], + check_type=True, + configuration=config, + spec_property_naming=False, + ) + else: + # Fallback to ReadCapacityResponse (should use discriminator) + read_capacity_spec = deserialize_model( + read_capacity_dict, + ReadCapacityResponse, + spec_path + ["serverless", "read_capacity"], + check_type=True, + configuration=config, + spec_property_naming=False, + ) + + # Create ServerlessSpecResponse with all required and optional fields + serverless_spec = ServerlessSpecResponse._from_openapi_data( + cloud=serverless_dict["cloud"], + region=serverless_dict["region"], + read_capacity=read_capacity_spec, + source_collection=serverless_dict.get("source_collection"), + schema=serverless_dict.get("schema"), + _check_type=False, + _path_to_item=spec_path + ["serverless"], + _configuration=config, + _spec_property_naming=False, + ) + + # Instantiate Serverless wrapper, which IS the IndexSpec (oneOf union) + # Note: We use _check_type=False because ServerlessSpecResponse (from GET responses) + # is compatible with but not identical to ServerlessSpec (used in POST requests) + return Serverless._new_from_openapi_data( + serverless=serverless_spec, + _check_type=False, + _path_to_item=spec_path, + _configuration=config, + _spec_property_naming=False, + ) + + +def _adapt_pod_spec(spec_value: dict[str, Any], spec_path: list[str], config: Any) -> Any: + """Adapt a pod-based spec. + + Args: + spec_value: Raw spec dict from _data_store + spec_path: Path to spec in response tree for error reporting + config: OpenAPI configuration object + + Returns: + PodBased wrapper instance containing PodSpec + """ + from pinecone.core.openapi.db_control.model.pod_based import PodBased + from pinecone.core.openapi.db_control.model.pod_spec import PodSpec + + pod_spec = deserialize_model( + spec_value["pod"], + PodSpec, + spec_path + ["pod"], + check_type=True, + configuration=config, + spec_property_naming=False, + ) + + return PodBased._new_from_openapi_data( + pod=pod_spec, + _check_type=True, + _path_to_item=spec_path, + _configuration=config, + _spec_property_naming=False, + ) + + +def _adapt_byoc_spec(spec_value: dict[str, Any], spec_path: list[str], config: Any) -> Any: + """Adapt a BYOC (Bring Your Own Cloud) spec. + + Args: + spec_value: Raw spec dict from _data_store + spec_path: Path to spec in response tree for error reporting + config: OpenAPI configuration object + + Returns: + BYOC wrapper instance containing ByocSpec + """ + from pinecone.core.openapi.db_control.model.byoc import BYOC + from pinecone.core.openapi.db_control.model.byoc_spec import ByocSpec + + byoc_spec = deserialize_model( + spec_value["byoc"], + ByocSpec, + spec_path + ["byoc"], + check_type=True, + configuration=config, + spec_property_naming=False, + ) + + return BYOC._new_from_openapi_data( + byoc=byoc_spec, + _check_type=True, + _path_to_item=spec_path, + _configuration=config, + _spec_property_naming=False, + ) diff --git a/pinecone/adapters/protocols.py b/pinecone/adapters/protocols.py index f4c01e3e..05993110 100644 --- a/pinecone/adapters/protocols.py +++ b/pinecone/adapters/protocols.py @@ -36,14 +36,14 @@ class QueryResponseAdapter(Protocol): 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. + matches: List of scored vectors returned by the query (optional). + namespace: The namespace that was queried (optional). 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] + matches: list[ScoredVector] | None namespace: str | None usage: Usage | None _data_store: dict[str, Any] @@ -57,11 +57,11 @@ class UpsertResponseAdapter(Protocol): adapting an OpenAPI UpsertResponse to the SDK UpsertResponse dataclass. Attributes: - upserted_count: Number of vectors that were successfully upserted. + upserted_count: Number of vectors that were successfully upserted (optional). _response_info: Response metadata including headers. """ - upserted_count: int + upserted_count: int | None _response_info: Any @@ -73,13 +73,13 @@ class FetchResponseAdapter(Protocol): Attributes: namespace: The namespace from which vectors were fetched (optional). - vectors: Dictionary mapping vector IDs to Vector objects. + vectors: Dictionary mapping vector IDs to Vector objects (optional). usage: Optional usage statistics for the fetch operation. _response_info: Response metadata including headers. """ namespace: str | None - vectors: dict[str, Any] + vectors: dict[str, Any] | None usage: Usage | None _response_info: Any diff --git a/pinecone/adapters/response_adapters.py b/pinecone/adapters/response_adapters.py index b77d6f32..dfffb46b 100644 --- a/pinecone/adapters/response_adapters.py +++ b/pinecone/adapters/response_adapters.py @@ -57,7 +57,7 @@ def adapt_query_response(openapi_response: QueryResponseAdapter) -> QueryRespons openapi_response._data_store.pop("results", None) return QR( - matches=openapi_response.matches, + matches=openapi_response.matches or [], namespace=openapi_response.namespace or "", usage=openapi_response.usage if hasattr(openapi_response, "usage") and openapi_response.usage @@ -85,7 +85,12 @@ def adapt_upsert_response(openapi_response: UpsertResponseAdapter) -> UpsertResp response_info = extract_response_metadata(openapi_response) - return UR(upserted_count=openapi_response.upserted_count, _response_info=response_info) + return UR( + upserted_count=openapi_response.upserted_count + if openapi_response.upserted_count is not None + else 0, + _response_info=response_info, + ) def adapt_fetch_response(openapi_response: FetchResponseAdapter) -> FetchResponse: @@ -116,7 +121,9 @@ def adapt_fetch_response(openapi_response: FetchResponseAdapter) -> FetchRespons return FR( namespace=openapi_response.namespace or "", - vectors={k: Vector.from_dict(v) for k, v in openapi_response.vectors.items()}, + vectors={k: Vector.from_dict(v) for k, v in openapi_response.vectors.items()} + if openapi_response.vectors is not None + else {}, usage=openapi_response.usage, _response_info=response_info, ) diff --git a/pinecone/db_control/models/index_model.py b/pinecone/db_control/models/index_model.py index 769667df..b58d70a6 100644 --- a/pinecone/db_control/models/index_model.py +++ b/pinecone/db_control/models/index_model.py @@ -1,27 +1,14 @@ from pinecone.core.openapi.db_control.model.index_model import IndexModel as OpenAPIIndexModel -from pinecone.core.openapi.db_control.model.index_spec import IndexSpec -from pinecone.core.openapi.db_control.model.serverless import Serverless -from pinecone.core.openapi.db_control.model.serverless_spec_response import ServerlessSpecResponse -from pinecone.core.openapi.db_control.model.read_capacity_response import ReadCapacityResponse -from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec_response import ( - ReadCapacityOnDemandSpecResponse, -) -from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec_response import ( - ReadCapacityDedicatedSpecResponse, -) -from pinecone.core.openapi.db_control.model.pod_based import PodBased -from pinecone.core.openapi.db_control.model.pod_spec import PodSpec -from pinecone.core.openapi.db_control.model.byoc import BYOC -from pinecone.core.openapi.db_control.model.byoc_spec import ByocSpec import json +from typing import Any from pinecone.utils.repr_overrides import custom_serializer -from pinecone.openapi_support.model_utils import deserialize_model +from pinecone.adapters import adapt_index_spec class IndexModel: def __init__(self, index: OpenAPIIndexModel): self.index = index - self._spec_cache = None + self._spec_cache: Any = None def __str__(self): return str(self.index) @@ -32,137 +19,15 @@ def __getattr__(self, attr): return getattr(self.index, attr) def _get_spec(self): + """Get the index spec, using adapter for oneOf schema resolution. + + Delegates to adapt_index_spec() which handles the complex logic of + deserializing serverless/pod/byoc spec variants. + """ if self._spec_cache is not None: return self._spec_cache - # Access _data_store directly to avoid OpenAPI model attribute resolution - spec_value = self.index._data_store.get("spec") - if spec_value is None: - # Fallback to getattr in case spec is stored differently - spec_value = getattr(self.index, "spec", None) - - if isinstance(spec_value, dict): - # Manually detect which oneOf schema to use and construct it directly - # This bypasses the broken oneOf matching logic in deserialize_model - # Get configuration from the underlying model if available - config = getattr(self.index, "_configuration", None) - path_to_item = getattr(self.index, "_path_to_item", ()) - # Convert to list if needed and append 'spec' to path_to_item for proper error reporting - if isinstance(path_to_item, (list, tuple)): - spec_path = list(path_to_item) + ["spec"] - else: - spec_path = ["spec"] - - # Check which oneOf key exists and construct the appropriate wrapper class - if "serverless" in spec_value: - # Deserialize the nested serverless dict to ServerlessSpecResponse - # (responses use ServerlessSpecResponse, not ServerlessSpec) - # First, handle nested read_capacity if present (it's also a oneOf with discriminator) - serverless_dict = dict(spec_value["serverless"]) - if "read_capacity" in serverless_dict and isinstance( - serverless_dict["read_capacity"], dict - ): - read_capacity_dict = serverless_dict["read_capacity"] - # Use discriminator to determine which ReadCapacity spec to use - mode = read_capacity_dict.get("mode") - if mode == "OnDemand": - read_capacity_spec = deserialize_model( - read_capacity_dict, - ReadCapacityOnDemandSpecResponse, - spec_path + ["serverless", "read_capacity"], - check_type=True, - configuration=config, - spec_property_naming=False, - ) - elif mode == "Dedicated": - read_capacity_spec = deserialize_model( - read_capacity_dict, - ReadCapacityDedicatedSpecResponse, - spec_path + ["serverless", "read_capacity"], - check_type=True, - configuration=config, - spec_property_naming=False, - ) - else: - # Fallback to ReadCapacityResponse (should use discriminator) - read_capacity_spec = deserialize_model( - read_capacity_dict, - ReadCapacityResponse, - spec_path + ["serverless", "read_capacity"], - check_type=True, - configuration=config, - spec_property_naming=False, - ) - serverless_dict["read_capacity"] = read_capacity_spec - - serverless_spec = deserialize_model( - serverless_dict, - ServerlessSpecResponse, - spec_path + ["serverless"], - check_type=True, - configuration=config, - spec_property_naming=False, - ) - # Instantiate Serverless wrapper, which IS the IndexSpec (oneOf union) - self._spec_cache = Serverless._new_from_openapi_data( - serverless=serverless_spec, - _check_type=True, - _path_to_item=spec_path, - _configuration=config, - _spec_property_naming=False, - ) - elif "pod" in spec_value: - # Deserialize the nested pod dict to PodSpec - pod_spec = deserialize_model( - spec_value["pod"], - PodSpec, - spec_path + ["pod"], - check_type=True, - configuration=config, - spec_property_naming=False, - ) - # Instantiate PodBased wrapper, which IS the IndexSpec (oneOf union) - self._spec_cache = PodBased._new_from_openapi_data( - pod=pod_spec, - _check_type=True, - _path_to_item=spec_path, - _configuration=config, - _spec_property_naming=False, - ) - elif "byoc" in spec_value: - # Deserialize the nested byoc dict to ByocSpec - byoc_spec = deserialize_model( - spec_value["byoc"], - ByocSpec, - spec_path + ["byoc"], - check_type=True, - configuration=config, - spec_property_naming=False, - ) - # Instantiate BYOC wrapper, which IS the IndexSpec (oneOf union) - self._spec_cache = BYOC._new_from_openapi_data( - byoc=byoc_spec, - _check_type=True, - _path_to_item=spec_path, - _configuration=config, - _spec_property_naming=False, - ) - else: - # Fallback: try deserialize_model (shouldn't happen with valid API responses) - self._spec_cache = deserialize_model( - spec_value, - IndexSpec, - spec_path, - check_type=True, - configuration=config, - spec_property_naming=False, - ) - elif spec_value is None: - self._spec_cache = None - else: - # Already an IndexSpec instance or some other object - self._spec_cache = spec_value - + self._spec_cache = adapt_index_spec(self.index) return self._spec_cache def __getitem__(self, key): diff --git a/tests/fixtures/db_data_models.py b/tests/fixtures/db_data_models.py index 089c48e8..4dd54a78 100644 --- a/tests/fixtures/db_data_models.py +++ b/tests/fixtures/db_data_models.py @@ -345,8 +345,8 @@ def make_scored_vector( def make_openapi_query_response( - matches: Optional[List[OpenApiScoredVector]] = None, - namespace: str = "", + matches: Optional[List[OpenApiScoredVector]] = _UNSET, # type: ignore[assignment] + namespace: Optional[str] = _UNSET, # type: ignore[assignment] usage: Optional[OpenApiUsage] = None, _check_type: bool = False, **overrides: Any, @@ -354,8 +354,8 @@ def make_openapi_query_response( """Create an OpenApiQueryResponse instance. Args: - matches: List of scored vectors - namespace: Query namespace + matches: List of scored vectors. Defaults to []. Pass None to omit. + namespace: Query namespace. Defaults to "". Pass None to omit. usage: Usage information _check_type: Whether to enable type checking **overrides: Additional fields to override @@ -363,14 +363,17 @@ def make_openapi_query_response( Returns: An OpenApiQueryResponse instance """ - if matches is None: - matches = [] + kwargs: Dict[str, Any] = {"_check_type": _check_type} + + if matches is _UNSET: + kwargs["matches"] = [] + else: + kwargs["matches"] = matches - kwargs: Dict[str, Any] = { - "matches": matches, - "namespace": namespace, - "_check_type": _check_type, - } + if namespace is _UNSET: + kwargs["namespace"] = "" + else: + kwargs["namespace"] = namespace if usage is not None: kwargs["usage"] = usage @@ -380,26 +383,34 @@ def make_openapi_query_response( def make_openapi_upsert_response( - upserted_count: int = 10, _check_type: bool = False, **overrides: Any + upserted_count: Optional[int] = _UNSET, # type: ignore[assignment] + _check_type: bool = False, + **overrides: Any, ) -> OpenApiUpsertResponse: """Create an OpenApiUpsertResponse instance. Args: - upserted_count: Number of vectors upserted + upserted_count: Number of vectors upserted. Defaults to 10. Pass None to omit. _check_type: Whether to enable type checking **overrides: Additional fields to override Returns: An OpenApiUpsertResponse instance """ - kwargs: Dict[str, Any] = {"upserted_count": upserted_count, "_check_type": _check_type} + kwargs: Dict[str, Any] = {"_check_type": _check_type} + + if upserted_count is _UNSET: + kwargs["upserted_count"] = 10 + else: + kwargs["upserted_count"] = upserted_count + kwargs.update(overrides) return OpenApiUpsertResponse(**kwargs) def make_openapi_fetch_response( - vectors: Optional[Dict[str, Dict[str, Any]]] = None, - namespace: str = "", + vectors: Optional[Dict[str, Dict[str, Any]]] = _UNSET, # type: ignore[assignment] + namespace: Optional[str] = _UNSET, # type: ignore[assignment] usage: Optional[OpenApiUsage] = None, _check_type: bool = False, **overrides: Any, @@ -407,8 +418,8 @@ def make_openapi_fetch_response( """Create an OpenApiFetchResponse instance. Args: - vectors: Dictionary mapping vector IDs to vector data - namespace: Fetch namespace + vectors: Dictionary mapping vector IDs to vector data. Defaults to {}. Pass None to omit. + namespace: Fetch namespace. Defaults to "". Pass None to omit. usage: Usage information _check_type: Whether to enable type checking **overrides: Additional fields to override @@ -416,14 +427,17 @@ def make_openapi_fetch_response( Returns: An OpenApiFetchResponse instance """ - if vectors is None: - vectors = {} + kwargs: Dict[str, Any] = {"_check_type": _check_type} + + if vectors is _UNSET: + kwargs["vectors"] = {} + else: + kwargs["vectors"] = vectors - kwargs: Dict[str, Any] = { - "vectors": vectors, - "namespace": namespace, - "_check_type": _check_type, - } + if namespace is _UNSET: + kwargs["namespace"] = "" + else: + kwargs["namespace"] = namespace if usage is not None: kwargs["usage"] = usage diff --git a/tests/unit/adapters/test_index_adapter.py b/tests/unit/adapters/test_index_adapter.py new file mode 100644 index 00000000..f583d3d3 --- /dev/null +++ b/tests/unit/adapters/test_index_adapter.py @@ -0,0 +1,150 @@ +"""Unit tests for index adapter functions. + +Tests the adapt_index_spec function that handles oneOf schema resolution +for IndexModel spec fields. +""" + +from pinecone.adapters import adapt_index_spec +from pinecone.core.openapi.db_control.model.serverless import Serverless +from pinecone.core.openapi.db_control.model.pod_based import PodBased +from pinecone.core.openapi.db_control.model.byoc import BYOC +from tests.fixtures import make_index_model + + +class TestAdaptIndexSpec: + """Test adapt_index_spec with different spec types.""" + + def test_adapt_serverless_spec_basic(self): + """Test adapting a basic serverless spec without read_capacity.""" + openapi_model = make_index_model( + name="test-index", spec={"serverless": {"cloud": "aws", "region": "us-east-1"}} + ) + + result = adapt_index_spec(openapi_model) + + assert result is not None + assert isinstance(result, Serverless) + assert hasattr(result, "serverless") + assert result.serverless.cloud == "aws" + assert result.serverless.region == "us-east-1" + + def test_adapt_pod_spec(self): + """Test adapting a pod-based spec.""" + openapi_model = make_index_model( + name="test-index", + spec={ + "pod": { + "environment": "us-east-1-aws", + "replicas": 1, + "shards": 1, + "pod_type": "p1.x1", + "pods": 1, + } + }, + ) + + result = adapt_index_spec(openapi_model) + + assert result is not None + assert isinstance(result, PodBased) + assert hasattr(result, "pod") + assert result.pod.environment == "us-east-1-aws" + assert result.pod.replicas == 1 + assert result.pod.shards == 1 + assert result.pod.pod_type == "p1.x1" + + def test_adapt_byoc_spec(self): + """Test adapting a BYOC (Bring Your Own Cloud) spec.""" + openapi_model = make_index_model( + name="test-index", spec={"byoc": {"environment": "custom-env"}} + ) + + result = adapt_index_spec(openapi_model) + + assert result is not None + assert isinstance(result, BYOC) + assert hasattr(result, "byoc") + assert result.byoc.environment == "custom-env" + + def test_adapt_spec_returns_none_when_spec_is_none(self): + """Test that None is returned when spec is not present.""" + # Create a model without spec in _data_store + openapi_model = make_index_model(name="test-index") + # Manually remove spec from _data_store to simulate missing spec + if "spec" in openapi_model._data_store: + del openapi_model._data_store["spec"] + + result = adapt_index_spec(openapi_model) + + assert result is None + + def test_adapt_spec_caching_in_index_model(self): + """Test that IndexModel properly caches the adapted spec.""" + from pinecone.db_control.models import IndexModel + + openapi_model = make_index_model( + name="test-index", spec={"serverless": {"cloud": "aws", "region": "us-east-1"}} + ) + + wrapped = IndexModel(openapi_model) + + # First access should populate cache + spec1 = wrapped.spec + assert spec1 is not None + + # Second access should return cached value + spec2 = wrapped.spec + assert spec2 is spec1 # Same object instance + + def test_adapt_spec_handles_already_deserialized_spec(self): + """Test that adapter handles specs that are already IndexSpec instances.""" + from pinecone.core.openapi.db_control.model.serverless import Serverless + from pinecone.core.openapi.db_control.model.serverless_spec_response import ( + ServerlessSpecResponse, + ) + + # Create a fully deserialized spec + serverless_spec = ServerlessSpecResponse._from_openapi_data( + cloud="aws", region="us-east-1", read_capacity=None, _check_type=False + ) + already_deserialized = Serverless._new_from_openapi_data( + serverless=serverless_spec, _check_type=False + ) + + openapi_model = make_index_model(name="test-index") + # Replace the dict spec with an already-deserialized one + openapi_model._data_store["spec"] = already_deserialized + + result = adapt_index_spec(openapi_model) + + # Should return the already-deserialized spec as-is + assert result is already_deserialized + + +class TestIndexModelAdapterProtocolCompliance: + """Test that OpenAPI IndexModel conforms to IndexModelAdapter protocol.""" + + def test_openapi_index_model_has_data_store(self): + """Verify OpenAPI IndexModel has _data_store attribute.""" + from pinecone.adapters.protocols import IndexModelAdapter + + openapi_model = make_index_model() + _protocol_check: IndexModelAdapter = openapi_model + assert hasattr(openapi_model, "_data_store") + assert isinstance(openapi_model._data_store, dict) + + def test_openapi_index_model_has_configuration(self): + """Verify OpenAPI IndexModel has _configuration attribute.""" + from pinecone.adapters.protocols import IndexModelAdapter + + openapi_model = make_index_model() + _protocol_check: IndexModelAdapter = openapi_model + assert hasattr(openapi_model, "_configuration") + + def test_openapi_index_model_has_path_to_item(self): + """Verify OpenAPI IndexModel has _path_to_item attribute.""" + from pinecone.adapters.protocols import IndexModelAdapter + + openapi_model = make_index_model() + _protocol_check: IndexModelAdapter = openapi_model + assert hasattr(openapi_model, "_path_to_item") diff --git a/tests/unit/adapters/test_protocols.py b/tests/unit/adapters/test_protocols.py index d2397b51..d668edc1 100644 --- a/tests/unit/adapters/test_protocols.py +++ b/tests/unit/adapters/test_protocols.py @@ -12,6 +12,11 @@ IndexModelAdapter, IndexStatusAdapter, ) +from pinecone.adapters.response_adapters import ( + adapt_query_response, + adapt_upsert_response, + adapt_fetch_response, +) from tests.fixtures import ( make_openapi_query_response, make_openapi_upsert_response, @@ -154,3 +159,54 @@ def test_openapi_index_status_has_required_attributes(self): assert hasattr(status, "state") assert status.ready is True assert status.state == "Ready" + + +class TestAdapterNoneHandling: + """Tests that adapters handle None/optional fields correctly.""" + + def test_adapt_query_response_with_none_matches(self): + """Test that adapt_query_response handles None matches gracefully.""" + openapi_response = make_openapi_query_response(matches=None, namespace="test") + sdk_response = adapt_query_response(openapi_response) + + assert sdk_response.matches == [] + assert sdk_response.namespace == "test" + + def test_adapt_query_response_with_none_namespace(self): + """Test that adapt_query_response handles None namespace gracefully.""" + openapi_response = make_openapi_query_response(matches=[], namespace=None) + sdk_response = adapt_query_response(openapi_response) + + assert sdk_response.matches == [] + assert sdk_response.namespace == "" + + def test_adapt_upsert_response_with_none_upserted_count(self): + """Test that adapt_upsert_response handles None upserted_count gracefully.""" + openapi_response = make_openapi_upsert_response(upserted_count=None) + sdk_response = adapt_upsert_response(openapi_response) + + assert sdk_response.upserted_count == 0 + + def test_adapt_fetch_response_with_none_namespace(self): + """Test that adapt_fetch_response handles None namespace gracefully.""" + openapi_response = make_openapi_fetch_response(vectors={}, namespace=None) + sdk_response = adapt_fetch_response(openapi_response) + + assert sdk_response.namespace == "" + assert sdk_response.vectors == {} + + def test_adapt_fetch_response_with_none_vectors(self): + """Test that adapt_fetch_response handles None vectors gracefully.""" + openapi_response = make_openapi_fetch_response(vectors=None, namespace="test") + sdk_response = adapt_fetch_response(openapi_response) + + assert sdk_response.namespace == "test" + assert sdk_response.vectors == {} + + def test_adapt_fetch_response_with_all_none_optionals(self): + """Test that adapt_fetch_response handles all None optional fields.""" + openapi_response = make_openapi_fetch_response(vectors=None, namespace=None) + sdk_response = adapt_fetch_response(openapi_response) + + assert sdk_response.namespace == "" + assert sdk_response.vectors == {} From 543743acf5eb33df1934977217116d3dab6629d6 Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Tue, 3 Feb 2026 11:59:34 -0500 Subject: [PATCH 4/4] fix: preserve already-deserialized read_capacity and remove unused export Address Cursor Bugbot feedback: - Fix: preserve already-deserialized read_capacity values instead of replacing with None - Refactor: remove IndexStatusAdapter from public exports (still used in tests for protocol compliance) Co-authored-by: Cursor --- pinecone/adapters/__init__.py | 2 -- pinecone/adapters/index_adapter.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pinecone/adapters/__init__.py b/pinecone/adapters/__init__.py index 15030aa9..2f2f25e5 100644 --- a/pinecone/adapters/__init__.py +++ b/pinecone/adapters/__init__.py @@ -16,7 +16,6 @@ from pinecone.adapters.protocols import ( FetchResponseAdapter, IndexModelAdapter, - IndexStatusAdapter, QueryResponseAdapter, UpsertResponseAdapter, ) @@ -36,7 +35,6 @@ "UpsertResponseTransformer", "FetchResponseAdapter", "IndexModelAdapter", - "IndexStatusAdapter", "QueryResponseAdapter", "UpsertResponseAdapter", ] diff --git a/pinecone/adapters/index_adapter.py b/pinecone/adapters/index_adapter.py index ce2a6bff..df931d14 100644 --- a/pinecone/adapters/index_adapter.py +++ b/pinecone/adapters/index_adapter.py @@ -112,8 +112,8 @@ def _adapt_serverless_spec(spec_value: dict[str, Any], spec_path: list[str], con serverless_dict = dict(spec_value["serverless"]) # Handle nested read_capacity if present (it's also a oneOf with discriminator) - # Set to None if not present, as it's optional in the OpenAPI spec - read_capacity_spec = None + # Preserve already-deserialized values, only deserialize dicts + read_capacity_spec = serverless_dict.get("read_capacity") if "read_capacity" in serverless_dict and isinstance(serverless_dict["read_capacity"], dict): read_capacity_dict = serverless_dict["read_capacity"] mode = read_capacity_dict.get("mode")