diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 6f19ea3..a09a4db 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -9,7 +9,7 @@ on: description: "From which folder this pipeline executes" env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.1" jobs: build: @@ -37,7 +37,7 @@ jobs: - name: Install dependencies shell: bash - run: poetry install + run: poetry install --extras gcs - name: Run core tests shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cb55d2..2a27b01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ concurrency: cancel-in-progress: true env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.1" WORKDIR: "." jobs: @@ -89,7 +89,7 @@ jobs: shell: bash run: | echo "Running tests, installing dependencies with poetry..." - poetry install --with test,lint,typing,docs + poetry install --with test,lint,typing,docs --extras gcs - name: Run tests run: make test env: diff --git a/langchain_postgres/v2/async_vectorstore.py b/langchain_postgres/v2/async_vectorstore.py index 11e5ff9..9ee5ef0 100644 --- a/langchain_postgres/v2/async_vectorstore.py +++ b/langchain_postgres/v2/async_vectorstore.py @@ -1,12 +1,15 @@ # TODO: Remove below import when minimum supported Python version is 3.10 from __future__ import annotations +import base64 import copy import json import uuid from typing import Any, Callable, Iterable, Optional, Sequence +from urllib.parse import urlparse import numpy as np +import requests from langchain_core.documents import Document from langchain_core.embeddings import Embeddings from langchain_core.vectorstores import VectorStore, utils @@ -365,6 +368,98 @@ async def aadd_documents( ids = await self.aadd_texts(texts, metadatas=metadatas, ids=ids, **kwargs) return ids + def _encode_image(self, uri: str) -> str: + """Get base64 string from a image URI.""" + if uri.startswith("gs://"): + from google.cloud import storage # type: ignore + + path_without_prefix = uri[len("gs://") :] + parts = path_without_prefix.split("/", 1) + bucket_name = parts[0] + object_name = "" # Default for bucket root if no object specified + if len(parts) == 2: + object_name = parts[1] + storage_client = storage.Client() + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(object_name) + return base64.b64encode(blob.download_as_bytes()).decode("utf-8") + + parsed_uri = urlparse(uri) + if parsed_uri.scheme in ["http", "https"]: + response = requests.get(uri, stream=True) + response.raise_for_status() + return base64.b64encode(response.content).decode("utf-8") + + with open(uri, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") + + async def aadd_images( + self, + uris: list[str], + metadatas: Optional[list[dict]] = None, + ids: Optional[list[str]] = None, + **kwargs: Any, + ) -> list[str]: + """Embed images and add to the table. + + Args: + uris (list[str]): List of local image URIs to add to the table. + metadatas (Optional[list[dict]]): List of metadatas to add to table records. + ids: (Optional[list[str]]): List of IDs to add to table records. + + Returns: + List of record IDs added. + """ + encoded_images = [] + if metadatas is None: + metadatas = [{"image_uri": uri} for uri in uris] + + for uri in uris: + encoded_image = self._encode_image(uri) + encoded_images.append(encoded_image) + + embeddings = self._images_embedding_helper(uris) + ids = await self.aadd_embeddings( + encoded_images, embeddings, metadatas=metadatas, ids=ids, **kwargs + ) + return ids + + def _images_embedding_helper(self, image_uris: list[str]) -> list[list[float]]: + # check if either `embed_images()` or `embed_image()` API is supported by the embedding service used + if hasattr(self.embedding_service, "embed_images"): + try: + embeddings = self.embedding_service.embed_images(image_uris) + except Exception as e: + raise Exception( + f"Make sure your selected embedding model supports list of image URIs as input. {str(e)}" + ) + elif hasattr(self.embedding_service, "embed_image"): + try: + embeddings = self.embedding_service.embed_image(image_uris) + except Exception as e: + raise Exception( + f"Make sure your selected embedding model supports list of image URIs as input. {str(e)}" + ) + else: + raise ValueError( + "Please use an embedding model that supports image embedding." + ) + return embeddings + + async def asimilarity_search_image( + self, + image_uri: str, + k: Optional[int] = None, + filter: Optional[dict] = None, + **kwargs: Any, + ) -> list[Document]: + """Return docs selected by similarity search on query.""" + embedding = self._images_embedding_helper([image_uri])[0] + + return await self.asimilarity_search_by_vector( + embedding=embedding, k=k, filter=filter, **kwargs + ) + async def adelete( self, ids: Optional[list] = None, @@ -1268,3 +1363,25 @@ def max_marginal_relevance_search_with_score_by_vector( raise NotImplementedError( "Sync methods are not implemented for AsyncPGVectorStore. Use PGVectorStore interface instead." ) + + def add_images( + self, + uris: list[str], + metadatas: Optional[list[dict]] = None, + ids: Optional[list[str]] = None, + **kwargs: Any, + ) -> list[str]: + raise NotImplementedError( + "Sync methods are not implemented for AsyncAlloyDBVectorStore. Use AlloyDBVectorStore interface instead." + ) + + def similarity_search_image( + self, + image_uri: str, + k: Optional[int] = None, + filter: Optional[dict] = None, + **kwargs: Any, + ) -> list[Document]: + raise NotImplementedError( + "Sync methods are not implemented for AsyncAlloyDBVectorStore. Use AlloyDBVectorStore interface instead." + ) diff --git a/langchain_postgres/v2/vectorstores.py b/langchain_postgres/v2/vectorstores.py index 1dc1be9..a85f9eb 100644 --- a/langchain_postgres/v2/vectorstores.py +++ b/langchain_postgres/v2/vectorstores.py @@ -840,3 +840,51 @@ def get_by_ids(self, ids: Sequence[str]) -> list[Document]: def get_table_name(self) -> str: return self.__vs.table_name + + async def aadd_images( + self, + uris: list[str], + metadatas: Optional[list[dict]] = None, + ids: Optional[list[str]] = None, + **kwargs: Any, + ) -> list[str]: + """Embed images and add to the table.""" + return await self._engine._run_as_async( + self.__vs.aadd_images(uris, metadatas, ids, **kwargs) # type: ignore + ) + + def add_images( + self, + uris: list[str], + metadatas: Optional[list[dict]] = None, + ids: Optional[list[str]] = None, + **kwargs: Any, + ) -> list[str]: + """Embed images and add to the table.""" + return self._engine._run_as_sync( + self.__vs.aadd_images(uris, metadatas, ids, **kwargs) # type: ignore + ) + + def similarity_search_image( + self, + image_uri: str, + k: Optional[int] = None, + filter: Optional[dict] = None, + **kwargs: Any, + ) -> list[Document]: + """Return docs selected by similarity search on image.""" + return self._engine._run_as_sync( + self.__vs.asimilarity_search_image(image_uri, k, filter, **kwargs) # type: ignore + ) + + async def asimilarity_search_image( + self, + image_uri: str, + k: Optional[int] = None, + filter: Optional[dict] = None, + **kwargs: Any, + ) -> list[Document]: + """Return docs selected by similarity search on image_uri.""" + return await self._engine._run_as_async( + self.__vs.asimilarity_search_image(image_uri, k, filter, **kwargs) # type: ignore + ) diff --git a/poetry.lock b/poetry.lock index 9860aa4..e13000a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -331,6 +331,19 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.5)"] +[[package]] +name = "cachetools" +version = "5.5.2" +description = "Extensible memoizing collections and decorators" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, + {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -784,6 +797,195 @@ files = [ {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, ] +[[package]] +name = "google-api-core" +version = "2.24.2" +description = "Google API client core library" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, + {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, +] + +[package.dependencies] +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" +proto-plus = [ + {version = ">=1.22.3,<2.0.0"}, + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, +] +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" + +[package.extras] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] + +[[package]] +name = "google-auth" +version = "2.39.0" +description = "Google Authentication Library" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2"}, + {file = "google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] + +[[package]] +name = "google-cloud-core" +version = "2.4.3" +description = "Google Cloud API client core library" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e"}, + {file = "google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53"}, +] + +[package.dependencies] +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" +google-auth = ">=1.25.0,<3.0dev" + +[package.extras] +grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] + +[[package]] +name = "google-cloud-storage" +version = "3.1.0" +description = "Google Cloud Storage API client library" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "google_cloud_storage-3.1.0-py2.py3-none-any.whl", hash = "sha256:eaf36966b68660a9633f03b067e4a10ce09f1377cae3ff9f2c699f69a81c66c6"}, + {file = "google_cloud_storage-3.1.0.tar.gz", hash = "sha256:944273179897c7c8a07ee15f2e6466a02da0c7c4b9ecceac2a26017cb2972049"}, +] + +[package.dependencies] +google-api-core = ">=2.15.0,<3.0.0dev" +google-auth = ">=2.26.1,<3.0dev" +google-cloud-core = ">=2.4.2,<3.0dev" +google-crc32c = ">=1.0,<2.0dev" +google-resumable-media = ">=2.7.2" +requests = ">=2.18.0,<3.0.0dev" + +[package.extras] +protobuf = ["protobuf (<6.0.0dev)"] +tracing = ["opentelemetry-api (>=1.1.0)"] + +[[package]] +name = "google-crc32c" +version = "1.7.1" +description = "A python wrapper of the C library 'Google CRC32C'" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76"}, + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603"}, + {file = "google_crc32c-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53"}, + {file = "google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65"}, + {file = "google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582"}, + {file = "google_crc32c-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82"}, + {file = "google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472"}, +] + +[package.extras] +testing = ["pytest"] + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +description = "Utilities for Google Media Downloads and Resumable Uploads" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa"}, + {file = "google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0"}, +] + +[package.dependencies] +google-crc32c = ">=1.0,<2.0dev" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] +requests = ["requests (>=2.18.0,<3.0.0dev)"] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +description = "Common protobufs used in Google APIs" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, + {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, +] + +[package.dependencies] +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] + [[package]] name = "greenlet" version = "3.2.1" @@ -1005,7 +1207,6 @@ description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version < \"3.12\"" files = [ {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, @@ -1082,22 +1283,6 @@ files = [ {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, ] -[[package]] -name = "ipython-pygments-lexers" -version = "1.1.1" -description = "Defines a variety of Pygments lexers for highlighting IPython code." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version >= \"3.12\"" -files = [ - {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, - {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, -] - -[package.dependencies] -pygments = "*" - [[package]] name = "isoduration" version = "20.11.0" @@ -2169,7 +2354,7 @@ description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" groups = ["dev"] -markers = "python_version < \"3.12\" and sys_platform != \"win32\" or sys_platform != \"win32\" and sys_platform != \"emscripten\"" +markers = "sys_platform != \"win32\"" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, @@ -2193,6 +2378,95 @@ files = [ [package.dependencies] numpy = "*" +[[package]] +name = "pillow" +version = "11.1.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +groups = ["test"] +files = [ + {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, + {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, + {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, + {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, + {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, + {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, + {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, + {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, + {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, + {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, + {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, + {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, + {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, + {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, + {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, + {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, + {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, + {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, + {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, + {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, + {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions ; python_version < \"3.10\""] +xmp = ["defusedxml"] + [[package]] name = "platformdirs" version = "4.3.7" @@ -2256,6 +2530,45 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "proto-plus" +version = "1.26.1" +description = "Beautiful, Pythonic protocol buffers" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<7.0.0" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "6.30.2" +description = "" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103"}, + {file = "protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9"}, + {file = "protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b"}, + {file = "protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815"}, + {file = "protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d"}, + {file = "protobuf-6.30.2-cp39-cp39-win32.whl", hash = "sha256:524afedc03b31b15586ca7f64d877a98b184f007180ce25183d1a5cb230ee72b"}, + {file = "protobuf-6.30.2-cp39-cp39-win_amd64.whl", hash = "sha256:acec579c39c88bd8fbbacab1b8052c793efe83a0a5bd99db4a31423a25c0a0e2"}, + {file = "protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51"}, + {file = "protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048"}, +] + [[package]] name = "psutil" version = "7.0.0" @@ -2326,7 +2639,7 @@ description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" groups = ["dev"] -markers = "sys_platform != \"win32\" and python_version < \"3.12\" or sys_platform != \"win32\" and sys_platform != \"emscripten\" or os_name != \"nt\"" +markers = "sys_platform != \"win32\" or os_name != \"nt\"" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -2347,6 +2660,35 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +description = "A collection of ASN.1-based protocols modules" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, +] + +[package.dependencies] +pyasn1 = ">=0.6.1,<0.7.0" + [[package]] name = "pycparser" version = "2.22" @@ -3052,6 +3394,22 @@ files = [ {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, ] +[[package]] +name = "rsa" +version = "4.9.1" +description = "Pure-Python RSA implementation" +optional = true +python-versions = "<4,>=3.6" +groups = ["main"] +markers = "extra == \"gcs\"" +files = [ + {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, + {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "ruff" version = "0.1.15" @@ -3412,6 +3770,21 @@ files = [ {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, ] +[[package]] +name = "types-requests" +version = "2.32.0.20250328" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2"}, + {file = "types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32"}, +] + +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" version = "4.13.2" @@ -3776,6 +4149,9 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ [package.extras] cffi = ["cffi (>=1.11)"] +[extras] +gcs = ["google-cloud-storage"] + [metadata] lock-version = "2.1" python-versions = "^3.9" diff --git a/pyproject.toml b/pyproject.toml index 465f3d7..7340eb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,11 @@ sqlalchemy = "^2" pgvector = ">=0.2.5,<0.4" numpy = ">=1.21,<3" asyncpg = "^0.30.0" +types-requests = "^2.0.0" +google-cloud-storage = { version = ">=2.18.2, <4.0.0", optional = true } + +[tool.poetry.extras] +gcs = ["google-cloud-storage"] [tool.poetry.group.docs.dependencies] @@ -37,6 +42,7 @@ pytest-socket = "^0.7.0" pytest-cov = "^5.0.0" pytest-timeout = "^2.3.1" langchain-tests = "0.3.7" +pillow = "11.1.0" [tool.poetry.group.codespell] optional = true diff --git a/tests/unit_tests/v2/test_async_pg_vectorstore.py b/tests/unit_tests/v2/test_async_pg_vectorstore.py index acad83f..40a93a4 100644 --- a/tests/unit_tests/v2/test_async_pg_vectorstore.py +++ b/tests/unit_tests/v2/test_async_pg_vectorstore.py @@ -1,3 +1,4 @@ +import os import uuid from typing import AsyncIterator, Sequence @@ -5,6 +6,7 @@ import pytest_asyncio from langchain_core.documents import Document from langchain_core.embeddings import DeterministicFakeEmbedding +from PIL import Image from sqlalchemy import text from sqlalchemy.engine.row import RowMapping @@ -15,6 +17,7 @@ DEFAULT_TABLE = "default" + str(uuid.uuid4()) DEFAULT_TABLE_SYNC = "default_sync" + str(uuid.uuid4()) CUSTOM_TABLE = "custom" + str(uuid.uuid4()) +IMAGE_TABLE = "image_table" + str(uuid.uuid4()) VECTOR_SIZE = 768 embeddings_service = DeterministicFakeEmbedding(size=VECTOR_SIZE) @@ -28,6 +31,14 @@ embeddings = [embeddings_service.embed_query(texts[i]) for i in range(len(texts))] +class FakeImageEmbedding(DeterministicFakeEmbedding): + def embed_image(self, image_paths: list[str]) -> list[list[float]]: + return [self.embed_query(path) for path in image_paths] + + +image_embedding_service = FakeImageEmbedding(size=VECTOR_SIZE) + + async def aexecute(engine: PGEngine, query: str) -> None: async with engine._pool.connect() as conn: await conn.execute(text(query)) @@ -52,6 +63,7 @@ async def engine(self) -> AsyncIterator[PGEngine]: yield engine await engine.adrop_table(DEFAULT_TABLE) await engine.adrop_table(CUSTOM_TABLE) + await engine.adrop_table(IMAGE_TABLE) await engine.close() @pytest_asyncio.fixture(scope="class") @@ -87,6 +99,44 @@ async def vs_custom(self, engine: PGEngine) -> AsyncIterator[AsyncPGVectorStore] ) yield vs + @pytest_asyncio.fixture(scope="class") + async def image_vs(self, engine: PGEngine) -> AsyncIterator[AsyncPGVectorStore]: + await engine._ainit_vectorstore_table( + IMAGE_TABLE, + VECTOR_SIZE, + metadata_columns=[ + Column("image_id", "TEXT"), + Column("source", "TEXT"), + ], + ) + vs = await AsyncPGVectorStore.create( + engine, + embedding_service=image_embedding_service, + table_name=IMAGE_TABLE, + metadata_columns=["image_id", "source"], + metadata_json_column="mymeta", + ) + yield vs + + @pytest_asyncio.fixture(scope="class") + async def image_uris(self) -> AsyncIterator[list[str]]: + red_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_red.jpg" + green_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_green.jpg" + blue_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_blue.jpg" + image = Image.new("RGB", (100, 100), color="red") + image.save(red_uri) + image = Image.new("RGB", (100, 100), color="green") + image.save(green_uri) + image = Image.new("RGB", (100, 100), color="blue") + image.save(blue_uri) + image_uris = [red_uri, green_uri, blue_uri] + yield image_uris + for uri in image_uris: + try: + os.remove(uri) + except FileNotFoundError: + pass + async def test_init_with_constructor(self, engine: PGEngine) -> None: with pytest.raises(Exception): AsyncPGVectorStore( @@ -165,6 +215,23 @@ async def test_adelete(self, engine: PGEngine, vs: AsyncPGVectorStore) -> None: assert result == False await aexecute(engine, f'TRUNCATE TABLE "{DEFAULT_TABLE}"') + async def test_aadd_images( + self, + engine: PGEngine, + image_vs: AsyncPGVectorStore, + image_uris: list[str], + ) -> None: + ids = [str(uuid.uuid4()) for i in range(len(image_uris))] + metadatas = [ + {"image_id": str(i), "source": "postgres"} for i in range(len(image_uris)) + ] + await image_vs.aadd_images(image_uris, metadatas, ids) + results = await afetch(engine, (f'SELECT * FROM "{IMAGE_TABLE}"')) + assert len(results) == len(image_uris) + assert results[0]["image_id"] == "0" + assert results[0]["source"] == "postgres" + await aexecute(engine, (f'TRUNCATE TABLE "{IMAGE_TABLE}"')) + ##### Custom Vector Store ##### async def test_aadd_embeddings( self, engine: PGEngine, vs_custom: AsyncPGVectorStore diff --git a/tests/unit_tests/v2/test_async_pg_vectorstore_search.py b/tests/unit_tests/v2/test_async_pg_vectorstore_search.py index 72f91d8..5e4d626 100644 --- a/tests/unit_tests/v2/test_async_pg_vectorstore_search.py +++ b/tests/unit_tests/v2/test_async_pg_vectorstore_search.py @@ -6,6 +6,7 @@ import pytest_asyncio from langchain_core.documents import Document from langchain_core.embeddings import DeterministicFakeEmbedding +from PIL import Image from sqlalchemy import text from langchain_postgres import Column, PGEngine @@ -20,6 +21,7 @@ DEFAULT_TABLE = "default" + str(uuid.uuid4()).replace("-", "_") CUSTOM_TABLE = "custom" + str(uuid.uuid4()).replace("-", "_") CUSTOM_FILTER_TABLE = "custom_filter" + str(uuid.uuid4()).replace("-", "_") +IMAGE_TABLE = "image_table" + str(uuid.uuid4()).replace("-", "_") VECTOR_SIZE = 768 sync_method_exception_str = "Sync methods are not implemented for AsyncPGVectorStore. Use PGVectorStore interface instead." @@ -43,6 +45,14 @@ ] +class FakeImageEmbedding(DeterministicFakeEmbedding): + def embed_image(self, image_paths: list[str]) -> list[list[float]]: + return [self.embed_query(path) for path in image_paths] + + +image_embedding_service = FakeImageEmbedding(size=VECTOR_SIZE) + + def get_env_var(key: str, desc: str) -> str: v = os.environ.get(key) if v is None: @@ -69,6 +79,7 @@ async def engine(self) -> AsyncIterator[PGEngine]: await engine.adrop_table(DEFAULT_TABLE) await engine.adrop_table(CUSTOM_TABLE) await engine.adrop_table(CUSTOM_FILTER_TABLE) + await engine.adrop_table(IMAGE_TABLE) await engine.close() @pytest_asyncio.fixture(scope="class") @@ -149,6 +160,39 @@ async def vs_custom_filter( await vs_custom_filter.aadd_documents(filter_docs, ids=ids) yield vs_custom_filter + @pytest_asyncio.fixture(scope="class") + async def image_uris(self) -> AsyncIterator[list[str]]: + red_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_red.jpg" + green_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_green.jpg" + blue_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_blue.jpg" + image = Image.new("RGB", (100, 100), color="red") + image.save(red_uri) + image = Image.new("RGB", (100, 100), color="green") + image.save(green_uri) + image = Image.new("RGB", (100, 100), color="blue") + image.save(blue_uri) + image_uris = [red_uri, green_uri, blue_uri] + yield image_uris + for uri in image_uris: + try: + os.remove(uri) + except FileNotFoundError: + pass + + @pytest_asyncio.fixture(scope="class") + async def image_vs( + self, engine: PGEngine, image_uris: list[str] + ) -> AsyncIterator[AsyncPGVectorStore]: + await engine._ainit_vectorstore_table(IMAGE_TABLE, VECTOR_SIZE) + vs = await AsyncPGVectorStore.create( + engine, + embedding_service=image_embedding_service, + table_name=IMAGE_TABLE, + distance_strategy=DistanceStrategy.COSINE_DISTANCE, + ) + await vs.aadd_images(image_uris, ids=ids[:3]) + yield vs + async def test_asimilarity_search_score(self, vs: AsyncPGVectorStore) -> None: results = await vs.asimilarity_search_with_score("foo") assert len(results) == 4 @@ -303,3 +347,19 @@ async def test_vectorstore_with_metadata_filters( "meow", k=5, filter=test_filter ) assert [doc.metadata["code"] for doc in docs] == expected_ids, test_filter + + async def test_asimilarity_search_image( + self, image_vs: AsyncPGVectorStore, image_uris: list[str] + ) -> None: + results = await image_vs.asimilarity_search_image(image_uris[0], k=1) + assert len(results) == 1 + assert results[0].metadata["image_uri"] == image_uris[0] + results = await image_vs.asimilarity_search_image(image_uris[2], k=1) + assert len(results) == 1 + assert results[0].metadata["image_uri"] == image_uris[2] + + async def test_similarity_search_image( + self, image_vs: AsyncPGVectorStore, image_uris: list[str] + ) -> None: + with pytest.raises(NotImplementedError): + image_vs.similarity_search_image(image_uris[0], k=1) diff --git a/tests/unit_tests/v2/test_pg_vectorstore.py b/tests/unit_tests/v2/test_pg_vectorstore.py index d9765fa..ac85a56 100644 --- a/tests/unit_tests/v2/test_pg_vectorstore.py +++ b/tests/unit_tests/v2/test_pg_vectorstore.py @@ -8,6 +8,7 @@ import pytest_asyncio from langchain_core.documents import Document from langchain_core.embeddings import DeterministicFakeEmbedding +from PIL import Image from sqlalchemy import text from sqlalchemy.engine.row import RowMapping from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine @@ -18,6 +19,8 @@ DEFAULT_TABLE = "test_table" + str(uuid.uuid4()) DEFAULT_TABLE_SYNC = "test_table_sync" + str(uuid.uuid4()) CUSTOM_TABLE = "test-table-custom" + str(uuid.uuid4()) +IMAGE_TABLE = "image_table" + str(uuid.uuid4()) +IMAGE_TABLE_SYNC = "image_table_sync" + str(uuid.uuid4()) VECTOR_SIZE = 768 embeddings_service = DeterministicFakeEmbedding(size=VECTOR_SIZE) @@ -31,6 +34,14 @@ embeddings = [embeddings_service.embed_query(texts[i]) for i in range(len(texts))] +class FakeImageEmbedding(DeterministicFakeEmbedding): + def embed_image(self, image_paths: list[str]) -> list[list[float]]: + return [self.embed_query(path) for path in image_paths] + + +image_embedding_service = FakeImageEmbedding(size=VECTOR_SIZE) + + def get_env_var(key: str, desc: str) -> str: v = os.environ.get(key) if v is None: @@ -70,6 +81,8 @@ async def engine(self) -> AsyncIterator[PGEngine]: yield engine await aexecute(engine, f'DROP TABLE IF EXISTS "{DEFAULT_TABLE}"') + await aexecute(engine, f'DROP TABLE IF EXISTS "{IMAGE_TABLE}"') + await aexecute(engine, f'DROP TABLE IF EXISTS "{CUSTOM_TABLE}"') await engine.close() @pytest_asyncio.fixture(scope="class") @@ -88,6 +101,7 @@ async def engine_sync(self) -> AsyncIterator[PGEngine]: yield engine_sync await aexecute(engine_sync, f'DROP TABLE IF EXISTS "{DEFAULT_TABLE_SYNC}"') + await aexecute(engine_sync, f'DROP TABLE IF EXISTS "{IMAGE_TABLE_SYNC}"') await engine_sync.close() @pytest_asyncio.fixture(scope="class") @@ -125,6 +139,25 @@ async def vs_custom(self, engine: PGEngine) -> AsyncIterator[PGVectorStore]: yield vs await aexecute(engine, f'DROP TABLE IF EXISTS "{CUSTOM_TABLE}"') + @pytest_asyncio.fixture(scope="class") + def image_uris(self) -> Iterator[list[str]]: + red_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_red.jpg" + green_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_green.jpg" + blue_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_blue.jpg" + image = Image.new("RGB", (100, 100), color="red") + image.save(red_uri) + image = Image.new("RGB", (100, 100), color="green") + image.save(green_uri) + image = Image.new("RGB", (100, 100), color="blue") + image.save(blue_uri) + image_uris = [red_uri, green_uri, blue_uri] + yield image_uris + for uri in image_uris: + try: + os.remove(uri) + except FileNotFoundError: + pass + async def test_init_with_constructor(self, engine: PGEngine) -> None: with pytest.raises(Exception): PGVectorStore( # type: ignore @@ -190,6 +223,52 @@ async def test_aadd_docs(self, engine: PGEngine, vs: PGVectorStore) -> None: assert len(results) == 3 await aexecute(engine, f'TRUNCATE TABLE "{DEFAULT_TABLE}"') + async def test_aadd_images( + self, engine_sync: PGEngine, image_uris: list[str] + ) -> None: + engine_sync.init_vectorstore_table( + IMAGE_TABLE, + VECTOR_SIZE, + metadata_columns=[ + Column("image_id", "TEXT"), + Column("source", "TEXT"), + ], + ) + vs = PGVectorStore.create_sync( + engine_sync, + embedding_service=image_embedding_service, + table_name=IMAGE_TABLE, + metadata_columns=["image_id", "source"], + metadata_json_column="mymeta", + ) + ids = [str(uuid.uuid4()) for i in range(len(image_uris))] + metadatas = [ + {"image_id": str(i), "source": "postgres"} for i in range(len(image_uris)) + ] + await vs.aadd_images(image_uris, metadatas, ids) + results = await afetch(engine_sync, f'SELECT * FROM "{IMAGE_TABLE}"') + assert len(results) == len(image_uris) + assert results[0]["image_id"] == "0" + assert results[0]["source"] == "postgres" + await aexecute(engine_sync, f'TRUNCATE TABLE "{IMAGE_TABLE}"') + + async def test_add_images( + self, engine_sync: PGEngine, image_uris: list[str] + ) -> None: + engine_sync.init_vectorstore_table(IMAGE_TABLE_SYNC, VECTOR_SIZE) + vs = PGVectorStore.create_sync( + engine_sync, + embedding_service=image_embedding_service, + table_name=IMAGE_TABLE_SYNC, + ) + + ids = [str(uuid.uuid4()) for i in range(len(image_uris))] + vs.add_images(image_uris, ids=ids) + results = await afetch(engine_sync, (f'SELECT * FROM "{IMAGE_TABLE_SYNC}"')) + assert len(results) == len(image_uris) + await vs.adelete(ids) + await aexecute(engine_sync, f'DROP TABLE IF EXISTS "{IMAGE_TABLE_SYNC}"') + async def test_aadd_embeddings( self, engine: PGEngine, vs_custom: PGVectorStore ) -> None: diff --git a/tests/unit_tests/v2/test_pg_vectorstore_search.py b/tests/unit_tests/v2/test_pg_vectorstore_search.py index 379f529..1216639 100644 --- a/tests/unit_tests/v2/test_pg_vectorstore_search.py +++ b/tests/unit_tests/v2/test_pg_vectorstore_search.py @@ -1,11 +1,12 @@ import os import uuid -from typing import AsyncIterator +from typing import AsyncIterator, Iterator import pytest import pytest_asyncio from langchain_core.documents import Document from langchain_core.embeddings import DeterministicFakeEmbedding +from PIL import Image from sqlalchemy import text from langchain_postgres import Column, PGEngine, PGVectorStore @@ -22,6 +23,8 @@ CUSTOM_TABLE = "custom" + str(uuid.uuid4()).replace("-", "_") CUSTOM_FILTER_TABLE = "custom_filter" + str(uuid.uuid4()).replace("-", "_") CUSTOM_FILTER_TABLE_SYNC = "custom_filter_sync" + str(uuid.uuid4()).replace("-", "_") +IMAGE_TABLE = "image_table" + str(uuid.uuid4()).replace("-", "_") +IMAGE_TABLE_SYNC = "image_table_sync" + str(uuid.uuid4()).replace("-", "_") VECTOR_SIZE = 768 embeddings_service = DeterministicFakeEmbedding(size=VECTOR_SIZE) @@ -43,6 +46,14 @@ embeddings = [embeddings_service.embed_query("foo") for i in range(len(texts))] +class FakeImageEmbedding(DeterministicFakeEmbedding): + def embed_image(self, image_paths: list[str]) -> list[list[float]]: + return [self.embed_query(path) for path in image_paths] + + +image_embedding_service = FakeImageEmbedding(size=VECTOR_SIZE) + + def get_env_var(key: str, desc: str) -> str: v = os.environ.get(key) if v is None: @@ -71,6 +82,7 @@ async def engine(self) -> AsyncIterator[PGEngine]: yield engine await aexecute(engine, f"DROP TABLE IF EXISTS {DEFAULT_TABLE}") await aexecute(engine, f"DROP TABLE IF EXISTS {CUSTOM_FILTER_TABLE}") + await aexecute(engine, f"DROP TABLE IF EXISTS {IMAGE_TABLE}") await engine.close() @pytest_asyncio.fixture(scope="class") @@ -158,6 +170,40 @@ async def vs_custom_filter(self, engine: PGEngine) -> AsyncIterator[PGVectorStor await vs_custom_filter.aadd_documents(filter_docs, ids=ids) yield vs_custom_filter + @pytest_asyncio.fixture(scope="class") + async def image_uris(self) -> AsyncIterator[list[str]]: + red_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_red.jpg" + green_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_green.jpg" + blue_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_blue.jpg" + image = Image.new("RGB", (100, 100), color="red") + image.save(red_uri) + image = Image.new("RGB", (100, 100), color="green") + image.save(green_uri) + image = Image.new("RGB", (100, 100), color="blue") + image.save(blue_uri) + image_uris = [red_uri, green_uri, blue_uri] + yield image_uris + for uri in image_uris: + try: + os.remove(uri) + except FileNotFoundError: + pass + + @pytest_asyncio.fixture(scope="class") + async def image_vs( + self, engine: PGEngine, image_uris: list[str] + ) -> AsyncIterator[PGVectorStore]: + await engine.ainit_vectorstore_table(IMAGE_TABLE, VECTOR_SIZE) + vs = await PGVectorStore.create( + engine, + embedding_service=image_embedding_service, + table_name=IMAGE_TABLE, + distance_strategy=DistanceStrategy.COSINE_DISTANCE, + ) + ids = [str(uuid.uuid4()) for i in range(len(image_uris))] + await vs.aadd_images(image_uris, ids=ids) + yield vs + async def test_asimilarity_search_score(self, vs: PGVectorStore) -> None: results = await vs.asimilarity_search_with_score("foo") assert len(results) == 4 @@ -173,6 +219,16 @@ async def test_asimilarity_search_by_vector(self, vs: PGVectorStore) -> None: assert result[0][0] == Document(page_content="foo", id=ids[0]) assert result[0][1] == 0 + async def test_asimilarity_search_image( + self, image_vs: PGVectorStore, image_uris: list[str] + ) -> None: + results = await image_vs.asimilarity_search_image(image_uris[0], k=1) + assert len(results) == 1 + assert results[0].metadata["image_uri"] == image_uris[0] + results = await image_vs.asimilarity_search_image(image_uris[2], k=1) + assert len(results) == 1 + assert results[0].metadata["image_uri"] == image_uris[2] + async def test_similarity_search_with_relevance_scores_threshold_cosine( self, vs: PGVectorStore ) -> None: @@ -270,6 +326,7 @@ async def engine_sync(self) -> AsyncIterator[PGEngine]: yield engine await aexecute(engine, f"DROP TABLE IF EXISTS {DEFAULT_TABLE_SYNC}") await aexecute(engine, f"DROP TABLE IF EXISTS {CUSTOM_FILTER_TABLE_SYNC}") + await aexecute(engine, f"DROP TABLE IF EXISTS {IMAGE_TABLE_SYNC}") await engine.close() @pytest_asyncio.fixture(scope="class") @@ -339,6 +396,37 @@ async def vs_custom_filter_sync( vs_custom_filter_sync.add_documents(filter_docs, ids=ids) yield vs_custom_filter_sync + @pytest_asyncio.fixture(scope="class") + async def image_uris(self) -> AsyncIterator[list[str]]: + red_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_red.jpg" + green_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_green.jpg" + blue_uri = str(uuid.uuid4()).replace("-", "_") + "test_image_blue.jpg" + image = Image.new("RGB", (100, 100), color="red") + image.save(red_uri) + image = Image.new("RGB", (100, 100), color="green") + image.save(green_uri) + image = Image.new("RGB", (100, 100), color="blue") + image.save(blue_uri) + image_uris = [red_uri, green_uri, blue_uri] + yield image_uris + for uri in image_uris: + os.remove(uri) + + @pytest_asyncio.fixture(scope="class") + def image_vs( + self, engine_sync: PGEngine, image_uris: list[str] + ) -> Iterator[PGVectorStore]: + engine_sync.init_vectorstore_table(IMAGE_TABLE_SYNC, VECTOR_SIZE) + vs = PGVectorStore.create_sync( + engine_sync, + embedding_service=image_embedding_service, + table_name=IMAGE_TABLE_SYNC, + distance_strategy=DistanceStrategy.COSINE_DISTANCE, + ) + ids = [str(uuid.uuid4()) for i in range(len(image_uris))] + vs.add_images(image_uris, ids=ids) + yield vs + def test_similarity_search_score(self, vs_custom: PGVectorStore) -> None: results = vs_custom.similarity_search_with_score("foo") assert len(results) == 4 @@ -354,6 +442,13 @@ def test_similarity_search_by_vector(self, vs_custom: PGVectorStore) -> None: assert result[0][0] == Document(page_content="foo", id=ids[0]) assert result[0][1] == 0 + def test_similarity_search_image( + self, image_vs: PGVectorStore, image_uris: list[str] + ) -> None: + results = image_vs.similarity_search_image(image_uris[0], k=1) + assert len(results) == 1 + assert results[0].metadata["image_uri"] == image_uris[0] + def test_max_marginal_relevance_search_vector( self, vs_custom: PGVectorStore ) -> None: