Skip to content

Commit 5854471

Browse files
authored
Implement deletion protection (#367)
## Problem Implement deletion protection for python sdk ## Solution - For the basic functionality, needed to update the create index and configure index actions - The UX around the generated enum class, `DeletionProtection`, seemed extremely poor so I added a custom wrapper class `IndexModel` and incorporated it into the `describe_index` and `list_indexes` actions - Update and add new tests - Remove export of the generated IndexModel from the top-level `__init__.py` file. This ended up being a much bigger lift than expected, but as a nice side effect we're no longer exposing generated objects in responses from these various actions. That creates some additional flexibility for future refactoring work. ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update - [ ] Infrastructure change (CI configs, etc) - [ ] Non-code change (docs, etc) ## Test Plan Describe specific steps for validating this change.
1 parent 5474c78 commit 5854471

28 files changed

+599
-144
lines changed

.github/workflows/testing-integration.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,17 @@ jobs:
117117
uses: ./.github/actions/setup-poetry
118118
- name: 'Run integration tests (REST, prod)'
119119
if: matrix.pineconeEnv == 'prod'
120-
run: poetry run pytest tests/integration/control/serverless -s -v
120+
run: poetry run pytest tests/integration/control/pod -s -v
121121
env:
122122
PINECONE_DEBUG_CURL: 'true'
123123
PINECONE_API_KEY: '${{ secrets.PINECONE_API_KEY }}'
124124
PINECONE_ENVIRONMENT: '${{ matrix.testConfig.pod.environment }}'
125125
GITHUB_BUILD_NUMBER: '${{ github.run_number }}-s-${{ matrix.testConfig.python-version}}'
126-
DIMENSION: 1536
126+
DIMENSION: 2500
127127
METRIC: 'cosine'
128128
- name: 'Run integration tests (REST, staging)'
129129
if: matrix.pineconeEnv == 'staging'
130-
run: poetry run pytest tests/integration/control/serverless -s -v
130+
run: poetry run pytest tests/integration/control/pod -s -v
131131
env:
132132
PINECONE_DEBUG_CURL: 'true'
133133
PINECONE_CONTROLLER_HOST: 'https://api-staging.pinecone.io'

codegen/apis

Submodule apis updated from fbd9d8d to e9b47c7

codegen/build-oas.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ update_apis_repo() {
1212
echo "Updating apis repo"
1313
pushd codegen/apis
1414
git fetch
15+
git checkout main
1516
git pull
1617
just build
1718
popd
@@ -21,6 +22,7 @@ update_templates_repo() {
2122
echo "Updating templates repo"
2223
pushd codegen/python-oas-templates
2324
git fetch
25+
git checkout main
2426
git pull
2527
popd
2628
}
@@ -126,6 +128,8 @@ extract_shared_classes() {
126128
done
127129
done
128130
done
131+
132+
echo "API_VERSION = '${version}'" > "${target_directory}/__init__.py"
129133
}
130134

131135
update_apis_repo

pinecone/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,4 @@
1414
from .data import *
1515
from .models import *
1616

17-
from .core.openapi.control.models import (
18-
IndexModel,
19-
)
20-
2117
from .utils import __version__

pinecone/control/pinecone.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import time
22
import warnings
33
import logging
4-
from typing import Optional, Dict, Any, Union, List, Tuple, cast, NamedTuple
4+
from typing import Optional, Dict, Any, Union, List, Tuple, Literal
55

66
from .index_host_store import IndexHostStore
77

@@ -23,12 +23,14 @@
2323
ConfigureIndexRequest,
2424
ConfigureIndexRequestSpec,
2525
ConfigureIndexRequestSpecPod,
26+
DeletionProtection,
2627
IndexSpec,
2728
ServerlessSpec as ServerlessSpecModel,
2829
PodSpec as PodSpecModel,
2930
PodSpecMetadataConfig,
3031
)
31-
from pinecone.models import ServerlessSpec, PodSpec, IndexList, CollectionList
32+
from pinecone.core.openapi.shared import API_VERSION
33+
from pinecone.models import ServerlessSpec, PodSpec, IndexModel, IndexList, CollectionList
3234
from .langchain_import_warnings import _build_langchain_attribute_error_message
3335

3436
from pinecone.data import Index
@@ -229,6 +231,7 @@ def __init__(
229231
config=self.config,
230232
openapi_config=self.openapi_config,
231233
pool_threads=pool_threads,
234+
api_version=API_VERSION,
232235
)
233236

234237
self.index_host_store = IndexHostStore()
@@ -258,6 +261,7 @@ def create_index(
258261
spec: Union[Dict, ServerlessSpec, PodSpec],
259262
metric: Optional[str] = "cosine",
260263
timeout: Optional[int] = None,
264+
deletion_protection: Optional[Literal["enabled", "disabled"]] = "disabled",
261265
):
262266
"""Creates a Pinecone index.
263267
@@ -278,6 +282,7 @@ def create_index(
278282
:type timeout: int, optional
279283
:param timeout: Specify the number of seconds to wait until index gets ready. If None, wait indefinitely; if >=0, time out after this many seconds;
280284
if -1, return immediately and do not wait. Default: None
285+
:param deletion_protection: If enabled, the index cannot be deleted. If disabled, the index can be deleted. Default: "disabled"
281286
282287
### Creating a serverless index
283288
@@ -291,7 +296,8 @@ def create_index(
291296
name="my_index",
292297
dimension=1536,
293298
metric="cosine",
294-
spec=ServerlessSpec(cloud="aws", region="us-west-2")
299+
spec=ServerlessSpec(cloud="aws", region="us-west-2"),
300+
deletion_protection="enabled"
295301
)
296302
```
297303
@@ -310,7 +316,8 @@ def create_index(
310316
spec=PodSpec(
311317
environment="us-east1-gcp",
312318
pod_type="p1.x1"
313-
)
319+
),
320+
deletion_protection="enabled"
314321
)
315322
```
316323
"""
@@ -320,6 +327,11 @@ def create_index(
320327
def _parse_non_empty_args(args: List[Tuple[str, Any]]) -> Dict[str, Any]:
321328
return {arg_name: val for arg_name, val in args if val is not None}
322329

330+
if deletion_protection in ["enabled", "disabled"]:
331+
dp = DeletionProtection(deletion_protection)
332+
else:
333+
raise ValueError("deletion_protection must be either 'enabled' or 'disabled'")
334+
323335
if isinstance(spec, dict):
324336
if "serverless" in spec:
325337
index_spec = IndexSpec(serverless=ServerlessSpecModel(**spec["serverless"]))
@@ -376,6 +388,7 @@ def _parse_non_empty_args(args: List[Tuple[str, Any]]) -> Dict[str, Any]:
376388
dimension=dimension,
377389
metric=metric,
378390
spec=index_spec,
391+
deletion_protection=dp,
379392
),
380393
)
381394

@@ -454,7 +467,7 @@ def list_indexes(self) -> IndexList:
454467
index name, dimension, metric, status, and spec.
455468
456469
:return: Returns an `IndexList` object, which is iterable and contains a
457-
list of `IndexDescription` objects. It also has a convenience method `names()`
470+
list of `IndexModel` objects. It also has a convenience method `names()`
458471
which returns a list of index names.
459472
460473
```python
@@ -498,7 +511,7 @@ def describe_index(self, name: str):
498511
"""Describes a Pinecone index.
499512
500513
:param name: the name of the index to describe.
501-
:return: Returns an `IndexDescription` object
514+
:return: Returns an `IndexModel` object
502515
which gives access to properties such as the
503516
index name, dimension, metric, host url, status,
504517
and spec.
@@ -530,13 +543,14 @@ def describe_index(self, name: str):
530543
host = description.host
531544
self.index_host_store.set_host(self.config, name, host)
532545

533-
return description
546+
return IndexModel(description)
534547

535548
def configure_index(
536549
self,
537550
name: str,
538551
replicas: Optional[int] = None,
539552
pod_type: Optional[str] = None,
553+
deletion_protection: Optional[Literal["enabled", "disabled"]] = None,
540554
):
541555
"""This method is used to scale configuration fields for your pod-based Pinecone index.
542556
@@ -561,15 +575,28 @@ def configure_index(
561575
562576
"""
563577
api_instance = self.index_api
564-
config_args: Dict[str, Any] = {}
578+
579+
if deletion_protection is None:
580+
description = self.describe_index(name=name)
581+
dp = DeletionProtection(description.deletion_protection)
582+
elif deletion_protection in ["enabled", "disabled"]:
583+
dp = DeletionProtection(deletion_protection)
584+
else:
585+
raise ValueError("deletion_protection must be either 'enabled' or 'disabled'")
586+
587+
pod_config_args: Dict[str, Any] = {}
565588
if pod_type:
566-
config_args.update(pod_type=pod_type)
589+
pod_config_args.update(pod_type=pod_type)
567590
if replicas:
568-
config_args.update(replicas=replicas)
569-
configure_index_request = ConfigureIndexRequest(
570-
spec=ConfigureIndexRequestSpec(pod=ConfigureIndexRequestSpecPod(**config_args))
571-
)
572-
api_instance.configure_index(name, configure_index_request=configure_index_request)
591+
pod_config_args.update(replicas=replicas)
592+
593+
if pod_config_args != {}:
594+
spec = ConfigureIndexRequestSpec(pod=ConfigureIndexRequestSpecPod(**pod_config_args))
595+
req = ConfigureIndexRequest(deletion_protection=dp, spec=spec)
596+
else:
597+
req = ConfigureIndexRequest(deletion_protection=dp)
598+
599+
api_instance.configure_index(name, configure_index_request=req)
573600

574601
def create_collection(self, name: str, source: str):
575602
"""Create a collection from a pod-based index
@@ -634,7 +661,6 @@ def describe_collection(self, name: str):
634661
print(description.source)
635662
print(description.status)
636663
print(description.size)
637-
print(description.)
638664
```
639665
"""
640666
api_instance = self.index_api

pinecone/core/openapi/control/api/inference_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
)
2424
from pinecone.core.openapi.control.model.embed_request import EmbedRequest
2525
from pinecone.core.openapi.control.model.embeddings_list import EmbeddingsList
26-
from pinecone.core.openapi.control.model.inline_response401 import InlineResponse401
26+
from pinecone.core.openapi.control.model.error_response import ErrorResponse
2727

2828

2929
class InferenceApi(object):

pinecone/core/openapi/control/api/manage_indexes_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
from pinecone.core.openapi.control.model.configure_index_request import ConfigureIndexRequest
2727
from pinecone.core.openapi.control.model.create_collection_request import CreateCollectionRequest
2828
from pinecone.core.openapi.control.model.create_index_request import CreateIndexRequest
29+
from pinecone.core.openapi.control.model.error_response import ErrorResponse
2930
from pinecone.core.openapi.control.model.index_list import IndexList
3031
from pinecone.core.openapi.control.model.index_model import IndexModel
31-
from pinecone.core.openapi.control.model.inline_response401 import InlineResponse401
3232

3333

3434
class ManageIndexesApi(object):
@@ -46,7 +46,7 @@ def __init__(self, api_client=None):
4646
def __configure_index(self, index_name, configure_index_request, **kwargs):
4747
"""Configure an index # noqa: E501
4848
49-
This operation specifies the pod type and number of replicas for an index. It applies to pod-based indexes only. Serverless indexes scale automatically based on usage. # noqa: E501
49+
This operation configures an existing index. For serverless indexes, you can configure only index deletion protection. For pod-based indexes, you can configure the pod size, number of replicas, and index deletion protection. It is not possible to change the pod type of a pod-based index. However, you can create a collection from a pod-based index and then [create a new pod-based index with a different pod type](http://docs.pinecone.io/guides/indexes/create-an-index#create-an-index-from-a-collection) from the collection. For guidance and examples, see [Configure an index](http://docs.pinecone.io/guides/indexes/configure-an-index). # noqa: E501
5050
This method makes a synchronous HTTP request by default. To make an
5151
asynchronous HTTP request, please pass async_req=True
5252
@@ -55,7 +55,7 @@ def __configure_index(self, index_name, configure_index_request, **kwargs):
5555
5656
Args:
5757
index_name (str): The name of the index to configure.
58-
configure_index_request (ConfigureIndexRequest): The desired pod type and replica configuration for the index.
58+
configure_index_request (ConfigureIndexRequest): The desired pod size and replica configuration for the index.
5959
6060
Keyword Args:
6161
_return_http_data_only (bool): response data without head status

pinecone/core/openapi/control/model/collection_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class CollectionModel(ModelNormal):
6363

6464
validations = {
6565
("dimension",): {
66-
"inclusive_maximum": 2000,
66+
"inclusive_maximum": 20000,
6767
"inclusive_minimum": 1,
6868
},
6969
}

pinecone/core/openapi/control/model/configure_index_request.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@
3131

3232
def lazy_import():
3333
from pinecone.core.openapi.control.model.configure_index_request_spec import ConfigureIndexRequestSpec
34+
from pinecone.core.openapi.control.model.deletion_protection import DeletionProtection
3435

3536
globals()["ConfigureIndexRequestSpec"] = ConfigureIndexRequestSpec
37+
globals()["DeletionProtection"] = DeletionProtection
3638

3739

3840
class ConfigureIndexRequest(ModelNormal):
@@ -97,6 +99,7 @@ def openapi_types():
9799
lazy_import()
98100
return {
99101
"spec": (ConfigureIndexRequestSpec,), # noqa: E501
102+
"deletion_protection": (DeletionProtection,), # noqa: E501
100103
}
101104

102105
@cached_property
@@ -105,6 +108,7 @@ def discriminator():
105108

106109
attribute_map = {
107110
"spec": "spec", # noqa: E501
111+
"deletion_protection": "deletion_protection", # noqa: E501
108112
}
109113

110114
read_only_vars = {}
@@ -113,12 +117,9 @@ def discriminator():
113117

114118
@classmethod
115119
@convert_js_args_to_python_args
116-
def _from_openapi_data(cls, spec, *args, **kwargs): # noqa: E501
120+
def _from_openapi_data(cls, *args, **kwargs): # noqa: E501
117121
"""ConfigureIndexRequest - a model defined in OpenAPI
118122
119-
Args:
120-
spec (ConfigureIndexRequestSpec):
121-
122123
Keyword Args:
123124
_check_type (bool): if True, values for parameters in openapi_types
124125
will be type checked and a TypeError will be
@@ -150,6 +151,8 @@ def _from_openapi_data(cls, spec, *args, **kwargs): # noqa: E501
150151
Animal class but this time we won't travel
151152
through its discriminator because we passed in
152153
_visited_composed_classes = (Animal,)
154+
spec (ConfigureIndexRequestSpec): [optional] # noqa: E501
155+
deletion_protection (DeletionProtection): [optional] # noqa: E501
153156
"""
154157

155158
_check_type = kwargs.pop("_check_type", True)
@@ -178,7 +181,6 @@ def _from_openapi_data(cls, spec, *args, **kwargs): # noqa: E501
178181
self._configuration = _configuration
179182
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
180183

181-
self.spec = spec
182184
for var_name, var_value in kwargs.items():
183185
if (
184186
var_name not in self.attribute_map
@@ -203,12 +205,9 @@ def _from_openapi_data(cls, spec, *args, **kwargs): # noqa: E501
203205
)
204206

205207
@convert_js_args_to_python_args
206-
def __init__(self, spec, *args, **kwargs): # noqa: E501
208+
def __init__(self, *args, **kwargs): # noqa: E501
207209
"""ConfigureIndexRequest - a model defined in OpenAPI
208210
209-
Args:
210-
spec (ConfigureIndexRequestSpec):
211-
212211
Keyword Args:
213212
_check_type (bool): if True, values for parameters in openapi_types
214213
will be type checked and a TypeError will be
@@ -240,6 +239,8 @@ def __init__(self, spec, *args, **kwargs): # noqa: E501
240239
Animal class but this time we won't travel
241240
through its discriminator because we passed in
242241
_visited_composed_classes = (Animal,)
242+
spec (ConfigureIndexRequestSpec): [optional] # noqa: E501
243+
deletion_protection (DeletionProtection): [optional] # noqa: E501
243244
"""
244245

245246
_check_type = kwargs.pop("_check_type", True)
@@ -266,7 +267,6 @@ def __init__(self, spec, *args, **kwargs): # noqa: E501
266267
self._configuration = _configuration
267268
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
268269

269-
self.spec = spec
270270
for var_name, var_value in kwargs.items():
271271
if (
272272
var_name not in self.attribute_map

0 commit comments

Comments
 (0)