nucliadb-utils 6.9.0.post5016__py3-none-any.whl → 6.9.6.post5473__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nucliadb-utils might be problematic. Click here for more details.

nucliadb_utils/const.py CHANGED
@@ -41,4 +41,4 @@ class Features:
41
41
  SKIP_EXTERNAL_INDEX = "nucliadb_skip_external_index"
42
42
  LOG_REQUEST_PAYLOADS = "nucliadb_log_request_payloads"
43
43
  IGNORE_EXTRACTED_IN_SEARCH = "nucliadb_ignore_extracted_in_search"
44
- DEBUG_MISSING_VECTORS = "nucliadb_debug_missing_vectors"
44
+ ASK_DECOUPLED = "nucliadb_ask_decoupled"
@@ -45,7 +45,7 @@ DEFAULT_FLAG_DATA: dict[str, Any] = {
45
45
  "rollout": 0,
46
46
  "variants": {"environment": ["local"]},
47
47
  },
48
- const.Features.DEBUG_MISSING_VECTORS: {
48
+ const.Features.ASK_DECOUPLED: {
49
49
  "rollout": 0,
50
50
  "variants": {"environment": ["local"]},
51
51
  },
@@ -20,6 +20,7 @@
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
+ import base64
23
24
  import logging
24
25
  from datetime import datetime
25
26
  from typing import AsyncGenerator, AsyncIterator, Optional, Union
@@ -426,13 +427,20 @@ class AzureObjectStore(ObjectStore):
426
427
  @ops_observer.wrap({"type": "multipart_start"})
427
428
  async def upload_multipart_start(self, bucket: str, key: str, metadata: ObjectMetadata) -> None:
428
429
  container_client = self.service_client.get_container_client(bucket)
429
- custom_metadata = {key: str(value) for key, value in metadata.model_dump().items()}
430
+ custom_metadata = {
431
+ "base64_filename": base64.b64encode(metadata.filename.encode()).decode(),
432
+ "content_type": metadata.content_type,
433
+ "size": str(metadata.size),
434
+ }
430
435
  blob_client = container_client.get_blob_client(key)
436
+ safe_filename = (
437
+ metadata.filename.encode("ascii", "replace").decode().replace('"', "").replace("\n", "")
438
+ )
431
439
  await blob_client.create_append_blob(
432
440
  metadata=custom_metadata,
433
441
  content_settings=ContentSettings(
434
442
  content_type=metadata.content_type,
435
- content_disposition=f"attachment; filename={metadata.filename}",
443
+ content_disposition=f'attachment; filename="{safe_filename}"',
436
444
  ),
437
445
  )
438
446
 
@@ -460,7 +468,12 @@ def parse_object_metadata(properties: BlobProperties, key: str) -> ObjectMetadat
460
468
  size = int(custom_metadata_size)
461
469
  else:
462
470
  size = properties.size
463
- filename = custom_metadata.get("filename") or key.split("/")[-1]
471
+
472
+ b64_filename = custom_metadata.get("base64_filename")
473
+ if b64_filename:
474
+ filename = base64.b64decode(b64_filename.encode()).decode()
475
+ else:
476
+ filename = key.split("/")[-1]
464
477
  content_type = custom_metadata.get("content_type") or properties.content_settings.content_type or ""
465
478
  return ObjectMetadata(
466
479
  filename=filename,
@@ -179,7 +179,7 @@ class S3StorageField(StorageField):
179
179
  Bucket=bucket_name,
180
180
  Key=upload_id,
181
181
  Metadata={
182
- "FILENAME": cf.filename,
182
+ "base64_filename": base64.b64encode(cf.filename.encode()).decode(),
183
183
  "SIZE": str(cf.size),
184
184
  "CONTENT_TYPE": cf.content_type,
185
185
  },
@@ -23,12 +23,14 @@ from typing import Any, Iterator, Type
23
23
  from unittest.mock import Mock
24
24
 
25
25
  import pytest
26
+ from pytest import FixtureRequest
26
27
  from pytest_lazy_fixtures import lazy_fixture
27
28
 
28
29
  from nucliadb_utils.storages.azure import AzureStorage
29
30
  from nucliadb_utils.storages.gcs import GCSStorage
30
31
  from nucliadb_utils.storages.local import LocalStorage
31
32
  from nucliadb_utils.storages.s3 import S3Storage
33
+ from nucliadb_utils.storages.storage import Storage
32
34
  from nucliadb_utils.utilities import Utility, clean_utility, set_utility
33
35
 
34
36
 
@@ -52,7 +54,7 @@ def hosted_nucliadb():
52
54
  nuclia_settings.onprem = original
53
55
 
54
56
 
55
- def get_testing_storage_backend():
57
+ def get_testing_storage_backend() -> str:
56
58
  """
57
59
  Default to gcs for linux users and s3 for macOS users. This is because some
58
60
  tests fail on macOS with the gcs backend with a weird nidx error (to be looked into).
@@ -72,7 +74,7 @@ def lazy_storage_fixture():
72
74
 
73
75
 
74
76
  @pytest.fixture(scope="function", params=lazy_storage_fixture())
75
- async def storage(request):
77
+ def storage(request: FixtureRequest) -> Iterator[Storage]:
76
78
  """
77
79
  Generic storage fixture that allows us to run the same tests for different storage backends.
78
80
  """
@@ -18,12 +18,16 @@
18
18
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
19
 
20
20
  import socket
21
+ from typing import AsyncIterator, Iterator
21
22
 
22
23
  import nats
23
24
  import pytest
24
25
  from pytest_docker_fixtures import images # type: ignore
25
26
  from pytest_docker_fixtures.containers._base import BaseImage # type: ignore
26
27
 
28
+ from nucliadb_utils.nats import NatsConnectionManager
29
+ from nucliadb_utils.utilities import start_nats_manager, stop_nats_manager
30
+
27
31
  images.settings["nats"] = {
28
32
  "image": "nats",
29
33
  "version": "2.10.21",
@@ -48,7 +52,7 @@ nats_image = NatsImage()
48
52
 
49
53
 
50
54
  @pytest.fixture(scope="session")
51
- def natsd(): # pragma: no cover
55
+ def natsd() -> Iterator[str]:
52
56
  nats_host, nats_port = nats_image.run()
53
57
  print("Started natsd docker")
54
58
  yield f"nats://{nats_host}:{nats_port}"
@@ -56,10 +60,17 @@ def natsd(): # pragma: no cover
56
60
 
57
61
 
58
62
  @pytest.fixture(scope="function")
59
- async def nats_server(natsd: str):
63
+ async def nats_server(natsd: str) -> AsyncIterator[str]:
60
64
  yield natsd
61
65
 
62
66
  # cleanup nats
63
67
  nc = await nats.connect(servers=[natsd])
64
68
  await nc.drain()
65
69
  await nc.close()
70
+
71
+
72
+ @pytest.fixture(scope="function")
73
+ async def nats_manager(nats_server: str) -> AsyncIterator[NatsConnectionManager]:
74
+ ncm = await start_nats_manager("nucliadb_tests", [nats_server], None)
75
+ yield ncm
76
+ await stop_nats_manager()
@@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union, cast
29
29
  from nucliadb_protos.writer_pb2_grpc import WriterStub
30
30
  from nucliadb_telemetry.metrics import Counter
31
31
  from nucliadb_utils import featureflagging
32
- from nucliadb_utils.aiopynecone.client import PineconeSession
33
32
  from nucliadb_utils.audit.audit import AuditStorage
34
33
  from nucliadb_utils.audit.basic import BasicAuditStorage
35
34
  from nucliadb_utils.audit.stream import StreamAuditStorage
@@ -85,7 +84,6 @@ class Utility(str, Enum):
85
84
  MAINDB_DRIVER = "driver"
86
85
  USAGE = "usage"
87
86
  ENDECRYPTOR = "endecryptor"
88
- PINECONE_SESSION = "pinecone_session"
89
87
  NIDX = "nidx"
90
88
 
91
89
 
@@ -367,6 +365,11 @@ async def stop_audit_utility():
367
365
  async def start_nats_manager(
368
366
  service_name: str, nats_servers: list[str], nats_creds: Optional[str] = None
369
367
  ) -> NatsConnectionManager:
368
+ util = get_utility(Utility.NATS_MANAGER)
369
+ if util is not None:
370
+ logger.warning("Warning, nats manager utility was already set, ignoring")
371
+ return util
372
+
370
373
  nats_manager = NatsConnectionManager(
371
374
  service_name=service_name,
372
375
  nats_servers=nats_servers,
@@ -439,16 +442,3 @@ def get_endecryptor() -> EndecryptorUtility:
439
442
  ) from ex
440
443
  set_utility(Utility.ENDECRYPTOR, util)
441
444
  return util
442
-
443
-
444
- def get_pinecone() -> PineconeSession:
445
- util = get_utility(Utility.PINECONE_SESSION)
446
- if util is not None:
447
- return util
448
- util = PineconeSession()
449
- set_utility(Utility.PINECONE_SESSION, util)
450
- return util
451
-
452
-
453
- def clean_pinecone():
454
- clean_utility(Utility.PINECONE_SESSION)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nucliadb_utils
3
- Version: 6.9.0.post5016
3
+ Version: 6.9.6.post5473
4
4
  Summary: NucliaDB util library
5
5
  Author-email: Nuclia <nucliadb@nuclia.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -8,13 +8,12 @@ Project-URL: Homepage, https://nuclia.com
8
8
  Project-URL: Repository, https://github.com/nuclia/nucliadb
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Programming Language :: Python
11
- Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Programming Language :: Python :: 3 :: Only
16
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
- Requires-Python: <4,>=3.9
16
+ Requires-Python: <4,>=3.10
18
17
  Description-Content-Type: text/markdown
19
18
  Requires-Dist: pydantic>=2.6
20
19
  Requires-Dist: pydantic-settings>=2.2
@@ -27,8 +26,9 @@ Requires-Dist: nats-py[nkeys]>=2.6.0
27
26
  Requires-Dist: PyNaCl
28
27
  Requires-Dist: pyjwt>=2.4.0
29
28
  Requires-Dist: mrflagly>=0.2.9
30
- Requires-Dist: nucliadb-protos>=6.9.0.post5016
31
- Requires-Dist: nucliadb-telemetry>=6.9.0.post5016
29
+ Requires-Dist: nidx-protos>=6.9.6.post5473
30
+ Requires-Dist: nucliadb-protos>=6.9.6.post5473
31
+ Requires-Dist: nucliadb-telemetry>=6.9.6.post5473
32
32
  Provides-Extra: cache
33
33
  Requires-Dist: redis>=4.3.4; extra == "cache"
34
34
  Requires-Dist: orjson>=3.6.7; extra == "cache"
@@ -1,10 +1,10 @@
1
1
  nucliadb_utils/__init__.py,sha256=EvBCH1iTODe-AgXm48aj4kVUt_Std3PeL8QnwimR5wI,895
2
2
  nucliadb_utils/asyncio_utils.py,sha256=h8Y-xpcFFRgNzaiIW0eidz7griAQa7ggbNk34-tAt2c,2888
3
3
  nucliadb_utils/authentication.py,sha256=5_b323v2ylJaJvM_0coeSQEtnD-p9IGD-6CPA6IXhik,6471
4
- nucliadb_utils/const.py,sha256=kgTPXlvVxJB_HPz9GkEigE94UNEkrnMECgrqhPW2Qi4,1537
4
+ nucliadb_utils/const.py,sha256=F1kZzTSbJvg9YYGhSi-H6KJsfTkTiX8PER4RBAOkq40,1521
5
5
  nucliadb_utils/debug.py,sha256=Q56Nx9Dp7V2ae3CU2H0ztaZcHTJXdlflPLKLeOPZ170,2436
6
6
  nucliadb_utils/exceptions.py,sha256=y_3wk77WLVUtdo-5FtbBsdSkCtK_DsJkdWb5BoPn3qo,1094
7
- nucliadb_utils/featureflagging.py,sha256=kLIJT-AJe8AqTuL0sY9nn9r5Soax8EvqPangM6UygfM,2380
7
+ nucliadb_utils/featureflagging.py,sha256=oKlSC3B-Bfx7xdrpalpmqUk3nCkMIm7BjyBMWlHC_RU,2372
8
8
  nucliadb_utils/grpc.py,sha256=apu0uePnkGHCAT7GRQ9YZfRYyFj26kJ440i8jitbM3U,3314
9
9
  nucliadb_utils/helpers.py,sha256=eed7_E1MKh9eW3CpqOXka3OvLw5C9eJGC_R-1MPYdfY,3336
10
10
  nucliadb_utils/nats.py,sha256=U21Cfg36_IHd3ZLXEC4eZ7nZ1Soh_ZNFFwjryNyd2-8,15248
@@ -15,11 +15,7 @@ nucliadb_utils/settings.py,sha256=H9yKrHPR5emTxai-D4owg4CjE4_-E0qR0HyuHERQNH4,84
15
15
  nucliadb_utils/signals.py,sha256=lo_Mk12NIX5Au--3H3WObvDOXq_OMurql2qiC2TnAao,2676
16
16
  nucliadb_utils/store.py,sha256=kQ35HemE0v4_Qg6xVqNIJi8vSFAYQtwI3rDtMsNy62Y,890
17
17
  nucliadb_utils/transaction.py,sha256=l3ZvrITYMnAs_fv1OOC-1nDZxWPG5qmbBhzvuC3DUzQ,8039
18
- nucliadb_utils/utilities.py,sha256=Q5sJmoX7JSnAId1BC9Lr3H9IQ85YmFwUMtw9Lco_brQ,16044
19
- nucliadb_utils/aiopynecone/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn4bIXMa20ks,835
20
- nucliadb_utils/aiopynecone/client.py,sha256=MPyHnDXwhukJr7U3CJh7BpsekfSuOkyM4g5b9LLtzc8,22941
21
- nucliadb_utils/aiopynecone/exceptions.py,sha256=fUErx3ceKQK1MUbOnYcZhIzpNe8UVAptZE9JIRDLXDE,4000
22
- nucliadb_utils/aiopynecone/models.py,sha256=XkNIZx4bxdbVo9zYVn8IRp70q4DWUMWN79ybGloFj2Q,3492
18
+ nucliadb_utils/utilities.py,sha256=SjPnCwCUH_lWUKSOZQp9vIcTYmLP0yL_UC8nPwlnds4,15817
23
19
  nucliadb_utils/audit/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn4bIXMa20ks,835
24
20
  nucliadb_utils/audit/audit.py,sha256=xmJJiAGG8rPGADwD9gXN9-QJ80GeGvqmY-kCwEf6PiQ,3598
25
21
  nucliadb_utils/audit/basic.py,sha256=fcCYvoFSGVbbB8cSCnm95bN2rf1AAeuWhGfh5no0S-Y,4246
@@ -39,25 +35,25 @@ nucliadb_utils/nuclia_usage/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn
39
35
  nucliadb_utils/nuclia_usage/utils/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn4bIXMa20ks,835
40
36
  nucliadb_utils/nuclia_usage/utils/kb_usage_report.py,sha256=6lLuxCCPQVn3dOuZNL5ThPjl2yws-1TJ_7duhQSWkPU,3934
41
37
  nucliadb_utils/storages/__init__.py,sha256=5Qc8AUWiJv9_JbGCBpAn88AIJhwDlm0OPQpg2ZdRL4U,872
42
- nucliadb_utils/storages/azure.py,sha256=pu0IyKPCn32oT0wI3oJIG6iUxnPtwNgg1zu00C8wDjo,18057
38
+ nucliadb_utils/storages/azure.py,sha256=t0ZL_698NqsBz-Ihwkc79tusfzGcm0BVTB5pqceNlvA,18456
43
39
  nucliadb_utils/storages/exceptions.py,sha256=6YhFLf8k0ABy5AVfxIJUo7w6AK0SJjktiyQTwF3gCdg,2344
44
40
  nucliadb_utils/storages/gcs.py,sha256=VyT72My34N4pEMmrQc5wdAMNLiuqpYl8OW3d50cJfSA,28222
45
41
  nucliadb_utils/storages/local.py,sha256=2aCHpZymORG_dUc1FDq0VFcgQulu0w2pZiUaj9dphFs,11686
46
42
  nucliadb_utils/storages/nuclia.py,sha256=vEv94xAT7QM2g80S25QyrOw2pzvP2BAX-ADgZLtuCVc,2097
47
43
  nucliadb_utils/storages/object_store.py,sha256=2PueRP5Q3XOuWgKhj6B9Kp2fyBql5np0T400YRUbqn4,4535
48
- nucliadb_utils/storages/s3.py,sha256=eFFVRgNTIxTz1Hpmd6ofRz9KQhPJAmiyetW4EmWN8EM,21835
44
+ nucliadb_utils/storages/s3.py,sha256=EUqlNoJW32AI6jpETbDla3teYbxlz8RFTfxSdHgWZdo,21878
49
45
  nucliadb_utils/storages/settings.py,sha256=mepN3wbLGL0Pv5yI6D-sNjSAFinEWT7aRi6N3eClNDg,1384
50
46
  nucliadb_utils/storages/storage.py,sha256=aOJnx6-WX8U3AAqPL_sWPCghIzlr8e3GKGi8z3-mtqw,22024
51
47
  nucliadb_utils/storages/utils.py,sha256=F4Iboa_0_bhDQr-JOKD9sGPld_-hKwJW5ptyZdn9Oag,1505
52
48
  nucliadb_utils/tests/__init__.py,sha256=Oo9CAE7B0eW5VHn8sHd6o30SQzOWUhktLPRXdlDOleA,1456
53
49
  nucliadb_utils/tests/asyncbenchmark.py,sha256=vrX_x9ifCXi18PfNShc23w9x_VUiB_Ph-2nuolh9z3Q,10707
54
50
  nucliadb_utils/tests/azure.py,sha256=rt1KRSYZW1EYhKy4Q0i7IEL9vdoOU6BYw2__S51YfGg,5039
55
- nucliadb_utils/tests/fixtures.py,sha256=4lzz-khYvbGzdbT18IG6KKg40f7CVex2q3ho88I-jL8,3799
51
+ nucliadb_utils/tests/fixtures.py,sha256=-OeR4NhtXveBqN6sZa7KVaNwyUdsvJUxS_yFmIgF148,3923
56
52
  nucliadb_utils/tests/gcs.py,sha256=JNqp5ymeNNU9Ci8rNYTh7-VqP4fjybElhyB3ap7EV1c,4721
57
53
  nucliadb_utils/tests/local.py,sha256=z9E11_ol1mu7N8Y6PkjKl-WMPPMl7JqQbDj3uhVa1A0,1933
58
- nucliadb_utils/tests/nats.py,sha256=RWHjwqq5esuO7OFbP24yYX1cXnpPLcWJwDUdmwCpH28,1897
54
+ nucliadb_utils/tests/nats.py,sha256=rbTaC6kv-u6SdZ7N-XBEGS40XCRiUmFUsKHIYWJfxTs,2325
59
55
  nucliadb_utils/tests/s3.py,sha256=kz9ULxrAYLVslZ59I8dtweZ9DJz5R8Ioy2XYrveZzHw,3829
60
- nucliadb_utils-6.9.0.post5016.dist-info/METADATA,sha256=TV9gR4uew98WpmF-KyxQiHmtWI2UDGy8LUWu9hQmHbU,2180
61
- nucliadb_utils-6.9.0.post5016.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
- nucliadb_utils-6.9.0.post5016.dist-info/top_level.txt,sha256=fE3vJtALTfgh7bcAWcNhcfXkNPp_eVVpbKK-2IYua3E,15
63
- nucliadb_utils-6.9.0.post5016.dist-info/RECORD,,
56
+ nucliadb_utils-6.9.6.post5473.dist-info/METADATA,sha256=V7vJvzCfdw4mJL-cZGkQsfGEdo45BVnKX1rI1OBg7fw,2174
57
+ nucliadb_utils-6.9.6.post5473.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ nucliadb_utils-6.9.6.post5473.dist-info/top_level.txt,sha256=fE3vJtALTfgh7bcAWcNhcfXkNPp_eVVpbKK-2IYua3E,15
59
+ nucliadb_utils-6.9.6.post5473.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- #
@@ -1,636 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- #
20
- import asyncio
21
- import json
22
- import logging
23
- import random
24
- from collections.abc import AsyncIterable, Iterable
25
- from itertools import islice
26
- from typing import Any, AsyncGenerator, Optional
27
-
28
- import backoff
29
- import httpx
30
-
31
- from nucliadb_telemetry.metrics import INF, Histogram, Observer
32
- from nucliadb_utils.aiopynecone.exceptions import (
33
- PineconeAPIError,
34
- PineconeRateLimitError,
35
- RetriablePineconeAPIError,
36
- raise_for_status,
37
- )
38
- from nucliadb_utils.aiopynecone.models import (
39
- CreateIndexRequest,
40
- CreateIndexResponse,
41
- IndexDescription,
42
- IndexStats,
43
- ListResponse,
44
- QueryResponse,
45
- UpsertRequest,
46
- Vector,
47
- )
48
-
49
- logger = logging.getLogger(__name__)
50
-
51
- upsert_batch_size_histogram = Histogram(
52
- "pinecone_upsert_batch_size",
53
- buckets=[10.0, 100.0, 200.0, 500.0, 1000.0, 5000.0, INF],
54
- )
55
- upsert_batch_count_histogram = Histogram(
56
- "pinecone_upsert_batch_count",
57
- buckets=[0.0, 1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 20.0, 30.0, 50.0, INF],
58
- )
59
-
60
- delete_batch_size_histogram = Histogram(
61
- "pinecone_delete_batch_size",
62
- buckets=[1.0, 5.0, 10.0, 20.0, 50.0, 100.0, 150.0, INF],
63
- )
64
-
65
- delete_batch_count_histogram = Histogram(
66
- "pinecone_delete_batch_count",
67
- buckets=[0.0, 1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 20.0, 30.0, 50.0, INF],
68
- )
69
-
70
-
71
- pinecone_observer = Observer(
72
- "pinecone_client",
73
- labels={"type": ""},
74
- error_mappings={
75
- "rate_limit": PineconeRateLimitError,
76
- },
77
- )
78
-
79
- DEFAULT_TIMEOUT = 30
80
- CONTROL_PLANE_BASE_URL = "https://api.pinecone.io/"
81
- BASE_API_HEADERS = {
82
- "Content-Type": "application/json",
83
- "Accept": "application/json",
84
- # This is needed so that it is easier for Pinecone to track which api requests
85
- # are coming from the Nuclia integration:
86
- # https://docs.pinecone.io/integrations/build-integration/attribute-usage-to-your-integration
87
- "User-Agent": "source_tag=nuclia",
88
- "X-Pinecone-API-Version": "2024-10",
89
- }
90
- MEGA_BYTE = 1024 * 1024
91
- MAX_UPSERT_PAYLOAD_SIZE = 2 * MEGA_BYTE
92
- MAX_DELETE_BATCH_SIZE = 1000
93
- MAX_LIST_PAGE_SIZE = 100
94
-
95
-
96
- RETRIABLE_EXCEPTIONS = (
97
- PineconeRateLimitError,
98
- RetriablePineconeAPIError,
99
- httpx.ConnectError,
100
- httpx.NetworkError,
101
- httpx.WriteTimeout,
102
- httpx.ReadTimeout,
103
- )
104
-
105
-
106
- class ControlPlane:
107
- """
108
- Client for interacting with the Pinecone control plane API.
109
- https://docs.pinecone.io/reference/api/control-plane
110
- """
111
-
112
- def __init__(self, api_key: str, http_session: httpx.AsyncClient):
113
- self.api_key = api_key
114
- self.http_session = http_session
115
-
116
- @pinecone_observer.wrap({"type": "create_index"})
117
- async def create_index(
118
- self,
119
- name: str,
120
- dimension: int,
121
- metric: str = "dotproduct",
122
- serverless_cloud: Optional[dict[str, str]] = None,
123
- ) -> str:
124
- """
125
- Create a new index in Pinecone. It can only create serverless indexes on the AWS us-east-1 region.
126
- Params:
127
- - `name`: The name of the index.
128
- - `dimension`: The dimension of the vectors in the index.
129
- - `metric`: The similarity metric to use. Default is "dotproduct".
130
- - `serverless_cloud`: The serverless provider to use. Default is AWS us-east-1.
131
- Returns:
132
- - The index host to be used for data plane operations.
133
- """
134
- serverless_cloud = serverless_cloud or {"cloud": "aws", "region": "us-east-1"}
135
- payload = CreateIndexRequest(
136
- name=name,
137
- dimension=dimension,
138
- metric=metric,
139
- spec={"serverless": serverless_cloud},
140
- )
141
- headers = {"Api-Key": self.api_key}
142
- http_response = await self.http_session.post(
143
- "/indexes", json=payload.model_dump(), headers=headers
144
- )
145
- raise_for_status("create_index", http_response)
146
- response = CreateIndexResponse.model_validate(http_response.json())
147
- return response.host
148
-
149
- @pinecone_observer.wrap({"type": "delete_index"})
150
- async def delete_index(self, name: str) -> None:
151
- """
152
- Delete an index in Pinecone.
153
- Params:
154
- - `name`: The name of the index to delete.
155
- """
156
- headers = {"Api-Key": self.api_key}
157
- response = await self.http_session.delete(f"/indexes/{name}", headers=headers)
158
- if response.status_code == 404: # pragma: no cover
159
- logger.warning("Pinecone index not found.", extra={"index_name": name})
160
- return
161
- raise_for_status("delete_index", response)
162
-
163
- @pinecone_observer.wrap({"type": "describe_index"})
164
- async def describe_index(self, name: str) -> IndexDescription:
165
- """
166
- Describe an index in Pinecone.
167
- Params:
168
- - `name`: The name of the index to describe.
169
- """
170
- headers = {"Api-Key": self.api_key}
171
- response = await self.http_session.get(f"/indexes/{name}", headers=headers)
172
- raise_for_status("describe_index", response)
173
- return IndexDescription.model_validate(response.json())
174
-
175
-
176
- class DataPlane:
177
- """
178
- Client for interacting with the Pinecone data plane API, hosted by an index host.
179
- https://docs.pinecone.io/reference/api/data-plane
180
- """
181
-
182
- def __init__(
183
- self, api_key: str, index_host_session: httpx.AsyncClient, timeout: Optional[float] = None
184
- ):
185
- """
186
- Params:
187
- - `api_key`: The Pinecone API key.
188
- - `index_host_session`: The http session for the index host.
189
- - `timeout`: The default timeout for all requests. If not set, the default timeout from httpx.AsyncClient is used.
190
- """
191
- self.api_key = api_key
192
- self.http_session = index_host_session
193
- self.client_timeout = timeout
194
- self._upsert_batch_size: Optional[int] = None
195
-
196
- def _get_request_timeout(self, timeout: Optional[float] = None) -> Optional[float]:
197
- return timeout or self.client_timeout
198
-
199
- @pinecone_observer.wrap({"type": "stats"})
200
- async def stats(self, filter: Optional[dict[str, Any]] = None) -> IndexStats:
201
- """
202
- Get the index stats.
203
- Params:
204
- - `filter`: to filter the stats by their metadata. See:
205
- https://docs.pinecone.io/reference/api/2024-07/data-plane/describeindexstats
206
- """
207
- post_kwargs: dict[str, Any] = {
208
- "headers": {"Api-Key": self.api_key},
209
- }
210
- if filter is not None:
211
- post_kwargs["json"] = {
212
- "filter": filter,
213
- }
214
- response = await self.http_session.post("/describe_index_stats", **post_kwargs)
215
- raise_for_status("stats", response)
216
- return IndexStats.model_validate(response.json())
217
-
218
- @backoff.on_exception(
219
- backoff.expo,
220
- RETRIABLE_EXCEPTIONS,
221
- jitter=backoff.random_jitter,
222
- max_tries=4,
223
- )
224
- @pinecone_observer.wrap({"type": "upsert"})
225
- async def upsert(self, vectors: list[Vector], timeout: Optional[float] = None) -> None:
226
- """
227
- Upsert vectors into the index.
228
- Params:
229
- - `vectors`: The vectors to upsert.
230
- - `timeout`: to control the request timeout. If not set, the default timeout is used.
231
- """
232
- if len(vectors) == 0:
233
- # Nothing to upsert.
234
- return
235
- upsert_batch_size_histogram.observe(len(vectors))
236
- headers = {"Api-Key": self.api_key}
237
- payload = UpsertRequest(vectors=vectors)
238
- post_kwargs: dict[str, Any] = {
239
- "headers": headers,
240
- "json": payload.model_dump(),
241
- }
242
- request_timeout = self._get_request_timeout(timeout)
243
- if request_timeout is not None:
244
- post_kwargs["timeout"] = timeout
245
- response = await self.http_session.post("/vectors/upsert", **post_kwargs)
246
- raise_for_status("upsert", response)
247
-
248
- def _estimate_upsert_batch_size(self, vectors: list[Vector]) -> int:
249
- """
250
- Estimate a batch size so that the upsert payload does not exceed the hard limit.
251
- https://docs.pinecone.io/reference/quotas-and-limits#hard-limits
252
- """
253
- if self._upsert_batch_size is not None:
254
- # Return the cached value.
255
- return self._upsert_batch_size
256
- # Take the dimension of the first vector as the vector dimension.
257
- # Assumes all vectors have the same dimension.
258
- vector_dimension = len(vectors[0].values)
259
- # Estimate the metadata size by taking the average of 20 random vectors.
260
- metadata_sizes = []
261
- for _ in range(20):
262
- metadata_sizes.append(len(json.dumps(random.choice(vectors).metadata)))
263
- average_metadata_size = sum(metadata_sizes) / len(metadata_sizes)
264
- # Estimate the size of the vector payload. 4 bytes per float.
265
- vector_size = 4 * vector_dimension + average_metadata_size
266
- # Cache the value.
267
- self._upsert_batch_size = max(int(MAX_UPSERT_PAYLOAD_SIZE // vector_size), 1)
268
- return self._upsert_batch_size
269
-
270
- @pinecone_observer.wrap({"type": "upsert_in_batches"})
271
- async def upsert_in_batches(
272
- self,
273
- vectors: list[Vector],
274
- batch_size: Optional[int] = None,
275
- max_parallel_batches: int = 1,
276
- batch_timeout: Optional[float] = None,
277
- ) -> None:
278
- """
279
- Upsert vectors in batches.
280
- Params:
281
- - `vectors`: The vectors to upsert.
282
- - `batch_size`: to control the number of vectors in each batch.
283
- - `max_parallel_batches`: to control the number of batches sent concurrently.
284
- - `batch_timeout`: to control the request timeout for each batch.
285
- """
286
- if batch_size is None:
287
- batch_size = self._estimate_upsert_batch_size(vectors)
288
-
289
- semaphore = asyncio.Semaphore(max_parallel_batches)
290
-
291
- async def _upsert_batch(batch):
292
- async with semaphore:
293
- await self.upsert(vectors=batch, timeout=batch_timeout)
294
-
295
- tasks = []
296
- for batch in batchify(vectors, batch_size):
297
- tasks.append(asyncio.create_task(_upsert_batch(batch)))
298
-
299
- upsert_batch_count_histogram.observe(len(tasks))
300
-
301
- if len(tasks) > 0:
302
- await asyncio.gather(*tasks)
303
-
304
- @backoff.on_exception(
305
- backoff.expo,
306
- RETRIABLE_EXCEPTIONS,
307
- jitter=backoff.random_jitter,
308
- max_tries=4,
309
- )
310
- @pinecone_observer.wrap({"type": "delete"})
311
- async def delete(self, ids: list[str], timeout: Optional[float] = None) -> None:
312
- """
313
- Delete vectors by their ids.
314
- Maximum number of ids in a single request is 1000.
315
- Params:
316
- - `ids`: The ids of the vectors to delete.
317
- - `timeout`: to control the request timeout. If not set, the default timeout is used.
318
- """
319
- if len(ids) > MAX_DELETE_BATCH_SIZE:
320
- raise ValueError(f"Maximum number of ids in a single request is {MAX_DELETE_BATCH_SIZE}.")
321
- if len(ids) == 0: # pragma: no cover
322
- return
323
-
324
- delete_batch_size_histogram.observe(len(ids))
325
- headers = {"Api-Key": self.api_key}
326
-
327
- # This is a temporary log info to hunt down a bug.
328
- rids = {vid.split("/")[0] for vid in ids}
329
- logger.info(f"Deleting vectors from resources: {list(rids)}")
330
-
331
- payload = {"ids": ids}
332
- post_kwargs: dict[str, Any] = {
333
- "headers": headers,
334
- "json": payload,
335
- }
336
- request_timeout = self._get_request_timeout(timeout)
337
- if request_timeout is not None:
338
- post_kwargs["timeout"] = timeout
339
- response = await self.http_session.post("/vectors/delete", **post_kwargs)
340
- raise_for_status("delete", response)
341
-
342
- @backoff.on_exception(
343
- backoff.expo,
344
- RETRIABLE_EXCEPTIONS,
345
- jitter=backoff.random_jitter,
346
- max_tries=4,
347
- )
348
- @pinecone_observer.wrap({"type": "list_page"})
349
- async def list_page(
350
- self,
351
- id_prefix: Optional[str] = None,
352
- limit: int = MAX_LIST_PAGE_SIZE,
353
- pagination_token: Optional[str] = None,
354
- timeout: Optional[float] = None,
355
- ) -> ListResponse:
356
- """
357
- List vectors in a paginated manner.
358
- Params:
359
- - `id_prefix`: to filter vectors by their id prefix.
360
- - `limit`: to control the number of vectors fetched in each page.
361
- - `pagination_token`: to fetch the next page. The token is provided in the response
362
- if there are more pages to fetch.
363
- - `timeout`: to control the request timeout. If not set, the default timeout is used.
364
- """
365
- if limit > MAX_LIST_PAGE_SIZE: # pragma: no cover
366
- raise ValueError(f"Maximum limit is {MAX_LIST_PAGE_SIZE}.")
367
- headers = {"Api-Key": self.api_key}
368
- params = {"limit": str(limit)}
369
- if id_prefix is not None:
370
- params["prefix"] = id_prefix
371
- if pagination_token is not None:
372
- params["paginationToken"] = pagination_token
373
-
374
- post_kwargs: dict[str, Any] = {
375
- "headers": headers,
376
- "params": params,
377
- }
378
- request_timeout = self._get_request_timeout(timeout)
379
- if request_timeout is not None:
380
- post_kwargs["timeout"] = timeout
381
- response = await self.http_session.get(
382
- "/vectors/list",
383
- **post_kwargs,
384
- )
385
- raise_for_status("list_page", response)
386
- return ListResponse.model_validate(response.json())
387
-
388
- async def list_all(
389
- self,
390
- id_prefix: Optional[str] = None,
391
- page_size: int = MAX_LIST_PAGE_SIZE,
392
- page_timeout: Optional[float] = None,
393
- ) -> AsyncGenerator[str, None]:
394
- """
395
- Iterate over all vector ids from the index in a paginated manner.
396
- Params:
397
- - `id_prefix`: to filter vectors by their id prefix.
398
- - `page_size`: to control the number of vectors fetched in each page.
399
- - `page_timeout`: to control the request timeout for each page. If not set, the default timeout is used.
400
- """
401
- pagination_token = None
402
- while True:
403
- response = await self.list_page(
404
- id_prefix=id_prefix,
405
- limit=page_size,
406
- pagination_token=pagination_token,
407
- timeout=page_timeout,
408
- )
409
- for vector_id in response.vectors:
410
- yield vector_id.id
411
- if response.pagination is None:
412
- break
413
- pagination_token = response.pagination.next
414
-
415
- @backoff.on_exception(
416
- backoff.expo,
417
- RETRIABLE_EXCEPTIONS,
418
- jitter=backoff.random_jitter,
419
- max_tries=4,
420
- )
421
- @pinecone_observer.wrap({"type": "delete_all"})
422
- async def delete_all(self, timeout: Optional[float] = None):
423
- """
424
- Delete all vectors in the index.
425
- Params:
426
- - `timeout`: to control the request timeout. If not set, the default timeout is used.
427
- """
428
- headers = {"Api-Key": self.api_key}
429
- payload = {"deleteAll": True, "ids": [], "namespace": ""}
430
- post_kwargs: dict[str, Any] = {
431
- "headers": headers,
432
- "json": payload,
433
- }
434
- request_timeout = self._get_request_timeout(timeout)
435
- if request_timeout is not None:
436
- post_kwargs["timeout"] = timeout
437
- response = await self.http_session.post("/vectors/delete", **post_kwargs)
438
- try:
439
- raise_for_status("delete_all", response)
440
- except PineconeAPIError as err:
441
- if err.http_status_code == 404 and err.code == 5: # pragma: no cover
442
- # Namespace not found. No vectors to delete.
443
- return
444
- raise
445
-
446
- @pinecone_observer.wrap({"type": "delete_by_id_prefix"})
447
- async def delete_by_id_prefix(
448
- self,
449
- id_prefix: str,
450
- batch_size: int = MAX_DELETE_BATCH_SIZE,
451
- max_parallel_batches: int = 1,
452
- batch_timeout: Optional[float] = None,
453
- ) -> None:
454
- """
455
- Delete vectors by their id prefix. It lists all vectors with the given prefix and deletes them in batches.
456
- Params:
457
- - `id_prefix`: to filter vectors by their id prefix.
458
- - `batch_size`: to control the number of vectors deleted in each batch. Maximum is 1000.
459
- - `max_parallel_batches`: to control the number of batches sent concurrently.
460
- - `batch_timeout`: to control the request timeout for each batch.
461
- """
462
- if batch_size > MAX_DELETE_BATCH_SIZE:
463
- logger.warning(f"Batch size {batch_size} is too large. Limiting to {MAX_DELETE_BATCH_SIZE}.")
464
- batch_size = MAX_DELETE_BATCH_SIZE
465
-
466
- semaphore = asyncio.Semaphore(max_parallel_batches)
467
-
468
- async def _delete_batch(batch):
469
- async with semaphore:
470
- await self.delete(ids=batch, timeout=batch_timeout)
471
-
472
- tasks = []
473
- async_iterable = self.list_all(id_prefix=id_prefix, page_timeout=batch_timeout)
474
- async for batch in async_batchify(async_iterable, batch_size):
475
- tasks.append(asyncio.create_task(_delete_batch(batch)))
476
-
477
- delete_batch_count_histogram.observe(len(tasks))
478
-
479
- if len(tasks) > 0:
480
- await asyncio.gather(*tasks)
481
-
482
- @backoff.on_exception(
483
- backoff.expo,
484
- RETRIABLE_EXCEPTIONS,
485
- jitter=backoff.random_jitter,
486
- max_tries=4,
487
- )
488
- @pinecone_observer.wrap({"type": "query"})
489
- async def query(
490
- self,
491
- vector: list[float],
492
- top_k: int = 20,
493
- include_values: bool = False,
494
- include_metadata: bool = False,
495
- filter: Optional[dict[str, Any]] = None,
496
- timeout: Optional[float] = None,
497
- ) -> QueryResponse:
498
- """
499
- Query the index for similar vectors to the given vector.
500
- Params:
501
- - `vector`: The query vector.
502
- - `top_k`: to control the number of similar vectors to return.
503
- - `include_values`: to include the vector values in the response.
504
- - `include_metadata`: to include the vector metadata in the response.
505
- - `filter`: to filter the vectors by their metadata. See:
506
- https://docs.pinecone.io/guides/data/filter-with-metadata#metadata-query-language
507
- - `timeout`: to control the request timeout. If not set, the default timeout is used.
508
- """
509
- headers = {"Api-Key": self.api_key}
510
- payload = {
511
- "vector": vector,
512
- "topK": top_k,
513
- "includeValues": include_values,
514
- "includeMetadata": include_metadata,
515
- }
516
- if filter:
517
- payload["filter"] = filter
518
- post_kwargs: dict[str, Any] = {
519
- "headers": headers,
520
- "json": payload,
521
- }
522
- request_timeout = self._get_request_timeout(timeout)
523
- if request_timeout is not None:
524
- post_kwargs["timeout"] = timeout
525
- response = await self.http_session.post("/query", **post_kwargs)
526
- raise_for_status("query", response)
527
- return QueryResponse.model_validate(response.json())
528
-
529
-
530
- class PineconeSession:
531
- """
532
- Wrapper class that manages the sessions around all Pinecone http api interactions.
533
- Holds a single control plane session and multiple data plane sessions, one for each index host.
534
- """
535
-
536
- def __init__(self):
537
- self.control_plane_session = httpx.AsyncClient(
538
- base_url=CONTROL_PLANE_BASE_URL, headers=BASE_API_HEADERS, timeout=DEFAULT_TIMEOUT
539
- )
540
- self.index_host_sessions = {}
541
-
542
- async def __aenter__(self):
543
- return self
544
-
545
- async def __aexit__(self, exc_type, exc_val, exc_tb):
546
- await self.finalize()
547
-
548
- async def finalize(self):
549
- if not self.control_plane_session.is_closed:
550
- await self.control_plane_session.aclose()
551
- for session in self.index_host_sessions.values():
552
- if not session.is_closed:
553
- await session.aclose()
554
- self.index_host_sessions.clear()
555
-
556
- def control_plane(self, api_key: str) -> ControlPlane:
557
- return ControlPlane(api_key=api_key, http_session=self.control_plane_session)
558
-
559
- def _get_index_host_session(self, index_host: str) -> httpx.AsyncClient:
560
- """
561
- Get a session for the given index host.
562
- Cache http sessions so that they are reused for the same index host.
563
- """
564
- session = self.index_host_sessions.get(index_host, None)
565
- if session is not None:
566
- return session
567
-
568
- base_url = index_host
569
- if not index_host.startswith("https://"):
570
- base_url = f"https://{index_host}/"
571
-
572
- session = httpx.AsyncClient(
573
- base_url=base_url,
574
- headers=BASE_API_HEADERS,
575
- timeout=DEFAULT_TIMEOUT,
576
- )
577
- self.index_host_sessions[index_host] = session
578
- return session
579
-
580
- def data_plane(self, api_key: str, index_host: str, timeout: Optional[float] = None) -> DataPlane:
581
- index_host_session = self._get_index_host_session(index_host)
582
- return DataPlane(api_key=api_key, index_host_session=index_host_session, timeout=timeout)
583
-
584
-
585
- def batchify(iterable: Iterable, batch_size: int):
586
- """
587
- Split an iterable into batches of batch_size
588
- """
589
- iterator = iter(iterable)
590
- while True:
591
- batch = list(islice(iterator, batch_size))
592
- if not batch:
593
- break
594
- yield batch
595
-
596
-
597
- async def async_batchify(async_iterable: AsyncIterable, batch_size: int):
598
- """
599
- Split an async iterable into batches of batch_size
600
- """
601
- batch = []
602
- async for item in async_iterable:
603
- batch.append(item)
604
- if len(batch) == batch_size:
605
- yield batch
606
- batch = []
607
- if batch:
608
- yield batch
609
-
610
-
611
- class FilterOperator:
612
- """
613
- Filter operators for metadata queries.
614
- https://docs.pinecone.io/guides/data/filter-with-metadata#metadata-query-language
615
- """
616
-
617
- EQUALS = "$eq"
618
- NOT_EQUALS = "$ne"
619
- GREATER_THAN = "$gt"
620
- GREATER_THAN_OR_EQUAL = "$gte"
621
- LESS_THAN = "$lt"
622
- LESS_THAN_OR_EQUAL = "$lte"
623
- IN = "$in"
624
- NOT_IN = "$nin"
625
- EXISTS = "$exists"
626
-
627
-
628
- class LogicalOperator:
629
- """
630
- Logical operators for metadata queries.
631
- https://docs.pinecone.io/guides/data/filter-with-metadata#metadata-query-language
632
- """
633
-
634
- AND = "$and"
635
- OR = "$or"
636
- NOT = "$not"
@@ -1,131 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- #
20
-
21
- from typing import Any, Optional
22
-
23
- import httpx
24
-
25
- from nucliadb_telemetry.metrics import Counter
26
-
27
- pinecone_errors_counter = Counter("pinecone_errors", labels={"type": "", "status_code": ""})
28
-
29
-
30
- class PineconeAPIError(Exception):
31
- """
32
- Generic Pinecone API error.
33
- """
34
-
35
- def __init__(
36
- self,
37
- http_status_code: int,
38
- code: Optional[str] = None,
39
- message: Optional[str] = None,
40
- details: Optional[Any] = None,
41
- ):
42
- self.http_status_code = http_status_code
43
- self.code = code or ""
44
- self.message = message or ""
45
- self.details = details or {}
46
- exc_message = '[{http_status_code}] message="{message}" code={code} details={details}'.format(
47
- http_status_code=http_status_code,
48
- message=message,
49
- code=code,
50
- details=details,
51
- )
52
- super().__init__(exc_message)
53
-
54
-
55
- class RetriablePineconeAPIError(PineconeAPIError):
56
- """
57
- Raised when the client can retry the operation.
58
- """
59
-
60
- pass
61
-
62
-
63
- class PineconeRateLimitError(RetriablePineconeAPIError):
64
- """
65
- Raised when the client has exceeded the rate limit to be able to backoff and retry.
66
- """
67
-
68
- pass
69
-
70
-
71
- class PineconeNeedsPlanUpgradeError(PineconeAPIError):
72
- """
73
- Raised when the client needs to upgrade the plan to continue using the service.
74
- """
75
-
76
- pass
77
-
78
-
79
- class MetadataTooLargeError(ValueError):
80
- """
81
- Raised when the metadata of a vector to be upserted is too large.
82
- """
83
-
84
- pass
85
-
86
-
87
- def raise_for_status(operation: str, response: httpx.Response):
88
- try:
89
- response.raise_for_status()
90
- except httpx.HTTPStatusError:
91
- pinecone_errors_counter.inc(labels={"type": operation, "status_code": str(response.status_code)})
92
- code = None
93
- message = None
94
- details = None
95
- try:
96
- resp_json = response.json()
97
- error = resp_json.get("error") or {}
98
- code = resp_json.get("code") or error.get("code")
99
- message = resp_json.get("message") or error.get("message") or ""
100
- details = resp_json.get("details") or error.get("details")
101
- except Exception: # pragma: no cover
102
- message = response.text
103
- if response.status_code == 429:
104
- if "month" in message:
105
- raise PineconeNeedsPlanUpgradeError(
106
- http_status_code=response.status_code,
107
- code=code,
108
- message=message,
109
- details=details,
110
- )
111
- raise PineconeRateLimitError(
112
- http_status_code=response.status_code,
113
- code=code,
114
- message=message,
115
- details=details,
116
- )
117
-
118
- if str(response.status_code).startswith("5"):
119
- raise RetriablePineconeAPIError(
120
- http_status_code=response.status_code,
121
- code=code,
122
- message=message,
123
- details=details,
124
- )
125
-
126
- raise PineconeAPIError(
127
- http_status_code=response.status_code,
128
- code=code,
129
- message=message,
130
- details=details,
131
- )
@@ -1,139 +0,0 @@
1
- # Copyright (C) 2021 Bosutech XXI S.L.
2
- #
3
- # nucliadb is offered under the AGPL v3.0 and as commercial software.
4
- # For commercial licensing, contact us at info@nuclia.com.
5
- #
6
- # AGPL:
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU Affero General Public License as
9
- # published by the Free Software Foundation, either version 3 of the
10
- # License, or (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU Affero General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU Affero General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- #
20
- import json
21
- from typing import Any, Optional
22
-
23
- import pydantic
24
- from pydantic import BaseModel, Field, field_validator
25
- from typing_extensions import Annotated
26
-
27
- from nucliadb_utils.aiopynecone.exceptions import MetadataTooLargeError
28
-
29
- KILO_BYTE = 1024
30
- MAX_METADATA_SIZE = 40 * KILO_BYTE
31
- MAX_INDEX_NAME_LENGTH = 45
32
- MAX_VECTOR_ID_LENGTH = 512
33
-
34
-
35
- # Requests
36
-
37
- IndexNamePattern = r"^[a-z0-9-]+$"
38
-
39
-
40
- def validate_index_name(value, handler, info):
41
- try:
42
- return handler(value)
43
- except pydantic.ValidationError as e:
44
- if any(x["type"] == "string_pattern_mismatch" for x in e.errors()):
45
- raise ValueError(
46
- f"Invalid field_id: '{value}'. Pinecone index names must be a string with only "
47
- "lowercase letters, numbers and dashes."
48
- )
49
- else:
50
- raise e
51
-
52
-
53
- IndexNameStr = Annotated[
54
- str,
55
- pydantic.StringConstraints(pattern=IndexNamePattern, min_length=1, max_length=MAX_INDEX_NAME_LENGTH),
56
- pydantic.WrapValidator(validate_index_name),
57
- ]
58
-
59
-
60
- class CreateIndexRequest(BaseModel):
61
- name: IndexNameStr
62
- dimension: int
63
- metric: str
64
- spec: dict[str, Any] = {}
65
-
66
-
67
- class Vector(BaseModel):
68
- id: str = Field(min_length=1, max_length=MAX_VECTOR_ID_LENGTH)
69
- values: list[float]
70
- metadata: dict[str, Any] = {}
71
-
72
- @field_validator("metadata", mode="after")
73
- @classmethod
74
- def validate_metadata_size(cls, value):
75
- json_value = json.dumps(value)
76
- if len(json_value) > MAX_METADATA_SIZE:
77
- raise MetadataTooLargeError(f"metadata size is too large: {len(json_value)} bytes")
78
- return value
79
-
80
-
81
- class UpsertRequest(BaseModel):
82
- vectors: list[Vector]
83
-
84
-
85
- # Responses
86
-
87
-
88
- class CreateIndexResponse(BaseModel):
89
- host: str
90
-
91
-
92
- class VectorId(BaseModel):
93
- id: str
94
-
95
-
96
- class Pagination(BaseModel):
97
- next: str
98
-
99
-
100
- class ListResponse(BaseModel):
101
- vectors: list[VectorId]
102
- pagination: Optional[Pagination] = None
103
-
104
-
105
- class VectorMatch(BaseModel):
106
- id: str
107
- score: float
108
- # Only populated if `includeValues` is set to `True
109
- values: Optional[list[float]] = None
110
- # Only populated if `includeMetadata` is set to `True
111
- metadata: Optional[dict[str, Any]] = None
112
-
113
-
114
- class QueryResponse(BaseModel):
115
- matches: list[VectorMatch]
116
-
117
-
118
- class IndexNamespaceStats(BaseModel):
119
- vectorCount: int
120
-
121
-
122
- class IndexStats(BaseModel):
123
- dimension: int
124
- namespaces: dict[str, IndexNamespaceStats] = {}
125
- totalVectorCount: int
126
-
127
-
128
- class IndexStatus(BaseModel):
129
- ready: bool
130
- state: str
131
-
132
-
133
- class IndexDescription(BaseModel):
134
- dimension: int
135
- host: str
136
- metric: str
137
- name: str
138
- spec: dict[str, Any]
139
- status: IndexStatus