From 703fd5855d9ee7232494d3ea191fe50df4182d57 Mon Sep 17 00:00:00 2001 From: wangsl <48207171+WSL0809@users.noreply.github.com> Date: Sat, 31 Jan 2026 13:58:54 +0800 Subject: [PATCH 01/10] [vibe coding] feat: use _source keyword to select the fields we want (#138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary close #103 ## Solution Description In hybrid_search, users use "include" on the SDK side to control the returned fields. However, "include" only filters data on the SDK side; in reality, the database returns data for all fields, which causes additional overhead. Therefore, "_source" is introduced to control the actual fields returned by the database. The main purpose is to reduce overhead, not to add a new filtering option. For users who are already using "include", efforts should be made to reduce overhead while keeping the change as unnoticeable as possible. The SDK will derive the minimal "_source" based on the "include" parameter to ensure that users are not affected while reducing overhead. ## Summary by CodeRabbit * **New Features** * Added an include parameter to hybrid search so users can request specific fields (documents, metadata, embeddings); results now always include the record id and return only requested fields in a deterministic order. * **Tests** * New unit and integration tests validating include-driven result shapes, SQL generation, and various include combinations. ✏️ Tip: You can customize this high-level summary in your review settings. --- src/pyseekdb/client/client_base.py | 49 +++-- ...llection_hybrid_search_source_inference.py | 193 ++++++++++++++++++ .../test_hybrid_search_source_inference.py | 89 ++++++++ 3 files changed, 315 insertions(+), 16 deletions(-) create mode 100644 tests/integration_tests/test_collection_hybrid_search_source_inference.py create mode 100644 tests/unit_tests/test_hybrid_search_source_inference.py diff --git a/src/pyseekdb/client/client_base.py b/src/pyseekdb/client/client_base.py index bd0aa89d..8b312113 100644 --- a/src/pyseekdb/client/client_base.py +++ b/src/pyseekdb/client/client_base.py @@ -2631,7 +2631,15 @@ def _collection_hybrid_search( table_name = CollectionNames.table_name(collection_name) # Build search_parm JSON - search_parm = self._build_search_parm(query, knn, rank, n_results, dimension=dimension, **kwargs) + search_parm = self._build_search_parm( + query, + knn, + rank, + n_results, + include=include, + dimension=dimension, + **kwargs, + ) # Convert search_parm to JSON string search_parm_json = json.dumps(search_parm, ensure_ascii=False) @@ -2684,6 +2692,7 @@ def _build_search_parm( # noqa: C901 knn: dict[str, Any] | list[dict[str, Any]] | None, rank: dict[str, Any] | None, n_results: int, + include: list[str] | None = None, dimension: int | None = None, **kwargs, ) -> dict[str, Any]: @@ -2695,6 +2704,8 @@ def _build_search_parm( # noqa: C901 knn: Vector search configuration dict or list of dicts rank: Ranking configuration dict n_results: Final number of results to return + include: Fields requested by the SDK caller. Used to infer the minimal OceanBase GET_SQL + `_source` allowlist to avoid returning large unused columns (e.g. `embedding`). dimension: Collection dimension for validating query_embeddings (optional) **kwargs: Additional parameters, including: embedding_function: EmbeddingFunction instance to convert query_texts in knn to embeddings. @@ -2739,6 +2750,9 @@ def _build_search_parm( # noqa: C901 if rank: search_parm["rank"] = rank + # Always infer a minimal `_source` allowlist from include to reduce response payload. + search_parm["_source"] = self._build_source_fields(include) + return search_parm def _build_query_expression(self, query: dict[str, Any]) -> dict[str, Any] | None: @@ -3092,23 +3106,26 @@ def _normalize_vectors(raw_embeddings: Any) -> list[list[float]]: return knn_exprs if len(knn_exprs) > 1 else knn_exprs[0] def _build_source_fields(self, include: list[str] | None) -> list[str]: - """Build _source fields list from include parameter""" - if not include: - return ["document", "metadata", "embedding"] - - source_fields = [] - field_mapping = { - "documents": "document", - "metadatas": "metadata", - "embeddings": "embedding", - } + """ + Infer OceanBase GET_SQL `_source` allowlist from include. + """ + if include is None: + requested = {"documents", "metadatas"} + else: + if not isinstance(include, list) or not all(isinstance(item, str) for item in include): + raise TypeError("include must be a List[str] or None") + requested = {item.lower() for item in include} - for field in include: - mapped = field_mapping.get(field.lower(), field) - if mapped not in source_fields: - source_fields.append(mapped) + source = ["_id"] + + if {"documents", "document"} & requested: + source.append("document") + if {"metadatas", "metadata"} & requested: + source.append("metadata") + if {"embeddings", "embedding"} & requested: + source.append("embedding") - return source_fields if source_fields else ["document", "metadata", "embedding"] + return source def _transform_sql_result( # noqa: C901 self, result_rows: list[dict[str, Any]], include: list[str] | None diff --git a/tests/integration_tests/test_collection_hybrid_search_source_inference.py b/tests/integration_tests/test_collection_hybrid_search_source_inference.py new file mode 100644 index 00000000..bbf2cbfe --- /dev/null +++ b/tests/integration_tests/test_collection_hybrid_search_source_inference.py @@ -0,0 +1,193 @@ +""" +Integration tests for hybrid_search `_source` inference from include against a real database. + +These tests require a running OceanBase/MySQL-compatible endpoint that supports +`DBMS_HYBRID_SEARCH.GET_SQL`. +""" + +import contextlib +import json +import time +import uuid + +from pymysql.converters import escape_string + +from pyseekdb import HNSWConfiguration +from pyseekdb.client.meta_info import CollectionNames + + +class TestCollectionHybridSearchSourceInferenceRealDB: + _QUERY_TIMEOUT_SECONDS = 10.0 + _QUERY_RETRY_INTERVAL_SECONDS = 0.2 + + def _unique_collection_name(self, prefix: str) -> str: + # Keep names short to avoid MySQL/OceanBase identifier length limits after + # internal table-name prefixing (e.g. "c$v1$..."). + return f"{prefix}_{uuid.uuid4().hex[:8]}" + + def _create_test_collection(self, client, collection_name: str, dimension: int = 3): + config = HNSWConfiguration(dimension=dimension, distance="l2") + collection = client.create_collection(name=collection_name, configuration=config, embedding_function=None) + return collection, collection.dimension + + def _generate_query_vector(self, dimension: int) -> list[float]: + base = [1.0, 2.0, 3.0] + if dimension <= len(base): + return base[:dimension] + extended = base * ((dimension // len(base)) + 1) + return extended[:dimension] + + def _insert_test_data(self, collection, dimension: int): + test_data = [ + ( + "Machine learning is a subset of artificial intelligence", + [1.0, 2.0, 3.0], + {"category": "AI", "tag": "ml"}, + ), + ( + "Python programming language is widely used in data science", + [2.0, 3.0, 4.0], + {"category": "Programming", "tag": "python"}, + ), + ] + + ids: list[str] = [] + documents: list[str] = [] + embeddings: list[list[float]] = [] + metadatas: list[dict] = [] + + for document, base_vec, metadata in test_data: + record_id = str(uuid.uuid4()) + ids.append(record_id) + documents.append(document) + metadatas.append(metadata) + + if dimension <= len(base_vec): + embedding = base_vec[:dimension] + else: + embedding = (base_vec * ((dimension // len(base_vec)) + 1))[:dimension] + + embeddings.append(embedding) + + collection.add(ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas) + return ids + + def _get_sql_query(self, client, table_name: str, search_parm: dict) -> str: + search_parm_json = json.dumps(search_parm, ensure_ascii=False) + client._server._execute(f"SET @search_parm = '{escape_string(search_parm_json)}'") + get_sql_query = f"SELECT DBMS_HYBRID_SEARCH.GET_SQL('{table_name}', @search_parm) as query_sql FROM dual" # noqa: S608 + rows = client._server._execute(get_sql_query) + assert rows and rows[0].get("query_sql") + query_sql = rows[0]["query_sql"] + if isinstance(query_sql, str): + return query_sql.strip().strip("'\"") + return str(query_sql) + + def test_include_infers_source_result_shape_matrix(self, server_client): + """ + Verify `_source` inference end-to-end: + 1) GET_SQL result columns match requested include (avoid returning large unused columns like embedding) + 2) SDK return shape matches include + """ + collection_name = self._unique_collection_name("hs_include_matrix") + collection = None + try: + collection, dimension = self._create_test_collection(server_client, collection_name) + self._insert_test_data(collection, dimension=dimension) + + query_vector = self._generate_query_vector(dimension) + knn = {"query_embeddings": query_vector, "n_results": 2} + table_name = ( + CollectionNames.table_name_v2(collection.id) + if getattr(collection, "id", None) + else CollectionNames.table_name(collection.name) + ) + + def execute_get_sql(include: list[str] | None) -> tuple[dict, list[dict]]: + search_parm = server_client._server._build_search_parm( + query=None, + knn=knn, + rank=None, + n_results=2, + include=include, + dimension=dimension, + ) + query_sql = self._get_sql_query(server_client, table_name, search_parm) + deadline = time.time() + self._QUERY_TIMEOUT_SECONDS + last_exc: Exception | None = None + while time.time() < deadline: + try: + rows = server_client._server._execute(query_sql) + if rows: + return search_parm, rows + except Exception as exc: + last_exc = exc + time.sleep(self._QUERY_RETRY_INTERVAL_SECONDS) + if last_exc is not None: + raise AssertionError("Timed out waiting for GET_SQL query to return rows") from last_exc + raise AssertionError("Timed out waiting for GET_SQL query to return rows") + + def assert_columns(rows: list[dict], *, present: set[str], absent: set[str]) -> None: + assert rows + keys = {str(k).lower() for k in rows[0]} + for col in present: + assert col in keys + for col in absent: + assert col not in keys + + # 1) include=None: default returns documents+metadatas; should not return embedding column + _, rows = execute_get_sql(include=None) + assert_columns(rows, present={"document", "metadata"}, absent={"embedding"}) + + default_include = collection.hybrid_search(knn=knn, n_results=2) + assert set(default_include.keys()) == {"ids", "distances", "documents", "metadatas"} + assert all(isinstance(d, str) for d in default_include["documents"][0]) + assert all(isinstance(m, dict) and m for m in default_include["metadatas"][0]) + + # 2) include=[]: ids/distances only; should not return document/metadata/embedding columns + _, rows = execute_get_sql(include=[]) + assert_columns(rows, present=set(), absent={"document", "metadata", "embedding"}) + + ids_only = collection.hybrid_search(knn=knn, n_results=2, include=[]) + assert set(ids_only.keys()) == {"ids", "distances"} + + # 3) include=["documents"]: only document column + _, rows = execute_get_sql(include=["documents"]) + assert_columns(rows, present={"document"}, absent={"metadata", "embedding"}) + + docs_only = collection.hybrid_search(knn=knn, n_results=2, include=["documents"]) + assert set(docs_only.keys()) == {"ids", "distances", "documents"} + assert all(isinstance(d, str) for d in docs_only["documents"][0]) + + # 4) include=["metadatas"]: only metadata column + _, rows = execute_get_sql(include=["metadatas"]) + assert_columns(rows, present={"metadata"}, absent={"document", "embedding"}) + + metadatas_only = collection.hybrid_search(knn=knn, n_results=2, include=["metadatas"]) + assert set(metadatas_only.keys()) == {"ids", "distances", "metadatas"} + assert all(isinstance(m, dict) and m for m in metadatas_only["metadatas"][0]) + + # 5) include=["embeddings"]: only embedding column + _, rows = execute_get_sql(include=["embeddings"]) + assert_columns(rows, present={"embedding"}, absent={"document", "metadata"}) + + embeddings_only = collection.hybrid_search(knn=knn, n_results=2, include=["embeddings"]) + assert set(embeddings_only.keys()) == {"ids", "distances", "embeddings"} + first_embedding = embeddings_only["embeddings"][0][0] + assert isinstance(first_embedding, list) + assert len(first_embedding) == dimension + + # 6) include=["documents","embeddings"]: document+embedding columns + _, rows = execute_get_sql(include=["documents", "embeddings"]) + assert_columns(rows, present={"document", "embedding"}, absent={"metadata"}) + + docs_and_embeddings = collection.hybrid_search(knn=knn, n_results=2, include=["documents", "embeddings"]) + assert set(docs_and_embeddings.keys()) == {"ids", "distances", "documents", "embeddings"} + assert all(isinstance(d, str) for d in docs_and_embeddings["documents"][0]) + assert all(isinstance(e, list) and len(e) == dimension for e in docs_and_embeddings["embeddings"][0]) + finally: + with contextlib.suppress(Exception): + server_client.delete_collection(name=collection_name) + + # NOTE: `HybridSearch` fluent builder was removed on `develop` (rollback enhanced hybrid search). + # Keep this file focused on verifying OceanBase GET_SQL `_source` inference and result shapes. diff --git a/tests/unit_tests/test_hybrid_search_source_inference.py b/tests/unit_tests/test_hybrid_search_source_inference.py new file mode 100644 index 00000000..1ea019e3 --- /dev/null +++ b/tests/unit_tests/test_hybrid_search_source_inference.py @@ -0,0 +1,89 @@ +""" +Unit tests for hybrid_search `_source` inference from include. + +Public API exposes only `include`. The SDK infers a minimal OceanBase GET_SQL `_source` +allowlist from `include` to reduce response payload size. +""" + +from typing import Any + +from pyseekdb.client.client_base import BaseClient +from pyseekdb.client.collection import Collection + + +class _CapturingClient: + mode = "dummy" + + def __init__(self) -> None: + self.captured: dict[str, Any] | None = None + + def _collection_hybrid_search(self, **kwargs: Any) -> dict[str, Any]: + self.captured = kwargs + return {"captured": kwargs} + + +class _DummyClient: + def _build_source_fields(self, include: list[str] | None) -> list[str]: + return BaseClient._build_source_fields(self, include) + + +class TestHybridSearchPublicSurfaceUnit: + def test_collection_forwards_include_only(self) -> None: + client = _CapturingClient() + collection = Collection(client=client, name="test", dimension=3) + collection.hybrid_search(query={"where_document": {"$contains": "hi"}}, include=["documents"]) + assert client.captured is not None + assert client.captured["include"] == ["documents"] + assert "return_fields" not in client.captured + + +class TestBuildSearchParmSourceInferenceUnit: + def test_build_search_parm_sets_source_from_default_include(self) -> None: + dummy = _DummyClient() + result = BaseClient._build_search_parm( + dummy, + query=None, + knn=None, + rank=None, + n_results=10, + include=None, + ) + assert result["_source"] == ["_id", "document", "metadata"] + + def test_build_search_parm_sets_source_from_empty_include(self) -> None: + dummy = _DummyClient() + result = BaseClient._build_search_parm( + dummy, + query=None, + knn=None, + rank=None, + n_results=10, + include=[], + ) + assert result["_source"] == ["_id"] + + +class TestBuildSourceFieldsUnit: + def test_build_source_fields_defaults_to_documents_and_metadatas(self) -> None: + dummy = _DummyClient() + assert BaseClient._build_source_fields(dummy, include=None) == ["_id", "document", "metadata"] + + def test_build_source_fields_empty_include_is_id_only(self) -> None: + dummy = _DummyClient() + assert BaseClient._build_source_fields(dummy, include=[]) == ["_id"] + + def test_build_source_fields_includes_embedding_only_when_requested(self) -> None: + dummy = _DummyClient() + assert BaseClient._build_source_fields(dummy, include=["embeddings"]) == ["_id", "embedding"] + + def test_build_source_fields_accepts_singular_aliases(self) -> None: + dummy = _DummyClient() + assert BaseClient._build_source_fields(dummy, include=["document"]) == ["_id", "document"] + assert BaseClient._build_source_fields(dummy, include=["metadata"]) == ["_id", "metadata"] + assert BaseClient._build_source_fields(dummy, include=["embedding"]) == ["_id", "embedding"] + + def test_build_source_fields_ignores_non_source_include_items(self) -> None: + dummy = _DummyClient() + assert BaseClient._build_source_fields(dummy, include=["ids"]) == ["_id"] + assert BaseClient._build_source_fields(dummy, include=["distances"]) == ["_id"] + assert BaseClient._build_source_fields(dummy, include=["documents", "ids", "distances"]) == ["_id", "document"] From 35491a265bf574b7f338f0a9c672d4cbb568529b Mon Sep 17 00:00:00 2001 From: chakkk309 Date: Sat, 31 Jan 2026 14:27:43 +0800 Subject: [PATCH 02/10] [vibe coding] feat: add Morph embedding function support (#143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix https://github.com/oceanbase/pyseekdb/issues/136. Support [Morph](https://docs.morphllm.com/api-reference/endpoint/embedding) embedding platform based on OpenAI. ## Solution Description Morph provides an OpenAI-compatible Embedding API and morph-embedding-v4 as the default model (1536 dimensions). Morph test result: ``` .venv/bin/python -m pytest tests/unit_tests/test_morph_embedding_function.py -vv =========================================================================================== test session starts ============================================================================================ platform darwin -- Python 3.11.13, pytest-9.0.2, pluggy-1.6.0 -- /pyseekdb/.venv/bin/python cachedir: .pytest_cache rootdir: /pyseekdb configfile: pyproject.toml plugins: anyio-4.12.1 collected 22 items tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_morph_env PASSED [ 4%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_initialization_with_model_name PASSED [ 9%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_initialization_with_custom_api_key_env PASSED [ 13%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_initialization_with_custom_api_base PASSED [ 18%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_initialization_with_kwargs PASSED [ 22%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_initialization_missing_api_key PASSED [ 27%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_initialization_missing_model_name PASSED [ 31%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_dimension_property_known_model PASSED [ 36%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_call_single_document PASSED [ 40%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_call_multiple_documents PASSED [ 45%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_call_empty_input PASSED [ 50%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_dimension_of_function PASSED [ 54%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_get_default_api_base PASSED [ 59%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_get_default_api_key_env PASSED [ 63%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunction::test_get_model_dimensions PASSED [ 68%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunctionPersistence::test_name PASSED [ 72%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunctionPersistence::test_get_config_with_defaults PASSED [ 77%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunctionPersistence::test_get_config_with_custom_values PASSED [ 81%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunctionPersistence::test_build_from_config_with_defaults PASSED [ 86%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunctionPersistence::test_build_from_config_with_custom_values PASSED [ 90%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunctionPersistence::test_build_from_config_with_dimensions_ignored PASSED [ 95%] tests/unit_tests/test_morph_embedding_function.py::TestMorphEmbeddingFunctionPersistence::test_persistence_roundtrip PASSED [100%] ============================================================================================ 22 passed in 3.97s ============================================================================================ ``` ## Summary by CodeRabbit * **New Features** * Added Morph embedding function ("morph") with support for morph-embedding-v4, discoverable via the embedding registry * Configurable via environment variables and custom API endpoints * **Tests** * Added comprehensive unit tests covering initialization, configuration, dimensions, embedding calls, and persistence round-trips ✏️ Tip: You can customize this high-level summary in your review settings. --- src/pyseekdb/client/embedding_function.py | 2 + .../utils/embedding_functions/__init__.py | 2 + .../morph_embedding_function.py | 137 +++++++ .../test_morph_embedding_function.py | 343 ++++++++++++++++++ 4 files changed, 484 insertions(+) create mode 100644 src/pyseekdb/utils/embedding_functions/morph_embedding_function.py create mode 100644 tests/unit_tests/test_morph_embedding_function.py diff --git a/src/pyseekdb/client/embedding_function.py b/src/pyseekdb/client/embedding_function.py index 58347fc3..31bc761d 100644 --- a/src/pyseekdb/client/embedding_function.py +++ b/src/pyseekdb/client/embedding_function.py @@ -634,6 +634,7 @@ def _initialize(cls) -> None: CohereEmbeddingFunction, GoogleVertexEmbeddingFunction, JinaEmbeddingFunction, + MorphEmbeddingFunction, OllamaEmbeddingFunction, OpenAIEmbeddingFunction, QwenEmbeddingFunction, @@ -646,6 +647,7 @@ def _initialize(cls) -> None: cls._registry["sentence_transformer"] = SentenceTransformerEmbeddingFunction cls._registry["openai"] = OpenAIEmbeddingFunction cls._registry["qwen"] = QwenEmbeddingFunction + cls._registry["morph"] = MorphEmbeddingFunction cls._registry["siliconflow"] = SiliconflowEmbeddingFunction cls._registry["tencent_hunyuan"] = TencentHunyuanEmbeddingFunction cls._registry["ollama"] = OllamaEmbeddingFunction diff --git a/src/pyseekdb/utils/embedding_functions/__init__.py b/src/pyseekdb/utils/embedding_functions/__init__.py index f589b70a..05a4aa41 100644 --- a/src/pyseekdb/utils/embedding_functions/__init__.py +++ b/src/pyseekdb/utils/embedding_functions/__init__.py @@ -10,6 +10,7 @@ from .google_vertex_embedding_function import GoogleVertexEmbeddingFunction from .jina_embedding_function import JinaEmbeddingFunction from .litellm_base_embedding_function import LiteLLMBaseEmbeddingFunction +from .morph_embedding_function import MorphEmbeddingFunction from .ollama_embedding_function import OllamaEmbeddingFunction from .openai_base_embedding_function import OpenAIBaseEmbeddingFunction from .openai_embedding_function import OpenAIEmbeddingFunction @@ -27,6 +28,7 @@ "GoogleVertexEmbeddingFunction", "JinaEmbeddingFunction", "LiteLLMBaseEmbeddingFunction", + "MorphEmbeddingFunction", "OllamaEmbeddingFunction", "OpenAIBaseEmbeddingFunction", "OpenAIEmbeddingFunction", diff --git a/src/pyseekdb/utils/embedding_functions/morph_embedding_function.py b/src/pyseekdb/utils/embedding_functions/morph_embedding_function.py new file mode 100644 index 00000000..4d4661ef --- /dev/null +++ b/src/pyseekdb/utils/embedding_functions/morph_embedding_function.py @@ -0,0 +1,137 @@ +import logging +from typing import Any + +from pyseekdb.utils.embedding_functions.openai_base_embedding_function import ( + OpenAIBaseEmbeddingFunction, +) + +# Known Morph embedding model dimensions +# Source: https://docs.morphllm.com/api-reference/endpoint/embedding +_MORPH_MODEL_DIMENSIONS = { + "morph-embedding-v4": 1536, +} + +logger = logging.getLogger(__name__) + + +class MorphEmbeddingFunction(OpenAIBaseEmbeddingFunction): + """ + A convenient embedding function for Morph embedding models. + + This class provides a simplified interface to Morph embedding models using the + OpenAI-compatible API. + + Example: + pip install pyseekdb openai + + .. code-block:: python + import pyseekdb + from pyseekdb.utils.embedding_functions import MorphEmbeddingFunction + + # Using Morph embedding model + # Set MORPH_API_KEY environment variable first + ef = MorphEmbeddingFunction( + model_name="morph-embedding-v4" + ) + + # Using with custom api_key_env + ef = MorphEmbeddingFunction( + model_name="morph-embedding-v4", + api_key_env="MORPH_API_KEY" + ) + + db = pyseekdb.Client(path="./seekdb.db") + collection = db.create_collection(name="my_collection", embedding_function=ef) + # Add documents + collection.add(ids=["1", "2"], documents=["Hello world", "How are you?"], metadatas=[{"id": 1}, {"id": 2}]) + # Query using semantic search + results = collection.query("How are you?", n_results=1) + print(results) + """ + + def __init__( + self, + model_name: str, + api_key_env: str | None = None, + api_base: str | None = None, + **kwargs: Any, + ): + """Initialize MorphEmbeddingFunction. + + Args: + model_name (str): Name of the Morph embedding model. + api_key_env (str, optional): Name of the environment variable containing the Morph API key. + Defaults to "MORPH_API_KEY" if not provided. + api_base (str, optional): Base URL for the Morph API endpoint. + Defaults to "https://api.morphllm.com/v1" if not provided. + **kwargs: Additional arguments to pass to the OpenAI client. + Common options include: + - timeout: Request timeout in seconds + - max_retries: Maximum number of retries + - See https://github.com/openai/openai-python for more options + """ + super().__init__( + model_name=model_name, + api_key_env=api_key_env, + api_base=api_base, + dimensions=None, + **kwargs, + ) + + def _get_default_api_base(self) -> str: + """Get the default API base URL for Morph. + + Returns: + str: Default Morph API base URL + """ + return "https://api.morphllm.com/v1" + + def _get_default_api_key_env(self) -> str: + """Get the default API key environment variable name for Morph. + + Returns: + str: Default Morph API key environment variable name + """ + return "MORPH_API_KEY" + + def _get_model_dimensions(self) -> dict[str, int]: + """Get a dictionary mapping Morph model names to their default dimensions. + + Returns: + dict[str, int]: Dictionary mapping model names to dimensions + """ + return _MORPH_MODEL_DIMENSIONS + + @staticmethod + def name() -> str: + """Get the unique name identifier for MorphEmbeddingFunction. + + Returns: + The name identifier for this embedding function type + """ + return "morph" + + def get_config(self) -> dict[str, Any]: + return super().get_config() + + @staticmethod + def build_from_config(config: dict[str, Any]) -> "MorphEmbeddingFunction": + model_name = config.get("model_name") + if model_name is None: + raise ValueError("Missing required field 'model_name' in configuration") + + api_key_env = config.get("api_key_env") + api_base = config.get("api_base") + dimensions = config.get("dimensions") + if dimensions is not None: + logger.warning("Ignoring unsupported 'dimensions' for MorphEmbeddingFunction") + client_kwargs = config.get("client_kwargs", {}) + if not isinstance(client_kwargs, dict): + raise TypeError(f"client_kwargs must be a dictionary, but got {client_kwargs}") + + return MorphEmbeddingFunction( + model_name=model_name, + api_key_env=api_key_env, + api_base=api_base, + **client_kwargs, + ) diff --git a/tests/unit_tests/test_morph_embedding_function.py b/tests/unit_tests/test_morph_embedding_function.py new file mode 100644 index 00000000..402b025f --- /dev/null +++ b/tests/unit_tests/test_morph_embedding_function.py @@ -0,0 +1,343 @@ +""" +Unit tests for MorphEmbeddingFunction. + +Tests Morph embedding function initialization, embedding generation, and config handling. +Uses real API calls - requires MORPH_API_KEY environment variable to be set. + +To run this test manually: + pytest tests/unit_tests/test_morph_embedding_function.py -v -s + # Or with environment variable: + MORPH_API_KEY=your-key pytest tests/unit_tests/test_morph_embedding_function.py -v -s +""" + +import importlib.util +import os + +import pytest + +from pyseekdb.client.embedding_function import dimension_of +from pyseekdb.utils.embedding_functions import MorphEmbeddingFunction + +from .test_utils import env_guard + + +def is_openai_available() -> bool: + """ + Check if openai is available for testing. + + Returns: + True if openai is available, False otherwise. + """ + return importlib.util.find_spec("openai") is not None + + +# Skip this test by default - it requires external API access and API keys +@pytest.mark.skipif( + not os.environ.get("MORPH_API_KEY") or not is_openai_available(), + reason="MORPH_API_KEY environment variable must be set", +) +class TestMorphEmbeddingFunction: + """Test MorphEmbeddingFunction - skipped by default, requires manual execution""" + + def test_morph_env(self): + """Test if openai package is installed and required environment variables are set.""" + if not is_openai_available(): + print("openai package is not installed") + raise AssertionError("openai package is not installed") + + if not os.environ.get("MORPH_API_KEY"): + print("MORPH_API_KEY environment variable is not set") + raise AssertionError("MORPH_API_KEY environment variable is not set") + + def test_initialization_with_model_name(self): + """Test MorphEmbeddingFunction initialization with required model_name""" + print("\nTesting MorphEmbeddingFunction initialization with required model_name") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + + assert ef is not None + assert ef.model_name == "morph-embedding-v4" + assert ef.api_key_env == "MORPH_API_KEY" + assert ef.api_base == "https://api.morphllm.com/v1" + assert ef._dimensions_param is None + print(f" Model name: {ef.model_name}") + print(f" API key env: {ef.api_key_env}") + print(f" API base: {ef.api_base}") + + def test_initialization_with_custom_api_key_env(self): + """Test MorphEmbeddingFunction initialization with custom API key env""" + print("\nTesting MorphEmbeddingFunction initialization with custom API key env") + + self.test_morph_env() + + custom_key_env = "CUSTOM_MORPH_KEY" + if not os.environ.get(custom_key_env): + os.environ[custom_key_env] = "your-custom-key" + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4", api_key_env=custom_key_env) + assert ef.api_key_env == custom_key_env + print(f" Custom API key env: {ef.api_key_env}") + + def test_initialization_with_custom_api_base(self): + """Test MorphEmbeddingFunction initialization with custom API base""" + print("\nTesting MorphEmbeddingFunction initialization with custom API base") + + self.test_morph_env() + + custom_base = "https://api.morphllm.com/v1" + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4", api_base=custom_base) + assert ef.api_base == custom_base + print(f" Custom API base: {ef.api_base}") + + def test_initialization_with_kwargs(self): + """Test MorphEmbeddingFunction initialization with additional kwargs""" + print("\nTesting MorphEmbeddingFunction initialization with kwargs") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4", timeout=30, max_retries=3) + assert ef is not None + print(" Initialized with timeout and max_retries") + + def test_initialization_missing_api_key(self): + """Test that missing API key raises ValueError""" + print("\nTesting MorphEmbeddingFunction initialization with missing API key") + + original_key = os.environ.pop("MORPH_API_KEY", None) + try: + with pytest.raises(ValueError, match="API key environment variable"): + MorphEmbeddingFunction(model_name="morph-embedding-v4") + finally: + if original_key: + os.environ["MORPH_API_KEY"] = original_key + + def test_initialization_missing_model_name(self): + """Test that missing model_name raises TypeError""" + print("\nTesting MorphEmbeddingFunction initialization with missing model_name") + + self.test_morph_env() + + with pytest.raises(TypeError): + MorphEmbeddingFunction() + + def test_dimension_property_known_model(self): + """Test dimension property for known Morph model""" + print("\nTesting MorphEmbeddingFunction dimension property for known model") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + dim = ef.dimension + assert dim == 1536, f"Expected dimension 1536 for morph-embedding-v4, got {dim}" + print(f" morph-embedding-v4 dimension: {dim}") + + def test_call_single_document(self): + """Test __call__ with single document""" + print("\nTesting MorphEmbeddingFunction embedding generation (single document)") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + single_doc = "def add(a, b): return a + b" + embeddings = ef(single_doc) + + assert isinstance(embeddings, list) + assert len(embeddings) == 1 + assert isinstance(embeddings[0], list) + assert len(embeddings[0]) > 0 + print(f" Single document embedding dimension: {len(embeddings[0])}") + + def test_call_multiple_documents(self): + """Test __call__ with multiple documents""" + print("\nTesting MorphEmbeddingFunction embedding generation (multiple documents)") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + multiple_docs = [ + "def foo():\n return 1", + "class Bar:\n pass", + "print('hello')", + ] + embeddings = ef(multiple_docs) + + assert isinstance(embeddings, list) + assert len(embeddings) == len(multiple_docs) + for emb in embeddings: + assert isinstance(emb, list) + assert len(emb) == len(embeddings[0]), "All embeddings should have same dimension" + print(f" Multiple documents embedding dimension: {len(embeddings[0])}") + print(f" Number of embeddings: {len(embeddings)}") + + def test_call_empty_input(self): + """Test __call__ with empty input""" + print("\nTesting MorphEmbeddingFunction with empty input") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + empty_embeddings = ef([]) + assert empty_embeddings == [] + print(" Empty input correctly returns empty list") + + def test_dimension_of_function(self): + """Test dimension_of function with MorphEmbeddingFunction""" + print("\nTesting dimension_of function with MorphEmbeddingFunction") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + dim = dimension_of(ef) + assert dim == 1536 + print(f" dimension_of result: {dim}") + + def test_get_default_api_base(self): + """Test _get_default_api_base method""" + print("\nTesting _get_default_api_base method") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + api_base = ef._get_default_api_base() + assert api_base == "https://api.morphllm.com/v1" + print(f" Default API base: {api_base}") + + def test_get_default_api_key_env(self): + """Test _get_default_api_key_env method""" + print("\nTesting _get_default_api_key_env method") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + api_key_env = ef._get_default_api_key_env() + assert api_key_env == "MORPH_API_KEY" + print(f" Default API key env: {api_key_env}") + + def test_get_model_dimensions(self): + """Test _get_model_dimensions method""" + print("\nTesting _get_model_dimensions method") + + self.test_morph_env() + + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + dimensions = ef._get_model_dimensions() + + assert isinstance(dimensions, dict) + assert "morph-embedding-v4" in dimensions + assert dimensions["morph-embedding-v4"] == 1536 + print(f" Model dimensions: {dimensions}") + + +@pytest.mark.skipif(not is_openai_available(), reason="openai is not available on this system") +class TestMorphEmbeddingFunctionPersistence: + """Test persistence for MorphEmbeddingFunction""" + + def test_name(self): + """Test that name() returns the correct identifier""" + assert MorphEmbeddingFunction.name() == "morph" + + def test_get_config_with_defaults(self): + """Test that get_config() returns correct config with default values""" + with env_guard(MORPH_API_KEY="test-key"): + ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + config = ef.get_config() + + assert isinstance(config, dict) + assert config["model_name"] == "morph-embedding-v4" + assert config["api_key_env"] == "MORPH_API_KEY" + assert config["api_base"] == "https://api.morphllm.com/v1" + assert config["dimensions"] is None + assert isinstance(config["client_kwargs"], dict) + assert "name" not in config + + def test_get_config_with_custom_values(self): + """Test that get_config() returns correct config with custom values""" + with env_guard(CUSTOM_MORPH_KEY="test-key"): + ef = MorphEmbeddingFunction( + model_name="morph-embedding-v4", + api_key_env="CUSTOM_MORPH_KEY", + api_base="https://custom-morph.example.com/v1", + timeout=60, + ) + config = ef.get_config() + + assert config["model_name"] == "morph-embedding-v4" + assert config["api_key_env"] == "CUSTOM_MORPH_KEY" + assert config["api_base"] == "https://custom-morph.example.com/v1" + assert config["dimensions"] is None + assert config["client_kwargs"]["timeout"] == 60 + + def test_build_from_config_with_defaults(self): + """Test that build_from_config() restores instance with default values""" + config = { + "model_name": "morph-embedding-v4", + "api_key_env": "MORPH_API_KEY", + "api_base": "https://api.morphllm.com/v1", + "dimensions": None, + "client_kwargs": {}, + } + + with env_guard(MORPH_API_KEY="test-key"): + restored_ef = MorphEmbeddingFunction.build_from_config(config) + + assert isinstance(restored_ef, MorphEmbeddingFunction) + assert restored_ef.model_name == "morph-embedding-v4" + assert restored_ef.api_key_env == "MORPH_API_KEY" + assert restored_ef.api_base == "https://api.morphllm.com/v1" + assert restored_ef._dimensions_param is None + + def test_build_from_config_with_custom_values(self): + """Test that build_from_config() restores instance with custom values""" + config = { + "model_name": "morph-embedding-v4", + "api_key_env": "CUSTOM_MORPH_KEY", + "api_base": "https://custom-morph.example.com/v1", + "dimensions": None, + "client_kwargs": {"timeout": 60}, + } + + with env_guard(CUSTOM_MORPH_KEY="test-key"): + restored_ef = MorphEmbeddingFunction.build_from_config(config) + + assert isinstance(restored_ef, MorphEmbeddingFunction) + assert restored_ef.model_name == "morph-embedding-v4" + assert restored_ef.api_key_env == "CUSTOM_MORPH_KEY" + assert restored_ef.api_base == "https://custom-morph.example.com/v1" + assert restored_ef._dimensions_param is None + assert restored_ef._client_kwargs["timeout"] == 60 + + def test_build_from_config_with_dimensions_ignored(self, caplog): + """Test that build_from_config() ignores dimensions parameter""" + config = { + "model_name": "morph-embedding-v4", + "api_key_env": "MORPH_API_KEY", + "api_base": "https://api.morphllm.com/v1", + "dimensions": 1536, + "client_kwargs": {}, + } + + with env_guard(MORPH_API_KEY="test-key"): + restored_ef = MorphEmbeddingFunction.build_from_config(config) + + assert isinstance(restored_ef, MorphEmbeddingFunction) + assert restored_ef._dimensions_param is None + + def test_persistence_roundtrip(self): + """Test complete roundtrip: get_config -> build_from_config""" + with env_guard(MORPH_API_KEY="test-key"): + original_ef = MorphEmbeddingFunction(model_name="morph-embedding-v4") + + config = original_ef.get_config() + restored_ef = MorphEmbeddingFunction.build_from_config(config) + + assert isinstance(restored_ef, MorphEmbeddingFunction) + assert restored_ef.model_name == original_ef.model_name + assert restored_ef.api_key_env == original_ef.api_key_env + assert restored_ef.api_base == original_ef.api_base + assert restored_ef._dimensions_param == original_ef._dimensions_param + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"]) From 2dfa0e68b89fca50fb0f9303b6fe9c399d9865d3 Mon Sep 17 00:00:00 2001 From: chakkk309 Date: Sat, 31 Jan 2026 15:00:11 +0800 Subject: [PATCH 03/10] [vibe coding] feat: add mistral embedding function support (#153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix https://github.com/oceanbase/pyseekdb/issues/135. Integrate [Mistral](https://docs.morphllm.com/api-reference/endpoint/embedding) embedding function ## Solution Description Mistral test result: ``` uv run pytest tests/unit_tests/test_mistral_embedding_function.py -vv ================================================================================ test session starts ================================================================================ platform darwin -- Python 3.11.13, pytest-9.0.2, pluggy-1.6.0 -- /pyseekdb/.venv/bin/python3 cachedir: .pytest_cache rootdir: /pyseekdb configfile: pyproject.toml plugins: anyio-4.12.1 collected 12 items tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_mistral_env PASSED [ 8%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_defaults PASSED [ 16%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_custom_api_key_env PASSED [ 25%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_custom_api_base PASSED [ 33%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_additional_kwargs PASSED [ 41%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_initialization_with_missing_api_key PASSED [ 50%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_dimension_property_for_known_model PASSED [ 58%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_embedding_generation_single_document PASSED [ 66%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_embedding_generation_multiple_documents PASSED [ 75%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_embedding_with_empty_input PASSED [ 83%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_dimension_of_function PASSED [ 91%] tests/unit_tests/test_mistral_embedding_function.py::TestMistralEmbeddingFunction::test_persistence PASSED [100%] ================================================================================ 12 passed in 3.22s ================================================================================= ## Summary by CodeRabbit * **New Features** * Added support for Mistral text embeddings: a new embedding provider is available and can be configured via environment variable and standard embedding settings; supports batch inputs and serialization. * **Tests** * Added comprehensive unit tests for the Mistral embedding provider (guarded for runtime API availability). ✏️ Tip: You can customize this high-level summary in your review settings. --- src/pyseekdb/client/embedding_function.py | 2 + .../utils/embedding_functions/__init__.py | 2 + .../mistral_embedding_function.py | 153 ++++++++++++++ .../test_mistral_embedding_function.py | 188 ++++++++++++++++++ 4 files changed, 345 insertions(+) create mode 100644 src/pyseekdb/utils/embedding_functions/mistral_embedding_function.py create mode 100644 tests/unit_tests/test_mistral_embedding_function.py diff --git a/src/pyseekdb/client/embedding_function.py b/src/pyseekdb/client/embedding_function.py index 31bc761d..5d1d2d86 100644 --- a/src/pyseekdb/client/embedding_function.py +++ b/src/pyseekdb/client/embedding_function.py @@ -634,6 +634,7 @@ def _initialize(cls) -> None: CohereEmbeddingFunction, GoogleVertexEmbeddingFunction, JinaEmbeddingFunction, + MistralEmbeddingFunction, MorphEmbeddingFunction, OllamaEmbeddingFunction, OpenAIEmbeddingFunction, @@ -647,6 +648,7 @@ def _initialize(cls) -> None: cls._registry["sentence_transformer"] = SentenceTransformerEmbeddingFunction cls._registry["openai"] = OpenAIEmbeddingFunction cls._registry["qwen"] = QwenEmbeddingFunction + cls._registry["mistral"] = MistralEmbeddingFunction cls._registry["morph"] = MorphEmbeddingFunction cls._registry["siliconflow"] = SiliconflowEmbeddingFunction cls._registry["tencent_hunyuan"] = TencentHunyuanEmbeddingFunction diff --git a/src/pyseekdb/utils/embedding_functions/__init__.py b/src/pyseekdb/utils/embedding_functions/__init__.py index 05a4aa41..eace5d2d 100644 --- a/src/pyseekdb/utils/embedding_functions/__init__.py +++ b/src/pyseekdb/utils/embedding_functions/__init__.py @@ -10,6 +10,7 @@ from .google_vertex_embedding_function import GoogleVertexEmbeddingFunction from .jina_embedding_function import JinaEmbeddingFunction from .litellm_base_embedding_function import LiteLLMBaseEmbeddingFunction +from .mistral_embedding_function import MistralEmbeddingFunction from .morph_embedding_function import MorphEmbeddingFunction from .ollama_embedding_function import OllamaEmbeddingFunction from .openai_base_embedding_function import OpenAIBaseEmbeddingFunction @@ -28,6 +29,7 @@ "GoogleVertexEmbeddingFunction", "JinaEmbeddingFunction", "LiteLLMBaseEmbeddingFunction", + "MistralEmbeddingFunction", "MorphEmbeddingFunction", "OllamaEmbeddingFunction", "OpenAIBaseEmbeddingFunction", diff --git a/src/pyseekdb/utils/embedding_functions/mistral_embedding_function.py b/src/pyseekdb/utils/embedding_functions/mistral_embedding_function.py new file mode 100644 index 00000000..e390ddde --- /dev/null +++ b/src/pyseekdb/utils/embedding_functions/mistral_embedding_function.py @@ -0,0 +1,153 @@ +import warnings +from typing import Any + +from pyseekdb.client.embedding_function import Documents, Embeddings +from pyseekdb.utils.embedding_functions.openai_base_embedding_function import ( + OpenAIBaseEmbeddingFunction, +) + +# Known Mistral embedding model dimensions +# Source: https://docs.mistral.ai/capabilities/embeddings/text_embeddings +_MISTRAL_MODEL_DIMENSIONS = { + "mistral-embed": 1024, +} + + +class MistralEmbeddingFunction(OpenAIBaseEmbeddingFunction): + """ + A convenient embedding function for Mistral text embedding models. + + This class provides a simplified interface to Mistral text embeddings using the + OpenAI-compatible API. + + Note: The embeddings API only accepts the model name and input texts. + + For more information about Mistral embeddings, see: + https://docs.mistral.ai/capabilities/embeddings/text_embeddings + + Example: + pip install pyseekdb openai + + .. code-block:: python + import pyseekdb + from pyseekdb.utils.embedding_functions import MistralEmbeddingFunction + + # Using Mistral text embedding model + # Set MISTRAL_API_KEY environment variable first + ef = MistralEmbeddingFunction(model_name="mistral-embed") + + # Using with additional parameters + ef = MistralEmbeddingFunction( + model_name="mistral-embed", + timeout=30, + max_retries=3 + ) + + db = pyseekdb.Client(path="./seekdb.db") + collection = db.create_collection(name="my_collection", embedding_function=ef) + # Add documents + collection.add(ids=["1", "2"], documents=["Hello world", "How are you?"], metadatas=[{"id": 1}, {"id": 2}]) + # Query using semantic search + results = collection.query("How are you?", n_results=1) + print(results) + + """ + + def __init__( + self, + model_name: str = "mistral-embed", + api_key_env: str | None = None, + api_base: str | None = None, + dimensions: int | None = None, + **kwargs: Any, + ): + """Initialize MistralEmbeddingFunction. + + Args: + model_name (str, optional): Name of the Mistral embedding model. + Defaults to "mistral-embed". + api_key_env (str, optional): Name of the environment variable containing the Mistral API key. + Defaults to "MISTRAL_API_KEY" if not provided. + api_base (str, optional): Base URL for the Mistral API endpoint. + Defaults to "https://api.mistral.ai/v1" if not provided. + dimensions (int, optional): This parameter is not supported by the Mistral embeddings API. + If provided, a warning will be issued and the parameter will be ignored. + **kwargs: Additional arguments to pass to the OpenAI client. + Common options include: + - timeout: Request timeout in seconds + - max_retries: Maximum number of retries + - See https://github.com/openai/openai-python for more options + """ + if dimensions is not None: + warnings.warn( + "The dimensions parameter is not supported by Mistral embeddings. " + "The provided dimensions parameter will be ignored.", + UserWarning, + stacklevel=2, + ) + + super().__init__( + model_name=model_name, + api_key_env=api_key_env, + api_base=api_base, + dimensions=None, + **kwargs, + ) + + def _get_default_api_base(self) -> str: + return "https://api.mistral.ai/v1" + + def _get_default_api_key_env(self) -> str: + return "MISTRAL_API_KEY" + + def _get_model_dimensions(self) -> dict[str, int]: + return _MISTRAL_MODEL_DIMENSIONS + + @staticmethod + def name() -> str: + return "mistral" + + def __call__(self, documents: Documents) -> Embeddings: + """Generate embeddings for the given documents using Mistral's input parameter.""" + if isinstance(documents, str): + documents = [documents] + + if not documents: + return [] + + request_params = { + "model": self.model_name, + "input": documents, + } + + response = self._client.embeddings.create(**request_params) + embeddings = [item.embedding for item in response.data] + + if len(embeddings) != len(documents): + raise ValueError(f"Expected {len(documents)} embeddings but got {len(embeddings)} from API") + + return embeddings + + def get_config(self) -> dict[str, Any]: + return super().get_config() + + @staticmethod + def build_from_config(config: dict[str, Any]) -> "MistralEmbeddingFunction": + model_name = config.get("model_name") + if model_name is None: + raise ValueError("Missing required field 'model_name' in configuration") + + api_key_env = config.get("api_key_env") + api_base = config.get("api_base") + dimensions = config.get("dimensions") + client_kwargs = config.get("client_kwargs", {}) + if not isinstance(client_kwargs, dict): + raise TypeError(f"client_kwargs must be a dictionary, but got {client_kwargs}") + + return MistralEmbeddingFunction( + model_name=model_name, + api_key_env=api_key_env, + api_base=api_base, + dimensions=dimensions, + **client_kwargs, + ) diff --git a/tests/unit_tests/test_mistral_embedding_function.py b/tests/unit_tests/test_mistral_embedding_function.py new file mode 100644 index 00000000..9077f1fa --- /dev/null +++ b/tests/unit_tests/test_mistral_embedding_function.py @@ -0,0 +1,188 @@ +""" +Unit tests for MistralEmbeddingFunction. + +Tests Mistral embedding function initialization, embedding generation, and dimension detection. +Uses real API calls - requires MISTRAL_API_KEY environment variable to be set. + +To run this test manually: + pytest tests/unit_tests/test_mistral_embedding_function.py -v -s + # Or with environment variable: + MISTRAL_API_KEY=your-key pytest tests/unit_tests/test_mistral_embedding_function.py -v -s +""" + +import importlib.util +import os + +import pytest + +from pyseekdb.client.embedding_function import dimension_of +from pyseekdb.utils.embedding_functions import MistralEmbeddingFunction + +from .test_utils import env_guard + + +def is_openai_available() -> bool: + """ + Check if openai is available for testing. + + Returns: + True if openai is available, False otherwise. + """ + return importlib.util.find_spec("openai") is not None + + +# Skip this test by default - it requires external API access and API keys +@pytest.mark.skipif( + not os.environ.get("MISTRAL_API_KEY") or not is_openai_available(), + reason="MISTRAL_API_KEY environment variable must be set", +) +class TestMistralEmbeddingFunction: + """Test MistralEmbeddingFunction - skipped by default, requires manual execution""" + + def test_mistral_env(self): + """Test if openai package is installed and required environment variables are set.""" + if not is_openai_available(): + print("openai package is not installed") + raise AssertionError("openai package is not installed") + + if not os.environ.get("MISTRAL_API_KEY"): + print("MISTRAL_API_KEY environment variable is not set") + raise AssertionError("MISTRAL_API_KEY environment variable is not set") + + def test_initialization_with_defaults(self): + """Test MistralEmbeddingFunction initialization with default values""" + print("\nTesting MistralEmbeddingFunction initialization with defaults") + + # Check if openai is available and env vars are set + self.test_mistral_env() + + ef = MistralEmbeddingFunction() + + assert ef is not None + assert ef.model_name == "mistral-embed" + assert ef.api_key_env == "MISTRAL_API_KEY" + assert ef.api_base == "https://api.mistral.ai/v1" + assert ef._dimensions_param is None + print(f" Model name: {ef.model_name}") + print(f" API key env: {ef.api_key_env}") + print(f" API base: {ef.api_base}") + + def test_initialization_with_custom_api_key_env(self): + """Test MistralEmbeddingFunction initialization with custom API key env""" + print("\nTesting MistralEmbeddingFunction initialization with custom API key env") + + self.test_mistral_env() + + custom_key_env = "CUSTOM_MISTRAL_KEY" + if not os.environ.get(custom_key_env): + os.environ[custom_key_env] = "your-custom-key" + + ef = MistralEmbeddingFunction(model_name="mistral-embed", api_key_env=custom_key_env) + assert ef.api_key_env == custom_key_env + print(f" Custom API key env: {ef.api_key_env}") + + def test_initialization_with_custom_api_base(self): + """Test MistralEmbeddingFunction initialization with custom API base""" + print("\nTesting MistralEmbeddingFunction initialization with custom API base") + + self.test_mistral_env() + + custom_base = "https://api.mistral.ai/v1" + ef = MistralEmbeddingFunction(model_name="mistral-embed", api_base=custom_base) + assert ef.api_base == custom_base + print(f" Custom API base: {ef.api_base}") + + def test_initialization_with_additional_kwargs(self): + """Test MistralEmbeddingFunction initialization with additional kwargs""" + print("\nTesting MistralEmbeddingFunction initialization with kwargs") + + self.test_mistral_env() + + ef = MistralEmbeddingFunction(model_name="mistral-embed", timeout=30, max_retries=3) + assert ef._client_kwargs.get("timeout") == 30 + assert ef._client_kwargs.get("max_retries") == 3 + print(f" Client kwargs: {ef._client_kwargs}") + + def test_initialization_with_missing_api_key(self): + """Test MistralEmbeddingFunction initialization with missing API key""" + print("\nTesting MistralEmbeddingFunction initialization with missing API key") + + with env_guard(MISTRAL_API_KEY=None): + try: + MistralEmbeddingFunction(model_name="mistral-embed") + raise AssertionError("Expected ValueError for missing API key") + except ValueError as e: + print(f" Caught expected error: {e}") + + def test_dimension_property_for_known_model(self): + """Test MistralEmbeddingFunction dimension property for known model""" + print("\nTesting MistralEmbeddingFunction dimension property for known model") + + self.test_mistral_env() + + ef = MistralEmbeddingFunction(model_name="mistral-embed") + assert ef.dimension == 1024 + print(f" Model mistral-embed dimension: {ef.dimension}") + + def test_embedding_generation_single_document(self): + """Test MistralEmbeddingFunction embedding generation (single document)""" + print("\nTesting MistralEmbeddingFunction embedding generation (single document)") + + self.test_mistral_env() + + ef = MistralEmbeddingFunction(model_name="mistral-embed") + embeddings = ef("Hello world") + assert len(embeddings) == 1 + assert len(embeddings[0]) > 0 + print(f" Embedding dimension: {len(embeddings[0])}") + + def test_embedding_generation_multiple_documents(self): + """Test MistralEmbeddingFunction embedding generation (multiple documents)""" + print("\nTesting MistralEmbeddingFunction embedding generation (multiple documents)") + + self.test_mistral_env() + + ef = MistralEmbeddingFunction(model_name="mistral-embed") + embeddings = ef(["Hello world", "How are you?"]) + assert len(embeddings) == 2 + assert len(embeddings[0]) > 0 + print(f" Embedding dimension: {len(embeddings[0])}") + + def test_embedding_with_empty_input(self): + """Test MistralEmbeddingFunction with empty input""" + print("\nTesting MistralEmbeddingFunction with empty input") + + self.test_mistral_env() + + ef = MistralEmbeddingFunction(model_name="mistral-embed") + embeddings = ef([]) + assert embeddings == [] + print(" Empty input returned empty embeddings") + + def test_dimension_of_function(self): + """Test dimension_of function with MistralEmbeddingFunction""" + print("\nTesting dimension_of function with MistralEmbeddingFunction") + + self.test_mistral_env() + + ef = MistralEmbeddingFunction(model_name="mistral-embed") + dim = dimension_of(ef) + assert dim == 1024 + print(f" dimension_of returned: {dim}") + + def test_persistence(self): + """Test persistence for MistralEmbeddingFunction""" + print("\nTesting MistralEmbeddingFunction persistence") + + self.test_mistral_env() + + assert MistralEmbeddingFunction.name() == "mistral" + + ef = MistralEmbeddingFunction(model_name="mistral-embed") + config = ef.get_config() + + restored_ef = MistralEmbeddingFunction.build_from_config(config) + assert isinstance(restored_ef, MistralEmbeddingFunction) + assert restored_ef.model_name == ef.model_name + assert restored_ef.api_key_env == ef.api_key_env + assert restored_ef.api_base == ef.api_base From af7c225a857a71554448bf491ff5c0105e1e1afe Mon Sep 17 00:00:00 2001 From: NTLx Date: Sat, 31 Jan 2026 15:25:46 +0800 Subject: [PATCH 04/10] docs: format document strings for collection management APIs (#157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Addressing issue oceanbase/pyseekdb#105. Formatted the docstrings for the collection management APIs in `client_base.py` to improve readability and consistency. ### Changes - Updated docstrings for the following methods in `BaseClient` and `ClientAPI`: - `create_collection` - `get_collection` - `get_or_create_collection` - `delete_collection` - `list_collections` - `count_collection` - `has_collection` - Adopted a more standard docstring format (closer to Google style) with clear `Args`, `Returns`, `Raises`, and `Examples` sections. - Removed redundant or repetitive text. - Added comprehensive examples for `create_collection` showing different configuration options. ## Test plan - Verified that the code still parses and runs correctly by running existing tests: `uv run pytest tests/unit_tests/test_collection_name_validation.py -v`. - Checked formatting visually to ensure clarity. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Documentation** * Expanded user-facing documentation for collection management methods with detailed examples and clearer behavior descriptions. * **Improvements** * Enhanced validation and error messaging for collection operations. * Improved handling of embedding function configuration and automatic dimension inference. * More robust collection lifecycle management. ✏️ Tip: You can customize this high-level summary in your review settings. --- src/pyseekdb/client/client_base.py | 189 +++++++++++++---------------- 1 file changed, 86 insertions(+), 103 deletions(-) diff --git a/src/pyseekdb/client/client_base.py b/src/pyseekdb/client/client_base.py index 8b312113..ced46c02 100644 --- a/src/pyseekdb/client/client_base.py +++ b/src/pyseekdb/client/client_base.py @@ -210,14 +210,22 @@ def create_collection( @abstractmethod def get_collection(self, name: str, embedding_function: EmbeddingFunctionParam = _NOT_PROVIDED) -> "Collection": - """ - Get collection object + """Get an existing collection. Args: - name: Collection name - embedding_function: Embedding function to convert documents to embeddings. - Defaults to DefaultEmbeddingFunction. - If explicitly set to None, collection will not have an embedding function. + name: The name of the collection to retrieve. + embedding_function: The embedding function to use. If not provided, + it will try to load the function used when creating the collection. + If explicitly set to None, no embedding function will be used. + + Returns: + The ``Collection`` object. + + Raises: + ValueError: If the collection does not exist. + + Examples: + >>> collection = client.get_collection("my_collection") """ pass @@ -512,85 +520,48 @@ def create_collection( # noqa: C901 embedding_function: EmbeddingFunctionParam = _NOT_PROVIDED, **kwargs, ) -> "Collection": - """ - Create a collection (user-facing API) + """Create a new collection. Args: - name: Collection name - configuration: Index configuration (Configuration or HNSWConfiguration). - If not provided, uses default configuration (dimension=384, distance='cosine', analyzer='ik'). - If explicitly set to None, will try to calculate dimension from embedding_function. - If embedding_function is also None, will raise an error. - For backward compatibility, HNSWConfiguration is still accepted. - Configuration can include fulltext index configuration. - embedding_function: Embedding function to convert documents to embeddings. - Defaults to DefaultEmbeddingFunction. - If explicitly set to None, collection will not have an embedding function. - If provided, the actual dimension will be calculated by calling - embedding_function.__call__("seekdb"), and this dimension will be used - to create the table. If configuration.dimension is set and doesn't match - the calculated dimension, a ValueError will be raised. - **kwargs: Additional parameters + name: The name of the collection to create. Must contain only alphanumeric + characters or underscores. + configuration: Index configuration. Defaults to None (uses HNSW with + Cosine distance and dimension 384). Can be a ``Configuration`` or + ``HNSWConfiguration`` object. If set to None, the dimension will be + inferred from the embedding function. + embedding_function: The embedding function to use for this collection. + Defaults to ``DefaultEmbeddingFunction`` (all-MiniLM-L6-v2). If set to None, + no embedding function will be used (embeddings must be provided manually). + **kwargs: Additional parameters for collection creation. Returns: - Collection object + The created ``Collection`` object. Raises: - ValueError: If configuration is explicitly set to None and embedding_function is also None - (cannot determine dimension), or if embedding_function is provided and - configuration.dimension doesn't match the calculated dimension from embedding_function - TypeError: If configuration is not None, Configuration, or HNSWConfiguration + ValueError: If the collection name is invalid, already exists, or if the + configuration/embedding function combination is invalid (e.g., dimension mismatch). + TypeError: If the configuration object is of an invalid type. Examples: - .. code-block:: python - # Using default configuration and default embedding function (defaults to IK parser) - collection = client.create_collection('my_collection') - - .. code-block:: python - # Using custom embedding function (dimension will be calculated automatically) - from pyseekdb import DefaultEmbeddingFunction - ef = DefaultEmbeddingFunction(model_name='all-MiniLM-L6-v2') - config = HNSWConfiguration(dimension=384, distance='cosine') # Must match EF dimension - collection = client.create_collection( - 'my_collection', - configuration=config, - embedding_function=ef - ) - - .. code-block:: python - # Using Configuration wrapper with IK parser (default) - from pyseekdb import Configuration, HNSWConfiguration, FulltextIndexConfig - config = Configuration( - hnsw=HNSWConfiguration(dimension=384, distance='cosine'), - fulltext_config=FulltextIndexConfig(analyzer='ik') - ) - collection = client.create_collection('my_collection', configuration=config, embedding_function=ef) + Create a collection with default settings: - .. code-block:: python - # Using Space parser - config = Configuration( - hnsw=HNSWConfiguration(dimension=384, distance='cosine'), - fulltext_config=FulltextIndexConfig(analyzer='space') - ) - collection = client.create_collection('my_collection', configuration=config, embedding_function=ef) + >>> client.create_collection("my_collection") - .. code-block:: python - # Using Ngram parser with parameters - config = Configuration( - hnsw=HNSWConfiguration(dimension=384, distance='cosine'), - fulltext_config=FulltextIndexConfig(analyzer='ngram', properties={'size': 2}) - ) - collection = client.create_collection('my_collection', configuration=config, embedding_function=ef) + Create a collection with a custom embedding function: - .. code-block:: python - # Explicitly set configuration=None, use embedding function to determine dimension - collection = client.create_collection('my_collection', configuration=None, embedding_function=ef) + >>> from pyseekdb import DefaultEmbeddingFunction + >>> ef = DefaultEmbeddingFunction(model_name="all-MiniLM-L6-v2") + >>> collection = client.create_collection("my_docs", embedding_function=ef) - .. code-block:: python - # Explicitly disable embedding function (use configuration dimension) - config = HNSWConfiguration(dimension=128, distance='cosine') - collection = client.create_collection('my_collection', configuration=config, embedding_function=None) + Create a collection with specific configuration: + >>> from pyseekdb import HNSWConfiguration + >>> config = HNSWConfiguration(dimension=128, distance="l2") + >>> collection = client.create_collection( + ... "custom_config", + ... configuration=config, + ... embedding_function=None + ... ) """ _validate_collection_name(name) if self.has_collection(name): @@ -982,11 +953,16 @@ def _get_collection_v1(self, name: str, embedding_function: EmbeddingFunctionPar return Collection(client=self, name=name, embedding_function=embedding_function, **metadata) def delete_collection(self, name: str) -> None: - """ - Delete a collection (user-facing API) + """Delete a collection. Args: - name: Collection name + name: The name of the collection to delete. + + Raises: + ValueError: If the collection does not exist. + + Examples: + >>> client.delete_collection("my_collection") """ try: self._delete_collection_v2(name) @@ -1032,11 +1008,15 @@ def _delete_collection_v1(self, name: str) -> None: self._execute(f"DROP TABLE IF EXISTS `{table_name}`") def list_collections(self) -> list["Collection"]: - """ - List all collections (user-facing API) + """List all collections in the database. Returns: - List of Collection objects + A list of ``Collection`` objects. + + Examples: + >>> collections = client.list_collections() + >>> for col in collections: + ... print(col.name) """ collections = self._list_collections_v1() collections.extend(self._list_collections_v2()) @@ -1142,25 +1122,30 @@ def _list_collections_v1(self) -> list["Collection"]: return collections def count_collection(self) -> int: - """ - Count the number of collections in the current database + """Count the total number of collections. Returns: - Number of collections + The number of collections. Examples: - count = client.count_collection() - print(f"Database has {count} collections") + >>> count = client.count_collection() + >>> print(f"Database has {count} collections") """ collections = self.list_collections() return len(collections) def has_collection(self, name: str) -> bool: - """ - Check if a collection exists (user-facing API) + """Check if a collection exists. Args: - name: Collection name + name: The name of the collection to check. + + Returns: + True if the collection exists, False otherwise. + + Examples: + >>> if client.has_collection("my_collection"): + ... print("Collection exists!") """ return self._has_collection_v2(name) or self._has_collection_v1(name) @@ -1211,29 +1196,27 @@ def get_or_create_collection( embedding_function: EmbeddingFunctionParam = _NOT_PROVIDED, **kwargs, ) -> "Collection": - """ - Get an existing collection or create it if it doesn't exist (user-facing API) + """Get a collection if it exists, otherwise create it. Args: - name: Collection name - configuration: Configuration (HNSWConfiguration is accepted for backward compatibility) - Please refer to create_collection for more details. - embedding_function: Embedding function to convert documents to embeddings. - Defaults to DefaultEmbeddingFunction. - If explicitly set to None, collection will not have an embedding function. - If provided when creating a new collection, the actual dimension will be - calculated by calling embedding_function.__call__("seekdb"), and this - dimension will be used to create the table. If configuration.dimension is - set and doesn't match the calculated dimension, a ValueError will be raised. - **kwargs: Additional parameters for create_collection + name: The name of the collection. + configuration: Index configuration. Defaults to None (uses HNSW with + Cosine distance and dimension 384). Can be a ``Configuration`` or + ``HNSWConfiguration`` object. If set to None, the dimension will be + inferred from the embedding function. + embedding_function: The embedding function to use for this collection. + Defaults to ``DefaultEmbeddingFunction`` (all-MiniLM-L6-v2). If set to None, + no embedding function will be used (embeddings must be provided manually). + **kwargs: Additional parameters passed to ``create_collection`` if the collection is created. Returns: - Collection object + The existing or newly created ``Collection`` object. Raises: - ValueError: If creating a new collection and configuration is explicitly set to None and - embedding_function is also None (cannot determine dimension), or if embedding_function - is provided and configuration.dimension doesn't match the calculated dimension + ValueError: If the configuration/embedding function combination is invalid (e.g., dimension mismatch). + + Examples: + >>> collection = client.get_or_create_collection("my_collection") """ # Validate collection name before any database interaction _validate_collection_name(name) From 175c55f9bafd1fe78408ed3705ba37085efa36fa Mon Sep 17 00:00:00 2001 From: zhangjiongai-debug Date: Sat, 31 Jan 2026 15:30:47 +0800 Subject: [PATCH 05/10] Add explicit UV CLI workflow for pyseekdb (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add Development section to README with UV workflow documentation (prerequisites, setup, common commands, build artifacts) - Update Testing section to use `uv run pytest` consistently - Add `build` job to CI workflow to verify `uv build` produces valid artifacts Closes #130 ## Test plan - [x] Verified `uv build` produces both `.tar.gz` and `.whl` artifacts locally - [ ] CI build job runs successfully 🤖 Generated with [Claude Code](https://claude.ai/code) ## Summary by CodeRabbit * **Chores** * Enhanced CI/CD pipeline with automated build verification and artifact management. * **Documentation** * Restructured and expanded development and setup instructions for improved clarity and ease of contributor onboarding. ✏️ Tip: You can customize this high-level summary in your review settings. --------- Co-authored-by: Claude Opus 4.5 --- .github/workflows/ci.yml | 27 ++++++++++++++ README.md | 77 +++++++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75925037..256bd8b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,33 @@ jobs: - name: Run checks run: make check + build: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up environment + uses: ./.github/actions/setup-python-env + with: + python-version: '3.11' + + - name: Build package + run: make build + + - name: Verify build artifacts + run: | + ls -la dist/ + # Verify both sdist and wheel are created + test -f dist/*.tar.gz || (echo "Source distribution not found" && exit 1) + test -f dist/*.whl || (echo "Wheel not found" && exit 1) + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + unit-test: runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 5a2a6fb4..dc2edd6f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ To achieve the above design goals, this SDK follows the following design princip 6. [DQL Operations](#5-dql-operations) 7. [Embedding Functions](#6-embedding-functions) 8. [RAG Demo](#rag-demo) -9. [Testing](#testing) +9. [Development](#development) +10. [Testing](#testing) ## Installation @@ -1156,31 +1157,75 @@ The demo supports three embedding modes: For detailed instructions, see [demo/rag/README.md](demo/rag/README.md). -## Testing +## Development + +This project uses [uv](https://docs.astral.sh/uv/) as the package manager with [pdm-backend](https://pdm-backend.fming.dev/) as the build backend. All common development tasks are unified through the `Makefile`. + +### Prerequisites + +Install uv: ```bash -# Run all tests (unit + integration) -python3 -m pytest -v +# macOS/Linux +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Windows +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" -# Run tests with log output -python3 -m pytest -v -s +# Or via pip +pip install uv +``` + +### Setup Development Environment + +```bash +# Clone the repository +git clone https://github.com/oceanbase/pyseekdb.git +cd pyseekdb + +# Install dependencies and pre-commit hooks +make install +``` -# Run unit tests only -python3 -m pytest tests/unit_tests/ -v +### Make Targets + +Run `make help` to see all available targets: + +```bash +make help # Show all available targets +make install # Install dependencies and pre-commit hooks +make check # Run code quality tools (lint, format check) +make test # Run unit tests +make test-integration-embedded # Run embedded integration tests +make build # Build the package +make docs # Build documentation +make clean # Clean build artifacts +``` + +### Build Artifacts + +After running `make build`, the distribution files will be in the `dist/` directory: +- `pyseekdb-.tar.gz` - Source distribution +- `pyseekdb--py3-none-any.whl` - Wheel distribution + +## Testing + +```bash +# Run unit tests +make test -# Run integration tests only -python3 -m pytest tests/integration_tests/ -v +# Run embedded integration tests +make test-integration-embedded -# Run integration tests for specific mode -python3 -m pytest tests/integration_tests/ -v -k "embedded" # embedded mode -python3 -m pytest tests/integration_tests/ -v -k "server" # server mode (requires seekdb server) -python3 -m pytest tests/integration_tests/ -v -k "oceanbase" # oceanbase mode (requires OceanBase) +# Run specific tests with uv run +uv run pytest tests/integration_tests/ -v -k "server" # server mode (requires seekdb server) +uv run pytest tests/integration_tests/ -v -k "oceanbase" # oceanbase mode (requires OceanBase) # Run specific test file -python3 -m pytest tests/integration_tests/test_collection_query.py -v +uv run pytest tests/integration_tests/test_collection_query.py -v # Run specific test function -python3 -m pytest tests/integration_tests/test_collection_query.py::TestCollectionQuery::test_collection_query -v +uv run pytest tests/integration_tests/test_collection_query.py::TestCollectionQuery::test_collection_query -v ``` ## License From 4eef55fbefaca106811967cacd2834fd6276c718 Mon Sep 17 00:00:00 2001 From: taven Date: Sat, 31 Jan 2026 15:44:22 +0800 Subject: [PATCH 06/10] [vibe coding] feat: support multi-version docs (#159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary close #104 ## Solution Description 1. 多版本文档构建系统 - 集成 sphinx-multiversion 扩展,支持为多个分支和标签构建文档 - 配置了版本白名单规则: - 标签格式:v *.*.*(如 v1.0.0, v1.1.0) - 分支:main 和 develop - 自动创建重定向页面,默认跳转到 develop 版本 2. Makefile 新增命令 - `make docs-multiversion`:构建所有版本的文档 - `make docs-serve`:启动支持自动重载的开发服务器 - `make docs`:单版本构建(已更新注释) ## Summary by CodeRabbit * **New Features** * Multi-version documentation build and a local doc server with auto-reload for live previews * Version selector dropdown in the docs UI to switch between documentation versions * **Documentation** * New styling and banners for version-aware documentation and a redirect to the latest version for easy access ✏️ Tip: You can customize this high-level summary in your review settings. --- Makefile | 12 ++++++- docs/_static/custom.css | 66 +++++++++++++++++++++++++++++++++++++ docs/_templates/layout.html | 18 ++++++++++ docs/build_multiversion.sh | 50 ++++++++++++++++++++++++++++ docs/conf.py | 23 +++++++++++++ 5 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 docs/_templates/layout.html create mode 100755 docs/build_multiversion.sh diff --git a/Makefile b/Makefile index 6b6e5ae4..0885a3c6 100644 --- a/Makefile +++ b/Makefile @@ -34,10 +34,20 @@ test-integration-embedded: ## Run embedded integration tests @$(UV) run pytest tests/integration_tests/ -v --log-cli-level=INFO -k embedded .PHONY: docs -docs: ## Build documentation +docs: ## Build documentation (single version) @echo ">> Building documentation" @$(UV) run sphinx-build -b html docs docs/_build/html +.PHONY: docs-multiversion +docs-multiversion: ## Build multi-version documentation + @echo ">> Building multi-version documentation" + @bash docs/build_multiversion.sh + +.PHONY: docs-serve +docs-serve: ## Serve documentation with auto-reload + @echo ">> Starting documentation server" + @$(UV) run sphinx-autobuild docs docs/_build/html --host 127.0.0.1 --port 8000 + .PHONY: build build: ## Build package @echo ">> Building package" diff --git a/docs/_static/custom.css b/docs/_static/custom.css index e69de29b..853148d2 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -0,0 +1,66 @@ +/* 版本选择器下拉框样式 - 放在左侧导航栏搜索框下方 */ +.version-selector-dropdown { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + border-radius: 50px; +} + +.version-selector-dropdown label { + font-size: 13px; + font-weight: 600; + color: #fff; + margin: 0; + white-space: nowrap; +} + +.version-selector-dropdown select { + flex: 1; + padding: 6px 10px; + font-size: 13px; + color: #404040; + background-color: #fff; + border: 1px solid #d0d0d0; + border-radius: 4px; + cursor: pointer; + outline: none; + transition: all 0.2s ease; +} + +.version-selector-dropdown select:hover { + border-color: #2980b9; +} + +.version-selector-dropdown select:focus { + border-color: #2980b9; + box-shadow: 0 0 0 2px rgba(41, 128, 185, 0.1); +} + +/* RTD 主题版本横幅样式 - 隐藏左下角的版本选择器 */ +.rst-versions { + display: none !important; +} + +/* 版本警告横幅 */ +.version-warning { + padding: 10px 15px; + margin-bottom: 20px; + background-color: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 4px; + color: #856404; +} + +.version-warning strong { + font-weight: bold; +} + +.version-warning a { + color: #856404; + text-decoration: underline; +} + +.version-warning a:hover { + color: #533f03; +} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000..2bdb8d8c --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,18 @@ +{% extends "!layout.html" %} + +{# 在左侧导航栏的搜索框下方插入版本选择器 #} +{% block sidebartitle %} + {{ super() }} + {% if versions %} +
+ + +
+ {% endif %} +{% endblock %} diff --git a/docs/build_multiversion.sh b/docs/build_multiversion.sh new file mode 100755 index 00000000..9abd9c90 --- /dev/null +++ b/docs/build_multiversion.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# 构建多版本文档的脚本 + +set -e + +# 颜色输出 +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}开始构建多版本文档...${NC}" + +# 确保在项目根目录 +cd "$(dirname "$0")/.." + +# 激活虚拟环境(如果存在) +if [ -d ".venv" ]; then + echo -e "${GREEN}激活虚拟环境...${NC}" + source .venv/bin/activate +fi + +# 清理旧的构建文件 +echo -e "${GREEN}清理旧的构建文件...${NC}" +rm -rf docs/_build/html + +# 使用 sphinx-multiversion 构建文档 +echo -e "${GREEN}使用 sphinx-multiversion 构建文档...${NC}" +sphinx-multiversion docs docs/_build/html + +# 创建重定向到最新版本的 index.html +echo -e "${GREEN}创建重定向页面...${NC}" +cat > docs/_build/html/index.html << 'EOF' + + + + Redirecting to latest version + + + + + +

Redirecting to latest version...

+ + +EOF + +echo -e "${BLUE}多版本文档构建完成!${NC}" +echo -e "${GREEN}文档位置: docs/_build/html${NC}" +echo -e "${GREEN}可以使用以下命令启动本地服务器查看:${NC}" +echo -e " cd docs/_build/html && python -m http.server 8000" diff --git a/docs/conf.py b/docs/conf.py index 1b1be534..75897341 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,6 +26,7 @@ "sphinx.ext.viewcode", "sphinx.ext.napoleon", "myst_parser", + "sphinx_multiversion", ] # Enable Markdown in docstrings @@ -59,3 +60,25 @@ # HTML theme html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] +html_css_files = [ + "custom.css", +] + +# Sphinx-multiversion configuration +# 配置要包含的分支和标签 +smv_tag_whitelist = r"^v\d+\.\d+\.\d+$" # 匹配 v1.0.0 格式的标签 +smv_branch_whitelist = r"^(main|develop)$" # 包含 main 和 develop 分支 +smv_remote_whitelist = r"^origin$" # 只使用 origin 远程仓库 +smv_released_pattern = r"^refs/tags/.*$" # 标记已发布的版本 + +# 自定义模板路径 +templates_path = ["_templates"] + +# 版本横幅配置 +html_context = { + "display_github": True, + "github_user": "oceanbase", + "github_repo": "pyseekdb", + "github_version": "develop", + "conf_py_path": "/docs/", +} From dc87918ff22a96dc43cf422157326ded9dc4afec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:53:02 +0800 Subject: [PATCH 07/10] Bump protobuf from 6.33.2 to 6.33.5 (#160) Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 6.33.2 to 6.33.5.
Release notes

Sourced from protobuf's releases.

Protocol Buffers v34.0-rc1

Announcements

Bazel

Compiler

C++

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=protobuf&package-manager=uv&previous-version=6.33.2&new-version=6.33.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/oceanbase/pyseekdb/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/uv.lock b/uv.lock index dc335b1d..1ca532d0 100644 --- a/uv.lock +++ b/uv.lock @@ -1051,17 +1051,17 @@ wheels = [ [[package]] name = "protobuf" -version = "6.33.2" +version = "6.33.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" }, - { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" }, - { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" }, - { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" }, - { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" }, - { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] [[package]] From 82f1f4e45504acfdc0051decfd6d122eae97e891 Mon Sep 17 00:00:00 2001 From: Jingchao Date: Sat, 31 Jan 2026 16:14:26 +0800 Subject: [PATCH 08/10] feat: add pyseekdb CLI entry point in pyproject.toml (#161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a **CLI for pyseekdb** to support debugging and managing SeekDB databases/collections (close #55). ### Changes - **CLI 模块** - 新增 `src/pyseekdb/cli/__init__.py`、`src/pyseekdb/cli/main.py`,实现命令行入口与子命令分发。 - **连接方式**(与 spec 一致) - 嵌入式:`--path`(默认当前目录下 `seekdb.db`)。 - 服务端:`--host`、`--port`(默认 2881)、`--tenant`、`--database`、`--user`、`--password`(或环境变量 `SEEKDB_PASSWORD`)。 - **子命令** - **db**:`list` / `create ` / `delete `(数据库管理)。 - **collections**(别名 **coll**):`list` / `create [--dimension N]` / `delete ` / `info [--sample N]`(collection 管理及预览)。 - **sql**:执行原始 SQL(调试用)。 - **query**:按文本对指定 collection 做向量/语义查询(`--text`、`--n`)。 - **get**:按 id 或 limit 拉取 collection 文档(`--ids`、`--limit`)。 - **输出**:全局 `-o table`(默认)或 `-o json`。 - **入口**:在 `pyproject.toml` 中增加 `[project.scripts]`,将 `pyseekdb` 指向 `pyseekdb.cli:main`。 ## Solution Description 针对 [issue #55](https://github.com/oceanbase/pyseekdb/issues/55),基于现有 `Client` / `AdminClient` 和 spec 中的连接与能力说明,实现一个统一的 CLI,便于: - **管理**:列出/创建/删除数据库与 collections,查看 collection 信息与样本数据。 - **调试**:执行任意 SQL、对 collection 做 query/get,支持 table 与 json 两种输出。 CLI 复用 `Client()` 与 `AdminClient()` 的工厂逻辑,支持嵌入式(`--path`)与服务端(`--host`/`--port` 等)两种模式;子命令通过 argparse 组织,无额外依赖。 ## Test plan - 安装后执行 `pyseekdb --help`、`pyseekdb db --help`、`pyseekdb collections --help` 等,确认子命令与参数展示正确。 - 在嵌入式或服务端环境下分别验证:`db list`、`collections list`、`collections info --sample N`、`sql "SHOW TABLES"`、`query --text "..."`、`get --limit N` 及 `-o json` 输出,确认无报错且结果符合预期。 related to https://github.com/oceanbase/pyseekdb/issues/55 ## Summary by CodeRabbit * **New Features** * Packs a command-line interface (CLI) as a public executable entry point. * Adds DB management commands: list, create, delete. * Adds collection commands: list, create, delete, info (with optional sample). * Adds query, document retrieval, and raw SQL execution commands. * Supports embedded/server connection modes, configurable credentials, and output formats (table or JSON). ✏️ Tip: You can customize this high-level summary in your review settings. --- pyproject.toml | 3 + src/pyseekdb/cli/__init__.py | 14 ++ src/pyseekdb/cli/main.py | 400 +++++++++++++++++++++++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 src/pyseekdb/cli/__init__.py create mode 100644 src/pyseekdb/cli/main.py diff --git a/pyproject.toml b/pyproject.toml index 38406b6a..11067c2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,9 @@ ignore = [ [tool.ruff.format] preview = true +[project.scripts] +pyseekdb = "pyseekdb.cli:main" + [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" diff --git a/src/pyseekdb/cli/__init__.py b/src/pyseekdb/cli/__init__.py new file mode 100644 index 00000000..f9062630 --- /dev/null +++ b/src/pyseekdb/cli/__init__.py @@ -0,0 +1,14 @@ +""" +pyseekdb CLI - Debug and manage seekdb collections/databases. + +Usage: + pyseekdb [OPTIONS] db list|create|delete ... + pyseekdb [OPTIONS] collections list|create|delete|info ... + pyseekdb [OPTIONS] sql "SELECT ..." + pyseekdb [OPTIONS] query --text "..." [--n 5] + pyseekdb [OPTIONS] get [--limit 10] [--ids id1,id2] +""" + +from .main import main + +__all__ = ["main"] diff --git a/src/pyseekdb/cli/main.py b/src/pyseekdb/cli/main.py new file mode 100644 index 00000000..39a7b993 --- /dev/null +++ b/src/pyseekdb/cli/main.py @@ -0,0 +1,400 @@ +""" +CLI main: argument parsing, connection factory, and subcommand dispatch. +""" + +import argparse +import json +import os +import sys +from typing import Any + +from .. import AdminClient, Client +from ..client.admin_client import _AdminClientProxy, _ClientProxy +from ..client.configuration import HNSWConfiguration + + +def _add_connection_args(parser: argparse.ArgumentParser) -> None: + """Add global connection options (embedded vs server).""" + g = parser.add_argument_group("connection (choose one)") + g.add_argument( + "--path", + metavar="DIR", + default=None, + help="Embedded mode: path to seekdb data directory (default: seekdb.db in cwd)", + ) + g.add_argument("--host", default=None, help="Server mode: host (e.g. localhost)") + g.add_argument("--port", type=int, default=2881, help="Server mode: port (default: 2881)") + g.add_argument("--tenant", default="sys", help="Server mode: tenant (default: sys)") + g.add_argument("--database", "-d", default="test", help="Database name (default: test)") + g.add_argument("--user", default="root", help="Server mode: user (default: root)") + g.add_argument( + "--password", + "-p", + default=None, + help="Server mode: password (or set SEEKDB_PASSWORD)", + ) + parser.add_argument( + "-o", + "--output", + choices=["table", "json"], + default="table", + help="Output format (default: table)", + ) + + +def _make_client(args: argparse.Namespace) -> _ClientProxy: + """Build Client from parsed args (for collection operations).""" + password = args.password or os.environ.get("SEEKDB_PASSWORD", "") + return Client( + path=args.path, + host=args.host, + port=args.port, + tenant=args.tenant, + database=args.database, + user=args.user, + password=password, + ) + + +def _make_admin(args: argparse.Namespace) -> _AdminClientProxy: + """Build AdminClient from parsed args (for db operations).""" + password = args.password or os.environ.get("SEEKDB_PASSWORD", "") + return AdminClient( + path=args.path, + host=args.host, + port=args.port, + tenant=args.tenant, + user=args.user, + password=password, + ) + + +def _execute_sql(client: _ClientProxy, sql: str) -> Any: + """Run raw SQL using the underlying server (for debug).""" + server = client._server + return server._execute(sql) + + +def _print_table(rows: list[dict[str, Any]] | None, columns: list[str] | None = None) -> None: + """Print rows as a simple text table.""" + if not rows: + print("(0 rows)") + return + if columns is None: + columns = list(rows[0].keys()) if rows else [] + col_widths = [max(len(str(c)), 3) for c in columns] + for r in rows: + for i, c in enumerate(columns): + val = r.get(c, "") + if isinstance(val, (dict, list)): + val = json.dumps(val, ensure_ascii=False)[:40] + col_widths[i] = max(col_widths[i], min(len(str(val)), 50)) + fmt = " ".join(f"%-{w}s" for w in col_widths) + print(fmt % tuple(columns)) + print("-" * (sum(col_widths) + 2 * (len(columns) - 1))) + for r in rows: + cells = [] + for c in columns: + v = r.get(c, "") + if isinstance(v, (dict, list)): + v = json.dumps(v, ensure_ascii=False)[:40] + s = str(v) + if len(s) > 50: + s = s[:47] + "..." + cells.append(s) + print(fmt % tuple(cells)) + + +def _print_json(obj: Any) -> None: + """Print object as JSON.""" + if hasattr(obj, "__iter__") and not isinstance(obj, (str, dict)): + obj = list(obj) + print(json.dumps(obj, indent=2, ensure_ascii=False, default=str)) + + +# ---------- Commands ---------- + + +def cmd_db_list(args: argparse.Namespace) -> int: + """List databases.""" + admin = _make_admin(args) + try: + dbs = admin.list_databases() + rows = [{"name": d.name, "tenant": d.tenant or ""} for d in dbs] + if args.output == "json": + _print_json(rows) + else: + _print_table(rows, ["name", "tenant"]) + return 0 + finally: + admin._server._cleanup() + + +def cmd_db_create(args: argparse.Namespace) -> int: + """Create database.""" + admin = _make_admin(args) + try: + admin.create_database(args.name, tenant=args.tenant) + print(f"Created database: {args.name}") + return 0 + finally: + admin._server._cleanup() + + +def cmd_db_delete(args: argparse.Namespace) -> int: + """Delete database.""" + admin = _make_admin(args) + try: + admin.delete_database(args.name, tenant=args.tenant) + print(f"Deleted database: {args.name}") + return 0 + finally: + admin._server._cleanup() + + +def cmd_collections_list(args: argparse.Namespace) -> int: + """List collections.""" + client = _make_client(args) + try: + colls = client.list_collections() + rows = [ + { + "name": c.name, + "dimension": c.dimension or "", + "distance": c.distance or "", + } + for c in colls + ] + if args.output == "json": + _print_json(rows) + else: + _print_table(rows, ["name", "dimension", "distance"]) + return 0 + finally: + client._server._cleanup() + + +def cmd_collections_create(args: argparse.Namespace) -> int: + """Create collection.""" + client = _make_client(args) + try: + config = HNSWConfiguration(dimension=args.dimension) if args.dimension else None + client.create_collection(args.name, configuration=config) + print(f"Created collection: {args.name}") + return 0 + finally: + client._server._cleanup() + + +def cmd_collections_delete(args: argparse.Namespace) -> int: + """Delete collection.""" + client = _make_client(args) + try: + client.delete_collection(args.name) + print(f"Deleted collection: {args.name}") + return 0 + finally: + client._server._cleanup() + + +def cmd_collections_info(args: argparse.Namespace) -> int: + """Show collection info (schema, count, sample).""" + client = _make_client(args) + try: + if not client.has_collection(args.name): + print(f"Collection not found: {args.name}", file=sys.stderr) + return 1 + coll = client.get_collection(args.name) + info = { + "name": coll.name, + "dimension": coll.dimension, + "distance": coll.distance, + "metadata": coll.metadata, + "count": coll.count(), + } + if args.output == "json": + sample = coll.peek(limit=args.sample) if args.sample and info["count"] > 0 else {} + payload = {"info": info, "sample": sample} + _print_json(payload) + else: + print(f"Name: {info['name']}") + print(f"Dimension: {info['dimension']}") + print(f"Distance: {info['distance']}") + print(f"Count: {info['count']}") + if info.get("metadata"): + print(f"Metadata: {info['metadata']}") + if args.sample and info["count"] > 0: + sample = coll.peek(limit=args.sample) + print("\nSample (peek):") + for i in range(len(sample["ids"])): + print(f" id={sample['ids'][i]}") + if sample.get("documents"): + doc = sample["documents"][i] + print(f" document: {str(doc)[:80]}...") + if sample.get("metadatas"): + print(f" metadata: {sample['metadatas'][i]}") + return 0 + finally: + client._server._cleanup() + + +def cmd_sql(args: argparse.Namespace) -> int: + """Execute raw SQL (debug).""" + client = _make_client(args) + try: + result = _execute_sql(client, args.sql) + if result is None: + print("OK") + return 0 + if args.output == "json": + # result may be list of tuples or list of dicts + if not result: + _print_json([]) + elif hasattr(result[0], "keys"): + _print_json(result) + else: + _print_json([list(r) for r in result]) + else: + if not result: + print("(0 rows)") + elif hasattr(result[0], "keys"): + _print_table(result) + else: + cols = [f"col_{i}" for i in range(len(result[0]))] + _print_table([dict(zip(cols, r, strict=True)) for r in result], cols) + return 0 + finally: + client._server._cleanup() + + +def cmd_query(args: argparse.Namespace) -> int: + """Query collection by text or embedding.""" + client = _make_client(args) + try: + if not client.has_collection(args.collection): + print(f"Collection not found: {args.collection}", file=sys.stderr) + return 1 + coll = client.get_collection(args.collection) + if args.text: + res = coll.query( + query_texts=[args.text], n_results=args.n, include=args.include or ["documents", "metadatas"] + ) + else: + print("Specify --text for query by text (embedding not supported in CLI)", file=sys.stderr) + return 1 + if args.output == "json": + _print_json(res) + else: + ids = res.get("ids", [[]])[0] + docs = res.get("documents", [[]])[0] if res.get("documents") else [] + metas = res.get("metadatas", [[]])[0] if res.get("metadatas") else [] + dists = res.get("distances", [[]])[0] if res.get("distances") else [] + for i in range(len(ids)): + print(f"id={ids[i]} distance={dists[i] if i < len(dists) else ''}") + if i < len(docs): + print(f" document: {str(docs[i])[:80]}...") + if i < len(metas): + print(f" metadata: {metas[i]}") + return 0 + finally: + client._server._cleanup() + + +def cmd_get(args: argparse.Namespace) -> int: + """Get documents from collection by ids or limit.""" + client = _make_client(args) + try: + if not client.has_collection(args.collection): + print(f"Collection not found: {args.collection}", file=sys.stderr) + return 1 + coll = client.get_collection(args.collection) + ids = None + if args.ids: + ids = [x.strip() for x in args.ids.split(",") if x.strip()] + res = coll.get( + ids=ids, + limit=args.limit, + include=args.include or ["documents", "metadatas"], + ) + if args.output == "json": + _print_json(res) + else: + for i in range(len(res.get("ids", []))): + print(f"id={res['ids'][i]}") + if res.get("documents") and i < len(res["documents"]): + print(f" document: {str(res['documents'][i])[:80]}...") + if res.get("metadatas") and i < len(res["metadatas"]): + print(f" metadata: {res['metadatas'][i]}") + return 0 + finally: + client._server._cleanup() + + +def main(argv: list[str] | None = None) -> int: + """CLI entry point.""" + parser = argparse.ArgumentParser( + prog="pyseekdb", + description="seekdb CLI: debug and manage collections/databases (see spec.md).", + ) + _add_connection_args(parser) + sub = parser.add_subparsers(dest="command", required=True, help="command") + + # db + db = sub.add_parser("db", help="Database (admin) operations") + db_sub = db.add_subparsers(dest="db_command", required=True) + db_list = db_sub.add_parser("list", help="List databases") + db_list.set_defaults(func=cmd_db_list) + db_create = db_sub.add_parser("create", help="Create database") + db_create.add_argument("name", help="Database name") + db_create.set_defaults(func=cmd_db_create) + db_delete = db_sub.add_parser("delete", help="Delete database") + db_delete.add_argument("name", help="Database name") + db_delete.set_defaults(func=cmd_db_delete) + + # collections (and alias coll) + def _add_collection_subparsers(parent: argparse.ArgumentParser) -> None: + csub = parent.add_subparsers(dest="coll_command", required=True) + csub.add_parser("list", help="List collections").set_defaults(func=cmd_collections_list) + create = csub.add_parser("create", help="Create collection") + create.add_argument("name", help="Collection name") + create.add_argument("--dimension", type=int, default=None, help="Vector dimension") + create.set_defaults(func=cmd_collections_create) + delete = csub.add_parser("delete", help="Delete collection") + delete.add_argument("name", help="Collection name") + delete.set_defaults(func=cmd_collections_delete) + info = csub.add_parser("info", help="Show collection info and optional sample") + info.add_argument("name", help="Collection name") + info.add_argument("--sample", type=int, default=0, metavar="N", help="Peek first N rows") + info.set_defaults(func=cmd_collections_info) + + _add_collection_subparsers(sub.add_parser("collections", help="Collection operations")) + _add_collection_subparsers(sub.add_parser("coll", help="Alias for collections")) + + # sql + sql_p = sub.add_parser("sql", help="Execute raw SQL (debug)") + _add_connection_args(sql_p) + sql_p.add_argument("sql", help="SQL statement") + sql_p.set_defaults(func=cmd_sql) + + # query + query_p = sub.add_parser("query", help="Query collection by text") + _add_connection_args(query_p) + query_p.add_argument("collection", help="Collection name") + query_p.add_argument( + "--text", "-t", required=True, help="Query text (will be embedded if collection has embedding)" + ) + query_p.add_argument("--n", type=int, default=10, help="Number of results (default: 10)") + query_p.add_argument("--include", nargs="+", default=None, help="Include fields: documents, metadatas, embeddings") + query_p.set_defaults(func=cmd_query) + + # get + get_p = sub.add_parser("get", help="Get documents from collection") + _add_connection_args(get_p) + get_p.add_argument("collection", help="Collection name") + get_p.add_argument("--ids", default=None, help="Comma-separated IDs") + get_p.add_argument("--limit", type=int, default=10, help="Max rows (default: 10)") + get_p.add_argument("--include", nargs="+", default=None, help="Include fields") + get_p.set_defaults(func=cmd_get) + + args = parser.parse_args(argv) + return args.func(args) From 38e58bfe1716a3865f39ec2948a21637f0d17186 Mon Sep 17 00:00:00 2001 From: wangyunlai Date: Mon, 2 Feb 2026 16:23:46 +0800 Subject: [PATCH 09/10] fix ruff check (#162) ## Summary close #137 refactor DefaultEmbeddingFunction. Split it into two classes: OnnxEmbeddingFunction and DefaultEmbeddingFunction. DefaultEmbeddingFunction worked based on OnnxEmbeddingFunction. OnnxEmbeddingFunction can execute model based on ONNX. ## Summary by CodeRabbit * **New Features** * Added a standalone ONNX-based embedding implementation and exposed it as an embedding option. * **Refactor** * Default embedding now delegates embedding work to the new ONNX embedding path for simpler, modular behavior. * **Breaking Change** * Removed several public embedding-related attributes and updated the default embedding constructor signature. * **Tests** * Added unit tests for the ONNX embedding and adjusted test startup path handling. --- src/pyseekdb/client/embedding_function.py | 372 ++--------------- .../utils/embedding_functions/__init__.py | 2 + .../onnx_embedding_function.py | 392 ++++++++++++++++++ tests/integration_tests/conftest.py | 9 +- .../test_onnx_embedding_function.py | 72 ++++ 5 files changed, 495 insertions(+), 352 deletions(-) create mode 100644 src/pyseekdb/utils/embedding_functions/onnx_embedding_function.py create mode 100644 tests/unit_tests/test_onnx_embedding_function.py diff --git a/src/pyseekdb/client/embedding_function.py b/src/pyseekdb/client/embedding_function.py index 5d1d2d86..460e335b 100644 --- a/src/pyseekdb/client/embedding_function.py +++ b/src/pyseekdb/client/embedding_function.py @@ -5,12 +5,9 @@ for converting text documents to vector embeddings. """ -import contextlib import logging -import os +import warnings from abc import abstractmethod -from functools import cached_property -from pathlib import Path from typing import ( Any, ClassVar, @@ -20,15 +17,6 @@ runtime_checkable, ) -import httpx -import numpy as np -import numpy.typing as npt - -# Set Hugging Face mirror endpoint for better download speed in China -# Users can override this by setting HF_ENDPOINT environment variable -if "HF_ENDPOINT" not in os.environ: - os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" - logger = logging.getLogger(__name__) # Type variable for input types @@ -152,11 +140,8 @@ class DefaultEmbeddingFunction(EmbeddingFunction[Documents]): >>> print(len(embeddings[0])) # 384 """ - MODEL_NAME = "all-MiniLM-L6-v2" - HF_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2" # Hugging Face model ID - DOWNLOAD_PATH = Path.home() / ".cache" / "pyseekdb" / "onnx_models" / MODEL_NAME - EXTRACTED_FOLDER_NAME = "onnx" - ARCHIVE_FILENAME = "onnx.tar.gz" + _MODEL_NAME = "all-MiniLM-L6-v2" + _HF_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2" # Hugging Face model ID _DIMENSION = 384 # all-MiniLM-L6-v2 produces 384-dimensional embeddings def __init__( @@ -168,344 +153,35 @@ def __init__( Initialize the default embedding function. Args: - model_name: Name of the model (currently only 'all-MiniLM-L6-v2' is supported). - Default is 'all-MiniLM-L6-v2' (384 dimensions). - preferred_providers: The preferred ONNX runtime providers. - Defaults to None (uses available providers). + model_name: str = "all-MiniLM-L6-v2", # Deprecated. Will be removed in a future version. + preferred_providers: list[str] | None = None, # Deprecated. Will be removed in a future version. + # The preferred ONNX runtime providers. Defaults to None (uses available providers). """ - if model_name != "all-MiniLM-L6-v2": - raise ValueError(f"Currently only 'all-MiniLM-L6-v2' is supported, got '{model_name}'") - self.model_name = model_name - - # Validate preferred_providers - if preferred_providers and not all(isinstance(i, str) for i in preferred_providers): - raise ValueError("Preferred providers must be a list of strings") - if preferred_providers and len(preferred_providers) != len(set(preferred_providers)): - raise ValueError("Preferred providers must be unique") - - self._preferred_providers = preferred_providers - - # Import required modules - import onnxruntime as ort_module - import tokenizers - import tqdm + if model_name != self._MODEL_NAME: + raise ValueError(f"Currently only '{self._MODEL_NAME}' is supported, got '{model_name}'") + if preferred_providers: + warnings.warn( + "preferred_providers is deprecated and will be removed in a future version. " + "Use the preferred_providers argument of OnnxEmbeddingFunction instead.", + DeprecationWarning, + stacklevel=2, + ) + self.model_name = self._MODEL_NAME + from pyseekdb.utils.embedding_functions import OnnxEmbeddingFunction - self.ort = ort_module - self.tokenizers = tokenizers # Store the module - self.tqdm = tqdm.tqdm + self._onnx = OnnxEmbeddingFunction( + model_name=self._MODEL_NAME, + hf_model_id=self._HF_MODEL_ID, + dimension=self._DIMENSION, + ) @property def dimension(self) -> int: - """Get the dimension of embeddings produced by this function""" + """Get the dimension of embeddings produced by this function.""" return self._DIMENSION - def _download(self, url: str, fname: str, chunk_size: int = 8192) -> None: - """ - Download a file from the URL and save it to the file path. - - Args: - url: The URL to download the file from. - fname: The path to save the file to. - chunk_size: The chunk size to use when downloading (default: 8192 for better speed). - """ - logger.info(f"Downloading from {url}") - # Use Client to ensure correct handling of redirects - with httpx.Client(timeout=600.0, follow_redirects=True) as client, client.stream("GET", url) as resp: - resp.raise_for_status() - total = int(resp.headers.get("content-length", 0)) - with ( - open(fname, "wb") as file, - self.tqdm( - desc=os.path.basename(fname), - total=total, - unit="iB", - unit_scale=True, - unit_divisor=1024, - ) as bar, - ): - for data in resp.iter_bytes(chunk_size=chunk_size): - size = file.write(data) - bar.update(size) - - def _get_hf_endpoint(self) -> str: - """Get Hugging Face endpoint URL, using HF_ENDPOINT environment variable if set.""" - return os.environ.get("HF_ENDPOINT", "https://huggingface.co") - - def _download_from_huggingface(self) -> bool: # noqa: C901 - """ - Download model files from Hugging Face (supports mirror acceleration). - - Returns: - True if download successful, False otherwise. - """ - try: - hf_endpoint = self._get_hf_endpoint() - # Remove trailing slash - hf_endpoint = hf_endpoint.rstrip("/") - - # List of files to download - # ONNX model files are in the onnx/ subdirectory, other files in the root directory - files_to_download = { - "onnx/model.onnx": "model.onnx", # ONNX file in onnx subdirectory - "tokenizer.json": "tokenizer.json", - "config.json": "config.json", - "special_tokens_map.json": "special_tokens_map.json", - "tokenizer_config.json": "tokenizer_config.json", - "vocab.txt": "vocab.txt", - } - - extracted_folder = os.path.join(self.DOWNLOAD_PATH, self.EXTRACTED_FOLDER_NAME) - os.makedirs(extracted_folder, exist_ok=True) - - logger.info(f"Downloading model from Hugging Face (endpoint: {hf_endpoint})") - - # Download each file - for hf_filename, local_filename in files_to_download.items(): - local_path = os.path.join(extracted_folder, local_filename) - - # Skip if file already exists - if os.path.exists(local_path): - continue - - # Construct Hugging Face download URL - # Format: https://hf-mirror.com/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx - # Or: https://hf-mirror.com/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json - url = f"{hf_endpoint}/{self.HF_MODEL_ID}/resolve/main/{hf_filename}" - - try: - # First check if file exists (HEAD request) - with contextlib.suppress(Exception): - head_resp = httpx.head(url, timeout=10.0, follow_redirects=True) - if head_resp.status_code == 404: - logger.warning(f"File {hf_filename} not found on Hugging Face (404), will try fallback") - return False - - self._download(url, local_path, chunk_size=8192) - logger.info(f"Successfully downloaded {local_filename}") - except httpx.HTTPStatusError as e: - if e.response.status_code == 404: - logger.warning(f"File {hf_filename} not found on Hugging Face (404), will try fallback") - return False - logger.warning(f"HTTP error downloading {hf_filename} from Hugging Face: {e}") - if os.path.exists(local_path): - os.remove(local_path) - return False - except Exception as e: - logger.warning(f"Failed to download {hf_filename} from Hugging Face: {e}") - # If download fails, try to delete partially downloaded file - if os.path.exists(local_path): - os.remove(local_path) - return False - - # Verify critical files exist - if not os.path.exists(os.path.join(extracted_folder, "model.onnx")): - logger.error("model.onnx not found after download") - return False - if not os.path.exists(os.path.join(extracted_folder, "tokenizer.json")): - logger.error("tokenizer.json not found after download") - return False - - logger.info("Successfully downloaded all model files from Hugging Face") - return True # noqa: TRY300 - - except Exception: - logger.exception("Error downloading from Hugging Face") - return False - - def _forward(self, documents: list[str], batch_size: int = 32) -> npt.NDArray[np.float32]: - """ - Generate embeddings for a list of documents. - - Args: - documents: The documents to generate embeddings for. - batch_size: The batch size to use when generating embeddings. - - Returns: - The embeddings for the documents. - """ - all_embeddings = [] - for i in range(0, len(documents), batch_size): - batch = documents[i : i + batch_size] - - # Encode each document separately - encoded = [self.tokenizer.encode(d) for d in batch] - - # Check if any document exceeds the max tokens - for doc_tokens in encoded: - if len(doc_tokens.ids) > self.max_tokens(): - raise ValueError( - f"Document length {len(doc_tokens.ids)} is greater than the max tokens {self.max_tokens()}" - ) - - # Create input arrays exactly like the working standalone script - # Create input arrays, ensuring int64 type - input_ids = np.array([e.ids for e in encoded], dtype=np.int64) - attention_mask = np.array([e.attention_mask for e in encoded], dtype=np.int64) - - # Ensure 2D arrays (batch_size, seq_length) - if input_ids.ndim == 1: - input_ids = input_ids.reshape(1, -1) - if attention_mask.ndim == 1: - attention_mask = attention_mask.reshape(1, -1) - - # Use zeros_like to create token_type_ids, ensuring exact shape match - token_type_ids = np.zeros_like(input_ids, dtype=np.int64) - - # Ensure all arrays are contiguous, which is important for onnxruntime 1.19.0 - input_ids = np.ascontiguousarray(input_ids, dtype=np.int64) - attention_mask = np.ascontiguousarray(attention_mask, dtype=np.int64) - token_type_ids = np.ascontiguousarray(token_type_ids, dtype=np.int64) - - onnx_input = { - "input_ids": input_ids, - "attention_mask": attention_mask, - "token_type_ids": token_type_ids, - } - - model_output = self.model.run(None, onnx_input) - last_hidden_state = model_output[0] - - # Mean pooling (exactly as in the code) - # Note: attention_mask needs to be converted to float type for floating point operations - attention_mask_float = attention_mask.astype(np.float32) - input_mask_expanded = np.broadcast_to(np.expand_dims(attention_mask_float, -1), last_hidden_state.shape) - embeddings = np.sum(last_hidden_state * input_mask_expanded, 1) / np.clip( - input_mask_expanded.sum(1), a_min=1e-9, a_max=None - ) - - embeddings = embeddings.astype(np.float32) - all_embeddings.append(embeddings) - - return np.concatenate(all_embeddings) - - @cached_property - def tokenizer(self) -> Any: - """ - Get the tokenizer for the model. - - Returns: - The tokenizer for the model. - """ - tokenizer = self.tokenizers.Tokenizer.from_file( - os.path.join(self.DOWNLOAD_PATH, self.EXTRACTED_FOLDER_NAME, "tokenizer.json") - ) - # max_seq_length = 256, for some reason sentence-transformers uses 256 - # even though the HF config has a max length of 128 - tokenizer.enable_truncation(max_length=256) - tokenizer.enable_padding(pad_id=0, pad_token="[PAD]", length=256) # noqa: S106 - return tokenizer - - @cached_property - def model(self) -> Any: - """ - Get the model. - - Returns: - The model. - """ - if self._preferred_providers is None or len(self._preferred_providers) == 0: - if len(self.ort.get_available_providers()) > 0: - logger.debug( - f"WARNING: No ONNX providers provided, defaulting to available providers: " - f"{self.ort.get_available_providers()}" - ) - self._preferred_providers = self.ort.get_available_providers() - elif not set(self._preferred_providers).issubset(set(self.ort.get_available_providers())): - raise ValueError( - f"Preferred providers must be subset of available providers: {self.ort.get_available_providers()}" - ) - - # Create minimal session options to avoid issues - so = self.ort.SessionOptions() - so.log_severity_level = 3 - # Disable all optimizations that might cause issues - so.graph_optimization_level = self.ort.GraphOptimizationLevel.ORT_DISABLE_ALL - so.execution_mode = self.ort.ExecutionMode.ORT_SEQUENTIAL - so.inter_op_num_threads = 1 - so.intra_op_num_threads = 1 - - if self._preferred_providers and "CoreMLExecutionProvider" in self._preferred_providers: - # remove CoreMLExecutionProvider from the list, it is not as well optimized as CPU. - self._preferred_providers.remove("CoreMLExecutionProvider") - - return self.ort.InferenceSession( - os.path.join(self.DOWNLOAD_PATH, self.EXTRACTED_FOLDER_NAME, "model.onnx"), - # Force CPU execution provider to avoid provider issues - providers=["CPUExecutionProvider"], - sess_options=so, - ) - - def _download_model_if_not_exists(self) -> None: - """ - Download from Hugging Face with image mirror if the model doesn't exist. - """ - onnx_files = [ - "config.json", - "model.onnx", - "special_tokens_map.json", - "tokenizer_config.json", - "tokenizer.json", - "vocab.txt", - ] - extracted_folder = os.path.join(self.DOWNLOAD_PATH, self.EXTRACTED_FOLDER_NAME) - onnx_files_exist = True - for f in onnx_files: - if not os.path.exists(os.path.join(extracted_folder, f)): - onnx_files_exist = False - break - - # Model is not downloaded yet - if not onnx_files_exist: - os.makedirs(self.DOWNLOAD_PATH, exist_ok=True) - - logger.info("Attempting to download model from Hugging Face...") - hf_endpoint = self._get_hf_endpoint() - if not self._download_from_huggingface(): - raise RuntimeError( - f"Failed to download model from Hugging Face (endpoint: {hf_endpoint}). " - f"Please check your network connection or set HF_ENDPOINT environment variable " - f"to use a mirror site (e.g., export HF_ENDPOINT='https://hf-mirror.com'). " - f"Model ID: {self.HF_MODEL_ID}" - ) - logger.info("Model downloaded successfully from Hugging Face") - - def max_tokens(self) -> int: - """Get the maximum number of tokens supported by the model.""" - return 256 - def __call__(self, documents: Documents) -> Embeddings: - """ - Generate embeddings for the given documents. - - Args: - documents: Single document (str) or list of documents (List[str]) - - Returns: - List of embedding vectors - - Example: - >>> ef = DefaultEmbeddingFunction() - >>> # Single document - >>> embedding = ef("Hello world") - >>> # Multiple documents - >>> embeddings = ef(["Hello", "World"]) - """ - # Handle single string input - if isinstance(documents, str): - documents = [documents] - - # Handle empty input - if not documents: - return [] - - # Only download the model when it is actually used - self._download_model_if_not_exists() - - # Generate embeddings - embeddings = self._forward(documents) - - # Convert numpy arrays to lists - return [embedding.tolist() for embedding in embeddings] + return self._onnx(documents) @staticmethod def name() -> str: diff --git a/src/pyseekdb/utils/embedding_functions/__init__.py b/src/pyseekdb/utils/embedding_functions/__init__.py index eace5d2d..de32a65b 100644 --- a/src/pyseekdb/utils/embedding_functions/__init__.py +++ b/src/pyseekdb/utils/embedding_functions/__init__.py @@ -13,6 +13,7 @@ from .mistral_embedding_function import MistralEmbeddingFunction from .morph_embedding_function import MorphEmbeddingFunction from .ollama_embedding_function import OllamaEmbeddingFunction +from .onnx_embedding_function import OnnxEmbeddingFunction from .openai_base_embedding_function import OpenAIBaseEmbeddingFunction from .openai_embedding_function import OpenAIEmbeddingFunction from .qwen_embedding_function import QwenEmbeddingFunction @@ -32,6 +33,7 @@ "MistralEmbeddingFunction", "MorphEmbeddingFunction", "OllamaEmbeddingFunction", + "OnnxEmbeddingFunction", "OpenAIBaseEmbeddingFunction", "OpenAIEmbeddingFunction", "QwenEmbeddingFunction", diff --git a/src/pyseekdb/utils/embedding_functions/onnx_embedding_function.py b/src/pyseekdb/utils/embedding_functions/onnx_embedding_function.py new file mode 100644 index 00000000..8328cf0a --- /dev/null +++ b/src/pyseekdb/utils/embedding_functions/onnx_embedding_function.py @@ -0,0 +1,392 @@ +""" +ONNX-based embedding function implementation. + +This module provides a generic ONNX embedding function that can run +sentence-transformer style models via onnxruntime. +""" + +import contextlib +import logging +import os +from functools import cached_property +from pathlib import Path +from typing import Any + +import httpx +import numpy as np +import numpy.typing as npt + +logger = logging.getLogger(__name__) + +Documents = str | list[str] +Embeddings = list[list[float]] + + +class OnnxEmbeddingFunction: + """ + Generic ONNX runtime embedding function. + + This class handles model download, tokenizer/model loading, and embedding + generation using onnxruntime. + """ + + EXTRACTED_FOLDER_NAME = "onnx" + ARCHIVE_FILENAME = "onnx.tar.gz" + + def __init__( + self, + model_name: str, + hf_model_id: str, + dimension: int, + download_path: Path | None = None, + preferred_providers: list[str] | None = None, + ): + """ + Initialize an ONNX embedding function. + + Args: + model_name: Name of the model (used for cache directory naming). + hf_model_id: Hugging Face model ID. + dimension: Output embedding dimension. + download_path: Optional cache path override. + preferred_providers: Preferred ONNX runtime providers. + """ + if not model_name: + raise ValueError("model_name must be a non-empty string") + if not hf_model_id: + raise ValueError("hf_model_id must be a non-empty string") + if dimension <= 0: + raise ValueError("dimension must be a positive integer") + + self.model_name = model_name + self.hf_model_id = hf_model_id + self._dimension = dimension + self.download_path = ( + download_path + if download_path is not None + else Path.home() / ".cache" / "pyseekdb" / "onnx_models" / model_name + ) + + # Validate preferred_providers + if preferred_providers and not all(isinstance(i, str) for i in preferred_providers): + raise ValueError("Preferred providers must be a list of strings") + if preferred_providers and len(preferred_providers) != len(set(preferred_providers)): + raise ValueError("Preferred providers must be unique") + + self._preferred_providers = preferred_providers + + # Import required modules lazily to avoid hard dependencies at import time + import onnxruntime as ort_module + import tokenizers + import tqdm + + self.ort = ort_module + self.tokenizers = tokenizers # Store the module + self.tqdm = tqdm.tqdm + + @property + def dimension(self) -> int: + """Get the dimension of embeddings produced by this function.""" + return self._dimension + + def _download(self, url: str, fname: str, chunk_size: int = 8192) -> None: + """ + Download a file from the URL and save it to the file path. + + Args: + url: The URL to download the file from. + fname: The path to save the file to. + chunk_size: The chunk size to use when downloading. + """ + logger.info(f"Downloading from {url}") + # Use Client to ensure correct handling of redirects + with httpx.Client(timeout=600.0, follow_redirects=True) as client, client.stream("GET", url) as resp: + resp.raise_for_status() + total = int(resp.headers.get("content-length", 0)) + with ( + open(fname, "wb") as file, + self.tqdm( + desc=os.path.basename(fname), + total=total, + unit="iB", + unit_scale=True, + unit_divisor=1024, + ) as bar, + ): + for data in resp.iter_bytes(chunk_size=chunk_size): + size = file.write(data) + bar.update(size) + + def _get_hf_endpoint(self) -> str: + """Get Hugging Face endpoint URL, using HF_ENDPOINT environment variable if set.""" + return os.environ.get("HF_ENDPOINT", "https://hf-mirror.com") + + def _download_from_huggingface(self) -> bool: # noqa: C901 + """ + Download model files from Hugging Face (supports mirror acceleration). + + Returns: + True if download successful, False otherwise. + """ + try: + hf_endpoint = self._get_hf_endpoint() + # Remove trailing slash + hf_endpoint = hf_endpoint.rstrip("/") + + # List of files to download + # ONNX model files are in the onnx/ subdirectory, other files in the root directory + files_to_download = { + "onnx/model.onnx": "model.onnx", # ONNX file in onnx subdirectory + "tokenizer.json": "tokenizer.json", + "config.json": "config.json", + "special_tokens_map.json": "special_tokens_map.json", + "tokenizer_config.json": "tokenizer_config.json", + "vocab.txt": "vocab.txt", + } + + extracted_folder = os.path.join(self.download_path, self.EXTRACTED_FOLDER_NAME) + os.makedirs(extracted_folder, exist_ok=True) + + logger.info(f"Downloading model from Hugging Face (endpoint: {hf_endpoint})") + + # Download each file + for hf_filename, local_filename in files_to_download.items(): + local_path = os.path.join(extracted_folder, local_filename) + + # Skip if file already exists + if os.path.exists(local_path): + continue + + # Construct Hugging Face download URL + url = f"{hf_endpoint}/{self.hf_model_id}/resolve/main/{hf_filename}" + + try: + # First check if file exists (HEAD request) + with contextlib.suppress(Exception): + head_resp = httpx.head(url, timeout=10.0, follow_redirects=True) + if head_resp.status_code == 404: + logger.warning(f"File {hf_filename} not found on Hugging Face (404), will try fallback") + return False + + self._download(url, local_path, chunk_size=8192) + logger.info(f"Successfully downloaded {local_filename}") + except httpx.HTTPStatusError as e: + if e.response.status_code == 404: + logger.warning(f"File {hf_filename} not found on Hugging Face (404), will try fallback") + return False + logger.warning(f"HTTP error downloading {hf_filename} from Hugging Face: {e}") + if os.path.exists(local_path): + os.remove(local_path) + return False + except Exception as e: + logger.warning(f"Failed to download {hf_filename} from Hugging Face: {e}") + # If download fails, try to delete partially downloaded file + if os.path.exists(local_path): + os.remove(local_path) + return False + + # Verify critical files exist + if not os.path.exists(os.path.join(extracted_folder, "model.onnx")): + logger.error("model.onnx not found after download") + return False + if not os.path.exists(os.path.join(extracted_folder, "tokenizer.json")): + logger.error("tokenizer.json not found after download") + return False + + logger.info("Successfully downloaded all model files from Hugging Face") + return True # noqa: TRY300 + + except Exception: + logger.exception("Error downloading from Hugging Face") + return False + + def _forward(self, documents: list[str], batch_size: int = 32) -> npt.NDArray[np.float32]: + """ + Generate embeddings for a list of documents. + + Args: + documents: The documents to generate embeddings for. + batch_size: The batch size to use when generating embeddings. + + Returns: + The embeddings for the documents. + """ + all_embeddings = [] + for i in range(0, len(documents), batch_size): + batch = documents[i : i + batch_size] + + # Encode each document separately + encoded = [self.tokenizer.encode(d) for d in batch] + + # Check if any document exceeds the max tokens + for doc_tokens in encoded: + if len(doc_tokens.ids) > self.max_tokens(): + raise ValueError( + f"Document length {len(doc_tokens.ids)} is greater than the max tokens {self.max_tokens()}" + ) + + # Create input arrays exactly like the working standalone script + # Create input arrays, ensuring int64 type + input_ids = np.array([e.ids for e in encoded], dtype=np.int64) + attention_mask = np.array([e.attention_mask for e in encoded], dtype=np.int64) + + # Ensure 2D arrays (batch_size, seq_length) + if input_ids.ndim == 1: + input_ids = input_ids.reshape(1, -1) + if attention_mask.ndim == 1: + attention_mask = attention_mask.reshape(1, -1) + + # Use zeros_like to create token_type_ids, ensuring exact shape match + token_type_ids = np.zeros_like(input_ids, dtype=np.int64) + + # Ensure all arrays are contiguous, which is important for onnxruntime 1.19.0 + input_ids = np.ascontiguousarray(input_ids, dtype=np.int64) + attention_mask = np.ascontiguousarray(attention_mask, dtype=np.int64) + token_type_ids = np.ascontiguousarray(token_type_ids, dtype=np.int64) + + onnx_input = { + "input_ids": input_ids, + "attention_mask": attention_mask, + "token_type_ids": token_type_ids, + } + + model_output = self.model.run(None, onnx_input) + last_hidden_state = model_output[0] + + # Mean pooling (exactly as in the code) + # Note: attention_mask needs to be converted to float type for floating point operations + attention_mask_float = attention_mask.astype(np.float32) + input_mask_expanded = np.broadcast_to(np.expand_dims(attention_mask_float, -1), last_hidden_state.shape) + embeddings = np.sum(last_hidden_state * input_mask_expanded, 1) / np.clip( + input_mask_expanded.sum(1), a_min=1e-9, a_max=None + ) + + embeddings = embeddings.astype(np.float32) + all_embeddings.append(embeddings) + + return np.concatenate(all_embeddings) + + @cached_property + def tokenizer(self) -> Any: + """ + Get the tokenizer for the model. + + Returns: + The tokenizer for the model. + """ + tokenizer = self.tokenizers.Tokenizer.from_file( + os.path.join(self.download_path, self.EXTRACTED_FOLDER_NAME, "tokenizer.json") + ) + # max_seq_length = 256, for some reason sentence-transformers uses 256 + # even though the HF config has a max length of 128 + tokenizer.enable_truncation(max_length=256) + tokenizer.enable_padding(pad_id=0, pad_token="[PAD]", length=256) # noqa: S106 + return tokenizer + + @cached_property + def model(self) -> Any: + """ + Get the model. + + Returns: + The model. + """ + if self._preferred_providers is None or len(self._preferred_providers) == 0: + if len(self.ort.get_available_providers()) > 0: + logger.debug( + f"WARNING: No ONNX providers provided, defaulting to available providers: " + f"{self.ort.get_available_providers()}" + ) + self._preferred_providers = self.ort.get_available_providers() + elif not set(self._preferred_providers).issubset(set(self.ort.get_available_providers())): + raise ValueError( + f"Preferred providers must be subset of available providers: {self.ort.get_available_providers()}" + ) + + # Create minimal session options to avoid issues + so = self.ort.SessionOptions() + so.log_severity_level = 3 + # Disable all optimizations that might cause issues + so.graph_optimization_level = self.ort.GraphOptimizationLevel.ORT_DISABLE_ALL + so.execution_mode = self.ort.ExecutionMode.ORT_SEQUENTIAL + so.inter_op_num_threads = 1 + so.intra_op_num_threads = 1 + + if self._preferred_providers and "CoreMLExecutionProvider" in self._preferred_providers: + # remove CoreMLExecutionProvider from the list, it is not as well optimized as CPU. + self._preferred_providers.remove("CoreMLExecutionProvider") + + return self.ort.InferenceSession( + os.path.join(self.download_path, self.EXTRACTED_FOLDER_NAME, "model.onnx"), + # Force CPU execution provider to avoid provider issues + providers=["CPUExecutionProvider"], + sess_options=so, + ) + + def _download_model_if_not_exists(self) -> None: + """ + Download from Hugging Face with image mirror if the model doesn't exist. + """ + onnx_files = [ + "config.json", + "model.onnx", + "special_tokens_map.json", + "tokenizer_config.json", + "tokenizer.json", + "vocab.txt", + ] + extracted_folder = os.path.join(self.download_path, self.EXTRACTED_FOLDER_NAME) + onnx_files_exist = True + for f in onnx_files: + if not os.path.exists(os.path.join(extracted_folder, f)): + onnx_files_exist = False + break + + # Model is not downloaded yet + if not onnx_files_exist: + os.makedirs(self.download_path, exist_ok=True) + + logger.info("Attempting to download model from Hugging Face...") + hf_endpoint = self._get_hf_endpoint() + if not self._download_from_huggingface(): + raise RuntimeError( + f"Failed to download model from Hugging Face (endpoint: {hf_endpoint}). " + f"Please check your network connection or set HF_ENDPOINT environment variable " + f"to use a mirror site (e.g., export HF_ENDPOINT='https://hf-mirror.com'). " + f"Model ID: {self.hf_model_id}" + ) + logger.info("Model downloaded successfully from Hugging Face") + + def max_tokens(self) -> int: + """Get the maximum number of tokens supported by the model.""" + return 256 + + def __call__(self, documents: Documents) -> Embeddings: + """ + Generate embeddings for the given documents. + + Args: + documents: Single document (str) or list of documents (List[str]) + + Returns: + List of embedding vectors + """ + # Handle single string input + if isinstance(documents, str): + documents = [documents] + + # Handle empty input + if not documents: + return [] + + # Only download the model when it is actually used + self._download_model_if_not_exists() + + # Generate embeddings + embeddings = self._forward(documents) + + # Convert numpy arrays to lists + return [embedding.tolist() for embedding in embeddings] + + def __repr__(self) -> str: + return f"OnnxEmbeddingFunction(model_name='{self.model_name}')" diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 3739e7dc..257c24df 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -10,15 +10,16 @@ import pytest -# Add project path -project_root = Path(__file__).parent.parent -sys.path.insert(0, str(project_root)) +# Add project path (repo root + src) +repo_root = Path(__file__).resolve().parents[2] +src_root = repo_root / "src" +sys.path.insert(0, str(src_root)) import pyseekdb # noqa: E402 # ==================== Environment Variable Configuration ==================== # Embedded mode -SEEKDB_PATH = os.environ.get("SEEKDB_PATH", os.path.join(project_root, "seekdb.db")) +SEEKDB_PATH = os.environ.get("SEEKDB_PATH", os.path.join(repo_root, "seekdb.db")) SEEKDB_DATABASE = os.environ.get("SEEKDB_DATABASE", "test") # Server mode diff --git a/tests/unit_tests/test_onnx_embedding_function.py b/tests/unit_tests/test_onnx_embedding_function.py new file mode 100644 index 00000000..4f34b339 --- /dev/null +++ b/tests/unit_tests/test_onnx_embedding_function.py @@ -0,0 +1,72 @@ +""" +Unit tests for OnnxEmbeddingFunction. +""" + +from __future__ import annotations + +import pytest + +from pyseekdb.utils.embedding_functions.onnx_embedding_function import OnnxEmbeddingFunction + + +def _make_onnx() -> OnnxEmbeddingFunction: + return OnnxEmbeddingFunction( + model_name="all-MiniLM-L6-v2", + hf_model_id="sentence-transformers/all-MiniLM-L6-v2", + dimension=384, + preferred_providers=None, + ) + + +def test_init_validates_model_name() -> None: + with pytest.raises(ValueError, match="model_name must be a non-empty string"): + OnnxEmbeddingFunction(model_name="", hf_model_id="org/test", dimension=3) + + +def test_init_validates_hf_model_id() -> None: + with pytest.raises(ValueError, match="hf_model_id must be a non-empty string"): + OnnxEmbeddingFunction(model_name="test", hf_model_id="", dimension=3) + + +def test_init_validates_dimension() -> None: + with pytest.raises(ValueError, match="dimension must be a positive integer"): + OnnxEmbeddingFunction(model_name="test", hf_model_id="org/test", dimension=0) + + +def test_init_validates_preferred_providers() -> None: + with pytest.raises(ValueError, match="Preferred providers must be a list of strings"): + OnnxEmbeddingFunction(model_name="test", hf_model_id="org/test", dimension=3, preferred_providers=[1]) # type: ignore[list-item] + with pytest.raises(ValueError, match="Preferred providers must be unique"): + OnnxEmbeddingFunction( + model_name="test", + hf_model_id="org/test", + dimension=3, + preferred_providers=["CPUExecutionProvider", "CPUExecutionProvider"], + ) + + +def test_dimension_property() -> None: + ef = _make_onnx() + assert ef.dimension == 384 + + +def test_call_empty_returns_empty() -> None: + ef = _make_onnx() + assert ef([]) == [] + + +def test_call_generates_embeddings() -> None: + pytest.importorskip("onnxruntime") + pytest.importorskip("tokenizers") + pytest.importorskip("tqdm") + + ef = _make_onnx() + embeddings = ef("hello world") + + assert isinstance(embeddings, list) + assert len(embeddings) == 1 + assert len(embeddings[0]) == ef.dimension + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"]) From fab366b4201fc56c3779c93bbe294840cb23296d Mon Sep 17 00:00:00 2001 From: wangyunlai Date: Mon, 2 Feb 2026 19:57:31 +0800 Subject: [PATCH 10/10] support python3.14 by using sentence-transformers (#163) ## Summary close #70 Pyseekdb depends on onnxruntime to execute default embedding function. But onnxruntime doesn't support python3.14 yet and we waited it too long. ## Solution Description We use Sentence Transformers to execute default embedding model on python3.14. ## Summary by CodeRabbit * **New Features** * Broadened Python support to include newer 3.14+ runtimes with an optimized embedding backend on those versions * **Chores** * CI updated to run tests across multiple Python versions and test modes with more robust pass/fail checks * Dependency declarations adjusted for broader Python compatibility and conditional packages * Reduced routine log verbosity for successful client operations; improved connection-close log details * Deferred network library imports to improve import-time behavior * **Tests** * Added and refactored tests to validate embedding behavior across Python versions and simplified integration flows --- .github/workflows/ci.yml | 15 +- pyproject.toml | 11 +- src/pyseekdb/__init__.py | 5 +- src/pyseekdb/client/client_base.py | 18 +- src/pyseekdb/client/client_seekdb_embedded.py | 2 +- src/pyseekdb/client/embedding_function.py | 23 +- .../onnx_embedding_function.py | 4 +- ...llection_hybrid_search_source_inference.py | 66 +--- .../test_default_embedding_function.py | 17 + .../test_onnx_embedding_function.py | 112 +++--- uv.lock | 337 +++++++++++++++++- 11 files changed, 446 insertions(+), 164 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 256bd8b0..4868c2e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,10 @@ jobs: unit-test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.14'] steps: - name: Check out code uses: actions/checkout@v4 @@ -70,7 +74,7 @@ jobs: - name: Set up environment uses: ./.github/actions/setup-python-env with: - python-version: '3.11' + python-version: ${{ matrix.python-version }} - name: Install depencies for unit tests run: | @@ -83,8 +87,9 @@ jobs: else log_level=INFO fi + set -o pipefail uv run pytest tests/unit_tests/ -v --log-cli-level=${log_level} | tee pytest.log - tail -n 1 pytest.log | grep -q 'failed' && exit 1 || exit 0 + tail -n 1 pytest.log | grep '=======' | grep 'passed' |grep -q 'failed' && exit 1 || exit 0 - name: Run integration tests without any test mode run: | @@ -93,8 +98,9 @@ jobs: else log_level=INFO fi + set -o pipefail uv run pytest tests/integration_tests/ -v --log-cli-level=${log_level} -k "not server and not embedded and not oceanbase" | tee pytest.log - tail -n 1 pytest.log | grep -q 'failed' && exit 1 || exit 0 + tail -n 1 pytest.log | grep '=======' | grep 'passed' | grep -q 'failed' && exit 1 || exit 0 integration-test: runs-on: ubuntu-latest @@ -155,5 +161,6 @@ jobs: else log_level=INFO fi + set -o pipefail uv run pytest tests/integration_tests/ -v --log-cli-level=${log_level} -k "${{ matrix.test_mode }}" | tee pytest.log - tail -n 1 pytest.log | grep -q 'failed' && exit 1 || exit 0 + tail -n 1 pytest.log | grep '=======' | grep 'passed' | grep -q 'failed' && exit 1 || exit 0 diff --git a/pyproject.toml b/pyproject.toml index 11067c2b..f519f93d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,17 +5,18 @@ description = "A unified Python client for seekdb that supports embedded, server readme = "README.md" license = { text = "Apache-2.0" } authors = [{ name = "OceanBase", email = "open_oceanbase@oceanbase.com" }] -requires-python = ">=3.11,<3.14" +requires-python = ">=3.11,<4" classifiers = [ "Development Status :: 4 - Beta", ] dependencies = [ "pymysql>=1.1.1", "pylibseekdb; sys_platform == \"linux\"", - "onnxruntime>=1.19.0", - "tokenizers>=0.15.0", - "httpx", - "tqdm", + "onnxruntime>=1.19.0; python_version < \"3.14\"", + "tokenizers>=0.15.0; python_version < \"3.14\"", + "httpx; python_version < \"3.14\"", + "tqdm; python_version < \"3.14\"", + "sentence-transformers; python_version >= \"3.14\"", "tenacity", "numpy>=1.26", ] diff --git a/src/pyseekdb/__init__.py b/src/pyseekdb/__init__.py index fabcaf6b..86c10492 100644 --- a/src/pyseekdb/__init__.py +++ b/src/pyseekdb/__init__.py @@ -60,10 +60,11 @@ import importlib.metadata -# Note: pylibseekdb built with ABI=0 and onnxruntime built with ABI=1, so there's a conflict between the two libraries. +# Note: pylibseekdb built with ABI=0 and torch built with ABI=1, so there's a conflict between the two libraries. # pylibseekdb is built both with ABI=0 and the -Bsymbolic flag, so we can load libraries with ABI=1 first # and then pylibseekdb to avoid these conflicts. -import onnxruntime # noqa: F401 +if importlib.util.find_spec("torch"): + import torch # noqa: F401 from .client import ( AdminAPI, diff --git a/src/pyseekdb/client/client_base.py b/src/pyseekdb/client/client_base.py index ced46c02..18cd4166 100644 --- a/src/pyseekdb/client/client_base.py +++ b/src/pyseekdb/client/client_base.py @@ -1376,10 +1376,10 @@ def _collection_add( # noqa: C901 elif documents: # embeddings not provided but documents are provided, check for embedding_function if embedding_function is not None: - logger.info(f"Generating embeddings for {len(documents)} documents using embedding function") + logger.debug(f"Generating embeddings for {len(documents)} documents using embedding function") try: embeddings = embedding_function(documents) - logger.info(f"✅ Successfully generated {len(embeddings)} embeddings") + logger.debug(f"✅ Successfully generated {len(embeddings)} embeddings") except Exception as e: logger.exception("Failed to generate embeddings") raise ValueError(f"Failed to generate embeddings from documents: {e}") from e @@ -1472,7 +1472,7 @@ def _collection_add( # noqa: C901 logger.debug(f"Executing SQL: {sql}") self._execute(sql) - logger.info(f"✅ Successfully added {num_items} item(s) to collection '{collection_name}'") + logger.debug(f"✅ Successfully added {num_items} item(s) to collection '{collection_name}'") def _collection_update( # noqa: C901 self, @@ -1533,10 +1533,10 @@ def _collection_update( # noqa: C901 elif documents: # embeddings not provided but documents are provided, check for embedding_function if embedding_function is not None: - logger.info(f"Generating embeddings for {len(documents)} documents using embedding function") + logger.debug(f"Generating embeddings for {len(documents)} documents using embedding function") try: embeddings = embedding_function(documents) - logger.info(f"✅ Successfully generated {len(embeddings)} embeddings") + logger.debug(f"✅ Successfully generated {len(embeddings)} embeddings") except Exception as e: logger.exception("Failed to generate embeddings") raise ValueError(f"Failed to generate embeddings from documents: {e}") from e @@ -1675,7 +1675,7 @@ def _collection_upsert( # noqa: C901 elif documents: # embeddings not provided but documents are provided, check for embedding_function if embedding_function is not None: - logger.info(f"Generating embeddings for {len(documents)} documents using embedding function") + logger.debug(f"Generating embeddings for {len(documents)} documents using embedding function") try: embeddings = embedding_function(documents) logger.info(f"✅ Successfully generated {len(embeddings)} embeddings") @@ -1818,7 +1818,7 @@ def _collection_delete( where_document: Filter condition on documents (optional) **kwargs: Additional parameters """ - logger.info(f"Deleting data from collection '{collection_name}'") + logger.debug(f"Deleting data from collection '{collection_name}'") # Validate that at least one filter is provided if not ids and not where and not where_document: @@ -1849,7 +1849,7 @@ def _collection_delete( use_context_manager = self._use_context_manager_for_cursor() self._execute_query_with_cursor(conn, sql, params, use_context_manager) - logger.info(f"✅ Successfully deleted data from collection '{collection_name}'") + logger.debug(f"✅ Successfully deleted data from collection '{collection_name}'") # -------------------- DQL Operations -------------------- # Note: _collection_query() and _collection_get() are implemented below with common SQL-based logic @@ -3267,7 +3267,7 @@ def _collection_count(self, collection_id: str | None, collection_name: str) -> Returns: Item count """ - logger.info(f"Counting items in collection '{collection_name}'") + logger.debug(f"Counting items in collection '{collection_name}'") conn = self._ensure_connection() # Convert collection name to table name diff --git a/src/pyseekdb/client/client_seekdb_embedded.py b/src/pyseekdb/client/client_seekdb_embedded.py index 156656a6..48532580 100644 --- a/src/pyseekdb/client/client_seekdb_embedded.py +++ b/src/pyseekdb/client/client_seekdb_embedded.py @@ -93,7 +93,7 @@ def _cleanup(self): if self._connection is not None: self._connection.close() self._connection = None - logger.info("Connection closed") + logger.info(f"Connection closed: path={self.path}, database={self.database}") def is_connected(self) -> bool: """Check connection status""" diff --git a/src/pyseekdb/client/embedding_function.py b/src/pyseekdb/client/embedding_function.py index 460e335b..0c7f249c 100644 --- a/src/pyseekdb/client/embedding_function.py +++ b/src/pyseekdb/client/embedding_function.py @@ -6,6 +6,7 @@ """ import logging +import sys import warnings from abc import abstractmethod from typing import ( @@ -167,13 +168,21 @@ def __init__( stacklevel=2, ) self.model_name = self._MODEL_NAME - from pyseekdb.utils.embedding_functions import OnnxEmbeddingFunction + if sys.version_info >= (3, 14): + from pyseekdb.utils.embedding_functions.sentence_transformer_embedding_function import ( + SentenceTransformerEmbeddingFunction, + ) - self._onnx = OnnxEmbeddingFunction( - model_name=self._MODEL_NAME, - hf_model_id=self._HF_MODEL_ID, - dimension=self._DIMENSION, - ) + self._backend = SentenceTransformerEmbeddingFunction(model_name=self._MODEL_NAME) + else: + from pyseekdb.utils.embedding_functions import OnnxEmbeddingFunction + + self._backend = OnnxEmbeddingFunction( + model_name=self._MODEL_NAME, + hf_model_id=self._HF_MODEL_ID, + dimension=self._DIMENSION, + preferred_providers=preferred_providers, + ) @property def dimension(self) -> int: @@ -181,7 +190,7 @@ def dimension(self) -> int: return self._DIMENSION def __call__(self, documents: Documents) -> Embeddings: - return self._onnx(documents) + return self._backend(documents) @staticmethod def name() -> str: diff --git a/src/pyseekdb/utils/embedding_functions/onnx_embedding_function.py b/src/pyseekdb/utils/embedding_functions/onnx_embedding_function.py index 8328cf0a..6fad2f99 100644 --- a/src/pyseekdb/utils/embedding_functions/onnx_embedding_function.py +++ b/src/pyseekdb/utils/embedding_functions/onnx_embedding_function.py @@ -12,7 +12,6 @@ from pathlib import Path from typing import Any -import httpx import numpy as np import numpy.typing as npt @@ -100,6 +99,8 @@ def _download(self, url: str, fname: str, chunk_size: int = 8192) -> None: """ logger.info(f"Downloading from {url}") # Use Client to ensure correct handling of redirects + import httpx + with httpx.Client(timeout=600.0, follow_redirects=True) as client, client.stream("GET", url) as resp: resp.raise_for_status() total = int(resp.headers.get("content-length", 0)) @@ -148,6 +149,7 @@ def _download_from_huggingface(self) -> bool: # noqa: C901 os.makedirs(extracted_folder, exist_ok=True) logger.info(f"Downloading model from Hugging Face (endpoint: {hf_endpoint})") + import httpx # Download each file for hf_filename, local_filename in files_to_download.items(): diff --git a/tests/integration_tests/test_collection_hybrid_search_source_inference.py b/tests/integration_tests/test_collection_hybrid_search_source_inference.py index bbf2cbfe..8ee21afb 100644 --- a/tests/integration_tests/test_collection_hybrid_search_source_inference.py +++ b/tests/integration_tests/test_collection_hybrid_search_source_inference.py @@ -7,19 +7,14 @@ import contextlib import json -import time import uuid from pymysql.converters import escape_string from pyseekdb import HNSWConfiguration -from pyseekdb.client.meta_info import CollectionNames class TestCollectionHybridSearchSourceInferenceRealDB: - _QUERY_TIMEOUT_SECONDS = 10.0 - _QUERY_RETRY_INTERVAL_SECONDS = 0.2 - def _unique_collection_name(self, prefix: str) -> str: # Keep names short to avoid MySQL/OceanBase identifier length limits after # internal table-name prefixing (e.g. "c$v1$..."). @@ -83,7 +78,7 @@ def _get_sql_query(self, client, table_name: str, search_parm: dict) -> str: return query_sql.strip().strip("'\"") return str(query_sql) - def test_include_infers_source_result_shape_matrix(self, server_client): + def test_include_infers_source_result_shape_matrix(self, db_client): """ Verify `_source` inference end-to-end: 1) GET_SQL result columns match requested include (avoid returning large unused columns like embedding) @@ -92,85 +87,33 @@ def test_include_infers_source_result_shape_matrix(self, server_client): collection_name = self._unique_collection_name("hs_include_matrix") collection = None try: - collection, dimension = self._create_test_collection(server_client, collection_name) + collection, dimension = self._create_test_collection(db_client, collection_name) self._insert_test_data(collection, dimension=dimension) query_vector = self._generate_query_vector(dimension) knn = {"query_embeddings": query_vector, "n_results": 2} - table_name = ( - CollectionNames.table_name_v2(collection.id) - if getattr(collection, "id", None) - else CollectionNames.table_name(collection.name) - ) - - def execute_get_sql(include: list[str] | None) -> tuple[dict, list[dict]]: - search_parm = server_client._server._build_search_parm( - query=None, - knn=knn, - rank=None, - n_results=2, - include=include, - dimension=dimension, - ) - query_sql = self._get_sql_query(server_client, table_name, search_parm) - deadline = time.time() + self._QUERY_TIMEOUT_SECONDS - last_exc: Exception | None = None - while time.time() < deadline: - try: - rows = server_client._server._execute(query_sql) - if rows: - return search_parm, rows - except Exception as exc: - last_exc = exc - time.sleep(self._QUERY_RETRY_INTERVAL_SECONDS) - if last_exc is not None: - raise AssertionError("Timed out waiting for GET_SQL query to return rows") from last_exc - raise AssertionError("Timed out waiting for GET_SQL query to return rows") - - def assert_columns(rows: list[dict], *, present: set[str], absent: set[str]) -> None: - assert rows - keys = {str(k).lower() for k in rows[0]} - for col in present: - assert col in keys - for col in absent: - assert col not in keys # 1) include=None: default returns documents+metadatas; should not return embedding column - _, rows = execute_get_sql(include=None) - assert_columns(rows, present={"document", "metadata"}, absent={"embedding"}) - default_include = collection.hybrid_search(knn=knn, n_results=2) assert set(default_include.keys()) == {"ids", "distances", "documents", "metadatas"} assert all(isinstance(d, str) for d in default_include["documents"][0]) assert all(isinstance(m, dict) and m for m in default_include["metadatas"][0]) # 2) include=[]: ids/distances only; should not return document/metadata/embedding columns - _, rows = execute_get_sql(include=[]) - assert_columns(rows, present=set(), absent={"document", "metadata", "embedding"}) - ids_only = collection.hybrid_search(knn=knn, n_results=2, include=[]) assert set(ids_only.keys()) == {"ids", "distances"} # 3) include=["documents"]: only document column - _, rows = execute_get_sql(include=["documents"]) - assert_columns(rows, present={"document"}, absent={"metadata", "embedding"}) - docs_only = collection.hybrid_search(knn=knn, n_results=2, include=["documents"]) assert set(docs_only.keys()) == {"ids", "distances", "documents"} assert all(isinstance(d, str) for d in docs_only["documents"][0]) # 4) include=["metadatas"]: only metadata column - _, rows = execute_get_sql(include=["metadatas"]) - assert_columns(rows, present={"metadata"}, absent={"document", "embedding"}) - metadatas_only = collection.hybrid_search(knn=knn, n_results=2, include=["metadatas"]) assert set(metadatas_only.keys()) == {"ids", "distances", "metadatas"} assert all(isinstance(m, dict) and m for m in metadatas_only["metadatas"][0]) # 5) include=["embeddings"]: only embedding column - _, rows = execute_get_sql(include=["embeddings"]) - assert_columns(rows, present={"embedding"}, absent={"document", "metadata"}) - embeddings_only = collection.hybrid_search(knn=knn, n_results=2, include=["embeddings"]) assert set(embeddings_only.keys()) == {"ids", "distances", "embeddings"} first_embedding = embeddings_only["embeddings"][0][0] @@ -178,16 +121,13 @@ def assert_columns(rows: list[dict], *, present: set[str], absent: set[str]) -> assert len(first_embedding) == dimension # 6) include=["documents","embeddings"]: document+embedding columns - _, rows = execute_get_sql(include=["documents", "embeddings"]) - assert_columns(rows, present={"document", "embedding"}, absent={"metadata"}) - docs_and_embeddings = collection.hybrid_search(knn=knn, n_results=2, include=["documents", "embeddings"]) assert set(docs_and_embeddings.keys()) == {"ids", "distances", "documents", "embeddings"} assert all(isinstance(d, str) for d in docs_and_embeddings["documents"][0]) assert all(isinstance(e, list) and len(e) == dimension for e in docs_and_embeddings["embeddings"][0]) finally: with contextlib.suppress(Exception): - server_client.delete_collection(name=collection_name) + db_client.delete_collection(name=collection_name) # NOTE: `HybridSearch` fluent builder was removed on `develop` (rollback enhanced hybrid search). # Keep this file focused on verifying OceanBase GET_SQL `_source` inference and result shapes. diff --git a/tests/unit_tests/test_default_embedding_function.py b/tests/unit_tests/test_default_embedding_function.py index 37c3f410..0d0969e4 100644 --- a/tests/unit_tests/test_default_embedding_function.py +++ b/tests/unit_tests/test_default_embedding_function.py @@ -2,6 +2,7 @@ Unit tests for DefaultEmbeddingFunction. """ +import sys from typing import Any import pytest @@ -59,5 +60,21 @@ def test_persistence_roundtrip(self): assert restored_ef.model_name == original_ef.model_name +def test_default_embedding_function_on_py314(): + if sys.version_info < (3, 14): + pytest.skip("Python < 3.14") + embedding_function = DefaultEmbeddingFunction() + assert embedding_function.dimension == 384 + assert len(embedding_function("hello")[0]) == 384 + + +def test_default_embedding_function_uses_onnx_on_pre314(): + if sys.version_info >= (3, 14): + pytest.skip("Python >= 3.14") + embedding_function = DefaultEmbeddingFunction() + assert embedding_function.dimension == 384 + assert len(embedding_function("hello")[0]) == 384 + + if __name__ == "__main__": pytest.main([__file__, "-v", "-s"]) diff --git a/tests/unit_tests/test_onnx_embedding_function.py b/tests/unit_tests/test_onnx_embedding_function.py index 4f34b339..f1825e6e 100644 --- a/tests/unit_tests/test_onnx_embedding_function.py +++ b/tests/unit_tests/test_onnx_embedding_function.py @@ -4,68 +4,74 @@ from __future__ import annotations +import importlib + import pytest from pyseekdb.utils.embedding_functions.onnx_embedding_function import OnnxEmbeddingFunction -def _make_onnx() -> OnnxEmbeddingFunction: - return OnnxEmbeddingFunction( - model_name="all-MiniLM-L6-v2", - hf_model_id="sentence-transformers/all-MiniLM-L6-v2", - dimension=384, - preferred_providers=None, - ) - - -def test_init_validates_model_name() -> None: - with pytest.raises(ValueError, match="model_name must be a non-empty string"): - OnnxEmbeddingFunction(model_name="", hf_model_id="org/test", dimension=3) - - -def test_init_validates_hf_model_id() -> None: - with pytest.raises(ValueError, match="hf_model_id must be a non-empty string"): - OnnxEmbeddingFunction(model_name="test", hf_model_id="", dimension=3) - - -def test_init_validates_dimension() -> None: - with pytest.raises(ValueError, match="dimension must be a positive integer"): - OnnxEmbeddingFunction(model_name="test", hf_model_id="org/test", dimension=0) +def is_onnx_available() -> bool: + """Check if onnxruntime is available for testing.""" + return importlib.util.find_spec("onnxruntime") is not None -def test_init_validates_preferred_providers() -> None: - with pytest.raises(ValueError, match="Preferred providers must be a list of strings"): - OnnxEmbeddingFunction(model_name="test", hf_model_id="org/test", dimension=3, preferred_providers=[1]) # type: ignore[list-item] - with pytest.raises(ValueError, match="Preferred providers must be unique"): - OnnxEmbeddingFunction( - model_name="test", - hf_model_id="org/test", - dimension=3, - preferred_providers=["CPUExecutionProvider", "CPUExecutionProvider"], +# Skip this test by default - it requires external API access and API keys +@pytest.mark.skipif( + not is_onnx_available(), + reason="onnxruntime is not available", +) +class TestOnnxEmbeddingFunction: + def _make_onnx(self) -> OnnxEmbeddingFunction: + return OnnxEmbeddingFunction( + model_name="all-MiniLM-L6-v2", + hf_model_id="sentence-transformers/all-MiniLM-L6-v2", + dimension=384, + preferred_providers=None, ) - -def test_dimension_property() -> None: - ef = _make_onnx() - assert ef.dimension == 384 - - -def test_call_empty_returns_empty() -> None: - ef = _make_onnx() - assert ef([]) == [] - - -def test_call_generates_embeddings() -> None: - pytest.importorskip("onnxruntime") - pytest.importorskip("tokenizers") - pytest.importorskip("tqdm") - - ef = _make_onnx() - embeddings = ef("hello world") - - assert isinstance(embeddings, list) - assert len(embeddings) == 1 - assert len(embeddings[0]) == ef.dimension + def test_init_validates_model_name(self) -> None: + with pytest.raises(ValueError, match="model_name must be a non-empty string"): + OnnxEmbeddingFunction(model_name="", hf_model_id="org/test", dimension=3) + + def test_init_validates_hf_model_id(self) -> None: + with pytest.raises(ValueError, match="hf_model_id must be a non-empty string"): + OnnxEmbeddingFunction(model_name="test", hf_model_id="", dimension=3) + + def test_init_validates_dimension(self) -> None: + with pytest.raises(ValueError, match="dimension must be a positive integer"): + OnnxEmbeddingFunction(model_name="test", hf_model_id="org/test", dimension=0) + + def test_init_validates_preferred_providers(self) -> None: + with pytest.raises(ValueError, match="Preferred providers must be a list of strings"): + OnnxEmbeddingFunction(model_name="test", hf_model_id="org/test", dimension=3, preferred_providers=[1]) # type: ignore[list-item] + with pytest.raises(ValueError, match="Preferred providers must be unique"): + OnnxEmbeddingFunction( + model_name="test", + hf_model_id="org/test", + dimension=3, + preferred_providers=["CPUExecutionProvider", "CPUExecutionProvider"], + ) + + def test_dimension_property(self) -> None: + ef = self._make_onnx() + assert ef.dimension == 384 + + def test_call_empty_returns_empty(self) -> None: + ef = self._make_onnx() + assert ef([]) == [] + + def test_call_generates_embeddings(self) -> None: + pytest.importorskip("onnxruntime") + pytest.importorskip("tokenizers") + pytest.importorskip("tqdm") + + ef = self._make_onnx() + embeddings = ef("hello world") + + assert isinstance(embeddings, list) + assert len(embeddings) == 1 + assert len(embeddings[0]) == ef.dimension if __name__ == "__main__": diff --git a/uv.lock b/uv.lock index 1ca532d0..101dd5b0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.11, <3.14" +requires-python = ">=3.11, <4" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version < '3.12'", ] @@ -30,7 +31,7 @@ dependencies = [ { name = "jsonschema" }, { name = "narwhals" }, { name = "packaging" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/c0/184a89bd5feba14ff3c41cfaf1dd8a82c05f5ceedbc92145e17042eb08a4/altair-6.0.0.tar.gz", hash = "sha256:614bf5ecbe2337347b590afb111929aa9c16c9527c4887d96c9bc7f6640756b4", size = 763834, upload-time = "2025-11-12T08:59:11.519Z" } wheels = [ @@ -167,6 +168,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] @@ -196,7 +213,7 @@ name = "coloredlogs" version = "15.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "humanfriendly" }, + { name = "humanfriendly", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } wheels = [ @@ -302,6 +319,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, @@ -363,7 +387,7 @@ name = "humanfriendly" version = "10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "pyreadline3", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } wheels = [ @@ -468,6 +492,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, @@ -576,6 +625,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] @@ -701,6 +772,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/52/41f3d71296a3dcaa4f456aaa3c6fc8e745b43d0552b6bde56571bb4b4a0f/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216, upload-time = "2025-12-20T16:17:11.437Z" }, { url = "https://files.pythonhosted.org/packages/35/ff/46fbfe60ab0710d2a2b16995f708750307d30eccbb4c38371ea9e986866e/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263, upload-time = "2025-12-20T16:17:13.182Z" }, { url = "https://files.pythonhosted.org/packages/a3/e3/9189ab319c01d2ed556c932ccf55064c5d75bb5850d1df7a482ce0badead/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265, upload-time = "2025-12-20T16:17:15.211Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476, upload-time = "2025-12-20T16:17:17.671Z" }, + { url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563, upload-time = "2025-12-20T16:17:20.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107, upload-time = "2025-12-20T16:17:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067, upload-time = "2025-12-20T16:17:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926, upload-time = "2025-12-20T16:17:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295, upload-time = "2025-12-20T16:17:28.308Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242, upload-time = "2025-12-20T16:17:30.993Z" }, + { url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875, upload-time = "2025-12-20T16:17:33.327Z" }, + { url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530, upload-time = "2025-12-20T16:17:35.729Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890, upload-time = "2025-12-20T16:17:37.599Z" }, + { url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892, upload-time = "2025-12-20T16:17:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312, upload-time = "2025-12-20T16:17:41.714Z" }, + { url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862, upload-time = "2025-12-20T16:17:44.145Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986, upload-time = "2025-12-20T16:17:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958, upload-time = "2025-12-20T16:17:48.017Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394, upload-time = "2025-12-20T16:17:50.409Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044, upload-time = "2025-12-20T16:17:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772, upload-time = "2025-12-20T16:17:54.947Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320, upload-time = "2025-12-20T16:17:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460, upload-time = "2025-12-20T16:17:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" }, { url = "https://files.pythonhosted.org/packages/4b/ef/088e7c7342f300aaf3ee5f2c821c4b9996a1bef2aaf6a49cc8ab4883758e/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003, upload-time = "2025-12-20T16:18:03.41Z" }, { url = "https://files.pythonhosted.org/packages/ff/ce/a53017b5443b4b84517182d463fc7bcc2adb4faa8b20813f8e5f5aeb5faa/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105, upload-time = "2025-12-20T16:18:05.594Z" }, { url = "https://files.pythonhosted.org/packages/77/58/5ff91b161f2ec650c88a626c3905d938c89aaadabd0431e6d9c1330c83e2/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590, upload-time = "2025-12-20T16:18:08.031Z" }, @@ -849,12 +941,12 @@ name = "onnxruntime" version = "1.23.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coloredlogs" }, - { name = "flatbuffers" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "sympy" }, + { name = "coloredlogs", marker = "python_full_version < '3.14'" }, + { name = "flatbuffers", marker = "python_full_version < '3.14'" }, + { name = "numpy", marker = "python_full_version < '3.14'" }, + { name = "packaging", marker = "python_full_version < '3.14'" }, + { name = "protobuf", marker = "python_full_version < '3.14'" }, + { name = "sympy", marker = "python_full_version < '3.14'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" }, @@ -943,6 +1035,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] [[package]] @@ -998,6 +1103,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, @@ -1098,6 +1228,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, + { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, ] [[package]] @@ -1166,6 +1310,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, @@ -1222,6 +1394,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/b6/a81291350f864f8c2bf58c1d413b2fdce24cc57cc0866f71b23e2f3406b3/pylibseekdb-1.0.0.post1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e12ac23e8409fdb7785d8054ff4361910e761540ab7eafede9aa464e7dbc1ac7", size = 80346064, upload-time = "2025-11-17T11:01:38.447Z" }, { url = "https://files.pythonhosted.org/packages/bb/f9/4b83f9118a2884be01e09362fa9963f6e7892fd4bed326059445072485c6/pylibseekdb-1.0.0.post1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:82d2c56dbce6d41f2059cd4285292f00bf801eb6f5c36df12ab30eb18c714070", size = 59927878, upload-time = "2025-11-17T11:31:28.833Z" }, { url = "https://files.pythonhosted.org/packages/b1/93/28c9ff560032b7e7fe2fb5e76979b482c75de516838775c956b3999b00c5/pylibseekdb-1.0.0.post1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:7249a82f55e1e8f4c13c0f6a1a773fa95902d8e2334c674491e601b2efbcc89b", size = 80349282, upload-time = "2025-11-17T11:01:43.298Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0b/0084a0d78deb0dfc6ff36b4f6ae64c091398a2e3ef3ee7b3db0d8d78e115/pylibseekdb-1.0.0.post1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:c1113b6b6579a8743cb6dcceed3a21473713c988cab91650c85e01d58682a4b7", size = 59929351, upload-time = "2025-11-17T11:31:38.231Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/615f960f70f5b8caf512f8c32c93b4677fa638da2d8ed2bc792d1fb76264/pylibseekdb-1.0.0.post1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:fb8b195338f42a0c5d021b0ad67aad342348167adacdb500eadd3c649a93d485", size = 80353322, upload-time = "2025-11-17T11:01:47.772Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7f/a3c81ef9c4fab8f2c8d48d7a184100c59e70f384310ddbb12a135d6ec082/pylibseekdb-1.0.0.post1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4cdb0b11e64b465c77dc52a76ec46ab9ff85eba2c5af7ef5f2a4001288e1c49a", size = 59935108, upload-time = "2025-11-17T11:31:46.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/24/2dfc7721493e492e50d29cda9fee99360a39334b6e63aef0f9895cb9be8c/pylibseekdb-1.0.0.post1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9878fb337111d3c3367a936aee61fac34238f0d0901cedf7773d061855407d06", size = 80354154, upload-time = "2025-11-17T11:01:52.224Z" }, ] [[package]] @@ -1237,6 +1413,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/aa/ac67fe8376af4ecc6619d453692caac2bb3bd6473e3236767baee17c6bc9/pylibseekdb_runtime-1.0.0.post1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d58a30a9ea32238ec7db72bc6c8e199c3e4a2c54ffda039c0fc69573c78afdf8", size = 86347710, upload-time = "2025-11-17T11:02:21.603Z" }, { url = "https://files.pythonhosted.org/packages/b7/7b/ad6e1b82f57f24e5bf6aedff61cae60a54ad0ce6994a94df9ab04378ff40/pylibseekdb_runtime-1.0.0.post1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:b491b37347d2e806c9359e6c73ad5c399cd6bf9c3fa7daa8ee0b75b513f486ad", size = 84222093, upload-time = "2025-11-17T11:32:55.587Z" }, { url = "https://files.pythonhosted.org/packages/82/e3/74d77fdc385c6b0b9c46ac36c1fe745438af6a23deecca881d5a49f35af0/pylibseekdb_runtime-1.0.0.post1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:82bc2201dde2fd94f60ba7b4e52406602d40feff4301a4f5af82188beb5d9a2c", size = 86365110, upload-time = "2025-11-17T11:02:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c4/9e351a8c52c508091c3af1b802b0a66043282669eb8f50007b71b70782ce/pylibseekdb_runtime-1.0.0.post1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:57e46c2dec66f255e9a1a34476f5e80348b2ddfaa8ee8b39c6c6509506e45446", size = 84224151, upload-time = "2025-11-17T11:33:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/ea/23/aa13383e5fb18200dc2bc86c46f0158012f0cbabecd5cfbe3ac4bda064cb/pylibseekdb_runtime-1.0.0.post1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:eec0d00d824620b2f36635c12c920a3f3cd03a5509521d8a618a800b1b75ae04", size = 86354643, upload-time = "2025-11-17T11:02:32.552Z" }, + { url = "https://files.pythonhosted.org/packages/77/bd/73d0525048c30fa2ff18fe4516943095d452e77484a27def88fe8fa17438/pylibseekdb_runtime-1.0.0.post1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aa2f6be0abdc0b4e35f2971cf96c26a72d1715901b85a7584ed4e395cf999027", size = 84222095, upload-time = "2025-11-17T11:33:15.46Z" }, + { url = "https://files.pythonhosted.org/packages/0f/3d/22d4bd6cba2fd8093e01cd1246dfee7f970306cf6c60520481135f305eed/pylibseekdb_runtime-1.0.0.post1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:916771f9fc6a613d0b8334f1688eb4519b314cd30a21c5e83096f84a039c7b06", size = 86352963, upload-time = "2025-11-17T11:02:37.896Z" }, ] [[package]] @@ -1280,14 +1460,15 @@ name = "pyseekdb" version = "1.0.0b4" source = { editable = "." } dependencies = [ - { name = "httpx" }, + { name = "httpx", marker = "python_full_version < '3.14'" }, { name = "numpy" }, - { name = "onnxruntime" }, + { name = "onnxruntime", marker = "python_full_version < '3.14'" }, { name = "pylibseekdb", marker = "sys_platform == 'linux'" }, { name = "pymysql" }, + { name = "sentence-transformers", marker = "python_full_version >= '3.14'" }, { name = "tenacity" }, - { name = "tokenizers" }, - { name = "tqdm" }, + { name = "tokenizers", marker = "python_full_version < '3.14'" }, + { name = "tqdm", marker = "python_full_version < '3.14'" }, ] [package.dev-dependencies] @@ -1303,14 +1484,15 @@ dev = [ [package.metadata] requires-dist = [ - { name = "httpx" }, + { name = "httpx", marker = "python_full_version < '3.14'" }, { name = "numpy", specifier = ">=1.26" }, - { name = "onnxruntime", specifier = ">=1.19.0" }, + { name = "onnxruntime", marker = "python_full_version < '3.14'", specifier = ">=1.19.0" }, { name = "pylibseekdb", marker = "sys_platform == 'linux'" }, { name = "pymysql", specifier = ">=1.1.1" }, + { name = "sentence-transformers", marker = "python_full_version >= '3.14'" }, { name = "tenacity" }, - { name = "tokenizers", specifier = ">=0.15.0" }, - { name = "tqdm" }, + { name = "tokenizers", marker = "python_full_version < '3.14'", specifier = ">=0.15.0" }, + { name = "tqdm", marker = "python_full_version < '3.14'" }, ] [package.metadata.requires-dev] @@ -1405,6 +1587,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -1483,6 +1683,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, ] [[package]] @@ -1586,6 +1814,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, @@ -1684,6 +1941,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, ] [[package]] @@ -1735,6 +2004,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, ] [[package]] @@ -2088,6 +2377,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/dd/f1c0d879f2863ef209e18823a988dc7a1bf40470750e3ebe927efdb9407f/torch-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2", size = 899748978, upload-time = "2025-11-12T15:23:04.568Z" }, { url = "https://files.pythonhosted.org/packages/1f/9f/6986b83a53b4d043e36f3f898b798ab51f7f20fdf1a9b01a2720f445043d/torch-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db", size = 111176995, upload-time = "2025-11-12T15:22:01.618Z" }, { url = "https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587", size = 74480347, upload-time = "2025-11-12T15:21:57.648Z" }, + { url = "https://files.pythonhosted.org/packages/48/50/c4b5112546d0d13cc9eaa1c732b823d676a9f49ae8b6f97772f795874a03/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245, upload-time = "2025-11-12T15:22:39.027Z" }, + { url = "https://files.pythonhosted.org/packages/81/c9/2628f408f0518b3bae49c95f5af3728b6ab498c8624ab1e03a43dd53d650/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804, upload-time = "2025-11-12T15:22:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/28/fc/5bc91d6d831ae41bf6e9e6da6468f25330522e92347c9156eb3f1cb95956/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132, upload-time = "2025-11-12T15:23:36.068Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/e8d4e009e52b6b2cf1684bde2a6be157b96fb873732542fb2a9a99e85a83/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845, upload-time = "2025-11-12T15:22:48.367Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b2/2d15a52516b2ea3f414643b8de68fa4cb220d3877ac8b1028c83dc8ca1c4/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558, upload-time = "2025-11-12T15:22:43.392Z" }, + { url = "https://files.pythonhosted.org/packages/86/5c/5b2e5d84f5b9850cd1e71af07524d8cbb74cba19379800f1f9f7c997fc70/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788, upload-time = "2025-11-12T15:23:52.109Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8c/3da60787bcf70add986c4ad485993026ac0ca74f2fc21410bc4eb1bb7695/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500, upload-time = "2025-11-12T15:24:08.788Z" }, + { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" }, ] [[package]] @@ -2151,6 +2448,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/50/9a8358d3ef58162c0a415d173cfb45b67de60176e1024f71fbc4d24c0b6d/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207, upload-time = "2025-11-11T17:41:00.253Z" }, { url = "https://files.pythonhosted.org/packages/27/46/8c3bbb5b0a19313f50edcaa363b599e5a1a5ac9683ead82b9b80fe497c8d/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410, upload-time = "2025-11-11T17:41:06.319Z" }, { url = "https://files.pythonhosted.org/packages/37/92/e97fcc6b2c27cdb87ce5ee063d77f8f26f19f06916aa680464c8104ef0f6/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924, upload-time = "2025-11-11T17:41:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e6/c595c35e5c50c4bc56a7bac96493dad321e9e29b953b526bbbe20f9911d0/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488, upload-time = "2025-11-11T17:41:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192, upload-time = "2025-11-11T17:41:23.963Z" }, ] [[package]]