diff --git a/docs/db_control/serverless-indexes.md b/docs/db_control/serverless-indexes.md index 1c3944f3d..0a9e71ff3 100644 --- a/docs/db_control/serverless-indexes.md +++ b/docs/db_control/serverless-indexes.md @@ -126,6 +126,133 @@ pc.create_index( ) ``` +## Read Capacity Configuration + +You can configure the read capacity mode for your serverless index. By default, indexes are created with `OnDemand` mode. You can also specify `Dedicated` mode with dedicated read nodes. + +### Dedicated Read Capacity + +Dedicated mode allocates dedicated read nodes for your workload. You must specify `node_type`, `scaling`, and scaling configuration. + +```python +from pinecone import ( + Pinecone, + ServerlessSpec, + CloudProvider, + GcpRegion, + Metric +) + +pc = Pinecone(api_key='<>') + +pc.create_index( + name='my-index', + dimension=1536, + metric=Metric.COSINE, + spec=ServerlessSpec( + cloud=CloudProvider.GCP, + region=GcpRegion.US_CENTRAL1, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": { + "shards": 2, + "replicas": 2 + } + } + } + ) +) +``` + +### Configuring Read Capacity + +You can change the read capacity configuration of an existing serverless index using `configure_index`. This allows you to: + +- Switch between OnDemand and Dedicated modes +- Adjust the number of shards and replicas for Dedicated mode with manual scaling + +```python +from pinecone import Pinecone + +pc = Pinecone(api_key='<>') + +# Switch to OnDemand read capacity +pc.configure_index( + name='my-index', + read_capacity={"mode": "OnDemand"} +) + +# Switch to Dedicated read capacity with manual scaling +pc.configure_index( + name='my-index', + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": { + "shards": 3, + "replicas": 2 + } + } + } +) + +# Scale up by increasing shards and replicas +pc.configure_index( + name='my-index', + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": { + "shards": 4, + "replicas": 3 + } + } + } +) +``` + +When you change read capacity configuration, the index will transition to the new configuration. You can use `describe_index` to check the status of the transition. + +## Metadata Schema Configuration + +You can configure which metadata fields are filterable by specifying a metadata schema. By default, all metadata fields are indexed. However, large amounts of metadata can cause slower index building as well as slower query execution, particularly when data is not cached in a query executor's memory and local SSD and must be fetched from object storage. + +To prevent performance issues due to excessive metadata, you can limit metadata indexing to the fields that you plan to use for query filtering. When you specify a metadata schema, only fields marked as `filterable: True` are indexed and can be used in filters. + +```python +from pinecone import ( + Pinecone, + ServerlessSpec, + CloudProvider, + AwsRegion, + Metric +) + +pc = Pinecone(api_key='<>') + +pc.create_index( + name='my-index', + dimension=1536, + metric=Metric.COSINE, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_WEST_2, + schema={ + "genre": {"filterable": True}, + "year": {"filterable": True}, + "description": {"filterable": True} + } + ) +) +``` + ## Configuring, listing, describing, and deleting See [shared index actions](shared-index-actions.md) to learn about how to manage the lifecycle of your index after it is created. diff --git a/pinecone/__init__.py b/pinecone/__init__.py index 3b9dde4f6..242054d82 100644 --- a/pinecone/__init__.py +++ b/pinecone/__init__.py @@ -104,6 +104,29 @@ "pinecone.db_control.types", "CreateIndexForModelEmbedTypedDict", ), + # Read capacity TypedDict classes + "ScalingConfigManualDict": ( + "pinecone.db_control.models.serverless_spec", + "ScalingConfigManualDict", + ), + "ReadCapacityDedicatedConfigDict": ( + "pinecone.db_control.models.serverless_spec", + "ReadCapacityDedicatedConfigDict", + ), + "ReadCapacityOnDemandDict": ( + "pinecone.db_control.models.serverless_spec", + "ReadCapacityOnDemandDict", + ), + "ReadCapacityDedicatedDict": ( + "pinecone.db_control.models.serverless_spec", + "ReadCapacityDedicatedDict", + ), + "ReadCapacityDict": ("pinecone.db_control.models.serverless_spec", "ReadCapacityDict"), + # Metadata schema TypedDict class + "MetadataSchemaFieldConfig": ( + "pinecone.db_control.models.serverless_spec", + "MetadataSchemaFieldConfig", + ), } _config_lazy_imports = { diff --git a/pinecone/db_control/models/backup_model.py b/pinecone/db_control/models/backup_model.py index 59dec7ba4..be2c340a7 100644 --- a/pinecone/db_control/models/backup_model.py +++ b/pinecone/db_control/models/backup_model.py @@ -1,12 +1,40 @@ import json +from typing import Optional, TYPE_CHECKING from pinecone.core.openapi.db_control.model.backup_model import BackupModel as OpenAPIBackupModel from pinecone.utils.repr_overrides import custom_serializer +if TYPE_CHECKING: + from pinecone.core.openapi.db_control.model.backup_model_schema import BackupModelSchema + class BackupModel: + """Represents a Pinecone backup configuration and status. + + The BackupModel describes the configuration and status of a Pinecone backup, + including metadata about the source index, backup location, and schema + configuration. + """ + def __init__(self, backup: OpenAPIBackupModel): self._backup = backup + @property + def schema(self) -> Optional["BackupModelSchema"]: + """Schema for the behavior of Pinecone's internal metadata index. + + This property defines which metadata fields are indexed and filterable + in the backup. By default, all metadata is indexed. When ``schema`` is + present, only fields which are present in the ``fields`` object with + ``filterable: true`` are indexed. + + The schema is a map of metadata field names to their configuration, + where each field configuration specifies whether the field is filterable. + + :type: BackupModelSchema, optional + :returns: The metadata schema configuration, or None if not set. + """ + return getattr(self._backup, "schema", None) + def __getattr__(self, attr): return getattr(self._backup, attr) diff --git a/pinecone/db_control/models/serverless_spec.py b/pinecone/db_control/models/serverless_spec.py index 1fc515640..f7adc64d5 100644 --- a/pinecone/db_control/models/serverless_spec.py +++ b/pinecone/db_control/models/serverless_spec.py @@ -1,25 +1,117 @@ from dataclasses import dataclass -from typing import Union +from typing import Union, Optional, Dict, Any, TypedDict, TYPE_CHECKING, Literal from enum import Enum +try: + from typing_extensions import NotRequired +except ImportError: + try: + from typing import NotRequired # type: ignore + except ImportError: + # Fallback for older Python versions - NotRequired not available + NotRequired = None # type: ignore + from ..enums import CloudProvider, AwsRegion, GcpRegion, AzureRegion +if TYPE_CHECKING: + from pinecone.core.openapi.db_control.model.read_capacity import ReadCapacity + from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec import ( + ReadCapacityOnDemandSpec, + ) + from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec import ( + ReadCapacityDedicatedSpec, + ) + + +class ScalingConfigManualDict(TypedDict, total=False): + """TypedDict for manual scaling configuration.""" + + shards: int + replicas: int + + +if NotRequired is not None: + # Python 3.11+ or typing_extensions available - use NotRequired for better type hints + class ReadCapacityDedicatedConfigDict(TypedDict): + """TypedDict for dedicated read capacity configuration. + + Required fields: node_type, scaling + Optional fields: manual + """ + + node_type: str # Required: "t1" or "b1" + scaling: str # Required: "Manual" or other scaling types + manual: NotRequired[ScalingConfigManualDict] # Optional +else: + # Fallback for older Python versions - all fields optional + class ReadCapacityDedicatedConfigDict(TypedDict, total=False): # type: ignore[no-redef] + """TypedDict for dedicated read capacity configuration. + + Note: In older Python versions without NotRequired support, all fields + are marked as optional. However, node_type and scaling are required + when using Dedicated mode. Users must provide these fields. + """ + + node_type: str # Required: "t1" or "b1" + scaling: str # Required: "Manual" or other scaling types + manual: ScalingConfigManualDict # Optional + + +class ReadCapacityOnDemandDict(TypedDict): + """TypedDict for OnDemand read capacity mode.""" + + mode: Literal["OnDemand"] + + +class ReadCapacityDedicatedDict(TypedDict): + """TypedDict for Dedicated read capacity mode.""" + + mode: Literal["Dedicated"] + dedicated: ReadCapacityDedicatedConfigDict + + +ReadCapacityDict = Union[ReadCapacityOnDemandDict, ReadCapacityDedicatedDict] + +if TYPE_CHECKING: + ReadCapacityType = Union[ + ReadCapacityDict, "ReadCapacity", "ReadCapacityOnDemandSpec", "ReadCapacityDedicatedSpec" + ] +else: + ReadCapacityType = Union[ReadCapacityDict, Any] + + +class MetadataSchemaFieldConfig(TypedDict): + """TypedDict for metadata schema field configuration.""" + + filterable: bool + @dataclass(frozen=True) class ServerlessSpec: cloud: str region: str + read_capacity: Optional[ReadCapacityType] = None + schema: Optional[Dict[str, MetadataSchemaFieldConfig]] = None def __init__( self, cloud: Union[CloudProvider, str], region: Union[AwsRegion, GcpRegion, AzureRegion, str], + read_capacity: Optional[ReadCapacityType] = None, + schema: Optional[Dict[str, MetadataSchemaFieldConfig]] = None, ): # Convert Enums to their string values if necessary object.__setattr__(self, "cloud", cloud.value if isinstance(cloud, Enum) else str(cloud)) object.__setattr__( self, "region", region.value if isinstance(region, Enum) else str(region) ) + object.__setattr__(self, "read_capacity", read_capacity) + object.__setattr__(self, "schema", schema) def asdict(self): - return {"serverless": {"cloud": self.cloud, "region": self.region}} + result = {"serverless": {"cloud": self.cloud, "region": self.region}} + if self.read_capacity is not None: + result["serverless"]["read_capacity"] = self.read_capacity + if self.schema is not None: + result["serverless"]["schema"] = {"fields": self.schema} + return result diff --git a/pinecone/db_control/request_factory.py b/pinecone/db_control/request_factory.py index 2cd674cab..32a456482 100644 --- a/pinecone/db_control/request_factory.py +++ b/pinecone/db_control/request_factory.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Dict, Any, Union +from typing import Optional, Dict, Any, Union, TYPE_CHECKING from enum import Enum from pinecone.utils import parse_non_empty_args, convert_enum_to_string @@ -21,6 +21,20 @@ from pinecone.core.openapi.db_control.model.serverless_spec import ( ServerlessSpec as ServerlessSpecModel, ) +from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec import ( + ReadCapacityOnDemandSpec, +) +from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec import ( + ReadCapacityDedicatedSpec, +) +from pinecone.core.openapi.db_control.model.read_capacity_dedicated_config import ( + ReadCapacityDedicatedConfig, +) +from pinecone.core.openapi.db_control.model.scaling_config_manual import ScalingConfigManual +from pinecone.core.openapi.db_control.model.backup_model_schema import BackupModelSchema +from pinecone.core.openapi.db_control.model.backup_model_schema_fields import ( + BackupModelSchemaFields, +) from pinecone.core.openapi.db_control.model.byoc_spec import ByocSpec as ByocSpecModel from pinecone.core.openapi.db_control.model.pod_spec import PodSpec as PodSpecModel from pinecone.core.openapi.db_control.model.pod_spec_metadata_config import PodSpecMetadataConfig @@ -41,6 +55,12 @@ ) from .types import CreateIndexForModelEmbedTypedDict, ConfigureIndexEmbed +if TYPE_CHECKING: + from pinecone.db_control.models.serverless_spec import ( + ReadCapacityDict, + MetadataSchemaFieldConfig, + ) + from pinecone.core.openapi.db_control.model.read_capacity import ReadCapacity logger = logging.getLogger(__name__) """ :meta private: """ @@ -68,6 +88,144 @@ def __parse_deletion_protection(deletion_protection: Union[DeletionProtection, s else: raise ValueError("deletion_protection must be either 'enabled' or 'disabled'") + @staticmethod + def __parse_read_capacity( + read_capacity: Union[ + "ReadCapacityDict", "ReadCapacity", ReadCapacityOnDemandSpec, ReadCapacityDedicatedSpec + ], + ) -> Union[ReadCapacityOnDemandSpec, ReadCapacityDedicatedSpec, "ReadCapacity"]: + """Parse read_capacity dict into appropriate ReadCapacity model instance. + + :param read_capacity: Dict with read capacity configuration or existing ReadCapacity model instance + :return: ReadCapacityOnDemandSpec, ReadCapacityDedicatedSpec, or existing model instance + """ + if isinstance(read_capacity, dict): + mode = read_capacity.get("mode", "OnDemand") + if mode == "OnDemand": + return ReadCapacityOnDemandSpec(mode="OnDemand") + elif mode == "Dedicated": + dedicated_dict: Dict[str, Any] = read_capacity.get("dedicated", {}) # type: ignore + # Construct ReadCapacityDedicatedConfig + # node_type and scaling are required fields + if "node_type" not in dedicated_dict or dedicated_dict.get("node_type") is None: + raise ValueError( + "node_type is required when using Dedicated read capacity mode. " + "Please specify 'node_type' (e.g., 't1' or 'b1') in the 'dedicated' configuration." + ) + if "scaling" not in dedicated_dict or dedicated_dict.get("scaling") is None: + raise ValueError( + "scaling is required when using Dedicated read capacity mode. " + "Please specify 'scaling' (e.g., 'Manual') in the 'dedicated' configuration." + ) + node_type = dedicated_dict["node_type"] + scaling = dedicated_dict["scaling"] + dedicated_config_kwargs = {"node_type": node_type, "scaling": scaling} + + # Validate that manual scaling configuration is provided when scaling is "Manual" + if scaling == "Manual": + if "manual" not in dedicated_dict or dedicated_dict.get("manual") is None: + raise ValueError( + "When using 'Manual' scaling with Dedicated read capacity mode, " + "the 'manual' field with 'shards' and 'replicas' is required. " + "Please specify 'manual': {'shards': , 'replicas': } " + "in the 'dedicated' configuration." + ) + manual_dict = dedicated_dict["manual"] + if not isinstance(manual_dict, dict): + raise ValueError( + "The 'manual' field must be a dictionary with 'shards' and 'replicas' keys." + ) + if "shards" not in manual_dict or "replicas" not in manual_dict: + missing = [] + if "shards" not in manual_dict: + missing.append("shards") + if "replicas" not in manual_dict: + missing.append("replicas") + raise ValueError( + f"The 'manual' configuration is missing required fields: {', '.join(missing)}. " + "Please provide both 'shards' and 'replicas' in the 'manual' configuration." + ) + dedicated_config_kwargs["manual"] = ScalingConfigManual(**manual_dict) + elif "manual" in dedicated_dict: + # Allow manual to be provided for other scaling types (future compatibility) + manual_dict = dedicated_dict["manual"] + dedicated_config_kwargs["manual"] = ScalingConfigManual(**manual_dict) + + dedicated_config = ReadCapacityDedicatedConfig(**dedicated_config_kwargs) + return ReadCapacityDedicatedSpec(mode="Dedicated", dedicated=dedicated_config) + else: + # Fallback: let OpenAPI handle it + return read_capacity # type: ignore + else: + # Already a ReadCapacity model instance + return read_capacity # type: ignore + + @staticmethod + def __parse_schema( + schema: Union[ + Dict[ + str, "MetadataSchemaFieldConfig" + ], # Direct field mapping: {field_name: {filterable: bool}} + Dict[ + str, Dict[str, Any] + ], # Dict with "fields" wrapper: {"fields": {field_name: {...}}, ...} + BackupModelSchema, # OpenAPI model instance + ], + ) -> BackupModelSchema: + """Parse schema dict into BackupModelSchema instance. + + :param schema: Dict with schema configuration (either {field_name: {filterable: bool, ...}} or + {"fields": {field_name: {filterable: bool, ...}}, ...}) or existing BackupModelSchema instance + :return: BackupModelSchema instance + """ + if isinstance(schema, dict): + schema_kwargs: Dict[str, Any] = {} + # Handle two formats: + # 1. {field_name: {filterable: bool, ...}} - direct field mapping + # 2. {"fields": {field_name: {filterable: bool, ...}}, ...} - with fields wrapper + if "fields" in schema: + # Format 2: has fields wrapper + fields = {} + for field_name, field_config in schema["fields"].items(): + if isinstance(field_config, dict): + # Pass through the entire field_config dict to allow future API fields + fields[field_name] = BackupModelSchemaFields(**field_config) + else: + # If not a dict, create with default filterable=True + fields[field_name] = BackupModelSchemaFields(filterable=True) + schema_kwargs["fields"] = fields + + # Pass through any other fields in schema_dict to allow future API fields + for key, value in schema.items(): + if key != "fields": + schema_kwargs[key] = value + else: + # Format 1: direct field mapping + # All items in schema are treated as field_name: field_config pairs + fields = {} + for field_name, field_config in schema.items(): + if isinstance(field_config, dict): + # Pass through the entire field_config dict to allow future API fields + fields[field_name] = BackupModelSchemaFields(**field_config) + else: + # If not a dict, create with default filterable=True + fields[field_name] = BackupModelSchemaFields(filterable=True) + # Ensure fields is always set, even if empty + schema_kwargs["fields"] = fields + + # Validate that fields is present before constructing BackupModelSchema + if "fields" not in schema_kwargs: + raise ValueError( + "Schema dict must contain field definitions. " + "Either provide a 'fields' key with field configurations, " + "or provide field_name: field_config pairs directly." + ) + + return BackupModelSchema(**schema_kwargs) + else: + # Already a BackupModelSchema instance + return schema # type: ignore + @staticmethod def __parse_index_spec(spec: Union[Dict, ServerlessSpec, PodSpec, ByocSpec]) -> IndexSpec: if isinstance(spec, dict): @@ -75,6 +233,38 @@ def __parse_index_spec(spec: Union[Dict, ServerlessSpec, PodSpec, ByocSpec]) -> spec["serverless"]["cloud"] = convert_enum_to_string(spec["serverless"]["cloud"]) spec["serverless"]["region"] = convert_enum_to_string(spec["serverless"]["region"]) + # Handle read_capacity if present + if "read_capacity" in spec["serverless"]: + spec["serverless"]["read_capacity"] = ( + PineconeDBControlRequestFactory.__parse_read_capacity( + spec["serverless"]["read_capacity"] + ) + ) + + # Handle schema if present - convert to BackupModelSchema + if "schema" in spec["serverless"]: + schema_dict = spec["serverless"]["schema"] + if isinstance(schema_dict, dict): + # Process fields if present, otherwise pass through as-is + schema_kwargs = {} + if "fields" in schema_dict: + fields = {} + for field_name, field_config in schema_dict["fields"].items(): + if isinstance(field_config, dict): + # Pass through the entire field_config dict to allow future API fields + fields[field_name] = BackupModelSchemaFields(**field_config) + else: + # If not a dict, create with default filterable=True + fields[field_name] = BackupModelSchemaFields(filterable=True) + schema_kwargs["fields"] = fields + + # Pass through any other fields in schema_dict to allow future API fields + for key, value in schema_dict.items(): + if key != "fields": + schema_kwargs[key] = value + + spec["serverless"]["schema"] = BackupModelSchema(**schema_kwargs) + index_spec = IndexSpec(serverless=ServerlessSpecModel(**spec["serverless"])) elif "pod" in spec: spec["pod"]["environment"] = convert_enum_to_string(spec["pod"]["environment"]) @@ -98,9 +288,31 @@ def __parse_index_spec(spec: Union[Dict, ServerlessSpec, PodSpec, ByocSpec]) -> else: raise ValueError("spec must contain either 'serverless', 'pod', or 'byoc' key") elif isinstance(spec, ServerlessSpec): - index_spec = IndexSpec( - serverless=ServerlessSpecModel(cloud=spec.cloud, region=spec.region) - ) + # Build args dict for ServerlessSpecModel + serverless_args: Dict[str, Any] = {"cloud": spec.cloud, "region": spec.region} + + # Handle read_capacity + if spec.read_capacity is not None: + serverless_args["read_capacity"] = ( + PineconeDBControlRequestFactory.__parse_read_capacity(spec.read_capacity) + ) + + # Handle schema + if spec.schema is not None: + # Convert dict to BackupModelSchema + # schema is {field_name: {filterable: bool, ...}} + # Pass through the entire field_config to allow future API fields + fields = {} + for field_name, field_config in spec.schema.items(): + if isinstance(field_config, dict): + # Pass through the entire field_config dict to allow future API fields + fields[field_name] = BackupModelSchemaFields(**field_config) + else: + # If not a dict, create with default filterable=True + fields[field_name] = BackupModelSchemaFields(filterable=True) + serverless_args["schema"] = BackupModelSchema(fields=fields) + + index_spec = IndexSpec(serverless=ServerlessSpecModel(**serverless_args)) elif isinstance(spec, PodSpec): args_dict = parse_non_empty_args( [ @@ -173,6 +385,25 @@ def create_index_for_model_request( embed: Union[IndexEmbed, CreateIndexForModelEmbedTypedDict], tags: Optional[Dict[str, str]] = None, deletion_protection: Optional[Union[DeletionProtection, str]] = DeletionProtection.DISABLED, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + ReadCapacityOnDemandSpec, + ReadCapacityDedicatedSpec, + ] + ] = None, + schema: Optional[ + Union[ + Dict[ + str, "MetadataSchemaFieldConfig" + ], # Direct field mapping: {field_name: {filterable: bool}} + Dict[ + str, Dict[str, Any] + ], # Dict with "fields" wrapper: {"fields": {field_name: {...}}, ...} + BackupModelSchema, # OpenAPI model instance + ] + ] = None, ) -> CreateIndexForModelRequest: cloud = convert_enum_to_string(cloud) region = convert_enum_to_string(region) @@ -198,6 +429,18 @@ def create_index_for_model_request( else: parsed_embed[key] = value + # Parse read_capacity if provided + parsed_read_capacity = None + if read_capacity is not None: + parsed_read_capacity = PineconeDBControlRequestFactory.__parse_read_capacity( + read_capacity + ) + + # Parse schema if provided + parsed_schema = None + if schema is not None: + parsed_schema = PineconeDBControlRequestFactory.__parse_schema(schema) + args = parse_non_empty_args( [ ("name", name), @@ -206,6 +449,8 @@ def create_index_for_model_request( ("embed", CreateIndexForModelRequestEmbed(**parsed_embed)), ("deletion_protection", dp), ("tags", tags_obj), + ("read_capacity", parsed_read_capacity), + ("schema", parsed_schema), ] ) @@ -234,6 +479,14 @@ def configure_index_request( deletion_protection: Optional[Union[DeletionProtection, str]] = None, tags: Optional[Dict[str, str]] = None, embed: Optional[Union[ConfigureIndexEmbed, Dict]] = None, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + ReadCapacityOnDemandSpec, + ReadCapacityDedicatedSpec, + ] + ] = None, ): if deletion_protection is None: dp = description.deletion_protection @@ -268,9 +521,19 @@ def configure_index_request( if embed is not None: embed_config = ConfigureIndexRequestEmbed(**dict(embed)) + # Parse read_capacity if provided + parsed_read_capacity = None + if read_capacity is not None: + parsed_read_capacity = PineconeDBControlRequestFactory.__parse_read_capacity( + read_capacity + ) + spec = None if pod_config_args: spec = {"pod": pod_config_args} + elif parsed_read_capacity is not None: + # Serverless index configuration + spec = {"serverless": {"read_capacity": parsed_read_capacity}} args_dict = parse_non_empty_args( [ diff --git a/pinecone/db_control/resources/asyncio/index.py b/pinecone/db_control/resources/asyncio/index.py index 5a844b5af..36871cf6d 100644 --- a/pinecone/db_control/resources/asyncio/index.py +++ b/pinecone/db_control/resources/asyncio/index.py @@ -1,6 +1,6 @@ import logging import asyncio -from typing import Optional, Dict, Union +from typing import Optional, Dict, Union, Any, TYPE_CHECKING from pinecone.db_control.models import ( @@ -32,6 +32,20 @@ logger = logging.getLogger(__name__) """ :meta private: """ +if TYPE_CHECKING: + from pinecone.db_control.models.serverless_spec import ( + ReadCapacityDict, + MetadataSchemaFieldConfig, + ) + from pinecone.core.openapi.db_control.model.read_capacity import ReadCapacity + from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec import ( + ReadCapacityOnDemandSpec, + ) + from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec import ( + ReadCapacityDedicatedSpec, + ) + from pinecone.core.openapi.db_control.model.backup_model_schema import BackupModelSchema + class IndexResourceAsyncio: def __init__(self, index_api, config): @@ -76,6 +90,25 @@ async def create_for_model( embed: Union[IndexEmbed, CreateIndexForModelEmbedTypedDict], tags: Optional[Dict[str, str]] = None, deletion_protection: Optional[Union[DeletionProtection, str]] = DeletionProtection.DISABLED, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, + schema: Optional[ + Union[ + Dict[ + str, "MetadataSchemaFieldConfig" + ], # Direct field mapping: {field_name: {filterable: bool}} + Dict[ + str, Dict[str, Any] + ], # Dict with "fields" wrapper: {"fields": {field_name: {...}}, ...} + "BackupModelSchema", # OpenAPI model instance + ] + ] = None, timeout: Optional[int] = None, ) -> IndexModel: req = PineconeDBControlRequestFactory.create_index_for_model_request( @@ -85,6 +118,8 @@ async def create_for_model( embed=embed, tags=tags, deletion_protection=deletion_protection, + read_capacity=read_capacity, + schema=schema, ) resp = await self._index_api.create_index_for_model(req) @@ -185,6 +220,14 @@ async def configure( deletion_protection: Optional[Union[DeletionProtection, str]] = None, tags: Optional[Dict[str, str]] = None, embed: Optional[Union[ConfigureIndexEmbed, Dict]] = None, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, ): description = await self.describe(name=name) @@ -195,5 +238,6 @@ async def configure( deletion_protection=deletion_protection, tags=tags, embed=embed, + read_capacity=read_capacity, ) await self._index_api.configure_index(name, configure_index_request=req) diff --git a/pinecone/db_control/resources/sync/index.py b/pinecone/db_control/resources/sync/index.py index faf5f9831..6a3096ae3 100644 --- a/pinecone/db_control/resources/sync/index.py +++ b/pinecone/db_control/resources/sync/index.py @@ -1,6 +1,6 @@ import time import logging -from typing import Optional, Dict, Union, TYPE_CHECKING +from typing import Optional, Dict, Union, TYPE_CHECKING, Any from pinecone.db_control.index_host_store import IndexHostStore @@ -29,6 +29,18 @@ AzureRegion, ) from pinecone.db_control.models import ServerlessSpec, PodSpec, ByocSpec, IndexEmbed + from pinecone.db_control.models.serverless_spec import ( + ReadCapacityDict, + MetadataSchemaFieldConfig, + ) + from pinecone.core.openapi.db_control.model.read_capacity import ReadCapacity + from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec import ( + ReadCapacityOnDemandSpec, + ) + from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec import ( + ReadCapacityDedicatedSpec, + ) + from pinecone.core.openapi.db_control.model.backup_model_schema import BackupModelSchema class IndexResource(PluginAware): @@ -94,6 +106,25 @@ def create_for_model( embed: Union["IndexEmbed", "CreateIndexForModelEmbedTypedDict"], tags: Optional[Dict[str, str]] = None, deletion_protection: Optional[Union["DeletionProtection", str]] = "disabled", + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, + schema: Optional[ + Union[ + Dict[ + str, "MetadataSchemaFieldConfig" + ], # Direct field mapping: {field_name: {filterable: bool}} + Dict[ + str, Dict[str, Any] + ], # Dict with "fields" wrapper: {"fields": {field_name: {...}}, ...} + "BackupModelSchema", # OpenAPI model instance + ] + ] = None, timeout: Optional[int] = None, ) -> IndexModel: req = PineconeDBControlRequestFactory.create_index_for_model_request( @@ -103,6 +134,8 @@ def create_for_model( embed=embed, tags=tags, deletion_protection=deletion_protection, + read_capacity=read_capacity, + schema=schema, ) resp = self._index_api.create_index_for_model(req) @@ -226,6 +259,14 @@ def configure( deletion_protection: Optional[Union["DeletionProtection", str]] = None, tags: Optional[Dict[str, str]] = None, embed: Optional[Union["ConfigureIndexEmbed", Dict]] = None, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, ) -> None: api_instance = self._index_api description = self.describe(name=name) @@ -237,6 +278,7 @@ def configure( deletion_protection=deletion_protection, tags=tags, embed=embed, + read_capacity=read_capacity, ) api_instance.configure_index(name, configure_index_request=req) diff --git a/pinecone/legacy_pinecone_interface.py b/pinecone/legacy_pinecone_interface.py index 0a085462d..93bcf3cea 100644 --- a/pinecone/legacy_pinecone_interface.py +++ b/pinecone/legacy_pinecone_interface.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from typing import Optional, Dict, Union, TYPE_CHECKING +from typing import Optional, Dict, Union, TYPE_CHECKING, Any if TYPE_CHECKING: from pinecone.db_control.models import ( @@ -27,6 +27,18 @@ AzureRegion, ) from pinecone.db_control.types import CreateIndexForModelEmbedTypedDict, ConfigureIndexEmbed + from pinecone.db_control.models.serverless_spec import ( + ReadCapacityDict, + MetadataSchemaFieldConfig, + ) + from pinecone.core.openapi.db_control.model.read_capacity import ReadCapacity + from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec import ( + ReadCapacityOnDemandSpec, + ) + from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec import ( + ReadCapacityDedicatedSpec, + ) + from pinecone.core.openapi.db_control.model.backup_model_schema import BackupModelSchema class LegacyPineconeDBControlInterface(ABC): @@ -68,7 +80,9 @@ def create_index( :param metric: Type of similarity metric used in the vector index when querying, one of ``{"cosine", "dotproduct", "euclidean"}``. :type metric: str, optional :param spec: A dictionary containing configurations describing how the index should be deployed. For serverless indexes, - specify region and cloud. For pod indexes, specify replicas, shards, pods, pod_type, metadata_config, and source_collection. + specify region and cloud. Optionally, you can specify ``read_capacity`` to configure dedicated read capacity mode + (OnDemand or Dedicated) and ``schema`` to configure which metadata fields are filterable. For pod indexes, specify + replicas, shards, pods, pod_type, metadata_config, and source_collection. Alternatively, use the ``ServerlessSpec``, ``PodSpec``, or ``ByocSpec`` objects to specify these configurations. :type spec: Dict :param dimension: If you are creating an index with ``vector_type="dense"`` (which is the default), you need to specify ``dimension`` to indicate the size of your vectors. @@ -198,6 +212,25 @@ def create_index_for_model( deletion_protection: Optional[ Union["DeletionProtection", str] ] = "DeletionProtection.DISABLED", + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, + schema: Optional[ + Union[ + Dict[ + str, "MetadataSchemaFieldConfig" + ], # Direct field mapping: {field_name: {filterable: bool}} + Dict[ + str, Dict[str, Any] + ], # Dict with "fields" wrapper: {"fields": {field_name: {...}}, ...} + "BackupModelSchema", # OpenAPI model instance + ] + ] = None, timeout: Optional[int] = None, ) -> "IndexModel": """ @@ -215,6 +248,13 @@ def create_index_for_model( :type tags: Optional[Dict[str, str]] :param deletion_protection: If enabled, the index cannot be deleted. If disabled, the index can be deleted. This setting can be changed with ``configure_index``. :type deletion_protection: Optional[Literal["enabled", "disabled"]] + :param read_capacity: Optional read capacity configuration. You can specify ``read_capacity`` to configure dedicated read capacity mode + (OnDemand or Dedicated). See ``ServerlessSpec`` documentation for details on read capacity configuration. + :type read_capacity: Optional[Union[ReadCapacityDict, ReadCapacity, ReadCapacityOnDemandSpec, ReadCapacityDedicatedSpec]] + :param schema: Optional metadata schema configuration. You can specify ``schema`` to configure which metadata fields are filterable. + The schema can be provided as a dictionary mapping field names to their configurations (e.g., ``{"genre": {"filterable": True}}``) + or as a dictionary with a ``fields`` key (e.g., ``{"fields": {"genre": {"filterable": True}}}``). + :type schema: Optional[Union[Dict[str, MetadataSchemaFieldConfig], Dict[str, Dict[str, Any]], BackupModelSchema]] :type timeout: Optional[int] :param timeout: Specify the number of seconds to wait until index is ready to receive data. If None, wait indefinitely; if >=0, time out after this many seconds; if -1, return immediately and do not wait. @@ -439,6 +479,14 @@ def configure_index( deletion_protection: Optional[Union["DeletionProtection", str]] = None, tags: Optional[Dict[str, str]] = None, embed: Optional[Union["ConfigureIndexEmbed", Dict]] = None, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, ): """ :param name: the name of the Index @@ -457,14 +505,53 @@ def configure_index( The index vector type and dimension must match the model vector type and dimension, and the index similarity metric must be supported by the model. You can later change the embedding configuration to update the field_map, read_parameters, or write_parameters. Once set, the model cannot be changed. :type embed: Optional[Union[ConfigureIndexEmbed, Dict]], optional + :param read_capacity: Optional read capacity configuration for serverless indexes. You can specify ``read_capacity`` to configure dedicated read capacity mode + (OnDemand or Dedicated). See ``ServerlessSpec`` documentation for details on read capacity configuration. + Note that read capacity configuration is only available for serverless indexes. + :type read_capacity: Optional[Union[ReadCapacityDict, ReadCapacity, ReadCapacityOnDemandSpec, ReadCapacityDedicatedSpec]] This method is used to modify an index's configuration. It can be used to: + * Configure read capacity for serverless indexes using ``read_capacity`` * Scale a pod-based index horizontally using ``replicas`` * Scale a pod-based index vertically using ``pod_type`` * Enable or disable deletion protection using ``deletion_protection`` * Add, change, or remove tags using ``tags`` + **Configuring read capacity for serverless indexes** + + To configure read capacity for serverless indexes, pass the ``read_capacity`` parameter to the ``configure_index`` method. + You can configure either OnDemand or Dedicated read capacity mode. + + .. code-block:: python + + from pinecone import Pinecone + + pc = Pinecone() + + # Configure to OnDemand read capacity (default) + pc.configure_index( + name="my_index", + read_capacity={"mode": "OnDemand"} + ) + + # Configure to Dedicated read capacity with manual scaling + pc.configure_index( + name="my_index", + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1} + } + } + ) + + # Verify the configuration was applied + desc = pc.describe_index("my_index") + assert desc.spec.serverless.read_capacity.mode == "Dedicated" + **Scaling pod-based indexes** To scale your pod-based index, you pass a ``replicas`` and/or ``pod_type`` param to the ``configure_index`` method. ``pod_type`` may be a string or a value from the ``PodType`` enum. diff --git a/pinecone/pinecone.py b/pinecone/pinecone.py index d8c8a1b4a..00fd4cfee 100644 --- a/pinecone/pinecone.py +++ b/pinecone/pinecone.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Dict, Union, TYPE_CHECKING +from typing import Optional, Dict, Union, TYPE_CHECKING, Any from multiprocessing import cpu_count import warnings @@ -19,6 +19,18 @@ from pinecone.db_control.index_host_store import IndexHostStore from pinecone.core.openapi.db_control.api.manage_indexes_api import ManageIndexesApi from pinecone.db_control.types import CreateIndexForModelEmbedTypedDict, ConfigureIndexEmbed + from pinecone.db_control.models.serverless_spec import ( + ReadCapacityDict, + MetadataSchemaFieldConfig, + ) + from pinecone.core.openapi.db_control.model.read_capacity import ReadCapacity + from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec import ( + ReadCapacityOnDemandSpec, + ) + from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec import ( + ReadCapacityDedicatedSpec, + ) + from pinecone.core.openapi.db_control.model.backup_model_schema import BackupModelSchema from pinecone.db_control.enums import ( Metric, VectorType, @@ -350,6 +362,25 @@ def create_index_for_model( embed: Union["IndexEmbed", "CreateIndexForModelEmbedTypedDict"], tags: Optional[Dict[str, str]] = None, deletion_protection: Optional[Union["DeletionProtection", str]] = "disabled", + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, + schema: Optional[ + Union[ + Dict[ + str, "MetadataSchemaFieldConfig" + ], # Direct field mapping: {field_name: {filterable: bool}} + Dict[ + str, Dict[str, Any] + ], # Dict with "fields" wrapper: {"fields": {field_name: {...}}, ...} + "BackupModelSchema", # OpenAPI model instance + ] + ] = None, timeout: Optional[int] = None, ) -> "IndexModel": return self.db.index.create_for_model( @@ -359,6 +390,8 @@ def create_index_for_model( embed=embed, tags=tags, deletion_protection=deletion_protection, + read_capacity=read_capacity, + schema=schema, timeout=timeout, ) @@ -400,6 +433,14 @@ def configure_index( deletion_protection: Optional[Union["DeletionProtection", str]] = None, tags: Optional[Dict[str, str]] = None, embed: Optional[Union["ConfigureIndexEmbed", Dict]] = None, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, ): return self.db.index.configure( name=name, @@ -408,6 +449,7 @@ def configure_index( deletion_protection=deletion_protection, tags=tags, embed=embed, + read_capacity=read_capacity, ) def create_collection(self, name: str, source: str) -> None: diff --git a/pinecone/pinecone_asyncio.py b/pinecone/pinecone_asyncio.py index 425eb776c..85e79b791 100644 --- a/pinecone/pinecone_asyncio.py +++ b/pinecone/pinecone_asyncio.py @@ -1,6 +1,6 @@ import logging import warnings -from typing import Optional, Dict, Union, TYPE_CHECKING +from typing import Optional, Dict, Union, TYPE_CHECKING, Any from pinecone.config import PineconeConfig, ConfigBuilder @@ -35,6 +35,18 @@ RestoreJobModel, RestoreJobList, ) + from pinecone.db_control.models.serverless_spec import ( + ReadCapacityDict, + MetadataSchemaFieldConfig, + ) + from pinecone.core.openapi.db_control.model.read_capacity import ReadCapacity + from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec import ( + ReadCapacityOnDemandSpec, + ) + from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec import ( + ReadCapacityDedicatedSpec, + ) + from pinecone.core.openapi.db_control.model.backup_model_schema import BackupModelSchema from pinecone.core.openapi.db_control.api.manage_indexes_api import AsyncioManageIndexesApi from pinecone.db_control.index_host_store import IndexHostStore @@ -224,6 +236,25 @@ async def create_index_for_model( embed: Union["IndexEmbed", "CreateIndexForModelEmbedTypedDict"], tags: Optional[Dict[str, str]] = None, deletion_protection: Optional[Union["DeletionProtection", str]] = "disabled", + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, + schema: Optional[ + Union[ + Dict[ + str, "MetadataSchemaFieldConfig" + ], # Direct field mapping: {field_name: {filterable: bool}} + Dict[ + str, Dict[str, Any] + ], # Dict with "fields" wrapper: {"fields": {field_name: {...}}, ...} + "BackupModelSchema", # OpenAPI model instance + ] + ] = None, timeout: Optional[int] = None, ) -> "IndexModel": return await self.db.index.create_for_model( @@ -233,6 +264,8 @@ async def create_index_for_model( embed=embed, tags=tags, deletion_protection=deletion_protection, + read_capacity=read_capacity, + schema=schema, timeout=timeout, ) @@ -274,6 +307,14 @@ async def configure_index( deletion_protection: Optional[Union["DeletionProtection", str]] = None, tags: Optional[Dict[str, str]] = None, embed: Optional[Union["ConfigureIndexEmbed", Dict]] = None, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, ): return await self.db.index.configure( name=name, @@ -282,6 +323,7 @@ async def configure_index( deletion_protection=deletion_protection, tags=tags, embed=embed, + read_capacity=read_capacity, ) async def create_collection(self, name: str, source: str): diff --git a/pinecone/pinecone_interface_asyncio.py b/pinecone/pinecone_interface_asyncio.py index 0d544f104..cdc31f415 100644 --- a/pinecone/pinecone_interface_asyncio.py +++ b/pinecone/pinecone_interface_asyncio.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from typing import Optional, Dict, Union, TYPE_CHECKING +from typing import Optional, Dict, Union, TYPE_CHECKING, Any if TYPE_CHECKING: from pinecone.config import Config @@ -31,6 +31,18 @@ AzureRegion, ) from pinecone.db_control.types import ConfigureIndexEmbed, CreateIndexForModelEmbedTypedDict + from pinecone.db_control.models.serverless_spec import ( + ReadCapacityDict, + MetadataSchemaFieldConfig, + ) + from pinecone.core.openapi.db_control.model.read_capacity import ReadCapacity + from pinecone.core.openapi.db_control.model.read_capacity_on_demand_spec import ( + ReadCapacityOnDemandSpec, + ) + from pinecone.core.openapi.db_control.model.read_capacity_dedicated_spec import ( + ReadCapacityDedicatedSpec, + ) + from pinecone.core.openapi.db_control.model.backup_model_schema import BackupModelSchema class PineconeAsyncioDBControlInterface(ABC): @@ -311,7 +323,9 @@ async def create_index( :param metric: Type of similarity metric used in the vector index when querying, one of ``{"cosine", "dotproduct", "euclidean"}``. :type metric: str, optional :param spec: A dictionary containing configurations describing how the index should be deployed. For serverless indexes, - specify region and cloud. For pod indexes, specify replicas, shards, pods, pod_type, metadata_config, and source_collection. + specify region and cloud. Optionally, you can specify ``read_capacity`` to configure dedicated read capacity mode + (OnDemand or Dedicated) and ``schema`` to configure which metadata fields are filterable. For pod indexes, specify + replicas, shards, pods, pod_type, metadata_config, and source_collection. Alternatively, use the ``ServerlessSpec`` or ``PodSpec`` objects to specify these configurations. :type spec: Dict :param dimension: If you are creating an index with ``vector_type="dense"`` (which is the default), you need to specify ``dimension`` to indicate the size of your vectors. @@ -417,6 +431,25 @@ async def create_index_for_model( embed: Union["IndexEmbed", "CreateIndexForModelEmbedTypedDict"], tags: Optional[Dict[str, str]] = None, deletion_protection: Optional[Union["DeletionProtection", str]] = "disabled", + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, + schema: Optional[ + Union[ + Dict[ + str, "MetadataSchemaFieldConfig" + ], # Direct field mapping: {field_name: {filterable: bool}} + Dict[ + str, Dict[str, Any] + ], # Dict with "fields" wrapper: {"fields": {field_name: {...}}, ...} + "BackupModelSchema", # OpenAPI model instance + ] + ] = None, timeout: Optional[int] = None, ) -> "IndexModel": """ @@ -434,6 +467,13 @@ async def create_index_for_model( :type tags: Optional[Dict[str, str]] :param deletion_protection: If enabled, the index cannot be deleted. If disabled, the index can be deleted. This setting can be changed with ``configure_index``. :type deletion_protection: Optional[Literal["enabled", "disabled"]] + :param read_capacity: Optional read capacity configuration. You can specify ``read_capacity`` to configure dedicated read capacity mode + (OnDemand or Dedicated). See ``ServerlessSpec`` documentation for details on read capacity configuration. + :type read_capacity: Optional[Union[ReadCapacityDict, ReadCapacity, ReadCapacityOnDemandSpec, ReadCapacityDedicatedSpec]] + :param schema: Optional metadata schema configuration. You can specify ``schema`` to configure which metadata fields are filterable. + The schema can be provided as a dictionary mapping field names to their configurations (e.g., ``{"genre": {"filterable": True}}``) + or as a dictionary with a ``fields`` key (e.g., ``{"fields": {"genre": {"filterable": True}}}``). + :type schema: Optional[Union[Dict[str, MetadataSchemaFieldConfig], Dict[str, Dict[str, Any]], BackupModelSchema]] :type timeout: Optional[int] :param timeout: Specify the number of seconds to wait until index is ready to receive data. If None, wait indefinitely; if >=0, time out after this many seconds; if -1, return immediately and do not wait. @@ -712,6 +752,14 @@ async def configure_index( deletion_protection: Optional[Union["DeletionProtection", str]] = None, tags: Optional[Dict[str, str]] = None, embed: Optional[Union["ConfigureIndexEmbed", Dict]] = None, + read_capacity: Optional[ + Union[ + "ReadCapacityDict", + "ReadCapacity", + "ReadCapacityOnDemandSpec", + "ReadCapacityDedicatedSpec", + ] + ] = None, ): """ :param: name: the name of the Index @@ -724,14 +772,56 @@ async def configure_index( The index vector type and dimension must match the model vector type and dimension, and the index similarity metric must be supported by the model. You can later change the embedding configuration to update the field_map, read_parameters, or write_parameters. Once set, the model cannot be changed. :type embed: Optional[Union[ConfigureIndexEmbed, Dict]], optional + :param read_capacity: Optional read capacity configuration for serverless indexes. You can specify ``read_capacity`` to configure dedicated read capacity mode + (OnDemand or Dedicated). See ``ServerlessSpec`` documentation for details on read capacity configuration. + Note that read capacity configuration is only available for serverless indexes. + :type read_capacity: Optional[Union[ReadCapacityDict, ReadCapacity, ReadCapacityOnDemandSpec, ReadCapacityDedicatedSpec]] This method is used to modify an index's configuration. It can be used to: + - Configure read capacity for serverless indexes using ``read_capacity`` - Scale a pod-based index horizontally using ``replicas`` - Scale a pod-based index vertically using ``pod_type`` - Enable or disable deletion protection using ``deletion_protection`` - Add, change, or remove tags using ``tags`` + **Configuring read capacity for serverless indexes** + + To configure read capacity for serverless indexes, pass the ``read_capacity`` parameter to the ``configure_index`` method. + You can configure either OnDemand or Dedicated read capacity mode. + + .. code-block:: python + + import asyncio + from pinecone import PineconeAsyncio + + async def main(): + async with PineconeAsyncio() as pc: + # Configure to OnDemand read capacity (default) + await pc.configure_index( + name="my_index", + read_capacity={"mode": "OnDemand"} + ) + + # Configure to Dedicated read capacity with manual scaling + await pc.configure_index( + name="my_index", + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1} + } + } + ) + + # Verify the configuration was applied + desc = await pc.describe_index("my_index") + assert desc.spec.serverless.read_capacity.mode == "Dedicated" + + asyncio.run(main()) + **Scaling pod-based indexes** To scale your pod-based index, you pass a ``replicas`` and/or ``pod_type`` param to the ``configure_index`` method. ``pod_type`` may be a string or a value from the ``PodType`` enum. diff --git a/tests/integration/control/serverless/test_configure_index_read_capacity.py b/tests/integration/control/serverless/test_configure_index_read_capacity.py new file mode 100644 index 000000000..5416c0d0d --- /dev/null +++ b/tests/integration/control/serverless/test_configure_index_read_capacity.py @@ -0,0 +1,83 @@ +class TestConfigureIndexReadCapacity: + def test_configure_serverless_index_read_capacity_ondemand(self, client, ready_sl_index): + """Test configuring a serverless index to use OnDemand read capacity.""" + # Configure to OnDemand (should be idempotent if already OnDemand) + client.configure_index(name=ready_sl_index, read_capacity={"mode": "OnDemand"}) + + # Verify the configuration was applied + desc = client.describe_index(name=ready_sl_index) + assert hasattr(desc.spec.serverless, "read_capacity") + assert desc.spec.serverless.read_capacity.mode == "OnDemand" + + def test_configure_serverless_index_read_capacity_dedicated(self, client, ready_sl_index): + """Test configuring a serverless index to use Dedicated read capacity.""" + # Configure to Dedicated + client.configure_index( + name=ready_sl_index, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + ) + + # Verify the configuration was applied + desc = client.describe_index(name=ready_sl_index) + assert hasattr(desc.spec.serverless, "read_capacity") + assert desc.spec.serverless.read_capacity.mode == "Dedicated" + assert desc.spec.serverless.read_capacity.dedicated.node_type == "t1" + assert desc.spec.serverless.read_capacity.dedicated.scaling == "Manual" + + def test_configure_serverless_index_read_capacity_dedicated_with_manual( + self, client, ready_sl_index + ): + """Test configuring a serverless index to use Dedicated read capacity with manual scaling.""" + # Configure to Dedicated with manual scaling configuration + client.configure_index( + name=ready_sl_index, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + ) + + # Verify the configuration was applied + desc = client.describe_index(name=ready_sl_index) + assert hasattr(desc.spec.serverless, "read_capacity") + assert desc.spec.serverless.read_capacity.mode == "Dedicated" + assert desc.spec.serverless.read_capacity.dedicated.node_type == "t1" + assert desc.spec.serverless.read_capacity.dedicated.scaling == "Manual" + assert desc.spec.serverless.read_capacity.dedicated.manual.shards == 1 + assert desc.spec.serverless.read_capacity.dedicated.manual.replicas == 1 + + def test_configure_serverless_index_read_capacity_from_ondemand_to_dedicated( + self, client, ready_sl_index + ): + """Test changing read capacity from OnDemand to Dedicated.""" + # First configure to OnDemand + client.configure_index(name=ready_sl_index, read_capacity={"mode": "OnDemand"}) + desc = client.describe_index(name=ready_sl_index) + assert desc.spec.serverless.read_capacity.mode == "OnDemand" + + # Then change to Dedicated + client.configure_index( + name=ready_sl_index, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + ) + desc = client.describe_index(name=ready_sl_index) + assert desc.spec.serverless.read_capacity.mode == "Dedicated" + assert desc.spec.serverless.read_capacity.dedicated.node_type == "t1" diff --git a/tests/integration/control/serverless/test_create_index.py b/tests/integration/control/serverless/test_create_index.py index 5e7f46fe6..9314921d6 100644 --- a/tests/integration/control/serverless/test_create_index.py +++ b/tests/integration/control/serverless/test_create_index.py @@ -113,3 +113,124 @@ def test_create_with_optional_tags(self, client, create_sl_index_params): client.create_index(**create_sl_index_params) desc = client.describe_index(create_sl_index_params["name"]) assert desc.tags.to_dict() == tags + + def test_create_with_read_capacity_ondemand(self, client, index_name): + resp = client.create_index( + name=index_name, + dimension=10, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + read_capacity={"mode": "OnDemand"}, + ), + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = client.describe_index(name=index_name) + assert desc.name == index_name + # Verify read_capacity is set (structure may vary in response) + assert hasattr(desc.spec.serverless, "read_capacity") + + def test_create_with_read_capacity_dedicated(self, client, index_name): + resp = client.create_index( + name=index_name, + dimension=10, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + ), + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = client.describe_index(name=index_name) + assert desc.name == index_name + # Verify read_capacity is set + assert hasattr(desc.spec.serverless, "read_capacity") + + def test_create_with_metadata_schema(self, client, index_name): + resp = client.create_index( + name=index_name, + dimension=10, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + schema={"genre": {"filterable": True}, "year": {"filterable": True}}, + ), + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = client.describe_index(name=index_name) + assert desc.name == index_name + # Verify schema is set (structure may vary in response) + assert hasattr(desc.spec.serverless, "schema") + + def test_create_with_read_capacity_and_metadata_schema(self, client, index_name): + resp = client.create_index( + name=index_name, + dimension=10, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + read_capacity={"mode": "OnDemand"}, + schema={"genre": {"filterable": True}, "year": {"filterable": True}}, + ), + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = client.describe_index(name=index_name) + assert desc.name == index_name + assert hasattr(desc.spec.serverless, "read_capacity") + assert hasattr(desc.spec.serverless, "schema") + + def test_create_with_dict_spec_metadata_schema(self, client, index_name): + """Test dict-based spec with schema (code path in request_factory.py lines 145-167)""" + resp = client.create_index( + name=index_name, + dimension=10, + spec={ + "serverless": { + "cloud": "aws", + "region": "us-east-1", + "schema": { + "fields": {"genre": {"filterable": True}, "year": {"filterable": True}} + }, + } + }, + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = client.describe_index(name=index_name) + assert desc.name == index_name + # Verify schema is set (structure may vary in response) + assert hasattr(desc.spec.serverless, "schema") + + def test_create_with_dict_spec_read_capacity_and_metadata_schema(self, client, index_name): + """Test dict-based spec with read_capacity and schema""" + resp = client.create_index( + name=index_name, + dimension=10, + spec={ + "serverless": { + "cloud": "aws", + "region": "us-east-1", + "read_capacity": {"mode": "OnDemand"}, + "schema": { + "fields": {"genre": {"filterable": True}, "year": {"filterable": True}} + }, + } + }, + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = client.describe_index(name=index_name) + assert desc.name == index_name + assert hasattr(desc.spec.serverless, "read_capacity") + assert hasattr(desc.spec.serverless, "schema") diff --git a/tests/integration/control/serverless/test_create_index_for_model.py b/tests/integration/control/serverless/test_create_index_for_model.py index 5f0258f75..cf062dbe3 100644 --- a/tests/integration/control/serverless/test_create_index_for_model.py +++ b/tests/integration/control/serverless/test_create_index_for_model.py @@ -66,3 +66,73 @@ def test_create_index_for_model_with_index_embed_dict( assert index.spec.serverless.region == "us-east-1" assert index.embed.field_map == field_map assert index.embed.model == EmbedModel.Multilingual_E5_Large.value + + def test_create_index_for_model_with_read_capacity_ondemand(self, client, index_name): + field_map = {"text": "my-sample-text"} + index = client.create_index_for_model( + name=index_name, + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + embed={"model": EmbedModel.Multilingual_E5_Large, "field_map": field_map}, + read_capacity={"mode": "OnDemand"}, + timeout=-1, + ) + assert index.name == index_name + assert hasattr(index.spec.serverless, "read_capacity") + desc = client.describe_index(name=index_name) + assert hasattr(desc.spec.serverless, "read_capacity") + + def test_create_index_for_model_with_read_capacity_dedicated(self, client, index_name): + field_map = {"text": "my-sample-text"} + index = client.create_index_for_model( + name=index_name, + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + embed={"model": EmbedModel.Multilingual_E5_Large, "field_map": field_map}, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + timeout=-1, + ) + assert index.name == index_name + assert hasattr(index.spec.serverless, "read_capacity") + desc = client.describe_index(name=index_name) + assert hasattr(desc.spec.serverless, "read_capacity") + + def test_create_index_for_model_with_schema(self, client, index_name): + field_map = {"text": "my-sample-text"} + index = client.create_index_for_model( + name=index_name, + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + embed={"model": EmbedModel.Multilingual_E5_Large, "field_map": field_map}, + schema={"genre": {"filterable": True}, "year": {"filterable": True}}, + timeout=-1, + ) + assert index.name == index_name + assert hasattr(index.spec.serverless, "schema") + desc = client.describe_index(name=index_name) + assert hasattr(desc.spec.serverless, "schema") + + def test_create_index_for_model_with_read_capacity_and_schema(self, client, index_name): + field_map = {"text": "my-sample-text"} + index = client.create_index_for_model( + name=index_name, + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + embed={"model": EmbedModel.Multilingual_E5_Large, "field_map": field_map}, + read_capacity={"mode": "OnDemand"}, + schema={"genre": {"filterable": True}, "year": {"filterable": True}}, + timeout=-1, + ) + assert index.name == index_name + assert hasattr(index.spec.serverless, "read_capacity") + assert hasattr(index.spec.serverless, "schema") + desc = client.describe_index(name=index_name) + assert hasattr(desc.spec.serverless, "read_capacity") + assert hasattr(desc.spec.serverless, "schema") diff --git a/tests/integration/control_asyncio/test_configure_index_read_capacity.py b/tests/integration/control_asyncio/test_configure_index_read_capacity.py new file mode 100644 index 000000000..2aa92f36c --- /dev/null +++ b/tests/integration/control_asyncio/test_configure_index_read_capacity.py @@ -0,0 +1,100 @@ +import pytest +from pinecone import PineconeAsyncio + + +@pytest.mark.asyncio +class TestConfigureIndexReadCapacity: + async def test_configure_serverless_index_read_capacity_ondemand(self, ready_sl_index): + """Test configuring a serverless index to use OnDemand read capacity.""" + pc = PineconeAsyncio() + + # Configure to OnDemand (should be idempotent if already OnDemand) + await pc.configure_index(name=ready_sl_index, read_capacity={"mode": "OnDemand"}) + + # Verify the configuration was applied + desc = await pc.describe_index(name=ready_sl_index) + assert hasattr(desc.spec.serverless, "read_capacity") + assert desc.spec.serverless.read_capacity.mode == "OnDemand" + await pc.close() + + async def test_configure_serverless_index_read_capacity_dedicated(self, ready_sl_index): + """Test configuring a serverless index to use Dedicated read capacity.""" + pc = PineconeAsyncio() + + # Configure to Dedicated + await pc.configure_index( + name=ready_sl_index, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + ) + + # Verify the configuration was applied + desc = await pc.describe_index(name=ready_sl_index) + assert hasattr(desc.spec.serverless, "read_capacity") + assert desc.spec.serverless.read_capacity.mode == "Dedicated" + assert desc.spec.serverless.read_capacity.dedicated.node_type == "t1" + assert desc.spec.serverless.read_capacity.dedicated.scaling == "Manual" + await pc.close() + + async def test_configure_serverless_index_read_capacity_dedicated_with_manual( + self, ready_sl_index + ): + """Test configuring a serverless index to use Dedicated read capacity with manual scaling.""" + pc = PineconeAsyncio() + + # Configure to Dedicated with manual scaling configuration + await pc.configure_index( + name=ready_sl_index, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + ) + + # Verify the configuration was applied + desc = await pc.describe_index(name=ready_sl_index) + assert hasattr(desc.spec.serverless, "read_capacity") + assert desc.spec.serverless.read_capacity.mode == "Dedicated" + assert desc.spec.serverless.read_capacity.dedicated.node_type == "t1" + assert desc.spec.serverless.read_capacity.dedicated.scaling == "Manual" + assert desc.spec.serverless.read_capacity.dedicated.manual.shards == 1 + assert desc.spec.serverless.read_capacity.dedicated.manual.replicas == 1 + await pc.close() + + async def test_configure_serverless_index_read_capacity_from_ondemand_to_dedicated( + self, ready_sl_index + ): + """Test changing read capacity from OnDemand to Dedicated.""" + pc = PineconeAsyncio() + + # First configure to OnDemand + await pc.configure_index(name=ready_sl_index, read_capacity={"mode": "OnDemand"}) + desc = await pc.describe_index(name=ready_sl_index) + assert desc.spec.serverless.read_capacity.mode == "OnDemand" + + # Then change to Dedicated + await pc.configure_index( + name=ready_sl_index, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + ) + desc = await pc.describe_index(name=ready_sl_index) + assert desc.spec.serverless.read_capacity.mode == "Dedicated" + assert desc.spec.serverless.read_capacity.dedicated.node_type == "t1" + await pc.close() diff --git a/tests/integration/control_asyncio/test_create_index.py b/tests/integration/control_asyncio/test_create_index.py index 683c53a89..7b5f85d97 100644 --- a/tests/integration/control_asyncio/test_create_index.py +++ b/tests/integration/control_asyncio/test_create_index.py @@ -158,3 +158,136 @@ async def test_create_with_deletion_protection(self, index_name, spec1): desc2 = await pc.describe_index(index_name) assert desc2.deletion_protection == "disabled" await pc.close() + + async def test_create_with_read_capacity_ondemand(self, index_name): + pc = PineconeAsyncio() + resp = await pc.create_index( + name=index_name, + dimension=10, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + read_capacity={"mode": "OnDemand"}, + ), + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = await pc.describe_index(name=index_name) + assert desc.name == index_name + # Verify read_capacity is set (structure may vary in response) + assert hasattr(desc.spec.serverless, "read_capacity") + await pc.close() + + async def test_create_with_read_capacity_dedicated(self, index_name): + pc = PineconeAsyncio() + resp = await pc.create_index( + name=index_name, + dimension=10, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + ), + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = await pc.describe_index(name=index_name) + assert desc.name == index_name + # Verify read_capacity is set + assert hasattr(desc.spec.serverless, "read_capacity") + await pc.close() + + async def test_create_with_metadata_schema(self, index_name): + pc = PineconeAsyncio() + resp = await pc.create_index( + name=index_name, + dimension=10, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + schema={"genre": {"filterable": True}, "year": {"filterable": True}}, + ), + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = await pc.describe_index(name=index_name) + assert desc.name == index_name + # Verify schema is set (structure may vary in response) + assert hasattr(desc.spec.serverless, "schema") + await pc.close() + + async def test_create_with_read_capacity_and_metadata_schema(self, index_name): + pc = PineconeAsyncio() + resp = await pc.create_index( + name=index_name, + dimension=10, + spec=ServerlessSpec( + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + read_capacity={"mode": "OnDemand"}, + schema={"genre": {"filterable": True}, "year": {"filterable": True}}, + ), + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = await pc.describe_index(name=index_name) + assert desc.name == index_name + assert hasattr(desc.spec.serverless, "read_capacity") + assert hasattr(desc.spec.serverless, "schema") + await pc.close() + + async def test_create_with_dict_spec_metadata_schema(self, index_name): + """Test dict-based spec with schema (code path in request_factory.py lines 145-167)""" + pc = PineconeAsyncio() + resp = await pc.create_index( + name=index_name, + dimension=10, + spec={ + "serverless": { + "cloud": "aws", + "region": "us-east-1", + "schema": { + "fields": {"genre": {"filterable": True}, "year": {"filterable": True}} + }, + } + }, + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = await pc.describe_index(name=index_name) + assert desc.name == index_name + # Verify schema is set (structure may vary in response) + assert hasattr(desc.spec.serverless, "schema") + await pc.close() + + async def test_create_with_dict_spec_read_capacity_and_metadata_schema(self, index_name): + """Test dict-based spec with read_capacity and schema""" + pc = PineconeAsyncio() + resp = await pc.create_index( + name=index_name, + dimension=10, + spec={ + "serverless": { + "cloud": "aws", + "region": "us-east-1", + "read_capacity": {"mode": "OnDemand"}, + "schema": { + "fields": {"genre": {"filterable": True}, "year": {"filterable": True}} + }, + } + }, + ) + assert resp.name == index_name + assert resp.dimension == 10 + desc = await pc.describe_index(name=index_name) + assert desc.name == index_name + assert hasattr(desc.spec.serverless, "read_capacity") + assert hasattr(desc.spec.serverless, "schema") + await pc.close() diff --git a/tests/integration/control_asyncio/test_create_index_for_model.py b/tests/integration/control_asyncio/test_create_index_for_model.py index 123c8668d..4e5ba34ce 100644 --- a/tests/integration/control_asyncio/test_create_index_for_model.py +++ b/tests/integration/control_asyncio/test_create_index_for_model.py @@ -76,3 +76,81 @@ async def test_create_index_for_model_with_index_embed_dict( assert index.embed.field_map == field_map assert index.embed.model == EmbedModel.Multilingual_E5_Large.value await pc.close() + + async def test_create_index_for_model_with_read_capacity_ondemand(self, index_name): + pc = PineconeAsyncio() + field_map = {"text": "my-sample-text"} + index = await pc.create_index_for_model( + name=index_name, + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + embed={"model": EmbedModel.Multilingual_E5_Large, "field_map": field_map}, + read_capacity={"mode": "OnDemand"}, + timeout=-1, + ) + assert index.name == index_name + assert hasattr(index.spec.serverless, "read_capacity") + desc = await pc.describe_index(name=index_name) + assert hasattr(desc.spec.serverless, "read_capacity") + await pc.close() + + async def test_create_index_for_model_with_read_capacity_dedicated(self, index_name): + pc = PineconeAsyncio() + field_map = {"text": "my-sample-text"} + index = await pc.create_index_for_model( + name=index_name, + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + embed={"model": EmbedModel.Multilingual_E5_Large, "field_map": field_map}, + read_capacity={ + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 1, "replicas": 1}, + }, + }, + timeout=-1, + ) + assert index.name == index_name + assert hasattr(index.spec.serverless, "read_capacity") + desc = await pc.describe_index(name=index_name) + assert hasattr(desc.spec.serverless, "read_capacity") + await pc.close() + + async def test_create_index_for_model_with_schema(self, index_name): + pc = PineconeAsyncio() + field_map = {"text": "my-sample-text"} + index = await pc.create_index_for_model( + name=index_name, + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + embed={"model": EmbedModel.Multilingual_E5_Large, "field_map": field_map}, + schema={"genre": {"filterable": True}, "year": {"filterable": True}}, + timeout=-1, + ) + assert index.name == index_name + assert hasattr(index.spec.serverless, "schema") + desc = await pc.describe_index(name=index_name) + assert hasattr(desc.spec.serverless, "schema") + await pc.close() + + async def test_create_index_for_model_with_read_capacity_and_schema(self, index_name): + pc = PineconeAsyncio() + field_map = {"text": "my-sample-text"} + index = await pc.create_index_for_model( + name=index_name, + cloud=CloudProvider.AWS, + region=AwsRegion.US_EAST_1, + embed={"model": EmbedModel.Multilingual_E5_Large, "field_map": field_map}, + read_capacity={"mode": "OnDemand"}, + schema={"genre": {"filterable": True}, "year": {"filterable": True}}, + timeout=-1, + ) + assert index.name == index_name + assert hasattr(index.spec.serverless, "read_capacity") + assert hasattr(index.spec.serverless, "schema") + desc = await pc.describe_index(name=index_name) + assert hasattr(desc.spec.serverless, "read_capacity") + assert hasattr(desc.spec.serverless, "schema") + await pc.close() diff --git a/tests/unit/db_control/test_index_request_factory.py b/tests/unit/db_control/test_index_request_factory.py index ee0d47fd1..a00e314d3 100644 --- a/tests/unit/db_control/test_index_request_factory.py +++ b/tests/unit/db_control/test_index_request_factory.py @@ -1,3 +1,4 @@ +import pytest from pinecone import ByocSpec, ServerlessSpec from pinecone.db_control.request_factory import PineconeDBControlRequestFactory @@ -60,3 +61,111 @@ def test_create_index_request_with_spec_byoc_dict(self): assert req.spec.byoc.environment == "test-byoc-spec-id" assert req.vector_type == "dense" assert req.deletion_protection == "disabled" + + def test_parse_read_capacity_ondemand(self): + """Test parsing OnDemand read capacity configuration.""" + read_capacity = {"mode": "OnDemand"} + result = ( + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + ) + assert result.mode == "OnDemand" + + def test_parse_read_capacity_dedicated_with_manual(self): + """Test parsing Dedicated read capacity with manual scaling configuration.""" + read_capacity = { + "mode": "Dedicated", + "dedicated": { + "node_type": "t1", + "scaling": "Manual", + "manual": {"shards": 2, "replicas": 3}, + }, + } + result = ( + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + ) + assert result.mode == "Dedicated" + assert result.dedicated.node_type == "t1" + assert result.dedicated.scaling == "Manual" + assert result.dedicated.manual.shards == 2 + assert result.dedicated.manual.replicas == 3 + + def test_parse_read_capacity_dedicated_missing_manual(self): + """Test that missing manual configuration raises ValueError when scaling is Manual.""" + read_capacity = {"mode": "Dedicated", "dedicated": {"node_type": "t1", "scaling": "Manual"}} + with pytest.raises(ValueError) as exc_info: + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + assert "manual" in str(exc_info.value).lower() + assert "required" in str(exc_info.value).lower() + + def test_parse_read_capacity_dedicated_missing_shards(self): + """Test that missing shards in manual configuration raises ValueError.""" + read_capacity = { + "mode": "Dedicated", + "dedicated": {"node_type": "t1", "scaling": "Manual", "manual": {"replicas": 3}}, + } + with pytest.raises(ValueError) as exc_info: + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + assert "shards" in str(exc_info.value).lower() + + def test_parse_read_capacity_dedicated_missing_replicas(self): + """Test that missing replicas in manual configuration raises ValueError.""" + read_capacity = { + "mode": "Dedicated", + "dedicated": {"node_type": "t1", "scaling": "Manual", "manual": {"shards": 2}}, + } + with pytest.raises(ValueError) as exc_info: + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + assert "replicas" in str(exc_info.value).lower() + + def test_parse_read_capacity_dedicated_missing_both_shards_and_replicas(self): + """Test that missing both shards and replicas raises appropriate error.""" + read_capacity = { + "mode": "Dedicated", + "dedicated": {"node_type": "t1", "scaling": "Manual", "manual": {}}, + } + with pytest.raises(ValueError) as exc_info: + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + assert "shards" in str(exc_info.value).lower() + assert "replicas" in str(exc_info.value).lower() + + def test_parse_read_capacity_dedicated_invalid_manual_type(self): + """Test that invalid manual type (not a dict) raises ValueError.""" + read_capacity = { + "mode": "Dedicated", + "dedicated": {"node_type": "t1", "scaling": "Manual", "manual": "invalid"}, + } + with pytest.raises(ValueError) as exc_info: + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + assert "dictionary" in str(exc_info.value).lower() + + def test_parse_read_capacity_dedicated_missing_node_type(self): + """Test that missing node_type raises ValueError.""" + read_capacity = {"mode": "Dedicated", "dedicated": {"scaling": "Manual"}} + with pytest.raises(ValueError) as exc_info: + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + assert "node_type" in str(exc_info.value).lower() + + def test_parse_read_capacity_dedicated_missing_scaling(self): + """Test that missing scaling raises ValueError.""" + read_capacity = {"mode": "Dedicated", "dedicated": {"node_type": "t1"}} + with pytest.raises(ValueError) as exc_info: + PineconeDBControlRequestFactory._PineconeDBControlRequestFactory__parse_read_capacity( + read_capacity + ) + assert "scaling" in str(exc_info.value).lower()