From f560821d0ef634d4bcfae06d67d1f45f69b89082 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 23 Sep 2026 12:38:18 +0200 Subject: [PATCH 1/9] feat(python): add SQL Server vector store connector --- .../workflows/python-integration-tests.yml | 2 + .github/workflows/python-merge-tests.yml | 3 + python/PACKAGE_STATUS.md | 1 + python/packages/sql-server/LICENSE | 21 + python/packages/sql-server/README.md | 151 ++++ .../agent_framework_sql_server/__init__.py | 22 + .../agent_framework_sql_server/_sql.py | 340 ++++++++ .../_vector_store.py | 760 ++++++++++++++++++ .../agent_framework_sql_server/py.typed | 0 python/packages/sql-server/pyproject.toml | 65 ++ .../sql-server/samples/sql_server_vectors.py | 70 ++ .../tests/sql_server/test_integration.py | 218 +++++ .../sql-server/tests/sql_server/test_sql.py | 170 ++++ .../tests/sql_server/test_vector_store.py | 645 +++++++++++++++ python/pyproject.toml | 1 + python/uv.lock | 83 ++ 16 files changed, 2552 insertions(+) create mode 100644 python/packages/sql-server/LICENSE create mode 100644 python/packages/sql-server/README.md create mode 100644 python/packages/sql-server/agent_framework_sql_server/__init__.py create mode 100644 python/packages/sql-server/agent_framework_sql_server/_sql.py create mode 100644 python/packages/sql-server/agent_framework_sql_server/_vector_store.py create mode 100644 python/packages/sql-server/agent_framework_sql_server/py.typed create mode 100644 python/packages/sql-server/pyproject.toml create mode 100644 python/packages/sql-server/samples/sql_server_vectors.py create mode 100644 python/packages/sql-server/tests/sql_server/test_integration.py create mode 100644 python/packages/sql-server/tests/sql_server/test_sql.py create mode 100644 python/packages/sql-server/tests/sql_server/test_vector_store.py diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index d85bba7c9f9..ac1312c65e2 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -269,6 +269,7 @@ jobs: LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }} OLLAMA_MODEL: qwen2.5:1.5b OLLAMA_EMBEDDING_MODEL: nomic-embed-text + SQL_SERVER_TEST_CONNECTION_STRING: ${{ secrets.SQL_SERVER_TEST_CONNECTION_STRING }} defaults: run: working-directory: python @@ -334,6 +335,7 @@ jobs: packages/hyperlight/tests packages/mistral/tests packages/ollama/tests + packages/sql-server/tests/sql_server/test_integration.py packages/core/tests/core/test_mcp.py packages/hosting-mcp/tests -m integration diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index 2d75ad02844..26c4a276469 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -73,6 +73,7 @@ jobs: - 'python/packages/hyperlight/**' - 'python/packages/mistral/**' - 'python/packages/ollama/**' + - 'python/packages/sql-server/**' - 'python/packages/core/agent_framework/_mcp.py' - 'python/packages/core/tests/core/test_mcp.py' - 'python/packages/hosting-mcp/**' @@ -405,6 +406,7 @@ jobs: LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }} OLLAMA_MODEL: qwen2.5:1.5b OLLAMA_EMBEDDING_MODEL: nomic-embed-text + SQL_SERVER_TEST_CONNECTION_STRING: ${{ secrets.SQL_SERVER_TEST_CONNECTION_STRING }} defaults: run: working-directory: python @@ -467,6 +469,7 @@ jobs: packages/hyperlight/tests packages/mistral/tests packages/ollama/tests + packages/sql-server/tests/sql_server/test_integration.py packages/core/tests/core/test_mcp.py packages/hosting-mcp/tests -m integration diff --git a/python/PACKAGE_STATUS.md b/python/PACKAGE_STATUS.md index 51ff0f2f7a6..bc1ec0e346d 100644 --- a/python/PACKAGE_STATUS.md +++ b/python/PACKAGE_STATUS.md @@ -53,6 +53,7 @@ Status is grouped into these buckets: | `agent-framework-purview` | `python/packages/purview` | `beta` | | `agent-framework-qdrant` | `python/packages/qdrant` | `alpha` | | `agent-framework-redis` | `python/packages/redis` | `beta` | +| `agent-framework-sql-server` | `python/packages/sql-server` | `alpha` | | `agent-framework-tools` | `python/packages/tools` | `beta` | ## Deprecated / removed packages diff --git a/python/packages/sql-server/LICENSE b/python/packages/sql-server/LICENSE new file mode 100644 index 00000000000..9e841e7a26e --- /dev/null +++ b/python/packages/sql-server/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/sql-server/README.md b/python/packages/sql-server/README.md new file mode 100644 index 00000000000..0cb36e4b65c --- /dev/null +++ b/python/packages/sql-server/README.md @@ -0,0 +1,151 @@ +# Agent Framework SQL Server vector store + +Store typed Agent Framework records in SQL Server and Azure SQL native `VECTOR` +columns, with exact, database-side similarity search. This alpha package exports +`SqlServerCollection`, `SqlServerStore`, `SqlServerSettings`, and the +`SqlServerClient` type from `agent_framework_sql_server`. + +## Install and provision + +```bash +pip install agent-framework-sql-server --pre +``` + +Requires Python 3.10+, a registered **Microsoft ODBC Driver 18 for SQL Server** +(18.6.1.1 or later recommended), and a vector-enabled database: SQL Server +2025 (17.x), Azure SQL Database, Azure SQL Managed Instance on the SQL Server +2025 or Always-up-to-date update policy, or SQL database in Microsoft Fabric. +Older SQL Server releases do not support `VECTOR`/`VECTOR_DISTANCE`. + +Install the ODBC driver **separately** on the machine running Python: +[macOS (Intel/Apple Silicon)](https://learn.microsoft.com/sql/connect/odbc/linux-mac/install-microsoft-odbc-driver-sql-server-macos), +[Linux](https://learn.microsoft.com/sql/connect/odbc/linux-mac/install-microsoft-odbc-driver-sql-server-linux), +or [Windows](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server). +On macOS the Microsoft Homebrew formula installs unixODBC as a dependency. +The Python dependencies `aioodbc` and `pyodbc` do **not** install the Microsoft +ODBC driver. Configure its `vectorTypeSupport=off` setting (the default): +the server stores native vectors while ODBC reads/writes their JSON-array +representation. `vectorTypeSupport=v1/v2` exposes a C-specific representation +that this Python package does not decode. + +The database administrator must provide an existing schema (default `dbo`). +`ensure_collection_exists()` creates the requested table and scalar indexes +there but never creates a schema, alters an existing table, or changes database +settings. `ensure_collection_deleted()` drops only that table. + +## Connection settings and ownership + +Set `SQL_SERVER_CONNECTION_STRING` to an ODBC connection string for the selected +database with a registered `Driver={ODBC Driver 18 for SQL Server}` and your +chosen server, encryption, and authentication settings. Do not commit +connection strings containing credentials. Alternatively, pass +`connection_string` as a string or Agent Framework `SecretString` to +`SqlServerStore` or `SqlServerCollection`. Settings precedence is **explicit +argument > selected `.env` file > process environment**. Choose a file with +`env_file_path` and optional `env_file_encoding`; no `.env` file is discovered +implicitly, and missing/empty strings are rejected. + +An owned store creates a lazy one-connection `aioodbc` pool with a dedicated +single-worker executor; `close()` or an async context manager releases both. +Collections created by the store share that pool: keep the store open while +using them. To borrow an existing `aioodbc.Connection` or `aioodbc.Pool`, pass +`client=` instead of connection settings. The connector never closes an +injected client. Raw `pyodbc.Connection` objects are **not** accepted; use the +async aioodbc type. Avoid using the same borrowed connection concurrently +outside the connector; for a caller-created pool, configure its executor +appropriately for your workload. + +Batch writes on owned connections commit or roll back together. On borrowed +**connections** the connector starts/commits its own transaction if none is +active; otherwise it uses a SQL Server savepoint and leaves the caller's +transaction open. On a borrowed **pool**, each acquired connection's operation +is committed or rolled back before release; the pool must use +`autocommit=False` (aioodbc's default), and callers must return clean +connections to it. If a caller's transaction becomes uncommittable, the caller +must roll it back. SQL Server savepoints are not available in distributed +transactions. +`aioodbc` uses worker threads internally: cancelling a coroutine cannot +interrupt an already-running ODBC query, so a write may still finish. Set +appropriate database query timeouts, and prefer stable application-provided +keys when retrying writes. + +## Example + +With `SQL_SERVER_CONNECTION_STRING` configured, run the +[typed sample](samples/sql_server_vectors.py) from the `python/` directory: + +```bash +uv run --package agent-framework-sql-server \ + python packages/sql-server/samples/sql_server_vectors.py +``` + +The sample creates a uniquely named table, upserts precomputed embeddings, +filters/ranks in SQL Server, retrieves an optional vector, and drops its own +table. Pass `generate_vectors=False` to preserve precomputed vectors; to +generate them locally, configure an `embedding_generator`. + +## Capabilities and limits + +The connector supports typed decorated models and dictionary definitions; +string/integer/UUID keys (including generated keys); multiple nullable float32 +vector columns with **1–1998 dimensions**; field storage aliases; batch +upsert/get/delete; paged and ordered retrieval; scalar data indexes; and +parameterized top-level `Filter`/`FilterGroup` expressions. String keys cannot +end with a space because SQL Server ignores trailing spaces in key comparisons. +Indexed strings use `NVARCHAR(450)`; other strings use `NVARCHAR(MAX)`. List and +dictionary fields are stored as JSON, and timezone-aware `datetime` values are +normalized to UTC in `DATETIME2(7)` columns. +For an auto-generated integer (`IDENTITY`) key, omit the key on insert; +explicit keys can update existing rows but cannot create new identity rows. + +Supported filters: scalar `eq`, `ne`, `in`, `not_in`, `is_null`, `is_not_null`, +`exists`; numeric/date/datetime `gt`, `gte`, `lt`, `lte`, `between`; and string +`starts_with`, `ends_with`, `contains_text`. `AND`/`OR`/`NOT` groups preserve +two-valued null semantics; string equality is byte-exact and text patterns +escape SQL Server wildcards. JSON fields support `is_null`, `is_not_null`, and +`exists`, **not** equality or collection-membership filters. Nested paths, +full-text filtering, and unknown operation options fail explicitly. The SQL +Server 2100-parameter limit is respected by batching key reads/deletes and +limiting other statements to 2000 bound parameters. + +Search uses SQL Server's exact `VECTOR_DISTANCE` on native `VECTOR` columns, +with filters and score thresholds applied **before** offset/limit. The default +metric is cosine **distance** (lower is better). Euclidean and negative dot +product also return distances (maximum thresholds); `cosine_similarity` and +`dot_prod` return similarity/positive-dot scores (minimum thresholds). +Scores are raw metric units, not probabilities. Retrieval excludes vectors by +default; use `include_vectors=True` to return them. Approximate DiskANN +indexes/search, preview-only float16 vectors, keyword-hybrid search, sparse or +binary vectors, server-side embedding generation, and schema migration are +not supported. + +`mssql-python` 1.15.0 was evaluated. It supports Python 3.10 and bound JSON +vectors, but has no released native async API, reports `threadsafety=1` (so +moving a caller-created raw connection between AF worker threads is unsafe), +and publishes only macOS 15+ wheels with no source distribution. This +connector instead uses `aioodbc`/`pyodbc` to retain an async, borrowed-client +interface and broader macOS compatibility at the cost of a separately +installed ODBC driver. + +## Service tests + +Unit tests need no database. Integration tests are opt-in: set +`SQL_SERVER_TEST_CONNECTION_STRING` to a deliberately designated test database +with table creation permissions and run: + +```bash +uv run --package agent-framework-sql-server pytest \ + packages/sql-server/tests/sql_server/test_integration.py -m integration +``` + +The tests create uniquely named tables and remove only those tables. They +skip when the variable is absent or empty (including an unconfigured CI +secret), and fail rather than silently skipping when an explicitly +designated server lacks vector support. + +## References + +- [SQL Server vector type and database availability](https://learn.microsoft.com/sql/t-sql/data-types/vector-data-type) +- [Exact vector distance metrics](https://learn.microsoft.com/sql/t-sql/functions/vector-distance-transact-sql) +- [ODBC vector JSON compatibility mode](https://learn.microsoft.com/sql/connect/odbc/vector-data-type) +- [Microsoft Agent Framework](https://learn.microsoft.com/agent-framework/) diff --git a/python/packages/sql-server/agent_framework_sql_server/__init__.py b/python/packages/sql-server/agent_framework_sql_server/__init__.py new file mode 100644 index 00000000000..412f91b5f1c --- /dev/null +++ b/python/packages/sql-server/agent_framework_sql_server/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""SQL Server native vector collections and stores for Agent Framework.""" + +from __future__ import annotations + +import importlib.metadata + +from ._vector_store import SqlServerClient, SqlServerCollection, SqlServerSettings, SqlServerStore + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" + +__all__ = [ + "SqlServerClient", + "SqlServerCollection", + "SqlServerSettings", + "SqlServerStore", + "__version__", +] diff --git a/python/packages/sql-server/agent_framework_sql_server/_sql.py b/python/packages/sql-server/agent_framework_sql_server/_sql.py new file mode 100644 index 00000000000..3b787f7a735 --- /dev/null +++ b/python/packages/sql-server/agent_framework_sql_server/_sql.py @@ -0,0 +1,340 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""SQL Server identifiers, values, and portable filter translation.""" + +# pyright: reportUnusedFunction=false, reportUnusedClass=false +# Package-private helpers are consumed by the sibling vector-store module. + +from __future__ import annotations + +import json +import math +from collections.abc import Sequence +from datetime import date, datetime, timezone +from typing import Any, cast +from uuid import UUID + +from agent_framework import FilterGroup, VectorStoreCollectionDefinition, VectorStoreField +from agent_framework._vector_filters import FilterExpression +from agent_framework.exceptions import IntegrationInvalidResponseException + +MAX_PARAMETERS = 2000 # SQL Server's limit is 2100; leave room for paging and search arguments. +_FLOAT32_MAX = 3.4028234663852886e38 +_COLLATION = "Latin1_General_100_BIN2" +_ORDER_OPERATORS = {"gt": ">", "gte": ">=", "lt": "<", "lte": "<="} +_METRICS = { + "DEFAULT": ("cosine", "distance"), + "cosine_distance": ("cosine", "distance"), + "cosine_similarity": ("cosine", "similarity"), + "euclidean_distance": ("euclidean", "distance"), + "dot_prod": ("dot", "negative"), + "negative_dot_prod": ("dot", "distance"), +} + + +def _quote_identifier(name: str) -> str: + """Quote one SQL identifier; data values must be passed as bound parameters.""" + if not isinstance(name, str) or not name or "\0" in name or len(name.encode("utf-16-le")) // 2 > 128: + raise ValueError("SQL Server identifiers must contain 1-128 UTF-16 code units and no NUL.") + return f"[{name.replace(']', ']]')}]" + + +def _metric_for(field: VectorStoreField) -> tuple[str, str]: + try: + return _METRICS[field.distance_function or "DEFAULT"] + except KeyError: + raise NotImplementedError(f"Unsupported SQL Server distance function '{field.distance_function}'.") from None + + +def _column_type(field: VectorStoreField) -> str: + if field.field_type == "vector": + dimensions = field.dimensions + if type(dimensions) is not int or not 1 <= dimensions <= 1998: + raise ValueError("SQL Server vector dimensions must be an integer between 1 and 1998.") + if field.type_ not in (None, "float", "float32"): + raise NotImplementedError("SQL Server VECTOR columns support float32 vectors only.") + return f"VECTOR({dimensions})" + types = { + "int": "BIGINT", + "float": "FLOAT(53)", + "bool": "BIT", + "UUID": "UNIQUEIDENTIFIER", + "bytes": "VARBINARY(MAX)", + "date": "DATE", + "datetime": "DATETIME2(7)", + "list": "NVARCHAR(MAX)", + "dict": "NVARCHAR(MAX)", + } + if field.type_ == "str": + width = "450" if field.field_type == "key" or field.is_indexed else "MAX" + return f"NVARCHAR({width}) COLLATE {_COLLATION}" + if field.type_ not in types: + raise NotImplementedError(f"Field '{field.name}' needs a supported explicit type; got '{field.type_}'.") + return types[field.type_] + + +def _validate_json(value: Any) -> None: + if value is None or type(value) in (str, bool, int): + return + if type(value) is float: + if not math.isfinite(value): + raise ValueError("JSON values must be finite.") + return + if isinstance(value, list): + for item in cast(list[Any], value): + _validate_json(item) + return + if isinstance(value, dict): + for key, item in cast(dict[Any, Any], value).items(): + if not isinstance(key, str): + raise TypeError("JSON object keys must be strings.") + _validate_json(item) + return + raise TypeError("JSON fields support only JSON scalars, lists, and string-keyed dictionaries.") + + +def _prepare_vector(field: VectorStoreField, value: Any) -> str: + if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): + raise TypeError(f"Vector field '{field.name}' requires a dense numeric sequence.") + vector = cast(Sequence[float | int], value) + if len(vector) != field.dimensions: + raise ValueError(f"Vector field '{field.name}' requires {field.dimensions} dimensions.") + components: list[float] = [] + for element in vector: + if type(element) not in (float, int): + raise TypeError(f"Vector field '{field.name}' requires numeric elements, not booleans or strings.") + try: + component = float(element) + except OverflowError as exc: + raise ValueError(f"Vector field '{field.name}' has an element outside the float32 range.") from exc + if not math.isfinite(component) or abs(component) > _FLOAT32_MAX: + raise ValueError(f"Vector field '{field.name}' requires finite float32 elements.") + components.append(component) + return json.dumps(components, separators=(",", ":"), allow_nan=False) + + +def _prepare_value(field: VectorStoreField, value: Any) -> Any: + if value is None: + return None + if field.field_type == "vector": + return _prepare_vector(field, value) + kind = field.type_ + if kind == "UUID" and isinstance(value, (str, UUID)): + return str(UUID(str(value))) + if kind == "int" and type(value) is int: + if not -(2**63) <= value < 2**63: + raise ValueError(f"Field '{field.name}' exceeds the SQL Server bigint range.") + return value + if kind == "float" and type(value) in (float, int): + try: + number = float(value) + except OverflowError as exc: + raise ValueError(f"Field '{field.name}' requires a finite number.") from exc + if not math.isfinite(number): + raise ValueError(f"Field '{field.name}' requires a finite number.") + return number + if kind == "bool" and type(value) is bool: + return value + if kind == "str" and isinstance(value, str): + if (field.field_type == "key" or field.is_indexed) and len(value.encode("utf-16-le")) // 2 > 450: + raise ValueError(f"Field '{field.name}' exceeds the indexed NVARCHAR(450) limit.") + if field.field_type == "key" and value.endswith(" "): + raise ValueError( + "SQL Server string keys cannot end in a space; SQL Server ignores trailing spaces in keys." + ) + return value + if kind == "bytes" and isinstance(value, bytes): + return value + if kind == "date" and (type(value) is date or isinstance(value, str)): + return date.fromisoformat(value) if isinstance(value, str) else value + if kind == "datetime" and isinstance(value, (datetime, str)): + resolved = datetime.fromisoformat(value.replace("Z", "+00:00")) if isinstance(value, str) else value + if resolved.tzinfo is None or resolved.utcoffset() is None: + raise ValueError(f"Datetime field '{field.name}' requires a timezone.") + return resolved.astimezone(timezone.utc).replace(tzinfo=None) + if (kind == "list" and isinstance(value, list)) or (kind == "dict" and isinstance(value, dict)): + _validate_json(value) + return json.dumps(value, separators=(",", ":"), ensure_ascii=False, allow_nan=False) + raise TypeError(f"Field '{field.name}' requires a value of type '{kind}'.") + + +def _parse_value(field: VectorStoreField, value: Any) -> Any: + if value is None: + return None + kind = field.type_ + if field.field_type == "vector" or kind in ("list", "dict"): + if not isinstance(value, str): + raise IntegrationInvalidResponseException(f"SQL Server returned a non-JSON value for '{field.name}'.") + try: + parsed: Any = json.loads(value) + except json.JSONDecodeError as exc: + raise IntegrationInvalidResponseException(f"SQL Server returned invalid JSON for '{field.name}'.") from exc + if field.field_type == "vector": + if not isinstance(parsed, list): + raise IntegrationInvalidResponseException(f"SQL Server returned an invalid vector for '{field.name}'.") + vector_values = cast(list[Any], parsed) + if len(vector_values) != field.dimensions: + raise IntegrationInvalidResponseException(f"SQL Server returned an invalid vector for '{field.name}'.") + components: list[float] = [] + for item in vector_values: + if type(item) not in (int, float): + raise IntegrationInvalidResponseException( + f"SQL Server returned invalid vector elements for '{field.name}'." + ) + try: + number = float(item) + except OverflowError as exc: + raise IntegrationInvalidResponseException( + f"SQL Server returned invalid vector elements for '{field.name}'." + ) from exc + if not math.isfinite(number) or abs(number) > _FLOAT32_MAX: + raise IntegrationInvalidResponseException( + f"SQL Server returned invalid vector elements for '{field.name}'." + ) + components.append(number) + return components + if not isinstance(parsed, list if kind == "list" else dict): + raise IntegrationInvalidResponseException(f"SQL Server returned the wrong JSON type for '{field.name}'.") + return cast(list[Any] | dict[str, Any], parsed) + if kind == "UUID": + try: + return UUID(str(value)) + except ValueError as exc: + raise IntegrationInvalidResponseException( + f"SQL Server returned an invalid UUID for '{field.name}'." + ) from exc + if kind == "datetime": + if not isinstance(value, datetime): + raise IntegrationInvalidResponseException(f"SQL Server returned an invalid datetime for '{field.name}'.") + return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) + if kind == "bool" and type(value) is int and value in (0, 1): + return bool(value) + return value + + +def _filter_field(definition: VectorStoreCollectionDefinition, name: str) -> VectorStoreField: + if "." in name: + raise NotImplementedError("SQL Server filters and ordering do not support nested field paths.") + field = definition.try_get_field(name) + if field is None: + raise ValueError(f"Unknown SQL Server field '{name}'.") + if field.field_type == "vector": + raise NotImplementedError("Filtering and ordering vector columns is not supported.") + return field + + +def _numeric_filter_value(value: int | float) -> int | float: + try: + if not math.isfinite(value): + raise ValueError("Numeric filter values must be finite.") + except OverflowError as exc: + raise ValueError("Numeric filter values must fit in the SQL Server numeric range.") from exc + if type(value) is int and not -(2**63) <= value < 2**63: + raise ValueError("Integer filter values must fit in the SQL Server bigint range.") + return value + + +class _FilterCompiler: + """Compile bounded data-only filter expressions into parameterized two-valued T-SQL.""" + + def __init__(self, definition: VectorStoreCollectionDefinition, *, alias: str = "") -> None: + self.definition = definition + self.alias = alias + self.parameters: list[Any] = [] + + def compile(self, expression: FilterExpression) -> tuple[str, list[Any]]: + """Compile one filter with its parameters in placeholder order.""" + self.parameters = [] + return self._condition(expression), self.parameters + + def _bind(self, value: Any) -> str: + if len(self.parameters) >= MAX_PARAMETERS: + raise ValueError(f"SQL Server queries support at most {MAX_PARAMETERS} bound parameters.") + self.parameters.append(value) + return "?" + + def _equality(self, field: VectorStoreField, column: str, value: Any) -> str: + if value is None: + return f"{column} IS NULL" + kind = field.type_ + if kind in ("list", "dict"): + raise NotImplementedError("Equality filtering JSON fields is not supported.") + if kind == "bool" and type(value) is not bool: + return "1 = 0" + if kind != "bool" and isinstance(value, bool): + return "1 = 0" + if kind in ("int", "float") and type(value) in (int, float): + adapted = _numeric_filter_value(value) + elif ( + (kind == "str" and isinstance(value, str)) + or (kind == "bytes" and isinstance(value, bytes)) + or (kind == "UUID" and isinstance(value, (UUID, str))) + or (kind == "date" and (type(value) is date or isinstance(value, str))) + or (kind == "datetime" and isinstance(value, (datetime, str))) + or (kind == "bool" and type(value) is bool) + ): + adapted = _prepare_value(field, value) + else: + return "1 = 0" + if kind == "str": + # SQL Server pads trailing spaces even with binary collations; compare bytes instead. + return ( + f"({column} IS NOT NULL AND CONVERT(VARBINARY(MAX), {column}) = " + f"CONVERT(VARBINARY(MAX), CONVERT(NVARCHAR(MAX), {self._bind(adapted)})))" + ) + return f"({column} IS NOT NULL AND {column} = {self._bind(adapted)})" + + def _condition(self, expression: FilterExpression) -> str: + if isinstance(expression, FilterGroup): + conditions = [self._condition(child) for child in expression.filters] + if expression.operator == "not": + return f"(NOT ({conditions[0]}))" + joiner = " AND " if expression.operator == "and" else " OR " + return f"({joiner.join(conditions)})" + field = _filter_field(self.definition, expression.field_name) + column = f"{self.alias}{_quote_identifier(field.storage_name or field.name)}" + op, value = expression.operator, expression.value + if op == "exists": + return "1 = 1" # SQL columns exist even when their values are NULL. + if op == "is_null": + return f"{column} IS NULL" + if op == "is_not_null": + return f"{column} IS NOT NULL" + if op in ("eq", "ne"): + equality = self._equality(field, column, value) + return equality if op == "eq" else f"(NOT ({equality}))" + if op in ("in", "not_in"): + choices = " OR ".join(self._equality(field, column, item) for item in value) or "1 = 0" + membership = f"({choices})" if op == "in" else f"(NOT ({choices}))" + return f"({column} IS NOT NULL AND {membership})" + if op in _ORDER_OPERATORS or op == "between": + if field.type_ not in ("int", "float", "date", "datetime"): + raise NotImplementedError(f"Ordered SQL Server filtering is not supported for '{field.type_}'.") + values: Sequence[Any] = value if op == "between" else [value] + if any(item is None or isinstance(item, bool) for item in values): + raise TypeError("Ordered filter operands must be non-null scalars of the column's type.") + adapted = [ + _numeric_filter_value(item) + if field.type_ in ("int", "float") and type(item) in (int, float) + else _prepare_value(field, item) + for item in values + ] + comparison = ( + f"{column} BETWEEN {self._bind(adapted[0])} AND {self._bind(adapted[1])}" + if op == "between" + else f"{column} {_ORDER_OPERATORS[op]} {self._bind(adapted[0])}" + ) + return f"({column} IS NOT NULL AND {comparison})" + if op in ("contains_text", "starts_with", "ends_with"): + if field.type_ != "str": + raise TypeError("Text filtering requires a string column.") + if not isinstance(value, str): + raise TypeError("Text filtering requires a string operand.") + if "\0" in value: + raise ValueError("SQL Server LIKE does not support NUL characters.") + escaped = value.replace("!", "!!").replace("%", "!%").replace("_", "!_").replace("[", "[[]") + pattern = ("%" if op != "starts_with" else "") + escaped + ("%" if op != "ends_with" else "") + if len(pattern.encode("utf-16-le")) > 8000: + raise ValueError("SQL Server LIKE patterns cannot exceed 8000 bytes.") + return f"({column} IS NOT NULL AND {column} COLLATE {_COLLATION} LIKE {self._bind(pattern)} ESCAPE '!')" + raise NotImplementedError(f"Unsupported SQL Server filter operator '{op}'.") diff --git a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py new file mode 100644 index 00000000000..f71e3f57032 --- /dev/null +++ b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py @@ -0,0 +1,760 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Asynchronous Agent Framework collections backed by SQL Server VECTOR columns.""" + +from __future__ import annotations + +import asyncio +import hashlib +import math +from collections.abc import AsyncGenerator, Mapping, Sequence +from concurrent.futures import ThreadPoolExecutor +from contextlib import asynccontextmanager +from dataclasses import replace +from typing import Any, ClassVar, Generic, Protocol, TypeAlias, cast + +import aioodbc +import pyodbc +from agent_framework import ( + BaseVectorCollection, + BaseVectorSearch, + BaseVectorStore, + SearchResults, + SecretString, + VectorStoreCollectionDefinition, + load_settings, +) +from agent_framework._vector_filters import FilterExpression +from agent_framework._vectors import EmbeddingClient, SearchType, Vector +from agent_framework.exceptions import IntegrationException, IntegrationInvalidResponseException +from typing_extensions import Self, TypedDict, TypeVar + +from ._sql import ( + MAX_PARAMETERS, + _column_type, # pyright: ignore[reportPrivateUsage] + _filter_field, # pyright: ignore[reportPrivateUsage] + _FilterCompiler, # pyright: ignore[reportPrivateUsage] + _metric_for, # pyright: ignore[reportPrivateUsage] + _parse_value, # pyright: ignore[reportPrivateUsage] + _prepare_value, # pyright: ignore[reportPrivateUsage] + _quote_identifier, # pyright: ignore[reportPrivateUsage] +) + +KeyT = TypeVar("KeyT", default=Any) +ModelT = TypeVar("ModelT", default=Any) +_KEY_BATCH_SIZE = 1000 +_SAVEPOINT = "af_vector_operation" +SqlServerClient: TypeAlias = aioodbc.Connection | aioodbc.Pool + + +class _Cursor(Protocol): + async def execute(self, sql: str, *parameters: Any) -> Any: ... + + async def fetchone(self) -> Sequence[Any] | None: ... + + async def fetchall(self) -> Sequence[Sequence[Any]]: ... + + async def close(self) -> None: ... + + +@asynccontextmanager +async def _cursor(connection: aioodbc.Connection) -> AsyncGenerator[_Cursor]: + # aioodbc's cursor context manager commits its connection on exit, including borrowed transactions. + cursor = cast(_Cursor, await connection.cursor()) # pyright: ignore[reportUnknownMemberType] + try: + yield cursor + finally: + await cursor.close() + + +class SqlServerSettings(TypedDict, total=False): + """Connection settings resolved from explicit values, a selected .env file, or ``SQL_SERVER_`` variables.""" + + connection_string: SecretString | None + """Driver connection string, resolved from ``SQL_SERVER_CONNECTION_STRING`` and kept masked.""" + + +def _validate_options(options: Mapping[str, Any] | None) -> None: + if options: + raise NotImplementedError(f"Unsupported SQL Server operation option(s): {', '.join(sorted(options))}.") + + +async def _execute(cursor: _Cursor, statement: str, parameters: Sequence[Any] = ()) -> None: + if parameters: + await cursor.execute(statement, tuple(parameters)) + else: + await cursor.execute(statement) + + +async def _rows(cursor: _Cursor, count: int) -> list[Sequence[Any]]: + result = list(await cursor.fetchall()) + for row in result: + if len(row) != count: + raise IntegrationInvalidResponseException("SQL Server returned a row with an unexpected column count.") + return result + + +def _check_parameter_count(parameters: Sequence[Any]) -> None: + if len(parameters) > MAX_PARAMETERS: + raise ValueError(f"SQL Server queries support at most {MAX_PARAMETERS} bound parameters.") + + +class _Client: + """Own a lazy pool and its dedicated ODBC worker, or borrow an async connection/pool.""" + + def __init__(self, connection_string: SecretString | None, client: SqlServerClient | None) -> None: + if (connection_string is None) == (client is None): + raise ValueError("Supply exactly one of connection_string or client.") + if connection_string is not None and not connection_string.get_secret_value().strip(): + raise ValueError("connection_string must not be empty.") + if client is not None and not isinstance(client, (aioodbc.Connection, aioodbc.Pool)): + raise TypeError("client must be an aioodbc Connection or Pool.") + self.connection_string = connection_string + self.client = client + self.closed = False + self._pool: aioodbc.Pool | None = None + self._executor: ThreadPoolExecutor | None = None + self._init_lock = asyncio.Lock() + self._borrowed_lock = asyncio.Lock() + + def ensure_open(self) -> None: + if self.closed: + raise RuntimeError("The SQL Server client is closed.") + + async def _owned_pool(self) -> aioodbc.Pool: + async with self._init_lock: + self.ensure_open() + if self._pool is not None: + return self._pool + if self.connection_string is None: + raise RuntimeError("No SQL Server connection string is configured.") + executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="af-sql-server") + try: + self._pool = await aioodbc.create_pool( + dsn=self.connection_string.get_secret_value(), + minsize=1, + maxsize=1, + autocommit=False, + executor=executor, + ) + except BaseException: + await asyncio.to_thread(executor.shutdown, wait=True) + raise + self._executor = executor + return self._pool + + @asynccontextmanager + async def _operation(self, connection: aioodbc.Connection, *, managed: bool, write: bool) -> AsyncGenerator[None]: + if managed: + try: + yield + await connection.commit() + except BaseException: + await connection.rollback() + raise + return + if not write: + yield + return + async with _cursor(connection) as cursor: + await _execute(cursor, "SELECT @@TRANCOUNT") + count = await cursor.fetchone() + if count is None or type(count[0]) is not int: + raise IntegrationInvalidResponseException("SQL Server did not return a transaction count.") + started = count[0] == 0 + await _execute(cursor, "BEGIN TRANSACTION" if started else f"SAVE TRANSACTION {_SAVEPOINT}") + try: + yield + if started: + async with _cursor(connection) as cursor: + await _execute(cursor, "COMMIT TRANSACTION") + except BaseException as exc: + async with _cursor(connection) as cursor: + if started: + await _execute(cursor, "ROLLBACK TRANSACTION") + else: + await _execute(cursor, "SELECT XACT_STATE()") + state = await cursor.fetchone() + if state is None or state[0] != 1: + raise IntegrationException( + "The caller's SQL Server transaction cannot be rolled back to its savepoint; " + "the caller must roll back the transaction." + ) from exc + await _execute(cursor, f"ROLLBACK TRANSACTION {_SAVEPOINT}") + raise + + @asynccontextmanager + async def connection(self, *, write: bool = False) -> AsyncGenerator[aioodbc.Connection]: + """Acquire one connection, keeping borrowed transactions under the caller's control.""" + self.ensure_open() + try: + if self.client is None: + pool = await self._owned_pool() + async with pool.acquire() as connection, self._operation(connection, managed=True, write=write): + yield connection + elif isinstance(self.client, aioodbc.Pool): + async with self.client.acquire() as connection: + self.ensure_open() + if cast(bool, connection.autocommit): # pyright: ignore[reportUnknownMemberType] + raise ValueError("Borrowed aioodbc pools must use autocommit=False for atomic operations.") + async with self._operation(connection, managed=True, write=write): + yield connection + else: + async with self._borrowed_lock, self._operation(self.client, managed=False, write=write): + self.ensure_open() + yield self.client + except pyodbc.Error as exc: + raise IntegrationException("SQL Server operation failed; inspect the chained driver exception.") from exc + + async def close(self) -> None: + """Close the owned pool and worker, never an injected client or pool.""" + async with self._init_lock: + if self.closed: + return + if isinstance(self.client, aioodbc.Connection): + async with self._borrowed_lock: + self.closed = True + return + self.closed = True + try: + if self._pool is not None: + self._pool.close() + await self._pool.wait_closed() + finally: + if self._executor is not None: + await asyncio.to_thread(self._executor.shutdown, wait=True) + + +def _create_client( + connection_string: str | SecretString | None, + *, + client: SqlServerClient | None, + env_file_path: str | None, + env_file_encoding: str | None, +) -> _Client: + if client is not None: + if connection_string is not None or env_file_path is not None or env_file_encoding is not None: + raise ValueError("client cannot be combined with connection_string, env_file_path, or env_file_encoding.") + return _Client(None, client) + settings = load_settings( + SqlServerSettings, + env_prefix="SQL_SERVER_", + connection_string=connection_string, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + resolved = settings.get("connection_string") + if resolved is not None and not isinstance(resolved, SecretString): + raise TypeError("connection_string must be a string or SecretString.") + return _Client(resolved, None) + + +class SqlServerCollection( + BaseVectorCollection[KeyT, ModelT], + BaseVectorSearch[KeyT, ModelT], + Generic[KeyT, ModelT], +): + """Store typed rows and perform exact searches on SQL Server native VECTOR columns. + + Tables are created in an existing schema and are never migrated. The owned + single-connection pool is lazy; injected connections and pools remain caller-owned. + """ + + supported_key_types: ClassVar[set[str] | None] = {"str", "int", "UUID"} + supported_vector_types: ClassVar[set[str] | None] = {"float", "float32"} + supported_search_types: ClassVar[set[SearchType]] = {"vector"} + + def __init__( + self, + record_type: type[ModelT], + *, + connection_string: str | SecretString | None = None, + client: SqlServerClient | None = None, + schema: str = "dbo", + definition: VectorStoreCollectionDefinition | None = None, + collection_name: str | None = None, + embedding_generator: EmbeddingClient | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + _shared_client: _Client | None = None, + ) -> None: + """Initialize a collection without connecting to the database. + + Args: + record_type: Decorated/registered model type, or ``dict``. + connection_string: Driver connection string for owned connections, or ``SQL_SERVER_CONNECTION_STRING``. + client: An open caller-owned aioodbc connection or pool instead of a connection string. + schema: Existing database schema containing the table. + definition: Explicit field definition for dictionary records. + collection_name: Table name overriding the model definition. + embedding_generator: Default local embedding generator. + env_file_path: Optional .env file; cannot be combined with an injected client. + env_file_encoding: Encoding of the selected .env file. + _shared_client: Internal shared client owned by a ``SqlServerStore``. + """ + super().__init__( + record_type, + definition=definition, + collection_name=collection_name, + embedding_generator=embedding_generator, + managed_client=client is None and _shared_client is None, + ) + self.schema = schema + self._table = f"{_quote_identifier(schema)}.{_quote_identifier(self.collection_name)}" + self._fields = tuple( + replace(field, provider_annotations=field.provider_annotations) for field in self.definition.fields + ) + for field in self._fields: + _quote_identifier(field.storage_name or field.name) + _column_type(field) + if field.is_full_text_indexed: + raise NotImplementedError("SQL Server full-text and keyword-hybrid search are not supported.") + if any(name.startswith("sql_server.") for name in field.provider_annotations): + raise NotImplementedError("SQL Server provider annotations are not supported.") + if field.field_type == "vector": + _metric_for(field) + if field.index_kind not in ("default", "flat"): + raise NotImplementedError("SQL Server approximate vector indexes are not supported.") + elif field.is_indexed and field.field_type == "data" and field.type_ in ("bytes", "list", "dict"): + raise NotImplementedError(f"SQL Server cannot index data fields of type '{field.type_}'.") + if _shared_client is not None: + if ( + client is not None + or connection_string is not None + or env_file_path is not None + or env_file_encoding is not None + ): + raise ValueError("A store's shared client cannot be combined with other connection settings.") + self._client = _shared_client + else: + self._client = _create_client( + connection_string, client=client, env_file_path=env_file_path, env_file_encoding=env_file_encoding + ) + self._shared_client = _shared_client is not None + + async def __aenter__(self) -> Self: + """Enter the collection context.""" + return self + + async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: + """Release collection-owned resources.""" + await self.close() + + async def close(self) -> None: + """Close only this collection's client, never a store's or the caller's connection.""" + if not self._shared_client: + await self._client.close() + + def _column_names(self, include_vectors: bool) -> list[str]: + return [ + field.storage_name or field.name + for field in self._fields + if include_vectors or field.field_type != "vector" + ] + + def _column_definitions(self) -> str: + definitions: list[str] = [] + for field in self._fields: + column = _quote_identifier(field.storage_name or field.name) + data_type = _column_type(field) + if field.field_type == "key": + generated = "" + if field.is_auto_generated: + generated = { + "int": " IDENTITY(1,1)", + "UUID": " DEFAULT NEWID()", + "str": " DEFAULT CONVERT(NVARCHAR(36), NEWID())", + }[field.type_ or ""] + definitions.append(f"{column} {data_type}{generated} NOT NULL PRIMARY KEY") + else: + definitions.append(f"{column} {data_type} NULL") + return ", ".join(definitions) + + @staticmethod + def _index_name(schema: str, table: str, column: str) -> str: + digest = hashlib.sha256(f"{schema}\0{table}\0{column}".encode()).hexdigest()[:32] + return f"af_sql_{digest}" + + async def ensure_collection_exists(self, *, operation_options: Mapping[str, Any] | None = None) -> None: + """Create a table and requested scalar indexes, without modifying existing tables.""" + _validate_options(operation_options) + self._client.ensure_open() + statement = ( + "IF NOT EXISTS (SELECT 1 FROM sys.tables AS t " # nosec B608 + "JOIN sys.schemas AS s ON s.schema_id = t.schema_id " + "WHERE s.name = ? AND t.name = ?) " + f"BEGIN CREATE TABLE {self._table} ({self._column_definitions()}) END" + ) + + async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: + await _execute(cursor, statement, [self.schema, self.collection_name]) + for field in self._fields: + if field.field_type != "data" or not field.is_indexed: + continue + name = field.storage_name or field.name + index_name = self._index_name(self.schema, self.collection_name, name) + index_statement = ( + "IF NOT EXISTS (SELECT 1 FROM sys.indexes AS i " # nosec B608 + "JOIN sys.tables AS t ON t.object_id = i.object_id " + "JOIN sys.schemas AS s ON s.schema_id = t.schema_id " + "WHERE s.name = ? AND t.name = ? AND i.name = ?) " + f"CREATE INDEX {_quote_identifier(index_name)} ON {self._table} ({_quote_identifier(name)})" + ) + await _execute(cursor, index_statement, [self.schema, self.collection_name, index_name]) + + async def collection_exists(self, *, operation_options: Mapping[str, Any] | None = None) -> bool: + """Return whether a base table exists in the configured schema.""" + _validate_options(operation_options) + + async with self._client.connection() as connection, _cursor(connection) as cursor: + await _execute( + cursor, + "SELECT 1 FROM sys.tables AS t JOIN sys.schemas AS s ON s.schema_id = t.schema_id " + "WHERE s.name = ? AND t.name = ?", + [self.schema, self.collection_name], + ) + return await cursor.fetchone() is not None + + async def ensure_collection_deleted(self, *, operation_options: Mapping[str, Any] | None = None) -> None: + """Drop only the table in this schema; never drop the schema or other tables.""" + _validate_options(operation_options) + async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: + await _execute(cursor, f"DROP TABLE IF EXISTS {self._table}") + + def _deserialize_store_models_to_dicts( + self, records: Sequence[Any], *, context: Mapping[str, Any] | None = None + ) -> Sequence[dict[str, Any]]: + result = super()._deserialize_store_models_to_dicts(records, context=context) + for record in result: + for field in self._fields: + name = field.storage_name or field.name + if name in record: + record[name] = _parse_value(field, record[name]) + return result + + def _prepare_key(self, key: Any) -> Any: + if key is None: + raise ValueError("SQL Server keys cannot be null.") + return _prepare_value(self.definition.key_field, key) + + def _parsed_key(self, key: Any) -> Any: + return _parse_value(self.definition.key_field, key) + + async def _inner_upsert( + self, records: Sequence[Any], *, operation_options: Mapping[str, Any] | None = None + ) -> Sequence[KeyT]: + _validate_options(operation_options) + self._client.ensure_open() + prepared: list[dict[str, Any]] = [] + for record in records: + row: dict[str, Any] = {} + for field in self._fields: + name = field.storage_name or field.name + if name not in record and field.field_type == "key" and field.is_auto_generated: + continue + row[name] = ( + self._prepare_key(record[name]) + if field.field_type == "key" + else _prepare_value(field, record[name]) + ) + prepared.append(row) + if not prepared: + return [] + key_name = self.definition.key_field_storage_name + key_column = _quote_identifier(key_name) + + async def upsert(cursor: _Cursor) -> list[KeyT]: + keys: list[KeyT] = [] + for row in prepared: + names = tuple(row) + if key_name in row: + await _execute( + cursor, + f"SELECT {key_column} FROM {self._table} WITH (UPDLOCK, HOLDLOCK) WHERE {key_column} = ?", # nosec B608 + [row[key_name]], + ) + if await cursor.fetchone() is not None: + updates = [name for name in names if name != key_name] + assignments = ", ".join(f"{_quote_identifier(name)} = ?" for name in updates) + if not assignments: + assignments = f"{key_column} = {key_column}" + parameters = [*(row[name] for name in updates), row[key_name]] + _check_parameter_count(parameters) + await _execute( + cursor, + f"UPDATE {self._table} SET {assignments} OUTPUT INSERTED.{key_column} " # nosec B608 + f"WHERE {key_column} = ?", + parameters, + ) + result = await cursor.fetchone() + if result is None: + raise IntegrationInvalidResponseException("SQL Server did not return an updated key.") + keys.append(cast(KeyT, self._parsed_key(result[0]))) + continue + if self.definition.key_field.is_auto_generated and self.definition.key_field.type_ == "int": + raise NotImplementedError( + "SQL Server IDENTITY columns cannot insert an explicit new key; omit it to generate a key." + ) + if names: + columns = ", ".join(_quote_identifier(name) for name in names) + markers = ", ".join("?" for _ in names) + statement = f"INSERT INTO {self._table} ({columns}) OUTPUT INSERTED.{key_column} VALUES ({markers})" # nosec B608 + parameters = [row[name] for name in names] + _check_parameter_count(parameters) + await _execute(cursor, statement, parameters) + else: + await _execute(cursor, f"INSERT INTO {self._table} OUTPUT INSERTED.{key_column} DEFAULT VALUES") + result = await cursor.fetchone() + if result is None: + raise IntegrationInvalidResponseException("SQL Server did not return an inserted key.") + keys.append(cast(KeyT, self._parsed_key(result[0]))) + return keys + + async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: + return await upsert(cursor) + + def _order_by(self, order_by: Mapping[str, bool] | None) -> str: + parts: list[str] = [] + for name, ascending in (order_by or {}).items(): + if type(ascending) is not bool: + raise TypeError("Order directions must be booleans.") + field = _filter_field(self.definition, name) + if field.type_ in ("list", "dict", "bytes"): + raise NotImplementedError(f"SQL Server ordering is not supported for '{field.type_}'.") + column = _quote_identifier(field.storage_name or field.name) + parts.append(f"CASE WHEN {column} IS NULL THEN 1 ELSE 0 END") + parts.append(f"{column} {'ASC' if ascending else 'DESC'}") + if self.definition.key_field.name not in (order_by or {}): + parts.append(f"{_quote_identifier(self.definition.key_field_storage_name)} ASC") + return ", ".join(parts) + + async def _inner_get( + self, + *, + keys: Sequence[KeyT] | None = None, + filter: FilterExpression | None = None, + top: int = 10, + skip: int = 0, + order_by: Mapping[str, bool] | None = None, + include_vectors: bool = False, + operation_options: Mapping[str, Any] | None = None, + ) -> Sequence[Any]: + _validate_options(operation_options) + self._client.ensure_open() + columns = self._column_names(include_vectors) + select = f"SELECT {', '.join(_quote_identifier(name) for name in columns)} FROM {self._table}" # nosec B608 + if keys is not None: + if order_by: + raise ValueError("order_by applies only to filtered retrieval, not key lookup.") + prepared_keys = [self._prepare_key(key) for key in keys] + if not prepared_keys: + return [] + + async def get_keys(cursor: _Cursor) -> list[dict[str, Any]]: + found: dict[Any, dict[str, Any]] = {} + key_column = _quote_identifier(self.definition.key_field_storage_name) + for offset in range(0, len(prepared_keys), _KEY_BATCH_SIZE): + batch = prepared_keys[offset : offset + _KEY_BATCH_SIZE] + markers = ", ".join("?" for _ in batch) + await _execute(cursor, f"{select} WHERE {key_column} IN ({markers})", batch) + for row in await _rows(cursor, len(columns)): + record = dict(zip(columns, row, strict=True)) + found[self._parsed_key(record[self.definition.key_field_storage_name])] = record + return [found[key] for raw in prepared_keys if (key := self._parsed_key(raw)) in found] + + async with self._client.connection() as connection, _cursor(connection) as cursor: + return await get_keys(cursor) + where, parameters = ("1 = 1", []) if filter is None else _FilterCompiler(self.definition).compile(filter) + parameters.extend((skip, top)) + _check_parameter_count(parameters) + statement = f"{select} WHERE {where} ORDER BY {self._order_by(order_by)} OFFSET ? ROWS FETCH NEXT ? ROWS ONLY" + + async with self._client.connection() as connection, _cursor(connection) as cursor: + await _execute(cursor, statement, parameters) + return [dict(zip(columns, row, strict=True)) for row in await _rows(cursor, len(columns))] + + async def _inner_delete(self, keys: Sequence[KeyT], *, operation_options: Mapping[str, Any] | None = None) -> None: + _validate_options(operation_options) + self._client.ensure_open() + prepared = [self._prepare_key(key) for key in keys] + if not prepared: + return + key_column = _quote_identifier(self.definition.key_field_storage_name) + + async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: + for offset in range(0, len(prepared), _KEY_BATCH_SIZE): + batch = prepared[offset : offset + _KEY_BATCH_SIZE] + markers = ", ".join("?" for _ in batch) + await _execute(cursor, f"DELETE FROM {self._table} WHERE {key_column} IN ({markers})", batch) # nosec B608 + + async def _inner_search( + self, + *, + search_type: SearchType, + filter: FilterExpression | None = None, + values: Any | None = None, + vector: Vector | None = None, + top: int = 3, + skip: int = 0, + include_vectors: bool = False, + vector_property_name: str | None = None, + additional_property_name: str | None = None, + score_threshold: float | None = None, + operation_options: Mapping[str, Any] | None = None, + ) -> SearchResults[Any]: + _validate_options(operation_options) + if search_type != "vector" or additional_property_name is not None: + raise NotImplementedError("SQL Server supports dense vector search only, not keyword-hybrid search.") + if vector is None: + raise NotImplementedError("SQL Server requires a vector or a local embedding generator.") + self._client.ensure_open() + resolved = self.definition.try_get_vector_field(vector_property_name) + if resolved is None: + raise ValueError("Select a vector_property_name from the collection definition.") + field = next(item for item in self._fields if item.name == resolved.name) + metric, result_kind = _metric_for(field) + where, filter_params = ( + ("1 = 1", []) if filter is None else _FilterCompiler(self.definition, alias="t.").compile(filter) + ) + column_names = self._column_names(include_vectors) + projections = ", ".join(f"t.{_quote_identifier(name)}" for name in column_names) + vector_column = f"t.{_quote_identifier(field.storage_name or field.name)}" + key_column = f"t.{_quote_identifier(self.definition.key_field_storage_name)}" + dimensions = field.dimensions + statement = ( + f"SELECT {projections}, d.[distance] FROM {self._table} AS t " # nosec B608 + f"CROSS APPLY (VALUES (VECTOR_DISTANCE('{metric}', CAST(? AS VECTOR({dimensions})), " + f"{vector_column}))) AS d([distance]) " + f"WHERE {vector_column} IS NOT NULL AND ({where}) AND d.[distance] IS NOT NULL" + ) + parameters: list[Any] = [_prepare_value(field, vector), *filter_params] + if score_threshold is not None: + if type(score_threshold) not in (int, float): + raise ValueError("score_threshold must be a finite number.") + try: + threshold = float(score_threshold) + except OverflowError as exc: + raise ValueError("score_threshold must be a finite number.") from exc + if not math.isfinite(threshold): + raise ValueError("score_threshold must be a finite number.") + cutoff = ( + 1 - threshold + if result_kind == "similarity" + else (-threshold if result_kind == "negative" else threshold) + ) + statement += " AND d.[distance] <= ?" + parameters.append(cutoff) + statement += f" ORDER BY d.[distance] ASC, {key_column} ASC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY" + parameters.extend((skip, top)) + _check_parameter_count(parameters) + + async def search(cursor: _Cursor) -> list[dict[str, Any]]: + await _execute(cursor, statement, parameters) + results: list[dict[str, Any]] = [] + for row in await _rows(cursor, len(column_names) + 1): + distance = row[-1] + if type(distance) not in (float, int) or not math.isfinite(distance): + raise IntegrationInvalidResponseException("SQL Server returned a non-finite vector distance.") + score = ( + 1 - distance + if result_kind == "similarity" + else -distance + if result_kind == "negative" + else distance + ) + results.append({"record": dict(zip(column_names, row[:-1], strict=True)), "score": float(score)}) + return results + + async with self._client.connection() as connection, _cursor(connection) as cursor: + rows = await search(cursor) + return SearchResults( + rows, + metadata={"distance_function": field.distance_function or "DEFAULT", "approximate": False}, + ) + + def _get_record_from_result(self, result: Any) -> Any: + return result["record"] + + def _get_score_from_result(self, result: Any) -> float | None: + return cast(float, result["score"]) + + +class SqlServerStore(BaseVectorStore): + """Create SQL Server collections sharing resolved settings or a caller-owned connection.""" + + def __init__( + self, + *, + connection_string: str | SecretString | None = None, + client: SqlServerClient | None = None, + schema: str = "dbo", + embedding_generator: EmbeddingClient | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize a store without connecting to SQL Server. + + Args: + connection_string: Driver connection string, or ``SQL_SERVER_CONNECTION_STRING``. + client: An open caller-owned aioodbc connection or pool. + schema: Existing database schema used by all collections. + embedding_generator: Default local embedding generator. + env_file_path: Optional .env file, incompatible with an injected client. + env_file_encoding: Encoding of the selected .env file. + """ + super().__init__(embedding_generator=embedding_generator, managed_client=client is None) + _quote_identifier(schema) + self.schema = schema + self._client = _create_client( + connection_string, client=client, env_file_path=env_file_path, env_file_encoding=env_file_encoding + ) + + def get_collection( + self, + record_type: type[ModelT], + *, + definition: VectorStoreCollectionDefinition | None = None, + collection_name: str | None = None, + embedding_generator: EmbeddingClient | None = None, + ) -> SqlServerCollection[Any, ModelT]: + """Create a collection borrowing this store's client and lifecycle.""" + return SqlServerCollection( + record_type, + schema=self.schema, + definition=definition, + collection_name=collection_name, + embedding_generator=embedding_generator if embedding_generator is not None else self.embedding_generator, + _shared_client=self._client, + ) + + async def list_collection_names(self, *, operation_options: Mapping[str, Any] | None = None) -> Sequence[str]: + """List base tables in the configured schema.""" + _validate_options(operation_options) + + async with self._client.connection() as connection, _cursor(connection) as cursor: + await _execute( + cursor, + "SELECT t.name FROM sys.tables AS t JOIN sys.schemas AS s ON s.schema_id = t.schema_id " + "WHERE s.name = ? ORDER BY t.name", + [self.schema], + ) + names = await _rows(cursor, 1) + if any(not isinstance(row[0], str) for row in names): + raise IntegrationInvalidResponseException("SQL Server returned a non-string table name.") + return [cast(str, row[0]) for row in names] + + async def _inner_ensure_collection_deleted( + self, collection_name: str, *, operation_options: Mapping[str, Any] | None = None + ) -> None: + _validate_options(operation_options) + table = f"{_quote_identifier(self.schema)}.{_quote_identifier(collection_name)}" + async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: + await _execute(cursor, f"DROP TABLE IF EXISTS {table}") + + async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: + """Release store-owned resources.""" + await self.close() + + async def close(self) -> None: + """Close the store wrapper without closing caller-owned connections.""" + await self._client.close() diff --git a/python/packages/sql-server/agent_framework_sql_server/py.typed b/python/packages/sql-server/agent_framework_sql_server/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/packages/sql-server/pyproject.toml b/python/packages/sql-server/pyproject.toml new file mode 100644 index 00000000000..580e9568881 --- /dev/null +++ b/python/packages/sql-server/pyproject.toml @@ -0,0 +1,65 @@ +[project] +name = "agent-framework-sql-server" +description = "SQL Server native vector integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "af-support@microsoft.com" }] +readme = "README.md" +requires-python = ">=3.10" +version = "1.0.0a260923" +license-files = ["LICENSE"] +urls.homepage = "https://aka.ms/agent-framework" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [ + "agent-framework-core>=1.19.0,<2", + "aioodbc>=0.5.0,<0.6", + "pyodbc>=5.3.0,<6", +] + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.ruff.lint.extend-per-file-ignores] +"samples/**" = ["D", "INP", "commented-out-code", "RUF", "S", "print", "CPY"] +# SQL structure contains only quoted, validated identifiers and fixed metric/type tokens; values are always bound. +"agent_framework_sql_server/_vector_store.py" = ["S608"] + +[tool.pyright] +extends = "../../pyproject.toml" +include = ["agent_framework_sql_server"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +timeout = 120 +markers = ["integration: requires an explicitly designated SQL Server 2025 or Azure SQL test database"] + +[tool.coverage.run] +omit = ["**/__init__.py"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" + +[tool.poe.tasks.test] +cmd = 'pytest -m "not integration" --cov=agent_framework_sql_server --cov-report=term-missing:skip-covered tests' + +[tool.poe.tasks.test-integration] +cmd = 'pytest -m integration tests' + +[build-system] +requires = ["flit-core>=3.11,<4.0"] +build-backend = "flit_core.buildapi" diff --git a/python/packages/sql-server/samples/sql_server_vectors.py b/python/packages/sql-server/samples/sql_server_vectors.py new file mode 100644 index 00000000000..a4ced8b660e --- /dev/null +++ b/python/packages/sql-server/samples/sql_server_vectors.py @@ -0,0 +1,70 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from typing import Annotated +from uuid import uuid4 + +from agent_framework import Filter, VectorStoreField, vectorstoremodel + +from agent_framework_sql_server import SqlServerStore + +""" +Search native VECTOR columns in SQL Server 2025 or Azure SQL. + +Set SQL_SERVER_CONNECTION_STRING to a connection string for an explicitly +designated development database. The example creates a unique table in the +existing dbo schema and drops only that table. No embedding service is needed. + +Run: uv run --package agent-framework-sql-server python packages/sql-server/samples/sql_server_vectors.py +""" + + +@vectorstoremodel +@dataclass +class Note: + id: Annotated[str, VectorStoreField("key")] + text: Annotated[str, VectorStoreField("data", storage_name="body")] + category: Annotated[str, VectorStoreField("data", is_indexed=True)] + embedding: Annotated[list[float] | None, VectorStoreField("vector", dimensions=3)] = None + + +async def main() -> None: + # 1. Resolve SQL_SERVER_CONNECTION_STRING using Agent Framework settings. + async with SqlServerStore() as store: + collection = store.get_collection(Note, collection_name=f"af_sql_demo_{uuid4().hex}") + await collection.ensure_collection_exists() + try: + # 2. Preserve precomputed vectors; pass a generator to produce them locally instead. + await collection.upsert( + [ + Note("sql", "SQL Server has native vector columns", "database", [1, 0, 0]), + Note("travel", "Travel journal", "travel", [0, 1, 0]), + ], + generate_vectors=False, + ) + + # 3. Filter, rank, and apply the score threshold in the database before paging. + results = await collection.search( + vector=[1, 0, 0], + filter=Filter("category", "eq", "database"), + score_threshold=0.1, + ) + async for result in results: + print(result["record"].text, result["score"]) + + # 4. Retrieval excludes embeddings by default; request them explicitly. + notes = await collection.get(["sql"], include_vectors=True) + print(notes[0].embedding) + finally: + await collection.ensure_collection_deleted() + + +if __name__ == "__main__": + asyncio.run(main()) + +# Expected output: +# SQL Server has native vector columns 0.0 +# [1.0, 0.0, 0.0] diff --git a/python/packages/sql-server/tests/sql_server/test_integration.py b/python/packages/sql-server/tests/sql_server/test_integration.py new file mode 100644 index 00000000000..065f482bbdc --- /dev/null +++ b/python/packages/sql-server/tests/sql_server/test_integration.py @@ -0,0 +1,218 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Annotated, Any +from uuid import uuid4 + +import aioodbc +import pytest +from agent_framework import ( + Filter, + FilterGroup, + InMemoryCollection, + VectorStoreCollectionDefinition, + VectorStoreField, + vectorstoremodel, +) +from agent_framework.exceptions import IntegrationException + +from agent_framework_sql_server import SqlServerCollection, SqlServerStore + +pytestmark = [pytest.mark.flaky, pytest.mark.integration] + + +@pytest.fixture +def connection_string() -> str: + value = os.getenv("SQL_SERVER_TEST_CONNECTION_STRING") + if not value: + pytest.skip("Set a non-empty SQL_SERVER_TEST_CONNECTION_STRING for a vector-enabled test database.") + assert value is not None + return value + + +@vectorstoremodel +@dataclass +class Note: + id: Annotated[str, VectorStoreField("key", storage_name="record]id")] + text: Annotated[str, VectorStoreField("data", storage_name="body [text]")] + count: Annotated[int | None, VectorStoreField("data")] + embedding: Annotated[list[float] | None, VectorStoreField("vector", dimensions=3)] = None + + +async def test_typed_lifecycle_crud_search_and_portable_filters(connection_string: str) -> None: + name = f"af_sql_test_{uuid4().hex}_]';--" + async with SqlServerStore(connection_string=connection_string) as store: + collection = store.get_collection(Note, collection_name=name) + assert not await collection.collection_exists() + await collection.ensure_collection_exists() + try: + assert await store.collection_exists(name) + assert name in await store.list_collection_names() + assert await collection.upsert( + [ + Note("same", "literal %_[ab]! sample", 1, [1, 0, 0]), + Note("other", "other [ab] sample", 2, [0, 1, 0]), + Note("null", "no embedding", None, None), + ], + generate_vectors=False, + ) == ["same", "other", "null"] + assert [item.id for item in await collection.get(["other", "absent", "same", "same"])] == [ + "other", + "same", + "same", + ] + assert (await collection.get(["same"], include_vectors=True))[0].embedding == [1, 0, 0] + assert [item.id for item in await collection.get(filter=Filter("count", "is_null"))] == ["null"] + assert [item.id for item in await collection.get(filter=Filter("count", "not_in", [1, None]))] == ["other"] + assert [item.id for item in await collection.get(filter=Filter("text", "contains_text", "%_[ab]!"))] == [ + "same" + ] + + results = [ + item + async for item in await collection.search( + vector=[1, 0, 0], + filter=FilterGroup("not", [Filter("count", "gt", 1)]), + score_threshold=0.2, + top=1, + ) + ] + assert len(results) == 1 and results[0]["record"].id == "same" + assert results[0]["score"] == pytest.approx(0) + assert results[0]["record"].embedding is None + + await collection.upsert([Note("same", "updated", 4, [0, 1, 0])], generate_vectors=False) + assert (await collection.get(["same"]))[0].text == "updated" + await collection.delete(["missing", "other"]) + assert [item.id for item in await collection.get(order_by={"count": False})] == ["same", "null"] + finally: + await collection.ensure_collection_deleted() + assert not await collection.collection_exists() + + +async def test_generated_key_and_vector_metrics(connection_string: str) -> None: + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="int", is_auto_generated=True), + VectorStoreField("data", name="text", type_="str"), + VectorStoreField( + "vector", + name="embedding", + type_="float32", + dimensions=3, + distance_function="dot_prod", + ), + ], + collection_name=f"af_sql_generated_{uuid4().hex}", + ) + async with SqlServerStore(connection_string=connection_string) as store: + collection = store.get_collection(dict, definition=definition) + await collection.ensure_collection_exists() + try: + keys = await collection.upsert( + [ + {"text": "same", "embedding": [1, 0, 0]}, + {"text": "opposite", "embedding": [-1, 0, 0]}, + ], + generate_vectors=False, + ) + assert len(keys) == 2 and all(type(key) is int for key in keys) + results = [item async for item in await collection.search(vector=[1, 0, 0], score_threshold=0.5)] + assert [item["record"]["id"] for item in results] == [keys[0]] + assert results[0]["score"] == pytest.approx(1) + finally: + await collection.ensure_collection_deleted() + + +async def test_portable_scalar_filters_match_in_memory(connection_string: str) -> None: + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="str"), + VectorStoreField("data", name="text", type_="str"), + VectorStoreField("data", name="count", type_="int"), + VectorStoreField("data", name="flag", type_="bool"), + VectorStoreField("vector", name="embedding", dimensions=3), + ], + collection_name=f"af_sql_filters_{uuid4().hex}", + ) + rows: list[dict[str, Any]] = [ + {"id": "alpha", "text": "A[ab]%_! ", "count": 1, "flag": True, "embedding": [1, 0, 0]}, + {"id": "beta", "text": "Aabx", "count": 2, "flag": False, "embedding": [0, 1, 0]}, + {"id": "null", "text": None, "count": None, "flag": None, "embedding": None}, + ] + filters: list[Filter | FilterGroup] = [ + Filter("text", "eq", "A[ab]%_! "), + Filter("text", "ne", "A[ab]%_! "), + Filter("text", "contains_text", "[ab]%_!"), + Filter("text", "starts_with", "A["), + Filter("text", "ends_with", "! "), + Filter("count", "gt", 1), + Filter("count", "between", [1, 2]), + Filter("count", "in", [2, None]), + Filter("count", "not_in", [2, None]), + Filter("count", "eq", True), + Filter("flag", "ne", 1), + Filter("text", "is_null"), + FilterGroup("not", [Filter("count", "gt", 1)]), + ] + memory = InMemoryCollection(dict, definition=definition) + await memory.ensure_collection_exists() + await memory.upsert(rows, generate_vectors=False) + async with SqlServerStore(connection_string=connection_string) as store: + collection = store.get_collection(dict, definition=definition) + await collection.ensure_collection_exists() + try: + await collection.upsert(rows, generate_vectors=False) + for expression in filters: + expected = await memory.get(filter=expression) + actual = await collection.get(filter=expression) + assert {row["id"] for row in actual} == {row["id"] for row in expected}, repr(expression) + finally: + await collection.ensure_collection_deleted() + + +async def test_borrowed_outer_transaction_preserves_prior_write_on_failed_batch(connection_string: str) -> None: + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="str"), + VectorStoreField("data", name="count", type_="int"), + VectorStoreField("vector", name="embedding", dimensions=3), + ], + collection_name=f"af_sql_tx_{uuid4().hex}", + ) + connection = await aioodbc.connect(dsn=connection_string, autocommit=False) + try: + collection = SqlServerCollection(dict, client=connection, definition=definition) + await collection.ensure_collection_exists() + try: + constraint = f"af_check_{uuid4().hex}" + cursor = await connection.cursor() + try: + await cursor.execute( + f"ALTER TABLE {collection._table} ADD CONSTRAINT [{constraint}] CHECK ([count] >= 0)" + ) + finally: + await cursor.close() + await connection.commit() + + await collection.upsert([{"id": "prior", "count": 1, "embedding": [1, 0, 0]}], generate_vectors=False) + with pytest.raises(IntegrationException): + await collection.upsert( + [ + {"id": "new", "count": 2, "embedding": [1, 0, 0]}, + {"id": "invalid", "count": -1, "embedding": [1, 0, 0]}, + ], + generate_vectors=False, + ) + assert [row["id"] for row in await collection.get()] == ["prior"] + await connection.commit() + assert [row["id"] for row in await collection.get()] == ["prior"] + finally: + await connection.rollback() + await collection.ensure_collection_deleted() + await connection.commit() + finally: + await connection.close() diff --git a/python/packages/sql-server/tests/sql_server/test_sql.py b/python/packages/sql-server/tests/sql_server/test_sql.py new file mode 100644 index 00000000000..7cfb6476d8b --- /dev/null +++ b/python/packages/sql-server/tests/sql_server/test_sql.py @@ -0,0 +1,170 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from uuid import UUID + +import pytest +from agent_framework import Filter, FilterGroup, VectorStoreCollectionDefinition, VectorStoreField +from agent_framework.exceptions import IntegrationInvalidResponseException + +from agent_framework_sql_server._sql import ( + MAX_PARAMETERS, + _column_type, + _FilterCompiler, + _metric_for, + _parse_value, + _prepare_value, + _quote_identifier, +) +from agent_framework_sql_server._vector_store import _check_parameter_count + + +@pytest.fixture +def definition() -> VectorStoreCollectionDefinition: + return VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="str", storage_name="record]id"), + VectorStoreField("data", name="text", type_="str", storage_name="body [text]"), + VectorStoreField("data", name="count", type_="int"), + VectorStoreField("data", name="flag", type_="bool"), + VectorStoreField("data", name="tags", type_="list"), + VectorStoreField("vector", name="embedding", type_="float32", dimensions=3), + ], + collection_name="documents", + ) + + +@pytest.mark.parametrize("name", ["", "no\0identifier", "a" * 129, "\U0001f680" * 65]) +def test_identifier_rejects_invalid_or_truncated_names(name): + with pytest.raises(ValueError): + _quote_identifier(name) + + +def test_identifier_quotes_each_segment_including_embedded_brackets(): + assert _quote_identifier("s]; DROP TABLE dbo.users;--") == "[s]]; DROP TABLE dbo.users;--]" + assert _quote_identifier("dbo.documents") == "[dbo.documents]" + assert _quote_identifier("it's") == "[it's]" + + +def test_vector_uses_native_type_and_json_values(): + field = VectorStoreField("vector", name="embedding", type_="float32", dimensions=3) + assert _column_type(field) == "VECTOR(3)" + assert _prepare_value(field, [1, 0.25, -0.5]) == "[1.0,0.25,-0.5]" + assert _parse_value(field, "[1.0,0.25,-0.5]") == [1.0, 0.25, -0.5] + for invalid in ([1, True, 2], [1, float("nan"), 2], [1, 2], b"vector", [1e40, 0, 0]): + with pytest.raises((TypeError, ValueError)): + _prepare_value(field, invalid) + for invalid_json in ('"not a vector"', "[1,2]", "[1,true,3]", "[1e99,2,3]", b"[1,2,3]"): + with pytest.raises(IntegrationInvalidResponseException): + _parse_value(field, invalid_json) + + +def test_vector_types_metrics_and_dimensions_are_restricted(): + for dimensions in (0, 1999): + with pytest.raises(ValueError): + _column_type(VectorStoreField("vector", name="v", dimensions=dimensions)) + with pytest.raises(NotImplementedError): + _column_type(VectorStoreField("vector", name="v", type_="float16", dimensions=3)) + assert _metric_for(VectorStoreField("vector", name="v", dimensions=3, distance_function="dot_prod")) == ( + "dot", + "negative", + ) + with pytest.raises(NotImplementedError): + _metric_for(VectorStoreField("vector", name="v", dimensions=3, distance_function="manhattan")) + + +def test_typed_values_and_json_do_not_coerce_invalid_data(): + string_key = VectorStoreField("key", name="id", type_="str") + with pytest.raises(ValueError, match="end in a space"): + _prepare_value(string_key, "alias ") + assert _prepare_value(VectorStoreField("data", name="enabled", type_="bool"), False) is False + with pytest.raises(TypeError): + _prepare_value(VectorStoreField("data", name="enabled", type_="bool"), 0) + with pytest.raises(TypeError): + _prepare_value(VectorStoreField("data", name="number", type_="int"), True) + with pytest.raises(ValueError): + _prepare_value(VectorStoreField("data", name="number", type_="int"), 2**63) + with pytest.raises(ValueError): + _prepare_value(VectorStoreField("data", name="when", type_="datetime"), "2026-01-01T00:00:00") + when = datetime(2026, 1, 1, tzinfo=timezone.utc) + datetime_field = VectorStoreField("data", name="when", type_="datetime") + assert _column_type(datetime_field) == "DATETIME2(7)" + assert _prepare_value(datetime_field, when.astimezone(timezone(timedelta(hours=2)))) == when.replace(tzinfo=None) + assert _parse_value(datetime_field, when.replace(tzinfo=None)) == when + uid = UUID(int=1) + assert _prepare_value(VectorStoreField("key", name="id", type_="UUID"), str(uid)) == str(uid) + assert _parse_value(VectorStoreField("key", name="id", type_="UUID"), str(uid)) == uid + tags = VectorStoreField("data", name="tags", type_="list") + assert _parse_value(tags, _prepare_value(tags, [1, True, None])) == [1, True, None] + with pytest.raises(TypeError): + _prepare_value(tags, [("nested",)]) + with pytest.raises(TypeError): + _prepare_value(VectorStoreField("data", name="meta", type_="dict"), {1: "bad"}) + + +def test_filter_values_bound_and_sql_server_wildcards_escaped(definition): + injection = "'; DROP TABLE [dbo].[users]; --%_[abc]^!" + condition, params = _FilterCompiler(definition, alias="t.").compile(Filter("text", "contains_text", injection)) + assert injection not in condition + assert "t.[body [text]]]" in condition and "ESCAPE '!'" in condition + assert params == ["%'; DROP TABLE [[]dbo].[[]users]; --!%!_[[]abc]^!!%"] + + +@pytest.mark.parametrize("operator", ["eq", "ne", "gt", "gte", "lt", "lte", "in", "not_in", "between"]) +def test_scalar_filters_bind_values_in_order(definition, operator): + value = [1, 2] if operator in ("in", "not_in", "between") else 3 + condition, params = _FilterCompiler(definition).compile(Filter("count", operator, value)) + assert condition.count("?") == len(params) == (2 if isinstance(value, list) else 1) + assert params == (value if isinstance(value, list) else [value]) + + +def test_filter_groups_and_nullable_membership_are_two_valued(definition): + expression = FilterGroup( + "not", + [FilterGroup("and", [Filter("id", "ne", "a"), Filter("count", "not_in", [1, None])])], + ) + condition, params = _FilterCompiler(definition).compile(expression) + assert "NOT" in condition and "IS NOT NULL AND" in condition and "IS NULL" in condition + assert params == ["a", 1] + assert "CONVERT(VARBINARY(MAX)" in condition + assert _FilterCompiler(definition).compile(Filter("count", "in", []))[0].endswith("(1 = 0))") + assert _FilterCompiler(definition).compile(Filter("count", "not_in", []))[0].endswith("(NOT (1 = 0)))") + + +@pytest.mark.parametrize("name,value", [("flag", 1), ("count", True), ("count", "1")]) +def test_equality_does_not_coerce_mismatched_types(definition, name, value): + condition, params = _FilterCompiler(definition).compile(Filter(name, "eq", value)) + assert condition == "1 = 0" and params == [] + condition, params = _FilterCompiler(definition).compile(Filter(name, "ne", value)) + assert condition == "(NOT (1 = 0))" and params == [] + + +@pytest.mark.parametrize("operator", ["eq", "gt"]) +def test_out_of_range_numeric_filter_rejected(definition, operator): + with pytest.raises(ValueError, match="range"): + _FilterCompiler(definition).compile(Filter("count", operator, 10**1000)) + + +@pytest.mark.parametrize( + "expression,error", + [ + (Filter("text.subfield", "eq", "a"), NotImplementedError), + (Filter("unknown", "eq", 1), ValueError), + (Filter("embedding", "is_null"), NotImplementedError), + (Filter("tags", "contains", 1), NotImplementedError), + (Filter("text", "gt", "a"), NotImplementedError), + (Filter("text", "contains_text", "\0"), ValueError), + (Filter("flag", "between", [False, True]), NotImplementedError), + (Filter("text", "sql.raw", "'; DROP TABLE x;--"), NotImplementedError), + ], +) +def test_unsupported_filters_fail_explicitly(definition, expression, error): + with pytest.raises(error): + _FilterCompiler(definition).compile(expression) + + +def test_query_parameter_budget_rejects_oversized_statements(): + with pytest.raises(ValueError, match=str(MAX_PARAMETERS)): + _check_parameter_count([None] * (MAX_PARAMETERS + 1)) diff --git a/python/packages/sql-server/tests/sql_server/test_vector_store.py b/python/packages/sql-server/tests/sql_server/test_vector_store.py new file mode 100644 index 00000000000..3df0fcf0d33 --- /dev/null +++ b/python/packages/sql-server/tests/sql_server/test_vector_store.py @@ -0,0 +1,645 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +from contextlib import asynccontextmanager +from dataclasses import dataclass +from functools import partial +from typing import Annotated +from unittest.mock import AsyncMock, MagicMock, patch + +import aioodbc +import pyodbc +import pytest +from agent_framework import ( + Embedding, + Filter, + FilterGroup, + GeneratedEmbeddings, + Param, + SecretString, + VectorStoreCollectionDefinition, + VectorStoreField, + create_vector_search_tool, + vectorstoremodel, +) +from agent_framework.exceptions import IntegrationException, IntegrationInvalidResponseException + +from agent_framework_sql_server import SqlServerCollection, SqlServerStore +from agent_framework_sql_server import _vector_store as module +from agent_framework_sql_server._vector_store import _Client + + +@pytest.fixture +def definition() -> VectorStoreCollectionDefinition: + return VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="str", storage_name="record]id"), + VectorStoreField("data", name="text", type_="str", storage_name="body [text]", is_indexed=True), + VectorStoreField("data", name="count", type_="int"), + VectorStoreField("data", name="tags", type_="list"), + VectorStoreField("vector", name="embedding", type_="float32", dimensions=3, storage_name="vector [one]"), + ], + collection_name="documents", + ) + + +@pytest.fixture +def collection(definition): + return SqlServerCollection(dict, definition=definition, connection_string="Driver={ODBC Driver 18 for SQL Server};") + + +def make_record(key="one", *, text="literal %_[abc]!", count=1, tags=None, embedding=None): + return { + "id": key, + "text": text, + "count": count, + "tags": ["tag"] if tags is None else tags, + "embedding": [1, 0, 0] if embedding is None else embedding, + } + + +def fake_connection(): + connection = MagicMock(spec=aioodbc.Connection) + connection.autocommit = False + connection.commit = AsyncMock() + connection.rollback = AsyncMock() + connection.close = AsyncMock() + cursor = MagicMock(spec=aioodbc.Cursor) + cursor.execute = AsyncMock() + cursor.fetchone = AsyncMock(return_value=None) + cursor.fetchall = AsyncMock(return_value=[]) + cursor.close = AsyncMock() + + async def open_cursor(): + return cursor + + connection.cursor.side_effect = open_cursor + return connection, cursor + + +@pytest.fixture +def mock_io(collection): + connection, cursor = fake_connection() + + @asynccontextmanager + async def acquire(*, write=False): + yield connection + + with patch.object(collection._client, "connection", side_effect=acquire) as request: + yield connection, cursor, request + + +def test_fields_fail_early_if_unsupported(definition): + with pytest.raises(NotImplementedError, match="approximate"): + SqlServerCollection( + dict, + connection_string="unused", + definition=VectorStoreCollectionDefinition( + [ + definition.key_field, + VectorStoreField("vector", name="v", dimensions=3, index_kind="disk_ann"), + ], + collection_name="documents", + ), + ) + with pytest.raises(NotImplementedError, match="full-text"): + SqlServerCollection( + dict, + connection_string="unused", + definition=VectorStoreCollectionDefinition( + [ + definition.key_field, + VectorStoreField("data", name="text", type_="str", is_full_text_indexed=True), + ], + collection_name="documents", + ), + ) + with pytest.raises(ValueError, match="must be one"): + SqlServerCollection( + dict, + connection_string="unused", + definition=VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="bool"), + VectorStoreField("vector", name="embedding", dimensions=3), + ], + collection_name="documents", + ), + ) + + +async def test_create_check_drop_use_scoped_quoted_identifiers_and_bound_catalog_values(definition): + name = "table]'; DROP TABLE dbo.other; --" + schema = "schema]'; --" + collection = SqlServerCollection( + dict, definition=definition, collection_name=name, schema=schema, connection_string="x" + ) + connection, cursor = fake_connection() + + @asynccontextmanager + async def acquire(*, write=False): + yield connection + + with patch.object(collection._client, "connection", side_effect=acquire): + await collection.ensure_collection_exists() + create, index = cursor.execute.call_args_list + query, params = create.args + assert "CREATE TABLE [schema]]'; --].[table]]'; DROP TABLE dbo.other; --]" in query + assert "[record]]id]" in query and "VECTOR(3)" in query + assert name not in query and schema not in query + assert params == (schema, name) + assert "CREATE INDEX [af_sql_" in index.args[0] + assert index.args[1][:2] == (schema, name) + assert await collection.collection_exists() is False + cursor.fetchone.return_value = (1,) + assert await collection.collection_exists() is True + await collection.ensure_collection_deleted() + assert cursor.execute.call_args.args[0] == f"DROP TABLE IF EXISTS {collection._table}" + assert "CASCADE" not in cursor.execute.call_args.args[0] + + +async def test_batch_upsert_preserves_duplicate_key_order_and_binds_json(collection, mock_io): + _, cursor, acquire = mock_io + cursor.fetchone.side_effect = [None, ("one",), ("one",), ("one",)] + keys = await collection.upsert([make_record("one"), make_record("one", text="updated")], generate_vectors=False) + assert keys == ["one", "one"] + assert acquire.call_args.kwargs == {"write": True} + calls = cursor.execute.call_args_list + assert len(calls) == 4 + assert "WITH (UPDLOCK, HOLDLOCK)" in calls[0].args[0] + assert "OUTPUT INSERTED.[record]]id]" in calls[1].args[0] + assert "OUTPUT INSERTED.[record]]id]" in calls[3].args[0] + assert "[1.0,0.0,0.0]" in calls[1].args[1] + assert "literal %_[abc]!" not in calls[1].args[0] + assert "literal %_[abc]!" in calls[1].args[1] + + +async def test_generated_key_inserts_without_key_or_prior_lookup(): + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="int", is_auto_generated=True), + VectorStoreField("data", name="text", type_="str"), + VectorStoreField("vector", name="v", dimensions=3), + ], + collection_name="generated", + ) + collection = SqlServerCollection(dict, definition=definition, connection_string="x") + connection, cursor = fake_connection() + cursor.fetchone.return_value = (123,) + + @asynccontextmanager + async def acquire(*, write=False): + yield connection + + with patch.object(collection._client, "connection", side_effect=acquire): + assert await collection.upsert([{"text": "a", "v": [1, 0, 0]}], generate_vectors=False) == [123] + assert cursor.execute.call_count == 1 + assert "INSERT INTO" in cursor.execute.call_args.args[0] + assert "OUTPUT INSERTED.[id]" in cursor.execute.call_args.args[0] + assert cursor.execute.call_args.args[1] == ("a", "[1.0,0.0,0.0]") + + +async def test_generated_identity_refuses_explicit_new_key(): + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="int", is_auto_generated=True), + VectorStoreField("vector", name="v", dimensions=3), + ], + collection_name="generated", + ) + collection = SqlServerCollection(dict, definition=definition, connection_string="x") + connection, cursor = fake_connection() + + @asynccontextmanager + async def acquire(*, write=False): + yield connection + + with ( + patch.object(collection._client, "connection", side_effect=acquire), + pytest.raises(NotImplementedError, match="IDENTITY"), + ): + await collection.upsert([{"id": 42, "v": [1, 0, 0]}], generate_vectors=False) + assert cursor.execute.call_count == 1 + assert "WITH (UPDLOCK, HOLDLOCK)" in cursor.execute.call_args.args[0] + + +async def test_upsert_missing_key_raises_integration_response_error(collection, mock_io): + _, cursor, _ = mock_io + cursor.fetchone.return_value = None + with pytest.raises(IntegrationInvalidResponseException, match="inserted key"): + await collection.upsert([make_record()], generate_vectors=False) + + +async def test_get_keys_preserves_input_order_and_optional_vectors(collection, mock_io): + _, cursor, _ = mock_io + cursor.fetchall.return_value = [ + ("two", "second", 2, '["b"]'), + ("one", "first", 1, '["a"]'), + ] + rows = await collection.get(["one", "missing", "two", "one"]) + assert [row["id"] for row in rows] == ["one", "two", "one"] + assert all("embedding" not in row for row in rows) + assert rows[0]["tags"] == ["a"] + assert "[vector [one]]]" not in cursor.execute.call_args.args[0] + + cursor.fetchall.return_value = [("one", "first", 1, '["a"]', "[1.0,0.0,0.0]")] + rows = await collection.get(["one"], include_vectors=True) + assert rows[0]["embedding"] == [1.0, 0.0, 0.0] + assert "[vector [one]]]" in cursor.execute.call_args.args[0] + + +async def test_get_delete_large_key_batches_do_not_exceed_server_parameter_budget(collection, mock_io): + _, cursor, _ = mock_io + keys = [str(index) for index in range(1001)] + cursor.fetchall.side_effect = [ + [("0", "first", 0, "[]")], + [("1000", "last", 1000, "[]")], + ] + assert [row["id"] for row in await collection.get(keys)] == ["0", "1000"] + assert [len(call.args[1]) for call in cursor.execute.call_args_list] == [1000, 1] + cursor.execute.reset_mock() + await collection.delete(keys) + assert [len(call.args[1]) for call in cursor.execute.call_args_list] == [1000, 1] + assert all("DELETE FROM [dbo].[documents]" in call.args[0] for call in cursor.execute.call_args_list) + + +async def test_filtered_listing_orders_and_pages_on_server(collection, mock_io): + _, cursor, _ = mock_io + await collection.get(filter=Filter("count", "gt", 1), order_by={"text": False}, skip=3, top=2) + sql, params = cursor.execute.call_args.args + assert "([count] IS NOT NULL AND [count] > ?)" in sql + assert "CASE WHEN [body [text]]] IS NULL THEN 1 ELSE 0 END" in sql + assert "[body [text]]] DESC" in sql + assert sql.endswith("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY") + assert params == (1, 3, 2) + + +async def test_exact_search_filters_and_threshold_before_paging(collection, mock_io): + _, cursor, _ = mock_io + cursor.fetchall.return_value = [("one", "first", 1, "[]", 0.2)] + results = await collection.search( + vector=[1, 0, 0], filter=Filter("count", "gte", 1), score_threshold=0.25, skip=1, top=2 + ) + assert results.metadata == {"distance_function": "DEFAULT", "approximate": False} + values = [item async for item in results] + assert values == [{"record": {"id": "one", "text": "first", "count": 1, "tags": []}, "score": 0.2}] + statement, params = cursor.execute.call_args.args + assert "VECTOR_DISTANCE('cosine', CAST(? AS VECTOR(3)), t.[vector [one]]])" in statement + assert statement.index("d.[distance] <= ?") < statement.index("ORDER BY") < statement.index("OFFSET ?") + assert "t.[count] >= ?" in statement + assert params == ("[1.0,0.0,0.0]", 1, 0.25, 1, 2) + assert "[vector [one]]]" not in statement.split(" FROM ")[0] + + +async def test_multiple_vector_columns_select_requested_dimensions_and_decode_values(): + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="str"), + VectorStoreField("vector", name="primary", dimensions=3), + VectorStoreField("vector", name="secondary", storage_name="other]vector", dimensions=2), + ], + collection_name="multi_vectors", + ) + collection = SqlServerCollection(dict, definition=definition, connection_string="x") + connection, cursor = fake_connection() + cursor.fetchall.return_value = [("first", 0.0)] + + @asynccontextmanager + async def acquire(*, write=False): + yield connection + + with patch.object(collection._client, "connection", side_effect=acquire): + results = [item async for item in await collection.search(vector=[0, 1], vector_property_name="secondary")] + assert results[0]["record"] == {"id": "first"} + assert "CAST(? AS VECTOR(2)), t.[other]]vector]" in cursor.execute.call_args.args[0] + cursor.fetchall.return_value = [("first", "[1,0,0]", "[0,1]")] + record = (await collection.get(["first"], include_vectors=True))[0] + assert record["primary"] == [1.0, 0.0, 0.0] + assert record["secondary"] == [0.0, 1.0] + + +@pytest.mark.parametrize( + "metric,raw_distance,cutoff,expected", + [ + ("cosine_similarity", 0.2, 0.2, 0.8), + ("dot_prod", -0.8, -0.8, 0.8), + ("negative_dot_prod", -0.8, -0.8, -0.8), + ("euclidean_distance", 0.2, 0.2, 0.2), + ], +) +async def test_metric_score_direction_and_threshold(metric, raw_distance, cutoff, expected): + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="str"), + VectorStoreField("vector", name="v", dimensions=3, distance_function=metric), + ], + collection_name="metrics", + ) + collection = SqlServerCollection(dict, definition=definition, connection_string="x") + connection, cursor = fake_connection() + cursor.fetchall.return_value = [("first", raw_distance)] + + @asynccontextmanager + async def acquire(*, write=False): + yield connection + + with patch.object(collection._client, "connection", side_effect=acquire): + results = [item async for item in await collection.search(vector=[1, 0, 0], score_threshold=expected)] + assert results[0]["score"] == pytest.approx(expected) + sql, params = cursor.execute.call_args.args + assert "d.[distance] <= ?" in sql + assert params[1] == pytest.approx(cutoff) + + +async def test_search_tool_resolves_filter_parameters_on_backend(collection, mock_io): + _, cursor, _ = mock_io + collection.embedding_generator = MagicMock( + get_embeddings=AsyncMock(return_value=GeneratedEmbeddings([Embedding(vector=[1, 0, 0])])) + ) + tool = create_vector_search_tool( + collection, + filter=FilterGroup( + "and", + [ + Filter("id", "eq", "one"), + Filter("text", "contains_text", Param("needle", str | None, default=None, omit_if_none=True)), + ], + ), + ) + assert await tool.invoke(arguments={"query": "sample", "needle": "[%_]"}, skip_parsing=True) == [] + sql, params = cursor.execute.call_args.args + assert "CONVERT(VARBINARY(MAX)" in sql + assert params[1:3] == ("one", "%[[]!%!_]%") + assert await tool.invoke(arguments={"query": "sample"}, skip_parsing=True) == [] + sql, params = cursor.execute.call_args.args + assert "LIKE" not in sql + assert params[1] == "one" + + +async def test_invalid_options_and_dimension_fail_before_io(collection, mock_io): + _, cursor, acquire = mock_io + with pytest.raises(NotImplementedError): + await collection.search(vector=[1, 0, 0], operation_options={"approximate": True}) + with pytest.raises(NotImplementedError): + await collection.search(vector=[1, 0, 0], search_type="keyword_hybrid", values="query") + with pytest.raises(ValueError, match="dimensions"): + await collection.search(vector=[1, 0]) + with pytest.raises(ValueError, match="finite"): + await collection.search(vector=[1, 0, 0], score_threshold=10**1000) + with pytest.raises(ValueError, match="index 1"): + await collection.upsert([make_record(), make_record(embedding=[1])], generate_vectors=False) + assert not acquire.called and not cursor.execute.called + + +async def test_closed_collection_rejects_empty_operations(collection, mock_io): + _, cursor, acquire = mock_io + await collection.close() + with pytest.raises(IntegrationException, match="closed"): + await collection.get([]) + with pytest.raises(IntegrationException, match="closed"): + await collection.upsert([], generate_vectors=False) + with pytest.raises(IntegrationException, match="closed"): + await collection.delete([]) + assert not acquire.called and not cursor.execute.called + + +@pytest.fixture(params=["collection", "store"]) +def constructor(request, definition, monkeypatch): + monkeypatch.delenv("SQL_SERVER_CONNECTION_STRING", raising=False) + return ( + partial(SqlServerCollection, dict, definition=definition) if request.param == "collection" else SqlServerStore + ) + + +def test_settings_priority_and_secret_masking(constructor, monkeypatch, tmp_path): + monkeypatch.setenv("SQL_SERVER_CONNECTION_STRING", "environment-secret") + env_file = tmp_path / "settings.env" + env_file.write_text("SQL_SERVER_CONNECTION_STRING='file-secret'\n", encoding="utf-8") + value = SecretString("explicit-secret") + instance = constructor(connection_string=value, env_file_path=str(env_file)) + assert instance._client.connection_string is value + assert "explicit-secret" not in repr(value) and str(value) == "**********" + assert constructor(env_file_path=str(env_file))._client.connection_string.get_secret_value() == "file-secret" + assert constructor()._client.connection_string.get_secret_value() == "environment-secret" + + +def test_settings_require_explicit_file_or_environment(constructor, monkeypatch, tmp_path): + (tmp_path / ".env").write_text("SQL_SERVER_CONNECTION_STRING=implicit\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + with pytest.raises(ValueError, match="exactly one"): + constructor() + with pytest.raises(ValueError, match="must not be empty"): + constructor(connection_string="") + with pytest.raises(FileNotFoundError): + constructor(env_file_path=str(tmp_path / "missing.env"), connection_string="explicit") + + +def test_borrowed_client_rejects_settings_and_raw_sync_connections(constructor): + borrowed, _ = fake_connection() + with pytest.raises(ValueError, match="cannot be combined"): + constructor(client=borrowed, connection_string="unused") + with pytest.raises(ValueError, match="cannot be combined"): + constructor(client=borrowed, env_file_path="unused.env") + with pytest.raises(TypeError, match="aioodbc"): + constructor(client=MagicMock(spec=pyodbc.Connection)) + + +async def test_store_deletion_quotes_supplied_table_name(definition): + store = SqlServerStore(connection_string="x") + connection, cursor = fake_connection() + + @asynccontextmanager + async def acquire(*, write=False): + yield connection + + with patch.object(store._client, "connection", side_effect=acquire): + cursor.fetchall.return_value = [("table]'; --",)] + await store.ensure_collection_deleted("table]'; --") + assert cursor.execute.call_args.args[0] == "DROP TABLE IF EXISTS [dbo].[table]]'; --]" + assert "CASCADE" not in cursor.execute.call_args.args[0] + + +async def test_borrowed_connection_savepoint_rollback_preserves_caller_transaction(): + connection, cursor = fake_connection() + cursor.fetchone.side_effect = [(1,), (1,)] + client = _Client(None, connection) + with pytest.raises(ValueError, match="failed"): + async with client.connection(write=True) as borrowed: + assert borrowed is connection + async with module._cursor(borrowed) as operation_cursor: + await operation_cursor.execute("SELECT 1") + raise ValueError("failed") + commands = [call.args[0] for call in cursor.execute.call_args_list] + assert commands == [ + "SELECT @@TRANCOUNT", + "SAVE TRANSACTION af_vector_operation", + "SELECT 1", + "SELECT XACT_STATE()", + "ROLLBACK TRANSACTION af_vector_operation", + ] + connection.commit.assert_not_awaited() + connection.rollback.assert_not_awaited() + assert cursor.close.await_count == 3 + await client.close() + await client.close() + connection.close.assert_not_awaited() + with pytest.raises(RuntimeError, match="closed"): + async with client.connection(): + pytest.fail("Closed client must not borrow the connection") + + +async def test_borrowed_connection_commits_only_its_own_transaction(): + connection, cursor = fake_connection() + cursor.fetchone.return_value = (0,) + client = _Client(None, connection) + async with client.connection(write=True) as borrowed, module._cursor(borrowed) as operation_cursor: + await operation_cursor.execute("SELECT 1") + assert [call.args[0] for call in cursor.execute.call_args_list] == [ + "SELECT @@TRANCOUNT", + "BEGIN TRANSACTION", + "SELECT 1", + "COMMIT TRANSACTION", + ] + connection.commit.assert_not_awaited() + assert cursor.close.await_count == 3 + connection.close.assert_not_awaited() + await client.close() + + +async def test_borrowed_outer_transaction_success_closes_cursor_without_committing(): + connection, cursor = fake_connection() + cursor.fetchone.return_value = (1,) + client = _Client(None, connection) + async with client.connection(write=True) as borrowed, module._cursor(borrowed) as operation_cursor: + await operation_cursor.execute("SELECT 1") + assert [call.args[0] for call in cursor.execute.call_args_list] == [ + "SELECT @@TRANCOUNT", + "SAVE TRANSACTION af_vector_operation", + "SELECT 1", + ] + assert cursor.close.await_count == 2 + connection.commit.assert_not_awaited() + connection.rollback.assert_not_awaited() + await client.close() + + +async def test_doomed_caller_transaction_is_not_silently_rolled_back(): + connection, cursor = fake_connection() + cursor.fetchone.side_effect = [(1,), (-1,)] + client = _Client(None, connection) + with pytest.raises(IntegrationException, match="caller must roll back"): + async with client.connection(write=True): + raise ValueError("failed") + assert "ROLLBACK TRANSACTION af_vector_operation" not in [call.args[0] for call in cursor.execute.call_args_list] + connection.rollback.assert_not_awaited() + await client.close() + + +async def test_owned_pool_uses_one_worker_and_releases_it_with_store(): + pool = MagicMock(spec=aioodbc.Pool) + pool.close = MagicMock() + pool.wait_closed = AsyncMock() + connection, _ = fake_connection() + pool.acquire.return_value.__aenter__.return_value = connection + client = _Client(SecretString("Driver={ODBC Driver 18 for SQL Server};"), None) + with patch.object(module.aioodbc, "create_pool", new_callable=AsyncMock, return_value=pool) as create_pool: + async with client.connection(write=True) as acquired: + assert acquired is connection + executor = create_pool.call_args.kwargs["executor"] + assert executor._max_workers == 1 + assert create_pool.call_args.kwargs["autocommit"] is False + connection.commit.assert_awaited_once() + await client.close() + await client.close() + pool.close.assert_called_once() + pool.wait_closed.assert_awaited_once() + assert executor._shutdown + connection.close.assert_not_awaited() + + +async def test_owned_commit_failure_rolls_back_before_releasing_pool_connection(): + pool = MagicMock(spec=aioodbc.Pool) + pool.close = MagicMock() + pool.wait_closed = AsyncMock() + connection, _ = fake_connection() + connection.commit.side_effect = pyodbc.OperationalError("commit failed") + pool.acquire.return_value.__aenter__.return_value = connection + client = _Client(SecretString("Driver={ODBC Driver 18 for SQL Server};"), None) + with patch.object(module.aioodbc, "create_pool", new_callable=AsyncMock, return_value=pool): + with pytest.raises(IntegrationException): + async with client.connection(write=True): + pass + connection.rollback.assert_awaited_once() + await client.close() + + +async def test_borrowed_pool_remains_open_after_store_closes(definition): + pool = MagicMock(spec=aioodbc.Pool) + pool.close = MagicMock() + pool.wait_closed = AsyncMock() + connection, cursor = fake_connection() + pool.acquire.return_value.__aenter__.return_value = connection + cursor.fetchall.return_value = [("existing",)] + async with SqlServerStore(client=pool) as store: + collection = store.get_collection(dict, definition=definition) + assert not collection.managed_client + assert await store.list_collection_names() == ["existing"] + await collection.close() + pool.close.assert_not_called() + pool.wait_closed.assert_not_awaited() + connection.commit.assert_awaited_once() + connection.close.assert_not_awaited() + + +async def test_borrowed_autocommit_pool_rejected_before_database_operations(): + pool = MagicMock(spec=aioodbc.Pool) + connection, cursor = fake_connection() + connection.autocommit = True + pool.acquire.return_value.__aenter__.return_value = connection + client = _Client(None, pool) + with pytest.raises(ValueError, match="autocommit=False"): + async with client.connection(write=True): + pytest.fail("Autocommit pool must be rejected before the operation starts") + cursor.execute.assert_not_awaited() + connection.commit.assert_not_awaited() + await client.close() + pool.close.assert_not_called() + + +async def test_driver_error_is_chained_and_owned_connection_rolled_back(): + pool = MagicMock(spec=aioodbc.Pool) + pool.close = MagicMock() + pool.wait_closed = AsyncMock() + connection, cursor = fake_connection() + cursor.execute.side_effect = pyodbc.OperationalError("unavailable") + pool.acquire.return_value.__aenter__.return_value = connection + client = _Client(SecretString("Driver={ODBC Driver 18 for SQL Server};"), None) + with patch.object(module.aioodbc, "create_pool", new_callable=AsyncMock, return_value=pool): + with pytest.raises(IntegrationException) as error: + async with client.connection(write=True) as acquired, module._cursor(acquired) as cursor: + await cursor.execute("SELECT 1") + await client.close() + assert isinstance(error.value.__cause__, pyodbc.OperationalError) + connection.rollback.assert_awaited_once() + + +@vectorstoremodel(collection_name="typed_sql_notes") +@dataclass +class TypedNote: + id: Annotated[str, VectorStoreField("key")] + title: Annotated[str, VectorStoreField("data")] + embedding: Annotated[list[float] | None, VectorStoreField("vector", dimensions=3)] = None + + +async def test_decorated_records_round_trip_without_vectors_by_default(): + collection = SqlServerCollection(TypedNote, connection_string="x") + connection, cursor = fake_connection() + cursor.fetchall.return_value = [("one", "title")] + + @asynccontextmanager + async def acquire(*, write=False): + yield connection + + with patch.object(collection._client, "connection", side_effect=acquire): + assert await collection.get(["one"]) == [TypedNote("one", "title", None)] diff --git a/python/pyproject.toml b/python/pyproject.toml index 35066954f0e..c353167a1b9 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -106,6 +106,7 @@ agent-framework-ollama = { workspace = true } agent-framework-openai = { workspace = true } agent-framework-orchestrations = { workspace = true } agent-framework-postgres = { workspace = true } +agent-framework-sql-server = { workspace = true } agent-framework-purview = { workspace = true } agent-framework-qdrant = { workspace = true } agent-framework-redis = { workspace = true } diff --git a/python/uv.lock b/python/uv.lock index 67edc89074c..a634098791a 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -67,6 +67,7 @@ members = [ "agent-framework-purview", "agent-framework-qdrant", "agent-framework-redis", + "agent-framework-sql-server", "agent-framework-tools", ] overrides = [ @@ -999,6 +1000,23 @@ requires-dist = [ { name = "redisvl", specifier = ">=0.11.0,<0.16" }, ] +[[package]] +name = "agent-framework-sql-server" +version = "1.0.0a260923" +source = { editable = "packages/sql-server" } +dependencies = [ + { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "aioodbc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyodbc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework-core", editable = "packages/core" }, + { name = "aioodbc", specifier = ">=0.5.0,<0.6" }, + { name = "pyodbc", specifier = ">=5.3.0,<6" }, +] + [[package]] name = "agent-framework-tools" version = "1.0.0b260918" @@ -1173,6 +1191,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/d3/2d310b1b839014034dba0cba685e492df8a5c7ad32c19cab7e979eed6554/aiologic-0.17.1-py3-none-any.whl", hash = "sha256:c66b319830fedb7ca3d2b2125fa6f5b653f89418c2a27ea76f259ec5f00943c0", size = 161331, upload-time = "2026-06-27T20:41:31.877Z" }, ] +[[package]] +name = "aioodbc" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyodbc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/87/3a7580938f217212a574ba0d1af78203fc278fc439815f3fc515a7fdc12b/aioodbc-0.5.0.tar.gz", hash = "sha256:cbccd89ce595c033a49c9e6b4b55bbace7613a104b8a46e3d4c58c4bc4f25075", size = 41298, upload-time = "2023-10-28T21:37:29.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/80/4d1565bc16b53cd603c73dc4bc770e2e6418d957417e05031314760dc28c/aioodbc-0.5.0-py3-none-any.whl", hash = "sha256:bcaf16f007855fa4bf0ce6754b1f72c6c5a3d544188849577ddd55c5dc42985e", size = 19449, upload-time = "2023-10-28T21:37:28.51Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -5146,6 +5176,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/f2/12afe9d59fb82b60832356f9fe8f454a99371c0e0a42f0ec7856f5c893fd/pymongo-4.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:960c61ed86a316d3636b1f184006d3cc2b972b8546186ae8e2c8324e397632ff", size = 817669, upload-time = "2026-09-10T14:56:50.602Z" }, ] +[[package]] +name = "pyodbc" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/85/44b10070a769a56bd910009bb185c0c0a82daff8d567cd1a116d7d730c7d/pyodbc-5.3.0.tar.gz", hash = "sha256:2fe0e063d8fb66efd0ac6dc39236c4de1a45f17c33eaded0d553d21c199f4d05", size = 121770, upload-time = "2025-10-17T18:04:09.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/c7/534986d97a26cb8f40ef456dfcf00d8483161eade6d53fa45fcf2d5c2b87/pyodbc-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebc3be93f61ea0553db88589e683ace12bf975baa954af4834ab89f5ee7bf8ae", size = 71958, upload-time = "2025-10-17T18:03:10.163Z" }, + { url = "https://files.pythonhosted.org/packages/69/3c/6fe3e9eae6db1c34d6616a452f9b954b0d5516c430f3dd959c9d8d725f2a/pyodbc-5.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9b987a25a384f31e373903005554230f5a6d59af78bce62954386736a902a4b3", size = 71843, upload-time = "2025-10-17T18:03:11.058Z" }, + { url = "https://files.pythonhosted.org/packages/44/0e/81a0315d0bf7e57be24338dbed616f806131ab706d87c70f363506dc13d5/pyodbc-5.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:676031723aac7dcbbd2813bddda0e8abf171b20ec218ab8dfb21d64a193430ea", size = 327191, upload-time = "2025-10-17T18:03:11.93Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/b95bb2068f911950322a97172c68675c85a3e87dc04a98448c339fcbef21/pyodbc-5.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5c30c5cd40b751f77bbc73edd32c4498630939bcd4e72ee7e6c9a4b982cc5ca", size = 332228, upload-time = "2025-10-17T18:03:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/dc/21/2433625f7d5922ee9a34e3805805fa0f1355d01d55206c337bb23ec869bf/pyodbc-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2035c7dfb71677cd5be64d3a3eb0779560279f0a8dc6e33673499498caa88937", size = 1296469, upload-time = "2025-10-17T18:03:14.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f4/c760caf7bb9b3ab988975d84bd3e7ebda739fe0075c82f476d04ee97324c/pyodbc-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5cbe4d753723c8a8f65020b7a259183ef5f14307587165ce37e8c7e251951852", size = 1353163, upload-time = "2025-10-17T18:03:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/14/ad/f9ca1e9e44fd91058f6e35b233b1bb6213d590185bfcc2a2c4f1033266e7/pyodbc-5.3.0-cp311-cp311-win32.whl", hash = "sha256:d255f6b117d05cfc046a5201fdf39535264045352ea536c35777cf66d321fbb8", size = 62925, upload-time = "2025-10-17T18:03:17.649Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/52b9b94efd8cfd11890ae04f31f50561710128d735e4e38a8fbb964cd2c2/pyodbc-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:f1ad0e93612a6201621853fc661209d82ff2a35892b7d590106fe8f97d9f1f2a", size = 69329, upload-time = "2025-10-17T18:03:18.474Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6f/bf5433bb345007f93003fa062e045890afb42e4e9fc6bd66acc2c3bd12ca/pyodbc-5.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:0df7ff47fab91ea05548095b00e5eb87ed88ddf4648c58c67b4db95ea4913e23", size = 64447, upload-time = "2025-10-17T18:03:19.691Z" }, + { url = "https://files.pythonhosted.org/packages/f5/0c/7ecf8077f4b932a5d25896699ff5c394ffc2a880a9c2c284d6a3e6ea5949/pyodbc-5.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ebf6b5d989395efe722b02b010cb9815698a4d681921bf5db1c0e1195ac1bde", size = 72994, upload-time = "2025-10-17T18:03:20.551Z" }, + { url = "https://files.pythonhosted.org/packages/03/78/9fbde156055d88c1ef3487534281a5b1479ee7a2f958a7e90714968749ac/pyodbc-5.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:197bb6ddafe356a916b8ee1b8752009057fce58e216e887e2174b24c7ab99269", size = 72535, upload-time = "2025-10-17T18:03:21.423Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f9/8c106dcd6946e95fee0da0f1ba58cd90eb872eebe8968996a2ea1f7ac3c1/pyodbc-5.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6ccb5315ec9e081f5cbd66f36acbc820ad172b8fa3736cf7f993cdf69bd8a96", size = 333565, upload-time = "2025-10-17T18:03:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/2c70f47a76a4fafa308d148f786aeb35a4d67a01d41002f1065b465d9994/pyodbc-5.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5dd3d5e469f89a3112cf8b0658c43108a4712fad65e576071e4dd44d2bd763c7", size = 340283, upload-time = "2025-10-17T18:03:23.691Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b2/0631d84731606bfe40d3b03a436b80cbd16b63b022c7b13444fb30761ca8/pyodbc-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b180bc5e49b74fd40a24ef5b0fe143d0c234ac1506febe810d7434bf47cb925b", size = 1302767, upload-time = "2025-10-17T18:03:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/74/b9/707c5314cca9401081b3757301241c167a94ba91b4bd55c8fa591bf35a4a/pyodbc-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3c39de3005fff3ae79246f952720d44affc6756b4b85398da4c5ea76bf8f506", size = 1361251, upload-time = "2025-10-17T18:03:26.538Z" }, + { url = "https://files.pythonhosted.org/packages/97/7c/893036c8b0c8d359082a56efdaa64358a38dda993124162c3faa35d1924d/pyodbc-5.3.0-cp312-cp312-win32.whl", hash = "sha256:d32c3259762bef440707098010035bbc83d1c73d81a434018ab8c688158bd3bb", size = 63413, upload-time = "2025-10-17T18:03:27.903Z" }, + { url = "https://files.pythonhosted.org/packages/c0/70/5e61b216cc13c7f833ef87f4cdeab253a7873f8709253f5076e9bb16c1b3/pyodbc-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe77eb9dcca5fc1300c9121f81040cc9011d28cff383e2c35416e9ec06d4bc95", size = 70133, upload-time = "2025-10-17T18:03:28.746Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/e7d0629c9714a85eb4f85d21602ce6d8a1ec0f313fde8017990cf913e3b4/pyodbc-5.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:afe7c4ac555a8d10a36234788fc6cfc22a86ce37fc5ba88a1f75b3e6696665dc", size = 64700, upload-time = "2025-10-17T18:03:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/9e74cbcc1d4878553eadfd59138364b38656369eb58f7e5b42fb344c0ce7/pyodbc-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e9ab0b91de28a5ab838ac4db0253d7cc8ce2452efe4ad92ee6a57b922bf0c24", size = 72975, upload-time = "2025-10-17T18:03:30.466Z" }, + { url = "https://files.pythonhosted.org/packages/37/c7/27d83f91b3144d3e275b5b387f0564b161ddbc4ce1b72bb3b3653e7f4f7a/pyodbc-5.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6132554ffbd7910524d643f13ce17f4a72f3a6824b0adef4e9a7f66efac96350", size = 72541, upload-time = "2025-10-17T18:03:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/2bb24e7fc95e98a7b11ea5ad1f256412de35d2e9cc339be198258c1d9a76/pyodbc-5.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1629af4706e9228d79dabb4863c11cceb22a6dab90700db0ef449074f0150c0d", size = 343287, upload-time = "2025-10-17T18:03:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/fa/24/88cde8b6dc07a93a92b6c15520a947db24f55db7bd8b09e85956642b7cf3/pyodbc-5.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ceaed87ba2ea848c11223f66f629ef121f6ebe621f605cde9cfdee4fd9f4b68", size = 350094, upload-time = "2025-10-17T18:03:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/99/53c08562bc171a618fa1699297164f8885e66cde38c3b30f454730d0c488/pyodbc-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3cc472c8ae2feea5b4512e23b56e2b093d64f7cbc4b970af51da488429ff7818", size = 1301029, upload-time = "2025-10-17T18:03:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/d8/10/68a0b5549876d4b53ba4c46eed2a7aca32d589624ed60beef5bd7382619e/pyodbc-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c79df54bbc25bce9f2d87094e7b39089c28428df5443d1902b0cc5f43fd2da6f", size = 1361420, upload-time = "2025-10-17T18:03:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/41/0f/9dfe4987283ffcb981c49a002f0339d669215eb4a3fe4ee4e14537c52852/pyodbc-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c2eb0b08e24fe5c40c7ebe9240c5d3bd2f18cd5617229acee4b0a0484dc226f2", size = 63399, upload-time = "2025-10-17T18:03:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/56/03/15dcefe549d3888b649652af7cca36eda97c12b6196d92937ca6d11306e9/pyodbc-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:01166162149adf2b8a6dc21a212718f205cabbbdff4047dc0c415af3fd85867e", size = 70133, upload-time = "2025-10-17T18:03:38.47Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c1/c8b128ae59a14ecc8510e9b499208e342795aecc3af4c3874805c720b8db/pyodbc-5.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:363311bd40320b4a61454bebf7c38b243cd67c762ed0f8a5219de3ec90c96353", size = 64683, upload-time = "2025-10-17T18:03:39.68Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/c26d82a7ce1e90b8bbb8731d3d53de73814e2f6606b9db9d978303aa8d5f/pyodbc-5.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3f1bdb3ce6480a17afaaef4b5242b356d4997a872f39e96f015cabef00613797", size = 73513, upload-time = "2025-10-17T18:03:40.536Z" }, + { url = "https://files.pythonhosted.org/packages/82/d5/1ab1b7c4708cbd701990a8f7183c5bb5e0712d5e8479b919934e46dadab4/pyodbc-5.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7713c740a10f33df3cb08f49a023b7e1e25de0c7c99650876bbe717bc95ee780", size = 72631, upload-time = "2025-10-17T18:03:41.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/7e3831eeac2b09b31a77e6b3495491ce162035ff2903d7261b49d35aa3c2/pyodbc-5.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf18797a12e70474e1b7f5027deeeccea816372497e3ff2d46b15bec2d18a0cc", size = 344580, upload-time = "2025-10-17T18:03:42.67Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a6/71d26d626a3c45951620b7ff356ec920e420f0e09b0a924123682aa5e4ab/pyodbc-5.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:08b2439500e212625471d32f8fde418075a5ddec556e095e5a4ba56d61df2dc6", size = 350224, upload-time = "2025-10-17T18:03:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/93/14/f702c5e8c2d595776266934498505f11b7f1545baf21ffec1d32c258e9d3/pyodbc-5.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:729c535341bb09c476f219d6f7ab194bcb683c4a0a368010f1cb821a35136f05", size = 1301503, upload-time = "2025-10-17T18:03:45.013Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b2/ad92ebdd1b5c7fec36b065e586d1d34b57881e17ba5beec5c705f1031058/pyodbc-5.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c67e7f2ce649155ea89beb54d3b42d83770488f025cf3b6f39ca82e9c598a02e", size = 1361050, upload-time = "2025-10-17T18:03:46.298Z" }, + { url = "https://files.pythonhosted.org/packages/19/40/dc84e232da07056cb5aaaf5f759ba4c874bc12f37569f7f1670fc71e7ae1/pyodbc-5.3.0-cp314-cp314-win32.whl", hash = "sha256:a48d731432abaee5256ed6a19a3e1528b8881f9cb25cb9cf72d8318146ea991b", size = 65670, upload-time = "2025-10-17T18:03:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/b8/79/c48be07e8634f764662d7a279ac204f93d64172162dbf90f215e2398b0bd/pyodbc-5.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:58635a1cc859d5af3f878c85910e5d7228fe5c406d4571bffcdd281375a54b39", size = 72177, upload-time = "2025-10-17T18:03:57.296Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/e304574446b2263f428ce14df590ba52c2e0e0205e8d34b235b582b7d57e/pyodbc-5.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:754d052030d00c3ac38da09ceb9f3e240e8dd1c11da8906f482d5419c65b9ef5", size = 66668, upload-time = "2025-10-17T18:03:58.174Z" }, + { url = "https://files.pythonhosted.org/packages/43/17/f4eabf443b838a2728773554017d08eee3aca353102934a7e3ba96fb0e31/pyodbc-5.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f927b440c38ade1668f0da64047ffd20ec34e32d817f9a60d07553301324b364", size = 75780, upload-time = "2025-10-17T18:03:47.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/ea/e79e168c3d38c27d59d5d96273fd9e3c3ba55937cc944c4e60618f51de90/pyodbc-5.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:25c4cfb2c08e77bc6e82f666d7acd52f0e52a0401b1876e60f03c73c3b8aedc0", size = 75503, upload-time = "2025-10-17T18:03:48.171Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/d1d7c125ec4a20e83fdc28e119b8321192b2bd694f432cf63e1199b2b929/pyodbc-5.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc834567c2990584b9726cba365834d039380c9dbbcef3030ddeb00c6541b943", size = 398356, upload-time = "2025-10-17T18:03:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fc/f6be4b3cc3910f8c2aba37aa41671121fd6f37b402ae0fefe53a70ac7cd5/pyodbc-5.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8339d3094858893c1a68ee1af93efc4dff18b8b65de54d99104b99af6306320d", size = 397291, upload-time = "2025-10-17T18:03:50.18Z" }, + { url = "https://files.pythonhosted.org/packages/03/2e/0610b1ed05a5625528d52f6cece9610e84617d35f475c89c2a52f66d13f7/pyodbc-5.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74528fe148980d0c735c0ebb4a4dc74643ac4574337c43c1006ac4d09593f92d", size = 1353900, upload-time = "2025-10-17T18:03:51.339Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f1/43497e1d37f9f71b43b2b3172e7b1bdf50851e278390c3fb6b46a3630c53/pyodbc-5.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d89a7f2e24227150c13be8164774b7e1f9678321a4248f1356a465b9cc17d31e", size = 1406062, upload-time = "2025-10-17T18:03:52.546Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/88a1277c2f7d9ab1cec0a71e074ba24fd4a1710a43974682546da90a1343/pyodbc-5.3.0-cp314-cp314t-win32.whl", hash = "sha256:af4d8c9842fc4a6360c31c35508d6594d5a3b39922f61b282c2b4c9d9da99514", size = 70132, upload-time = "2025-10-17T18:03:53.715Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/ee98c62050de4aa8bafb6eb1e11b95e0b0c898bd5930137c6dc776e06a9b/pyodbc-5.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bfeb3e34795d53b7d37e66dd54891d4f9c13a3889a8f5fe9640e56a82d770955", size = 79452, upload-time = "2025-10-17T18:03:54.664Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/d8889efd96bbe8e5d43ff9701f6b1565a8e09c3e1f58c388d550724f777b/pyodbc-5.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:13656184faa3f2d5c6f19b701b8f247342ed581484f58bf39af7315c054e69db", size = 70142, upload-time = "2025-10-17T18:03:55.551Z" }, +] + [[package]] name = "pyrefly" version = "1.3.1" From 0c3db93ef1a77a0df13b0e78d7cc3a0eb3abbc6d Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 23 Sep 2026 13:49:13 +0200 Subject: [PATCH 2/9] refactor(python): use mssql-python for SQL Server vectors --- .github/workflows/python-tests.yml | 2 +- python/packages/sql-server/README.md | 94 ++- .../agent_framework_sql_server/__init__.py | 3 +- .../_vector_store.py | 320 ++++------ python/packages/sql-server/pyproject.toml | 5 +- .../sql-server/samples/sql_server_vectors.py | 12 +- .../tests/sql_server/test_integration.py | 25 +- .../tests/sql_server/test_vector_store.py | 398 ++++++------- python/uv.lock | 560 +++--------------- 9 files changed, 440 insertions(+), 979 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 56f8b92e5b1..ff182ebfdb8 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -39,7 +39,7 @@ jobs: with: python-version: ${{ matrix.python-version }} os: ${{ runner.os }} - exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot agent-framework-azure-cosmos-memory' || '' }} + exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot agent-framework-azure-cosmos-memory' || matrix.python-version == '3.15' && 'agent-framework-sql-server' || '' }} env: # Configure a constant location for the uv cache UV_CACHE_DIR: /tmp/.uv-cache diff --git a/python/packages/sql-server/README.md b/python/packages/sql-server/README.md index 0cb36e4b65c..7932b5aae91 100644 --- a/python/packages/sql-server/README.md +++ b/python/packages/sql-server/README.md @@ -2,8 +2,8 @@ Store typed Agent Framework records in SQL Server and Azure SQL native `VECTOR` columns, with exact, database-side similarity search. This alpha package exports -`SqlServerCollection`, `SqlServerStore`, `SqlServerSettings`, and the -`SqlServerClient` type from `agent_framework_sql_server`. +`SqlServerCollection`, `SqlServerStore`, and `SqlServerSettings` directly from +`agent_framework_sql_server`. ## Install and provision @@ -11,22 +11,24 @@ columns, with exact, database-side similarity search. This alpha package exports pip install agent-framework-sql-server --pre ``` -Requires Python 3.10+, a registered **Microsoft ODBC Driver 18 for SQL Server** -(18.6.1.1 or later recommended), and a vector-enabled database: SQL Server -2025 (17.x), Azure SQL Database, Azure SQL Managed Instance on the SQL Server -2025 or Always-up-to-date update policy, or SQL database in Microsoft Fabric. +Requires Python 3.10–3.14 and a vector-enabled database: SQL Server 2025 +(17.x), Azure SQL Database, Azure SQL Managed Instance on the SQL Server 2025 +or Always-up-to-date update policy, or SQL database in Microsoft Fabric. Older SQL Server releases do not support `VECTOR`/`VECTOR_DISTANCE`. -Install the ODBC driver **separately** on the machine running Python: -[macOS (Intel/Apple Silicon)](https://learn.microsoft.com/sql/connect/odbc/linux-mac/install-microsoft-odbc-driver-sql-server-macos), -[Linux](https://learn.microsoft.com/sql/connect/odbc/linux-mac/install-microsoft-odbc-driver-sql-server-linux), -or [Windows](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server). -On macOS the Microsoft Homebrew formula installs unixODBC as a dependency. -The Python dependencies `aioodbc` and `pyodbc` do **not** install the Microsoft -ODBC driver. Configure its `vectorTypeSupport=off` setting (the default): -the server stores native vectors while ODBC reads/writes their JSON-array -representation. `vectorTypeSupport=v1/v2` exposes a C-specific representation -that this Python package does not decode. +The package uses Microsoft's [`mssql-python` 1.15+ driver](https://pypi.org/project/mssql-python/). +It installs its `mssql-python-odbc` binary companion automatically; no external +ODBC Driver 18 or driver manager is required. Published wheels cover CPython +3.10–3.14 on Windows x64, Linux x64/ARM64, and **macOS 15+** +Intel/Apple Silicon; Windows ARM64 wheels start at Python 3.11. macOS 14 is +listed in the driver's support documentation, but the published macOS wheels +are tagged `macosx_15_0_universal2` and there is no source distribution. +Python 3.15 has no published wheel yet: this package caps `requires-python` +below 3.15, and the repository's non-blocking 3.15 CI lane excludes it. +The workspace lockfile likewise limits Python below 3.15 while this package +remains a member; the experimental 3.15 lane excludes it before resolving. +Follow the [driver's installation instructions](https://learn.microsoft.com/sql/connect/python/mssql-python/installation) +for system libraries on Linux and OpenSSL on macOS. The database administrator must provide an existing schema (default `dbo`). `ensure_collection_exists()` creates the requested table and scalar indexes @@ -35,38 +37,32 @@ settings. `ensure_collection_deleted()` drops only that table. ## Connection settings and ownership -Set `SQL_SERVER_CONNECTION_STRING` to an ODBC connection string for the selected -database with a registered `Driver={ODBC Driver 18 for SQL Server}` and your -chosen server, encryption, and authentication settings. Do not commit -connection strings containing credentials. Alternatively, pass +Set `SQL_SERVER_CONNECTION_STRING` to a driver connection string such as +`Server=tcp:,1433;Database=;Authentication=ActiveDirectoryDefault;Encrypt=yes`. +Do not commit connection strings containing credentials. Alternatively, pass `connection_string` as a string or Agent Framework `SecretString` to `SqlServerStore` or `SqlServerCollection`. Settings precedence is **explicit argument > selected `.env` file > process environment**. Choose a file with `env_file_path` and optional `env_file_encoding`; no `.env` file is discovered implicitly, and missing/empty strings are rejected. -An owned store creates a lazy one-connection `aioodbc` pool with a dedicated -single-worker executor; `close()` or an async context manager releases both. -Collections created by the store share that pool: keep the store open while -using them. To borrow an existing `aioodbc.Connection` or `aioodbc.Pool`, pass -`client=` instead of connection settings. The connector never closes an -injected client. Raw `pyodbc.Connection` objects are **not** accepted; use the -async aioodbc type. Avoid using the same borrowed connection concurrently -outside the connector; for a caller-created pool, configure its executor -appropriately for your workload. - -Batch writes on owned connections commit or roll back together. On borrowed -**connections** the connector starts/commits its own transaction if none is -active; otherwise it uses a SQL Server savepoint and leaves the caller's -transaction open. On a borrowed **pool**, each acquired connection's operation -is committed or rolled back before release; the pool must use -`autocommit=False` (aioodbc's default), and callers must return clean -connections to it. If a caller's transaction becomes uncommittable, the caller -must roll it back. SQL Server savepoints are not available in distributed -transactions. -`aioodbc` uses worker threads internally: cancelling a coroutine cannot -interrupt an already-running ODBC query, so a write may still finish. Set -appropriate database query timeouts, and prefer stable application-provided +The connector owns all connections. Each whole operation opens, uses, commits +or rolls back, and closes a `mssql-python` connection on a dedicated worker +thread, keeping Agent Framework's async calls nonblocking. The driver's +built-in pooling can reuse the underlying physical connection. A store and +its collections share one worker; a standalone collection owns its own. +Call `close()` or use an async context manager to release the worker. There is +no `client=` or connection-factory constructor argument: arbitrary caller-owned +`mssql-python` connections cannot safely cross threads (`threadsafety=1`). +This is intentionally narrower than connectors that support borrowed async +clients. + +Batch writes commit or roll back together on one connection. Cancelling an +async operation waits for its worker to finish cleanup; it cannot interrupt an +already-running synchronous SQL statement, and the transaction may already +have committed. Set `query_timeout=30` (seconds, for example) on the store or +collection when bounding database calls; leaving it unset uses the driver's +default, and `0` disables the timeout. Prefer stable application-provided keys when retrying writes. ## Example @@ -119,13 +115,10 @@ indexes/search, preview-only float16 vectors, keyword-hybrid search, sparse or binary vectors, server-side embedding generation, and schema migration are not supported. -`mssql-python` 1.15.0 was evaluated. It supports Python 3.10 and bound JSON -vectors, but has no released native async API, reports `threadsafety=1` (so -moving a caller-created raw connection between AF worker threads is unsafe), -and publishes only macOS 15+ wheels with no source distribution. This -connector instead uses `aioodbc`/`pyodbc` to retain an async, borrowed-client -interface and broader macOS compatibility at the cost of a separately -installed ODBC driver. +The server stores native `VECTOR` columns, but `mssql-python` 1.15 does not +expose a native Python vector type. The connector binds JSON-encoded vectors +as parameters and parses JSON on retrieval; SQL Server converts to/from the +native type. It does not enable native driver vector bindings. ## Service tests @@ -147,5 +140,6 @@ designated server lacks vector support. - [SQL Server vector type and database availability](https://learn.microsoft.com/sql/t-sql/data-types/vector-data-type) - [Exact vector distance metrics](https://learn.microsoft.com/sql/t-sql/functions/vector-distance-transact-sql) -- [ODBC vector JSON compatibility mode](https://learn.microsoft.com/sql/connect/odbc/vector-data-type) +- [Microsoft's Python vector JSON example](https://learn.microsoft.com/sql/t-sql/data-types/vector-data-type#python) +- [mssql-python asynchronous integration patterns](https://learn.microsoft.com/sql/connect/python/mssql-python/asynchronous-patterns) - [Microsoft Agent Framework](https://learn.microsoft.com/agent-framework/) diff --git a/python/packages/sql-server/agent_framework_sql_server/__init__.py b/python/packages/sql-server/agent_framework_sql_server/__init__.py index 412f91b5f1c..456e38131c4 100644 --- a/python/packages/sql-server/agent_framework_sql_server/__init__.py +++ b/python/packages/sql-server/agent_framework_sql_server/__init__.py @@ -6,7 +6,7 @@ import importlib.metadata -from ._vector_store import SqlServerClient, SqlServerCollection, SqlServerSettings, SqlServerStore +from ._vector_store import SqlServerCollection, SqlServerSettings, SqlServerStore try: __version__ = importlib.metadata.version(__name__) @@ -14,7 +14,6 @@ __version__ = "0.0.0" __all__ = [ - "SqlServerClient", "SqlServerCollection", "SqlServerSettings", "SqlServerStore", diff --git a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py index f71e3f57032..2667defcf7e 100644 --- a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py +++ b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py @@ -7,14 +7,12 @@ import asyncio import hashlib import math -from collections.abc import AsyncGenerator, Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from concurrent.futures import ThreadPoolExecutor -from contextlib import asynccontextmanager from dataclasses import replace -from typing import Any, ClassVar, Generic, Protocol, TypeAlias, cast +from typing import Any, ClassVar, Generic, cast -import aioodbc -import pyodbc +import mssql_python from agent_framework import ( BaseVectorCollection, BaseVectorSearch, @@ -42,29 +40,8 @@ KeyT = TypeVar("KeyT", default=Any) ModelT = TypeVar("ModelT", default=Any) +ResultT = TypeVar("ResultT") _KEY_BATCH_SIZE = 1000 -_SAVEPOINT = "af_vector_operation" -SqlServerClient: TypeAlias = aioodbc.Connection | aioodbc.Pool - - -class _Cursor(Protocol): - async def execute(self, sql: str, *parameters: Any) -> Any: ... - - async def fetchone(self) -> Sequence[Any] | None: ... - - async def fetchall(self) -> Sequence[Sequence[Any]]: ... - - async def close(self) -> None: ... - - -@asynccontextmanager -async def _cursor(connection: aioodbc.Connection) -> AsyncGenerator[_Cursor]: - # aioodbc's cursor context manager commits its connection on exit, including borrowed transactions. - cursor = cast(_Cursor, await connection.cursor()) # pyright: ignore[reportUnknownMemberType] - try: - yield cursor - finally: - await cursor.close() class SqlServerSettings(TypedDict, total=False): @@ -79,15 +56,15 @@ def _validate_options(options: Mapping[str, Any] | None) -> None: raise NotImplementedError(f"Unsupported SQL Server operation option(s): {', '.join(sorted(options))}.") -async def _execute(cursor: _Cursor, statement: str, parameters: Sequence[Any] = ()) -> None: +def _execute(cursor: mssql_python.Cursor, statement: str, parameters: Sequence[Any] = ()) -> None: if parameters: - await cursor.execute(statement, tuple(parameters)) + cursor.execute(statement, tuple(parameters)) # pyright: ignore[reportUnknownMemberType] else: - await cursor.execute(statement) + cursor.execute(statement) # pyright: ignore[reportUnknownMemberType] -async def _rows(cursor: _Cursor, count: int) -> list[Sequence[Any]]: - result = list(await cursor.fetchall()) +def _rows(cursor: mssql_python.Cursor, count: int) -> list[Sequence[Any]]: + result: list[Sequence[Any]] = [tuple(row) for row in cursor.fetchall()] for row in result: if len(row) != count: raise IntegrationInvalidResponseException("SQL Server returned a row with an unexpected column count.") @@ -100,142 +77,76 @@ def _check_parameter_count(parameters: Sequence[Any]) -> None: class _Client: - """Own a lazy pool and its dedicated ODBC worker, or borrow an async connection/pool.""" + """Own one worker; every operation creates and releases a connection on that worker.""" - def __init__(self, connection_string: SecretString | None, client: SqlServerClient | None) -> None: - if (connection_string is None) == (client is None): - raise ValueError("Supply exactly one of connection_string or client.") - if connection_string is not None and not connection_string.get_secret_value().strip(): + def __init__(self, connection_string: SecretString | None, *, query_timeout: int | None = None) -> None: + if connection_string is None: + raise ValueError("SQL_SERVER_CONNECTION_STRING or an explicit connection_string is required.") + if not connection_string.get_secret_value().strip(): raise ValueError("connection_string must not be empty.") - if client is not None and not isinstance(client, (aioodbc.Connection, aioodbc.Pool)): - raise TypeError("client must be an aioodbc Connection or Pool.") + if query_timeout is not None and (type(query_timeout) is not int or query_timeout < 0): + raise ValueError("query_timeout must be a non-negative integer number of seconds.") self.connection_string = connection_string - self.client = client + self.query_timeout = query_timeout self.closed = False - self._pool: aioodbc.Pool | None = None self._executor: ThreadPoolExecutor | None = None - self._init_lock = asyncio.Lock() - self._borrowed_lock = asyncio.Lock() + self._close_lock = asyncio.Lock() def ensure_open(self) -> None: if self.closed: raise RuntimeError("The SQL Server client is closed.") - async def _owned_pool(self) -> aioodbc.Pool: - async with self._init_lock: - self.ensure_open() - if self._pool is not None: - return self._pool - if self.connection_string is None: - raise RuntimeError("No SQL Server connection string is configured.") - executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="af-sql-server") - try: - self._pool = await aioodbc.create_pool( - dsn=self.connection_string.get_secret_value(), - minsize=1, - maxsize=1, - autocommit=False, - executor=executor, - ) - except BaseException: - await asyncio.to_thread(executor.shutdown, wait=True) - raise - self._executor = executor - return self._pool - - @asynccontextmanager - async def _operation(self, connection: aioodbc.Connection, *, managed: bool, write: bool) -> AsyncGenerator[None]: - if managed: - try: - yield - await connection.commit() - except BaseException: - await connection.rollback() - raise - return - if not write: - yield - return - async with _cursor(connection) as cursor: - await _execute(cursor, "SELECT @@TRANCOUNT") - count = await cursor.fetchone() - if count is None or type(count[0]) is not int: - raise IntegrationInvalidResponseException("SQL Server did not return a transaction count.") - started = count[0] == 0 - await _execute(cursor, "BEGIN TRANSACTION" if started else f"SAVE TRANSACTION {_SAVEPOINT}") + def _run_sync(self, operation: Callable[[mssql_python.Cursor], ResultT]) -> ResultT: try: - yield - if started: - async with _cursor(connection) as cursor: - await _execute(cursor, "COMMIT TRANSACTION") - except BaseException as exc: - async with _cursor(connection) as cursor: - if started: - await _execute(cursor, "ROLLBACK TRANSACTION") - else: - await _execute(cursor, "SELECT XACT_STATE()") - state = await cursor.fetchone() - if state is None or state[0] != 1: - raise IntegrationException( - "The caller's SQL Server transaction cannot be rolled back to its savepoint; " - "the caller must roll back the transaction." - ) from exc - await _execute(cursor, f"ROLLBACK TRANSACTION {_SAVEPOINT}") - raise + connection = mssql_python.connect(self.connection_string.get_secret_value(), autocommit=False) + try: + if self.query_timeout is not None: + connection.timeout = self.query_timeout + cursor = connection.cursor() + try: + try: + result = operation(cursor) + finally: + cursor.close() + connection.commit() + return result + except BaseException: + connection.rollback() + raise + finally: + connection.close() + except mssql_python.Error as exc: + raise IntegrationException("SQL Server operation failed; inspect the chained driver exception.") from exc - @asynccontextmanager - async def connection(self, *, write: bool = False) -> AsyncGenerator[aioodbc.Connection]: - """Acquire one connection, keeping borrowed transactions under the caller's control.""" + async def run(self, operation: Callable[[mssql_python.Cursor], ResultT]) -> ResultT: + """Offload a whole transaction and wait for worker cleanup on cancellation.""" self.ensure_open() + if self._executor is None: + self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="af-sql-server") + future = asyncio.get_running_loop().run_in_executor(self._executor, self._run_sync, operation) try: - if self.client is None: - pool = await self._owned_pool() - async with pool.acquire() as connection, self._operation(connection, managed=True, write=write): - yield connection - elif isinstance(self.client, aioodbc.Pool): - async with self.client.acquire() as connection: - self.ensure_open() - if cast(bool, connection.autocommit): # pyright: ignore[reportUnknownMemberType] - raise ValueError("Borrowed aioodbc pools must use autocommit=False for atomic operations.") - async with self._operation(connection, managed=True, write=write): - yield connection - else: - async with self._borrowed_lock, self._operation(self.client, managed=False, write=write): - self.ensure_open() - yield self.client - except pyodbc.Error as exc: - raise IntegrationException("SQL Server operation failed; inspect the chained driver exception.") from exc + return await asyncio.shield(future) + except asyncio.CancelledError: + await asyncio.shield(future) + raise async def close(self) -> None: - """Close the owned pool and worker, never an injected client or pool.""" - async with self._init_lock: + """Wait for queued operations and shut down the owned worker.""" + async with self._close_lock: if self.closed: return - if isinstance(self.client, aioodbc.Connection): - async with self._borrowed_lock: - self.closed = True - return self.closed = True - try: - if self._pool is not None: - self._pool.close() - await self._pool.wait_closed() - finally: - if self._executor is not None: - await asyncio.to_thread(self._executor.shutdown, wait=True) + if self._executor is not None: + await asyncio.to_thread(self._executor.shutdown, wait=True) def _create_client( connection_string: str | SecretString | None, *, - client: SqlServerClient | None, + query_timeout: int | None, env_file_path: str | None, env_file_encoding: str | None, ) -> _Client: - if client is not None: - if connection_string is not None or env_file_path is not None or env_file_encoding is not None: - raise ValueError("client cannot be combined with connection_string, env_file_path, or env_file_encoding.") - return _Client(None, client) settings = load_settings( SqlServerSettings, env_prefix="SQL_SERVER_", @@ -246,7 +157,7 @@ def _create_client( resolved = settings.get("connection_string") if resolved is not None and not isinstance(resolved, SecretString): raise TypeError("connection_string must be a string or SecretString.") - return _Client(resolved, None) + return _Client(resolved, query_timeout=query_timeout) class SqlServerCollection( @@ -256,8 +167,8 @@ class SqlServerCollection( ): """Store typed rows and perform exact searches on SQL Server native VECTOR columns. - Tables are created in an existing schema and are never migrated. The owned - single-connection pool is lazy; injected connections and pools remain caller-owned. + Tables are created in an existing schema and are never migrated. Connections + are owned by the collection, or by its store when created from a store. """ supported_key_types: ClassVar[set[str] | None] = {"str", "int", "UUID"} @@ -269,7 +180,7 @@ def __init__( record_type: type[ModelT], *, connection_string: str | SecretString | None = None, - client: SqlServerClient | None = None, + query_timeout: int | None = None, schema: str = "dbo", definition: VectorStoreCollectionDefinition | None = None, collection_name: str | None = None, @@ -282,13 +193,13 @@ def __init__( Args: record_type: Decorated/registered model type, or ``dict``. - connection_string: Driver connection string for owned connections, or ``SQL_SERVER_CONNECTION_STRING``. - client: An open caller-owned aioodbc connection or pool instead of a connection string. + connection_string: Driver connection string, or ``SQL_SERVER_CONNECTION_STRING``. + query_timeout: Optional per-statement timeout in seconds; ``0`` disables the timeout. schema: Existing database schema containing the table. definition: Explicit field definition for dictionary records. collection_name: Table name overriding the model definition. embedding_generator: Default local embedding generator. - env_file_path: Optional .env file; cannot be combined with an injected client. + env_file_path: Optional .env file. env_file_encoding: Encoding of the selected .env file. _shared_client: Internal shared client owned by a ``SqlServerStore``. """ @@ -297,7 +208,7 @@ def __init__( definition=definition, collection_name=collection_name, embedding_generator=embedding_generator, - managed_client=client is None and _shared_client is None, + managed_client=_shared_client is None, ) self.schema = schema self._table = f"{_quote_identifier(schema)}.{_quote_identifier(self.collection_name)}" @@ -319,8 +230,8 @@ def __init__( raise NotImplementedError(f"SQL Server cannot index data fields of type '{field.type_}'.") if _shared_client is not None: if ( - client is not None - or connection_string is not None + connection_string is not None + or query_timeout is not None or env_file_path is not None or env_file_encoding is not None ): @@ -328,7 +239,10 @@ def __init__( self._client = _shared_client else: self._client = _create_client( - connection_string, client=client, env_file_path=env_file_path, env_file_encoding=env_file_encoding + connection_string, + query_timeout=query_timeout, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, ) self._shared_client = _shared_client is not None @@ -341,7 +255,7 @@ async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None await self.close() async def close(self) -> None: - """Close only this collection's client, never a store's or the caller's connection.""" + """Close a collection-owned worker, never its store's worker.""" if not self._shared_client: await self._client.close() @@ -386,8 +300,8 @@ async def ensure_collection_exists(self, *, operation_options: Mapping[str, Any] f"BEGIN CREATE TABLE {self._table} ({self._column_definitions()}) END" ) - async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: - await _execute(cursor, statement, [self.schema, self.collection_name]) + def create(cursor: mssql_python.Cursor) -> None: + _execute(cursor, statement, [self.schema, self.collection_name]) for field in self._fields: if field.field_type != "data" or not field.is_indexed: continue @@ -400,26 +314,29 @@ async def ensure_collection_exists(self, *, operation_options: Mapping[str, Any] "WHERE s.name = ? AND t.name = ? AND i.name = ?) " f"CREATE INDEX {_quote_identifier(index_name)} ON {self._table} ({_quote_identifier(name)})" ) - await _execute(cursor, index_statement, [self.schema, self.collection_name, index_name]) + _execute(cursor, index_statement, [self.schema, self.collection_name, index_name]) + + await self._client.run(create) async def collection_exists(self, *, operation_options: Mapping[str, Any] | None = None) -> bool: """Return whether a base table exists in the configured schema.""" _validate_options(operation_options) - async with self._client.connection() as connection, _cursor(connection) as cursor: - await _execute( + def exists(cursor: mssql_python.Cursor) -> bool: + _execute( cursor, "SELECT 1 FROM sys.tables AS t JOIN sys.schemas AS s ON s.schema_id = t.schema_id " "WHERE s.name = ? AND t.name = ?", [self.schema, self.collection_name], ) - return await cursor.fetchone() is not None + return cursor.fetchone() is not None + + return await self._client.run(exists) async def ensure_collection_deleted(self, *, operation_options: Mapping[str, Any] | None = None) -> None: """Drop only the table in this schema; never drop the schema or other tables.""" _validate_options(operation_options) - async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: - await _execute(cursor, f"DROP TABLE IF EXISTS {self._table}") + await self._client.run(lambda cursor: _execute(cursor, f"DROP TABLE IF EXISTS {self._table}")) def _deserialize_store_models_to_dicts( self, records: Sequence[Any], *, context: Mapping[str, Any] | None = None @@ -463,30 +380,30 @@ async def _inner_upsert( key_name = self.definition.key_field_storage_name key_column = _quote_identifier(key_name) - async def upsert(cursor: _Cursor) -> list[KeyT]: + def upsert(cursor: mssql_python.Cursor) -> list[KeyT]: keys: list[KeyT] = [] for row in prepared: names = tuple(row) if key_name in row: - await _execute( + _execute( cursor, f"SELECT {key_column} FROM {self._table} WITH (UPDLOCK, HOLDLOCK) WHERE {key_column} = ?", # nosec B608 [row[key_name]], ) - if await cursor.fetchone() is not None: + if cursor.fetchone() is not None: updates = [name for name in names if name != key_name] assignments = ", ".join(f"{_quote_identifier(name)} = ?" for name in updates) if not assignments: assignments = f"{key_column} = {key_column}" parameters = [*(row[name] for name in updates), row[key_name]] _check_parameter_count(parameters) - await _execute( + _execute( cursor, f"UPDATE {self._table} SET {assignments} OUTPUT INSERTED.{key_column} " # nosec B608 f"WHERE {key_column} = ?", parameters, ) - result = await cursor.fetchone() + result = cursor.fetchone() if result is None: raise IntegrationInvalidResponseException("SQL Server did not return an updated key.") keys.append(cast(KeyT, self._parsed_key(result[0]))) @@ -501,17 +418,16 @@ async def upsert(cursor: _Cursor) -> list[KeyT]: statement = f"INSERT INTO {self._table} ({columns}) OUTPUT INSERTED.{key_column} VALUES ({markers})" # nosec B608 parameters = [row[name] for name in names] _check_parameter_count(parameters) - await _execute(cursor, statement, parameters) + _execute(cursor, statement, parameters) else: - await _execute(cursor, f"INSERT INTO {self._table} OUTPUT INSERTED.{key_column} DEFAULT VALUES") - result = await cursor.fetchone() + _execute(cursor, f"INSERT INTO {self._table} OUTPUT INSERTED.{key_column} DEFAULT VALUES") + result = cursor.fetchone() if result is None: raise IntegrationInvalidResponseException("SQL Server did not return an inserted key.") keys.append(cast(KeyT, self._parsed_key(result[0]))) return keys - async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: - return await upsert(cursor) + return await self._client.run(upsert) def _order_by(self, order_by: Mapping[str, bool] | None) -> str: parts: list[str] = [] @@ -550,28 +466,29 @@ async def _inner_get( if not prepared_keys: return [] - async def get_keys(cursor: _Cursor) -> list[dict[str, Any]]: + def get_keys(cursor: mssql_python.Cursor) -> list[dict[str, Any]]: found: dict[Any, dict[str, Any]] = {} key_column = _quote_identifier(self.definition.key_field_storage_name) for offset in range(0, len(prepared_keys), _KEY_BATCH_SIZE): batch = prepared_keys[offset : offset + _KEY_BATCH_SIZE] markers = ", ".join("?" for _ in batch) - await _execute(cursor, f"{select} WHERE {key_column} IN ({markers})", batch) - for row in await _rows(cursor, len(columns)): + _execute(cursor, f"{select} WHERE {key_column} IN ({markers})", batch) + for row in _rows(cursor, len(columns)): record = dict(zip(columns, row, strict=True)) found[self._parsed_key(record[self.definition.key_field_storage_name])] = record return [found[key] for raw in prepared_keys if (key := self._parsed_key(raw)) in found] - async with self._client.connection() as connection, _cursor(connection) as cursor: - return await get_keys(cursor) + return await self._client.run(get_keys) where, parameters = ("1 = 1", []) if filter is None else _FilterCompiler(self.definition).compile(filter) parameters.extend((skip, top)) _check_parameter_count(parameters) statement = f"{select} WHERE {where} ORDER BY {self._order_by(order_by)} OFFSET ? ROWS FETCH NEXT ? ROWS ONLY" - async with self._client.connection() as connection, _cursor(connection) as cursor: - await _execute(cursor, statement, parameters) - return [dict(zip(columns, row, strict=True)) for row in await _rows(cursor, len(columns))] + def get_page(cursor: mssql_python.Cursor) -> list[dict[str, Any]]: + _execute(cursor, statement, parameters) + return [dict(zip(columns, row, strict=True)) for row in _rows(cursor, len(columns))] + + return await self._client.run(get_page) async def _inner_delete(self, keys: Sequence[KeyT], *, operation_options: Mapping[str, Any] | None = None) -> None: _validate_options(operation_options) @@ -581,11 +498,13 @@ async def _inner_delete(self, keys: Sequence[KeyT], *, operation_options: Mappin return key_column = _quote_identifier(self.definition.key_field_storage_name) - async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: + def delete(cursor: mssql_python.Cursor) -> None: for offset in range(0, len(prepared), _KEY_BATCH_SIZE): batch = prepared[offset : offset + _KEY_BATCH_SIZE] markers = ", ".join("?" for _ in batch) - await _execute(cursor, f"DELETE FROM {self._table} WHERE {key_column} IN ({markers})", batch) # nosec B608 + _execute(cursor, f"DELETE FROM {self._table} WHERE {key_column} IN ({markers})", batch) # nosec B608 + + await self._client.run(delete) async def _inner_search( self, @@ -648,10 +567,10 @@ async def _inner_search( parameters.extend((skip, top)) _check_parameter_count(parameters) - async def search(cursor: _Cursor) -> list[dict[str, Any]]: - await _execute(cursor, statement, parameters) + def search(cursor: mssql_python.Cursor) -> list[dict[str, Any]]: + _execute(cursor, statement, parameters) results: list[dict[str, Any]] = [] - for row in await _rows(cursor, len(column_names) + 1): + for row in _rows(cursor, len(column_names) + 1): distance = row[-1] if type(distance) not in (float, int) or not math.isfinite(distance): raise IntegrationInvalidResponseException("SQL Server returned a non-finite vector distance.") @@ -665,8 +584,7 @@ async def search(cursor: _Cursor) -> list[dict[str, Any]]: results.append({"record": dict(zip(column_names, row[:-1], strict=True)), "score": float(score)}) return results - async with self._client.connection() as connection, _cursor(connection) as cursor: - rows = await search(cursor) + rows = await self._client.run(search) return SearchResults( rows, metadata={"distance_function": field.distance_function or "DEFAULT", "approximate": False}, @@ -680,13 +598,13 @@ def _get_score_from_result(self, result: Any) -> float | None: class SqlServerStore(BaseVectorStore): - """Create SQL Server collections sharing resolved settings or a caller-owned connection.""" + """Create SQL Server collections sharing one store-owned worker.""" def __init__( self, *, connection_string: str | SecretString | None = None, - client: SqlServerClient | None = None, + query_timeout: int | None = None, schema: str = "dbo", embedding_generator: EmbeddingClient | None = None, env_file_path: str | None = None, @@ -696,17 +614,20 @@ def __init__( Args: connection_string: Driver connection string, or ``SQL_SERVER_CONNECTION_STRING``. - client: An open caller-owned aioodbc connection or pool. + query_timeout: Optional per-statement timeout in seconds; ``0`` disables the timeout. schema: Existing database schema used by all collections. embedding_generator: Default local embedding generator. - env_file_path: Optional .env file, incompatible with an injected client. + env_file_path: Optional .env file. env_file_encoding: Encoding of the selected .env file. """ - super().__init__(embedding_generator=embedding_generator, managed_client=client is None) + super().__init__(embedding_generator=embedding_generator, managed_client=True) _quote_identifier(schema) self.schema = schema self._client = _create_client( - connection_string, client=client, env_file_path=env_file_path, env_file_encoding=env_file_encoding + connection_string, + query_timeout=query_timeout, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, ) def get_collection( @@ -717,7 +638,7 @@ def get_collection( collection_name: str | None = None, embedding_generator: EmbeddingClient | None = None, ) -> SqlServerCollection[Any, ModelT]: - """Create a collection borrowing this store's client and lifecycle.""" + """Create a collection sharing this store's owned worker and lifecycle.""" return SqlServerCollection( record_type, schema=self.schema, @@ -731,30 +652,31 @@ async def list_collection_names(self, *, operation_options: Mapping[str, Any] | """List base tables in the configured schema.""" _validate_options(operation_options) - async with self._client.connection() as connection, _cursor(connection) as cursor: - await _execute( + def list_names(cursor: mssql_python.Cursor) -> list[str]: + _execute( cursor, "SELECT t.name FROM sys.tables AS t JOIN sys.schemas AS s ON s.schema_id = t.schema_id " "WHERE s.name = ? ORDER BY t.name", [self.schema], ) - names = await _rows(cursor, 1) + names = _rows(cursor, 1) if any(not isinstance(row[0], str) for row in names): raise IntegrationInvalidResponseException("SQL Server returned a non-string table name.") return [cast(str, row[0]) for row in names] + return await self._client.run(list_names) + async def _inner_ensure_collection_deleted( self, collection_name: str, *, operation_options: Mapping[str, Any] | None = None ) -> None: _validate_options(operation_options) table = f"{_quote_identifier(self.schema)}.{_quote_identifier(collection_name)}" - async with self._client.connection(write=True) as connection, _cursor(connection) as cursor: - await _execute(cursor, f"DROP TABLE IF EXISTS {table}") + await self._client.run(lambda cursor: _execute(cursor, f"DROP TABLE IF EXISTS {table}")) async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: """Release store-owned resources.""" await self.close() async def close(self) -> None: - """Close the store wrapper without closing caller-owned connections.""" + """Wait for in-flight operations and shut down the store-owned worker.""" await self._client.close() diff --git a/python/packages/sql-server/pyproject.toml b/python/packages/sql-server/pyproject.toml index 580e9568881..3ee3ab090ba 100644 --- a/python/packages/sql-server/pyproject.toml +++ b/python/packages/sql-server/pyproject.toml @@ -3,7 +3,7 @@ name = "agent-framework-sql-server" description = "SQL Server native vector integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com" }] readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.10,<3.15" version = "1.0.0a260923" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" @@ -23,8 +23,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.19.0,<2", - "aioodbc>=0.5.0,<0.6", - "pyodbc>=5.3.0,<6", + "mssql-python>=1.15.0,<2", ] [tool.ruff] diff --git a/python/packages/sql-server/samples/sql_server_vectors.py b/python/packages/sql-server/samples/sql_server_vectors.py index a4ced8b660e..57d084e2b84 100644 --- a/python/packages/sql-server/samples/sql_server_vectors.py +++ b/python/packages/sql-server/samples/sql_server_vectors.py @@ -14,9 +14,11 @@ """ Search native VECTOR columns in SQL Server 2025 or Azure SQL. -Set SQL_SERVER_CONNECTION_STRING to a connection string for an explicitly -designated development database. The example creates a unique table in the -existing dbo schema and drops only that table. No embedding service is needed. +Set SQL_SERVER_CONNECTION_STRING to a Microsoft mssql-python connection string +for an explicitly designated development database (Server=...;Database=...). +No separately installed ODBC driver is needed. The example creates a unique +table in the existing dbo schema and drops only that table. No embedding service +is needed. Run: uv run --package agent-framework-sql-server python packages/sql-server/samples/sql_server_vectors.py """ @@ -32,8 +34,8 @@ class Note: async def main() -> None: - # 1. Resolve SQL_SERVER_CONNECTION_STRING using Agent Framework settings. - async with SqlServerStore() as store: + # 1. Resolve SQL_SERVER_CONNECTION_STRING and bound query timeout. + async with SqlServerStore(query_timeout=30) as store: collection = store.get_collection(Note, collection_name=f"af_sql_demo_{uuid4().hex}") await collection.ensure_collection_exists() try: diff --git a/python/packages/sql-server/tests/sql_server/test_integration.py b/python/packages/sql-server/tests/sql_server/test_integration.py index 065f482bbdc..80bf3089b92 100644 --- a/python/packages/sql-server/tests/sql_server/test_integration.py +++ b/python/packages/sql-server/tests/sql_server/test_integration.py @@ -7,7 +7,6 @@ from typing import Annotated, Any from uuid import uuid4 -import aioodbc import pytest from agent_framework import ( Filter, @@ -174,7 +173,7 @@ async def test_portable_scalar_filters_match_in_memory(connection_string: str) - await collection.ensure_collection_deleted() -async def test_borrowed_outer_transaction_preserves_prior_write_on_failed_batch(connection_string: str) -> None: +async def test_failed_batch_rolls_back_without_losing_prior_committed_write(connection_string: str) -> None: definition = VectorStoreCollectionDefinition( [ VectorStoreField("key", name="id", type_="str"), @@ -183,21 +182,15 @@ async def test_borrowed_outer_transaction_preserves_prior_write_on_failed_batch( ], collection_name=f"af_sql_tx_{uuid4().hex}", ) - connection = await aioodbc.connect(dsn=connection_string, autocommit=False) - try: - collection = SqlServerCollection(dict, client=connection, definition=definition) + async with SqlServerCollection(dict, connection_string=connection_string, definition=definition) as collection: await collection.ensure_collection_exists() try: constraint = f"af_check_{uuid4().hex}" - cursor = await connection.cursor() - try: - await cursor.execute( - f"ALTER TABLE {collection._table} ADD CONSTRAINT [{constraint}] CHECK ([count] >= 0)" - ) - finally: - await cursor.close() - await connection.commit() + def add_constraint(cursor): + cursor.execute(f"ALTER TABLE {collection._table} ADD CONSTRAINT [{constraint}] CHECK ([count] >= 0)") + + await collection._client.run(add_constraint) await collection.upsert([{"id": "prior", "count": 1, "embedding": [1, 0, 0]}], generate_vectors=False) with pytest.raises(IntegrationException): await collection.upsert( @@ -208,11 +201,5 @@ async def test_borrowed_outer_transaction_preserves_prior_write_on_failed_batch( generate_vectors=False, ) assert [row["id"] for row in await collection.get()] == ["prior"] - await connection.commit() - assert [row["id"] for row in await collection.get()] == ["prior"] finally: - await connection.rollback() await collection.ensure_collection_deleted() - await connection.commit() - finally: - await connection.close() diff --git a/python/packages/sql-server/tests/sql_server/test_vector_store.py b/python/packages/sql-server/tests/sql_server/test_vector_store.py index 3df0fcf0d33..af18c1bf80a 100644 --- a/python/packages/sql-server/tests/sql_server/test_vector_store.py +++ b/python/packages/sql-server/tests/sql_server/test_vector_store.py @@ -2,14 +2,15 @@ from __future__ import annotations -from contextlib import asynccontextmanager +import asyncio +import inspect +import threading from dataclasses import dataclass from functools import partial from typing import Annotated from unittest.mock import AsyncMock, MagicMock, patch -import aioodbc -import pyodbc +import mssql_python import pytest from agent_framework import ( Embedding, @@ -46,7 +47,7 @@ def definition() -> VectorStoreCollectionDefinition: @pytest.fixture def collection(definition): - return SqlServerCollection(dict, definition=definition, connection_string="Driver={ODBC Driver 18 for SQL Server};") + return SqlServerCollection(dict, definition=definition, connection_string="Server=unused;Database=test") def make_record(key="one", *, text="literal %_[abc]!", count=1, tags=None, embedding=None): @@ -60,21 +61,11 @@ def make_record(key="one", *, text="literal %_[abc]!", count=1, tags=None, embed def fake_connection(): - connection = MagicMock(spec=aioodbc.Connection) - connection.autocommit = False - connection.commit = AsyncMock() - connection.rollback = AsyncMock() - connection.close = AsyncMock() - cursor = MagicMock(spec=aioodbc.Cursor) - cursor.execute = AsyncMock() - cursor.fetchone = AsyncMock(return_value=None) - cursor.fetchall = AsyncMock(return_value=[]) - cursor.close = AsyncMock() - - async def open_cursor(): - return cursor - - connection.cursor.side_effect = open_cursor + connection = MagicMock(spec=mssql_python.Connection) + cursor = MagicMock(spec=mssql_python.Cursor) + cursor.fetchone.return_value = None + cursor.fetchall.return_value = [] + connection.cursor.return_value = cursor return connection, cursor @@ -82,11 +73,7 @@ async def open_cursor(): def mock_io(collection): connection, cursor = fake_connection() - @asynccontextmanager - async def acquire(*, write=False): - yield connection - - with patch.object(collection._client, "connection", side_effect=acquire) as request: + with patch.object(collection._client, "run", side_effect=lambda operation: operation(cursor)) as request: yield connection, cursor, request @@ -137,11 +124,7 @@ async def test_create_check_drop_use_scoped_quoted_identifiers_and_bound_catalog ) connection, cursor = fake_connection() - @asynccontextmanager - async def acquire(*, write=False): - yield connection - - with patch.object(collection._client, "connection", side_effect=acquire): + with patch.object(collection._client, "run", side_effect=lambda operation: operation(cursor)): await collection.ensure_collection_exists() create, index = cursor.execute.call_args_list query, params = create.args @@ -164,7 +147,7 @@ async def test_batch_upsert_preserves_duplicate_key_order_and_binds_json(collect cursor.fetchone.side_effect = [None, ("one",), ("one",), ("one",)] keys = await collection.upsert([make_record("one"), make_record("one", text="updated")], generate_vectors=False) assert keys == ["one", "one"] - assert acquire.call_args.kwargs == {"write": True} + acquire.assert_awaited_once() calls = cursor.execute.call_args_list assert len(calls) == 4 assert "WITH (UPDLOCK, HOLDLOCK)" in calls[0].args[0] @@ -188,11 +171,7 @@ async def test_generated_key_inserts_without_key_or_prior_lookup(): connection, cursor = fake_connection() cursor.fetchone.return_value = (123,) - @asynccontextmanager - async def acquire(*, write=False): - yield connection - - with patch.object(collection._client, "connection", side_effect=acquire): + with patch.object(collection._client, "run", side_effect=lambda operation: operation(cursor)): assert await collection.upsert([{"text": "a", "v": [1, 0, 0]}], generate_vectors=False) == [123] assert cursor.execute.call_count == 1 assert "INSERT INTO" in cursor.execute.call_args.args[0] @@ -211,12 +190,8 @@ async def test_generated_identity_refuses_explicit_new_key(): collection = SqlServerCollection(dict, definition=definition, connection_string="x") connection, cursor = fake_connection() - @asynccontextmanager - async def acquire(*, write=False): - yield connection - with ( - patch.object(collection._client, "connection", side_effect=acquire), + patch.object(collection._client, "run", side_effect=lambda operation: operation(cursor)), pytest.raises(NotImplementedError, match="IDENTITY"), ): await collection.upsert([{"id": 42, "v": [1, 0, 0]}], generate_vectors=False) @@ -305,11 +280,7 @@ async def test_multiple_vector_columns_select_requested_dimensions_and_decode_va connection, cursor = fake_connection() cursor.fetchall.return_value = [("first", 0.0)] - @asynccontextmanager - async def acquire(*, write=False): - yield connection - - with patch.object(collection._client, "connection", side_effect=acquire): + with patch.object(collection._client, "run", side_effect=lambda operation: operation(cursor)): results = [item async for item in await collection.search(vector=[0, 1], vector_property_name="secondary")] assert results[0]["record"] == {"id": "first"} assert "CAST(? AS VECTOR(2)), t.[other]]vector]" in cursor.execute.call_args.args[0] @@ -340,11 +311,7 @@ async def test_metric_score_direction_and_threshold(metric, raw_distance, cutoff connection, cursor = fake_connection() cursor.fetchall.return_value = [("first", raw_distance)] - @asynccontextmanager - async def acquire(*, write=False): - yield connection - - with patch.object(collection._client, "connection", side_effect=acquire): + with patch.object(collection._client, "run", side_effect=lambda operation: operation(cursor)): results = [item async for item in await collection.search(vector=[1, 0, 0], score_threshold=expected)] assert results[0]["score"] == pytest.approx(expected) sql, params = cursor.execute.call_args.args @@ -427,7 +394,7 @@ def test_settings_priority_and_secret_masking(constructor, monkeypatch, tmp_path def test_settings_require_explicit_file_or_environment(constructor, monkeypatch, tmp_path): (tmp_path / ".env").write_text("SQL_SERVER_CONNECTION_STRING=implicit\n", encoding="utf-8") monkeypatch.chdir(tmp_path) - with pytest.raises(ValueError, match="exactly one"): + with pytest.raises(ValueError, match="required"): constructor() with pytest.raises(ValueError, match="must not be empty"): constructor(connection_string="") @@ -435,193 +402,200 @@ def test_settings_require_explicit_file_or_environment(constructor, monkeypatch, constructor(env_file_path=str(tmp_path / "missing.env"), connection_string="explicit") -def test_borrowed_client_rejects_settings_and_raw_sync_connections(constructor): - borrowed, _ = fake_connection() - with pytest.raises(ValueError, match="cannot be combined"): - constructor(client=borrowed, connection_string="unused") - with pytest.raises(ValueError, match="cannot be combined"): - constructor(client=borrowed, env_file_path="unused.env") - with pytest.raises(TypeError, match="aioodbc"): - constructor(client=MagicMock(spec=pyodbc.Connection)) +def test_public_api_has_no_borrowed_connection_or_factory(constructor): + connection, _ = fake_connection() + for api in (SqlServerStore, SqlServerCollection): + parameters = inspect.signature(api).parameters + assert "client" not in parameters + assert "client_factory" not in parameters + assert "SqlServerClient" not in module.__dict__ + with pytest.raises(TypeError, match="client"): + constructor(connection_string="Server=unused", client=connection) + + +@pytest.mark.parametrize("timeout", [-1, True, 1.5, "30"]) +def test_query_timeout_requires_non_negative_integer(constructor, timeout): + with pytest.raises(ValueError, match="query_timeout"): + constructor(connection_string="Server=unused", query_timeout=timeout) async def test_store_deletion_quotes_supplied_table_name(definition): store = SqlServerStore(connection_string="x") connection, cursor = fake_connection() - @asynccontextmanager - async def acquire(*, write=False): - yield connection - - with patch.object(store._client, "connection", side_effect=acquire): + with patch.object(store._client, "run", side_effect=lambda operation: operation(cursor)): cursor.fetchall.return_value = [("table]'; --",)] await store.ensure_collection_deleted("table]'; --") assert cursor.execute.call_args.args[0] == "DROP TABLE IF EXISTS [dbo].[table]]'; --]" assert "CASCADE" not in cursor.execute.call_args.args[0] -async def test_borrowed_connection_savepoint_rollback_preserves_caller_transaction(): +async def test_connection_and_all_operations_stay_on_one_worker(): connection, cursor = fake_connection() - cursor.fetchone.side_effect = [(1,), (1,)] - client = _Client(None, connection) - with pytest.raises(ValueError, match="failed"): - async with client.connection(write=True) as borrowed: - assert borrowed is connection - async with module._cursor(borrowed) as operation_cursor: - await operation_cursor.execute("SELECT 1") - raise ValueError("failed") - commands = [call.args[0] for call in cursor.execute.call_args_list] - assert commands == [ - "SELECT @@TRANCOUNT", - "SAVE TRANSACTION af_vector_operation", - "SELECT 1", - "SELECT XACT_STATE()", - "ROLLBACK TRANSACTION af_vector_operation", - ] - connection.commit.assert_not_awaited() - connection.rollback.assert_not_awaited() - assert cursor.close.await_count == 3 - await client.close() - await client.close() - connection.close.assert_not_awaited() - with pytest.raises(RuntimeError, match="closed"): - async with client.connection(): - pytest.fail("Closed client must not borrow the connection") + client = _Client(SecretString("Server=unused;Database=test"), query_timeout=30) + caller_thread = threading.get_ident() + worker_threads: list[int] = [] + def record_thread(*args, **kwargs): + worker_threads.append(threading.get_ident()) -async def test_borrowed_connection_commits_only_its_own_transaction(): - connection, cursor = fake_connection() - cursor.fetchone.return_value = (0,) - client = _Client(None, connection) - async with client.connection(write=True) as borrowed, module._cursor(borrowed) as operation_cursor: - await operation_cursor.execute("SELECT 1") - assert [call.args[0] for call in cursor.execute.call_args_list] == [ - "SELECT @@TRANCOUNT", - "BEGIN TRANSACTION", - "SELECT 1", - "COMMIT TRANSACTION", - ] - connection.commit.assert_not_awaited() - assert cursor.close.await_count == 3 - connection.close.assert_not_awaited() - await client.close() + def open_connection(*args, **kwargs): + record_thread() + return connection + def open_cursor(): + record_thread() + return cursor -async def test_borrowed_outer_transaction_success_closes_cursor_without_committing(): - connection, cursor = fake_connection() - cursor.fetchone.return_value = (1,) - client = _Client(None, connection) - async with client.connection(write=True) as borrowed, module._cursor(borrowed) as operation_cursor: - await operation_cursor.execute("SELECT 1") - assert [call.args[0] for call in cursor.execute.call_args_list] == [ - "SELECT @@TRANCOUNT", - "SAVE TRANSACTION af_vector_operation", - "SELECT 1", - ] - assert cursor.close.await_count == 2 - connection.commit.assert_not_awaited() - connection.rollback.assert_not_awaited() - await client.close() - + def fetchone(): + record_thread() + return ("found",) -async def test_doomed_caller_transaction_is_not_silently_rolled_back(): - connection, cursor = fake_connection() - cursor.fetchone.side_effect = [(1,), (-1,)] - client = _Client(None, connection) - with pytest.raises(IntegrationException, match="caller must roll back"): - async with client.connection(write=True): - raise ValueError("failed") - assert "ROLLBACK TRANSACTION af_vector_operation" not in [call.args[0] for call in cursor.execute.call_args_list] - connection.rollback.assert_not_awaited() - await client.close() - - -async def test_owned_pool_uses_one_worker_and_releases_it_with_store(): - pool = MagicMock(spec=aioodbc.Pool) - pool.close = MagicMock() - pool.wait_closed = AsyncMock() - connection, _ = fake_connection() - pool.acquire.return_value.__aenter__.return_value = connection - client = _Client(SecretString("Driver={ODBC Driver 18 for SQL Server};"), None) - with patch.object(module.aioodbc, "create_pool", new_callable=AsyncMock, return_value=pool) as create_pool: - async with client.connection(write=True) as acquired: - assert acquired is connection - executor = create_pool.call_args.kwargs["executor"] - assert executor._max_workers == 1 - assert create_pool.call_args.kwargs["autocommit"] is False - connection.commit.assert_awaited_once() - await client.close() - await client.close() - pool.close.assert_called_once() - pool.wait_closed.assert_awaited_once() + connection.cursor.side_effect = open_cursor + connection.commit.side_effect = record_thread + connection.close.side_effect = record_thread + cursor.execute.side_effect = record_thread + cursor.fetchone.side_effect = fetchone + cursor.close.side_effect = record_thread + with patch.object(module.mssql_python, "connect", side_effect=open_connection) as connect: + assert client._executor is None + try: + + def operation(raw): + module._execute(raw, "SELECT ?", ["O'Brien"]) + return raw.fetchone() + + assert await client.run(operation) == ("found",) + assert await client.run(operation) == ("found",) + executor = client._executor + assert executor is not None and executor._max_workers == 1 + assert cursor.execute.call_args.args == ("SELECT ?", ("O'Brien",)) + assert connect.call_count == 2 + assert connect.call_args.args == ("Server=unused;Database=test",) + assert connect.call_args.kwargs == {"autocommit": False} + assert connection.timeout == 30 + assert len(set(worker_threads)) == 1 and worker_threads[0] != caller_thread + assert connection.commit.call_count == connection.close.call_count == cursor.close.call_count == 2 + connection.rollback.assert_not_called() + finally: + await client.close() + await client.close() assert executor._shutdown - connection.close.assert_not_awaited() - - -async def test_owned_commit_failure_rolls_back_before_releasing_pool_connection(): - pool = MagicMock(spec=aioodbc.Pool) - pool.close = MagicMock() - pool.wait_closed = AsyncMock() - connection, _ = fake_connection() - connection.commit.side_effect = pyodbc.OperationalError("commit failed") - pool.acquire.return_value.__aenter__.return_value = connection - client = _Client(SecretString("Driver={ODBC Driver 18 for SQL Server};"), None) - with patch.object(module.aioodbc, "create_pool", new_callable=AsyncMock, return_value=pool): - with pytest.raises(IntegrationException): - async with client.connection(write=True): - pass - connection.rollback.assert_awaited_once() - await client.close() + with pytest.raises(RuntimeError, match="closed"): + await client.run(operation) -async def test_borrowed_pool_remains_open_after_store_closes(definition): - pool = MagicMock(spec=aioodbc.Pool) - pool.close = MagicMock() - pool.wait_closed = AsyncMock() +async def test_store_children_share_ownership_without_closing_worker(definition): connection, cursor = fake_connection() - pool.acquire.return_value.__aenter__.return_value = connection cursor.fetchall.return_value = [("existing",)] - async with SqlServerStore(client=pool) as store: - collection = store.get_collection(dict, definition=definition) - assert not collection.managed_client - assert await store.list_collection_names() == ["existing"] - await collection.close() - pool.close.assert_not_called() - pool.wait_closed.assert_not_awaited() - connection.commit.assert_awaited_once() - connection.close.assert_not_awaited() - - -async def test_borrowed_autocommit_pool_rejected_before_database_operations(): - pool = MagicMock(spec=aioodbc.Pool) + with patch.object(module.mssql_python, "connect", return_value=connection): + async with SqlServerStore(connection_string="Server=unused;Database=test", query_timeout=5) as store: + collection = store.get_collection(dict, definition=definition) + assert collection._client is store._client + assert collection._client.query_timeout == 5 + assert not collection.managed_client + assert store.managed_client + assert await store.list_collection_names() == ["existing"] + executor = store._client._executor + assert executor is not None + await collection.close() + assert not executor._shutdown + assert executor._shutdown + connection.commit.assert_called_once() + connection.close.assert_called_once() + + +async def test_cancelled_operation_and_close_wait_for_worker_cleanup(): connection, cursor = fake_connection() - connection.autocommit = True - pool.acquire.return_value.__aenter__.return_value = connection - client = _Client(None, pool) - with pytest.raises(ValueError, match="autocommit=False"): - async with client.connection(write=True): - pytest.fail("Autocommit pool must be rejected before the operation starts") - cursor.execute.assert_not_awaited() - connection.commit.assert_not_awaited() - await client.close() - pool.close.assert_not_called() + started, release = threading.Event(), threading.Event() + + def slow_execute(*args): + started.set() + assert release.wait(timeout=10) + + cursor.execute.side_effect = slow_execute + client = _Client(SecretString("Server=unused;Database=test")) + with patch.object(module.mssql_python, "connect", return_value=connection): + try: + operation = asyncio.create_task(client.run(lambda raw: module._execute(raw, "SELECT 1"))) + assert await asyncio.to_thread(started.wait, 5) + operation.cancel() + closing = asyncio.create_task(client.close()) + await asyncio.sleep(0) + assert not operation.done() and not closing.done() + finally: + release.set() + with pytest.raises(asyncio.CancelledError): + await operation + await closing + connection.commit.assert_called_once() + cursor.close.assert_called_once() + connection.close.assert_called_once() + assert client._executor is not None and client._executor._shutdown async def test_driver_error_is_chained_and_owned_connection_rolled_back(): - pool = MagicMock(spec=aioodbc.Pool) - pool.close = MagicMock() - pool.wait_closed = AsyncMock() connection, cursor = fake_connection() - cursor.execute.side_effect = pyodbc.OperationalError("unavailable") - pool.acquire.return_value.__aenter__.return_value = connection - client = _Client(SecretString("Driver={ODBC Driver 18 for SQL Server};"), None) - with patch.object(module.aioodbc, "create_pool", new_callable=AsyncMock, return_value=pool): - with pytest.raises(IntegrationException) as error: - async with client.connection(write=True) as acquired, module._cursor(acquired) as cursor: - await cursor.execute("SELECT 1") + cursor.execute.side_effect = [ + None, + mssql_python.OperationalError("constraint violation", "bad record"), + ] + client = _Client(SecretString("Server=unused;Database=test")) + + def batch(raw): + module._execute(raw, "INSERT INTO [notes] ([id]) VALUES (?)", ["first"]) + module._execute(raw, "INSERT INTO [notes] ([id]) VALUES (?)", ["invalid"]) + + try: + with ( + patch.object(module.mssql_python, "connect", return_value=connection), + pytest.raises(IntegrationException) as error, + ): + await client.run(batch) + finally: + await client.close() + assert isinstance(error.value.__cause__, mssql_python.OperationalError) + assert cursor.execute.call_count == 2 + connection.commit.assert_not_called() + connection.rollback.assert_called_once() + cursor.close.assert_called_once() + connection.close.assert_called_once() + + +async def test_commit_failure_rolls_back_and_closes_connection(): + connection, cursor = fake_connection() + connection.commit.side_effect = mssql_python.OperationalError("commit failed", "database unavailable") + client = _Client(SecretString("Server=unused;Database=test")) + try: + with ( + patch.object(module.mssql_python, "connect", return_value=connection), + pytest.raises(IntegrationException) as error, + ): + await client.run(lambda raw: module._execute(raw, "SELECT 1")) + finally: await client.close() - assert isinstance(error.value.__cause__, pyodbc.OperationalError) - connection.rollback.assert_awaited_once() + assert isinstance(error.value.__cause__, mssql_python.OperationalError) + connection.rollback.assert_called_once() + cursor.close.assert_called_once() + connection.close.assert_called_once() + + +async def test_connection_failure_is_chained_and_worker_shuts_down(): + client = _Client(SecretString("Server=unused;Database=test")) + try: + with ( + patch.object( + module.mssql_python, + "connect", + side_effect=mssql_python.OperationalError("connect failed", "server unavailable"), + ), + pytest.raises(IntegrationException) as error, + ): + await client.run(lambda raw: module._execute(raw, "SELECT 1")) + finally: + await client.close() + assert isinstance(error.value.__cause__, mssql_python.OperationalError) + assert client._executor is not None and client._executor._shutdown @vectorstoremodel(collection_name="typed_sql_notes") @@ -637,9 +611,5 @@ async def test_decorated_records_round_trip_without_vectors_by_default(): connection, cursor = fake_connection() cursor.fetchall.return_value = [("one", "title")] - @asynccontextmanager - async def acquire(*, write=False): - yield connection - - with patch.object(collection._client, "connection", side_effect=acquire): + with patch.object(collection._client, "run", side_effect=lambda operation: operation(cursor)): assert await collection.get(["one"]) == [TypedNote("one", "title", None)] diff --git a/python/uv.lock b/python/uv.lock index a634098791a..6e18ba8e628 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -1,19 +1,16 @@ version = 1 revision = 3 -requires-python = ">=3.11" +requires-python = ">=3.11, <3.15" resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'darwin'", - "python_full_version == '3.14.*' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version < '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.15' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform == 'linux'", "python_full_version == '3.13.*' and sys_platform == 'linux'", "python_full_version == '3.12.*' and sys_platform == 'linux'", "python_full_version < '3.12' and sys_platform == 'linux'", - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version < '3.12' and sys_platform == 'win32'", @@ -474,7 +471,7 @@ all = [ { name = "agent-framework-foundry-local", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-gemini", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-github-copilot", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "agent-framework-hyperlight", marker = "(python_full_version < '3.15' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.15' and platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "agent-framework-hyperlight", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, { name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-mistral", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "agent-framework-monty", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -816,7 +813,7 @@ source = { editable = "packages/hyperlight" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "hyperlight-sandbox", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "hyperlight-sandbox-backend-wasm", marker = "(python_full_version < '3.15' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.15' and platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "hyperlight-sandbox-backend-wasm", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, { name = "hyperlight-sandbox-python-guest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -936,8 +933,7 @@ source = { editable = "packages/postgres" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pgvector", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "psycopg", extra = ["binary"], marker = "(python_full_version < '3.15' and sys_platform == 'darwin') or (python_full_version < '3.15' and sys_platform == 'linux') or (python_full_version < '3.15' and sys_platform == 'win32')" }, - { name = "psycopg", extra = ["pool"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "psycopg", extra = ["binary", "pool"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.metadata] @@ -1006,15 +1002,13 @@ version = "1.0.0a260923" source = { editable = "packages/sql-server" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "aioodbc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "pyodbc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "mssql-python", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "aioodbc", specifier = ">=0.5.0,<0.6" }, - { name = "pyodbc", specifier = ">=5.3.0,<6" }, + { name = "mssql-python", specifier = ">=1.15.0,<2" }, ] [[package]] @@ -1182,27 +1176,15 @@ name = "aiologic" version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "sniffio", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, - { name = "wrapt", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "wrapt", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/7a/d51f2fde1e8ae8a83431f8e97b7a71e9358cdb1d4d2ce6be387fa44d68de/aiologic-0.17.1.tar.gz", hash = "sha256:2e1b93b9e88ced318c2a63ad7b382688f40cbfe40e3d42258d49dc9c5aea179d", size = 252354, upload-time = "2026-06-27T20:41:33.25Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/d3/2d310b1b839014034dba0cba685e492df8a5c7ad32c19cab7e979eed6554/aiologic-0.17.1-py3-none-any.whl", hash = "sha256:c66b319830fedb7ca3d2b2125fa6f5b653f89418c2a27ea76f259ec5f00943c0", size = 161331, upload-time = "2026-06-27T20:41:31.877Z" }, ] -[[package]] -name = "aioodbc" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyodbc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/87/3a7580938f217212a574ba0d1af78203fc278fc439815f3fc515a7fdc12b/aioodbc-0.5.0.tar.gz", hash = "sha256:cbccd89ce595c033a49c9e6b4b55bbace7613a104b8a46e3d4c58c4bc4f25075", size = 41298, upload-time = "2023-10-28T21:37:29.966Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/80/4d1565bc16b53cd603c73dc4bc770e2e6418d957417e05031314760dc28c/aioodbc-0.5.0-py3-none-any.whl", hash = "sha256:bcaf16f007855fa4bf0ce6754b1f72c6c5a3d544188849577ddd55c5dc42985e", size = 19449, upload-time = "2023-10-28T21:37:28.51Z" }, -] - [[package]] name = "aiosignal" version = "1.4.0" @@ -1259,7 +1241,7 @@ version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "typing-extensions", marker = "(python_full_version < '3.15' and sys_platform == 'darwin') or (python_full_version < '3.15' and sys_platform == 'linux') or (python_full_version < '3.15' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ @@ -1318,7 +1300,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/e4/d974e55c2e247ef26ed1df01c74940583db9a5b3a8bcaad5732c6e2047fb/ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4", size = 1119739, upload-time = "2026-09-13T18:48:19.005Z" }, { url = "https://files.pythonhosted.org/packages/0d/00/d229443488e095054d5e0c0cc20689a2633b899d735849ff1b2c8e4f0cbf/ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f", size = 1158602, upload-time = "2026-09-13T18:48:20.524Z" }, { url = "https://files.pythonhosted.org/packages/11/75/389fc1a6cfa0c4b2ce522f47d8401329d8bb11732e516d46465960fef1d9/ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d", size = 1128842, upload-time = "2026-09-13T18:48:22.052Z" }, - { url = "https://files.pythonhosted.org/packages/45/7d/c4f36898f19c728d091cdfdf960c9488e8d82bbe2e49ba13c05f43907a5d/ast_serialize-0.11.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1", size = 897204, upload-time = "2026-09-13T18:48:23.695Z" }, { url = "https://files.pythonhosted.org/packages/b1/54/f67120006fc73a55b6d057d4662d061fbb4eceafce3047c76ca8b382eb11/ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4", size = 1240734, upload-time = "2026-09-13T18:48:25.287Z" }, { url = "https://files.pythonhosted.org/packages/9a/7e/8f2ab68bddbe58a66fbbaad87beeae3e7d7edddb17263d1fc423936cf34d/ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4", size = 1228053, upload-time = "2026-09-13T18:48:26.788Z" }, { url = "https://files.pythonhosted.org/packages/d2/1b/8a69ab68f4c1603819f0481d756abdd8caf27cec7f1d77caa71007ebe997/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b", size = 1292542, upload-time = "2026-09-13T18:48:28.295Z" }, @@ -1844,38 +1825,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, - { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, - { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, - { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, - { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, - { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, - { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, - { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, - { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, - { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, - { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, - { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, - { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, - { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, - { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, - { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, - { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, - { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, - { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, - { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, - { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, @@ -1928,7 +1877,7 @@ name = "clr-loader" version = "0.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } wheels = [ @@ -2025,36 +1974,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/72/6eff5456d7ba7f1c4678af531c33f9d957cae3201bd229b056fd13a204a3/coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2", size = 226026, upload-time = "2026-09-13T19:11:02.846Z" }, { url = "https://files.pythonhosted.org/packages/8e/c8/6e5ae3d8d4d0f2c0078985bf4db55fafd90e8107b1bf91ee3547a13f5694/coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d", size = 226862, upload-time = "2026-09-13T19:11:04.921Z" }, { url = "https://files.pythonhosted.org/packages/be/c7/68f9f0734afc904a92b974b489545b6a15700f3b1c4bd36eae764561e661/coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6", size = 226171, upload-time = "2026-09-13T19:11:06.969Z" }, - { url = "https://files.pythonhosted.org/packages/ae/16/e11addf5322d98e86307da9e00c94644b97cb72c534c74cda89705bebeef/coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743", size = 223544, upload-time = "2026-09-13T19:11:09.108Z" }, - { url = "https://files.pythonhosted.org/packages/92/d9/7ccd6484f615961d94b09c75904f87d86375109596069ae3a495a205dbbe/coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e", size = 223882, upload-time = "2026-09-13T19:11:11.132Z" }, - { url = "https://files.pythonhosted.org/packages/59/37/2fd78152a557df4a7e4ad78d6851ede90c211838d526e4916c6016f45144/coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae", size = 254987, upload-time = "2026-09-13T19:11:13.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4a/58e2f9b8b13cc422aa7f474a498945ec7f446ef473b5b6bae18d2981f2a9/coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370", size = 257902, upload-time = "2026-09-13T19:11:16.025Z" }, - { url = "https://files.pythonhosted.org/packages/3c/74/63f8f6a67c03b86455c726be273b2aa5b57fd1122f22350d141bbb142e91/coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae", size = 259522, upload-time = "2026-09-13T19:11:18.206Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3b/1f2261da7483e345fe55de11e41ffc16a543511e37c0912134c0dccc4f6c/coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de", size = 261723, upload-time = "2026-09-13T19:11:20.628Z" }, - { url = "https://files.pythonhosted.org/packages/a7/8a/7eb1a361e044b7293f49c921a974b532627ed37f3bef5e7e9f58bcd0d5a2/coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1", size = 255462, upload-time = "2026-09-13T19:11:22.733Z" }, - { url = "https://files.pythonhosted.org/packages/67/66/6d94f7ea87e99c519def5cfa5d4a1ab20247d6b403eb6e6b5b63d6aa5985/coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c", size = 257617, upload-time = "2026-09-13T19:11:24.933Z" }, - { url = "https://files.pythonhosted.org/packages/3d/52/c3de0a3868589a1463a4f1cb0222eca0e5fa9f91da865ccf20d2e19ec19c/coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6", size = 255495, upload-time = "2026-09-13T19:11:27.158Z" }, - { url = "https://files.pythonhosted.org/packages/9e/a3/ea10e75f20fc1e7be10b166a494826a965d19b1dc9c8a81c511a01153311/coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f", size = 259728, upload-time = "2026-09-13T19:11:29.33Z" }, - { url = "https://files.pythonhosted.org/packages/0e/96/9d8020d97de46f0390739beda196322586cdd87e1fc357223e6a28bc4135/coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df", size = 254904, upload-time = "2026-09-13T19:11:31.86Z" }, - { url = "https://files.pythonhosted.org/packages/5b/21/2ff8867d4560f4f639a82d8fcf13c27eea88a96193daba096027c48bb929/coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040", size = 256828, upload-time = "2026-09-13T19:11:34.078Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4b/feacad51cb5163e3baa13281f8a3098f15d7934cc1f28cfe656557eb4e46/coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827", size = 225722, upload-time = "2026-09-13T19:11:36.314Z" }, - { url = "https://files.pythonhosted.org/packages/39/34/66bbc6ffd3c51fa67604c566920e772c5e3baa57f88dcf7b109c7f18b2cb/coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d", size = 226195, upload-time = "2026-09-13T19:11:38.493Z" }, - { url = "https://files.pythonhosted.org/packages/f7/96/4bad920d4caed127c37c136a64b0730fa495517d2eb58809e0d81e6e7dff/coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855", size = 225625, upload-time = "2026-09-13T19:11:40.664Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2b/a88c7906eeb7da4394e932060d7008e153b3a2cd8d11782d82faa03bb7a3/coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6", size = 224271, upload-time = "2026-09-13T19:11:42.876Z" }, - { url = "https://files.pythonhosted.org/packages/1f/1f/98db59c595680d16f553890deac6d72610e04f192c1964ec9b69cbe790ab/coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d", size = 224564, upload-time = "2026-09-13T19:11:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/699e236d4cb97ec282b0655afc85acf12349e71a02fff0f1a7d0fb643079/coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6", size = 265423, upload-time = "2026-09-13T19:11:47.666Z" }, - { url = "https://files.pythonhosted.org/packages/96/1b/6eaa21912863b3e3bf23cfe605fb62929bb1a41dc33d2bfa8e92000232bd/coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1", size = 268503, upload-time = "2026-09-13T19:11:50.12Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ac/4eb46bffa98c28a80559a58c12f17e19308d52b31174af1bd49decf76606/coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1", size = 271059, upload-time = "2026-09-13T19:11:52.363Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d4/17a51ef1f6a084c9abbead522cfbee0fe9d29a593128781e724b7a0795b3/coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b", size = 272039, upload-time = "2026-09-13T19:11:54.935Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ff/9fdeba8b8aa0848030f45f869ddedc306795a8c6e53e901b8553afbf5415/coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de", size = 265869, upload-time = "2026-09-13T19:11:57.423Z" }, - { url = "https://files.pythonhosted.org/packages/95/eb/ab3eb506b2e4dd278440fbbbc7ed18424a86d8a981db0b90fdba66f5d34d/coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef", size = 268883, upload-time = "2026-09-13T19:11:59.71Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0c/f4edefdce33f01954a74237df5ee83ccef5dcd1165e04abf0adf47543fca/coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876", size = 265359, upload-time = "2026-09-13T19:12:01.953Z" }, - { url = "https://files.pythonhosted.org/packages/df/86/ce5ed885fc7ce2adc9a5971134ace257d0f84f19c3d117da89c179b9662e/coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8", size = 270056, upload-time = "2026-09-13T19:12:04.46Z" }, - { url = "https://files.pythonhosted.org/packages/48/a1/5dbd5f95070cece25c6df9e6ea4ded2a87dc695ce82a49bcb2116452b342/coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220", size = 265498, upload-time = "2026-09-13T19:12:06.811Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ff/254b26f3300f87c7c53ac97da41ae75f0e2ac066aa964bc073c0f3750b38/coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0", size = 267459, upload-time = "2026-09-13T19:12:09.479Z" }, - { url = "https://files.pythonhosted.org/packages/61/20/bf9958f8ddbbf6a2d39da3d4009f33e0fcf19c2d2fd99715c6494cdc8e8c/coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5", size = 226021, upload-time = "2026-09-13T19:12:11.933Z" }, - { url = "https://files.pythonhosted.org/packages/35/8f/12d46948704d9e437c8dca2718e5d54ecc2f85396b64b500c29d8dbe10db/coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db", size = 226853, upload-time = "2026-09-13T19:12:14.254Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7f/1ca5fd0601054fce5a3539375b997e67683646e2bf62f60c41e70b529fcc/coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0", size = 226163, upload-time = "2026-09-13T19:12:16.589Z" }, { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, ] @@ -2124,7 +2043,7 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "aiologic", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } @@ -2530,22 +2449,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/04/0d018e0d05bcdde19a0fcb907834155f1fc853a9bedd3f3f5e6acadcae19/greenlet-3.5.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca80a49b53ed1d22f7282da7255f7bb2fd1935fd0f623d8613fda38745f18961", size = 1641479, upload-time = "2026-09-14T15:10:11.216Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/f02ef9073919158f6403fe3701d4ed4403d646720e7201dfc6e9d264bac3/greenlet-3.5.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:916f92f2a8db10508f739d0b5e00b83defe5d1115a997c54532a6d7cf8c95404", size = 1698758, upload-time = "2026-09-14T14:35:54.336Z" }, { url = "https://files.pythonhosted.org/packages/08/a5/1f48fe647473a2dcccfd1839b2ff2c78eb57009be776b4da071e901c9bff/greenlet-3.5.6-cp314-cp314t-win_amd64.whl", hash = "sha256:886bcf1870af74c32bc310fd00a6b803445e17e51b7d5a107c7b35c0f362cc16", size = 331574, upload-time = "2026-09-14T14:27:18.451Z" }, - { url = "https://files.pythonhosted.org/packages/cd/72/3882855a75838faeb54a58aeef4fd77d20b2a86d4bad570c70d41b565dcf/greenlet-3.5.6-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:3ac3494c381dab876cad7d0b22f3a722f3e0c8deb3a65b9e7f35ad7f58b8fcb3", size = 295926, upload-time = "2026-09-14T14:27:21.16Z" }, - { url = "https://files.pythonhosted.org/packages/10/1f/be4d957d8a9b90bcbe8db206548a42134d96222d43e5ed3fc4708fb6e24b/greenlet-3.5.6-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:602024dae6d77e161f4b89491b62ca1d4f19949d79d47b2db057e476d21179d6", size = 666910, upload-time = "2026-09-14T15:12:07.901Z" }, - { url = "https://files.pythonhosted.org/packages/a1/af/60d62571a7d6de961e4ce7625d6c2faf359345659fc782d2cdf517c34577/greenlet-3.5.6-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8e63209c3e1e828ee6a457529b4a6d8b05d050fe0ae03a7ae49e967c5d312e0", size = 677415, upload-time = "2026-09-14T15:20:48.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/16/ac9e547b611539aaed1870eb1d6ddc57abdd5924b3a99bb9b5f0b44176b8/greenlet-3.5.6-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccadce0130fd813ec86ebfe969a6c58b42acc1d0fe55a47525375b740e07b605", size = 675927, upload-time = "2026-09-14T14:36:05.34Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b1/b7ba08d6431121741f1d30be0d5d292e76873325179a63586cd9217b62f6/greenlet-3.5.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9297fb9c39b9a2c039dbcd306c410bd6906b95244dec3bba4318d36c718c164c", size = 1637236, upload-time = "2026-09-14T15:10:12.442Z" }, - { url = "https://files.pythonhosted.org/packages/af/c5/3b1cbc68f0c082022fc8717f7fe4b8b13b8d583c52352be37f4e9f55bcd2/greenlet-3.5.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b374e79ffa7511afc11773aef40a4ccea6191fba1c856ea2f9c56738dca69d7a", size = 1698526, upload-time = "2026-09-14T14:35:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/12941ed2711400451c89d544e10f831800a2770f19dd55eac8f0f7f2003b/greenlet-3.5.6-cp315-cp315-win_amd64.whl", hash = "sha256:7969bffa322c097bd46ae595ada6a931cefda613f18ba64587e9cff4cb320756", size = 327739, upload-time = "2026-09-14T14:23:55.768Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3b/576b9ed5ac929252e340cf60b4bcb6a8515350dc20797064b1922dc4ea75/greenlet-3.5.6-cp315-cp315-win_arm64.whl", hash = "sha256:8dba0129b93e7091dfefaf4cf7000172741bff7f47bf6326fcf17f32fbb54d6b", size = 311715, upload-time = "2026-09-14T14:28:25.154Z" }, - { url = "https://files.pythonhosted.org/packages/16/c2/86cfc5555a98e12b86966ddbd24fd39af32f71f2f785c6595b7feb2db156/greenlet-3.5.6-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:de3de000d459402cda015068fd135aa50c0bf6f2477a80d4da1e646f123b4e78", size = 306244, upload-time = "2026-09-14T14:27:57.565Z" }, - { url = "https://files.pythonhosted.org/packages/14/6d/83ffc9d05a75a80ab3a7595dbb1d9604e5d4fc2996d73a8ae2dbd1284900/greenlet-3.5.6-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45663c01a4de48b9a64a2ee1509d92d1dfd3afb02b2ccfc9333029d11aef996a", size = 676578, upload-time = "2026-09-14T15:12:09.468Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d6/c2cf684810e5caded075970aaadea654ecb58b8382b9aecf1d231b936894/greenlet-3.5.6-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3deccbb57a481e3a408fe61cdfd5c13e0678fc0a30fdd09597917ca87b4be877", size = 684203, upload-time = "2026-09-14T15:20:50.261Z" }, - { url = "https://files.pythonhosted.org/packages/62/19/00e1bee5d2af890dc8f400b54d0b0f9b489965f92bc12b407ff72cc6f469/greenlet-3.5.6-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:311018b46472fb26ee85870847fb89eb64cc8aaddb617400789d87076f7cfeec", size = 681253, upload-time = "2026-09-14T14:36:06.742Z" }, - { url = "https://files.pythonhosted.org/packages/89/58/c9275fd0ca195d1d3402931bcce8cfcc74726ff76efb1883d229e6e1a3d7/greenlet-3.5.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7f924a5a9d5890649566f2f6682e0d8ad8ca23028bacffbbac36dbd7fd680176", size = 1645988, upload-time = "2026-09-14T15:10:13.758Z" }, - { url = "https://files.pythonhosted.org/packages/e0/36/b35747582fa4f1a5453f8f3002405dbac788e450cec7674dc2d204b6ccb5/greenlet-3.5.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:de9923832f2d8c1a5ecd8d7260465a6ca5a86888a0d129e3bd5cf0406d2fc5bf", size = 1702383, upload-time = "2026-09-14T14:35:58.143Z" }, - { url = "https://files.pythonhosted.org/packages/ed/69/6ec22ac9351e474d2a134d0ff9400dc80362d1c20f0721088ffffdfc205b/greenlet-3.5.6-cp315-cp315t-win_amd64.whl", hash = "sha256:2ab5f42ac6c238eb71770715e6e909ad9a1a92b6c681ccb64cd5a0f07edb953f", size = 331845, upload-time = "2026-09-14T14:27:41.723Z" }, - { url = "https://files.pythonhosted.org/packages/30/cf/697c051fd534e223461fb8b523890e21a24eeca229cd50624cff6f02fabd/greenlet-3.5.6-cp315-cp315t-win_arm64.whl", hash = "sha256:f9fe868463ec7e1363733af77e38a5fda3e9b63940337048c945d69e0c80ff24", size = 315070, upload-time = "2026-09-14T14:22:21.476Z" }, ] [[package]] @@ -2632,16 +2535,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, - { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, - { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, - { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, - { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, - { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, ] [[package]] @@ -2961,33 +2854,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/03/bcbaf8b6b9ea23c2c074411f8ecfbb02d820abac5d0cb8f4e280209174a2/jiter-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:8f770b0c77e5fac482e1ba03ca1a7e18286bfb213d749932a00a7e4cd5de5e06", size = 184434, upload-time = "2026-09-12T15:13:13.037Z" }, { url = "https://files.pythonhosted.org/packages/7a/b5/5d6ce2c93ef6fe1241b37a9005547f9b6d58db1f07f39fe95807d4b98f51/jiter-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c4289293e5278d9314b00f15c37f2120fa51d3d68565292e715524c750e775a9", size = 227392, upload-time = "2026-09-12T15:13:14.933Z" }, { url = "https://files.pythonhosted.org/packages/f5/4b/1e52baf90187606e33a7b8cfa8f96f5829acd7f01870077eb01059ab76d0/jiter-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4dfbfe5a6e1e80a7082af559f66386405025ec278833e0c649f69cbc6e1004cc", size = 182776, upload-time = "2026-09-12T15:13:16.239Z" }, - { url = "https://files.pythonhosted.org/packages/05/fc/efe3ac75564ab10f53517958f5ccdc231fc7334af66c76776cb554a88967/jiter-0.17.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:84963d3f395ef5e9a32ce47155e08a7962fa292c159a10cb98b931cef1416925", size = 292143, upload-time = "2026-09-12T15:13:17.502Z" }, - { url = "https://files.pythonhosted.org/packages/d1/4c/46982118d91f9ffe9714319d21ec4f98d9b7e0cfd9062826c524a54de24e/jiter-0.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ffa0380ad091de7d3fc33e17a97ff479851ee18a0a2a3ee56ff3215cdc886656", size = 321341, upload-time = "2026-09-12T15:13:19.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/12/9b1ac6ecc6307049913db54839ddba1c11c1ef72c5a8bbb5514bc3b50d1b/jiter-0.17.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:755079792868ce5d4938e83b91a0939b34fb858a1ca65a104f2d771bea57faa1", size = 344383, upload-time = "2026-09-12T15:13:20.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/b6/527cc72af836d824e9d4d666e64f0a1ca7eafd662a8da9657b78592172ba/jiter-0.17.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3bf4dc2b84a464117fb097d15a25c58d100d2692888e3b0d92df5b48ed16b7c0", size = 326841, upload-time = "2026-09-12T15:13:21.83Z" }, - { url = "https://files.pythonhosted.org/packages/d1/41/567f98617e88005b249503b933803f633ec6ba2d427cf4cc35e5c832125c/jiter-0.17.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02a360707033d8cef53f7f3480817a1489177a259ec6ec01e98c37e0b922ddca", size = 341354, upload-time = "2026-09-12T15:13:23.323Z" }, - { url = "https://files.pythonhosted.org/packages/40/da/b29cda895b785f7d426e224638a885b6145a08ce853b381f34afe3e88c5d/jiter-0.17.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:300ce01ab0215e3dea4d00090143c909aedc65c0f809b3c07983e1d038f291b9", size = 351985, upload-time = "2026-09-12T15:13:26.526Z" }, - { url = "https://files.pythonhosted.org/packages/f7/5c/8a73829e7389e72ea298a450f2b3cb58e71a3e464b45f6d8753740f1c4f5/jiter-0.17.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746243a080b4ca790b8499af3d7cf9825d5f5987933950cd818e767ee353d826", size = 346052, upload-time = "2026-09-12T15:13:27.887Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2f/98d6001026932c095ba440925570123043bed29f5ff56158dfe729a9e81b/jiter-0.17.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:b550585523339b71cb852b811aae49d08d7601ad8ffe9f5dc1562f4c3d22fd87", size = 329159, upload-time = "2026-09-12T15:13:31.569Z" }, - { url = "https://files.pythonhosted.org/packages/94/2e/708dc1d2678f092c31c12754e860cd8353e6a85ecbdb1010157edca0da9e/jiter-0.17.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0239520085cac678e77a606fd7e3f1c60c371d719790c5e3807388d3da4354c2", size = 336001, upload-time = "2026-09-12T15:13:32.846Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f0/75a5ae38862f4eaf0fe2f8a9fbf6484c4890df04c06dcdffc45e36bca61a/jiter-0.17.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:eb2295da7c3769f6719b227a237aa6a5cfa6550e478bc838001b592c57e16575", size = 484281, upload-time = "2026-09-12T15:13:35.333Z" }, - { url = "https://files.pythonhosted.org/packages/a0/32/6636fae811c27c7f93e1b11fb5800de6a5c9e4269a27cf718e0b31218ad1/jiter-0.17.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:e088612ff90ebc9247e1a43074b72835804261c47e6a6c01cb3ddcb55360d688", size = 521300, upload-time = "2026-09-12T15:13:37.101Z" }, - { url = "https://files.pythonhosted.org/packages/61/aa/12df7e0b0b1a2602e3d5a5a7104d7d9700f254b400f134a9b50955c4d231/jiter-0.17.0-cp315-cp315-win32.whl", hash = "sha256:0b52d52035b3907c5b1f6277857b29c1cbfc965e24e0f27330dbed83edb591ec", size = 185138, upload-time = "2026-09-12T15:13:38.901Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ec/3dd2e495032cddde05723c1f4c743b67a23e55d2af244692a7f58f0cdae3/jiter-0.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:10f5558eed511b830488003449d942bd75829ad6257dc58cb9a03e596a7777b1", size = 226950, upload-time = "2026-09-12T15:13:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c7/ef85704e0a57e9cadb2babc05f6d7c5df4a1c75da1a6ee31e1986b0099a5/jiter-0.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:fa13acf1046f95df808c64b1310705e143fab87aee73ae00cc42d640867fd2c1", size = 183618, upload-time = "2026-09-12T15:13:41.432Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/a4b348349de68762b58d6713973d363ad80a1c741d0bf8def7975f0ecb26/jiter-0.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:af2f7501580f274b63c4b2283bc425f5df7edf06ae5b171e5f87d912ff359a20", size = 321155, upload-time = "2026-09-12T15:13:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/c1/70/aebd6d0b5f0677de3a3d0bdc4a05fac949b97c4ede454c8809f180ac7b17/jiter-0.17.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10c5349312e5cb02b7a21e123a57665afa895953f05bf252a9dd4c13a572b7ab", size = 340985, upload-time = "2026-09-12T15:13:44.115Z" }, - { url = "https://files.pythonhosted.org/packages/a7/82/4c3b49796b5eb62f3f5046f957683f4ba0135fe1a60957c11180512460df/jiter-0.17.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86f3f9343a288eb85a81ef20a752b2f84564296636db54a9fff0b5c8deaf1df2", size = 325670, upload-time = "2026-09-12T15:13:45.901Z" }, - { url = "https://files.pythonhosted.org/packages/bc/43/f6341ecb4872202a4ef150486fcee0e1ace4aa3da39b71b82061452cdd3a/jiter-0.17.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4607ec7d93355fbc25b8dc5189153cf21d66063b9f9cd04dd2774e6e783f9b6a", size = 340339, upload-time = "2026-09-12T15:13:47.442Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c4/bc2c86e08fa065e03cb2fbc53b367c3640a7d257ef9d877b29118ea636b7/jiter-0.17.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10cd64a5720ad7f809ac5466ff1705813f1b6b510f195a73acafba0ac0e1f675", size = 350705, upload-time = "2026-09-12T15:13:48.848Z" }, - { url = "https://files.pythonhosted.org/packages/9d/67/91f12aa111cca6e3a197c3e36bf60a034bf9f122f6d41112a639e44217d8/jiter-0.17.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe9f61bb30174d2f5c8396445c360c96c44e78164d0815dfe627ccf57849574", size = 345011, upload-time = "2026-09-12T15:13:50.215Z" }, - { url = "https://files.pythonhosted.org/packages/f5/cb/9f5556e8f6ec89755fb5a709d8eb8270c9a324e31079eda0dfbeca451b6e/jiter-0.17.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:370d8fe5bf201dc6925e8a84c81ac7291f74d9fd1778234fc79d517064a5c76b", size = 328268, upload-time = "2026-09-12T15:13:51.809Z" }, - { url = "https://files.pythonhosted.org/packages/22/98/153f20680fb75781a490fb849940e2b00f95035c7aa054df592f36ed33fc/jiter-0.17.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b303d88e6a0bda789ec4b7801c7bad68e27230ba1fe4baffc756d1fbd32dc9d", size = 337024, upload-time = "2026-09-12T15:13:53.095Z" }, - { url = "https://files.pythonhosted.org/packages/af/59/b16c9be3a5035df4466cc72e888188c027562de90a723d290ab6814cb9d4/jiter-0.17.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:30793a24a31e968969757c9e08d830cbb15a2cd3c4959b4498b38f4b1c2258eb", size = 482766, upload-time = "2026-09-12T15:13:55.713Z" }, - { url = "https://files.pythonhosted.org/packages/d0/55/667dea313094024bef082175d6bfe8976f90d1c00c926af9df1d8e0eab48/jiter-0.17.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:686c93d86f2b426c803024b805bd161a6cd10e9627c23e901640eab646c0ad8a", size = 520367, upload-time = "2026-09-12T15:13:57.674Z" }, - { url = "https://files.pythonhosted.org/packages/21/e3/4b1a43501fb9ed17b01d137e380cb0e8fdcb39a254ce31aa2ab95bc861ac/jiter-0.17.0-cp315-cp315t-win32.whl", hash = "sha256:86d703d9faa1ffc8ae4e9de0fa007712ed2171b5c0d93811a8e2e105ac729b0d", size = 184603, upload-time = "2026-09-12T15:13:59.27Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f2/b8ee0372b6ebdf1bde5cc44495d5291d17f961065f5b48f8616cc67cac2e/jiter-0.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:42b0260445251b1bc520a63baa94a32d88e0f931fba234f1764db7feb7c72174", size = 227936, upload-time = "2026-09-12T15:14:00.472Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b4/923a1215daba959aed8355973315cb3f81f53e0d01c5b211870a27b41f45/jiter-0.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d47687806f9c54c84ea38733507081337922beca90ce819c7d852dd485bc0f23", size = 182977, upload-time = "2026-09-12T15:14:01.799Z" }, { url = "https://files.pythonhosted.org/packages/b9/3b/05a917204413e2e09906dfa35240c1021227aeb56c7305abea9562c598b4/jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:eaba834b72d573547b9d966465b3394b749d5e14208cc70acb63aca37619ab33", size = 288143, upload-time = "2026-09-12T15:14:02.998Z" }, { url = "https://files.pythonhosted.org/packages/d9/e1/a1cd3c0cf8f79945939e4f8caae9990529f67d7f77f671769f73956329b9/jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:51e1519d676a9f14dad9c2a411170d43b022ddb7989562df4e849b261ce127b2", size = 281847, upload-time = "2026-09-12T15:14:04.414Z" }, { url = "https://files.pythonhosted.org/packages/72/b4/9b797679e09f4a46c32986aeb3670bd9bc562fc0b373c7a0ee5c5dce1206/jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ce4feb52493e3513335b2accdcd75605652e4632772d3c8c2f7b86954d7f39", size = 304797, upload-time = "2026-09-12T15:14:05.733Z" }, @@ -3144,37 +3010,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, - { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, - { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, - { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, - { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, - { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, - { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, - { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, - { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, - { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, - { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, - { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, - { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, - { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, - { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, - { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, - { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, - { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, - { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, - { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, ] [[package]] @@ -3454,16 +3289,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/fb/8091c0aee7f2712de99c7fd4b1642382644dec6a4962effe4f5b9d16a973/ml_dtypes-0.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b76fa1d3f92967d58289ac47ab7458ede66e6f3527fff3e59142aee57d9307cd", size = 430645, upload-time = "2026-08-13T14:14:23.737Z" }, { url = "https://files.pythonhosted.org/packages/c4/6f/962d2c589513b5930d05b6eae5fbd22ad8bbcf26bb763449f3d8f912360f/ml_dtypes-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3be9911d953f97cddded4b9961d7b650473b7e55806d20f6176f8356dfe7b38e", size = 465667, upload-time = "2026-08-13T14:14:25.04Z" }, { url = "https://files.pythonhosted.org/packages/aa/ca/bcb25e246edd19af5fa1cf6267040bd9977a7afca846e6cfd4a52078b44f/ml_dtypes-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e74266ca8e97874a937b7646378c178025650a236584f7474d10d8086a6edea3", size = 572706, upload-time = "2026-08-13T14:14:26.296Z" }, - { url = "https://files.pythonhosted.org/packages/12/42/46cb442648e3c774d8cb25f2e1e41d496cdcc91fbe9c2a6f75c0b8df7af6/ml_dtypes-0.6.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b1b503864fada3f74fabf8d9fee7b4c1cbe956301e6fdece975d5f77c2fce958", size = 562550, upload-time = "2026-08-13T14:14:27.542Z" }, - { url = "https://files.pythonhosted.org/packages/07/56/844eff5af7a2d1a09d75df12c70225c3a6b6a771f95876b2bf5f7d10ad44/ml_dtypes-0.6.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6ad60af4102789a5c09824004beade2f7f28cd1cd581ee5c170d9dc2fbb00e", size = 360332, upload-time = "2026-08-13T14:14:28.767Z" }, - { url = "https://files.pythonhosted.org/packages/b6/29/b7165a3a76364a5baa6aa4ee82a0adf73a3c014b8cd126120b62cc087992/ml_dtypes-0.6.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4f1b9329a251e4affe3bb58f4d3e2db22a714396fd7ffb40d0b5db423c24d17", size = 409964, upload-time = "2026-08-13T14:14:30.023Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2e/f61c54a0544b6a170ac1bb89bcf406af53fb2deffc5476b6d2d3df5ba13e/ml_dtypes-0.6.0-cp315-cp315-win_amd64.whl", hash = "sha256:488c99ab181a2f59d9ec3b12c5fa11ec904e92be2c4ba18cded54dd7501208fe", size = 457249, upload-time = "2026-08-13T14:14:31.213Z" }, - { url = "https://files.pythonhosted.org/packages/63/00/bee1bc9faa02a46e7a851019fd23f47ca1f906609edbec8b6ba5decc3cc3/ml_dtypes-0.6.0-cp315-cp315-win_arm64.whl", hash = "sha256:de9d14748dbf3968951436ef514a29c9d1fe438aa680d110134ee2f7a9f9df18", size = 568381, upload-time = "2026-08-13T14:14:32.548Z" }, - { url = "https://files.pythonhosted.org/packages/72/f7/9a5edede28f73185fd51d75030ef7f11d76997bab3a92427d986e54fe2eb/ml_dtypes-0.6.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e25bb3b0ad1217b60626e4ed45b10ca170c41d99fbe44a12bebc1e07ec4aad55", size = 589877, upload-time = "2026-08-13T14:14:33.695Z" }, - { url = "https://files.pythonhosted.org/packages/fd/81/d5924a141b850b606eb027493c9c3ca3c665cca5163af3f5b6e5e3345503/ml_dtypes-0.6.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f1ce979d31a357e95aa81812f20412c8c954fa43c44ee3ead1e1c8a78575ef", size = 362788, upload-time = "2026-08-13T14:14:34.996Z" }, - { url = "https://files.pythonhosted.org/packages/59/8f/3298e3f334832bc28dd144af6b99cdc93502a8687e71922ea68b0a319929/ml_dtypes-0.6.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2d6149f3a57f405bcad5fb41e03218b8373936253f23e1ca84c0108abbc3392", size = 430823, upload-time = "2026-08-13T14:14:36.44Z" }, - { url = "https://files.pythonhosted.org/packages/93/d2/f2dbf118f42ce4c325a139c9236737f436b7f8e00cd18701c99ef2405e6f/ml_dtypes-0.6.0-cp315-cp315t-win_amd64.whl", hash = "sha256:ce7563e0b1a4482cbc1b4a6272145e54e4489e54fe7428f94908c3d87103abfa", size = 465119, upload-time = "2026-08-13T14:14:37.776Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ff/bda40387b5c5c64254595f4d81a12351770856acc5de4e6d43606a31f161/ml_dtypes-0.6.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f6cb525101b6b903779188c1e9e9490c343b455ab822883e02cf01e5547338d2", size = 572666, upload-time = "2026-08-13T14:14:38.993Z" }, ] [[package]] @@ -3556,6 +3381,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" }, ] +[[package]] +name = "mssql-python" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "mssql-python-odbc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/ed/38e52376a20f06dcd13c725a9706407ce72db5534f05242e463eb185e8f3/mssql_python-1.15.0-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:630b3f1c94ef051b5f12379607ceb88aa74d35a8cffedf37c03fa38d080504a6", size = 9935682, upload-time = "2026-09-11T15:21:47.657Z" }, + { url = "https://files.pythonhosted.org/packages/52/15/8d34096e15d59aa14cb0137561d30d049557eefd3ba9decd793ea807e6e7/mssql_python-1.15.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2e3b80c0f54342efc6d9ce162ab761f957befe3448a17c6146f688eae48b0906", size = 6073931, upload-time = "2026-09-11T15:21:50.15Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f6/050628231e49c694cf7a0def9dbc928e2f55f2dd7604f65d8598098687b7/mssql_python-1.15.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:016c61a4c82a1b69a8f8a43c91dc3537463d8ca068da73fa91f7e348b48e6d22", size = 6529330, upload-time = "2026-09-11T15:21:51.995Z" }, + { url = "https://files.pythonhosted.org/packages/47/e0/1f67b59bf027df0a9bafe743c4c7148c9f6fc8d0ce1192751aab2c4ed32b/mssql_python-1.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d58bc237ec90f6e6708fc1978ab99ff283b53508fb5adde15f63335cc23db6d3", size = 5935860, upload-time = "2026-09-11T15:21:54.043Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/b909876a86ef90cab9896279577acf3d20deb02bc2d06b3b08052cf27ee7/mssql_python-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b5aecf95d7ccc57ec7cdea9b636539aaffbadd5e69534ecc9c2a498b24263a6", size = 6343407, upload-time = "2026-09-11T15:21:55.659Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0b/ecaf4f78123017afe1a432da94f968a13f5dd8bf62f8ddba18775c9ca2f4/mssql_python-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a571327f60a77fa4e515f495b6da7b53008d56a57a901ce82617add84377d9c6", size = 5187884, upload-time = "2026-09-11T15:21:57.237Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/75b2eefdc22e11853c710b2921caf4a1b44bc569c9bc70121de3303e5de9/mssql_python-1.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:6a714a7ca15eff702e21d1e15856884bde4da89f438239c72ec423e69d02d56c", size = 4940428, upload-time = "2026-09-11T15:21:59.085Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ff8e4bfccf6e7bca17f67c9ca4db2375ccdfa0b4bcbfdb0a2ddcccd41e6d/mssql_python-1.15.0-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:97521247fbb2d11f957cc33de5676e284aef2923645dcdabf707b84b629ed25d", size = 9965726, upload-time = "2026-09-11T15:22:01.055Z" }, + { url = "https://files.pythonhosted.org/packages/fb/41/2ef9ddb99b672d6c47e341d14c904687b6f4e8483d4f672142596aad4f44/mssql_python-1.15.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:77b9f8ce2d95ba6bc5c83a70df57908366f0f6b84574b0055b9ba0ccf5775f7e", size = 6593472, upload-time = "2026-09-11T15:22:03.233Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4e/25879e63c11f311343e7dd424570ccce49eb6a0a51cd2464837524d35370/mssql_python-1.15.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:05cfea4fbbd1ec1ded54fd5d3a292c1cc8617d59c08d0b3005825b3e6cacf432", size = 7213764, upload-time = "2026-09-11T15:22:04.911Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/fde649be05d90b4cf07425afad6bc65439503b043e197f63f08202c94ee0/mssql_python-1.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f0943a42fca2234133aa673b444ffff67cc70c7703a6265ddf10c3d6e792c182", size = 6395871, upload-time = "2026-09-11T15:22:06.67Z" }, + { url = "https://files.pythonhosted.org/packages/c6/07/4903db1cc8eecbc3e8ed868b04ea6de936ae80d58c712085d5ea07136e66/mssql_python-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8821418222d4b3e2cdece5c37e4339133aa3d20c2ebccb1c36836f3a9a72e53a", size = 6960731, upload-time = "2026-09-11T15:22:08.322Z" }, + { url = "https://files.pythonhosted.org/packages/e2/50/603d2aea003aa186e6ca12bbc073fbdd0b6fc93eab8487a5aeae3fb050d6/mssql_python-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:88d8228cb6beccea04c03bf8606985989381f7e783527fb4c7b1b9d76d9c8a32", size = 5185162, upload-time = "2026-09-11T15:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9c/cfc86d803e2f7d395c5392257b251ac55ceb2edbe932b97c70f490d45276/mssql_python-1.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:b0b97f498777f874bece12f3f2210c0f954da36bd2e623da84d6dc1d5f5c6735", size = 4936441, upload-time = "2026-09-11T15:22:11.537Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/1c879dcab6f73e521b734f9224ac68b394958f267e6a278b76f6fae640d0/mssql_python-1.15.0-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:2e9d78861d4f97d779ad770b9b4af26c1aa332e2d877b9df3c8896fe766968c6", size = 9960882, upload-time = "2026-09-11T15:22:13.403Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/40b7cd3a6cb5ae13b3278192f31df3fa2ef3e153255d56662d71da2ae772/mssql_python-1.15.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1614c5d32fe5ddf30bb5a576412280eeb588a7bf371a6a81de3588c741059c13", size = 7130021, upload-time = "2026-09-11T15:22:15.609Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/591359e2ce07bf8024d372484be8fbb58d292425381763487d16f2fab626/mssql_python-1.15.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:384d5729eed457047cfb3b506f489950700f10d285370d388312807b3bcffa17", size = 7913266, upload-time = "2026-09-11T15:22:17.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/aa/275d459975bc06b92826226e1f631e8a6e59e27325927bc487a8d16abf49/mssql_python-1.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f92146ce5ba3f52838041df2cc699be508a2632fc50c25599844f7b50a016fb4", size = 6872555, upload-time = "2026-09-11T15:22:19.274Z" }, + { url = "https://files.pythonhosted.org/packages/59/e6/8f9825141b9875728cba0a5f402ad408e8fbab741412c6747e98b021e639/mssql_python-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e16bcad380466cc6232852d372f48ef4c23c981f4d5e12698957b398c4bd04e2", size = 7585129, upload-time = "2026-09-11T15:22:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2f/79ee4ec452e02af4fa3146818b976c1b00f0c8d8362faf031d292c4bca9a/mssql_python-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f88f4497d2e40c75bfeb37b6fe0a790b661ac9049dd85f4a5b85143c2cd1f68", size = 5183190, upload-time = "2026-09-11T15:22:23.028Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/a51b4485b303aa536a9fec1a84ac5927b81932a9764e4cabb0645f7933ab/mssql_python-1.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:7b08b2906ff7339f9fb1752b21a6f25a8a00c36fc311a1371b9ca4c2bd66a0e3", size = 4930532, upload-time = "2026-09-11T15:22:24.855Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/0942797afac9b2cf74b59ce3255364040ba3f50ba551c701df6e6262ffdb/mssql_python-1.15.0-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:6b4f7826e22e7c0ae8613cf78509a81ce4f5d95f7a16b10493bc073653bb0176", size = 9971965, upload-time = "2026-09-11T15:22:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6a/361e2e7fd7e1fccfcc3e7e56ab6793b0018b4b61597e6afd21171a350c47/mssql_python-1.15.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:061670881c8b6f395aa1dad606fe2ff68af579367f91effde436e9f10042152d", size = 7679594, upload-time = "2026-09-11T15:22:29.259Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bf/50d2e0911f383038e59cda0730c52a82a1fcd0763d084ebefd6b3f5387cd/mssql_python-1.15.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:69640f8ea7726e9c1d7b673098baec4b7b637bb5b144764e2e688cade664064d", size = 8642553, upload-time = "2026-09-11T15:22:31.258Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/5fb9ec913454cddd4a27ce9e142b1b5edc01d96dec25e8ec2c15ef12311f/mssql_python-1.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dcb141191daf6214f614907e40de2559aba9ff97f570ab2d4e3c1f02958c7e76", size = 7359238, upload-time = "2026-09-11T15:22:35.151Z" }, + { url = "https://files.pythonhosted.org/packages/96/c9/f3bdb4bf3fcac6102abdc4194a4b43b77a8da155e69c4c8aa1c18dbce265/mssql_python-1.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c7305c55ba30bb9135e7273ffcb6624a1d25546c8b09033e12d9631d5f1e8e65", size = 8240128, upload-time = "2026-09-11T15:22:36.92Z" }, + { url = "https://files.pythonhosted.org/packages/af/e8/0793bdcd7016adf52c8f39ac8e1abd1f0eeef658c75d25e90ecc36a3a733/mssql_python-1.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf265057da5337aa134358b97f80b0e4a118661848ccaff52db61d912c36d6f5", size = 5407508, upload-time = "2026-09-11T15:22:38.51Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f3/175854dc9ca8ad74cd1823c94d82cfbd58b8f9602401f056f834245125d6/mssql_python-1.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:f91c5d9027ed044d0d954525a05ce87fbcc5ac5a9c02072f0e50e391f37ed3dc", size = 5167181, upload-time = "2026-09-11T15:43:51.102Z" }, +] + +[[package]] +name = "mssql-python-odbc" +version = "18.6.2.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/21/5d8e34820d986426dc65f88702ccecd1a981184e6bffab2968011461eaef/mssql_python_odbc-18.6.2.1-py3-none-macosx_15_0_universal2.whl", hash = "sha256:c0c4d2446f37656be1ed32c8e3229233554c1fbfc9f23c73c2ab0d07a2bfdcf2", size = 2035381, upload-time = "2026-08-05T15:51:46.762Z" }, + { url = "https://files.pythonhosted.org/packages/85/98/e07617483dd395156fba003cda6ff85ca9ad64de1347b823951f2afb3d60/mssql_python_odbc-18.6.2.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d665529c195829970dcb72b7b3200e3c0ee1b053bee88e9a6eabaa385874dd6e", size = 2730032, upload-time = "2026-08-05T15:51:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/35/1f/6cdb575549fbbe6f49bcadb505768649c43226bd83ec57a53a934d48e459/mssql_python_odbc-18.6.2.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:b8aad821f3b8637b0e6d4637f4b730ed9897c4665fa99d47e44e97fd1c79c77a", size = 3897879, upload-time = "2026-08-05T15:51:50.134Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/fa18a5f48eb4f0f003010af808b032806082c5441a076d698835ed5151b6/mssql_python_odbc-18.6.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bcfca2a994b17499dc24a8b40228b851ccd0cd5807def855a6c41a8cedc4f8ac", size = 2730032, upload-time = "2026-08-05T15:51:51.672Z" }, + { url = "https://files.pythonhosted.org/packages/6c/66/7c96d719dfe7e9dcbbbf7acb345c72dfaef392fa6c5042e2718dfb1779b4/mssql_python_odbc-18.6.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3db256103b09156f0980bee2dedc47995f528f76d01388ab5c5f1758b7e8fc53", size = 3897877, upload-time = "2026-08-05T15:51:53.147Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f6/e7725d5dc791f5535fa7ec750c2ab9a1ff731f923d01b9c0d5cf1779b676/mssql_python_odbc-18.6.2.1-py3-none-win_amd64.whl", hash = "sha256:07a987221f99b368161db912065ef9e64c85f6b0901a2b05e9c1e6b41da08d55", size = 3712266, upload-time = "2026-08-05T15:51:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/cda7ae46dd3c723196043109204fcc7749d688194de834b3f8aa4cd94415/mssql_python_odbc-18.6.2.1-py3-none-win_arm64.whl", hash = "sha256:00e1eae3c44a157ca60f992c6305be45856f18276ccff5258c1e9619847f3a5a", size = 7017266, upload-time = "2026-08-05T15:51:56.958Z" }, +] + [[package]] name = "multidict" version = "6.8.0" @@ -3666,48 +3544,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/bc/ecfb8b6faa8e158a71b03bdf7f947f30e0bc5d899cc357573a76ab7bb1e5/multidict-6.8.0-cp314-cp314t-win32.whl", hash = "sha256:48ea524a25a1cd5972cf293bc95713918cba0bcd6fa9b992d906c857c546abe2", size = 50628, upload-time = "2026-09-09T13:56:17.837Z" }, { url = "https://files.pythonhosted.org/packages/30/7f/e27fb699b70ad24dbd02ddee604658acb36f907c03c045baffe4ea774501/multidict-6.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0be2b832435001bc623ca7f1499ca1a853d4f082fb61221a80ce71132f50b26", size = 55592, upload-time = "2026-09-09T13:56:19.652Z" }, { url = "https://files.pythonhosted.org/packages/eb/7a/76de70b2f6733696803f1ee56abe44a3757a52777383032c7373d3fea0f4/multidict-6.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:62b8e291a4f7edbf7cde7a43d831d893ba443a1b627498b53581943b0e348feb", size = 50300, upload-time = "2026-09-09T13:56:21.516Z" }, - { url = "https://files.pythonhosted.org/packages/5c/45/ecb641309dc2cdc6040f18e22c68eb5e94398f9404c4365d810f4292e053/multidict-6.8.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b25426f9f6ed402835617c8f23609a47045f91ecff365eb6734817e039a8ed25", size = 47505, upload-time = "2026-09-09T13:56:24.902Z" }, - { url = "https://files.pythonhosted.org/packages/eb/68/87d6161b9fef11943e0b894203da3fff561933ca3c9b2952b6e7100e9c9f/multidict-6.8.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa6c2880709c84457de104385b704fc28860f27e442ad13966fc4af8e714fe9c", size = 48549, upload-time = "2026-09-09T13:56:26.574Z" }, - { url = "https://files.pythonhosted.org/packages/97/f7/d852d2276407640cdbd29fe11cac6e93f70f59542cba174ef9d146738946/multidict-6.8.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:eabb03dc3e4ed6333ecd1cc9826ec80e7a98b5506deeb832d7260c8e44166d23", size = 83157, upload-time = "2026-09-09T13:56:28.227Z" }, - { url = "https://files.pythonhosted.org/packages/14/e3/16fe7ffa6090591d83cf6bc2486e77ce891705fb6d0191823140928311b5/multidict-6.8.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:59e539c4eb4d3a53b0e630a6ba2b2f2824732b5e73f90e30a280f12fde157b15", size = 50578, upload-time = "2026-09-09T13:56:30Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0c/e38e41c1087a599f86ff58a01f358abf7c4db3c26a3e90eebb3e02193ef1/multidict-6.8.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:835d5a90b11d1f5f8200ff3cc8316bded76eebebc92436398947a27657e645e7", size = 48815, upload-time = "2026-09-09T13:56:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f0/eb691f42af8e7775992f57904ec75dc356fc7cdc896e5f30879decdd26f2/multidict-6.8.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d2d236b8a44ae91536a12ebcb996bdb31cf27425f36b4d05c87f2ba2716050ba", size = 274804, upload-time = "2026-09-09T13:56:36.741Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/28472ccfeb43c00a043c0385ca4294da21a5957859fb7860e2ebdb3e3011/multidict-6.8.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb9a60b7faa5d37c426fa91cf4d6738182a1f2755b9fab7c9c64cd466c4ce51e", size = 279693, upload-time = "2026-09-09T13:56:38.531Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a1/2b4fe73e5fecff807b47650a155c391a103136428cb21d6ba8e39c5912b5/multidict-6.8.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ef606c15cac6c90279acf34120784b6f36662cbf382defd3955cd8f1115336b", size = 254969, upload-time = "2026-09-09T13:56:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/44/e0/c97d1822783dfe52e02fd150fa3f02eb22410211a9e2615f71541803ed4b/multidict-6.8.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:68186a2d4051c8ffd17be33553bea2ec9bbc8ef860fe2980a221d96126296f31", size = 286392, upload-time = "2026-09-09T13:56:42.234Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d9/772f1339e1d051236bcc137b0eac2b4aaaa0bbb56aaf924e9aaba901d9c1/multidict-6.8.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2cc66abb85e2108c9ff8a1c0d20fa260bf690bbb33caef4ff3ecb2c2cbdfff5d", size = 285348, upload-time = "2026-09-09T13:56:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/06/ae/cd045747e4680362e02955a82c468e95b5e4d319e3a79574b3fb677de568/multidict-6.8.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69b3e519a132bb943b0daae15fc8c2168706b17f826481d32a32a5e784b129e3", size = 282721, upload-time = "2026-09-09T13:56:46.088Z" }, - { url = "https://files.pythonhosted.org/packages/35/14/0802d9a3aae4ef21eaa39adbd729a380fa095932105e1424e417b53e783f/multidict-6.8.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e41226ecf607f062fe34a2f4cf64ad3a89e3a0180dc800b463b6b14c06dd10dc", size = 253168, upload-time = "2026-09-09T13:56:48.07Z" }, - { url = "https://files.pythonhosted.org/packages/b1/64/3f92298bab8fbe1332e708863fb55b66e755be6f416b3459720d48b33af9/multidict-6.8.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:610c7637bc36b90f39e6c66f710f93d57018f83d53e1e187caaa218c6892b95f", size = 274209, upload-time = "2026-09-09T13:56:50.023Z" }, - { url = "https://files.pythonhosted.org/packages/08/c2/2001ac0eac1a8b7390a5902d7115f66d4f256268057a502200b6ab12dad7/multidict-6.8.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:65c85c79f5a2c04fbbc18f006c014674dc5fdf270cb978d8862c82c6f694e60c", size = 268044, upload-time = "2026-09-09T13:56:52.033Z" }, - { url = "https://files.pythonhosted.org/packages/0d/90/78a9e26c85f89abd562a67f7fcbaef9007fd5c37bb9efac19f1cf604e7c2/multidict-6.8.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:628ff11e6720f90acd0c305dfa3339f04a783a20de8cda6ac333ba46447261e8", size = 274806, upload-time = "2026-09-09T13:56:53.975Z" }, - { url = "https://files.pythonhosted.org/packages/3d/71/713bd445421b21531234c1f3630b768192cb9d80c8b1c5b05c5b505ff4c0/multidict-6.8.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0935971bffd0b479fc90c4811ca787703e93fcb6afea939a375dfc80285ab368", size = 281890, upload-time = "2026-09-09T13:56:55.848Z" }, - { url = "https://files.pythonhosted.org/packages/de/a5/1387c538663e2dc8c27bbc7cd6955cb66de0f55c780cf7cd0fc06a1a16ca/multidict-6.8.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9442b14eec262a1f74369bbd07e75bc5155105164649a4b9fbc1ebc7b8fb0b14", size = 249749, upload-time = "2026-09-09T13:56:58.01Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/104296c9d70896b9759ce0812aa4899fab76d8b16bb32dcc5a78ab547c89/multidict-6.8.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:397599503b718f0137f26d3f6532d6955069cd2e5917c47ef581495bc2529ff8", size = 276138, upload-time = "2026-09-09T13:57:03.591Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0a/f2a0c2658e9d7ff5964ec2820a02054558636fafd663230ddc8310b8ed39/multidict-6.8.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9e37024b41d7a7e7e9cce14b248d54707c21c2a2ea30a47b71bdcefcafec00f2", size = 277077, upload-time = "2026-09-09T13:57:06.024Z" }, - { url = "https://files.pythonhosted.org/packages/98/50/bc46566caffba5c1c4a510519156371edf7c4ecd35c9ef917d0c1803487d/multidict-6.8.0-cp315-cp315-win32.whl", hash = "sha256:071da134651b04a8507dfb331ac0988f376337c2aea59486bf20989fb5b5a64e", size = 46930, upload-time = "2026-09-09T13:57:08.009Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1a/cafb31049ecc1a6ce52bcc69fa436cca239adc057b1718a0c49044848663/multidict-6.8.0-cp315-cp315-win_amd64.whl", hash = "sha256:3bafff8598f0528017ddc74194e5451d5c22d046c98935f8f86247b0f286e4f8", size = 50294, upload-time = "2026-09-09T13:57:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/6b/51/00e037da14cd1d894b123e0bbe62de5c561679a6ab23ab1c009f2965dcda/multidict-6.8.0-cp315-cp315-win_arm64.whl", hash = "sha256:e886ef8c9879105fe4fc99417447b3a5f35d1131412ce839470bd2089fe2043f", size = 47626, upload-time = "2026-09-09T13:57:11.738Z" }, - { url = "https://files.pythonhosted.org/packages/52/f7/aeb947982197e8b4f5c4da3961ee473ea5a050b94a6ff3b88baf64621401/multidict-6.8.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:883284137e25318ed9735b742ae46341a864888fae28e8b6314c4f84da080f08", size = 88801, upload-time = "2026-09-09T13:57:13.957Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/593948c016c3f850e3cd56a4e0144151eb409d2b8690f0c0ce7f7d33dbea/multidict-6.8.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:ca52b9ec80851366197577154c862c4c4c7036ca76ae94cef5cb59c5cfeab944", size = 53376, upload-time = "2026-09-09T13:57:15.94Z" }, - { url = "https://files.pythonhosted.org/packages/58/b9/097a05bca533027c0477b6a90bf927dbbb4b23cc9090bbb37a2e972af8d5/multidict-6.8.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:91fa75d0a693832106d98f66c849f034f21c828d14437f1fb97d3784aab89e84", size = 51629, upload-time = "2026-09-09T13:57:17.685Z" }, - { url = "https://files.pythonhosted.org/packages/fe/07/938ed21967f12380d0b8861645fb65a942f3669e31d5163ed94d23103b61/multidict-6.8.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:307c1acd812fe897e7fbe10c6758822e8c04be4e7c60a9f54901cdf8b5ab8bc3", size = 261967, upload-time = "2026-09-09T13:57:19.752Z" }, - { url = "https://files.pythonhosted.org/packages/89/e8/e66bf843fd29c01712dde9edeb9f4ad0ffab06ab4ada4b721ad7bc73b3d5/multidict-6.8.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf684986a2490628f059a99dd107b566a2d34cf947f8eb8387e0500a1f90c5", size = 265923, upload-time = "2026-09-09T13:57:21.784Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f8/e9be849b225af28a8eee2c6bfea23594a777c753fe97e2ff7e2180c8935a/multidict-6.8.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:77745725125d01fd613b6db043362aa7c6bfbfdb23d45dbfc3d92bf58160af62", size = 239380, upload-time = "2026-09-09T13:57:24.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/4dbad08f5081978c591afae9e836ec9ddae90e9e76be6d6ce10757483dc4/multidict-6.8.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8daafaa0b2eb43f76898ced78b1e0fb91b38c4fa50da516c18067f2a2d578c20", size = 271591, upload-time = "2026-09-09T13:57:26.611Z" }, - { url = "https://files.pythonhosted.org/packages/92/3f/e9c97222d7e104e54e556f118ec7d091ab41a0c10c630f2b97e5b43f5404/multidict-6.8.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c68e0c0649d17c2d0339e3674e86a4aeba4a7e6b21c1e394cf947a95433b31d0", size = 276091, upload-time = "2026-09-09T13:57:28.997Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/728e7ce05ac9c0303554e7162e74d91fe49e65bad7dfbb377f783dd32c0a/multidict-6.8.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8a5ac357ac283490a8d1899b0383355fd1f8634b14ba0d59e4c0dd97db85556", size = 266493, upload-time = "2026-09-09T13:57:31.256Z" }, - { url = "https://files.pythonhosted.org/packages/8f/74/7c658d2769863af16fb7d7c6be50b29659892a06a632858863eee3a31842/multidict-6.8.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46029e6e27a3ec0dc55b53f58df82d10f04c5e111f78248279b530bedad2c30a", size = 245302, upload-time = "2026-09-09T13:57:33.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/2c/d4350a20a0e8c66a447d694e8713438262665203fe826c3f4e385f052b72/multidict-6.8.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8d1046b5427dcafe6e8a0e07527dd74f1ee694006160162f53f3a17f15aad3b4", size = 261016, upload-time = "2026-09-09T13:57:35.651Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/83df999c8beb72a012cfac42f2b833c4a48f8e836fd4407747b355a2430e/multidict-6.8.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:f1f4a220db6ed7c8fd16b6d644ffd1f082651693204daf3275e049fadc849e39", size = 255021, upload-time = "2026-09-09T13:57:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/fb/13/f2c0a2dac6d91f74aa124f3e9f07ec497ceae5ed2df2753d249601cd7262/multidict-6.8.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:029897732a9c798737457e382bf84e8c64237eff224a90aea2639f4413c45e4e", size = 263066, upload-time = "2026-09-09T13:57:39.897Z" }, - { url = "https://files.pythonhosted.org/packages/59/1d/730008d4639ace731bbb1399e1ac13cbdf506f7d6fb861d75044ffb3994d/multidict-6.8.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:29be9fd289e9ab8f480996ea2f686e1654b80242033843cb11691688329423f1", size = 266510, upload-time = "2026-09-09T13:57:42.39Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/bec67a5d206dc5748e50c93f6f71deec14305c3657cfe250c3887caf7839/multidict-6.8.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:29631224698de1e42abc8fa7658d830e0aed0029785144b5832b695da5adef2f", size = 239423, upload-time = "2026-09-09T13:57:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/9e/db/5f153fe51fbac7d80f3bb8bd6fab8db8b6cd061e7a11371676dfed3712bc/multidict-6.8.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:962f18c59a000f30b084ea2e6b8001521bb315efd4e5f10acf9fb36f366b7882", size = 266902, upload-time = "2026-09-09T13:57:46.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/0f/9efca48a351551de4dc0c183f523109dbe87c645a4732d5f1c70b4880dca/multidict-6.8.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c60e50bc5b07faac92fd3a20fa21cc8cf3e3f7204d2867b206c73293ebc19101", size = 260887, upload-time = "2026-09-09T13:57:48.268Z" }, - { url = "https://files.pythonhosted.org/packages/df/d8/bb879a62e0809448e53f6237e71670066ecf3bbc5896a7a6705b6628d86a/multidict-6.8.0-cp315-cp315t-win32.whl", hash = "sha256:fc5460940f50dff00731b4132366840ba9685286ea88ea104b661899084f3fea", size = 50533, upload-time = "2026-09-09T13:57:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/fe/62/3e5308d8871636e4b9620e4b3acfcf2b5caf79b19d317690ec13f7fc8b57/multidict-6.8.0-cp315-cp315t-win_amd64.whl", hash = "sha256:b367c342327717d644db4c0ddb37ceb655c84822215ea0773a3a36911b74b71d", size = 55572, upload-time = "2026-09-09T13:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cc/d3c10e10ee3bb7a7b4abbb3157306b2ce7e0018c9c2d16b32b468739d2b7/multidict-6.8.0-cp315-cp315t-win_arm64.whl", hash = "sha256:0c1c4debad7337627b86837abdf0237ca3cb3d7e17de7eab0177c263878546d4", size = 50322, upload-time = "2026-09-09T13:57:54.099Z" }, { url = "https://files.pythonhosted.org/packages/b1/ee/be4e1a4b7a2b27f4fb6936510d4bebcb41b0562c946930ad26916e069cf9/multidict-6.8.0-py3-none-any.whl", hash = "sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e", size = 16297, upload-time = "2026-09-09T13:57:56.106Z" }, ] @@ -3755,19 +3591,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" }, - { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" }, - { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" }, - { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" }, - { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" }, - { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" }, - { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" }, - { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" }, - { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" }, - { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" }, - { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" }, { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, ] @@ -3878,16 +3701,13 @@ name = "numpy" version = "2.5.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'darwin'", - "python_full_version == '3.14.*' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.15' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and sys_platform == 'linux'", + "python_full_version >= '3.14' and sys_platform == 'linux'", "python_full_version == '3.13.*' and sys_platform == 'linux'", "python_full_version == '3.12.*' and sys_platform == 'linux'", - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", ] @@ -3936,28 +3756,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, - { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, - { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, - { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, - { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, - { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, - { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, - { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, - { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, - { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, - { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, - { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, - { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, ] [[package]] @@ -4526,8 +4324,8 @@ name = "powerfx" version = "0.0.34" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, - { name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" } wheels = [ @@ -4663,40 +4461,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, - { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" }, - { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" }, - { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" }, - { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" }, - { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" }, - { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" }, - { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" }, - { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" }, - { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" }, - { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" }, - { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" }, - { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" }, - { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" }, - { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" }, - { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" }, - { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" }, - { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" }, - { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" }, - { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" }, - { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" }, - { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" }, - { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" }, { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, ] @@ -4770,7 +4534,7 @@ wheels = [ [package.optional-dependencies] binary = [ - { name = "psycopg-binary", marker = "(python_full_version < '3.15' and implementation_name != 'pypy' and sys_platform == 'darwin') or (python_full_version < '3.15' and implementation_name != 'pypy' and sys_platform == 'linux') or (python_full_version < '3.15' and implementation_name != 'pypy' and sys_platform == 'win32')" }, + { name = "psycopg-binary", marker = "(implementation_name != 'pypy' and sys_platform == 'darwin') or (implementation_name != 'pypy' and sys_platform == 'linux') or (implementation_name != 'pypy' and sys_platform == 'win32')" }, ] pool = [ { name = "psycopg-pool", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -5176,59 +4940,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/f2/12afe9d59fb82b60832356f9fe8f454a99371c0e0a42f0ec7856f5c893fd/pymongo-4.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:960c61ed86a316d3636b1f184006d3cc2b972b8546186ae8e2c8324e397632ff", size = 817669, upload-time = "2026-09-10T14:56:50.602Z" }, ] -[[package]] -name = "pyodbc" -version = "5.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/85/44b10070a769a56bd910009bb185c0c0a82daff8d567cd1a116d7d730c7d/pyodbc-5.3.0.tar.gz", hash = "sha256:2fe0e063d8fb66efd0ac6dc39236c4de1a45f17c33eaded0d553d21c199f4d05", size = 121770, upload-time = "2025-10-17T18:04:09.43Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/c7/534986d97a26cb8f40ef456dfcf00d8483161eade6d53fa45fcf2d5c2b87/pyodbc-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebc3be93f61ea0553db88589e683ace12bf975baa954af4834ab89f5ee7bf8ae", size = 71958, upload-time = "2025-10-17T18:03:10.163Z" }, - { url = "https://files.pythonhosted.org/packages/69/3c/6fe3e9eae6db1c34d6616a452f9b954b0d5516c430f3dd959c9d8d725f2a/pyodbc-5.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9b987a25a384f31e373903005554230f5a6d59af78bce62954386736a902a4b3", size = 71843, upload-time = "2025-10-17T18:03:11.058Z" }, - { url = "https://files.pythonhosted.org/packages/44/0e/81a0315d0bf7e57be24338dbed616f806131ab706d87c70f363506dc13d5/pyodbc-5.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:676031723aac7dcbbd2813bddda0e8abf171b20ec218ab8dfb21d64a193430ea", size = 327191, upload-time = "2025-10-17T18:03:11.93Z" }, - { url = "https://files.pythonhosted.org/packages/43/ae/b95bb2068f911950322a97172c68675c85a3e87dc04a98448c339fcbef21/pyodbc-5.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5c30c5cd40b751f77bbc73edd32c4498630939bcd4e72ee7e6c9a4b982cc5ca", size = 332228, upload-time = "2025-10-17T18:03:13.096Z" }, - { url = "https://files.pythonhosted.org/packages/dc/21/2433625f7d5922ee9a34e3805805fa0f1355d01d55206c337bb23ec869bf/pyodbc-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2035c7dfb71677cd5be64d3a3eb0779560279f0a8dc6e33673499498caa88937", size = 1296469, upload-time = "2025-10-17T18:03:14.61Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f4/c760caf7bb9b3ab988975d84bd3e7ebda739fe0075c82f476d04ee97324c/pyodbc-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5cbe4d753723c8a8f65020b7a259183ef5f14307587165ce37e8c7e251951852", size = 1353163, upload-time = "2025-10-17T18:03:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/14/ad/f9ca1e9e44fd91058f6e35b233b1bb6213d590185bfcc2a2c4f1033266e7/pyodbc-5.3.0-cp311-cp311-win32.whl", hash = "sha256:d255f6b117d05cfc046a5201fdf39535264045352ea536c35777cf66d321fbb8", size = 62925, upload-time = "2025-10-17T18:03:17.649Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/52b9b94efd8cfd11890ae04f31f50561710128d735e4e38a8fbb964cd2c2/pyodbc-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:f1ad0e93612a6201621853fc661209d82ff2a35892b7d590106fe8f97d9f1f2a", size = 69329, upload-time = "2025-10-17T18:03:18.474Z" }, - { url = "https://files.pythonhosted.org/packages/8b/6f/bf5433bb345007f93003fa062e045890afb42e4e9fc6bd66acc2c3bd12ca/pyodbc-5.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:0df7ff47fab91ea05548095b00e5eb87ed88ddf4648c58c67b4db95ea4913e23", size = 64447, upload-time = "2025-10-17T18:03:19.691Z" }, - { url = "https://files.pythonhosted.org/packages/f5/0c/7ecf8077f4b932a5d25896699ff5c394ffc2a880a9c2c284d6a3e6ea5949/pyodbc-5.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ebf6b5d989395efe722b02b010cb9815698a4d681921bf5db1c0e1195ac1bde", size = 72994, upload-time = "2025-10-17T18:03:20.551Z" }, - { url = "https://files.pythonhosted.org/packages/03/78/9fbde156055d88c1ef3487534281a5b1479ee7a2f958a7e90714968749ac/pyodbc-5.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:197bb6ddafe356a916b8ee1b8752009057fce58e216e887e2174b24c7ab99269", size = 72535, upload-time = "2025-10-17T18:03:21.423Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f9/8c106dcd6946e95fee0da0f1ba58cd90eb872eebe8968996a2ea1f7ac3c1/pyodbc-5.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6ccb5315ec9e081f5cbd66f36acbc820ad172b8fa3736cf7f993cdf69bd8a96", size = 333565, upload-time = "2025-10-17T18:03:22.695Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/2c70f47a76a4fafa308d148f786aeb35a4d67a01d41002f1065b465d9994/pyodbc-5.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5dd3d5e469f89a3112cf8b0658c43108a4712fad65e576071e4dd44d2bd763c7", size = 340283, upload-time = "2025-10-17T18:03:23.691Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b2/0631d84731606bfe40d3b03a436b80cbd16b63b022c7b13444fb30761ca8/pyodbc-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b180bc5e49b74fd40a24ef5b0fe143d0c234ac1506febe810d7434bf47cb925b", size = 1302767, upload-time = "2025-10-17T18:03:25.311Z" }, - { url = "https://files.pythonhosted.org/packages/74/b9/707c5314cca9401081b3757301241c167a94ba91b4bd55c8fa591bf35a4a/pyodbc-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3c39de3005fff3ae79246f952720d44affc6756b4b85398da4c5ea76bf8f506", size = 1361251, upload-time = "2025-10-17T18:03:26.538Z" }, - { url = "https://files.pythonhosted.org/packages/97/7c/893036c8b0c8d359082a56efdaa64358a38dda993124162c3faa35d1924d/pyodbc-5.3.0-cp312-cp312-win32.whl", hash = "sha256:d32c3259762bef440707098010035bbc83d1c73d81a434018ab8c688158bd3bb", size = 63413, upload-time = "2025-10-17T18:03:27.903Z" }, - { url = "https://files.pythonhosted.org/packages/c0/70/5e61b216cc13c7f833ef87f4cdeab253a7873f8709253f5076e9bb16c1b3/pyodbc-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe77eb9dcca5fc1300c9121f81040cc9011d28cff383e2c35416e9ec06d4bc95", size = 70133, upload-time = "2025-10-17T18:03:28.746Z" }, - { url = "https://files.pythonhosted.org/packages/aa/85/e7d0629c9714a85eb4f85d21602ce6d8a1ec0f313fde8017990cf913e3b4/pyodbc-5.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:afe7c4ac555a8d10a36234788fc6cfc22a86ce37fc5ba88a1f75b3e6696665dc", size = 64700, upload-time = "2025-10-17T18:03:29.638Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1d/9e74cbcc1d4878553eadfd59138364b38656369eb58f7e5b42fb344c0ce7/pyodbc-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e9ab0b91de28a5ab838ac4db0253d7cc8ce2452efe4ad92ee6a57b922bf0c24", size = 72975, upload-time = "2025-10-17T18:03:30.466Z" }, - { url = "https://files.pythonhosted.org/packages/37/c7/27d83f91b3144d3e275b5b387f0564b161ddbc4ce1b72bb3b3653e7f4f7a/pyodbc-5.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6132554ffbd7910524d643f13ce17f4a72f3a6824b0adef4e9a7f66efac96350", size = 72541, upload-time = "2025-10-17T18:03:31.348Z" }, - { url = "https://files.pythonhosted.org/packages/1b/33/2bb24e7fc95e98a7b11ea5ad1f256412de35d2e9cc339be198258c1d9a76/pyodbc-5.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1629af4706e9228d79dabb4863c11cceb22a6dab90700db0ef449074f0150c0d", size = 343287, upload-time = "2025-10-17T18:03:32.287Z" }, - { url = "https://files.pythonhosted.org/packages/fa/24/88cde8b6dc07a93a92b6c15520a947db24f55db7bd8b09e85956642b7cf3/pyodbc-5.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ceaed87ba2ea848c11223f66f629ef121f6ebe621f605cde9cfdee4fd9f4b68", size = 350094, upload-time = "2025-10-17T18:03:33.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/99/53c08562bc171a618fa1699297164f8885e66cde38c3b30f454730d0c488/pyodbc-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3cc472c8ae2feea5b4512e23b56e2b093d64f7cbc4b970af51da488429ff7818", size = 1301029, upload-time = "2025-10-17T18:03:34.561Z" }, - { url = "https://files.pythonhosted.org/packages/d8/10/68a0b5549876d4b53ba4c46eed2a7aca32d589624ed60beef5bd7382619e/pyodbc-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c79df54bbc25bce9f2d87094e7b39089c28428df5443d1902b0cc5f43fd2da6f", size = 1361420, upload-time = "2025-10-17T18:03:35.958Z" }, - { url = "https://files.pythonhosted.org/packages/41/0f/9dfe4987283ffcb981c49a002f0339d669215eb4a3fe4ee4e14537c52852/pyodbc-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c2eb0b08e24fe5c40c7ebe9240c5d3bd2f18cd5617229acee4b0a0484dc226f2", size = 63399, upload-time = "2025-10-17T18:03:36.931Z" }, - { url = "https://files.pythonhosted.org/packages/56/03/15dcefe549d3888b649652af7cca36eda97c12b6196d92937ca6d11306e9/pyodbc-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:01166162149adf2b8a6dc21a212718f205cabbbdff4047dc0c415af3fd85867e", size = 70133, upload-time = "2025-10-17T18:03:38.47Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c1/c8b128ae59a14ecc8510e9b499208e342795aecc3af4c3874805c720b8db/pyodbc-5.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:363311bd40320b4a61454bebf7c38b243cd67c762ed0f8a5219de3ec90c96353", size = 64683, upload-time = "2025-10-17T18:03:39.68Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f2/c26d82a7ce1e90b8bbb8731d3d53de73814e2f6606b9db9d978303aa8d5f/pyodbc-5.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3f1bdb3ce6480a17afaaef4b5242b356d4997a872f39e96f015cabef00613797", size = 73513, upload-time = "2025-10-17T18:03:40.536Z" }, - { url = "https://files.pythonhosted.org/packages/82/d5/1ab1b7c4708cbd701990a8f7183c5bb5e0712d5e8479b919934e46dadab4/pyodbc-5.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7713c740a10f33df3cb08f49a023b7e1e25de0c7c99650876bbe717bc95ee780", size = 72631, upload-time = "2025-10-17T18:03:41.713Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/7e3831eeac2b09b31a77e6b3495491ce162035ff2903d7261b49d35aa3c2/pyodbc-5.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf18797a12e70474e1b7f5027deeeccea816372497e3ff2d46b15bec2d18a0cc", size = 344580, upload-time = "2025-10-17T18:03:42.67Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a6/71d26d626a3c45951620b7ff356ec920e420f0e09b0a924123682aa5e4ab/pyodbc-5.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:08b2439500e212625471d32f8fde418075a5ddec556e095e5a4ba56d61df2dc6", size = 350224, upload-time = "2025-10-17T18:03:43.731Z" }, - { url = "https://files.pythonhosted.org/packages/93/14/f702c5e8c2d595776266934498505f11b7f1545baf21ffec1d32c258e9d3/pyodbc-5.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:729c535341bb09c476f219d6f7ab194bcb683c4a0a368010f1cb821a35136f05", size = 1301503, upload-time = "2025-10-17T18:03:45.013Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b2/ad92ebdd1b5c7fec36b065e586d1d34b57881e17ba5beec5c705f1031058/pyodbc-5.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c67e7f2ce649155ea89beb54d3b42d83770488f025cf3b6f39ca82e9c598a02e", size = 1361050, upload-time = "2025-10-17T18:03:46.298Z" }, - { url = "https://files.pythonhosted.org/packages/19/40/dc84e232da07056cb5aaaf5f759ba4c874bc12f37569f7f1670fc71e7ae1/pyodbc-5.3.0-cp314-cp314-win32.whl", hash = "sha256:a48d731432abaee5256ed6a19a3e1528b8881f9cb25cb9cf72d8318146ea991b", size = 65670, upload-time = "2025-10-17T18:03:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/b8/79/c48be07e8634f764662d7a279ac204f93d64172162dbf90f215e2398b0bd/pyodbc-5.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:58635a1cc859d5af3f878c85910e5d7228fe5c406d4571bffcdd281375a54b39", size = 72177, upload-time = "2025-10-17T18:03:57.296Z" }, - { url = "https://files.pythonhosted.org/packages/fc/79/e304574446b2263f428ce14df590ba52c2e0e0205e8d34b235b582b7d57e/pyodbc-5.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:754d052030d00c3ac38da09ceb9f3e240e8dd1c11da8906f482d5419c65b9ef5", size = 66668, upload-time = "2025-10-17T18:03:58.174Z" }, - { url = "https://files.pythonhosted.org/packages/43/17/f4eabf443b838a2728773554017d08eee3aca353102934a7e3ba96fb0e31/pyodbc-5.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f927b440c38ade1668f0da64047ffd20ec34e32d817f9a60d07553301324b364", size = 75780, upload-time = "2025-10-17T18:03:47.273Z" }, - { url = "https://files.pythonhosted.org/packages/59/ea/e79e168c3d38c27d59d5d96273fd9e3c3ba55937cc944c4e60618f51de90/pyodbc-5.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:25c4cfb2c08e77bc6e82f666d7acd52f0e52a0401b1876e60f03c73c3b8aedc0", size = 75503, upload-time = "2025-10-17T18:03:48.171Z" }, - { url = "https://files.pythonhosted.org/packages/90/81/d1d7c125ec4a20e83fdc28e119b8321192b2bd694f432cf63e1199b2b929/pyodbc-5.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc834567c2990584b9726cba365834d039380c9dbbcef3030ddeb00c6541b943", size = 398356, upload-time = "2025-10-17T18:03:49.131Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fc/f6be4b3cc3910f8c2aba37aa41671121fd6f37b402ae0fefe53a70ac7cd5/pyodbc-5.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8339d3094858893c1a68ee1af93efc4dff18b8b65de54d99104b99af6306320d", size = 397291, upload-time = "2025-10-17T18:03:50.18Z" }, - { url = "https://files.pythonhosted.org/packages/03/2e/0610b1ed05a5625528d52f6cece9610e84617d35f475c89c2a52f66d13f7/pyodbc-5.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74528fe148980d0c735c0ebb4a4dc74643ac4574337c43c1006ac4d09593f92d", size = 1353900, upload-time = "2025-10-17T18:03:51.339Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f1/43497e1d37f9f71b43b2b3172e7b1bdf50851e278390c3fb6b46a3630c53/pyodbc-5.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d89a7f2e24227150c13be8164774b7e1f9678321a4248f1356a465b9cc17d31e", size = 1406062, upload-time = "2025-10-17T18:03:52.546Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/88a1277c2f7d9ab1cec0a71e074ba24fd4a1710a43974682546da90a1343/pyodbc-5.3.0-cp314-cp314t-win32.whl", hash = "sha256:af4d8c9842fc4a6360c31c35508d6594d5a3b39922f61b282c2b4c9d9da99514", size = 70132, upload-time = "2025-10-17T18:03:53.715Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c7/ee98c62050de4aa8bafb6eb1e11b95e0b0c898bd5930137c6dc776e06a9b/pyodbc-5.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bfeb3e34795d53b7d37e66dd54891d4f9c13a3889a8f5fe9640e56a82d770955", size = 79452, upload-time = "2025-10-17T18:03:54.664Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8f/d8889efd96bbe8e5d43ff9701f6b1565a8e09c3e1f58c388d550724f777b/pyodbc-5.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:13656184faa3f2d5c6f19b701b8f247342ed581484f58bf39af7315c054e69db", size = 70142, upload-time = "2025-10-17T18:03:55.551Z" }, -] - [[package]] name = "pyrefly" version = "1.3.1" @@ -5390,7 +5101,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } wheels = [ @@ -5423,9 +5134,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, - { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, - { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] @@ -5634,38 +5342,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/63/b19305c4b8d3d7867699f7d83c43550273d91a2f31793452d87edb1d259f/regex-2026.9.10-cp314-cp314t-win32.whl", hash = "sha256:fbc4e2f3cb7ce8436154e6483079e7d35eeb321a952fa936e180300630d8b873", size = 274607, upload-time = "2026-09-09T20:59:00.942Z" }, { url = "https://files.pythonhosted.org/packages/32/b8/1695072a512a49060294024e23b945eeb87675b02c06e53a92a1b42b0bfd/regex-2026.9.10-cp314-cp314t-win_amd64.whl", hash = "sha256:c37fa93bf18bf4f90b01c0fa9f11ea567ee4b7dd8bf96e63663e5edc37aa38cf", size = 283944, upload-time = "2026-09-09T20:59:02.876Z" }, { url = "https://files.pythonhosted.org/packages/a9/9f/65bac17f39991a67e22f8b3c849fdc02a56147c97029b5351494341959b4/regex-2026.9.10-cp314-cp314t-win_arm64.whl", hash = "sha256:ffc2da104e43db716ce30cef9f28049a1faa6aca385dd8771b033268d0730b07", size = 283780, upload-time = "2026-09-09T20:59:05.009Z" }, - { url = "https://files.pythonhosted.org/packages/67/ca/1d1f83bc2f8fff4f186266ac82d73254e686565530cec9ab5228fb5c63dc/regex-2026.9.10-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6afcad14310f1311d077553ed374b42a5e538f85a8c884b4e38e52de091c8077", size = 496869, upload-time = "2026-09-09T20:59:07.051Z" }, - { url = "https://files.pythonhosted.org/packages/33/42/4217510286501a2ebcd372b781b4754ac961e043fa13ef8dce803c44d89c/regex-2026.9.10-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:3fb4ae8cf83ef4e9addd43b2da31a9f45be816a8036fae8af59c8998b72718e2", size = 297121, upload-time = "2026-09-09T20:59:09.007Z" }, - { url = "https://files.pythonhosted.org/packages/55/a7/595468ed0bbccd94be92c6b5d67736ba128b204d942429a8485c2693914d/regex-2026.9.10-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f7d4656e17ab736e9415a6442a345bfc97bb8b7dcce47884bb74a37f70f08d0c", size = 292139, upload-time = "2026-09-09T20:59:10.859Z" }, - { url = "https://files.pythonhosted.org/packages/29/1c/ac92c123e0ab9bea75a904272171b356940bd6139e4a35de44f2254dca8f/regex-2026.9.10-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ba3bab0c45079735f55ac61526774de1d84bc4a0333cc554e1a4ab74913924", size = 802375, upload-time = "2026-09-09T20:59:12.823Z" }, - { url = "https://files.pythonhosted.org/packages/8d/16/d349f6fa9f908162359004e4f067353a0146e8074d24989490778244be44/regex-2026.9.10-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff6b3267318661dfddf6b3628663e00e5946bd0a5c8fa678537a1401f0388f91", size = 872328, upload-time = "2026-09-09T20:59:15.352Z" }, - { url = "https://files.pythonhosted.org/packages/df/81/251b5aef23147057e926346fdb7c8c352d0568f65a492e4e9eb6120f6446/regex-2026.9.10-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030fa9e23624e39b3b94e46b90a5abd1a1678eb2f58fcdd3fd6c27526bf91c7e", size = 919594, upload-time = "2026-09-09T20:59:17.31Z" }, - { url = "https://files.pythonhosted.org/packages/d0/6e/1f25319dc1b9cf4b7ffa303f3d16ca53260fe92aff45e81aa1b3c6c7cba2/regex-2026.9.10-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fbc8314436353e097c050e11b01a6c11433579437ed0579730157676ef59e2f", size = 807394, upload-time = "2026-09-09T20:59:19.328Z" }, - { url = "https://files.pythonhosted.org/packages/cc/fc/3da6b3dffddf12d5e96cbbe6f5e65ebcd64f92e3388a6690a53e0878232d/regex-2026.9.10-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e6c0b5ec6ddee4032247585dc491b0fa58627745b66a705728703a3f0331231", size = 786018, upload-time = "2026-09-09T20:59:21.592Z" }, - { url = "https://files.pythonhosted.org/packages/bb/6f/1cc86dafddc912ef44c5ccd4f729060be8c6735600932664eb6d0e25469b/regex-2026.9.10-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bf29611e5376fec8f795879bb5c6153a76c3a292573d173c26784042b01eb840", size = 793504, upload-time = "2026-09-09T20:59:23.734Z" }, - { url = "https://files.pythonhosted.org/packages/b1/cb/11692e29388d006211163627fcefa0c79917ab9f94d46a1b2254fe5efc81/regex-2026.9.10-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8855f08c17895a26fbf5f19ed829722e19b34a96629e49a43c92974924026b", size = 866766, upload-time = "2026-09-09T20:59:25.798Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/3631df969830f94d83fcfc5fc71a7b39caad905e18f7b17350a94135d625/regex-2026.9.10-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94c5ce3bc41d226b4eb89ca3f842b2e28c031487fb1f34eb2153d98235831325", size = 775825, upload-time = "2026-09-09T20:59:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8a/762892a8e3e21d119eacaffde848aa247e6cf4e1d90c46011671e86b1b9e/regex-2026.9.10-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:9ce239acb15843ab03976626af810a4424b0409689ec2bbc52088ab5479ab487", size = 858901, upload-time = "2026-09-09T20:59:30.656Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f0/78048fada5c2f61d795efb26ea24f820a5564c4aa26574920284d45cd656/regex-2026.9.10-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c22df8dd6373bbe3898e77429ffc85594300e39d752fd0e68a31e59d37899376", size = 796208, upload-time = "2026-09-09T20:59:32.852Z" }, - { url = "https://files.pythonhosted.org/packages/aa/58/632681f7b9aaa3d83b40e5862ba46364a453c8eb2bc7d43fece3dc31f972/regex-2026.9.10-cp315-cp315-win32.whl", hash = "sha256:1aa309ab7ba89a62d6cf70dbd38d4176440bce3c7001ab86256704cf4c18c6eb", size = 272704, upload-time = "2026-09-09T20:59:34.927Z" }, - { url = "https://files.pythonhosted.org/packages/5d/64/81cce28754c37037b1fe740b6d7a556d51d97cf935ea4547bfab104f43f7/regex-2026.9.10-cp315-cp315-win_amd64.whl", hash = "sha256:58da726d3e766c0b3f5a3997dfaf0275898a1107b8191cdd6b0437fe45fd817d", size = 281182, upload-time = "2026-09-09T20:59:36.86Z" }, - { url = "https://files.pythonhosted.org/packages/33/28/5a13a340c9c759e863a0e7f765d323601d03d6538c8a627ed628662e083b/regex-2026.9.10-cp315-cp315-win_arm64.whl", hash = "sha256:75f9297b16fcb588a1f8d8a55dabef3c0c20b0c7bac43c87ceaaaf1a825c12f4", size = 281512, upload-time = "2026-09-09T20:59:38.875Z" }, - { url = "https://files.pythonhosted.org/packages/f7/38/a3caebcd5105be90708071db20bd261b0961b8ea4fe5e8be45c2632519b5/regex-2026.9.10-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1270cdec69248592bbe38a0b263ed58d907b891bd2b93703e225c317e421bda1", size = 501336, upload-time = "2026-09-09T20:59:40.987Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b5/ec8887b2658bf0a5df143c7c1fcd562b0abd2fa208c04ebf15c6607c9bb2/regex-2026.9.10-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:681ed38664b64c6617d3c3c332018d1948c77e139c5ea667c1886efa671e426f", size = 299318, upload-time = "2026-09-09T20:59:43.039Z" }, - { url = "https://files.pythonhosted.org/packages/d5/58/84724a9eccf6e8cd46f7e4534576093e2476a5eeb55e2453aa6606e86059/regex-2026.9.10-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:8e127d9a80cbf1c3276bb465c6d047e8705e97b58c2b8f2f0c0a69c336b44b37", size = 294847, upload-time = "2026-09-09T20:59:44.954Z" }, - { url = "https://files.pythonhosted.org/packages/50/03/70ccc5e53905984abf8eab63eebd3ce740522ef8c8d392622b64d79ef290/regex-2026.9.10-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:990797e765d89a423880052c68b61c31afe701de94a8c060f61c40605ca6c727", size = 814359, upload-time = "2026-09-09T20:59:47.194Z" }, - { url = "https://files.pythonhosted.org/packages/29/53/40f7a11ec547e4a947883c9d5e8a075f6d4f59af2b8dbcaa8bf5b504aca0/regex-2026.9.10-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e5e4a6e0734a685d13b9685622bb503bdbb2927f8b0df025a5085f0ea067475b", size = 875586, upload-time = "2026-09-09T20:59:49.537Z" }, - { url = "https://files.pythonhosted.org/packages/0b/9d/83f3e022d99ce601727c4ef5f7b527753901ce4509ed89a1bb6a2263380a/regex-2026.9.10-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63bb62cf62217dc38c8a6b2b61b165b0e4eb8fa93b0aba12139251c0986a8fa3", size = 920990, upload-time = "2026-09-09T20:59:51.776Z" }, - { url = "https://files.pythonhosted.org/packages/b2/64/dfdb367d8f4f5c9b8ccb4b59789c2ce996f2c69c3b8192aa986fe7d92ec4/regex-2026.9.10-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb76a9c4e07a6a47849726af0ed14c41741a182f097f134a8cf29c1bc0f4dde8", size = 818660, upload-time = "2026-09-09T20:59:54.428Z" }, - { url = "https://files.pythonhosted.org/packages/88/a6/fb7d0b64487913845834f319aa84f8377d55305958a5db7e19c128798366/regex-2026.9.10-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31e4df2b11d48f61d511019bc1ee9b477055f17c352b68fe72db7a98b14d603c", size = 794976, upload-time = "2026-09-09T20:59:56.799Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1c/23484edaae387ea0d31f4d414463211e3516c8aa210ce8d0640f5aff3502/regex-2026.9.10-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:cf377960d2ac37d987394a9dbaa75e91338c41a46d41e1d25e90125e7b3ee2dc", size = 804081, upload-time = "2026-09-09T20:59:59.39Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e1/e30d138f13aecec99ea9aecef7e31563de6ec6a2f5ce1d65fc506aee33fe/regex-2026.9.10-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c8fbd9cb30c68c1686b94029b9ef845d5870d3d65baf66cb126b676849b9d72b", size = 870430, upload-time = "2026-09-09T21:00:03.304Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/81f9ce86f7ae4f57901543597c95751fa01c41a672ec1636dd913f8a000a/regex-2026.9.10-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:53e182b6b04d0011909b47d51a2d72d908de07c7b1c7f16b3adda2204d723bc1", size = 783327, upload-time = "2026-09-09T21:00:05.68Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1b/d7bf8f91534740f6a8ca17e5ac9c5337903baf527c8de240fbac1bbedfd0/regex-2026.9.10-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:0aa7589394230e0f0a422ab6b90841ff12c87e855e7aaf75d192a54a5f124548", size = 860874, upload-time = "2026-09-09T21:00:08.41Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1d/52cc88364aca7c9013dfe9abe0fea67f8d394efe384d07798300a6e2f27d/regex-2026.9.10-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1b891f77554bff991804cee24b78b40789f7d5993a24c7907bc7025fd2a70c8d", size = 806705, upload-time = "2026-09-09T21:00:10.768Z" }, - { url = "https://files.pythonhosted.org/packages/cb/51/37a194df7707f92f33173260ba7b221d4ec376419a53babaec50019a2804/regex-2026.9.10-cp315-cp315t-win32.whl", hash = "sha256:5bef622850cf760154719d4e0d74b0a855962432995168e250069899ae12fe8f", size = 274780, upload-time = "2026-09-09T21:00:14.003Z" }, - { url = "https://files.pythonhosted.org/packages/77/12/3227a52970d90908b230f15b2c86df903f49ac72c9eedf4f6e8b5bb5e1a7/regex-2026.9.10-cp315-cp315t-win_amd64.whl", hash = "sha256:07b45ba5c94b8fcb30cb6c56a11f715c57533a3017964504322ea52690a27b72", size = 283936, upload-time = "2026-09-09T21:00:16.275Z" }, - { url = "https://files.pythonhosted.org/packages/f3/bc/c567c5a61671f04d30e83f20b496b465432879196574d051007285576205/regex-2026.9.10-cp315-cp315t-win_arm64.whl", hash = "sha256:f70b9f0e39c2dba1d9da6bf7ef7c377cad7277f8440e9a69be05ede529ff024c", size = 283747, upload-time = "2026-09-09T21:00:19.113Z" }, ] [[package]] @@ -5789,35 +5465,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, - { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, - { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, - { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, - { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, - { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, - { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, - { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, - { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, - { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, - { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, - { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, - { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, - { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, - { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, - { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, - { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, - { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, @@ -6015,20 +5662,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/ce/af8964c38bc8226dd8950305b7a255fa33345d5572f78af7275a313d28e0/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a", size = 1250932, upload-time = "2026-08-17T19:49:19.28Z" }, { url = "https://files.pythonhosted.org/packages/1d/4b/323631116fc986d9cc5bbeb2b8223c7c85e61a8bb94ea5ab4951023b149b/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58", size = 1315190, upload-time = "2026-08-17T19:49:20.467Z" }, { url = "https://files.pythonhosted.org/packages/18/8b/ba48a73729c9270989b36f37ab2ed5525e52690d715097c9fa791aaa5d05/tiktoken-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0", size = 987717, upload-time = "2026-08-17T19:49:21.704Z" }, - { url = "https://files.pythonhosted.org/packages/1d/10/b73b7e319179e0f60b32475f783b044f9cece872c53b6662664e9084b0d0/tiktoken-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232", size = 1096280, upload-time = "2026-08-17T19:49:22.779Z" }, - { url = "https://files.pythonhosted.org/packages/c2/6b/09999a9bf1d559670d1680e8f8e419ac0e2c5f6aac82e9bfdf70f260b30a/tiktoken-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695", size = 1040433, upload-time = "2026-08-17T19:49:23.998Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7b/8537be0836f3df99b2a636b44399bfa43cd757f2b8b4097dacb794cf24a7/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49", size = 1186989, upload-time = "2026-08-17T19:49:25.021Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9d/f9c56d7a943a4468abf9ef37661bb9b8e0cd3aa8aa87368c7146cc3f3222/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4", size = 1204615, upload-time = "2026-08-17T19:49:26.37Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/98a38579db25c4a8a84e31dd95d9072ec5f21f7e70de591da0412e29b25b/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871", size = 1251828, upload-time = "2026-08-17T19:49:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/0c/83/467be424746c039c5493c0f4102feab16b9b48eb6f5c089b2a2438e3cde2/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f", size = 1316260, upload-time = "2026-08-17T19:49:29.101Z" }, - { url = "https://files.pythonhosted.org/packages/02/ee/ddf46ca78e371f5890e96b6e7d089a85b3536432be219851eb0481786ca8/tiktoken-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea", size = 988230, upload-time = "2026-08-17T19:49:30.246Z" }, - { url = "https://files.pythonhosted.org/packages/2a/00/5162e90c851a28da18ed382d34898b79a8022548e5619a64e14c03ce7c3d/tiktoken-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890", size = 1096186, upload-time = "2026-08-17T19:49:31.656Z" }, - { url = "https://files.pythonhosted.org/packages/65/97/a5a7bfccf25b1bb65e82bae8edff11ac3c9c041c374b7b4a823d60c38133/tiktoken-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5", size = 1039947, upload-time = "2026-08-17T19:49:32.848Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ba/ef427fc638f1439181c5e12dd26b70e881861f89c007aa7e5b36300f8342/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae", size = 1186997, upload-time = "2026-08-17T19:49:34.121Z" }, - { url = "https://files.pythonhosted.org/packages/3e/88/2f3f85a968cdc514152129af0a060ebcccb067005a2f29b0d5ef3c838514/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1", size = 1205211, upload-time = "2026-08-17T19:49:35.284Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f6/80760e98a08e6649d2d68afb6035af713121dfb615acce8c4f73810ec438/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89", size = 1251479, upload-time = "2026-08-17T19:49:36.419Z" }, - { url = "https://files.pythonhosted.org/packages/c5/84/50966fb6918a0fb9b32721277e5342bf729a2d74350074d662fbedf9772e/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3", size = 1316673, upload-time = "2026-08-17T19:49:37.756Z" }, - { url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" }, ] [[package]] @@ -6389,17 +6022,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, - { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, @@ -6684,40 +6306,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, - { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, - { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, - { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, - { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, - { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, - { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, - { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, - { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, - { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, - { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, - { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, - { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, - { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, - { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, - { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, - { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, - { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, - { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, - { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, - { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, - { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, - { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, ] From 75382b4cb00a845786636fb9157a798b620d3c0a Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 23 Sep 2026 13:51:20 +0200 Subject: [PATCH 3/9] refactor(python): keep SQL Server collection constructors owned-only --- .../_vector_store.py | 38 ++++++++----------- .../tests/sql_server/test_vector_store.py | 1 + 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py index 2667defcf7e..25e0cb46915 100644 --- a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py +++ b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py @@ -187,7 +187,6 @@ def __init__( embedding_generator: EmbeddingClient | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, - _shared_client: _Client | None = None, ) -> None: """Initialize a collection without connecting to the database. @@ -201,14 +200,13 @@ def __init__( embedding_generator: Default local embedding generator. env_file_path: Optional .env file. env_file_encoding: Encoding of the selected .env file. - _shared_client: Internal shared client owned by a ``SqlServerStore``. """ super().__init__( record_type, definition=definition, collection_name=collection_name, embedding_generator=embedding_generator, - managed_client=_shared_client is None, + managed_client=True, ) self.schema = schema self._table = f"{_quote_identifier(schema)}.{_quote_identifier(self.collection_name)}" @@ -228,23 +226,12 @@ def __init__( raise NotImplementedError("SQL Server approximate vector indexes are not supported.") elif field.is_indexed and field.field_type == "data" and field.type_ in ("bytes", "list", "dict"): raise NotImplementedError(f"SQL Server cannot index data fields of type '{field.type_}'.") - if _shared_client is not None: - if ( - connection_string is not None - or query_timeout is not None - or env_file_path is not None - or env_file_encoding is not None - ): - raise ValueError("A store's shared client cannot be combined with other connection settings.") - self._client = _shared_client - else: - self._client = _create_client( - connection_string, - query_timeout=query_timeout, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - ) - self._shared_client = _shared_client is not None + self._client = _create_client( + connection_string, + query_timeout=query_timeout, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) async def __aenter__(self) -> Self: """Enter the collection context.""" @@ -256,7 +243,7 @@ async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None async def close(self) -> None: """Close a collection-owned worker, never its store's worker.""" - if not self._shared_client: + if self.managed_client: await self._client.close() def _column_names(self, include_vectors: bool) -> list[str]: @@ -639,14 +626,19 @@ def get_collection( embedding_generator: EmbeddingClient | None = None, ) -> SqlServerCollection[Any, ModelT]: """Create a collection sharing this store's owned worker and lifecycle.""" - return SqlServerCollection( + collection = SqlServerCollection( record_type, + connection_string=self._client.connection_string, + query_timeout=self._client.query_timeout, schema=self.schema, definition=definition, collection_name=collection_name, embedding_generator=embedding_generator if embedding_generator is not None else self.embedding_generator, - _shared_client=self._client, ) + # Collection construction does no I/O; reuse the store's lazy worker. + collection._client = self._client # pyright: ignore[reportPrivateUsage] + collection.managed_client = False + return collection async def list_collection_names(self, *, operation_options: Mapping[str, Any] | None = None) -> Sequence[str]: """List base tables in the configured schema.""" diff --git a/python/packages/sql-server/tests/sql_server/test_vector_store.py b/python/packages/sql-server/tests/sql_server/test_vector_store.py index af18c1bf80a..1af1c76aa8d 100644 --- a/python/packages/sql-server/tests/sql_server/test_vector_store.py +++ b/python/packages/sql-server/tests/sql_server/test_vector_store.py @@ -408,6 +408,7 @@ def test_public_api_has_no_borrowed_connection_or_factory(constructor): parameters = inspect.signature(api).parameters assert "client" not in parameters assert "client_factory" not in parameters + assert "_shared_client" not in parameters assert "SqlServerClient" not in module.__dict__ with pytest.raises(TypeError, match="client"): constructor(connection_string="Server=unused", client=connection) From 4653e02961135a79abec5ad7d7e8672cdd3e0a40 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 23 Sep 2026 15:16:25 +0200 Subject: [PATCH 4/9] docs(python): show Entra SQL Server sample configuration --- python/packages/sql-server/README.md | 23 ++++++++++++++++-- .../sql-server/samples/sql_server_vectors.py | 24 +++++++++++++++---- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/python/packages/sql-server/README.md b/python/packages/sql-server/README.md index 7932b5aae91..5dd79f46ee3 100644 --- a/python/packages/sql-server/README.md +++ b/python/packages/sql-server/README.md @@ -37,8 +37,27 @@ settings. `ensure_collection_deleted()` drops only that table. ## Connection settings and ownership -Set `SQL_SERVER_CONNECTION_STRING` to a driver connection string such as -`Server=tcp:,1433;Database=;Authentication=ActiveDirectoryDefault;Encrypt=yes`. +For local Azure SQL development with passwordless Microsoft Entra authentication, +sign in with Azure CLI, then set the environment variable used by the +[sample](samples/sql_server_vectors.py): + +```bash +az login +export SQL_SERVER_CONNECTION_STRING='Server=;Database=;Authentication=ActiveDirectoryDefault;Encrypt=yes;' +``` + +Replace `` with the Azure SQL server hostname (for example, +`my-server.database.windows.net`) and `` with your existing database. +`ActiveDirectoryDefault` uses the driver's credential chain, which can use +your Azure CLI sign-in. The identity must be granted access to the database +and permission to create a table and index in the configured schema and +read/write its records. On an Azure-hosted app, use +`Authentication=ActiveDirectoryMSI` for managed identity; add +`UID=` for a user-assigned identity. See +[Microsoft's Entra authentication guide](https://learn.microsoft.com/sql/connect/python/mssql-python/entra-authentication) +for setup, permissions, and other supported modes. This connector accepts +connection strings, not the driver's `token_provider=` credential argument. + Do not commit connection strings containing credentials. Alternatively, pass `connection_string` as a string or Agent Framework `SecretString` to `SqlServerStore` or `SqlServerCollection`. Settings precedence is **explicit diff --git a/python/packages/sql-server/samples/sql_server_vectors.py b/python/packages/sql-server/samples/sql_server_vectors.py index 57d084e2b84..4318563c477 100644 --- a/python/packages/sql-server/samples/sql_server_vectors.py +++ b/python/packages/sql-server/samples/sql_server_vectors.py @@ -14,11 +14,25 @@ """ Search native VECTOR columns in SQL Server 2025 or Azure SQL. -Set SQL_SERVER_CONNECTION_STRING to a Microsoft mssql-python connection string -for an explicitly designated development database (Server=...;Database=...). -No separately installed ODBC driver is needed. The example creates a unique -table in the existing dbo schema and drops only that table. No embedding service -is needed. +For passwordless Microsoft Entra authentication to an Azure SQL development +database, sign in with Azure CLI and set the connection string before running: + + az login + export SQL_SERVER_CONNECTION_STRING='Server=;Database=;Authentication=ActiveDirectoryDefault;Encrypt=yes;' + +Replace with the Azure SQL server hostname (for example, +my-server.database.windows.net) and with an existing vector-enabled +database. The signed-in identity needs permission to create a table and index +in the dbo schema and read/write its records. For an Azure-hosted app, use +Authentication=ActiveDirectoryMSI instead (and UID= for a +user-assigned managed identity). The connector reads SQL_SERVER_CONNECTION_STRING +through Agent Framework settings; it does not discover a .env file implicitly. +See https://learn.microsoft.com/sql/connect/python/mssql-python/entra-authentication +for other supported Entra modes and database-user setup. + +No separately installed ODBC driver is needed. This example creates a unique +table in the existing dbo schema and drops only that table. No embedding +service is needed. Run: uv run --package agent-framework-sql-server python packages/sql-server/samples/sql_server_vectors.py """ From d05b6ca0177fdf4fae0522cf5247ef131bc68369 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 23 Sep 2026 15:19:15 +0200 Subject: [PATCH 5/9] docs(python): clarify SQL Server env file selection --- python/packages/sql-server/README.md | 7 ++++--- python/packages/sql-server/samples/sql_server_vectors.py | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/python/packages/sql-server/README.md b/python/packages/sql-server/README.md index 5dd79f46ee3..9f3d3fef15b 100644 --- a/python/packages/sql-server/README.md +++ b/python/packages/sql-server/README.md @@ -61,9 +61,10 @@ connection strings, not the driver's `token_provider=` credential argument. Do not commit connection strings containing credentials. Alternatively, pass `connection_string` as a string or Agent Framework `SecretString` to `SqlServerStore` or `SqlServerCollection`. Settings precedence is **explicit -argument > selected `.env` file > process environment**. Choose a file with -`env_file_path` and optional `env_file_encoding`; no `.env` file is discovered -implicitly, and missing/empty strings are rejected. +argument > selected `.env` file > process environment**. To read a `.env` file +in the run directory, pass `env_file_path=".env"` to `SqlServerStore` or +`SqlServerCollection`; `env_file_encoding` is optional. Missing or empty +connection strings are rejected. The connector owns all connections. Each whole operation opens, uses, commits or rolls back, and closes a `mssql-python` connection on a dedicated worker diff --git a/python/packages/sql-server/samples/sql_server_vectors.py b/python/packages/sql-server/samples/sql_server_vectors.py index 4318563c477..d376f179d19 100644 --- a/python/packages/sql-server/samples/sql_server_vectors.py +++ b/python/packages/sql-server/samples/sql_server_vectors.py @@ -25,8 +25,10 @@ database. The signed-in identity needs permission to create a table and index in the dbo schema and read/write its records. For an Azure-hosted app, use Authentication=ActiveDirectoryMSI instead (and UID= for a -user-assigned managed identity). The connector reads SQL_SERVER_CONNECTION_STRING -through Agent Framework settings; it does not discover a .env file implicitly. +user-assigned managed identity). Here the connector reads the exported +SQL_SERVER_CONNECTION_STRING through Agent Framework settings. To read it from +a .env file in the run directory instead, pass env_file_path=".env" to +SqlServerStore. See https://learn.microsoft.com/sql/connect/python/mssql-python/entra-authentication for other supported Entra modes and database-user setup. From 2512d7723f668eb62a98b975d675b91f59cfe513 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 23 Sep 2026 15:23:26 +0200 Subject: [PATCH 6/9] fix(python): load SQL Server settings from run-directory dotenv --- python/packages/sql-server/README.md | 10 ++++--- .../_vector_store.py | 10 ++++--- .../sql-server/samples/sql_server_vectors.py | 6 ++--- .../tests/sql_server/test_vector_store.py | 27 +++++++++++++++++-- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/python/packages/sql-server/README.md b/python/packages/sql-server/README.md index 9f3d3fef15b..768c27f85d8 100644 --- a/python/packages/sql-server/README.md +++ b/python/packages/sql-server/README.md @@ -61,10 +61,12 @@ connection strings, not the driver's `token_provider=` credential argument. Do not commit connection strings containing credentials. Alternatively, pass `connection_string` as a string or Agent Framework `SecretString` to `SqlServerStore` or `SqlServerCollection`. Settings precedence is **explicit -argument > selected `.env` file > process environment**. To read a `.env` file -in the run directory, pass `env_file_path=".env"` to `SqlServerStore` or -`SqlServerCollection`; `env_file_encoding` is optional. Missing or empty -connection strings are rejected. +argument > `.env` file > process environment**. By default the connector +loads `.env` from the current run directory if it exists; put the same +`SQL_SERVER_CONNECTION_STRING=...` value there instead of exporting it to the +shell. Pass `env_file_path` only to select a different file (and optionally +`env_file_encoding`). An explicitly selected missing file or a missing/empty +connection string is rejected. The connector owns all connections. Each whole operation opens, uses, commits or rolls back, and closes a `mssql-python` connection on a dedicated worker diff --git a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py index 25e0cb46915..749b62e1846 100644 --- a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py +++ b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py @@ -10,6 +10,7 @@ from collections.abc import Callable, Mapping, Sequence from concurrent.futures import ThreadPoolExecutor from dataclasses import replace +from pathlib import Path from typing import Any, ClassVar, Generic, cast import mssql_python @@ -147,11 +148,14 @@ def _create_client( env_file_path: str | None, env_file_encoding: str | None, ) -> _Client: + selected_env_file = env_file_path + if selected_env_file is None and Path(".env").is_file(): + selected_env_file = ".env" settings = load_settings( SqlServerSettings, env_prefix="SQL_SERVER_", connection_string=connection_string, - env_file_path=env_file_path, + env_file_path=selected_env_file, env_file_encoding=env_file_encoding, ) resolved = settings.get("connection_string") @@ -198,7 +202,7 @@ def __init__( definition: Explicit field definition for dictionary records. collection_name: Table name overriding the model definition. embedding_generator: Default local embedding generator. - env_file_path: Optional .env file. + env_file_path: Optional settings file other than the run directory's default ``.env``. env_file_encoding: Encoding of the selected .env file. """ super().__init__( @@ -604,7 +608,7 @@ def __init__( query_timeout: Optional per-statement timeout in seconds; ``0`` disables the timeout. schema: Existing database schema used by all collections. embedding_generator: Default local embedding generator. - env_file_path: Optional .env file. + env_file_path: Optional settings file other than the run directory's default ``.env``. env_file_encoding: Encoding of the selected .env file. """ super().__init__(embedding_generator=embedding_generator, managed_client=True) diff --git a/python/packages/sql-server/samples/sql_server_vectors.py b/python/packages/sql-server/samples/sql_server_vectors.py index d376f179d19..455f072803c 100644 --- a/python/packages/sql-server/samples/sql_server_vectors.py +++ b/python/packages/sql-server/samples/sql_server_vectors.py @@ -26,9 +26,9 @@ in the dbo schema and read/write its records. For an Azure-hosted app, use Authentication=ActiveDirectoryMSI instead (and UID= for a user-assigned managed identity). Here the connector reads the exported -SQL_SERVER_CONNECTION_STRING through Agent Framework settings. To read it from -a .env file in the run directory instead, pass env_file_path=".env" to -SqlServerStore. +SQL_SERVER_CONNECTION_STRING through Agent Framework settings. You can instead +put that same variable in a .env file in the run directory; SqlServerStore() +loads that file by default. Pass env_file_path only if the file is elsewhere. See https://learn.microsoft.com/sql/connect/python/mssql-python/entra-authentication for other supported Entra modes and database-user setup. diff --git a/python/packages/sql-server/tests/sql_server/test_vector_store.py b/python/packages/sql-server/tests/sql_server/test_vector_store.py index 1af1c76aa8d..1bd65f3d3f6 100644 --- a/python/packages/sql-server/tests/sql_server/test_vector_store.py +++ b/python/packages/sql-server/tests/sql_server/test_vector_store.py @@ -380,6 +380,7 @@ def constructor(request, definition, monkeypatch): def test_settings_priority_and_secret_masking(constructor, monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) monkeypatch.setenv("SQL_SERVER_CONNECTION_STRING", "environment-secret") env_file = tmp_path / "settings.env" env_file.write_text("SQL_SERVER_CONNECTION_STRING='file-secret'\n", encoding="utf-8") @@ -391,11 +392,33 @@ def test_settings_priority_and_secret_masking(constructor, monkeypatch, tmp_path assert constructor()._client.connection_string.get_secret_value() == "environment-secret" -def test_settings_require_explicit_file_or_environment(constructor, monkeypatch, tmp_path): - (tmp_path / ".env").write_text("SQL_SERVER_CONNECTION_STRING=implicit\n", encoding="utf-8") +def test_default_dotenv_in_run_directory_and_explicit_override(constructor, monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + connection_string = ( + "Server=example.database.windows.net;Database=demo;Authentication=ActiveDirectoryDefault;Encrypt=yes;" + ) + (tmp_path / ".env").write_text(f"SQL_SERVER_CONNECTION_STRING='{connection_string}'\n", encoding="utf-8") + monkeypatch.setenv("SQL_SERVER_CONNECTION_STRING", "Server=environment") + assert constructor()._client.connection_string.get_secret_value() == connection_string + assert constructor(connection_string="Server=explicit")._client.connection_string.get_secret_value() == ( + "Server=explicit" + ) + alternate_file = tmp_path / "alternate.env" + alternate_file.write_text("SQL_SERVER_CONNECTION_STRING=Server=alternate\n", encoding="utf-16") + alternate = constructor(env_file_path=str(alternate_file), env_file_encoding="utf-16") + assert alternate._client.connection_string.get_secret_value() == "Server=alternate" + with pytest.raises(FileNotFoundError): + constructor(env_file_path=str(tmp_path / "missing.env")) + + +def test_missing_default_dotenv_falls_back_to_environment(constructor, monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) with pytest.raises(ValueError, match="required"): constructor() + monkeypatch.setenv("SQL_SERVER_CONNECTION_STRING", "Server=environment") + assert constructor()._client.connection_string.get_secret_value() == "Server=environment" + (tmp_path / ".env").write_text("UNRELATED_SETTING=unused\n", encoding="utf-8") + assert constructor()._client.connection_string.get_secret_value() == "Server=environment" with pytest.raises(ValueError, match="must not be empty"): constructor(connection_string="") with pytest.raises(FileNotFoundError): From 400242cae880ec2704dbbec19fc713de784431bc Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 23 Sep 2026 15:29:15 +0200 Subject: [PATCH 7/9] Revert "fix(python): load SQL Server settings from run-directory dotenv" This reverts commit 2512d7723f668eb62a98b975d675b91f59cfe513. --- python/packages/sql-server/README.md | 10 +++---- .../_vector_store.py | 10 +++---- .../sql-server/samples/sql_server_vectors.py | 6 ++--- .../tests/sql_server/test_vector_store.py | 27 ++----------------- 4 files changed, 12 insertions(+), 41 deletions(-) diff --git a/python/packages/sql-server/README.md b/python/packages/sql-server/README.md index 768c27f85d8..9f3d3fef15b 100644 --- a/python/packages/sql-server/README.md +++ b/python/packages/sql-server/README.md @@ -61,12 +61,10 @@ connection strings, not the driver's `token_provider=` credential argument. Do not commit connection strings containing credentials. Alternatively, pass `connection_string` as a string or Agent Framework `SecretString` to `SqlServerStore` or `SqlServerCollection`. Settings precedence is **explicit -argument > `.env` file > process environment**. By default the connector -loads `.env` from the current run directory if it exists; put the same -`SQL_SERVER_CONNECTION_STRING=...` value there instead of exporting it to the -shell. Pass `env_file_path` only to select a different file (and optionally -`env_file_encoding`). An explicitly selected missing file or a missing/empty -connection string is rejected. +argument > selected `.env` file > process environment**. To read a `.env` file +in the run directory, pass `env_file_path=".env"` to `SqlServerStore` or +`SqlServerCollection`; `env_file_encoding` is optional. Missing or empty +connection strings are rejected. The connector owns all connections. Each whole operation opens, uses, commits or rolls back, and closes a `mssql-python` connection on a dedicated worker diff --git a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py index 749b62e1846..25e0cb46915 100644 --- a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py +++ b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py @@ -10,7 +10,6 @@ from collections.abc import Callable, Mapping, Sequence from concurrent.futures import ThreadPoolExecutor from dataclasses import replace -from pathlib import Path from typing import Any, ClassVar, Generic, cast import mssql_python @@ -148,14 +147,11 @@ def _create_client( env_file_path: str | None, env_file_encoding: str | None, ) -> _Client: - selected_env_file = env_file_path - if selected_env_file is None and Path(".env").is_file(): - selected_env_file = ".env" settings = load_settings( SqlServerSettings, env_prefix="SQL_SERVER_", connection_string=connection_string, - env_file_path=selected_env_file, + env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) resolved = settings.get("connection_string") @@ -202,7 +198,7 @@ def __init__( definition: Explicit field definition for dictionary records. collection_name: Table name overriding the model definition. embedding_generator: Default local embedding generator. - env_file_path: Optional settings file other than the run directory's default ``.env``. + env_file_path: Optional .env file. env_file_encoding: Encoding of the selected .env file. """ super().__init__( @@ -608,7 +604,7 @@ def __init__( query_timeout: Optional per-statement timeout in seconds; ``0`` disables the timeout. schema: Existing database schema used by all collections. embedding_generator: Default local embedding generator. - env_file_path: Optional settings file other than the run directory's default ``.env``. + env_file_path: Optional .env file. env_file_encoding: Encoding of the selected .env file. """ super().__init__(embedding_generator=embedding_generator, managed_client=True) diff --git a/python/packages/sql-server/samples/sql_server_vectors.py b/python/packages/sql-server/samples/sql_server_vectors.py index 455f072803c..d376f179d19 100644 --- a/python/packages/sql-server/samples/sql_server_vectors.py +++ b/python/packages/sql-server/samples/sql_server_vectors.py @@ -26,9 +26,9 @@ in the dbo schema and read/write its records. For an Azure-hosted app, use Authentication=ActiveDirectoryMSI instead (and UID= for a user-assigned managed identity). Here the connector reads the exported -SQL_SERVER_CONNECTION_STRING through Agent Framework settings. You can instead -put that same variable in a .env file in the run directory; SqlServerStore() -loads that file by default. Pass env_file_path only if the file is elsewhere. +SQL_SERVER_CONNECTION_STRING through Agent Framework settings. To read it from +a .env file in the run directory instead, pass env_file_path=".env" to +SqlServerStore. See https://learn.microsoft.com/sql/connect/python/mssql-python/entra-authentication for other supported Entra modes and database-user setup. diff --git a/python/packages/sql-server/tests/sql_server/test_vector_store.py b/python/packages/sql-server/tests/sql_server/test_vector_store.py index 1bd65f3d3f6..1af1c76aa8d 100644 --- a/python/packages/sql-server/tests/sql_server/test_vector_store.py +++ b/python/packages/sql-server/tests/sql_server/test_vector_store.py @@ -380,7 +380,6 @@ def constructor(request, definition, monkeypatch): def test_settings_priority_and_secret_masking(constructor, monkeypatch, tmp_path): - monkeypatch.chdir(tmp_path) monkeypatch.setenv("SQL_SERVER_CONNECTION_STRING", "environment-secret") env_file = tmp_path / "settings.env" env_file.write_text("SQL_SERVER_CONNECTION_STRING='file-secret'\n", encoding="utf-8") @@ -392,33 +391,11 @@ def test_settings_priority_and_secret_masking(constructor, monkeypatch, tmp_path assert constructor()._client.connection_string.get_secret_value() == "environment-secret" -def test_default_dotenv_in_run_directory_and_explicit_override(constructor, monkeypatch, tmp_path): - monkeypatch.chdir(tmp_path) - connection_string = ( - "Server=example.database.windows.net;Database=demo;Authentication=ActiveDirectoryDefault;Encrypt=yes;" - ) - (tmp_path / ".env").write_text(f"SQL_SERVER_CONNECTION_STRING='{connection_string}'\n", encoding="utf-8") - monkeypatch.setenv("SQL_SERVER_CONNECTION_STRING", "Server=environment") - assert constructor()._client.connection_string.get_secret_value() == connection_string - assert constructor(connection_string="Server=explicit")._client.connection_string.get_secret_value() == ( - "Server=explicit" - ) - alternate_file = tmp_path / "alternate.env" - alternate_file.write_text("SQL_SERVER_CONNECTION_STRING=Server=alternate\n", encoding="utf-16") - alternate = constructor(env_file_path=str(alternate_file), env_file_encoding="utf-16") - assert alternate._client.connection_string.get_secret_value() == "Server=alternate" - with pytest.raises(FileNotFoundError): - constructor(env_file_path=str(tmp_path / "missing.env")) - - -def test_missing_default_dotenv_falls_back_to_environment(constructor, monkeypatch, tmp_path): +def test_settings_require_explicit_file_or_environment(constructor, monkeypatch, tmp_path): + (tmp_path / ".env").write_text("SQL_SERVER_CONNECTION_STRING=implicit\n", encoding="utf-8") monkeypatch.chdir(tmp_path) with pytest.raises(ValueError, match="required"): constructor() - monkeypatch.setenv("SQL_SERVER_CONNECTION_STRING", "Server=environment") - assert constructor()._client.connection_string.get_secret_value() == "Server=environment" - (tmp_path / ".env").write_text("UNRELATED_SETTING=unused\n", encoding="utf-8") - assert constructor()._client.connection_string.get_secret_value() == "Server=environment" with pytest.raises(ValueError, match="must not be empty"): constructor(connection_string="") with pytest.raises(FileNotFoundError): From e63ed0fcf835e165a8b6fd2d8b0000bf62bee4bb Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Thu, 24 Sep 2026 09:17:51 +0200 Subject: [PATCH 8/9] fix(python): allow large integer float filters --- .../agent_framework_sql_server/_sql.py | 8 ++++-- .../sql-server/tests/sql_server/test_sql.py | 26 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/python/packages/sql-server/agent_framework_sql_server/_sql.py b/python/packages/sql-server/agent_framework_sql_server/_sql.py index 3b787f7a735..ab9d62a4b32 100644 --- a/python/packages/sql-server/agent_framework_sql_server/_sql.py +++ b/python/packages/sql-server/agent_framework_sql_server/_sql.py @@ -234,6 +234,10 @@ def _numeric_filter_value(value: int | float) -> int | float: return value +def _prepare_numeric_filter_value(field: VectorStoreField, value: int | float) -> int | float: + return _numeric_filter_value(value) if field.type_ == "int" else _prepare_value(field, value) + + class _FilterCompiler: """Compile bounded data-only filter expressions into parameterized two-valued T-SQL.""" @@ -264,7 +268,7 @@ def _equality(self, field: VectorStoreField, column: str, value: Any) -> str: if kind != "bool" and isinstance(value, bool): return "1 = 0" if kind in ("int", "float") and type(value) in (int, float): - adapted = _numeric_filter_value(value) + adapted = _prepare_numeric_filter_value(field, value) elif ( (kind == "str" and isinstance(value, str)) or (kind == "bytes" and isinstance(value, bytes)) @@ -314,7 +318,7 @@ def _condition(self, expression: FilterExpression) -> str: if any(item is None or isinstance(item, bool) for item in values): raise TypeError("Ordered filter operands must be non-null scalars of the column's type.") adapted = [ - _numeric_filter_value(item) + _prepare_numeric_filter_value(field, item) if field.type_ in ("int", "float") and type(item) in (int, float) else _prepare_value(field, item) for item in values diff --git a/python/packages/sql-server/tests/sql_server/test_sql.py b/python/packages/sql-server/tests/sql_server/test_sql.py index 7cfb6476d8b..9bbb1037833 100644 --- a/python/packages/sql-server/tests/sql_server/test_sql.py +++ b/python/packages/sql-server/tests/sql_server/test_sql.py @@ -28,6 +28,7 @@ def definition() -> VectorStoreCollectionDefinition: VectorStoreField("key", name="id", type_="str", storage_name="record]id"), VectorStoreField("data", name="text", type_="str", storage_name="body [text]"), VectorStoreField("data", name="count", type_="int"), + VectorStoreField("data", name="ratio", type_="float"), VectorStoreField("data", name="flag", type_="bool"), VectorStoreField("data", name="tags", type_="list"), VectorStoreField("vector", name="embedding", type_="float32", dimensions=3), @@ -141,10 +142,29 @@ def test_equality_does_not_coerce_mismatched_types(definition, name, value): assert condition == "(NOT (1 = 0))" and params == [] +@pytest.mark.parametrize("operator", ["eq", "ne", "gt", "gte", "lt", "lte"]) +def test_float_filters_accept_integral_values_outside_bigint_range(definition, operator): + _, params = _FilterCompiler(definition).compile(Filter("ratio", operator, 10**20)) + assert params == [1e20] + + +@pytest.mark.parametrize("operator", ["in", "not_in", "between"]) +def test_float_multi_value_filters_accept_integral_values_outside_bigint_range(definition, operator): + _, params = _FilterCompiler(definition).compile(Filter("ratio", operator, [10**20, 10**21])) + assert params == [1e20, 1e21] + + +@pytest.mark.parametrize("operator", ["eq", "gt"]) +def test_integer_filters_reject_values_outside_bigint_range(definition, operator): + with pytest.raises(ValueError, match="bigint"): + _FilterCompiler(definition).compile(Filter("count", operator, 10**20)) + + +@pytest.mark.parametrize(("field", "error"), [("count", "range"), ("ratio", "finite")]) @pytest.mark.parametrize("operator", ["eq", "gt"]) -def test_out_of_range_numeric_filter_rejected(definition, operator): - with pytest.raises(ValueError, match="range"): - _FilterCompiler(definition).compile(Filter("count", operator, 10**1000)) +def test_out_of_range_numeric_filter_rejected(definition, field, error, operator): + with pytest.raises(ValueError, match=error): + _FilterCompiler(definition).compile(Filter(field, operator, 10**1000)) @pytest.mark.parametrize( From 540647397e51e110f039081d5a9681ee4961bdb9 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Thu, 24 Sep 2026 12:02:19 +0200 Subject: [PATCH 9/9] fix(python): distinguish committed SQL cleanup and preserve cancellation --- python/packages/sql-server/README.md | 9 +- .../agent_framework_sql_server/__init__.py | 3 +- .../_vector_store.py | 41 +++++++- .../tests/sql_server/test_vector_store.py | 99 ++++++++++++++++++- 4 files changed, 143 insertions(+), 9 deletions(-) diff --git a/python/packages/sql-server/README.md b/python/packages/sql-server/README.md index 9f3d3fef15b..62ec3e6e89b 100644 --- a/python/packages/sql-server/README.md +++ b/python/packages/sql-server/README.md @@ -2,8 +2,8 @@ Store typed Agent Framework records in SQL Server and Azure SQL native `VECTOR` columns, with exact, database-side similarity search. This alpha package exports -`SqlServerCollection`, `SqlServerStore`, and `SqlServerSettings` directly from -`agent_framework_sql_server`. +`SqlServerCollection`, `SqlServerStore`, `SqlServerSettings`, and +`SqlServerCommittedCleanupException` directly from `agent_framework_sql_server`. ## Install and provision @@ -84,6 +84,11 @@ have committed. Set `query_timeout=30` (seconds, for example) on the store or collection when bounding database calls; leaving it unset uses the driver's default, and `0` disables the timeout. Prefer stable application-provided keys when retrying writes. +If closing the connection fails after a successful commit, +`SqlServerCommittedCleanupException` explicitly signals that the transaction +**already committed**; do not automatically retry, especially with generated +keys. Cancellation remains `CancelledError` even if the worker fails while +finishing; that worker error is logged after cleanup. ## Example diff --git a/python/packages/sql-server/agent_framework_sql_server/__init__.py b/python/packages/sql-server/agent_framework_sql_server/__init__.py index 456e38131c4..d26a8de00ff 100644 --- a/python/packages/sql-server/agent_framework_sql_server/__init__.py +++ b/python/packages/sql-server/agent_framework_sql_server/__init__.py @@ -6,7 +6,7 @@ import importlib.metadata -from ._vector_store import SqlServerCollection, SqlServerSettings, SqlServerStore +from ._vector_store import SqlServerCollection, SqlServerCommittedCleanupException, SqlServerSettings, SqlServerStore try: __version__ = importlib.metadata.version(__name__) @@ -15,6 +15,7 @@ __all__ = [ "SqlServerCollection", + "SqlServerCommittedCleanupException", "SqlServerSettings", "SqlServerStore", "__version__", diff --git a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py index 25e0cb46915..66654a4bf1d 100644 --- a/python/packages/sql-server/agent_framework_sql_server/_vector_store.py +++ b/python/packages/sql-server/agent_framework_sql_server/_vector_store.py @@ -6,6 +6,7 @@ import asyncio import hashlib +import logging import math from collections.abc import Callable, Mapping, Sequence from concurrent.futures import ThreadPoolExecutor @@ -42,6 +43,11 @@ ModelT = TypeVar("ModelT", default=Any) ResultT = TypeVar("ResultT") _KEY_BATCH_SIZE = 1000 +logger = logging.getLogger(__name__) + + +class SqlServerCommittedCleanupException(IntegrationException): + """The SQL Server transaction committed, but closing its connection failed.""" class SqlServerSettings(TypedDict, total=False): @@ -99,22 +105,39 @@ def ensure_open(self) -> None: def _run_sync(self, operation: Callable[[mssql_python.Cursor], ResultT]) -> ResultT: try: connection = mssql_python.connect(self.connection_string.get_secret_value(), autocommit=False) + committed = False + failed = False try: - if self.query_timeout is not None: - connection.timeout = self.query_timeout - cursor = connection.cursor() try: + if self.query_timeout is not None: + connection.timeout = self.query_timeout + cursor = connection.cursor() try: result = operation(cursor) finally: cursor.close() connection.commit() + committed = True return result except BaseException: + failed = True connection.rollback() raise finally: - connection.close() + try: + connection.close() + except Exception as exc: + if committed: + raise SqlServerCommittedCleanupException( + "SQL Server transaction committed, but connection cleanup failed; " + "do not retry this operation automatically." + ) from exc + if failed: + logger.warning( + "SQL Server connection cleanup also failed after an operation error.", exc_info=exc + ) + else: + raise except mssql_python.Error as exc: raise IntegrationException("SQL Server operation failed; inspect the chained driver exception.") from exc @@ -127,7 +150,15 @@ async def run(self, operation: Callable[[mssql_python.Cursor], ResultT]) -> Resu try: return await asyncio.shield(future) except asyncio.CancelledError: - await asyncio.shield(future) + while not future.done(): + try: + await asyncio.shield(future) + except asyncio.CancelledError: + continue + except Exception: + break + if not future.cancelled() and (error := future.exception()) is not None: + logger.warning("SQL Server worker failed after caller cancellation.", exc_info=error) raise async def close(self) -> None: diff --git a/python/packages/sql-server/tests/sql_server/test_vector_store.py b/python/packages/sql-server/tests/sql_server/test_vector_store.py index 1af1c76aa8d..f1725d56f27 100644 --- a/python/packages/sql-server/tests/sql_server/test_vector_store.py +++ b/python/packages/sql-server/tests/sql_server/test_vector_store.py @@ -4,6 +4,7 @@ import asyncio import inspect +import logging import threading from dataclasses import dataclass from functools import partial @@ -26,7 +27,7 @@ ) from agent_framework.exceptions import IntegrationException, IntegrationInvalidResponseException -from agent_framework_sql_server import SqlServerCollection, SqlServerStore +from agent_framework_sql_server import SqlServerCollection, SqlServerCommittedCleanupException, SqlServerStore from agent_framework_sql_server import _vector_store as module from agent_framework_sql_server._vector_store import _Client @@ -535,6 +536,49 @@ def slow_execute(*args): assert client._executor is not None and client._executor._shutdown +async def test_cancelled_failing_operation_preserves_cancellation_until_worker_cleanup(caplog): + connection, cursor = fake_connection() + started, release = threading.Event(), threading.Event() + driver_error = mssql_python.OperationalError("query failed", "worker failure") + + def fail_after_cancellation(*args): + started.set() + assert release.wait(timeout=10) + raise driver_error + + cursor.execute.side_effect = fail_after_cancellation + client = _Client(SecretString("Server=unused;Database=test")) + with ( + patch.object(module.mssql_python, "connect", return_value=connection), + caplog.at_level(logging.WARNING, logger=module.__name__), + ): + try: + operation = asyncio.create_task(client.run(lambda raw: module._execute(raw, "SELECT 1"))) + assert await asyncio.to_thread(started.wait, 5) + operation.cancel() + await asyncio.sleep(0) + operation.cancel() + closing = asyncio.create_task(client.close()) + await asyncio.sleep(0) + assert not operation.done() and not closing.done() + finally: + release.set() + with pytest.raises(asyncio.CancelledError): + await operation + await closing + connection.commit.assert_not_called() + connection.rollback.assert_called_once() + cursor.close.assert_called_once() + connection.close.assert_called_once() + assert any( + "worker failed after caller cancellation" in record.message + and record.exc_info is not None + and isinstance(record.exc_info[1], IntegrationException) + and record.exc_info[1].__cause__ is driver_error + for record in caplog.records + ) + + async def test_driver_error_is_chained_and_owned_connection_rolled_back(): connection, cursor = fake_connection() cursor.execute.side_effect = [ @@ -581,6 +625,59 @@ async def test_commit_failure_rolls_back_and_closes_connection(): connection.close.assert_called_once() +async def test_committed_generated_key_is_distinguishable_from_failed_upsert(): + definition = VectorStoreCollectionDefinition( + [ + VectorStoreField("key", name="id", type_="int", is_auto_generated=True), + VectorStoreField("data", name="text", type_="str"), + ], + collection_name="generated", + ) + collection = SqlServerCollection(dict, definition=definition, connection_string="Server=unused;Database=test") + connection, cursor = fake_connection() + cursor.fetchone.return_value = (42,) + cleanup_error = mssql_python.OperationalError("close failed", "connection cleanup error") + connection.close.side_effect = cleanup_error + try: + with ( + patch.object(module.mssql_python, "connect", return_value=connection), + pytest.raises(SqlServerCommittedCleanupException) as error, + ): + await collection.upsert([{"text": "created"}], generate_vectors=False) + finally: + await collection.close() + assert "committed" in str(error.value) + assert "do not retry" in str(error.value) + assert error.value.__cause__ is cleanup_error + connection.commit.assert_called_once() + connection.rollback.assert_not_called() + cursor.close.assert_called_once() + connection.close.assert_called_once() + + +async def test_cleanup_failure_during_rollback_preserves_original_error(caplog): + connection, cursor = fake_connection() + operation_error = mssql_python.OperationalError("query failed", "operation error") + cursor.execute.side_effect = operation_error + connection.close.side_effect = mssql_python.OperationalError("close failed", "cleanup error") + client = _Client(SecretString("Server=unused;Database=test")) + try: + with ( + patch.object(module.mssql_python, "connect", return_value=connection), + caplog.at_level(logging.WARNING, logger=module.__name__), + pytest.raises(IntegrationException) as error, + ): + await client.run(lambda raw: module._execute(raw, "INSERT INTO [notes] ([id]) VALUES (?)", ["first"])) + finally: + await client.close() + assert not isinstance(error.value, SqlServerCommittedCleanupException) + assert error.value.__cause__ is operation_error + assert any("cleanup also failed" in record.message for record in caplog.records) + connection.commit.assert_not_called() + connection.rollback.assert_called_once() + connection.close.assert_called_once() + + async def test_connection_failure_is_chained_and_worker_shuts_down(): client = _Client(SecretString("Server=unused;Database=test")) try: