nucliadb-utils 6.9.0.post5076__py3-none-any.whl → 6.9.2.post5282__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 +1 -1
- nucliadb_utils/featureflagging.py +1 -4
- nucliadb_utils/tests/fixtures.py +4 -2
- nucliadb_utils/tests/nats.py +13 -2
- nucliadb_utils/utilities.py +5 -15
- {nucliadb_utils-6.9.0.post5076.dist-info → nucliadb_utils-6.9.2.post5282.dist-info}/METADATA +4 -5
- {nucliadb_utils-6.9.0.post5076.dist-info → nucliadb_utils-6.9.2.post5282.dist-info}/RECORD +9 -13
- nucliadb_utils/aiopynecone/__init__.py +0 -19
- nucliadb_utils/aiopynecone/client.py +0 -636
- nucliadb_utils/aiopynecone/exceptions.py +0 -131
- nucliadb_utils/aiopynecone/models.py +0 -139
- {nucliadb_utils-6.9.0.post5076.dist-info → nucliadb_utils-6.9.2.post5282.dist-info}/WHEEL +0 -0
- {nucliadb_utils-6.9.0.post5076.dist-info → nucliadb_utils-6.9.2.post5282.dist-info}/top_level.txt +0 -0
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
|
-
|
|
44
|
+
REBALANCE_ENABLED = "nucliadb_rebalance"
|
|
@@ -45,10 +45,7 @@ DEFAULT_FLAG_DATA: dict[str, Any] = {
|
|
|
45
45
|
"rollout": 0,
|
|
46
46
|
"variants": {"environment": ["local"]},
|
|
47
47
|
},
|
|
48
|
-
const.Features.
|
|
49
|
-
"rollout": 0,
|
|
50
|
-
"variants": {"environment": ["local"]},
|
|
51
|
-
},
|
|
48
|
+
const.Features.REBALANCE_ENABLED: {"rollout": 0, "variants": {"environment": ["local"]}},
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
|
nucliadb_utils/tests/fixtures.py
CHANGED
|
@@ -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
|
-
|
|
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
|
"""
|
nucliadb_utils/tests/nats.py
CHANGED
|
@@ -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()
|
|
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()
|
nucliadb_utils/utilities.py
CHANGED
|
@@ -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)
|
{nucliadb_utils-6.9.0.post5076.dist-info → nucliadb_utils-6.9.2.post5282.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nucliadb_utils
|
|
3
|
-
Version: 6.9.
|
|
3
|
+
Version: 6.9.2.post5282
|
|
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.
|
|
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,8 @@ 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.
|
|
31
|
-
Requires-Dist: nucliadb-telemetry>=6.9.
|
|
29
|
+
Requires-Dist: nucliadb-protos>=6.9.2.post5282
|
|
30
|
+
Requires-Dist: nucliadb-telemetry>=6.9.2.post5282
|
|
32
31
|
Provides-Extra: cache
|
|
33
32
|
Requires-Dist: redis>=4.3.4; extra == "cache"
|
|
34
33
|
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=
|
|
4
|
+
nucliadb_utils/const.py,sha256=YtWadXGm044MbwLcfEImNC6skh3e4LKZDu6hjlO0qMU,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=
|
|
7
|
+
nucliadb_utils/featureflagging.py,sha256=ctd9Nqm_nhoedMIV2GC819-cSP5GlkLYXCRE0DbwxYU,2353
|
|
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=
|
|
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
|
|
@@ -52,12 +48,12 @@ nucliadb_utils/storages/utils.py,sha256=F4Iboa_0_bhDQr-JOKD9sGPld_-hKwJW5ptyZdn9
|
|
|
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
|
|
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=
|
|
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.
|
|
61
|
-
nucliadb_utils-6.9.
|
|
62
|
-
nucliadb_utils-6.9.
|
|
63
|
-
nucliadb_utils-6.9.
|
|
56
|
+
nucliadb_utils-6.9.2.post5282.dist-info/METADATA,sha256=q0qd9MBC9AOpmBgjqMw2xwPB5z6Fy2P_sIEqH8wfRWk,2131
|
|
57
|
+
nucliadb_utils-6.9.2.post5282.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
58
|
+
nucliadb_utils-6.9.2.post5282.dist-info/top_level.txt,sha256=fE3vJtALTfgh7bcAWcNhcfXkNPp_eVVpbKK-2IYua3E,15
|
|
59
|
+
nucliadb_utils-6.9.2.post5282.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
|
|
File without changes
|
{nucliadb_utils-6.9.0.post5076.dist-info → nucliadb_utils-6.9.2.post5282.dist-info}/top_level.txt
RENAMED
|
File without changes
|